diff options
author | fragosti <francesco.agosti93@gmail.com> | 2018-10-25 03:56:11 +0800 |
---|---|---|
committer | fragosti <francesco.agosti93@gmail.com> | 2018-10-25 03:56:11 +0800 |
commit | 6da6540c038640abf75f703a294cb758c6defc47 (patch) | |
tree | bf320d263a86da9b0f75358305649f4d00b94265 | |
parent | f89b314a94f867fa905a1ce18eba9336ee4d1634 (diff) | |
parent | 06ba26a6d30565e7c6c4032528089d30ecc39fdd (diff) | |
download | dexon-0x-contracts-6da6540c038640abf75f703a294cb758c6defc47.tar dexon-0x-contracts-6da6540c038640abf75f703a294cb758c6defc47.tar.gz dexon-0x-contracts-6da6540c038640abf75f703a294cb758c6defc47.tar.bz2 dexon-0x-contracts-6da6540c038640abf75f703a294cb758c6defc47.tar.lz dexon-0x-contracts-6da6540c038640abf75f703a294cb758c6defc47.tar.xz dexon-0x-contracts-6da6540c038640abf75f703a294cb758c6defc47.tar.zst dexon-0x-contracts-6da6540c038640abf75f703a294cb758c6defc47.zip |
Merge branch 'development' of https://github.com/0xProject/0x-monorepo into feature/instant/input-fees-rounding
97 files changed, 1556 insertions, 533 deletions
diff --git a/CODEOWNERS b/CODEOWNERS index 278234aff..ba40af31a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -26,3 +26,4 @@ packages/subproviders/ @fabioberger @dekz packages/connect/ @fragosti packages/monorepo-scripts/ @fabioberger packages/order-utils/ @fabioberger @LogvinovLeon +python-packages/ @feuGeneA
\ No newline at end of file @@ -94,6 +94,10 @@ We strongly recommend that the community help us make improvements and determine Make sure you are using Yarn v1.9.4. To install using brew: +```bash +brew install yarn@1.9.4 +``` + Then install dependencies ```bash diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index 5ebe2aa24..7ebcd8c2f 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -25,6 +25,10 @@ { "note": "Add missing types to public interface", "pr": 1139 + }, + { + "note": "Throw `SignatureRequestDenied` and `TransactionValueTooLow` errors when executing buy", + "pr": 1147 } ], "timestamp": 1539871071 diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 2fe8f6deb..2f9a3108e 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -1,4 +1,4 @@ -import { ContractWrappers } from '@0x/contract-wrappers'; +import { ContractWrappers, ContractWrappersError, ForwarderWrapperError } from '@0x/contract-wrappers'; import { schemas } from '@0x/json-schemas'; import { SignedOrder } from '@0x/order-utils'; import { ObjectMap } from '@0x/types'; @@ -210,21 +210,32 @@ export class AssetBuyer { throw new Error(AssetBuyerError.NoAddressAvailable); } } - // if no ethAmount is provided, default to the worst ethAmount from buyQuote - const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( - orders, - assetBuyAmount, - finalTakerAddress, - ethAmount || worstCaseQuoteInfo.totalEthAmount, - feeOrders, - feePercentage, - feeRecipient, - { - gasLimit, - gasPrice, - }, - ); - return txHash; + try { + // if no ethAmount is provided, default to the worst ethAmount from buyQuote + const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( + orders, + assetBuyAmount, + finalTakerAddress, + ethAmount || worstCaseQuoteInfo.totalEthAmount, + feeOrders, + feePercentage, + feeRecipient, + { + gasLimit, + gasPrice, + shouldValidate: true, + }, + ); + return txHash; + } catch (err) { + if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) { + throw new Error(AssetBuyerError.SignatureRequestDenied); + } else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) { + throw new Error(AssetBuyerError.TransactionValueTooLow); + } else { + throw err; + } + } } /** * Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 0d8e732d7..f15e7e934 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -112,6 +112,8 @@ export enum AssetBuyerError { NoAddressAvailable = 'NO_ADDRESS_AVAILABLE', InvalidOrderProviderResponse = 'INVALID_ORDER_PROVIDER_RESPONSE', AssetUnavailable = 'ASSET_UNAVAILABLE', + SignatureRequestDenied = 'SIGNATURE_REQUEST_DENIED', + TransactionValueTooLow = 'TRANSACTION_VALUE_TOO_LOW', } export interface OrdersAndFillableAmounts { diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index f66be3f0a..c3d986b4a 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -37,6 +37,14 @@ "note": "Removed ContractNotFound errors. Checking for this error was somewhat ineffecient. Relevant methods/functions now return the default error from web3-wrapper, which we feel provides enough information.", "pr": 1105 + }, + { + "note": "Add `ForwarderWrapperError` to public interface", + "pr": 1147 + }, + { + "note": "Add `ContractWrapperError.SignatureRequestDenied` to public interface", + "pr": 1147 } ], "timestamp": 1539871071 diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index a38bd122b..d66ff5c9c 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -39,6 +39,7 @@ export { TransactionEncoder } from './utils/transaction_encoder'; export { ContractWrappersError, + ForwarderWrapperError, IndexedFilterValues, BlockRange, ContractWrappersConfig, diff --git a/packages/contract-wrappers/src/types.ts b/packages/contract-wrappers/src/types.ts index 60750179f..5a5bdd530 100644 --- a/packages/contract-wrappers/src/types.ts +++ b/packages/contract-wrappers/src/types.ts @@ -18,6 +18,10 @@ export enum ExchangeWrapperError { AssetDataMismatch = 'ASSET_DATA_MISMATCH', } +export enum ForwarderWrapperError { + CompleteFillFailed = 'COMPLETE_FILL_FAILED', +} + export enum ContractWrappersError { ContractNotDeployedOnNetwork = 'CONTRACT_NOT_DEPLOYED_ON_NETWORK', InsufficientAllowanceForTransfer = 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER', @@ -30,6 +34,7 @@ export enum ContractWrappersError { SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT', ERC721OwnerNotFound = 'ERC_721_OWNER_NOT_FOUND', ERC721NoApproval = 'ERC_721_NO_APPROVAL', + SignatureRequestDenied = 'SIGNATURE_REQUEST_DENIED', } export enum InternalContractWrappersError { diff --git a/packages/contract-wrappers/src/utils/constants.ts b/packages/contract-wrappers/src/utils/constants.ts index 9ff61f62a..c587ba526 100644 --- a/packages/contract-wrappers/src/utils/constants.ts +++ b/packages/contract-wrappers/src/utils/constants.ts @@ -14,4 +14,5 @@ export const constants = { ZERO_AMOUNT: new BigNumber(0), ONE_AMOUNT: new BigNumber(1), ETHER_TOKEN_DECIMALS: 18, + USER_DENIED_SIGNATURE_PATTERN: 'User denied transaction signature', }; diff --git a/packages/contract-wrappers/src/utils/decorators.ts b/packages/contract-wrappers/src/utils/decorators.ts index e17246015..a4207ae4c 100644 --- a/packages/contract-wrappers/src/utils/decorators.ts +++ b/packages/contract-wrappers/src/utils/decorators.ts @@ -29,6 +29,14 @@ const schemaErrorTransformer = (error: Error) => { return error; }; +const signatureRequestErrorTransformer = (error: Error) => { + if (_.includes(error.message, constants.USER_DENIED_SIGNATURE_PATTERN)) { + const errMsg = ContractWrappersError.SignatureRequestDenied; + return new Error(errMsg); + } + return error; +}; + /** * Source: https://stackoverflow.com/a/29837695/3546986 */ @@ -87,7 +95,11 @@ const syncErrorHandlerFactory = (errorTransformer: ErrorTransformer) => { }; // _.flow(f, g) = f ∘ g -const zeroExErrorTransformer = _.flow(schemaErrorTransformer, contractCallErrorTransformer); +const zeroExErrorTransformer = _.flow( + schemaErrorTransformer, + contractCallErrorTransformer, + signatureRequestErrorTransformer, +); export const decorators = { asyncZeroExErrorHandler: asyncErrorHandlerFactory(zeroExErrorTransformer), diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 11579cf47..5aedb249f 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -4,29 +4,23 @@ Smart contracts that implement the 0x protocol. Addresses of the deployed contra ## Usage -### 2.0.0 +Contracts that make up and interact with version 2.0.0 of the protocol can be found in the [contracts](./contracts) directory. The contents of this directory are broken down into the following subdirectories: -Contracts that make up and interact with version 2.0.0 of the protocol can be found in the `src/2.0.0` directory. The contents of this directory are broken down into the following subdirectories: - -* protocol +* [protocol](./contracts/protocol) * This directory contains the contracts that make up version 2.0.0. A full specification can be found [here](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md). -* extensions +* [extensions](./contracts/extensions) * This directory contains contracts that interact with the 2.0.0 contracts and will be used in production, such as the [Forwarder](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarder-specification.md) contract. -* examples +* [examples](./contracts/examples) * This directory contains example implementations of contracts that interact with the protocol but are _not_ intended for use in production. Examples include [filter](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md#filter-contracts) contracts, a [Wallet](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md#wallet) contract, and a [Validator](https://github.com/0xProject/0x-protocol-specification/blob/master/v2/v2-specification.md#validator) contract, among others. -* tokens +* [tokens](./contracts/tokens) * This directory contains implementations of different tokens and token standards, including [wETH](https://weth.io/), ZRX, [ERC20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md), and [ERC721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md). -* multisig +* [multisig](./contracts/multisig) * This directory contains the [Gnosis MultiSigWallet](https://github.com/gnosis/MultiSigWallet) and a custom extension that adds a timelock to transactions within the MultiSigWallet. -* utils +* [utils](./contracts/utils) * This directory contains libraries and utils that are shared across all of the other directories. -* test +* [test](./contracts/test) * This directory contains mocks and other contracts that are used solely for testing contracts within the other directories. -### 1.0.0 - -Contracts that make up version 1.0.0 of the protocol can be found in `src/1.0.0`. These contracts are considered deprecated and will have limited support going forward. - ## Bug bounty A bug bounty for the 2.0.0 contracts is ongoing! Instructions can be found [here](https://0xproject.com/wiki#Bug-Bounty). diff --git a/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol b/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol index fea9a53c2..4991c0ea5 100644 --- a/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol +++ b/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol @@ -155,8 +155,10 @@ contract MixinExchangeWrapper is uint256 remainingMakerAssetFillAmount = safeSub(makerAssetFillAmount, totalFillResults.makerAssetFilledAmount); // Convert the remaining amount of makerAsset to buy into remaining amount - // of takerAsset to sell, assuming entire amount can be sold in the current order - uint256 remainingTakerAssetFillAmount = getPartialAmountFloor( + // of takerAsset to sell, assuming entire amount can be sold in the current order. + // We round up because the exchange rate computed by fillOrder rounds in favor + // of the Maker. In this case we want to overestimate the amount of takerAsset. + uint256 remainingTakerAssetFillAmount = getPartialAmountCeil( orders[i].takerAssetAmount, orders[i].makerAssetAmount, remainingMakerAssetFillAmount @@ -224,7 +226,9 @@ contract MixinExchangeWrapper is // Convert the remaining amount of ZRX to buy into remaining amount // of WETH to sell, assuming entire amount can be sold in the current order. - uint256 remainingWethSellAmount = getPartialAmountFloor( + // We round up because the exchange rate computed by fillOrder rounds in favor + // of the Maker. In this case we want to overestimate the amount of takerAsset. + uint256 remainingWethSellAmount = getPartialAmountCeil( orders[i].takerAssetAmount, safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees remainingZrxBuyAmount @@ -233,7 +237,7 @@ contract MixinExchangeWrapper is // Attempt to sell the remaining amount of WETH. FillResults memory singleFillResult = fillOrderNoThrow( orders[i], - safeAdd(remainingWethSellAmount, 1), // we add 1 wei to the fill amount to make up for rounding errors + remainingWethSellAmount, signatures[i] ); diff --git a/packages/contracts/test/extensions/forwarder.ts b/packages/contracts/test/extensions/forwarder.ts index b76624fa9..c006be0fe 100644 --- a/packages/contracts/test/extensions/forwarder.ts +++ b/packages/contracts/test/extensions/forwarder.ts @@ -45,6 +45,7 @@ describe(ContractName.Forwarder, () => { let weth: DummyERC20TokenContract; let zrxToken: DummyERC20TokenContract; + let erc20TokenA: DummyERC20TokenContract; let erc721Token: DummyERC721TokenContract; let forwarderContract: ForwarderContract; let wethContract: WETH9Contract; @@ -77,7 +78,6 @@ describe(ContractName.Forwarder, () => { erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); const numDummyErc20ToDeploy = 3; - let erc20TokenA; [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS, @@ -902,6 +902,269 @@ describe(ContractName.Forwarder, () => { ); expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded', async () => { + // The 0x Protocol contracts round the exchange rate in favor of the Maker. + // In this case, the taker must round up how much they're going to spend, which + // in turn increases the amount of MakerAsset being purchased. + // Example: + // The taker wants to buy 5 units of the MakerAsset at a rate of 3M/2T. + // For every 2 units of TakerAsset, the taker will receive 3 units of MakerAsset. + // To purchase 5 units, the taker must spend 10/3 = 3.33 units of TakerAssset. + // However, the Taker can only spend whole units. + // Spending floor(10/3) = 3 units will yield a profit of Floor(3*3/2) = Floor(4.5) = 4 units of MakerAsset. + // Spending ceil(10/3) = 4 units will yield a profit of Floor(4*3/2) = 6 units of MakerAsset. + // + // The forwarding contract will opt for the second option, which overbuys, to ensure the taker + // receives at least the amount of MakerAsset they requested. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('20'), + makerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const desiredMakerAssetFillAmount = new BigNumber('5'); + const makerAssetFillAmount = new BigNumber('6'); + const ethValue = new BigNumber('4'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded, and MakerAsset is ZRX', async () => { + // See the test case above for a detailed description of this case. + // The difference here is that the MakerAsset is ZRX. We expect the same result as above, + // but this tests a different code path. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('20'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const desiredMakerAssetFillAmount = new BigNumber('5'); + const makerAssetFillAmount = new BigNumber('6'); + const ethValue = new BigNumber('4'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded (Regression Test)', async () => { + // Order taken from a transaction on mainnet that failed due to a rounding error. + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('268166666666666666666'), + takerAssetAmount: new BigNumber('219090625878836371'), + makerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + // The taker will receive more than the desired amount of makerAsset due to rounding + const desiredMakerAssetFillAmount = new BigNumber('5000000000000000000'); + const ethValue = new BigNumber('4084971271824171'); + const makerAssetFillAmount = ethValue + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded, and MakerAsset is ZRX (Regression Test)', async () => { + // Order taken from a transaction on mainnet that failed due to a rounding error. + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('268166666666666666666'), + takerAssetAmount: new BigNumber('219090625878836371'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + // The taker will receive more than the desired amount of makerAsset due to rounding + const desiredMakerAssetFillAmount = new BigNumber('5000000000000000000'); + const ethValue = new BigNumber('4084971271824171'); + const makerAssetFillAmount = ethValue + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy correct MakerAsset when exchange rate is NOT rounded, and MakerAsset is ZRX (Regression Test)', async () => { + // An extra unit of TakerAsset was sent to the exchange contract to account for rounding errors, in Forwarder v1. + // Specifically, the takerFillAmount was calculated using Floor(desiredMakerAmount * exchangeRate) + 1 + // We have since changed this to be Ceil(desiredMakerAmount * exchangeRate) + // These calculations produce different results when `desiredMakerAmount * exchangeRate` is an integer. + // + // This test verifies that `ceil` is sufficient: + // Let TakerAssetAmount = MakerAssetAmount * 2 + // -> exchangeRate = TakerAssetAmount / MakerAssetAmount = (2*MakerAssetAmount)/MakerAssetAmount = 2 + // .: desiredMakerAmount * exchangeRate is an integer. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('60'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber('5'); + const ethValue = new BigNumber('10'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); }); describe('marketBuyOrdersWithEth with extra fees', () => { it('should buy an asset and send fee to feeRecipient', async () => { diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts index f1a64d47d..a0bfcfe1d 100644 --- a/packages/contracts/test/utils/forwarder_wrapper.ts +++ b/packages/contracts/test/utils/forwarder_wrapper.ts @@ -26,9 +26,12 @@ export class ForwarderWrapper { _.forEach(feeOrders, feeOrder => { const feeAvailable = feeOrder.makerAssetAmount.minus(feeOrder.takerFee); if (!remainingFeeAmount.isZero() && feeAvailable.gt(remainingFeeAmount)) { - wethAmount = wethAmount - .plus(feeOrder.takerAssetAmount.times(remainingFeeAmount).dividedToIntegerBy(feeAvailable)) - .plus(1); + wethAmount = wethAmount.plus( + feeOrder.takerAssetAmount + .times(remainingFeeAmount) + .dividedBy(feeAvailable) + .ceil(), + ); remainingFeeAmount = new BigNumber(0); } else if (!remainingFeeAmount.isZero()) { wethAmount = wethAmount.plus(feeOrder.takerAssetAmount); diff --git a/packages/instant/src/components/amount_placeholder.tsx b/packages/instant/src/components/amount_placeholder.tsx new file mode 100644 index 000000000..6ef8f0ac3 --- /dev/null +++ b/packages/instant/src/components/amount_placeholder.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; + +import { Pulse } from './animations/pulse'; + +import { Text } from './ui'; + +interface PlainPlaceholder { + color: ColorOption; +} +const PlainPlaceholder: React.StatelessComponent<PlainPlaceholder> = props => ( + <Text fontWeight="bold" fontColor={props.color}> + — + </Text> +); + +export interface AmountPlaceholderProps { + color: ColorOption; + isPulsating: boolean; +} +export const AmountPlaceholder: React.StatelessComponent<AmountPlaceholderProps> = props => { + if (props.isPulsating) { + return ( + <Pulse> + <PlainPlaceholder color={props.color} /> + </Pulse> + ); + } else { + return <PlainPlaceholder color={props.color} />; + } +}; diff --git a/packages/instant/src/components/animations/pulse.tsx b/packages/instant/src/components/animations/pulse.tsx new file mode 100644 index 000000000..01d6ea070 --- /dev/null +++ b/packages/instant/src/components/animations/pulse.tsx @@ -0,0 +1,15 @@ +import { keyframes, styled } from '../../style/theme'; + +const pulsingKeyframes = keyframes` + 0%, 100% { + opacity: 0.2; + } + 50% { + opacity: 100; + } +`; +export const Pulse = styled.div` + animation-name: ${pulsingKeyframes} + animation-duration: 2s; + animation-iteration-count: infinite; +`; diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 3ef7c1f5c..f999d59a8 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -6,7 +6,7 @@ import { ColorOption } from '../style/theme'; import { util } from '../util/util'; import { web3Wrapper } from '../util/web3_wrapper'; -import { Button, Container, Text } from './ui'; +import { Button, Text } from './ui'; export interface BuyButtonProps { buyQuote?: BuyQuote; @@ -14,7 +14,6 @@ export interface BuyButtonProps { onClick: (buyQuote: BuyQuote) => void; onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void; onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void; - text: string; } export class BuyButton extends React.Component<BuyButtonProps> { @@ -24,15 +23,13 @@ export class BuyButton extends React.Component<BuyButtonProps> { onBuyFailure: util.boundNoop, }; public render(): React.ReactNode { - const shouldDisableButton = _.isUndefined(this.props.buyQuote); + const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer); return ( - <Container padding="20px" width="100%"> - <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> - <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> - {this.props.text} - </Text> - </Button> - </Container> + <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> + <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> + Buy + </Text> + </Button> ); } private readonly _handleClick = async () => { diff --git a/packages/instant/src/components/buy_order_state_button.tsx b/packages/instant/src/components/buy_order_state_button.tsx new file mode 100644 index 000000000..e7641e7e7 --- /dev/null +++ b/packages/instant/src/components/buy_order_state_button.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; + +import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button'; +import { SelectedAssetRetryButton } from '../containers/selected_asset_retry_button'; + +import { AsyncProcessState } from '../types'; + +import { SecondaryButton } from './secondary_button'; + +export interface BuyOrderStateButtonProps { + buyOrderState: AsyncProcessState; +} + +export const BuyOrderStateButton: React.StatelessComponent<BuyOrderStateButtonProps> = props => { + if (props.buyOrderState === AsyncProcessState.FAILURE) { + return <SelectedAssetRetryButton />; + } else if (props.buyOrderState === AsyncProcessState.SUCCESS) { + return <SecondaryButton text="Success" isDisabled={true} />; + } else if (props.buyOrderState === AsyncProcessState.PENDING) { + return <SecondaryButton text="Processing" isDisabled={true} />; + } + + return <SelectedAssetBuyButton />; +}; diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index c9f0ea95a..0efb58360 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -7,81 +7,109 @@ import { ColorOption } from '../style/theme'; import { AsyncProcessState } from '../types'; import { format } from '../util/format'; +import { AmountPlaceholder } from './amount_placeholder'; import { Container, Flex, Text } from './ui'; +import { Icon } from './ui/icon'; export interface InstantHeadingProps { selectedAssetAmount?: BigNumber; totalEthBaseAmount?: BigNumber; ethUsdPrice?: BigNumber; - quoteState: AsyncProcessState; + quoteRequestState: AsyncProcessState; + buyOrderState: AsyncProcessState; } -const Placeholder = () => ( - <Text fontWeight="bold" fontColor={ColorOption.white}> - — - </Text> -); -const displaytotalEthBaseAmount = ({ - selectedAssetAmount, - totalEthBaseAmount, -}: InstantHeadingProps): React.ReactNode => { - if (_.isUndefined(selectedAssetAmount)) { - return '0 ETH'; +const placeholderColor = ColorOption.white; +export class InstantHeading extends React.Component<InstantHeadingProps, {}> { + public render(): React.ReactNode { + const iconOrAmounts = this._renderIcon() || this._renderAmountsSection(); + return ( + <Container + backgroundColor={ColorOption.primaryColor} + padding="20px" + width="100%" + borderRadius="3px 3px 0px 0px" + > + <Container marginBottom="5px"> + <Text + letterSpacing="1px" + fontColor={ColorOption.white} + opacity={0.7} + fontWeight={500} + textTransform="uppercase" + fontSize="12px" + > + {this._renderTopText()} + </Text> + </Container> + <Flex direction="row" justify="space-between"> + <Flex height="60px"> + <SelectedAssetAmountInput startingFontSizePx={38} /> + </Flex> + <Flex direction="column" justify="space-between"> + {iconOrAmounts} + </Flex> + </Flex> + </Container> + ); } - return format.ethBaseAmount(totalEthBaseAmount, 4, <Placeholder />); -}; -const displayUsdAmount = ({ - totalEthBaseAmount, - selectedAssetAmount, - ethUsdPrice, -}: InstantHeadingProps): React.ReactNode => { - if (_.isUndefined(selectedAssetAmount)) { - return '$0.00'; + private _renderAmountsSection(): React.ReactNode { + return ( + <Container> + <Container marginBottom="5px">{this._placeholderOrAmount(this._ethAmount)}</Container> + <Container opacity={0.7}>{this._placeholderOrAmount(this._dollarAmount)}</Container> + </Container> + ); + } + + private _renderIcon(): React.ReactNode { + if (this.props.buyOrderState === AsyncProcessState.FAILURE) { + return <Icon icon={'failed'} width={34} height={34} color={ColorOption.white} />; + } + return undefined; } - return format.ethBaseAmountInUsd(totalEthBaseAmount, ethUsdPrice, 2, <Placeholder />); -}; -const loadingOrAmount = (quoteState: AsyncProcessState, amount: React.ReactNode): React.ReactNode => { - if (quoteState === AsyncProcessState.PENDING) { + private _renderTopText(): React.ReactNode { + if (this.props.buyOrderState === AsyncProcessState.FAILURE) { + return 'Order failed'; + } + + return 'I want to buy'; + } + + private _placeholderOrAmount(amountFunction: () => React.ReactNode): React.ReactNode { + if (this.props.quoteRequestState === AsyncProcessState.PENDING) { + return <AmountPlaceholder isPulsating={true} color={placeholderColor} />; + } + if (_.isUndefined(this.props.selectedAssetAmount)) { + return <AmountPlaceholder isPulsating={false} color={placeholderColor} />; + } + return amountFunction(); + } + + private readonly _ethAmount = (): React.ReactNode => { return ( - <Text fontWeight="bold" fontColor={ColorOption.white}> - …loading + <Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}> + {format.ethBaseAmount( + this.props.totalEthBaseAmount, + 4, + <AmountPlaceholder isPulsating={false} color={placeholderColor} />, + )} </Text> ); - } else { - return amount; - } -}; + }; -export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = props => ( - <Container backgroundColor={ColorOption.primaryColor} padding="20px" width="100%" borderRadius="3px 3px 0px 0px"> - <Container marginBottom="5px"> - <Text - letterSpacing="1px" - fontColor={ColorOption.white} - opacity={0.7} - fontWeight={500} - textTransform="uppercase" - fontSize="12px" - > - I want to buy + private readonly _dollarAmount = (): React.ReactNode => { + return ( + <Text fontSize="16px" fontColor={ColorOption.white}> + {format.ethBaseAmountInUsd( + this.props.totalEthBaseAmount, + this.props.ethUsdPrice, + 2, + <AmountPlaceholder isPulsating={false} color={ColorOption.white} />, + )} </Text> - </Container> - <Flex direction="row" justify="space-between"> - <Flex height="65px"> - <SelectedAssetAmountInput startingFontSizePx={38} /> - </Flex> - <Flex direction="column" justify="space-between"> - <Container marginBottom="5px" whiteSpace="nowrap"> - <Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}> - {loadingOrAmount(props.quoteState, displaytotalEthBaseAmount(props))} - </Text> - </Container> - <Text fontSize="16px" fontColor={ColorOption.white} opacity={0.7}> - {loadingOrAmount(props.quoteState, displayUsdAmount(props))} - </Text> - </Flex> - </Flex> - </Container> -); + ); + }; +} diff --git a/packages/instant/src/components/order_details.tsx b/packages/instant/src/components/order_details.tsx index ad4a87714..704009d89 100644 --- a/packages/instant/src/components/order_details.tsx +++ b/packages/instant/src/components/order_details.tsx @@ -7,13 +7,14 @@ import { oc } from 'ts-optchain'; import { ColorOption } from '../style/theme'; import { format } from '../util/format'; +import { AmountPlaceholder } from './amount_placeholder'; import { Container, Flex, Text } from './ui'; export interface OrderDetailsProps { buyQuoteInfo?: BuyQuoteInfo; ethUsdPrice?: BigNumber; + isLoading: boolean; } - export class OrderDetails extends React.Component<OrderDetailsProps> { public render(): React.ReactNode { const { buyQuoteInfo, ethUsdPrice } = this.props; @@ -39,13 +40,20 @@ export class OrderDetails extends React.Component<OrderDetailsProps> { ethAmount={ethAssetPrice} ethUsdPrice={ethUsdPrice} isEthAmountInBaseUnits={false} + isLoading={this.props.isLoading} + /> + <EthAmountRow + rowLabel="Fee" + ethAmount={ethTokenFee} + ethUsdPrice={ethUsdPrice} + isLoading={this.props.isLoading} /> - <EthAmountRow rowLabel="Fee" ethAmount={ethTokenFee} ethUsdPrice={ethUsdPrice} /> <EthAmountRow rowLabel="Total Cost" ethAmount={totalEthAmount} ethUsdPrice={ethUsdPrice} shouldEmphasize={true} + isLoading={this.props.isLoading} /> </Container> ); @@ -58,43 +66,50 @@ export interface EthAmountRowProps { isEthAmountInBaseUnits?: boolean; ethUsdPrice?: BigNumber; shouldEmphasize?: boolean; + isLoading: boolean; } -export const EthAmountRow: React.StatelessComponent<EthAmountRowProps> = ({ - rowLabel, - ethAmount, - isEthAmountInBaseUnits, - ethUsdPrice, - shouldEmphasize, -}) => { - const fontWeight = shouldEmphasize ? 700 : 400; - const usdFormatter = isEthAmountInBaseUnits ? format.ethBaseAmountInUsd : format.ethUnitAmountInUsd; - const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseAmount : format.ethUnitAmount; - const usdPriceSection = _.isUndefined(ethUsdPrice) ? null : ( - <Container marginRight="3px" display="inline-block"> - <Text fontColor={ColorOption.lightGrey}>({usdFormatter(ethAmount, ethUsdPrice)})</Text> - </Container> - ); - return ( - <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}> - <Flex justify="space-between"> - <Text fontWeight={fontWeight} fontColor={ColorOption.grey}> - {rowLabel} - </Text> - <Container> - {usdPriceSection} +export class EthAmountRow extends React.Component<EthAmountRowProps> { + public static defaultProps = { + shouldEmphasize: false, + isEthAmountInBaseUnits: true, + }; + public render(): React.ReactNode { + const { rowLabel, ethAmount, isEthAmountInBaseUnits, shouldEmphasize, isLoading } = this.props; + + const fontWeight = shouldEmphasize ? 700 : 400; + const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseAmount : format.ethUnitAmount; + return ( + <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}> + <Flex justify="space-between"> <Text fontWeight={fontWeight} fontColor={ColorOption.grey}> - {ethFormatter(ethAmount)} + {rowLabel} </Text> - </Container> - </Flex> - </Container> - ); -}; - -EthAmountRow.defaultProps = { - shouldEmphasize: false, - isEthAmountInBaseUnits: true, -}; - -EthAmountRow.displayName = 'EthAmountRow'; + <Container> + {this._renderUsdSection()} + <Text fontWeight={fontWeight} fontColor={ColorOption.grey}> + {ethFormatter( + ethAmount, + 4, + <Container opacity={0.5}> + <AmountPlaceholder color={ColorOption.lightGrey} isPulsating={isLoading} /> + </Container>, + )} + </Text> + </Container> + </Flex> + </Container> + ); + } + private _renderUsdSection(): React.ReactNode { + const usdFormatter = this.props.isEthAmountInBaseUnits ? format.ethBaseAmountInUsd : format.ethUnitAmountInUsd; + const shouldHideUsdPriceSection = _.isUndefined(this.props.ethUsdPrice) || _.isUndefined(this.props.ethAmount); + return shouldHideUsdPriceSection ? null : ( + <Container marginRight="3px" display="inline-block"> + <Text fontColor={ColorOption.lightGrey}> + ({usdFormatter(this.props.ethAmount, this.props.ethUsdPrice)}) + </Text> + </Container> + ); + } +} diff --git a/packages/instant/src/components/retry_button.tsx b/packages/instant/src/components/retry_button.tsx new file mode 100644 index 000000000..28547ce54 --- /dev/null +++ b/packages/instant/src/components/retry_button.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +import { SecondaryButton } from './secondary_button'; + +export interface RetryButtonProps { + onClick: () => void; +} + +export const RetryButton: React.StatelessComponent<RetryButtonProps> = props => { + return <SecondaryButton text="Try Again" onClick={props.onClick} />; +}; diff --git a/packages/instant/src/components/secondary_button.tsx b/packages/instant/src/components/secondary_button.tsx new file mode 100644 index 000000000..e073f6061 --- /dev/null +++ b/packages/instant/src/components/secondary_button.tsx @@ -0,0 +1,28 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; + +import { Button, ButtonProps } from './ui/button'; +import { Text } from './ui/text'; + +export interface SecondaryButtonProps extends ButtonProps { + text: string; +} + +export const SecondaryButton: React.StatelessComponent<SecondaryButtonProps> = props => { + const buttonProps = _.omit(props, 'text'); + return ( + <Button + backgroundColor={ColorOption.white} + borderColor={ColorOption.lightGrey} + width="100%" + onClick={props.onClick} + {...buttonProps} + > + <Text fontColor={ColorOption.primaryColor} fontWeight={600} fontSize="16px"> + {props.text} + </Text> + </Button> + ); +}; diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index e44cfff1e..7077d0aa3 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -27,6 +27,7 @@ export interface ContainerProps { hasBoxShadow?: boolean; zIndex?: number; whiteSpace?: string; + opacity?: number; } export const Container = styled<ContainerProps, 'div'>('div')` @@ -52,6 +53,7 @@ export const Container = styled<ContainerProps, 'div'>('div')` ${props => cssRuleIfExists(props, 'border-bottom')} ${props => cssRuleIfExists(props, 'z-index')} ${props => cssRuleIfExists(props, 'white-space')} + ${props => cssRuleIfExists(props, 'opacity')} ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')}; diff --git a/packages/instant/src/components/ui/icon.tsx b/packages/instant/src/components/ui/icon.tsx new file mode 100644 index 000000000..7373c3acd --- /dev/null +++ b/packages/instant/src/components/ui/icon.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; + +import { ColorOption } from '../../style/theme'; + +type svgRule = 'evenodd' | 'nonzero' | 'inherit'; +interface IconInfo { + viewBox: string; + fillRule?: svgRule; + clipRule?: svgRule; + path: string; +} +interface IconInfoMapping { + failed: IconInfo; + success: IconInfo; +} +const ICONS: IconInfoMapping = { + failed: { + viewBox: '0 0 34 34', + fillRule: 'evenodd', + clipRule: 'evenodd', + path: + 'M6.65771 26.4362C9.21777 29.2406 12.9033 31 17 31C24.7319 31 31 24.7319 31 17C31 14.4468 30.3164 12.0531 29.1226 9.99219L6.65771 26.4362ZM4.88281 24.0173C3.68555 21.9542 3 19.5571 3 17C3 9.26807 9.26807 3 17 3C21.1006 3 24.7891 4.76294 27.3496 7.57214L4.88281 24.0173ZM0 17C0 26.3888 7.61133 34 17 34C26.3887 34 34 26.3888 34 17C34 7.61121 26.3887 0 17 0C7.61133 0 0 7.61121 0 17Z', + }, + success: { + viewBox: '0 0 34 34', + fillRule: 'evenodd', + clipRule: 'evenodd', + path: + 'M17 34C26.3887 34 34 26.3888 34 17C34 7.61121 26.3887 0 17 0C7.61133 0 0 7.61121 0 17C0 26.3888 7.61133 34 17 34ZM25.7539 13.0977C26.2969 12.4718 26.2295 11.5244 25.6035 10.9817C24.9775 10.439 24.0303 10.5063 23.4878 11.1323L15.731 20.0771L12.3936 16.7438C11.8071 16.1583 10.8574 16.1589 10.272 16.7451C9.68652 17.3313 9.6875 18.281 10.2734 18.8665L14.75 23.3373L15.8887 24.4746L16.9434 23.2587L25.7539 13.0977Z', + }, +}; + +export interface IconProps { + width: number; + height: number; + color: ColorOption; + icon: keyof IconInfoMapping; +} +export const Icon: React.SFC<IconProps> = props => { + const iconInfo = ICONS[props.icon]; + + return ( + <svg + width={props.width} + height={props.height} + viewBox={iconInfo.viewBox} + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d={iconInfo.path} + fill={props.color} + fillRule={iconInfo.fillRule || 'nonzero'} + clipRule={iconInfo.clipRule || 'nonzero'} + /> + </svg> + ); +}; diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index 142e74dae..ffa5a8250 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -31,7 +31,7 @@ export interface ZeroExInstantOptionalProps { } export class ZeroExInstant extends React.Component<ZeroExInstantProps> { - public store: Store; + private readonly _store: Store; private static _mergeInitialStateWithProps(props: ZeroExInstantProps, state: State = INITIAL_STATE): State { // Create merged object such that properties in props override default settings const optionalPropsWithDefaults: ZeroExInstantOptionalProps = { @@ -58,14 +58,14 @@ export class ZeroExInstant extends React.Component<ZeroExInstantProps> { } constructor(props: ZeroExInstantProps) { super(props); - this.store = store.create(ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE)); + this._store = store.create(ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE)); // tslint:disable-next-line:no-floating-promises - asyncData.fetchAndDispatchToStore(this.store); + asyncData.fetchAndDispatchToStore(this._store); } public render(): React.ReactNode { return ( - <Provider store={this.store}> + <Provider store={this._store}> <SelectedAssetThemeProvider> <ZeroExInstantContainer /> </SelectedAssetThemeProvider> diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index cf918d890..1d17ed12a 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details'; import { LatestError } from '../containers/latest_error'; -import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button'; +import { SelectedAssetBuyOrderStateButton } from '../containers/selected_asset_buy_order_state_button'; import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; import { ColorOption } from '../style/theme'; @@ -26,7 +26,9 @@ export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantConta <Flex direction="column" justify="flex-start"> <SelectedAssetInstantHeading /> <LatestBuyQuoteOrderDetails /> - <SelectedAssetBuyButton /> + <Container padding="20px" width="100%"> + <SelectedAssetBuyOrderStateButton /> + </Container> </Flex> </Container> </Container> diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 7f4c5a058..31491c80a 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -1,3 +1,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'; 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 597bf3088..092aaaf20 100644 --- a/packages/instant/src/containers/latest_buy_quote_order_details.ts +++ b/packages/instant/src/containers/latest_buy_quote_order_details.ts @@ -8,18 +8,21 @@ import { oc } from 'ts-optchain'; import { State } from '../redux/reducer'; import { OrderDetails } from '../components/order_details'; +import { AsyncProcessState } from '../types'; export interface LatestBuyQuoteOrderDetailsProps {} interface ConnectedState { buyQuoteInfo?: BuyQuoteInfo; ethUsdPrice?: BigNumber; + isLoading: boolean; } const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProps): ConnectedState => ({ // use the worst case quote info buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(), ethUsdPrice: state.ethUsdPrice, + isLoading: state.quoteRequestState === AsyncProcessState.PENDING, }); export const LatestBuyQuoteOrderDetails: React.ComponentClass<LatestBuyQuoteOrderDetailsProps> = connect( diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx index 1d02cab23..b75ec00aa 100644 --- a/packages/instant/src/containers/latest_error.tsx +++ b/packages/instant/src/containers/latest_error.tsx @@ -3,8 +3,8 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { SlidingError } from '../components/sliding_error'; -import { LatestErrorDisplay, State } from '../redux/reducer'; -import { Asset } from '../types'; +import { State } from '../redux/reducer'; +import { Asset, DisplayStatus } from '../types'; import { errorUtil } from '../util/error'; export interface LatestErrorComponentProps { @@ -30,7 +30,7 @@ export interface LatestErrorProps {} const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({ asset: state.selectedAsset, latestError: state.latestError, - slidingDirection: state.latestErrorDisplay === LatestErrorDisplay.Present ? 'up' : 'down', + slidingDirection: state.latestErrorDisplay === DisplayStatus.Present ? 'up' : 'down', }); export const LatestError = connect(mapStateToProps)(LatestErrorComponent); diff --git a/packages/instant/src/containers/selected_asset_amount_input.ts b/packages/instant/src/containers/selected_asset_amount_input.ts index 1b5f3cd22..e1438e920 100644 --- a/packages/instant/src/containers/selected_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_asset_amount_input.ts @@ -62,13 +62,13 @@ const updateBuyQuoteAsync = async ( const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); // mark quote as pending - dispatch(actions.updateBuyQuoteStatePending()); + dispatch(actions.setQuoteRequestStatePending()); let newBuyQuote: BuyQuote | undefined; try { newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue); } catch (error) { - dispatch(actions.updateBuyQuoteStateFailure()); + dispatch(actions.setQuoteRequestStateFailure()); errorUtil.errorFlasher.flashNewError(dispatch, error); return; } @@ -90,11 +90,11 @@ const mapDispatchToProps = ( // invalidate the last buy quote. dispatch(actions.updateLatestBuyQuote(undefined)); // reset our buy state - dispatch(actions.updatebuyOrderState(AsyncProcessState.NONE)); + dispatch(actions.updateBuyOrderState(AsyncProcessState.NONE)); if (!_.isUndefined(value) && !_.isUndefined(asset) && !_.isUndefined(assetBuyer)) { // even if it's debounced, give them the illusion it's loading - dispatch(actions.updateBuyQuoteStatePending()); + dispatch(actions.setQuoteRequestStatePending()); // tslint:disable-next-line:no-floating-promises debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value); } diff --git a/packages/instant/src/containers/selected_asset_buy_button.ts b/packages/instant/src/containers/selected_asset_buy_button.ts index 99f971321..208bb2582 100644 --- a/packages/instant/src/containers/selected_asset_buy_button.ts +++ b/packages/instant/src/containers/selected_asset_buy_button.ts @@ -14,7 +14,6 @@ export interface SelectedAssetBuyButtonProps {} interface ConnectedState { assetBuyer?: AssetBuyer; - text: string; buyQuote?: BuyQuote; } @@ -24,31 +23,15 @@ interface ConnectedDispatch { onBuyFailure: (buyQuote: BuyQuote) => void; } -const textForState = (state: AsyncProcessState): string => { - switch (state) { - case AsyncProcessState.NONE: - return 'Buy'; - case AsyncProcessState.PENDING: - return '...Loading'; - case AsyncProcessState.SUCCESS: - return 'Success!'; - case AsyncProcessState.FAILURE: - return 'Failed'; - default: - return 'Buy'; - } -}; - const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({ assetBuyer: state.assetBuyer, - text: textForState(state.buyOrderState), buyQuote: state.latestBuyQuote, }); const mapDispatchToProps = (dispatch: Dispatch<Action>, ownProps: SelectedAssetBuyButtonProps): ConnectedDispatch => ({ - onClick: buyQuote => dispatch(actions.updatebuyOrderState(AsyncProcessState.PENDING)), - onBuySuccess: buyQuote => dispatch(actions.updatebuyOrderState(AsyncProcessState.SUCCESS)), - onBuyFailure: buyQuote => dispatch(actions.updatebuyOrderState(AsyncProcessState.FAILURE)), + onClick: buyQuote => dispatch(actions.updateBuyOrderState(AsyncProcessState.PENDING)), + onBuySuccess: buyQuote => dispatch(actions.updateBuyOrderState(AsyncProcessState.SUCCESS)), + onBuyFailure: buyQuote => dispatch(actions.updateBuyOrderState(AsyncProcessState.FAILURE)), }); 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 new file mode 100644 index 000000000..3b7fc0054 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_buy_order_state_button.tsx @@ -0,0 +1,20 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { State } from '../redux/reducer'; +import { AsyncProcessState } from '../types'; + +import { BuyOrderStateButton } from '../components/buy_order_state_button'; + +interface ConnectedState { + buyOrderState: AsyncProcessState; +} +export interface SelectedAssetButtonProps {} +const mapStateToProps = (state: State, _ownProps: SelectedAssetButtonProps): ConnectedState => ({ + buyOrderState: state.buyOrderState, +}); + +export const SelectedAssetBuyOrderStateButton: React.ComponentClass<SelectedAssetButtonProps> = connect( + mapStateToProps, +)(BuyOrderStateButton); diff --git a/packages/instant/src/containers/selected_asset_instant_heading.ts b/packages/instant/src/containers/selected_asset_instant_heading.ts index 0509db5da..24efed32e 100644 --- a/packages/instant/src/containers/selected_asset_instant_heading.ts +++ b/packages/instant/src/containers/selected_asset_instant_heading.ts @@ -15,14 +15,16 @@ interface ConnectedState { selectedAssetAmount?: BigNumber; totalEthBaseAmount?: BigNumber; ethUsdPrice?: BigNumber; - quoteState: AsyncProcessState; + quoteRequestState: AsyncProcessState; + buyOrderState: AsyncProcessState; } const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({ selectedAssetAmount: state.selectedAssetAmount, totalEthBaseAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(), ethUsdPrice: state.ethUsdPrice, - quoteState: state.quoteState, + quoteRequestState: state.quoteRequestState, + buyOrderState: state.buyOrderState, }); export const SelectedAssetInstantHeading: React.ComponentClass<InstantHeadingProps> = connect(mapStateToProps)( diff --git a/packages/instant/src/containers/selected_asset_retry_button.tsx b/packages/instant/src/containers/selected_asset_retry_button.tsx new file mode 100644 index 000000000..b2b140be6 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_retry_button.tsx @@ -0,0 +1,26 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { Action, actions } from '../redux/actions'; + +import { RetryButton } from '../components/retry_button'; + +export interface SelectedAssetRetryButtonProps {} + +interface ConnectedDispatch { + onClick: () => void; +} + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + _ownProps: SelectedAssetRetryButtonProps, +): ConnectedDispatch => ({ + onClick: () => dispatch(actions.resetAmount()), +}); + +export const SelectedAssetRetryButton: React.ComponentClass<SelectedAssetRetryButtonProps> = connect( + undefined, + mapDispatchToProps, +)(RetryButton); diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index fe78ce401..f648b37f2 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -1,8 +1,9 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR } from './constants'; import { ZeroExInstant, ZeroExInstantProps } from './index'; -export const render = (props: ZeroExInstantProps, selector: string = '#zeroExInstantContainer') => { +export const render = (props: ZeroExInstantProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => { ReactDOM.render(React.createElement(ZeroExInstant, props), document.querySelector(selector)); }; diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index bc75ce66c..f5946d707 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -26,23 +26,25 @@ export enum ActionTypes { UPDATE_SELECTED_ASSET_BUY_STATE = 'UPDATE_SELECTED_ASSET_BUY_STATE', UPDATE_LATEST_BUY_QUOTE = 'UPDATE_LATEST_BUY_QUOTE', UPDATE_SELECTED_ASSET = 'UPDATE_SELECTED_ASSET', - UPDATE_BUY_QUOTE_STATE_PENDING = 'UPDATE_BUY_QUOTE_STATE_PENDING', - UPDATE_BUY_QUOTE_STATE_FAILURE = 'UPDATE_BUY_QUOTE_STATE_FAILURE', + SET_QUOTE_REQUEST_STATE_PENDING = 'SET_QUOTE_REQUEST_STATE_PENDING', + SET_QUOTE_REQUEST_STATE_FAILURE = 'SET_QUOTE_REQUEST_STATE_FAILURE', SET_ERROR = 'SET_ERROR', HIDE_ERROR = 'HIDE_ERROR', CLEAR_ERROR = 'CLEAR_ERROR', + RESET_AMOUNT = 'RESET_AMOUNT', } export const actions = { updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price), updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount), - updatebuyOrderState: (buyState: AsyncProcessState) => + updateBuyOrderState: (buyState: AsyncProcessState) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, buyState), updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote), updateSelectedAsset: (assetData?: string) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, assetData), - updateBuyQuoteStatePending: () => createAction(ActionTypes.UPDATE_BUY_QUOTE_STATE_PENDING), - updateBuyQuoteStateFailure: () => createAction(ActionTypes.UPDATE_BUY_QUOTE_STATE_FAILURE), + setQuoteRequestStatePending: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING), + setQuoteRequestStateFailure: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE), setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error), hideError: () => createAction(ActionTypes.HIDE_ERROR), clearError: () => createAction(ActionTypes.CLEAR_ERROR), + resetAmount: () => createAction(ActionTypes.RESET_AMOUNT), }; diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index e9e0a85e0..ddcb144b1 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -4,15 +4,11 @@ import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { assetMetaDataMap } from '../data/asset_meta_data_map'; -import { Asset, AssetMetaData, AsyncProcessState, Network } from '../types'; +import { Asset, AssetMetaData, AsyncProcessState, DisplayStatus, Network } from '../types'; import { assetUtils } from '../util/asset'; import { Action, ActionTypes } from './actions'; -export enum LatestErrorDisplay { - Present, - Hidden, -} export interface State { network: Network; assetBuyer?: AssetBuyer; @@ -22,9 +18,9 @@ export interface State { buyOrderState: AsyncProcessState; ethUsdPrice?: BigNumber; latestBuyQuote?: BuyQuote; - quoteState: AsyncProcessState; + quoteRequestState: AsyncProcessState; latestError?: any; - latestErrorDisplay: LatestErrorDisplay; + latestErrorDisplay: DisplayStatus; } export const INITIAL_STATE: State = { @@ -35,8 +31,8 @@ export const INITIAL_STATE: State = { ethUsdPrice: undefined, latestBuyQuote: undefined, latestError: undefined, - latestErrorDisplay: LatestErrorDisplay.Hidden, - quoteState: AsyncProcessState.NONE, + latestErrorDisplay: DisplayStatus.Hidden, + quoteRequestState: AsyncProcessState.NONE, }; export const reducer = (state: State = INITIAL_STATE, action: Action): State => { @@ -55,19 +51,19 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => return { ...state, latestBuyQuote: action.data, - quoteState: AsyncProcessState.SUCCESS, + quoteRequestState: AsyncProcessState.SUCCESS, }; - case ActionTypes.UPDATE_BUY_QUOTE_STATE_PENDING: + case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING: return { ...state, latestBuyQuote: undefined, - quoteState: AsyncProcessState.PENDING, + quoteRequestState: AsyncProcessState.PENDING, }; - case ActionTypes.UPDATE_BUY_QUOTE_STATE_FAILURE: + case ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE: return { ...state, latestBuyQuote: undefined, - quoteState: AsyncProcessState.FAILURE, + quoteRequestState: AsyncProcessState.FAILURE, }; case ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE: return { @@ -78,18 +74,18 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => return { ...state, latestError: action.data, - latestErrorDisplay: LatestErrorDisplay.Present, + latestErrorDisplay: DisplayStatus.Present, }; case ActionTypes.HIDE_ERROR: return { ...state, - latestErrorDisplay: LatestErrorDisplay.Hidden, + latestErrorDisplay: DisplayStatus.Hidden, }; case ActionTypes.CLEAR_ERROR: return { ...state, latestError: undefined, - latestErrorDisplay: LatestErrorDisplay.Hidden, + latestErrorDisplay: DisplayStatus.Hidden, }; case ActionTypes.UPDATE_SELECTED_ASSET: const newSelectedAssetData = action.data; @@ -105,6 +101,14 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => ...state, selectedAsset: newSelectedAsset, }; + case ActionTypes.RESET_AMOUNT: + return { + ...state, + latestBuyQuote: undefined, + quoteRequestState: AsyncProcessState.NONE, + buyOrderState: AsyncProcessState.NONE, + selectedAssetAmount: undefined, + }; default: return state; } diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 0454bceea..c340623ad 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -7,6 +7,10 @@ export enum AsyncProcessState { SUCCESS = 'Success', FAILURE = 'Failure', } +export enum DisplayStatus { + Present, + Hidden, +} export type FunctionType = (...args: any[]) => any; export type ActionCreatorsMapObject = ObjectMap<FunctionType>; @@ -37,6 +41,7 @@ export interface ERC721Asset { assetData: string; metaData: ERC721AssetMetaData; } + export interface Asset { assetData: string; metaData: AssetMetaData; diff --git a/packages/instant/test/util/asset.test.ts b/packages/instant/test/util/asset.test.ts new file mode 100644 index 000000000..c7db7eba7 --- /dev/null +++ b/packages/instant/test/util/asset.test.ts @@ -0,0 +1,47 @@ +import { AssetProxyId, ObjectMap } from '@0x/types'; + +import { Asset, AssetMetaData, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types'; +import { assetUtils } from '../../src/util/asset'; + +const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; +const ZRX_ASSET_DATA_KOVAN = '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa'; +const ZRX_META_DATA: ERC20AssetMetaData = { + assetProxyId: AssetProxyId.ERC20, + symbol: 'zrx', + decimals: 18, +}; +const ZRX_ASSET: Asset = { + assetData: ZRX_ASSET_DATA, + metaData: ZRX_META_DATA, +}; +const META_DATA_MAP: ObjectMap<AssetMetaData> = { + [ZRX_ASSET_DATA]: ZRX_META_DATA, +}; + +describe('assetDataUtil', () => { + describe('bestNameForAsset', () => { + it('should return default string if assetData is undefined', () => { + expect(assetUtils.bestNameForAsset(undefined, 'xyz')).toEqual('xyz'); + }); + it('should return ZRX for ZRX assetData', () => { + expect(assetUtils.bestNameForAsset(ZRX_ASSET, 'mah default')).toEqual('ZRX'); + }); + }); + describe('getMetaDataOrThrow', () => { + it('should return the metaData for the supplied mainnet asset data', () => { + expect(assetUtils.getMetaDataOrThrow(ZRX_ASSET_DATA, META_DATA_MAP, Network.Mainnet)).toEqual( + ZRX_META_DATA, + ); + }); + it('should return the metaData for the supplied non-mainnet asset data', () => { + expect(assetUtils.getMetaDataOrThrow(ZRX_ASSET_DATA_KOVAN, META_DATA_MAP, Network.Kovan)).toEqual( + ZRX_META_DATA, + ); + }); + it('should throw if the metaData for the asset is not available', () => { + expect(() => + assetUtils.getMetaDataOrThrow('asset data we dont have', META_DATA_MAP, Network.Mainnet), + ).toThrowError(ZeroExInstantError.AssetMetaDataNotAvailable); + }); + }); +}); diff --git a/packages/instant/tslint.json b/packages/instant/tslint.json index abe874a6d..08b76be97 100644 --- a/packages/instant/tslint.json +++ b/packages/instant/tslint.json @@ -2,6 +2,7 @@ "extends": ["@0x/tslint-config"], "rules": { "custom-no-magic-numbers": false, - "semicolon": [true, "always", "ignore-bound-class-methods"] + "semicolon": [true, "always", "ignore-bound-class-methods"], + "max-classes-per-file": false } } diff --git a/packages/json-schemas/CHANGELOG.json b/packages/json-schemas/CHANGELOG.json index 8a88df6e2..3bd6552e3 100644 --- a/packages/json-schemas/CHANGELOG.json +++ b/packages/json-schemas/CHANGELOG.json @@ -1,5 +1,15 @@ [ { + "version": "2.0.1", + "changes": [ + { + "note": + "Improve schemas by enforcing that amounts that must be whole numbers (e.g Order asset amounts) no longer allow decimal amounts", + "pr": 1173 + } + ] + }, + { "version": "2.0.0", "changes": [ { diff --git a/packages/json-schemas/schemas/order_cancel_schema.json b/packages/json-schemas/schemas/order_cancel_schema.json index 09d4068c6..8d0999941 100644 --- a/packages/json-schemas/schemas/order_cancel_schema.json +++ b/packages/json-schemas/schemas/order_cancel_schema.json @@ -4,7 +4,7 @@ "items": { "properties": { "order": { "$ref": "/orderSchema" }, - "takerTokenCancelAmount": { "$ref": "/numberSchema" } + "takerTokenCancelAmount": { "$ref": "/wholeNumberSchema" } }, "required": ["order", "takerTokenCancelAmount"], "type": "object" diff --git a/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json b/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json index c9549c72e..73bbf20bb 100644 --- a/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json +++ b/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json @@ -4,7 +4,7 @@ "items": { "properties": { "signedOrder": { "$ref": "/signedOrderSchema" }, - "fillTakerAmount": { "$ref": "/numberSchema" } + "fillTakerAmount": { "$ref": "/wholeNumberSchema" } }, "required": ["signedOrder", "fillTakerAmount"], "type": "object" diff --git a/packages/json-schemas/schemas/order_fill_requests_schema.json b/packages/json-schemas/schemas/order_fill_requests_schema.json index 98325653b..d06fb19a2 100644 --- a/packages/json-schemas/schemas/order_fill_requests_schema.json +++ b/packages/json-schemas/schemas/order_fill_requests_schema.json @@ -4,7 +4,7 @@ "items": { "properties": { "signedOrder": { "$ref": "/signedOrderSchema" }, - "takerTokenFillAmount": { "$ref": "/numberSchema" } + "takerTokenFillAmount": { "$ref": "/wholeNumberSchema" } }, "required": ["signedOrder", "takerTokenFillAmount"], "type": "object" diff --git a/packages/json-schemas/schemas/order_schema.json b/packages/json-schemas/schemas/order_schema.json index de2c8c7c2..c70b9e2dd 100644 --- a/packages/json-schemas/schemas/order_schema.json +++ b/packages/json-schemas/schemas/order_schema.json @@ -3,17 +3,17 @@ "properties": { "makerAddress": { "$ref": "/addressSchema" }, "takerAddress": { "$ref": "/addressSchema" }, - "makerFee": { "$ref": "/numberSchema" }, - "takerFee": { "$ref": "/numberSchema" }, + "makerFee": { "$ref": "/wholeNumberSchema" }, + "takerFee": { "$ref": "/wholeNumberSchema" }, "senderAddress": { "$ref": "/addressSchema" }, - "makerAssetAmount": { "$ref": "/numberSchema" }, - "takerAssetAmount": { "$ref": "/numberSchema" }, + "makerAssetAmount": { "$ref": "/wholeNumberSchema" }, + "takerAssetAmount": { "$ref": "/wholeNumberSchema" }, "makerAssetData": { "$ref": "/hexSchema" }, "takerAssetData": { "$ref": "/hexSchema" }, - "salt": { "$ref": "/numberSchema" }, + "salt": { "$ref": "/wholeNumberSchema" }, "exchangeAddress": { "$ref": "/addressSchema" }, "feeRecipientAddress": { "$ref": "/addressSchema" }, - "expirationTimeSeconds": { "$ref": "/numberSchema" } + "expirationTimeSeconds": { "$ref": "/wholeNumberSchema" } }, "required": [ "makerAddress", diff --git a/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json b/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json index 0ab9b444f..e0f274c5d 100644 --- a/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json +++ b/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json @@ -3,8 +3,8 @@ "type": "object", "properties": { "assetData": { "$ref": "/hexSchema" }, - "minAmount": { "$ref": "/numberSchema" }, - "maxAmount": { "$ref": "/numberSchema" }, + "minAmount": { "$ref": "/wholeNumberSchema" }, + "maxAmount": { "$ref": "/wholeNumberSchema" }, "precision": { "type": "number" } }, "required": ["assetData"] diff --git a/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json b/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json index bc95b61ef..f4583fc62 100644 --- a/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json +++ b/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json @@ -4,12 +4,12 @@ "properties": { "makerAddress": { "$ref": "/addressSchema" }, "takerAddress": { "$ref": "/addressSchema" }, - "makerAssetAmount": { "$ref": "/numberSchema" }, - "takerAssetAmount": { "$ref": "/numberSchema" }, + "makerAssetAmount": { "$ref": "/wholeNumberSchema" }, + "takerAssetAmount": { "$ref": "/wholeNumberSchema" }, "makerAssetData": { "$ref": "/hexSchema" }, "takerAssetData": { "$ref": "/hexSchema" }, "exchangeAddress": { "$ref": "/addressSchema" }, - "expirationTimeSeconds": { "$ref": "/numberSchema" } + "expirationTimeSeconds": { "$ref": "/wholeNumberSchema" } }, "required": [ "makerAddress", diff --git a/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json b/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json index 0742febdf..8193861e1 100644 --- a/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json +++ b/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json @@ -2,8 +2,8 @@ "id": "/relayerApiOrderConfigResponseSchema", "type": "object", "properties": { - "makerFee": { "$ref": "/numberSchema" }, - "takerFee": { "$ref": "/numberSchema" }, + "makerFee": { "$ref": "/wholeNumberSchema" }, + "takerFee": { "$ref": "/wholeNumberSchema" }, "feeRecipientAddress": { "$ref": "/addressSchema" }, "senderAddress": { "$ref": "/addressSchema" } }, diff --git a/packages/json-schemas/schemas/whole_number_schema.json b/packages/json-schemas/schemas/whole_number_schema.json new file mode 100644 index 000000000..944ce820e --- /dev/null +++ b/packages/json-schemas/schemas/whole_number_schema.json @@ -0,0 +1,5 @@ +{ + "id": "/wholeNumberSchema", + "type": "string", + "pattern": "^\\d+$" +} diff --git a/packages/json-schemas/schemas/zero_ex_transaction_schema.json b/packages/json-schemas/schemas/zero_ex_transaction_schema.json index ad3b11583..0c714f14d 100644 --- a/packages/json-schemas/schemas/zero_ex_transaction_schema.json +++ b/packages/json-schemas/schemas/zero_ex_transaction_schema.json @@ -3,7 +3,7 @@ "properties": { "data": { "$ref": "/hexSchema" }, "signerAddress": { "$ref": "/addressSchema" }, - "salt": { "$ref": "/numberSchema" } + "salt": { "$ref": "/wholeNumberSchema" } }, "required": ["data", "salt", "signerAddress"], "type": "object" diff --git a/packages/json-schemas/src/schemas.ts b/packages/json-schemas/src/schemas.ts index 8318df617..8ece5de75 100644 --- a/packages/json-schemas/src/schemas.ts +++ b/packages/json-schemas/src/schemas.ts @@ -34,6 +34,7 @@ import * as signedOrderSchema from '../schemas/signed_order_schema.json'; import * as signedOrdersSchema from '../schemas/signed_orders_schema.json'; import * as tokenSchema from '../schemas/token_schema.json'; import * as txDataSchema from '../schemas/tx_data_schema.json'; +import * as wholeNumberSchema from '../schemas/whole_number_schema.json'; import * as zeroExTransactionSchema from '../schemas/zero_ex_transaction_schema.json'; export const schemas = { @@ -74,4 +75,5 @@ export const schemas = { relayerApiOrdersResponseSchema, relayerApiAssetDataPairsSchema, zeroExTransactionSchema, + wholeNumberSchema, }; diff --git a/packages/json-schemas/test/schema_test.ts b/packages/json-schemas/test/schema_test.ts index 795261ef2..bfa2c5668 100644 --- a/packages/json-schemas/test/schema_test.ts +++ b/packages/json-schemas/test/schema_test.ts @@ -36,6 +36,7 @@ const { relayerApiOrdersChannelUpdateSchema, relayerApiOrdersResponseSchema, relayerApiOrderSchema, + wholeNumberSchema, } = schemas; describe('Schema', () => { @@ -73,6 +74,17 @@ describe('Schema', () => { validateAgainstSchema(testCases, numberSchema, shouldFail); }); }); + describe('#wholeNumberSchema', () => { + it('should validate valid numbers', () => { + const testCases = ['5', '42', '0']; + validateAgainstSchema(testCases, wholeNumberSchema); + }); + it('should fail for invalid numbers', () => { + const testCases = ['1.3', '0.2', '00.00', '.3', '1.', 'abacaba', 'и', '1..0']; + const shouldFail = true; + validateAgainstSchema(testCases, wholeNumberSchema, shouldFail); + }); + }); describe('#addressSchema', () => { it('should validate valid addresses', () => { const testCases = ['0x8b0292b11a196601ed2ce54b665cafeca0347d42', NULL_ADDRESS]; diff --git a/packages/json-schemas/tsconfig.json b/packages/json-schemas/tsconfig.json index 425dfcfe1..7b14166c0 100644 --- a/packages/json-schemas/tsconfig.json +++ b/packages/json-schemas/tsconfig.json @@ -43,6 +43,7 @@ "./schemas/js_number.json", "./schemas/zero_ex_transaction_schema.json", "./schemas/tx_data_schema.json", - "./schemas/index_filter_values_schema.json" + "./schemas/index_filter_values_schema.json", + "./schemas/whole_number_schema.json" ] } diff --git a/packages/monorepo-scripts/CHANGELOG.json b/packages/monorepo-scripts/CHANGELOG.json index 4797fd929..170a97a33 100644 --- a/packages/monorepo-scripts/CHANGELOG.json +++ b/packages/monorepo-scripts/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Add AssetBuyerError to the IGNORED_EXCESSIVE_TYPES array", "pr": 1139 + }, + { + "note": "Add ForwarderError to the IGNORED_EXCESSIVE_TYPES array", + "pr": 1147 } ] }, diff --git a/packages/monorepo-scripts/src/doc_gen_configs.ts b/packages/monorepo-scripts/src/doc_gen_configs.ts index 0aaf5a6a5..7a14f8664 100644 --- a/packages/monorepo-scripts/src/doc_gen_configs.ts +++ b/packages/monorepo-scripts/src/doc_gen_configs.ts @@ -56,6 +56,7 @@ export const docGenConfigs: DocGenConfigs = { 'ContractWrappersError', 'OrderError', 'AssetBuyerError', + 'ForwarderWrapperError', ], // Some libraries only export types. In those cases, we cannot check if the exported types are part of the // "exported public interface". Thus we add them here and skip those checks. diff --git a/packages/order-utils/test/asset_data_utils_test.ts b/packages/order-utils/test/asset_data_utils_test.ts new file mode 100644 index 000000000..f8b850604 --- /dev/null +++ b/packages/order-utils/test/asset_data_utils_test.ts @@ -0,0 +1,31 @@ +import * as chai from 'chai'; + +import { ERC20AssetData } from '@0x/types'; + +import { assetDataUtils } from '../src/asset_data_utils'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +const KNOWN_ENCODINGS = [ + { + address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', + assetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48', + }, +]; + +const ERC20_ASSET_PROXY_ID = '0xf47261b0'; + +describe('assetDataUtils', () => { + it('should encode', () => { + const assetData = assetDataUtils.encodeERC20AssetData(KNOWN_ENCODINGS[0].address); + expect(assetData).to.equal(KNOWN_ENCODINGS[0].assetData); + }); + it('should decode', () => { + const assetData: ERC20AssetData = assetDataUtils.decodeERC20AssetData(KNOWN_ENCODINGS[0].assetData); + expect(assetData.tokenAddress).to.equal(KNOWN_ENCODINGS[0].address); + expect(assetData.assetProxyId).to.equal(ERC20_ASSET_PROXY_ID); + }); +}); diff --git a/packages/react-docs/src/components/doc_reference.tsx b/packages/react-docs/src/components/doc_reference.tsx index 00b1286a4..85547576b 100644 --- a/packages/react-docs/src/components/doc_reference.tsx +++ b/packages/react-docs/src/components/doc_reference.tsx @@ -52,12 +52,6 @@ export interface DocReferenceProps { export interface DocReferenceState {} export class DocReference extends React.Component<DocReferenceProps, DocReferenceState> { - public componentDidMount(): void { - window.addEventListener('hashchange', this._onHashChanged.bind(this), false); - } - public componentWillUnmount(): void { - window.removeEventListener('hashchange', this._onHashChanged.bind(this), false); - } public componentDidUpdate(prevProps: DocReferenceProps, _prevState: DocReferenceState): void { if (!_.isEqual(prevProps.docAgnosticFormat, this.props.docAgnosticFormat)) { const hash = window.location.hash.slice(1); @@ -323,8 +317,4 @@ export class DocReference extends React.Component<DocReferenceProps, DocReferenc /> ); } - private _onHashChanged(_event: any): void { - const hash = window.location.hash.slice(1); - sharedUtils.scrollToHash(hash, sharedConstants.SCROLL_CONTAINER_ID); - } } diff --git a/packages/react-shared/src/components/link.tsx b/packages/react-shared/src/components/link.tsx index 5a456109b..089e6e2ba 100644 --- a/packages/react-shared/src/components/link.tsx +++ b/packages/react-shared/src/components/link.tsx @@ -7,7 +7,7 @@ import * as validUrl from 'valid-url'; import { LinkType } from '../types'; import { constants } from '../utils/constants'; -interface LinkProps { +interface BaseLinkProps { to: string; shouldOpenInNewTab?: boolean; className?: string; @@ -18,6 +18,12 @@ interface LinkProps { fontColor?: string; } +interface ScrollLinkProps extends BaseLinkProps { + onActivityChanged?: (isActive: boolean) => void; +} + +type LinkProps = BaseLinkProps & ScrollLinkProps; + export interface LinkState {} /** @@ -103,11 +109,14 @@ export class Link extends React.Component<LinkProps, LinkState> { <ScrollLink to={this.props.to} offset={0} + spy={true} hashSpy={true} duration={constants.DOCS_SCROLL_DURATION_MS} containerId={constants.SCROLL_CONTAINER_ID} className={this.props.className} style={styleWithDefault} + onSetActive={this._onActivityChanged.bind(this, true)} + onSetInactive={this._onActivityChanged.bind(this, false)} > <span onClick={this._onClickPropagateClickEventAroundScrollLink.bind(this)}> {this.props.children} @@ -119,6 +128,11 @@ export class Link extends React.Component<LinkProps, LinkState> { throw new Error(`Unrecognized LinkType: ${type}`); } } + private _onActivityChanged(isActive: boolean): void { + if (this.props.onActivityChanged) { + this.props.onActivityChanged(isActive); + } + } // HACK(fabio): For some reason, the react-scroll link decided to stop the propagation of click events. // We do however rely on these events being propagated in certain scenarios (e.g when the link // is within a dropdown we want to close upon being clicked). Because of this, we register the diff --git a/packages/react-shared/src/components/nested_sidebar_menu.tsx b/packages/react-shared/src/components/nested_sidebar_menu.tsx deleted file mode 100644 index 196c91af1..000000000 --- a/packages/react-shared/src/components/nested_sidebar_menu.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { ObjectMap } from '@0x/types'; -import * as _ from 'lodash'; -import MenuItem from 'material-ui/MenuItem'; -import * as React from 'react'; - -import { ALink, Styles } from '../types'; -import { colors } from '../utils/colors'; -import { utils } from '../utils/utils'; - -import { Link } from './link'; - -export interface NestedSidebarMenuProps { - sectionNameToLinks: ObjectMap<ALink[]>; - sidebarHeader?: React.ReactNode; - shouldReformatMenuItemNames?: boolean; -} - -export interface NestedSidebarMenuState { - scrolledToId?: string; -} - -const styles: Styles = { - menuItem: { - minHeight: 0, - paddingLeft: 8, - borderRadius: 6, - }, - menuItemInnerDiv: { - color: colors.grey800, - fontSize: 14, - lineHeight: 2, - padding: 0, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, -}; - -export class NestedSidebarMenu extends React.Component<NestedSidebarMenuProps, NestedSidebarMenuState> { - public static defaultProps: Partial<NestedSidebarMenuProps> = { - shouldReformatMenuItemNames: true, - }; - private _urlIntervalCheckId: number | undefined = undefined; - constructor(props: NestedSidebarMenuProps) { - super(props); - this.state = {}; - } - public componentDidMount(): void { - this._urlIntervalCheckId = window.setInterval(() => { - const scrollId = location.hash.slice(1); - if (scrollId !== this.state.scrolledToId) { - this.setState({ - scrolledToId: scrollId, - }); - } - }, 200); - } - public componentWillUnmount(): void { - window.clearInterval(this._urlIntervalCheckId); - } - public render(): React.ReactNode { - const navigation = _.map(this.props.sectionNameToLinks, (links: ALink[], sectionName: string) => { - const finalSectionName = utils.convertCamelCaseToSpaces(sectionName); - // tslint:disable-next-line:no-unused-variable - return ( - <div key={`section-${sectionName}`} className="py1" style={{ color: colors.greyTheme }}> - <div style={{ fontSize: 14, letterSpacing: 0.5 }} className="py1 pl1"> - {finalSectionName.toUpperCase()} - </div> - {this._renderMenuItems(links)} - </div> - ); - }); - return ( - <div> - {this.props.sidebarHeader} - <div>{navigation}</div> - </div> - ); - } - private _renderMenuItems(links: ALink[]): React.ReactNode[] { - const scrolledToId = this.state.scrolledToId; - const menuItems = _.map(links, link => { - const finalMenuItemName = this.props.shouldReformatMenuItemNames - ? utils.convertDashesToSpaces(link.title) - : link.title; - let menuItemStyle = styles.menuItem; - let menuItemInnerDivStyle = styles.menuItemInnerDiv; - const isScrolledTo = link.to === scrolledToId; - if (isScrolledTo) { - menuItemStyle = { - ...menuItemStyle, - backgroundColor: colors.lightLinkBlue, - }; - menuItemInnerDivStyle = { - ...menuItemInnerDivStyle, - color: colors.white, - fontWeight: 'bold', - }; - } - return ( - <div key={`menuItem-${finalMenuItemName}`}> - <Link to={link.to} shouldOpenInNewTab={link.shouldOpenInNewTab}> - <MenuItem style={menuItemStyle} innerDivStyle={menuItemInnerDivStyle}> - <span - style={{ - textTransform: this.props.shouldReformatMenuItemNames ? 'capitalize' : 'none', - }} - > - {finalMenuItemName} - </span> - </MenuItem> - </Link> - </div> - ); - }); - return menuItems; - } -} diff --git a/packages/react-shared/src/index.ts b/packages/react-shared/src/index.ts index e33b09f19..a693f2a36 100644 --- a/packages/react-shared/src/index.ts +++ b/packages/react-shared/src/index.ts @@ -2,7 +2,6 @@ export { AnchorTitle } from './components/anchor_title'; export { MarkdownLinkBlock } from './components/markdown_link_block'; export { MarkdownCodeBlock } from './components/markdown_code_block'; export { MarkdownSection } from './components/markdown_section'; -export { NestedSidebarMenu } from './components/nested_sidebar_menu'; export { SectionHeader } from './components/section_header'; export { Link } from './components/link'; diff --git a/packages/web3-wrapper/CHANGELOG.json b/packages/web3-wrapper/CHANGELOG.json index b8d06eac1..6b554110f 100644 --- a/packages/web3-wrapper/CHANGELOG.json +++ b/packages/web3-wrapper/CHANGELOG.json @@ -1,5 +1,15 @@ [ { + "version": "3.1.1", + "changes": [ + { + "note": + "Fix bug in `getTransactionByHashAsync` which was causing the return value to have the wrong type (raw fields instead of unmarshalled fields).", + "pr": 1177 + } + ] + }, + { "version": "3.1.0", "changes": [ { diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts index 3ba153680..56877fef3 100644 --- a/packages/web3-wrapper/src/web3_wrapper.ts +++ b/packages/web3-wrapper/src/web3_wrapper.ts @@ -23,7 +23,13 @@ import { import * as _ from 'lodash'; import { marshaller } from './marshaller'; -import { BlockWithoutTransactionDataRPC, BlockWithTransactionDataRPC, NodeType, Web3WrapperErrors } from './types'; +import { + BlockWithoutTransactionDataRPC, + BlockWithTransactionDataRPC, + NodeType, + TransactionRPC, + Web3WrapperErrors, +} from './types'; import { utils } from './utils'; const BASE_TEN = 10; @@ -228,10 +234,11 @@ export class Web3Wrapper { */ public async getTransactionByHashAsync(txHash: string): Promise<Transaction> { assert.isHexString('txHash', txHash); - const transaction = await this.sendRawPayloadAsync<Transaction>({ + const transactionRpc = await this.sendRawPayloadAsync<TransactionRPC>({ method: 'eth_getTransactionByHash', params: [txHash], }); + const transaction = marshaller.unmarshalTransaction(transactionRpc); return transaction; } /** diff --git a/packages/website/md/docs/asset_buyer/installation.md b/packages/website/md/docs/asset_buyer/installation.md index 76affaa09..3c0c95068 100644 --- a/packages/website/md/docs/asset_buyer/installation.md +++ b/packages/website/md/docs/asset_buyer/installation.md @@ -1,10 +1,10 @@ -**Install** +#### Install ```bash yarn add @0x/asset-buyer ``` -**Import** +#### Import ```javascript import { AssetBuyer } from '@0x/asset-buyer'; diff --git a/packages/website/md/docs/asset_buyer/usage.md b/packages/website/md/docs/asset_buyer/usage.md index 6462d938e..209c15062 100644 --- a/packages/website/md/docs/asset_buyer/usage.md +++ b/packages/website/md/docs/asset_buyer/usage.md @@ -1,4 +1,4 @@ -**Construction** +#### Construction Connecting to a standard relayer API compliant url: @@ -16,7 +16,7 @@ const orders = []; // get these from your own API, a relayer, a friend, from any const assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, orders); ``` -**Get a quote** +#### Get a quote A [BuyQuote](#types-BuyQuote) object contains enough information to display buy information to an end user @@ -30,7 +30,7 @@ console.log(quoteInfo.feeAmount); // a portion of the total ethAmount above that console.log(quoteInfo.ethPerAssetPrice); // the rate that this quote provides (e.g. 0.0035ETH / REP) ``` -**Perform a buy** +#### Perform a buy Pass the [BuyQuote](#types-BuyQuote) object from above back to the assetBuyer in order to initiate the buy transaction diff --git a/packages/website/md/docs/json_schemas/2/schemas.md b/packages/website/md/docs/json_schemas/2/schemas.md index ec7cf6f15..8ea6c8e85 100644 --- a/packages/website/md/docs/json_schemas/2/schemas.md +++ b/packages/website/md/docs/json_schemas/2/schemas.md @@ -1,40 +1,40 @@ Basic Schemas -* [Address type](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/address.json) -* [Number type](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/number.json) -* [Hex type](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/hex.json) -* [JS number](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/js_number.json) +* [Address type](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/address_schema.json) +* [Number type](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/number_schema.json) +* [Hex type](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/hex_schema.json) +* [JS number](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/js_number.json) 0x Protocol Schemas -* [Order](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_schema.json) -* [SignedOrder](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/signed_order_schema.json) -* [OrderHash](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_hash_schema.json) -* [ECSignature](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/ec_signature_schema.json) +* [Order](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_schema.json) +* [SignedOrder](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/signed_order_schema.json) +* [OrderHash](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_hash_schema.json) +* [ECSignature](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/ec_signature_schema.json) 0x.js Schemas -* [BlockParam](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/block_param_schema.json) -* [BlockRange](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/block_range_schema.json) -* [IndexFilter Values](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/index_filter_values_schema.json) -* [OrderFillRequests](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_fill_requests_schema.json) -* [OrderCancellationRequests](https://github.com/0xProjet/0x-monorepo/blob/development/packages/json-schemas/schemas/order_cancel_schema.json) -* [OrderFillOrKillRequests](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json) -* [SignedOrders](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/signed_orders_schema.json) -* [Token](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/token_schema.json) -* [TxData](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/tx_data_schema.json) +* [BlockParam](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/block_param_schema.json) +* [BlockRange](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/block_range_schema.json) +* [IndexFilter Values](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/index_filter_values_schema.json) +* [OrderFillRequests](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_fill_requests_schema.json) +* [OrderCancellationRequests](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_cancel_schema.json) +* [OrderFillOrKillRequests](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json) +* [SignedOrders](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/signed_orders_schema.json) +* [Token](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/token_schema.json) +* [TxData](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/tx_data_schema.json) Standard Relayer API Schemas -* [Paginated collection](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/paginated_collection_schema.json) -* [Error response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_error_response_schema.json) -* [Order config payload](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json) -* [Order config response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json) -* [Orders channel subscribe payload](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_payload_schema.json) -* [Orders channel subscribe](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_schema.json) -* [Orders channel update](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_orders_channel_update_response_schema.json) -* [Orderbook response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.json) -* [Asset pairs](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_asset_pairs_schema.json) -* [Trade info](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_asset_trade_info_schema.json) -* [Asset pairs response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_asset_pairs_response_schema.json) -* [Fee recipients response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_fee_recipients_response_schema.json) +* [Paginated collection](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/paginated_collection_schema.json) +* [Error response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_error_response_schema.json) +* [Order config payload](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json) +* [Order config response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json) +* [Orders channel subscribe payload](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_payload_schema.json) +* [Orders channel subscribe](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_schema.json) +* [Orders channel update](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_orders_channel_update_response_schema.json) +* [Orderbook response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.json) +* [Asset pairs](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_asset_data_pairs_schema.json) +* [Trade info](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json) +* [Asset pairs response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_asset_data_pairs_response_schema.json) +* [Fee recipients response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_fee_recipients_response_schema.json) diff --git a/packages/website/md/docs/json_schemas/3/schemas.md b/packages/website/md/docs/json_schemas/3/schemas.md new file mode 100644 index 000000000..99aff7b5a --- /dev/null +++ b/packages/website/md/docs/json_schemas/3/schemas.md @@ -0,0 +1,41 @@ +Basic Schemas + +* [Address type](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/address_schema.json) +* [Number type](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/number_schema.json) +* [Whole number type](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/whole_number_schema.json) +* [Hex type](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/hex_schema.json) +* [JS number](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/js_number.json) + +0x Protocol Schemas + +* [Order](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_schema.json) +* [SignedOrder](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/signed_order_schema.json) +* [OrderHash](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_hash_schema.json) +* [ECSignature](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/ec_signature_schema.json) + +0x.js Schemas + +* [BlockParam](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/block_param_schema.json) +* [BlockRange](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/block_range_schema.json) +* [IndexFilter Values](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/index_filter_values_schema.json) +* [OrderFillRequests](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_fill_requests_schema.json) +* [OrderCancellationRequests](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_cancel_schema.json) +* [OrderFillOrKillRequests](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json) +* [SignedOrders](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/signed_orders_schema.json) +* [Token](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/token_schema.json) +* [TxData](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/tx_data_schema.json) + +Standard Relayer API Schemas + +* [Paginated collection](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/paginated_collection_schema.json) +* [Error response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_error_response_schema.json) +* [Order config payload](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json) +* [Order config response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json) +* [Orders channel subscribe payload](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_payload_schema.json) +* [Orders channel subscribe](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_schema.json) +* [Orders channel update](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_orders_channel_update_response_schema.json) +* [Orderbook response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.json) +* [Asset pairs](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_asset_data_pairs_schema.json) +* [Trade info](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json) +* [Asset pairs response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_asset_data_pairs_response_schema.json) +* [Fee recipients response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_fee_recipients_response_schema.json) diff --git a/packages/website/public/images/landing/0x_homepage.svg b/packages/website/public/images/landing/0x_homepage.svg index 061ac8939..d2ccef8f4 100644 --- a/packages/website/public/images/landing/0x_homepage.svg +++ b/packages/website/public/images/landing/0x_homepage.svg @@ -1,24 +1,18 @@ <svg width="410" height="254" viewBox="0 0 410 254" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect width="410" height="254" fill="black" fill-opacity="0"/> -<rect width="410" height="254" fill="black" fill-opacity="0"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M409.979 122.524C410.195 134.531 408.758 137.636 404.251 140.251L210.864 252.464C207.334 254.512 202.985 254.512 199.455 252.464L6.06657 140.251C1.55911 137.635 -1.2295 135.189 0.536218 121.797L409.979 122.524Z" fill="#D0CAFF"/> -<rect width="98.1672" height="56.8942" fill="black" fill-opacity="0" transform="translate(87.1703 125.679)"/> <path d="M178.097 125.679L87.1703 178.372" stroke="#C4AED9" stroke-width="1.32046" stroke-linecap="round"/> <path d="M181.659 127.746L90.732 180.439" stroke="#C4AED9" stroke-width="1.32046" stroke-linecap="round"/> <path d="M185.338 129.88L94.41 182.573" stroke="#C4AED9" stroke-width="1.32046" stroke-linecap="round"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M199.455 245.823L6.06657 133.61C-1.53963 129.196 -1.53963 118.163 6.06657 113.749L199.453 1.53604C202.984 -0.512014 207.333 -0.512014 210.863 1.53604L404.251 113.75C411.858 118.163 411.858 129.197 404.251 133.611L210.864 245.823C207.334 247.871 202.985 247.871 199.455 245.823Z" fill="#F0EEFF"/> -<rect width="107.203" height="36.2243" fill="black" fill-opacity="0" transform="translate(79.7358 74.3805)"/> <path d="M79.7358 107.066L133.382 75.8612C136.304 73.965 139.326 73.974 141.767 75.4102L186.939 101.973" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> <path d="M82.4662 108.801L134.79 78.5156C136.798 77.3507 139.273 77.3583 141.274 78.5356L184.001 103.681" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> <path d="M85.1381 110.605L134.534 82.5144C136.541 81.3495 139.017 81.3571 141.017 82.5348L180.403 105.714" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> -<rect width="100.727" height="37.9813" fill="black" fill-opacity="0" transform="translate(226.643 123.734)"/> -<path d="M327.37 126.923L270.289 160.234C267.367 162.131 264.345 162.121 261.903 160.685L226.643 140.288" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> -<path d="M323.336 125.886L268.881 157.58C266.873 158.745 264.398 158.737 262.397 157.56L229.58 138.58" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> -<path d="M319.925 123.734L269.137 153.581C267.129 154.746 264.654 154.739 262.653 153.561L233.178 136.547" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M322.112 124.6C313.477 119.589 313.477 111.465 322.112 106.454C330.747 101.444 344.748 101.444 353.383 106.454C362.018 111.465 362.018 119.589 353.383 124.6C344.748 129.611 330.747 129.611 322.112 124.6Z" stroke="#DBDBFF" stroke-width="2"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M50.8048 126.749C42.1698 121.739 42.1698 113.614 50.8048 108.604C59.4403 103.593 73.4409 103.593 82.0764 108.604C90.7115 113.614 90.7115 121.739 82.0764 126.749C73.4409 131.76 59.4403 131.76 50.8048 126.749Z" stroke="#DBDBFF" stroke-width="2"/> +<path d="M330.5 124L270.289 160.234C267.367 162.131 264.345 162.121 261.903 160.685L226.643 140.288" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> +<path d="M325.5 123.5L268.881 157.58C266.873 158.745 264.398 158.737 262.397 157.56L229.58 138.58" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> +<path d="M322.5 122L269.137 153.581C267.129 154.746 264.654 154.739 262.653 153.561L233.178 136.547" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M323.711 121.778C316.096 117.482 316.096 110.517 323.711 106.222C331.327 101.926 343.673 101.926 351.289 106.222C358.904 110.517 358.904 117.482 351.289 121.778C343.673 126.074 331.327 126.074 323.711 121.778Z" stroke="#DBDBFF" stroke-width="2"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M52.7112 125.924C45.0963 121.824 45.0963 115.176 52.7112 111.075C60.3266 106.975 72.6734 106.975 80.2888 111.075C87.9038 115.176 87.9038 121.824 80.2888 125.924C72.6734 130.025 60.3266 130.025 52.7112 125.924Z" stroke="#DBDBFF" stroke-width="2"/> <g opacity="0.372566"> -<rect width="34.2587" height="23.7724" fill="black" fill-opacity="0" transform="translate(169.644 179.583)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M171.171 194.759L170.656 194.461C170.619 194.439 170.596 194.399 170.596 194.357V192.616C170.596 192.524 170.695 192.466 170.775 192.513L171.29 192.811C171.327 192.833 171.35 192.872 171.35 192.915V194.655C171.35 194.748 171.251 194.805 171.171 194.759Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M172.622 195.59L172.107 195.291C172.07 195.269 172.047 195.23 172.047 195.187V193.447C172.047 193.354 172.146 193.297 172.226 193.343L172.741 193.642C172.778 193.663 172.801 193.703 172.801 193.745V195.486C172.801 195.578 172.702 195.635 172.622 195.59Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M174.073 196.42L173.558 196.122C173.521 196.1 173.499 196.06 173.499 196.018V194.277C173.499 194.185 173.598 194.127 173.677 194.174L174.192 194.472C174.23 194.494 174.252 194.533 174.252 194.576V196.316C174.252 196.409 174.153 196.466 174.073 196.42Z" fill="#8C87C4"/> @@ -56,7 +50,6 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M288.178 149.398C288.178 152.004 286.076 154.116 283.483 154.116C280.891 154.116 278.789 152.004 278.789 149.398C278.789 146.792 280.891 144.68 283.483 144.68C286.076 144.68 288.178 146.792 288.178 149.398Z" fill="#8CB6FF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M302.505 141.805C302.505 144.411 300.403 146.523 297.811 146.523C295.218 146.523 293.116 144.411 293.116 141.805C293.116 139.199 295.218 137.087 297.811 137.087C300.403 137.087 302.505 139.199 302.505 141.805Z" fill="#8CB6FF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M315.374 133.208C315.374 135.813 313.272 137.926 310.679 137.926C308.086 137.926 305.984 135.813 305.984 133.208C305.984 130.602 308.086 128.489 310.679 128.489C313.272 128.489 315.374 130.602 315.374 133.208Z" fill="#8CB6FF"/> -<rect width="94.7557" height="54.8453" fill="black" fill-opacity="0" transform="translate(89.638 130.896)"/> <path d="M177.154 130.896L89.638 182.735" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> <path d="M180.715 132.963L93.2464 184.097" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> <path d="M184.394 135.097L96.4554 185.742" stroke="#DBDBFF" stroke-width="1.32046" stroke-linecap="round"/> @@ -89,7 +82,6 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M230.792 106.981C229.705 104.573 227.673 102.272 224.657 100.277C220.492 97.5232 215.09 95.7649 209.349 95.0321C206.508 94.6693 203.586 94.5587 200.688 94.7034C200.256 96.2311 199.844 97.8272 199.468 99.4797C199.366 99.9231 199.271 100.376 199.175 100.826C202.597 100.386 206.182 100.522 209.476 101.217L209.48 101.218L209.558 104.945L230.792 106.981Z" fill="white"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M210.774 127.184C214.812 126.542 218.668 125.381 222.022 123.674C226.764 121.26 229.793 118.129 231.057 114.802C231.686 113.147 231.878 111.444 231.626 109.757C228.994 109.506 226.244 109.268 223.397 109.051C222.632 108.992 221.851 108.936 221.073 108.881C221.837 110.871 221.602 112.957 220.398 114.873L220.371 114.912L213.699 114.981L210.774 127.184Z" fill="white"/> <g opacity="0.372566"> -<rect width="57.1535" height="39.6589" fill="black" fill-opacity="0" transform="translate(180.849 191.194)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M183.395 216.512L182.537 216.014C182.475 215.978 182.437 215.912 182.437 215.842V212.937C182.437 212.784 182.603 212.687 182.735 212.764L183.593 213.262C183.655 213.298 183.693 213.364 183.693 213.435V216.34C183.693 216.493 183.527 216.589 183.395 216.512Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M185.816 217.898L184.958 217.399C184.896 217.364 184.858 217.298 184.858 217.227V214.323C184.858 214.169 185.024 214.073 185.156 214.15L186.014 214.648C186.076 214.683 186.114 214.75 186.114 214.821V217.725C186.114 217.879 185.948 217.974 185.816 217.898Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M188.237 219.284L187.379 218.785C187.317 218.75 187.279 218.684 187.279 218.613V215.709C187.279 215.555 187.445 215.459 187.577 215.536L188.435 216.034C188.497 216.069 188.535 216.135 188.535 216.207V219.111C188.535 219.264 188.369 219.36 188.237 219.284Z" fill="#8C87C4"/> @@ -116,7 +108,6 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M208.63 225.495L181.646 209.837C180.584 209.221 180.584 207.682 181.646 207.065L208.63 191.408C209.123 191.122 209.73 191.122 210.222 191.408L237.207 207.065C238.268 207.682 238.268 209.221 237.207 209.837L210.222 225.495C209.73 225.781 209.123 225.781 208.63 225.495Z" fill="#A59EF7"/> </g> <g opacity="0.327785"> -<rect width="73.5288" height="51.0612" fill="black" fill-opacity="0" transform="translate(200.318 30.1121)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M203.593 62.7101L202.489 62.0689C202.41 62.0228 202.361 61.9378 202.361 61.8468V58.1075C202.361 57.9094 202.574 57.7857 202.744 57.8848L203.848 58.5259C203.927 58.5721 203.976 58.657 203.976 58.7487V62.4879C203.976 62.6855 203.763 62.8085 203.593 62.7101Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M206.707 64.4942L205.603 63.8524C205.524 63.8069 205.475 63.722 205.475 63.6303V59.8916C205.475 59.6935 205.688 59.5698 205.858 59.6689L206.963 60.31C207.042 60.3556 207.09 60.4411 207.09 60.5328V64.2721C207.09 64.4696 206.878 64.5926 206.707 64.4942Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M209.822 66.2782L208.718 65.6365C208.638 65.5909 208.59 65.506 208.59 65.4143V61.6757C208.59 61.4775 208.803 61.3539 208.973 61.4529L210.077 62.0941C210.156 62.1396 210.205 62.2245 210.205 62.3168V66.0555C210.205 66.253 209.992 66.3767 209.822 66.2782Z" fill="#8C87C4"/> @@ -143,11 +134,10 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M236.058 74.2755L201.342 54.1153C199.977 53.3227 199.977 51.3408 201.342 50.547L236.058 30.388C236.692 30.0201 237.473 30.0201 238.107 30.388L272.823 50.547C274.188 51.3408 274.188 53.3227 272.823 54.1153L238.107 74.2755C237.473 74.6435 236.692 74.6435 236.058 74.2755Z" fill="#A59EF7"/> </g> <g opacity="0.120697"> -<rect width="28.3611" height="20.2117" fill="black" fill-opacity="0" transform="translate(79.9083 149.104)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M81.1767 162.008L80.746 161.754C80.7152 161.736 80.6961 161.702 80.6961 161.666V160.186C80.6961 160.107 80.7792 160.058 80.8455 160.098L81.2762 160.351C81.3072 160.37 81.3261 160.403 81.3261 160.44V161.92C81.3261 161.998 81.243 162.047 81.1767 162.008Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M82.3781 162.714L81.9474 162.46C81.9166 162.442 81.8975 162.408 81.8975 162.372V160.892C81.8975 160.814 81.9806 160.765 82.0469 160.804L82.4776 161.058C82.5084 161.076 82.5275 161.11 82.5275 161.146V162.626C82.5275 162.704 82.4444 162.753 82.3781 162.714Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M83.5795 163.42L83.1488 163.166C83.1178 163.148 83.0989 163.114 83.0989 163.078V161.598C83.0989 161.52 83.182 161.471 83.2483 161.51L83.679 161.764C83.7098 161.782 83.7288 161.815 83.7288 161.852V163.332C83.7288 163.41 83.6458 163.459 83.5795 163.42Z" fill="#8C87C4"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M84.7809 164.126L84.3502 163.872C84.3191 163.854 84.3003 163.821 84.3003 163.784V162.304C84.3003 162.226 84.3833 162.177 84.4497 162.216L84.8804 162.47C84.9111 162.488 84.9302 162.522 84.9302 162.558V164.038C84.9302 164.116 84.8472 164.165 84.7809 164.126Z" fill="#8C87C4"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M84.7808 164.126L84.3502 163.872C84.3191 163.854 84.3003 163.821 84.3003 163.784V162.304C84.3003 162.226 84.3833 162.177 84.4497 162.216L84.8803 162.47C84.9111 162.488 84.9302 162.522 84.9302 162.558V164.038C84.9302 164.116 84.8472 164.165 84.7808 164.126Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M85.9823 164.832L85.5516 164.578C85.5205 164.56 85.5017 164.527 85.5017 164.49V163.01C85.5017 162.932 85.5847 162.883 85.6511 162.922L86.0817 163.176C86.1125 163.194 86.1316 163.228 86.1316 163.264V164.744C86.1316 164.822 86.0486 164.871 85.9823 164.832Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M87.1837 165.538L86.753 165.285C86.7219 165.266 86.7031 165.233 86.7031 165.196V163.717C86.7031 163.638 86.7861 163.589 86.8525 163.628L87.2832 163.882C87.3139 163.9 87.333 163.934 87.333 163.97V165.45C87.333 165.529 87.25 165.577 87.1837 165.538Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M88.385 166.245L87.9543 165.991C87.9233 165.972 87.9045 165.939 87.9045 165.903V164.423C87.9045 164.344 87.9875 164.295 88.0538 164.335L88.4845 164.588C88.5153 164.606 88.5344 164.64 88.5344 164.676V166.157C88.5344 166.235 88.4514 166.284 88.385 166.245Z" fill="#8C87C4"/> @@ -170,7 +160,6 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M93.6938 166.586L80.3033 158.606C79.7767 158.292 79.7767 157.507 80.3033 157.193L93.6938 149.214C93.9383 149.068 94.2394 149.068 94.4839 149.214L107.874 157.193C108.401 157.507 108.401 158.292 107.874 158.606L94.4839 166.586C94.2396 166.731 93.9383 166.731 93.6938 166.586Z" fill="#A59EF7"/> </g> <g opacity="0.120697"> -<rect width="28.3611" height="20.2117" fill="black" fill-opacity="0" transform="translate(99.2107 137.283)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M100.479 150.186L100.048 149.932C100.018 149.914 99.9986 149.88 99.9986 149.844V148.364C99.9986 148.286 100.082 148.237 100.148 148.276L100.579 148.53C100.61 148.548 100.629 148.582 100.629 148.618V150.098C100.629 150.176 100.545 150.225 100.479 150.186Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M101.681 150.892L101.25 150.638C101.219 150.62 101.2 150.587 101.2 150.55V149.07C101.2 148.992 101.283 148.943 101.349 148.982L101.78 149.236C101.811 149.254 101.83 149.288 101.83 149.324V150.804C101.83 150.882 101.747 150.931 101.681 150.892Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M102.882 151.598L102.451 151.344C102.42 151.326 102.401 151.293 102.401 151.256V149.777C102.401 149.698 102.484 149.649 102.551 149.688L102.981 149.942C103.012 149.96 103.031 149.994 103.031 150.03V151.51C103.031 151.588 102.948 151.637 102.882 151.598Z" fill="#8C87C4"/> @@ -197,7 +186,6 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M112.996 154.764L99.6058 146.784C99.0791 146.47 99.0791 145.686 99.6058 145.371L112.996 137.392C113.241 137.246 113.542 137.246 113.786 137.392L127.177 145.371C127.703 145.686 127.703 146.47 127.177 146.784L113.786 154.764C113.542 154.91 113.241 154.91 112.996 154.764Z" fill="#A59EF7"/> </g> <g opacity="0.120697"> -<rect width="28.3611" height="20.2117" fill="black" fill-opacity="0" transform="translate(268.643 57.7552)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M269.912 70.6586L269.481 70.4048C269.45 70.3865 269.431 70.3529 269.431 70.3169V68.8367C269.431 68.7583 269.514 68.7093 269.581 68.7486L270.011 69.0024C270.042 69.0206 270.061 69.0542 270.061 69.0905V70.5707C270.061 70.6488 269.978 70.6975 269.912 70.6586Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M271.113 71.3648L270.683 71.1108C270.652 71.0927 270.633 71.0591 270.633 71.0228V69.5429C270.633 69.4645 270.716 69.4156 270.782 69.4548L271.213 69.7086C271.243 69.7266 271.263 69.7604 271.263 69.7967V71.2769C271.263 71.355 271.18 71.4038 271.113 71.3648Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M272.315 72.071L271.884 71.8169C271.853 71.7989 271.834 71.7653 271.834 71.729V70.2491C271.834 70.1707 271.917 70.1217 271.983 70.161L272.414 70.4147C272.445 70.4328 272.464 70.4664 272.464 70.5029V71.9828C272.464 72.061 272.381 72.1099 272.315 72.071Z" fill="#8C87C4"/> @@ -224,7 +212,6 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M282.429 75.2366L269.038 67.2565C268.512 66.9428 268.512 66.1582 269.038 65.844L282.429 57.8644C282.673 57.7188 282.974 57.7188 283.219 57.8644L296.609 65.844C297.136 66.1582 297.136 66.9428 296.609 67.2565L283.219 75.2366C282.975 75.3822 282.673 75.3822 282.429 75.2366Z" fill="#A59EF7"/> </g> <g opacity="0.327785"> -<rect width="49.9817" height="34.933" fill="black" fill-opacity="0" transform="translate(180.671 11.4867)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M182.898 33.788L182.147 33.3494C182.093 33.3176 182.06 33.2598 182.06 33.1975V30.6391C182.06 30.5041 182.205 30.4195 182.32 30.4867L183.071 30.9253C183.125 30.957 183.158 31.0149 183.158 31.0784V33.6362C183.158 33.7712 183.013 33.8552 182.898 33.788Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M185.015 35.0087L184.264 34.57C184.21 34.5383 184.177 34.4798 184.177 34.4176V31.8592C184.177 31.7248 184.322 31.6395 184.438 31.7073L185.188 32.146C185.242 32.1777 185.275 32.2356 185.275 32.2984V34.8562C185.275 34.9919 185.13 35.0759 185.015 35.0087Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M187.132 36.2287L186.381 35.7901C186.327 35.759 186.295 35.7005 186.295 35.6383V33.0798C186.295 32.9448 186.439 32.8602 186.555 32.9274L187.306 33.3667C187.359 33.3978 187.392 33.4563 187.392 33.5191V36.0769C187.392 36.2119 187.247 36.2966 187.132 36.2287Z" fill="#8C87C4"/> @@ -251,7 +238,6 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M204.966 41.7003L181.368 27.9082C180.439 27.3656 180.439 26.0092 181.368 25.4673L204.966 11.6752C205.397 11.4238 205.927 11.4238 206.358 11.6752L229.957 25.4673C230.885 26.0099 230.885 27.3656 229.957 27.9082L206.358 41.7003C205.927 41.9517 205.397 41.9517 204.966 41.7003Z" fill="#A59EF7"/> </g> <g opacity="0.327785"> -<rect width="34.9717" height="24.8959" fill="black" fill-opacity="0" transform="translate(167.802 36.5651)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M169.36 52.459L168.834 52.1463C168.797 52.1237 168.774 52.0822 168.774 52.0375V50.215C168.774 50.1181 168.875 50.0584 168.956 50.1062L169.482 50.4188C169.52 50.4415 169.543 50.4824 169.543 50.5277V52.3501C169.543 52.4464 169.442 52.5068 169.36 52.459Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M170.842 53.3282L170.316 53.0156C170.278 52.9936 170.255 52.952 170.255 52.9074V51.0849C170.255 50.988 170.357 50.9283 170.437 50.9761L170.963 51.2887C171.001 51.3108 171.024 51.3523 171.024 51.3976V53.22C171.024 53.3163 170.923 53.3767 170.842 53.3282Z" fill="#8C87C4"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M172.323 54.1982L171.797 53.8855C171.76 53.8635 171.737 53.822 171.737 53.7773V51.9542C171.737 51.8573 171.838 51.7976 171.919 51.846L172.445 52.1586C172.483 52.1807 172.505 52.2222 172.505 52.2669V54.09C172.505 54.1862 172.405 54.2466 172.323 54.1982Z" fill="#8C87C4"/> @@ -277,109 +263,78 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M167.808 47.5141L167.819 49.6951C167.747 50.0902 167.904 50.5224 168.289 50.7514L184.801 60.5809C185.102 60.7608 185.474 60.7608 185.775 60.5809L202.286 50.752C202.671 50.523 202.828 50.0902 202.757 49.6951L202.768 47.5141" fill="#8B81E6"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M184.801 58.0974L168.289 48.2686C167.64 47.8817 167.64 46.9154 168.289 46.5285L184.801 36.6996C185.102 36.5203 185.473 36.5203 185.775 36.6996L202.287 46.5285C202.936 46.9154 202.936 47.8823 202.287 48.2686L185.775 58.0974C185.474 58.2767 185.102 58.2767 184.801 58.0974Z" fill="#A59EF7"/> </g> -<rect width="29.9899" height="49.737" fill="black" fill-opacity="0" transform="translate(189.659 18.6826)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M189.659 43.6672L204.642 52.5638V18.6826L189.659 43.6672Z" fill="#6659FF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M204.672 18.6826V52.5638L219.649 43.6672L204.672 18.6826Z" fill="#5135E6"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M189.659 46.6247L204.861 68.4196L204.642 55.5213L189.659 46.6247Z" fill="#6659FF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M204.672 54.7454L204.917 67.4584L219.482 45.55L204.672 54.7454Z" fill="#4259DB"/> -<rect width="37.5326" height="24.718" fill="black" fill-opacity="0" transform="translate(318.852 99.4725)"/> -<rect width="37.5326" height="24.2005" fill="black" fill-opacity="0" transform="translate(318.852 99.4725)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M350.888 120.595C343.56 124.699 331.678 124.699 324.349 120.595C317.02 116.492 317.02 109.838 324.349 105.734C331.677 101.631 343.56 101.631 350.888 105.735C358.217 109.838 358.217 116.492 350.888 120.595Z" fill="#7F95EF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M318.852 112.846H356.385V109.662H318.852V112.846Z" fill="#838DB7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M350.888 117.411C343.56 121.515 331.678 121.515 324.349 117.411C317.02 113.307 317.02 106.654 324.349 102.55C331.677 98.4465 343.56 98.4465 350.888 102.55C358.217 106.654 358.217 113.307 350.888 117.411Z" fill="#93A8FF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M326.211 115.967C319.911 112.485 319.911 106.839 326.211 103.358C332.511 99.8757 342.726 99.8757 349.026 103.358C355.326 106.84 355.326 112.485 349.026 115.967C342.726 119.449 332.511 119.449 326.211 115.967Z" fill="#9ABFFF"/> <g opacity="0.370867"> -<rect width="19.296" height="11.091" fill="black" fill-opacity="0" transform="translate(327.431 104.017)"/> <g opacity="0.653363"> -<rect width="14.9457" height="5.18221" fill="black" fill-opacity="0" transform="translate(327.431 109.926)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M342.161 110.157C342.497 110.398 342.355 110.571 342.298 110.734C341.315 113.539 336.078 115.549 331.129 115.025C328.909 114.79 327.621 114.016 327.452 112.816C327.266 111.493 328.324 110.596 330.572 110.173C331.906 109.922 333.285 109.901 334.667 109.943C337.204 110.021 339.711 110.353 342.161 110.157Z" fill="#4562DF"/> </g> <g opacity="0.653363"> -<rect width="14.909" height="5.19137" fill="black" fill-opacity="0" transform="translate(331.818 104.017)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M331.818 108.942C332.063 106.358 336.216 104.162 340.869 104.029C341.632 104.007 342.397 104.01 343.152 104.108C345.409 104.4 346.637 105.208 346.723 106.465C346.805 107.672 345.734 108.521 343.592 108.934C342.419 109.16 341.201 109.235 339.972 109.201C337.284 109.128 334.63 108.791 331.818 108.942Z" fill="#4562DF"/> </g> </g> -<rect width="37.5326" height="24.718" fill="black" fill-opacity="0" transform="translate(318.852 95.1737)"/> -<rect width="37.5326" height="24.2005" fill="black" fill-opacity="0" transform="translate(318.852 95.1737)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M350.888 116.296C343.56 120.4 331.678 120.4 324.349 116.296C317.02 112.193 317.02 105.539 324.349 101.436C331.677 97.332 343.56 97.332 350.888 101.436C358.217 105.539 358.217 112.193 350.888 116.296Z" fill="#7F95EF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M318.852 108.548H356.385V105.363H318.852V108.548Z" fill="#838DB7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M350.888 113.112C343.56 117.216 331.678 117.216 324.349 113.112C317.02 109.008 317.02 102.355 324.349 98.2514C331.677 94.1478 343.56 94.1478 350.888 98.2515C358.217 102.355 358.217 109.009 350.888 113.112Z" fill="#93A8FF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M326.211 111.668C319.911 108.186 319.911 102.541 326.211 99.0588C332.511 95.5769 342.726 95.5769 349.026 99.0588C355.326 102.541 355.326 108.186 349.026 111.668C342.726 115.15 332.511 115.15 326.211 111.668Z" fill="#9ABFFF"/> <g opacity="0.370867"> -<rect width="19.296" height="11.091" fill="black" fill-opacity="0" transform="translate(327.431 99.7187)"/> <g opacity="0.653363"> -<rect width="14.9457" height="5.18221" fill="black" fill-opacity="0" transform="translate(327.431 105.627)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M342.161 105.859C342.497 106.1 342.355 106.272 342.298 106.435C341.315 109.24 336.078 111.251 331.129 110.726C328.909 110.491 327.621 109.717 327.452 108.517C327.266 107.194 328.324 106.297 330.572 105.874C331.906 105.624 333.285 105.603 334.667 105.644C337.204 105.722 339.711 106.054 342.161 105.859Z" fill="#4562DF"/> </g> <g opacity="0.653363"> -<rect width="14.909" height="5.19137" fill="black" fill-opacity="0" transform="translate(331.818 99.7187)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M331.818 104.643C332.063 102.059 336.216 99.8633 340.869 99.7298C341.632 99.7079 342.397 99.7109 343.152 99.8089C345.409 100.102 346.637 100.909 346.723 102.166C346.805 103.373 345.734 104.222 343.592 104.635C342.419 104.861 341.201 104.936 339.972 104.903C337.284 104.829 334.63 104.492 331.818 104.643Z" fill="#4562DF"/> </g> </g> -<rect width="37.5326" height="24.718" fill="black" fill-opacity="0" transform="translate(47.5456 103.771)"/> -<rect width="37.5326" height="24.2005" fill="black" fill-opacity="0" transform="translate(47.5456 103.771)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M79.5817 124.894C72.253 128.998 60.3709 128.998 53.0422 124.894C45.7135 120.79 45.7134 114.137 53.0421 110.033C60.3708 105.93 72.2529 105.93 79.5816 110.033C86.9103 114.137 86.9104 120.79 79.5817 124.894Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M47.5456 117.145H85.0782V113.961H47.5456V117.145Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M53.0422 121.71C45.7135 117.606 45.7134 110.953 53.0421 106.849C60.3708 102.745 72.2529 102.745 79.5816 106.849C86.9103 110.953 86.9104 117.606 79.5817 121.71C72.253 125.813 60.3709 125.813 53.0422 121.71Z" fill="#86F7D1"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M54.9046 120.265C48.6045 116.784 48.6044 111.138 54.9046 107.656C61.2046 104.174 71.4191 104.174 77.7192 107.656C84.0193 111.138 84.0194 116.784 77.7193 120.266C71.4192 123.747 61.2047 123.747 54.9046 120.265Z" fill="#95FFDA"/> -<rect width="26.809" height="10.7469" fill="black" fill-opacity="0" transform="translate(52.9074 109.145)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M78.1519 112.724C78.1579 112.71 78.1629 112.697 78.1681 112.684C78.1741 112.687 78.1793 112.69 78.1852 112.692L78.2281 112.712L78.1519 112.724ZM75.2505 115.293C74.8038 115.693 74.3155 116.07 73.7899 116.421C73.5585 116.576 73.3192 116.726 73.0736 116.871C71.4102 116.816 70.5534 116.576 70.1138 116.331C69.5898 116.039 69.6552 115.742 69.6552 115.742C69.6552 115.702 69.6552 115.662 69.6711 115.622C69.6711 115.622 69.6791 115.586 69.7054 115.531C69.7421 115.454 69.8139 115.34 69.9495 115.225C70.1816 115.027 70.6037 114.83 71.3568 114.828H71.3703C71.5698 114.824 71.7692 114.842 71.9367 114.886C72.7345 115.083 72.216 115.45 72.216 115.45C72.2758 115.446 72.3364 115.44 72.3978 115.433C73.3567 115.315 74.4376 114.814 74.2502 113.976C74.2079 113.785 74.0682 113.615 73.8744 113.464C73.3862 113.083 72.5533 112.83 72.0795 112.708C71.8632 112.653 71.7213 112.625 71.7213 112.625C71.7452 112.557 71.7532 112.504 71.7532 112.464C71.7532 112.456 71.7532 112.449 71.7515 112.442C71.7476 112.377 71.7213 112.352 71.7213 112.352C66.7753 110.986 62.9222 111.281 62.9222 111.281L63.8795 111.718C63.8755 111.718 63.8723 111.718 63.8684 111.718C63.7758 111.715 63.6761 111.715 63.5844 111.715C62.2306 111.737 60.9685 111.926 59.8908 112.241C57.7728 112.857 56.3649 113.954 56.3649 115.207C56.3649 115.265 56.3687 115.322 56.3752 115.376C56.3464 115.351 56.3186 115.326 56.2898 115.3C56.1422 115.169 56.0042 115.039 55.875 114.911C54.8969 113.945 54.4 113.097 54.1861 112.662C59.0388 110.884 64.9445 109.989 65.7701 109.872C66.0015 109.905 66.6285 109.999 67.5188 110.155C68.5295 110.333 69.8785 110.592 71.3719 110.935C73.2681 111.372 75.3956 111.944 77.3542 112.66C77.11 113.151 76.5037 114.17 75.2505 115.293ZM67.8363 112.25C69.1517 112.246 70.3396 112.388 71.2092 112.628C71.1812 112.624 70.7249 112.558 70.2615 112.53C70.0221 112.643 69.8147 112.691 69.7509 112.705C69.7429 112.709 69.7349 112.712 69.7349 112.712C68.3389 112.578 67.8363 112.25 67.8363 112.25ZM78.3792 112.23C73.4834 110.339 67.3313 109.373 66.1507 109.199L65.7677 109.145L65.3928 109.198C64.2081 109.373 58.0481 110.34 53.1515 112.233L52.9074 112.327L52.9672 112.482C53.0685 112.733 53.6549 114.058 55.291 115.528C56.3425 116.47 57.6284 117.296 59.1131 117.985C60.9223 118.822 63.0339 119.442 65.3929 119.83L65.7758 119.892L66.0246 119.847L66.1571 119.826C68.5016 119.441 70.6133 118.82 72.4345 117.982C73.9111 117.301 75.197 116.474 76.2572 115.524C77.903 114.049 78.5212 112.682 78.5842 112.468H78.585L78.644 112.316L78.3792 112.23Z" fill="#6CF0D1"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.9765 112.492L78.644 112.548C78.6703 112.485 78.6928 112.425 78.7153 112.369C78.7416 112.382 78.7636 112.392 78.7899 112.405L78.9765 112.492Z" fill="#000CA5"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.7893 112.405C78.7631 112.392 78.7411 112.382 78.7148 112.369C78.6923 112.425 78.6698 112.485 78.644 112.548L78.9759 112.492L78.7893 112.405Z" fill="#000CA5"/> -<rect width="37.5326" height="24.718" fill="black" fill-opacity="0" transform="translate(47.5456 99.4725)"/> -<rect width="37.5326" height="24.2005" fill="black" fill-opacity="0" transform="translate(47.5456 99.4725)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M79.5817 120.595C72.253 124.699 60.3709 124.699 53.0422 120.595C45.7135 116.492 45.7134 109.838 53.0421 105.734C60.3708 101.631 72.2529 101.631 79.5816 105.735C86.9103 109.838 86.9104 116.492 79.5817 120.595Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M47.5456 112.846H85.0782V109.662H47.5456V112.846Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M53.0422 117.411C45.7135 113.307 45.7134 106.654 53.0421 102.55C60.3708 98.4465 72.2529 98.4465 79.5816 102.55C86.9103 106.654 86.9104 113.307 79.5817 117.411C72.253 121.515 60.3709 121.515 53.0422 117.411Z" fill="#86F7D1"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M54.9046 115.967C48.6045 112.485 48.6044 106.839 54.9046 103.358C61.2046 99.8757 71.4191 99.8757 77.7192 103.358C84.0193 106.84 84.0194 112.485 77.7193 115.967C71.4192 119.449 61.2047 119.449 54.9046 115.967Z" fill="#95FFDA"/> -<rect width="26.809" height="10.7469" fill="black" fill-opacity="0" transform="translate(52.9074 104.846)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M78.1519 108.426C78.1579 108.412 78.1629 108.398 78.1681 108.386C78.1741 108.388 78.1793 108.391 78.1852 108.394L78.2281 108.413L78.1519 108.426ZM75.2505 110.994C74.8038 111.395 74.3155 111.771 73.7899 112.122C73.5585 112.277 73.3192 112.427 73.0736 112.572C71.4102 112.517 70.5534 112.277 70.1138 112.032C69.5898 111.741 69.6552 111.443 69.6552 111.443C69.6552 111.403 69.6552 111.363 69.6711 111.323C69.6711 111.323 69.6791 111.288 69.7054 111.232C69.7421 111.155 69.8139 111.041 69.9495 110.926C70.1816 110.729 70.6037 110.532 71.3568 110.529H71.3703C71.5698 110.525 71.7692 110.544 71.9367 110.587C72.7345 110.784 72.216 111.152 72.216 111.152C72.2758 111.147 72.3364 111.142 72.3978 111.134C73.3567 111.016 74.4376 110.516 74.2502 109.677C74.2079 109.487 74.0682 109.316 73.8744 109.165C73.3862 108.784 72.5533 108.531 72.0795 108.41C71.8632 108.354 71.7213 108.326 71.7213 108.326C71.7452 108.258 71.7532 108.205 71.7532 108.165C71.7532 108.157 71.7532 108.15 71.7515 108.143C71.7476 108.078 71.7213 108.053 71.7213 108.053C66.7753 106.688 62.9222 106.983 62.9222 106.983L63.8795 107.42C63.8755 107.42 63.8723 107.419 63.8684 107.419C63.7758 107.416 63.6761 107.416 63.5844 107.416C62.2306 107.438 60.9685 107.627 59.8908 107.942C57.7728 108.558 56.3649 109.656 56.3649 110.908C56.3649 110.966 56.3687 111.023 56.3752 111.077C56.3464 111.052 56.3186 111.027 56.2898 111.002C56.1422 110.87 56.0042 110.74 55.875 110.612C54.8969 109.646 54.4 108.798 54.1861 108.363C59.0388 106.586 64.9445 105.691 65.7701 105.573C66.0015 105.606 66.6285 105.7 67.5188 105.856C68.5295 106.034 69.8785 106.293 71.3719 106.637C73.2681 107.073 75.3956 107.645 77.3542 108.361C77.11 108.852 76.5037 109.871 75.2505 110.994ZM67.8363 107.951C69.1517 107.948 70.3396 108.09 71.2092 108.329C71.1812 108.325 70.7249 108.26 70.2615 108.232C70.0221 108.344 69.8147 108.392 69.7509 108.406C69.7429 108.41 69.7349 108.414 69.7349 108.414C68.3389 108.279 67.8363 107.951 67.8363 107.951ZM78.3792 107.931C73.4834 106.04 67.3313 105.074 66.1507 104.9L65.7677 104.846L65.3928 104.899C64.2081 105.074 58.0481 106.041 53.1515 107.935L52.9074 108.029L52.9672 108.183C53.0685 108.434 53.6549 109.759 55.291 111.229C56.3425 112.171 57.6284 112.997 59.1131 113.686C60.9223 114.523 63.0339 115.144 65.3929 115.531L65.7758 115.593L66.0246 115.548L66.1571 115.527C68.5016 115.142 70.6133 114.522 72.4345 113.683C73.9111 113.002 75.197 112.175 76.2572 111.225C77.903 109.75 78.5212 108.383 78.5842 108.17H78.585L78.644 108.017L78.3792 107.931Z" fill="#6CF0D1"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.9765 108.193L78.644 108.249C78.6703 108.186 78.6928 108.126 78.7153 108.07C78.7416 108.083 78.7636 108.093 78.7899 108.107L78.9765 108.193Z" fill="#000CA5"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.7893 108.107C78.7631 108.094 78.7411 108.083 78.7148 108.07C78.6923 108.126 78.6698 108.186 78.644 108.249L78.9759 108.193L78.7893 108.107Z" fill="#000CA5"/> -<rect width="37.5326" height="24.718" fill="black" fill-opacity="0" transform="translate(47.5456 95.1737)"/> -<rect width="37.5326" height="24.2005" fill="black" fill-opacity="0" transform="translate(47.5456 95.1737)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M79.5817 116.296C72.253 120.4 60.3709 120.4 53.0422 116.296C45.7135 112.193 45.7134 105.539 53.0421 101.436C60.3708 97.332 72.2529 97.332 79.5816 101.436C86.9103 105.539 86.9104 112.193 79.5817 116.296Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M47.5456 108.548H85.0782V105.363H47.5456V108.548Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M53.0422 113.112C45.7135 109.008 45.7134 102.355 53.0421 98.2514C60.3708 94.1478 72.2529 94.1478 79.5816 98.2515C86.9103 102.355 86.9104 109.009 79.5817 113.112C72.253 117.216 60.3709 117.216 53.0422 113.112Z" fill="#86F7D1"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M54.9046 111.668C48.6045 108.186 48.6044 102.541 54.9046 99.0588C61.2046 95.5769 71.4191 95.5769 77.7192 99.0588C84.0193 102.541 84.0194 108.186 77.7193 111.668C71.4192 115.15 61.2047 115.15 54.9046 111.668Z" fill="#95FFDA"/> -<rect width="26.809" height="10.7469" fill="black" fill-opacity="0" transform="translate(52.9074 100.547)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M78.1519 104.127C78.1579 104.113 78.1629 104.099 78.1681 104.087C78.1741 104.09 78.1793 104.092 78.1852 104.095L78.2281 104.114L78.1519 104.127ZM75.2505 106.696C74.8038 107.096 74.3155 107.472 73.7899 107.824C73.5585 107.978 73.3192 108.128 73.0736 108.273C71.4102 108.218 70.5534 107.978 70.1138 107.733C69.5898 107.442 69.6552 107.144 69.6552 107.144C69.6552 107.104 69.6552 107.064 69.6711 107.024C69.6711 107.024 69.6791 106.989 69.7054 106.933C69.7421 106.857 69.8139 106.742 69.9495 106.627C70.1816 106.43 70.6037 106.233 71.3568 106.23H71.3703C71.5698 106.227 71.7692 106.245 71.9367 106.289C72.7345 106.485 72.216 106.853 72.216 106.853C72.2758 106.849 72.3364 106.843 72.3978 106.835C73.3567 106.717 74.4376 106.217 74.2502 105.378C74.2079 105.188 74.0682 105.017 73.8744 104.867C73.3862 104.485 72.5533 104.232 72.0795 104.111C71.8632 104.055 71.7213 104.027 71.7213 104.027C71.7452 103.959 71.7532 103.907 71.7532 103.866C71.7532 103.858 71.7532 103.851 71.7515 103.844C71.7476 103.779 71.7213 103.754 71.7213 103.754C66.7753 102.389 62.9222 102.684 62.9222 102.684L63.8795 103.121C63.8755 103.121 63.8723 103.12 63.8684 103.12C63.7758 103.117 63.6761 103.117 63.5844 103.117C62.2306 103.139 60.9685 103.329 59.8908 103.643C57.7728 104.26 56.3649 105.357 56.3649 106.609C56.3649 106.668 56.3687 106.724 56.3752 106.779C56.3464 106.754 56.3186 106.728 56.2898 106.703C56.1422 106.572 56.0042 106.441 55.875 106.313C54.8969 105.348 54.4 104.5 54.1861 104.065C59.0388 102.287 64.9445 101.392 65.7701 101.275C66.0015 101.307 66.6285 101.401 67.5188 101.558C68.5295 101.735 69.8785 101.994 71.3719 102.338C73.2681 102.774 75.3956 103.347 77.3542 104.062C77.11 104.553 76.5037 105.573 75.2505 106.696ZM67.8363 103.652C69.1517 103.649 70.3396 103.791 71.2092 104.031C71.1812 104.027 70.7249 103.961 70.2615 103.933C70.0221 104.046 69.8147 104.093 69.7509 104.108C69.7429 104.111 69.7349 104.115 69.7349 104.115C68.3389 103.98 67.8363 103.652 67.8363 103.652ZM78.3792 103.632C73.4834 101.741 67.3313 100.775 66.1507 100.601L65.7677 100.547L65.3928 100.601C64.2081 100.775 58.0481 101.742 53.1515 103.636L52.9074 103.73L52.9672 103.884C53.0685 104.135 53.6549 105.46 55.291 106.93C56.3425 107.872 57.6284 108.699 59.1131 109.388C60.9223 110.224 63.0339 110.845 65.3929 111.232L65.7758 111.294L66.0246 111.25L66.1571 111.229C68.5016 110.843 70.6133 110.223 72.4345 109.384C73.9111 108.703 75.197 107.876 76.2572 106.927C77.903 105.451 78.5212 104.084 78.5842 103.871H78.585L78.644 103.718L78.3792 103.632Z" fill="#6CF0D1"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.9765 103.894L78.644 103.95C78.6703 103.887 78.6928 103.827 78.7153 103.771C78.7416 103.784 78.7636 103.794 78.7899 103.808L78.9765 103.894Z" fill="#000CA5"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.7893 103.808C78.7631 103.795 78.7411 103.784 78.7148 103.771C78.6923 103.827 78.6698 103.887 78.644 103.951L78.9759 103.894L78.7893 103.808Z" fill="#000CA5"/> -<rect width="37.5326" height="24.718" fill="black" fill-opacity="0" transform="translate(47.5456 90.8749)"/> -<rect width="37.5326" height="24.2005" fill="black" fill-opacity="0" transform="translate(47.5456 90.8749)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M79.5817 111.998C72.253 116.101 60.3709 116.101 53.0422 111.998C45.7135 107.894 45.7134 101.241 53.0421 97.1369C60.3708 93.0333 72.2529 93.0333 79.5816 97.137C86.9103 101.241 86.9104 107.894 79.5817 111.998Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M47.5456 104.249H85.0782V101.065H47.5456V104.249Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M53.0422 108.813C45.7135 104.71 45.7134 98.0563 53.0421 93.9526C60.3708 89.849 72.2529 89.849 79.5816 93.9527C86.9103 98.0564 86.9104 104.71 79.5817 108.813C72.253 112.917 60.3709 112.917 53.0422 108.813Z" fill="#86F7D1"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M54.9046 107.369C48.6045 103.887 48.6044 98.2419 54.9046 94.7601C61.2046 91.2781 71.4191 91.2781 77.7192 94.7601C84.0193 98.242 84.0194 103.887 77.7193 107.369C71.4192 110.851 61.2047 110.851 54.9046 107.369Z" fill="#95FFDA"/> -<rect width="26.809" height="10.7469" fill="black" fill-opacity="0" transform="translate(52.9074 96.2484)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M78.1519 99.828C78.1579 99.814 78.1629 99.8006 78.1681 99.788C78.1741 99.7909 78.1793 99.7933 78.1852 99.7962L78.2281 99.8156L78.1519 99.828ZM75.2505 102.397C74.8038 102.797 74.3155 103.174 73.7899 103.525C73.5585 103.68 73.3192 103.829 73.0736 103.974C71.4102 103.92 70.5534 103.679 70.1138 103.434C69.5898 103.143 69.6552 102.845 69.6552 102.845C69.6552 102.805 69.6552 102.765 69.6711 102.725C69.6711 102.725 69.6791 102.69 69.7054 102.634C69.7421 102.558 69.8139 102.443 69.9495 102.328C70.1816 102.131 70.6037 101.934 71.3568 101.932H71.3703C71.5698 101.928 71.7692 101.946 71.9367 101.99C72.7345 102.186 72.216 102.554 72.216 102.554C72.2758 102.55 72.3364 102.544 72.3978 102.536C73.3567 102.419 74.4376 101.918 74.2502 101.08C74.2079 100.889 74.0682 100.719 73.8744 100.568C73.3862 100.187 72.5533 99.9333 72.0795 99.812C71.8632 99.7567 71.7213 99.7287 71.7213 99.7287C71.7452 99.6605 71.7532 99.6078 71.7532 99.5673C71.7532 99.5597 71.7532 99.5524 71.7515 99.5455C71.7476 99.4806 71.7213 99.4555 71.7213 99.4555C66.7753 98.0901 62.9222 98.3851 62.9222 98.3851L63.8795 98.822C63.8755 98.822 63.8723 98.8216 63.8684 98.8216C63.7758 98.8183 63.6761 98.8183 63.5844 98.8183C62.2306 98.8405 60.9685 99.0299 59.8908 99.3442C57.7728 99.9609 56.3649 101.058 56.3649 102.31C56.3649 102.369 56.3687 102.425 56.3752 102.48C56.3464 102.455 56.3186 102.43 56.2898 102.404C56.1422 102.273 56.0042 102.142 55.875 102.015C54.8969 101.049 54.4 100.201 54.1861 99.7658C59.0388 97.9881 64.9445 97.0932 65.7701 96.9759C66.0015 97.0086 66.6285 97.1023 67.5188 97.2588C68.5295 97.4365 69.8785 97.6953 71.3719 98.0391C73.2681 98.4754 75.3956 99.0478 77.3542 99.7635C77.11 100.254 76.5037 101.274 75.2505 102.397ZM67.8363 99.3536C69.1517 99.35 70.3396 99.4919 71.2092 99.7319C71.1812 99.7279 70.7249 99.662 70.2615 99.634C70.0221 99.7469 69.8147 99.7942 69.7509 99.8087C69.7429 99.8125 69.7349 99.8161 69.7349 99.8161C68.3389 99.6813 67.8363 99.3536 67.8363 99.3536ZM78.3792 99.3332C73.4834 97.4423 67.3313 96.4767 66.1507 96.3026L65.7677 96.2484L65.3928 96.3019C64.2081 96.4767 58.0481 97.4434 53.1515 99.3371L52.9074 99.4312L52.9672 99.5855C53.0685 99.8364 53.6549 101.161 55.291 102.631C56.3425 103.573 57.6284 104.4 59.1131 105.089C60.9223 105.926 63.0339 106.546 65.3929 106.933L65.7758 106.995L66.0246 106.951L66.1571 106.93C68.5016 106.545 70.6133 105.924 72.4345 105.085C73.9111 104.404 75.197 103.577 76.2572 102.628C77.903 101.153 78.5212 99.7855 78.5842 99.572H78.585L78.644 99.4196L78.3792 99.3332Z" fill="#6CF0D1"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.9765 99.5955L78.644 99.6514C78.6703 99.5885 78.6928 99.5284 78.7153 99.4725C78.7416 99.4854 78.7636 99.4956 78.7899 99.509L78.9765 99.5955Z" fill="#000CA5"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.7893 99.509C78.7631 99.4961 78.7411 99.4854 78.7148 99.4725C78.6923 99.5284 78.6698 99.5885 78.644 99.6519L78.9759 99.5955L78.7893 99.509Z" fill="#000CA5"/> -<rect width="37.5326" height="24.718" fill="black" fill-opacity="0" transform="translate(47.5456 86.5761)"/> -<rect width="37.5326" height="24.2005" fill="black" fill-opacity="0" transform="translate(47.5456 86.5761)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M79.5817 107.699C72.253 111.803 60.3709 111.803 53.0422 107.699C45.7135 103.595 45.7134 96.9418 53.0421 92.8381C60.3708 88.7345 72.2529 88.7345 79.5816 92.8382C86.9103 96.9419 86.9104 103.595 79.5817 107.699Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M47.5456 99.9501H85.0782V96.7658H47.5456V99.9501Z" fill="#61C9A7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M53.0422 104.515C45.7135 100.411 45.7134 93.7575 53.0421 89.6539C60.3708 85.5502 72.2529 85.5502 79.5816 89.6539C86.9103 93.7576 86.9104 100.411 79.5817 104.515C72.253 108.618 60.3709 108.618 53.0422 104.515Z" fill="#86F7D1"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M54.9046 103.07C48.6045 99.5884 48.6044 93.9431 54.9046 90.4613C61.2046 86.9794 71.4191 86.9794 77.7192 90.4613C84.0193 93.9432 84.0194 99.5885 77.7193 103.07C71.4192 106.552 61.2047 106.552 54.9046 103.07Z" fill="#95FFDA"/> -<rect width="26.809" height="10.7469" fill="black" fill-opacity="0" transform="translate(52.9074 91.9496)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M78.1519 95.5293C78.1579 95.5152 78.1629 95.5018 78.1681 95.4892C78.1741 95.4921 78.1793 95.4945 78.1852 95.4974L78.2281 95.5168L78.1519 95.5293ZM75.2505 98.0981C74.8038 98.4983 74.3155 98.8747 73.7899 99.2261C73.5585 99.3809 73.3192 99.5305 73.0736 99.6755C71.4102 99.6208 70.5534 99.3805 70.1138 99.1355C69.5898 98.8442 69.6552 98.5467 69.6552 98.5467C69.6552 98.5067 69.6552 98.4665 69.6711 98.4266C69.6711 98.4266 69.6791 98.3912 69.7054 98.3355C69.7421 98.259 69.8139 98.1444 69.9495 98.0297C70.1816 97.8323 70.6037 97.6353 71.3568 97.6327H71.3703C71.5698 97.6291 71.7692 97.6473 71.9367 97.6911C72.7345 97.8876 72.216 98.2554 72.216 98.2554C72.2758 98.251 72.3364 98.2452 72.3978 98.2376C73.3567 98.1199 74.4376 97.6193 74.2502 96.7808C74.2079 96.5903 74.0682 96.4199 73.8744 96.2691C73.3862 95.8879 72.5533 95.6345 72.0795 95.5132C71.8632 95.4579 71.7213 95.4299 71.7213 95.4299C71.7452 95.3618 71.7532 95.309 71.7532 95.2685C71.7532 95.2609 71.7532 95.2536 71.7515 95.2467C71.7476 95.1818 71.7213 95.1567 71.7213 95.1567C66.7753 93.7913 62.9222 94.0863 62.9222 94.0863L63.8795 94.5232C63.8755 94.5232 63.8723 94.5228 63.8684 94.5228C63.7758 94.5195 63.6761 94.5195 63.5844 94.5195C62.2306 94.5417 60.9685 94.7311 59.8908 95.0454C57.7728 95.6622 56.3649 96.7592 56.3649 98.0115C56.3649 98.07 56.3687 98.1264 56.3752 98.1811C56.3464 98.1561 56.3186 98.1309 56.2898 98.1053C56.1422 97.974 56.0042 97.8435 55.875 97.7158C54.8969 96.7501 54.4 95.9021 54.1861 95.467C59.0388 93.6893 64.9445 92.7944 65.7701 92.6771C66.0015 92.7099 66.6285 92.8035 67.5188 92.9601C68.5295 93.1377 69.8785 93.3965 71.3719 93.7403C73.2681 94.1766 75.3956 94.749 77.3542 95.4648C77.11 95.9556 76.5037 96.9751 75.2505 98.0981ZM67.8363 95.0548C69.1517 95.0512 70.3396 95.1932 71.2092 95.4331C71.1812 95.4292 70.7249 95.3632 70.2615 95.3352C70.0221 95.4481 69.8147 95.4954 69.7509 95.51C69.7429 95.5137 69.7349 95.5173 69.7349 95.5173C68.3389 95.3825 67.8363 95.0548 67.8363 95.0548ZM78.3792 95.0344C73.4834 93.1435 67.3313 92.1779 66.1507 92.0039L65.7677 91.9496L65.3928 92.0031C64.2081 92.1779 58.0481 93.1446 53.1515 95.0383L52.9074 95.1324L52.9672 95.2867C53.0685 95.5376 53.6549 96.8626 55.291 98.3326C56.3425 99.2745 57.6284 100.101 59.1131 100.79C60.9223 101.627 63.0339 102.247 65.3929 102.635L65.7758 102.697L66.0246 102.652L66.1571 102.631C68.5016 102.246 70.6133 101.625 72.4345 100.786C73.9111 100.106 75.197 99.2786 76.2572 98.329C77.903 96.8539 78.5212 95.4867 78.5842 95.2732H78.585L78.644 95.1208L78.3792 95.0344Z" fill="#6CF0D1"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.9765 95.2967L78.644 95.3526C78.6703 95.2898 78.6928 95.2296 78.7153 95.1737C78.7416 95.1866 78.7636 95.1968 78.7899 95.2102L78.9765 95.2967Z" fill="#000CA5"/> <path opacity="0.175894" fill-rule="evenodd" clip-rule="evenodd" d="M78.7893 95.2102C78.7631 95.1973 78.7411 95.1866 78.7148 95.1737C78.6923 95.2296 78.6698 95.2898 78.644 95.3532L78.9759 95.2967L78.7893 95.2102Z" fill="#000CA5"/> -<rect width="37.5326" height="24.718" fill="black" fill-opacity="0" transform="translate(318.852 90.8749)"/> -<rect width="37.5326" height="24.2005" fill="black" fill-opacity="0" transform="translate(318.852 90.8749)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M350.888 111.998C343.56 116.101 331.678 116.101 324.349 111.998C317.02 107.894 317.02 101.241 324.349 97.1369C331.677 93.0333 343.56 93.0333 350.888 97.137C358.217 101.241 358.217 107.894 350.888 111.998Z" fill="#7F95EF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M318.852 104.249H356.385V101.065H318.852V104.249Z" fill="#838DB7"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M350.888 108.813C343.56 112.917 331.678 112.917 324.349 108.813C317.02 104.71 317.02 98.0563 324.349 93.9526C331.677 89.849 343.56 89.849 350.888 93.9527C358.217 98.0564 358.217 104.71 350.888 108.813Z" fill="#93A8FF"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M326.211 107.369C319.911 103.887 319.911 98.2419 326.211 94.7601C332.511 91.2781 342.726 91.2781 349.026 94.7601C355.326 98.242 355.326 103.887 349.026 107.369C342.726 110.851 332.511 110.851 326.211 107.369Z" fill="#9ABFFF"/> <g opacity="0.370867"> -<rect width="19.296" height="11.091" fill="black" fill-opacity="0" transform="translate(327.431 95.4199)"/> <g opacity="0.653363"> -<rect width="14.9457" height="5.18222" fill="black" fill-opacity="0" transform="translate(327.431 101.329)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M342.161 101.56C342.497 101.801 342.355 101.973 342.298 102.137C341.315 104.941 336.078 106.952 331.129 106.428C328.909 106.192 327.621 105.418 327.452 104.219C327.266 102.896 328.324 101.998 330.572 101.576C331.906 101.325 333.285 101.304 334.667 101.346C337.204 101.423 339.711 101.755 342.161 101.56Z" fill="#4562DF"/> </g> <g opacity="0.653363"> -<rect width="14.909" height="5.19137" fill="black" fill-opacity="0" transform="translate(331.818 95.4199)"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M331.818 100.345C332.063 97.7602 336.216 95.5645 340.869 95.4311C341.632 95.4091 342.397 95.4121 343.152 95.5101C345.409 95.8029 346.637 96.6105 346.723 97.8673C346.805 99.0741 345.734 99.9235 343.592 100.336C342.419 100.562 341.201 100.637 339.972 100.604C337.284 100.53 334.63 100.194 331.818 100.345Z" fill="#4562DF"/> </g> </g> diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index efb5aef23..62e16dd1d 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -163,7 +163,7 @@ export class Blockchain { const providerName = this._getNameGivenProvider(injectedWeb3.currentProvider); // Wrap Metamask in a compatability wrapper MetamaskSubprovider (to handle inconsistencies) const signerSubprovider = - providerName === Providers.Metamask + providerName === constants.PROVIDER_NAME_METAMASK ? new MetamaskSubprovider(injectedWeb3.currentProvider) : new SignerSubprovider(injectedWeb3.currentProvider); provider.addProvider(signerSubprovider); diff --git a/packages/website/ts/components/dropdowns/developers_drop_down.tsx b/packages/website/ts/components/dropdowns/developers_drop_down.tsx index b279566e0..6e85c1499 100644 --- a/packages/website/ts/components/dropdowns/developers_drop_down.tsx +++ b/packages/website/ts/components/dropdowns/developers_drop_down.tsx @@ -89,26 +89,22 @@ export class DevelopersDropDown extends React.Component<DevelopersDropDownProps, ); } private _renderDropdownMenu(): React.ReactNode { + const sectionPadding = '28px'; const dropdownMenu = ( <Container> - <Container padding="1.75rem"> - {this._renderTitle('Getting started')} - <Container className="flex"> - <Container className="pr4 mr2"> - {this._renderLinkSection(gettingStartedKeyToLinkInfo1)} - </Container> - <Container>{this._renderLinkSection(gettingStartedKeyToLinkInfo2)}</Container> + <Container className="flex" padding={sectionPadding}> + <Container paddingRight="45px"> + {this._renderLinkSection(gettingStartedKeyToLinkInfo1, 'Getting started')} </Container> + <Container>{this._renderLinkSection(gettingStartedKeyToLinkInfo2)}</Container> </Container> <Container width="100%" height="1px" backgroundColor={colors.grey300} /> - <Container className="flex" padding="1.75rem"> - <Container className="pr4 mr2"> - <Container>{this._renderTitle('Popular docs')}</Container> - <Container>{this._renderLinkSection(popularDocsToLinkInfos)}</Container> + <Container className="flex" padding={sectionPadding}> + <Container paddingRight="62px"> + <Container>{this._renderLinkSection(popularDocsToLinkInfos, 'Popular docs')}</Container> </Container> <Container> - <Container>{this._renderTitle('Useful links')}</Container> - <Container>{this._renderLinkSection(usefulLinksToLinkInfo)}</Container> + <Container>{this._renderLinkSection(usefulLinksToLinkInfo, 'Useful links')}</Container> </Container> </Container> <Link to={WebsitePaths.Docs} fontColor={colors.lightBlueA700}> @@ -127,16 +123,7 @@ export class DevelopersDropDown extends React.Component<DevelopersDropDownProps, ); return dropdownMenu; } - private _renderTitle(title: string): React.ReactNode { - return ( - <Container paddingBottom="12px"> - <Text letterSpacing={1} fontColor={colors.linkSectionGrey} fontSize="14px" fontWeight={600}> - {title.toUpperCase()} - </Text> - </Container> - ); - } - private _renderLinkSection(links: ALink[]): React.ReactNode { + private _renderLinkSection(links: ALink[], title: string = ''): React.ReactNode { const numLinks = links.length; let i = 0; const renderLinks = _.map(links, (link: ALink) => { @@ -159,6 +146,17 @@ export class DevelopersDropDown extends React.Component<DevelopersDropDownProps, </Container> ); }); - return <Container>{renderLinks}</Container>; + return ( + <Container> + <Container height="33px"> + {!_.isEmpty(title) && ( + <Text letterSpacing={1} fontColor={colors.linkSectionGrey} fontSize="14px" fontWeight={600}> + {title.toUpperCase()} + </Text> + )} + </Container> + {renderLinks} + </Container> + ); } } diff --git a/packages/website/ts/components/nested_sidebar_menu.tsx b/packages/website/ts/components/nested_sidebar_menu.tsx new file mode 100644 index 000000000..db7d55261 --- /dev/null +++ b/packages/website/ts/components/nested_sidebar_menu.tsx @@ -0,0 +1,92 @@ +import { ALink, colors, Link, utils as sharedUtils } from '@0x/react-shared'; +import { ObjectMap } from '@0x/types'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { Button } from 'ts/components/ui/button'; +import { Text } from 'ts/components/ui/text'; + +export interface NestedSidebarMenuProps { + sectionNameToLinks: ObjectMap<ALink[]>; + sidebarHeader?: React.ReactNode; + shouldReformatMenuItemNames?: boolean; +} + +export const NestedSidebarMenu = (props: NestedSidebarMenuProps) => { + const navigation = _.map(props.sectionNameToLinks, (links: ALink[], sectionName: string) => { + const finalSectionName = sharedUtils.convertCamelCaseToSpaces(sectionName); + const menuItems = _.map(links, (link, i) => { + const menuItemTitle = props.shouldReformatMenuItemNames + ? _.capitalize(sharedUtils.convertDashesToSpaces(link.title)) + : link.title; + const finalLink = { + ...link, + title: menuItemTitle, + }; + return <MenuItem key={`menu-item-${menuItemTitle}`} link={finalLink} />; + }); + // tslint:disable-next-line:no-unused-variable + return ( + <div key={`section-${sectionName}`} className="py1" style={{ color: colors.greyTheme }}> + <Text fontSize="14px" letterSpacing="0.5" className="py1 pl1"> + {finalSectionName.toUpperCase()} + </Text> + {menuItems} + </div> + ); + }); + return ( + <div> + {props.sidebarHeader} + <div>{navigation}</div> + </div> + ); +}; + +export interface MenuItemProps { + link: ALink; +} + +export interface MenuItemState { + isActive: boolean; +} + +export class MenuItem extends React.Component<MenuItemProps, MenuItemState> { + constructor(props: MenuItemProps) { + super(props); + const isActive = window.location.hash.slice(1) === props.link.to; + this.state = { + isActive, + }; + } + public render(): React.ReactNode { + const isActive = this.state.isActive; + return ( + <Link + to={this.props.link.to} + shouldOpenInNewTab={this.props.link.shouldOpenInNewTab} + onActivityChanged={this._onActivityChanged.bind(this)} + > + <Button + borderRadius="4px" + padding="0.4em 0.375em" + width="100%" + backgroundColor={isActive ? colors.lightLinkBlue : colors.grey100} + fontSize="14px" + textAlign="left" + > + <Text + fontWeight={isActive ? 'bold' : 'normal'} + fontColor={isActive ? colors.white : colors.grey800} + > + {this.props.link.title} + </Text> + </Button> + </Link> + ); + } + private _onActivityChanged(isActive: boolean): void { + this.setState({ + isActive, + }); + } +} diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 2299881c4..94ab63ac6 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -210,12 +210,16 @@ export class Portal extends React.Component<PortalProps, PortalState> { isLoaded: false, }; } - this.setState({ - trackedTokenStateByAddress, - }); - // Fetch the actual balance/allowance. - // tslint:disable-next-line:no-floating-promises - this._fetchBalancesAndAllowancesAsync(newTokenAddresses); + this.setState( + { + trackedTokenStateByAddress, + }, + () => { + // Fetch the actual balance/allowance. + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(newTokenAddresses); + }, + ); } } public render(): React.ReactNode { @@ -644,6 +648,9 @@ export class Portal extends React.Component<PortalProps, PortalState> { } private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> { + if (_.isEmpty(tokenAddresses)) { + return; + } const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; const balancesAndAllowances = await Promise.all( diff --git a/packages/website/ts/containers/json_schemas_documentation.ts b/packages/website/ts/containers/json_schemas_documentation.ts index cb5918784..9c4bb8e26 100644 --- a/packages/website/ts/containers/json_schemas_documentation.ts +++ b/packages/website/ts/containers/json_schemas_documentation.ts @@ -15,8 +15,9 @@ const InstallationMarkdown1 = require('md/docs/json_schemas/1/installation'); const InstallationMarkdown3 = require('md/docs/json_schemas/3/installation'); const usageMarkdown1 = require('md/docs/json_schemas/1/usage'); const usageMarkdown3 = require('md/docs/json_schemas/3/usage'); -const SchemasMarkdownV1 = require('md/docs/json_schemas/1/schemas'); -const SchemasMarkdownV2 = require('md/docs/json_schemas/2/schemas'); +const SchemasMarkdown1 = require('md/docs/json_schemas/1/schemas'); +const SchemasMarkdown2 = require('md/docs/json_schemas/2/schemas'); +const SchemasMarkdown3 = require('md/docs/json_schemas/3/schemas'); /* tslint:enable:no-var-requires */ const markdownSections = { @@ -40,19 +41,25 @@ const docsInfoConfig: DocsInfoConfig = { '0.0.1': { [markdownSections.introduction]: IntroMarkdown1, [markdownSections.installation]: InstallationMarkdown1, - [markdownSections.schemas]: SchemasMarkdownV1, + [markdownSections.schemas]: SchemasMarkdown1, [markdownSections.usage]: usageMarkdown1, }, '1.0.0': { [markdownSections.introduction]: IntroMarkdown1, [markdownSections.installation]: InstallationMarkdown1, - [markdownSections.schemas]: SchemasMarkdownV2, + [markdownSections.schemas]: SchemasMarkdown2, [markdownSections.usage]: usageMarkdown1, }, '2.0.0': { [markdownSections.introduction]: IntroMarkdown3, [markdownSections.installation]: InstallationMarkdown3, - [markdownSections.schemas]: SchemasMarkdownV2, + [markdownSections.schemas]: SchemasMarkdown2, + [markdownSections.usage]: usageMarkdown3, + }, + '2.0.1': { + [markdownSections.introduction]: IntroMarkdown3, + [markdownSections.installation]: InstallationMarkdown3, + [markdownSections.schemas]: SchemasMarkdown3, [markdownSections.usage]: usageMarkdown3, }, }, diff --git a/packages/website/ts/pages/documentation/developers_page.tsx b/packages/website/ts/pages/documentation/developers_page.tsx index 30b79552f..361dbc86e 100644 --- a/packages/website/ts/pages/documentation/developers_page.tsx +++ b/packages/website/ts/pages/documentation/developers_page.tsx @@ -6,6 +6,7 @@ import { DocsLogo } from 'ts/components/documentation/docs_logo'; import { DocsTopBar } from 'ts/components/documentation/docs_top_bar'; import { Container } from 'ts/components/ui/container'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { media } from 'ts/style/media'; import { styled } from 'ts/style/theme'; import { BrowserType, OperatingSystemType, ScreenWidths } from 'ts/types'; import { Translate } from 'ts/utils/translate'; @@ -72,7 +73,6 @@ const SidebarContainer = ` ${scrollableContainerStyles} padding-top: 27px; - padding-bottom: 100px; padding-left: ${SIDEBAR_PADDING}px; padding-right: ${SIDEBAR_PADDING}px; overflow: hidden; @@ -99,14 +99,14 @@ const MainContentContainer = padding-right: ${50 - SCROLLBAR_WIDTH}px; overflow: auto; } - @media (max-width: 40em) { + ${media.small` padding-left: 20px; padding-right: 20px; &:hover { padding-right: ${20 - SCROLLBAR_WIDTH}px; overflow: auto; } - } + `} `; export class DevelopersPage extends React.Component<DevelopersPageProps, DevelopersPageState> { @@ -163,7 +163,9 @@ export class DevelopersPage extends React.Component<DevelopersPageProps, Develop </Container> </Container> <SidebarContainer onWheel={this._throttledSidebarScrolling}> - {this.props.screenWidth !== ScreenWidths.Sm && this.props.sidebar} + <Container paddingBottom="100px"> + {this.props.screenWidth !== ScreenWidths.Sm && this.props.sidebar} + </Container> </SidebarContainer> </Container> <Container diff --git a/packages/website/ts/pages/documentation/doc_page.tsx b/packages/website/ts/pages/documentation/doc_page.tsx index 7157abfc9..28bf2dba1 100644 --- a/packages/website/ts/pages/documentation/doc_page.tsx +++ b/packages/website/ts/pages/documentation/doc_page.tsx @@ -6,13 +6,13 @@ import { SupportedDocJson, TypeDocUtils, } from '@0x/react-docs'; -import { NestedSidebarMenu } from '@0x/react-shared'; import findVersions = require('find-versions'); import * as _ from 'lodash'; import CircularProgress from 'material-ui/CircularProgress'; import * as React from 'react'; import semverSort = require('semver-sort'); import { SidebarHeader } from 'ts/components/documentation/sidebar_header'; +import { NestedSidebarMenu } from 'ts/components/nested_sidebar_menu'; import { Container } from 'ts/components/ui/container'; import { DevelopersPage } from 'ts/pages/documentation/developers_page'; import { Dispatcher } from 'ts/redux/dispatcher'; diff --git a/packages/website/ts/pages/documentation/docs_home.tsx b/packages/website/ts/pages/documentation/docs_home.tsx index cf229cb3b..bbbee2d2d 100644 --- a/packages/website/ts/pages/documentation/docs_home.tsx +++ b/packages/website/ts/pages/documentation/docs_home.tsx @@ -1,8 +1,9 @@ -import { ALink, colors, Link, NestedSidebarMenu } from '@0x/react-shared'; +import { ALink, colors, Link } from '@0x/react-shared'; import { ObjectMap } from '@0x/types'; import * as _ from 'lodash'; import * as React from 'react'; import { OverviewContent } from 'ts/components/documentation/overview_content'; +import { NestedSidebarMenu } from 'ts/components/nested_sidebar_menu'; import { Button } from 'ts/components/ui/button'; import { DevelopersPage } from 'ts/pages/documentation/developers_page'; import { Dispatcher } from 'ts/redux/dispatcher'; @@ -368,7 +369,7 @@ export class DocsHome extends React.Component<DocsHomeProps, DocsHomeState> { > <Button borderRadius="4px" - padding="0.4em 6px" + padding="0.4em 0.375em" width="100%" fontColor={colors.grey800} fontSize="14px" diff --git a/packages/website/ts/pages/wiki/wiki.tsx b/packages/website/ts/pages/wiki/wiki.tsx index e62300ddf..c1802b1f8 100644 --- a/packages/website/ts/pages/wiki/wiki.tsx +++ b/packages/website/ts/pages/wiki/wiki.tsx @@ -5,7 +5,6 @@ import { HeaderSizes, Link, MarkdownSection, - NestedSidebarMenu, utils as sharedUtils, } from '@0x/react-shared'; import { ObjectMap } from '@0x/types'; @@ -13,6 +12,7 @@ import * as _ from 'lodash'; import CircularProgress from 'material-ui/CircularProgress'; import * as React from 'react'; import { SidebarHeader } from 'ts/components/documentation/sidebar_header'; +import { NestedSidebarMenu } from 'ts/components/nested_sidebar_menu'; import { Button } from 'ts/components/ui/button'; import { Container } from 'ts/components/ui/container'; import { DevelopersPage } from 'ts/pages/documentation/developers_page'; @@ -49,9 +49,6 @@ export class Wiki extends React.Component<WikiProps, WikiState> { isHoveringSidebar: false, }; } - public componentDidMount(): void { - window.addEventListener('hashchange', this._onHashChanged.bind(this), false); - } public componentWillMount(): void { // tslint:disable-next-line:no-floating-promises this._fetchArticlesBySectionAsync(); @@ -59,7 +56,6 @@ export class Wiki extends React.Component<WikiProps, WikiState> { public componentWillUnmount(): void { this._isUnmounted = true; clearTimeout(this._wikiBackoffTimeoutId); - window.removeEventListener('hashchange', this._onHashChanged.bind(this), false); } public render(): React.ReactNode { const sectionNameToLinks = _.isUndefined(this.state.articlesBySection) @@ -103,7 +99,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> { > <Button borderRadius="4px" - padding="0.4em 6px" + padding="0.4em 0.375em" width="100%" fontColor={colors.grey800} fontSize="14px" @@ -198,8 +194,4 @@ export class Wiki extends React.Component<WikiProps, WikiState> { } return sectionNameToLinks; } - private _onHashChanged(_event: any): void { - const hash = window.location.hash.slice(1); - sharedUtils.scrollToHash(hash, sharedConstants.SCROLL_CONTAINER_ID); - } } diff --git a/python-packages/order_utils/setup.py b/python-packages/order_utils/setup.py index a76d724aa..1a094cfe1 100644..100755 --- a/python-packages/order_utils/setup.py +++ b/python-packages/order_utils/setup.py @@ -1,13 +1,16 @@ +#!/usr/bin/env python + """setuptools module for order_utils package.""" import subprocess # nosec from shutil import rmtree -from os import path, remove, walk +from os import environ, path, remove, walk +from sys import argv -from distutils.command.clean import clean # type: ignore -from setuptools import setup # type: ignore -import setuptools.command.build_py # type: ignore -from setuptools.command.test import test as TestCommand # type: ignore +from distutils.command.clean import clean +import distutils.command.build_py +from setuptools import setup +from setuptools.command.test import test as TestCommand class TestCommandExtension(TestCommand): @@ -15,13 +18,13 @@ class TestCommandExtension(TestCommand): def run_tests(self): """Invoke pytest.""" - import pytest # type: ignore + import pytest pytest.main() # pylint: disable=too-many-ancestors -class LintCommand(setuptools.command.build_py.build_py): +class LintCommand(distutils.command.build_py.build_py): """Custom setuptools command class for running linters.""" def run(self): @@ -34,7 +37,7 @@ class LintCommand(setuptools.command.build_py.build_py): # docstring style checker: "pydocstyle src test setup.py".split(), # static type checker: - "mypy src setup.py".split(), + "mypy src test setup.py".split(), # security issue checker: "bandit -r src ./setup.py".split(), # general linter: @@ -42,6 +45,21 @@ class LintCommand(setuptools.command.build_py.build_py): # pylint takes relatively long to run, so it runs last, to enable # fast failures. ] + + # tell mypy where to find interface stubs for 3rd party libs + environ["MYPYPATH"] = path.join( + path.dirname(path.realpath(argv[0])), "stubs" + ) + + # HACK(gene): until eth_abi releases + # https://github.com/ethereum/eth-abi/pull/107 , we need to simply + # create an empty file `py.typed` in the eth_abi package directory. + import eth_abi + + eth_abi_dir = path.dirname(path.realpath(eth_abi.__file__)) + with open(path.join(eth_abi_dir, "py.typed"), "a"): + pass + for lint_command in lint_commands: print( "Running lint command `", " ".join(lint_command).strip(), "`" @@ -79,7 +97,7 @@ setup( "test": TestCommandExtension, }, include_package_data=True, - install_requires=["web3"], + install_requires=["eth-abi", "web3"], extras_require={ "dev": [ "bandit", @@ -87,6 +105,7 @@ setup( "coverage", "coveralls", "mypy", + "mypy_extensions", "pycodestyle", "pydocstyle", "pylint", @@ -118,7 +137,7 @@ setup( "Topic :: Software Development :: Libraries", "Topic :: Utilities", ], - zip_safe=False, + zip_safe=False, # required per mypy command_options={ "build_sphinx": { "source_dir": ("setup.py", "src"), diff --git a/python-packages/order_utils/src/conf.py b/python-packages/order_utils/src/conf.py index f3f15967c..e74a29d00 100644 --- a/python-packages/order_utils/src/conf.py +++ b/python-packages/order_utils/src/conf.py @@ -2,6 +2,9 @@ # Reference: http://www.sphinx-doc.org/en/master/config +from typing import List + + # pylint: disable=invalid-name # because these variables are not named in upper case, as globals should be. @@ -29,7 +32,7 @@ master_doc = "index" # The master toctree document. language = None -exclude_patterns = [] # type: ignore +exclude_patterns: List[str] = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None diff --git a/python-packages/order_utils/src/index.rst b/python-packages/order_utils/src/index.rst index cbc4c8409..e09abcca2 100644 --- a/python-packages/order_utils/src/index.rst +++ b/python-packages/order_utils/src/index.rst @@ -10,9 +10,12 @@ order_utils.py .. automodule:: zero_ex.order_utils :members: -.. automodule:: zero_ex.order_utils.signature_utils +.. automodule:: zero_ex.order_utils.asset_data_utils :members: +.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData + +See source for properties. Sphinx does not easily generate class property docs; pull requests welcome. Indices and tables ================== diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py b/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py new file mode 100644 index 000000000..b6a224d2c --- /dev/null +++ b/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py @@ -0,0 +1 @@ +"""Dev utils to be shared across 0x projects and packages.""" 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 new file mode 100644 index 000000000..71b6128ca --- /dev/null +++ b/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py @@ -0,0 +1,102 @@ +"""Ethereum ABI utilities. + +Builds on the eth-abi package, adding some convenience methods like those found +in npmjs.com/package/ethereumjs-abi. Ideally, all of this code should be +pushed upstream into eth-abi. +""" + +import re +from typing import Any, List + +from mypy_extensions import TypedDict + +from eth_abi import encode_abi +from web3 import Web3 + +from .type_assertions import assert_is_string, assert_is_list + + +class MethodSignature(TypedDict, total=False): + """Object interface to an ABI method signature.""" + + method: str + args: List[str] + + +def parse_signature(signature: str) -> MethodSignature: + """Parse a method signature into its constituent parts. + + >>> parse_signature("ERC20Token(address)") + {'method': 'ERC20Token', 'args': ['address']} + """ + assert_is_string(signature, "signature") + + matches = re.match(r"^(\w+)\((.+)\)$", signature) + if matches is None: + raise ValueError(f"Invalid method signature {signature}") + return {"method": matches[1], "args": matches[2].split(",")} + + +def elementary_name(name: str) -> str: + """Convert from short to canonical names; barely implemented. + + Modeled after ethereumjs-abi's ABI.elementaryName(), but only implemented + to support our particular use case and a few other simple ones. + + >>> elementary_name("address") + 'address' + >>> elementary_name("uint") + 'uint256' + """ + assert_is_string(name, "name") + + return { + "int": "int256", + "uint": "uint256", + "fixed": "fixed128x128", + "ufixed": "ufixed128x128", + }.get(name, name) + + +def event_id(name: str, types: List[str]) -> str: + """Return the Keccak-256 hash of the given method. + + >>> event_id("ERC20Token", ["address"]) + '0xf47261b06eedbfce68afd46d0f3c27c60b03faad319eaf33103611cf8f6456ad' + """ + assert_is_string(name, "name") + assert_is_list(types, "types") + + signature = f"{name}({','.join(list(map(elementary_name, types)))})" + return Web3.sha3(text=signature).hex() + + +def method_id(name: str, types: List[str]) -> str: + """Return the 4-byte method identifier. + + >>> method_id("ERC20Token", ["address"]) + '0xf47261b0' + """ + assert_is_string(name, "name") + assert_is_list(types, "types") + + return event_id(name, types)[0:10] + + +def simple_encode(method: str, *args: Any) -> bytes: + # docstring considered all one line by pylint: disable=line-too-long + r"""Encode a method ABI. + + >>> simple_encode("ERC20Token(address)", "0x1dc4c1cefef38a777b15aa20260a54e584b16c48") + b'\xf4ra\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\xc4\xc1\xce\xfe\xf3\x8aw{\x15\xaa &\nT\xe5\x84\xb1lH' + """ # noqa: E501 (line too long) + assert_is_string(method, "method") + + signature: MethodSignature = parse_signature(method) + + return bytes.fromhex( + ( + method_id(signature["method"], signature["args"]) + + encode_abi(signature["args"], args).hex() + )[2:] + ) diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py new file mode 100644 index 000000000..745d014e6 --- /dev/null +++ b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py @@ -0,0 +1,33 @@ +"""Assertions for runtime type checking of function arguments.""" + +from typing import Any + + +def assert_is_string(value: Any, name: str) -> None: + """If :param value: isn't of type str, raise a TypeError. + + >>> try: assert_is_string(123, 'var') + ... except TypeError as type_error: print(str(type_error)) + ... + expected variable 'var', with value 123, to have type 'str', not 'int' + """ + if not isinstance(value, str): + raise TypeError( + f"expected variable '{name}', with value {str(value)}, to have" + + f" type 'str', not '{type(value).__name__}'" + ) + + +def assert_is_list(value: Any, name: str) -> None: + """If :param value: isn't of type list, raise a TypeError. + + >>> try: assert_is_list(123, 'var') + ... except TypeError as type_error: print(str(type_error)) + ... + expected variable 'var', with value 123, to have type 'list', not 'int' + """ + if not isinstance(value, list): + raise TypeError( + f"expected variable '{name}', with value {str(value)}, to have" + + f" type 'list', not '{type(value).__name__}'" + ) diff --git a/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py new file mode 100644 index 000000000..451de39af --- /dev/null +++ b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py @@ -0,0 +1,72 @@ +"""Asset data encoding and decoding utilities.""" + +from mypy_extensions import TypedDict + +import eth_abi + +from zero_ex.dev_utils import abi_utils +from zero_ex.dev_utils.type_assertions import assert_is_string + + +ERC20_ASSET_DATA_BYTE_LENGTH = 36 +SELECTOR_LENGTH = 10 + + +class ERC20AssetData(TypedDict): + """Object interface to ERC20 asset data.""" + + asset_proxy_id: str + token_address: str + + +def encode_erc20_asset_data(token_address: str) -> str: + """Encode an ERC20 token address into an asset data string. + + :param token_address: the ERC20 token's contract address. + :rtype: hex encoded asset data string, usable in the makerAssetData or + takerAssetData fields in a 0x order. + + >>> encode_erc20_asset_data('0x1dc4c1cefef38a777b15aa20260a54e584b16c48') + '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48' + """ + assert_is_string(token_address, "token_address") + + return ( + "0x" + + abi_utils.simple_encode("ERC20Token(address)", token_address).hex() + ) + + +def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData: + # docstring considered all one line by pylint: disable=line-too-long + """Decode an ERC20 assetData hex string. + + :param asset_data: String produced by prior call to encode_erc20_asset_data() + + >>> decode_erc20_asset_data("0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48") + {'asset_proxy_id': '0xf47261b0', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'} + """ # noqa: E501 (line too long) + assert_is_string(asset_data, "asset_data") + + if len(asset_data) < ERC20_ASSET_DATA_BYTE_LENGTH: + raise ValueError( + "Could not decode ERC20 Proxy Data. Expected length of encoded" + + f" data to be at least {str(ERC20_ASSET_DATA_BYTE_LENGTH)}." + + f" Got {str(len(asset_data))}." + ) + + asset_proxy_id: str = asset_data[0:10] + if asset_proxy_id != abi_utils.method_id("ERC20Token", ["address"]): + raise ValueError( + "Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be" + + f" ERC20 ({abi_utils.method_id('ERC20Token', ['address'])})" + + f" but got {asset_proxy_id}." + ) + + # workaround for https://github.com/PyCQA/pylint/issues/1498 + # pylint: disable=unsubscriptable-object + token_address = eth_abi.decode_abi( + ["address"], bytes.fromhex(asset_data[SELECTOR_LENGTH:]) + )[0] + + return {"asset_proxy_id": asset_proxy_id, "token_address": token_address} 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 deleted file mode 100644 index 7f4697106..000000000 --- a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Signature utilities.""" - - -def ec_sign_order_hash(): - """Signs an orderHash. - - Returns its elliptic curve signature and signature type. This method - currently supports TestRPC, Geth, and Parity above and below v1.6.6. - - >>> ec_sign_order_hash() - 'stub return value' - """ - return "stub return value" diff --git a/python-packages/order_utils/stubs/distutils/__init__.pyi b/python-packages/order_utils/stubs/distutils/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/distutils/__init__.pyi diff --git a/python-packages/order_utils/stubs/distutils/command/__init__.pyi b/python-packages/order_utils/stubs/distutils/command/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/distutils/command/__init__.pyi diff --git a/python-packages/order_utils/stubs/distutils/command/clean.pyi b/python-packages/order_utils/stubs/distutils/command/clean.pyi new file mode 100644 index 000000000..46a42ddb1 --- /dev/null +++ b/python-packages/order_utils/stubs/distutils/command/clean.pyi @@ -0,0 +1,7 @@ +from distutils.core import Command + +class clean(Command): + def initialize_options(self: clean) -> None: ... + def finalize_options(self: clean) -> None: ... + def run(self: clean) -> None: ... + ... diff --git a/python-packages/order_utils/stubs/pytest/__init__.pyi b/python-packages/order_utils/stubs/pytest/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/pytest/__init__.pyi diff --git a/python-packages/order_utils/stubs/pytest/raises.pyi b/python-packages/order_utils/stubs/pytest/raises.pyi new file mode 100644 index 000000000..2e3b29f3d --- /dev/null +++ b/python-packages/order_utils/stubs/pytest/raises.pyi @@ -0,0 +1 @@ +def raises(exception: Exception) -> ExceptionInfo: ... diff --git a/python-packages/order_utils/stubs/setuptools/__init__.pyi b/python-packages/order_utils/stubs/setuptools/__init__.pyi new file mode 100644 index 000000000..baa349d70 --- /dev/null +++ b/python-packages/order_utils/stubs/setuptools/__init__.pyi @@ -0,0 +1,6 @@ +from distutils.dist import Distribution +from typing import Any + +def setup(**attrs: Any) -> Distribution: ... + +class Command: ... diff --git a/python-packages/order_utils/stubs/setuptools/command/__init__.pyi b/python-packages/order_utils/stubs/setuptools/command/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/setuptools/command/__init__.pyi diff --git a/python-packages/order_utils/stubs/setuptools/command/test.pyi b/python-packages/order_utils/stubs/setuptools/command/test.pyi new file mode 100644 index 000000000..c5ec770ad --- /dev/null +++ b/python-packages/order_utils/stubs/setuptools/command/test.pyi @@ -0,0 +1,3 @@ +from setuptools import Command + +class test(Command): ... diff --git a/python-packages/order_utils/stubs/web3/__init__.pyi b/python-packages/order_utils/stubs/web3/__init__.pyi new file mode 100644 index 000000000..c6f357009 --- /dev/null +++ b/python-packages/order_utils/stubs/web3/__init__.pyi @@ -0,0 +1,10 @@ +from typing import Optional, Union + +class Web3: + @staticmethod + def sha3( + primitive: Optional[Union[bytes, int, None]] = None, + text: Optional[str] = None, + hexstr: Optional[str] = None + ) -> bytes: ... + ... diff --git a/python-packages/order_utils/test/test_abi_utils.py b/python-packages/order_utils/test/test_abi_utils.py new file mode 100644 index 000000000..49a2a4f20 --- /dev/null +++ b/python-packages/order_utils/test/test_abi_utils.py @@ -0,0 +1,53 @@ +"""Tests of 0x.abi_utils.""" + +import pytest + +from zero_ex.dev_utils.abi_utils import ( + elementary_name, + event_id, + method_id, + parse_signature, + simple_encode, +) + + +def test_parse_signature_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + parse_signature(123) + + +def test_parse_signature_bad_input(): + """Test that passing a non-signature string raises a ValueError.""" + with pytest.raises(ValueError): + parse_signature("a string that's not even close to a signature") + + +def test_elementary_name_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + elementary_name(123) + + +def test_event_id_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + event_id(123, []) + + with pytest.raises(TypeError): + event_id("valid string", 123) + + +def test_method_id_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + method_id(123, []) + + with pytest.raises(TypeError): + method_id("ERC20Token", 123) + + +def test_simple_encode_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + simple_encode(123) diff --git a/python-packages/order_utils/test/test_asset_data_utils.py b/python-packages/order_utils/test/test_asset_data_utils.py new file mode 100644 index 000000000..eeada5873 --- /dev/null +++ b/python-packages/order_utils/test/test_asset_data_utils.py @@ -0,0 +1,35 @@ +"""Tests of 0x.order_utils.asset_data_utils.""" + +import pytest + +from zero_ex.order_utils.asset_data_utils import ( + encode_erc20_asset_data, + decode_erc20_asset_data, + ERC20_ASSET_DATA_BYTE_LENGTH, +) + + +def test_encode_erc20_asset_data_type_error(): + """Test that passing in a non-string raises a TypeError.""" + with pytest.raises(TypeError): + encode_erc20_asset_data(123) + + +def test_decode_erc20_asset_data_type_error(): + """Test that passing in a non-string raises a TypeError.""" + with pytest.raises(TypeError): + decode_erc20_asset_data(123) + + +def test_decode_erc20_asset_data_too_short(): + """Test that passing an insufficiently long string raises a ValueError.""" + with pytest.raises(ValueError): + decode_erc20_asset_data(" " * (ERC20_ASSET_DATA_BYTE_LENGTH - 1)) + + +def test_decode_erc20_asset_data_invalid_proxy_id(): + """Test that passing data with an invalid proxy ID raises a ValueError.""" + with pytest.raises(ValueError): + decode_erc20_asset_data( + "0xffffffff" + (" " * ERC20_ASSET_DATA_BYTE_LENGTH) + ) diff --git a/python-packages/order_utils/test/test_doctest.py b/python-packages/order_utils/test/test_doctest.py index a0e61f84a..ba5da5418 100644 --- a/python-packages/order_utils/test/test_doctest.py +++ b/python-packages/order_utils/test/test_doctest.py @@ -1,10 +1,24 @@ """Exercise doctests for order_utils module.""" from doctest import testmod -from zero_ex.order_utils import signature_utils +from zero_ex.dev_utils import abi_utils, type_assertions +from zero_ex.order_utils import asset_data_utils -def test_doctest(): - """Invoke doctest on the module.""" - (failure_count, _) = testmod(signature_utils) + +def test_doctest_asset_data_utils(): + """Invoke doctest on the asset_data_utils module.""" + (failure_count, _) = testmod(asset_data_utils) + assert failure_count == 0 + + +def test_doctest_abi_utils(): + """Invoke doctest on the abi_utils module.""" + (failure_count, _) = testmod(abi_utils) + assert failure_count == 0 + + +def test_doctest_type_assertions(): + """Invoke doctest on the type_assertions module.""" + (failure_count, _) = testmod(type_assertions) assert failure_count == 0 diff --git a/python-packages/order_utils/test/test_signature_utils.py b/python-packages/order_utils/test/test_signature_utils.py deleted file mode 100644 index 7e830f9f8..000000000 --- a/python-packages/order_utils/test/test_signature_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Tests of 0x.order_utils.signature_utils.*.""" - -from zero_ex.order_utils.signature_utils import ec_sign_order_hash - - -def test_ec_sign_order_hash(): - """Test the signing of order hashes.""" - assert ec_sign_order_hash() == "stub return value" |