diff options
106 files changed, 4387 insertions, 632 deletions
diff --git a/.github/stale.yml b/.github/stale.yml index af12c62d5..09eb40a77 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Number of days of inactivity before an issue becomes stale daysUntilStale: 30 # Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 +daysUntilClose: 30 # Issues with these labels will never be considered stale exemptLabels: - pinned diff --git a/contracts/examples/package.json b/contracts/examples/package.json index 77846241e..37b73f98c 100644 --- a/contracts/examples/package.json +++ b/contracts/examples/package.json @@ -13,7 +13,8 @@ "build": "yarn pre_build && tsc -b", "build:ci": "yarn build", "pre_build": "run-s compile generate_contract_wrappers", - "compile": "sol-compiler --contracts-dir contracts", + "compile": "sol-compiler", + "watch": "sol-compiler -w", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", diff --git a/contracts/extensions/CHANGELOG.json b/contracts/extensions/CHANGELOG.json index 19ac770af..da4d9c2ba 100644 --- a/contracts/extensions/CHANGELOG.json +++ b/contracts/extensions/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "1.1.0", + "changes": [ + { + "note": "Added Balance Threshold Filter", + "pr": 1383 + } + ] + }, + { "timestamp": 1544741676, "version": "1.0.2", "changes": [ diff --git a/contracts/extensions/compiler.json b/contracts/extensions/compiler.json index 69d607b3e..e6ed0c215 100644 --- a/contracts/extensions/compiler.json +++ b/contracts/extensions/compiler.json @@ -18,5 +18,5 @@ } } }, - "contracts": ["DutchAuction", "Forwarder"] + "contracts": ["BalanceThresholdFilter", "DutchAuction", "Forwarder"] } diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol b/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol new file mode 100644 index 000000000..16cacd461 --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/BalanceThresholdFilter.sol @@ -0,0 +1,45 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol"; +import "./interfaces/IThresholdAsset.sol"; +import "./MixinBalanceThresholdFilterCore.sol"; + + +contract BalanceThresholdFilter is + MixinBalanceThresholdFilterCore +{ + + /// @dev Constructs BalanceThresholdFilter. + /// @param exchange Address of 0x exchange. + /// @param thresholdAsset The asset that must be held by makers/takers. + /// @param balanceThreshold The minimum balance of `thresholdAsset` that must be held by makers/takers. + constructor( + address exchange, + address thresholdAsset, + uint256 balanceThreshold + ) + public + { + EXCHANGE = IExchange(exchange); + THRESHOLD_ASSET = IThresholdAsset(thresholdAsset); + BALANCE_THRESHOLD = balanceThreshold; + } +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol new file mode 100644 index 000000000..df830f36e --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/MixinBalanceThresholdFilterCore.sol @@ -0,0 +1,135 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "@0x/contracts-libs/contracts/libs/LibExchangeSelectors.sol"; +import "@0x/contracts-libs/contracts/libs/LibOrder.sol"; +import "./mixins/MBalanceThresholdFilterCore.sol"; +import "./MixinExchangeCalldata.sol"; + + +contract MixinBalanceThresholdFilterCore is + MBalanceThresholdFilterCore, + MixinExchangeCalldata, + LibOrder, + LibExchangeSelectors +{ + + /// @dev Executes an Exchange transaction iff the maker and taker meet + /// the hold at least `BALANCE_THRESHOLD` of the asset `THRESHOLD_ASSET` OR + /// the exchange function is a cancellation. + /// Supported Exchange functions: + /// batchFillOrders + /// batchFillOrdersNoThrow + /// batchFillOrKillOrders + /// fillOrder + /// fillOrderNoThrow + /// fillOrKillOrder + /// marketBuyOrders + /// marketBuyOrdersNoThrow + /// marketSellOrders + /// marketSellOrdersNoThrow + /// matchOrders + /// cancelOrder + /// batchCancelOrders + /// cancelOrdersUpTo + /// Trying to call any other exchange function will throw. + /// @param salt Arbitrary number to ensure uniqueness of transaction hash. + /// @param signerAddress Address of transaction signer. + /// @param signedExchangeTransaction AbiV2 encoded calldata. + /// @param signature Proof of signer transaction by signer. + function executeTransaction( + uint256 salt, + address signerAddress, + bytes signedExchangeTransaction, + bytes signature + ) + external + { + // Get accounts whose balances must be validated + address[] memory addressesToValidate = getAddressesToValidate(signerAddress); + + // Validate account balances + uint256 balanceThreshold = BALANCE_THRESHOLD; + IThresholdAsset thresholdAsset = THRESHOLD_ASSET; + for (uint256 i = 0; i < addressesToValidate.length; ++i) { + uint256 addressBalance = thresholdAsset.balanceOf(addressesToValidate[i]); + require( + addressBalance >= balanceThreshold, + "AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD" + ); + } + emit ValidatedAddresses(addressesToValidate); + + // All addresses are valid. Execute exchange function. + EXCHANGE.executeTransaction( + salt, + signerAddress, + signedExchangeTransaction, + signature + ); + } + + /// @dev Constructs an array of addresses to be validated. + /// Addresses depend on which Exchange function is to be called + /// (defined by `signedExchangeTransaction` above). + /// @param signerAddress Address of transaction signer. + /// @return addressesToValidate Array of addresses to validate. + function getAddressesToValidate(address signerAddress) + internal pure + returns (address[] memory addressesToValidate) + { + bytes4 exchangeFunctionSelector = bytes4(exchangeCalldataload(0)); + // solhint-disable expression-indent + if ( + exchangeFunctionSelector == BATCH_FILL_ORDERS_SELECTOR || + exchangeFunctionSelector == BATCH_FILL_ORDERS_NO_THROW_SELECTOR || + exchangeFunctionSelector == BATCH_FILL_OR_KILL_ORDERS_SELECTOR || + exchangeFunctionSelector == MARKET_BUY_ORDERS_SELECTOR || + exchangeFunctionSelector == MARKET_BUY_ORDERS_NO_THROW_SELECTOR || + exchangeFunctionSelector == MARKET_SELL_ORDERS_SELECTOR || + exchangeFunctionSelector == MARKET_SELL_ORDERS_NO_THROW_SELECTOR + ) { + addressesToValidate = loadMakerAddressesFromOrderArray(0); + addressesToValidate = addressesToValidate.append(signerAddress); + } else if ( + exchangeFunctionSelector == FILL_ORDER_SELECTOR || + exchangeFunctionSelector == FILL_ORDER_NO_THROW_SELECTOR || + exchangeFunctionSelector == FILL_OR_KILL_ORDER_SELECTOR + ) { + address makerAddress = loadMakerAddressFromOrder(0); + addressesToValidate = addressesToValidate.append(makerAddress); + addressesToValidate = addressesToValidate.append(signerAddress); + } else if (exchangeFunctionSelector == MATCH_ORDERS_SELECTOR) { + address leftMakerAddress = loadMakerAddressFromOrder(0); + addressesToValidate = addressesToValidate.append(leftMakerAddress); + address rightMakerAddress = loadMakerAddressFromOrder(1); + addressesToValidate = addressesToValidate.append(rightMakerAddress); + addressesToValidate = addressesToValidate.append(signerAddress); + } else if ( + exchangeFunctionSelector != CANCEL_ORDER_SELECTOR && + exchangeFunctionSelector != BATCH_CANCEL_ORDERS_SELECTOR && + exchangeFunctionSelector != CANCEL_ORDERS_UP_TO_SELECTOR + ) { + revert("INVALID_OR_BLOCKED_EXCHANGE_SELECTOR"); + } + // solhint-enable expression-indent + return addressesToValidate; + } +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol b/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol new file mode 100644 index 000000000..bd26a468f --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/MixinExchangeCalldata.sol @@ -0,0 +1,103 @@ + + /* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "./mixins/MExchangeCalldata.sol"; +import "@0x/contracts-libs/contracts/libs/LibAddressArray.sol"; + + +contract MixinExchangeCalldata is + MExchangeCalldata +{ + + using LibAddressArray for address[]; + + /// @dev Emulates the `calldataload` opcode on the embedded Exchange calldata, + /// which is accessed through `signedExchangeTransaction`. + /// @param offset Offset into the Exchange calldata. + /// @return value Corresponding 32 byte value stored at `offset`. + function exchangeCalldataload(uint256 offset) + internal pure + returns (bytes32 value) + { + assembly { + // Pointer to exchange transaction + // 0x04 for calldata selector + // 0x40 to access `signedExchangeTransaction`, which is the third parameter + let exchangeTxPtr := calldataload(0x44) + + // Offset into Exchange calldata + // We compute this by adding 0x24 to the `exchangeTxPtr` computed above. + // 0x04 for calldata selector + // 0x20 for length field of `signedExchangeTransaction` + let exchangeCalldataOffset := add(exchangeTxPtr, add(0x24, offset)) + value := calldataload(exchangeCalldataOffset) + } + return value; + } + + /// @dev Convenience function that skips the 4 byte selector when loading + /// from the embedded Exchange calldata. + /// @param offset Offset into the Exchange calldata (minus the 4 byte selector) + /// @return value Corresponding 32 byte value stored at `offset` + 4. + function loadExchangeData(uint256 offset) + internal pure + returns (bytes32 value) + { + value = exchangeCalldataload(offset + 4); + return value; + } + + /// @dev Extracts the maker address from an order stored in the Exchange calldata + /// (which is embedded in `signedExchangeTransaction`). + /// @param orderParamIndex Index of the order in the Exchange function's signature. + /// @return makerAddress The extracted maker address. + function loadMakerAddressFromOrder(uint256 orderParamIndex) + internal pure + returns (address makerAddress) + { + uint256 orderOffsetInBytes = orderParamIndex * 32; + uint256 orderPtr = uint256(loadExchangeData(orderOffsetInBytes)); + makerAddress = address(loadExchangeData(orderPtr)); + return makerAddress; + } + + /// @dev Extracts the maker addresses from an array of orders stored in the Exchange calldata + /// (which is embedded in `signedExchangeTransaction`). + /// @param orderArrayParamIndex Index of the order array in the Exchange function's signature + /// @return makerAddresses The extracted maker addresses. + function loadMakerAddressesFromOrderArray(uint256 orderArrayParamIndex) + internal pure + returns (address[] makerAddresses) + { + uint256 orderArrayOffsetInBytes = orderArrayParamIndex * 32; + uint256 orderArrayPtr = uint256(loadExchangeData(orderArrayOffsetInBytes)); + uint256 orderArrayLength = uint256(loadExchangeData(orderArrayPtr)); + uint256 orderArrayLengthInBytes = orderArrayLength * 32; + uint256 orderArrayElementPtr = orderArrayPtr + 32; + uint256 orderArrayElementEndPtr = orderArrayElementPtr + orderArrayLengthInBytes; + for (uint orderPtrOffset = orderArrayElementPtr; orderPtrOffset < orderArrayElementEndPtr; orderPtrOffset += 32) { + uint256 orderPtr = uint256(loadExchangeData(orderPtrOffset)); + address makerAddress = address(loadExchangeData(orderPtr + orderArrayElementPtr)); + makerAddresses = makerAddresses.append(makerAddress); + } + return makerAddresses; + } +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol new file mode 100644 index 000000000..3d8e2bbd1 --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IBalanceThresholdFilterCore.sol @@ -0,0 +1,55 @@ + +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + + +contract IBalanceThresholdFilterCore { + + /// @dev Executes an Exchange transaction iff the maker and taker meet + /// the hold at least `BALANCE_THRESHOLD` of the asset `THRESHOLD_ASSET` OR + /// the exchange function is a cancellation. + /// Supported Exchange functions: + /// - batchFillOrders + /// - batchFillOrdersNoThrow + /// - batchFillOrKillOrders + /// - fillOrder + /// - fillOrderNoThrow + /// - fillOrKillOrder + /// - marketBuyOrders + /// - marketBuyOrdersNoThrow + /// - marketSellOrders + /// - marketSellOrdersNoThrow + /// - matchOrders + /// - cancelOrder + /// - batchCancelOrders + /// - cancelOrdersUpTo + /// Trying to call any other exchange function will throw. + /// @param salt Arbitrary number to ensure uniqueness of transaction hash. + /// @param signerAddress Address of transaction signer. + /// @param signedExchangeTransaction AbiV2 encoded calldata. + /// @param signature Proof of signer transaction by signer. + function executeTransaction( + uint256 salt, + address signerAddress, + bytes signedExchangeTransaction, + bytes signature + ) + external; +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol new file mode 100644 index 000000000..3e424b9f4 --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/interfaces/IThresholdAsset.sol @@ -0,0 +1,31 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + + +contract IThresholdAsset { + + /// @param _owner The address from which the balance will be retrieved + /// @return Balance of owner + function balanceOf(address _owner) + external + view + returns (uint256); + +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol new file mode 100644 index 000000000..b8b67e6ee --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MBalanceThresholdFilterCore.sol @@ -0,0 +1,54 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "@0x/contracts-interfaces/contracts/protocol/Exchange/IExchange.sol"; +import "../interfaces/IThresholdAsset.sol"; +import "../interfaces/IBalanceThresholdFilterCore.sol"; + + +contract MBalanceThresholdFilterCore is + IBalanceThresholdFilterCore +{ + + // Points to 0x exchange contract + // solhint-disable var-name-mixedcase + IExchange internal EXCHANGE; + + // The asset that must be held by makers/takers + IThresholdAsset internal THRESHOLD_ASSET; + + // The minimum balance of `THRESHOLD_ASSET` that must be held by makers/takers + uint256 internal BALANCE_THRESHOLD; + // solhint-enable var-name-mixedcase + + // Addresses that hold at least `BALANCE_THRESHOLD` of `THRESHOLD_ASSET` + event ValidatedAddresses ( + address[] addresses + ); + + /// @dev Constructs an array of addresses to be validated. + /// Addresses depend on which Exchange function is to be called + /// (defined by `signedExchangeTransaction` above). + /// @param signerAddress Address of transaction signer. + /// @return addressesToValidate Array of addresses to validate. + function getAddressesToValidate(address signerAddress) + internal pure + returns (address[] memory addressesToValidate); +} diff --git a/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol new file mode 100644 index 000000000..bf2940fe1 --- /dev/null +++ b/contracts/extensions/contracts/BalanceThresholdFilter/mixins/MExchangeCalldata.sol @@ -0,0 +1,56 @@ + + /* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + + +contract MExchangeCalldata { + + /// @dev Emulates the `calldataload` opcode on the embedded Exchange calldata, + /// which is accessed through `signedExchangeTransaction`. + /// @param offset Offset into the Exchange calldata. + /// @return value Corresponding 32 byte value stored at `offset`. + function exchangeCalldataload(uint256 offset) + internal pure + returns (bytes32 value); + + /// @dev Convenience function that skips the 4 byte selector when loading + /// from the embedded Exchange calldata. + /// @param offset Offset into the Exchange calldata (minus the 4 byte selector) + /// @return value Corresponding 32 byte value stored at `offset` + 4. + function loadExchangeData(uint256 offset) + internal pure + returns (bytes32 value); + + /// @dev Extracts the maker address from an order stored in the Exchange calldata + /// (which is embedded in `signedExchangeTransaction`). + /// @param orderParamIndex Index of the order in the Exchange function's signature. + /// @return makerAddress The extracted maker address. + function loadMakerAddressFromOrder(uint256 orderParamIndex) + internal pure + returns (address makerAddress); + + /// @dev Extracts the maker addresses from an array of orders stored in the Exchange calldata + /// (which is embedded in `signedExchangeTransaction`). + /// @param orderArrayParamIndex Index of the order array in the Exchange function's signature + /// @return makerAddresses The extracted maker addresses. + function loadMakerAddressesFromOrderArray(uint256 orderArrayParamIndex) + internal pure + returns (address[] makerAddresses); +} diff --git a/contracts/extensions/package.json b/contracts/extensions/package.json index 938e1138c..aa5cef462 100644 --- a/contracts/extensions/package.json +++ b/contracts/extensions/package.json @@ -19,7 +19,8 @@ "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", - "compile": "sol-compiler --contracts-dir contracts", + "compile": "sol-compiler", + "watch": "sol-compiler -w", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", @@ -31,7 +32,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "generated-artifacts/@(DutchAuction|Forwarder).json" + "abis": "generated-artifacts/@(BalanceThresholdFilter|DutchAuction|Forwarder).json" }, "repository": { "type": "git", diff --git a/contracts/extensions/src/artifacts/index.ts b/contracts/extensions/src/artifacts/index.ts index 7588178f0..ebf0b8050 100644 --- a/contracts/extensions/src/artifacts/index.ts +++ b/contracts/extensions/src/artifacts/index.ts @@ -1,9 +1,11 @@ import { ContractArtifact } from 'ethereum-types'; +import * as BalanceThresholdFilter from '../../generated-artifacts/BalanceThresholdFilter.json'; import * as DutchAuction from '../../generated-artifacts/DutchAuction.json'; import * as Forwarder from '../../generated-artifacts/Forwarder.json'; export const artifacts = { + BalanceThresholdFilter: BalanceThresholdFilter as ContractArtifact, DutchAuction: DutchAuction as ContractArtifact, Forwarder: Forwarder as ContractArtifact, }; diff --git a/contracts/extensions/src/wrappers/index.ts b/contracts/extensions/src/wrappers/index.ts index 90880e37f..8a8122caa 100644 --- a/contracts/extensions/src/wrappers/index.ts +++ b/contracts/extensions/src/wrappers/index.ts @@ -1,2 +1,3 @@ +export * from '../../generated-wrappers/balance_threshold_filter'; export * from '../../generated-wrappers/dutch_auction'; export * from '../../generated-wrappers/forwarder'; diff --git a/contracts/extensions/test/extensions/balance_threshold_filter.ts b/contracts/extensions/test/extensions/balance_threshold_filter.ts new file mode 100644 index 000000000..07199d60b --- /dev/null +++ b/contracts/extensions/test/extensions/balance_threshold_filter.ts @@ -0,0 +1,1644 @@ +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { Order, RevertReason, SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as chai from 'chai'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { + artifacts as protocolArtifacts, + ERC20Wrapper, + ERC721Wrapper, + ExchangeContract, + ExchangeWrapper, +} from '@0x/contracts-protocol'; +import { + chaiSetup, + constants, + ContractName, + ERC20BalancesByOwner, + expectTransactionFailedAsync, + OrderFactory, + OrderStatus, + provider, + TransactionFactory, + txDefaults, + web3Wrapper, +} from '@0x/contracts-test-utils'; +import { DummyERC20TokenContract } from '@0x/contracts-tokens'; + +import { BalanceThresholdFilterContract } from '../../generated-wrappers/balance_threshold_filter'; +import { artifacts } from '../../src/artifacts'; +import { BalanceThresholdWrapper } from '../utils/balance_threshold_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const DECIMALS_DEFAULT = 18; + +interface ValidatedAddressesLog { + args: { addresses: string[] }; +} + +describe(ContractName.BalanceThresholdFilter, () => { + const takerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(500), DECIMALS_DEFAULT); + const makerAssetAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1000), DECIMALS_DEFAULT); + const takerAssetFillAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(250), DECIMALS_DEFAULT); + + let validMakerAddress: string; + let validMakerAddress2: string; + let owner: string; + let validTakerAddress: string; + let feeRecipientAddress: string; + let invalidAddress: string; + let defaultMakerAssetAddress: string; + let defaultTakerAssetAddress: string; + let zrxAssetData: string; + let zrxToken: DummyERC20TokenContract; + let exchangeInstance: ExchangeContract; + let exchangeWrapper: ExchangeWrapper; + + let orderFactory: OrderFactory; + let orderFactory2: OrderFactory; + let invalidOrderFactory: OrderFactory; + let erc20Wrapper: ERC20Wrapper; + let erc20Balances: ERC20BalancesByOwner; + let erc20TakerBalanceThresholdWrapper: BalanceThresholdWrapper; + let erc721TakerBalanceThresholdWrapper: BalanceThresholdWrapper; + let erc721MakerBalanceThresholdWrapper: BalanceThresholdWrapper; + let erc721NonValidBalanceThresholdWrapper: BalanceThresholdWrapper; + + let defaultOrderParams: Partial<Order>; + let validSignedOrder: SignedOrder; + let validSignedOrder2: SignedOrder; + + let erc721BalanceThresholdFilterInstance: BalanceThresholdFilterContract; + let erc20BalanceThresholdFilterInstance: BalanceThresholdFilterContract; + + const assertValidatedAddressesLog = async ( + txReceipt: TransactionReceiptWithDecodedLogs, + expectedValidatedAddresses: string[], + ) => { + expect(txReceipt.logs.length).to.be.gte(1); + const validatedAddressesLog = (txReceipt.logs[0] as any) as ValidatedAddressesLog; + const validatedAddresses = validatedAddressesLog.args.addresses; + // @HACK-hysz: Nested addresses are not translated to lower-case but this will change once + // the new ABI Encoder/Decoder is used by the contract templates. + const validatedAddressesNormalized: string[] = []; + _.each(validatedAddresses, address => { + const normalizedAddress = _.toLower(address); + validatedAddressesNormalized.push(normalizedAddress); + }); + expect(validatedAddressesNormalized).to.be.deep.equal(expectedValidatedAddresses); + }; + + before(async () => { + // Create accounts + await blockchainLifecycle.startAsync(); + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([ + owner, + validMakerAddress, + validMakerAddress2, + validTakerAddress, + feeRecipientAddress, + invalidAddress, + ] = accounts); + const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validTakerAddress)]; + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validMakerAddress)]; + const secondMakerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(validMakerAddress2)]; + const invalidAddressPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(invalidAddress)]; + // Create wrappers + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + const validAddresses = _.cloneDeepWith(usedAddresses); + _.remove(validAddresses, (address: string) => { + return address === invalidAddress; + }); + const erc721Wrapper = new ERC721Wrapper(provider, validAddresses, owner); + // Deploy ERC20 tokens + const numDummyErc20ToDeploy = 4; + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let erc20BalanceThresholdAsset: DummyERC20TokenContract; + [erc20TokenA, erc20TokenB, zrxToken, erc20BalanceThresholdAsset] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + defaultMakerAssetAddress = erc20TokenA.address; + defaultTakerAssetAddress = erc20TokenB.address; + zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + // Create proxies + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + // Deploy Exchange contract + exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( + protocolArtifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider); + // Register proxies + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { + from: owner, + }); + // Deploy Balance Threshold Filters + // One uses an ERC721 token as its balance threshold asset; the other uses an ERC20 + const erc721alanceThreshold = new BigNumber(1); + await erc721Wrapper.deployProxyAsync(); + const [erc721BalanceThresholdAsset] = await erc721Wrapper.deployDummyTokensAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + erc721BalanceThresholdFilterInstance = await BalanceThresholdFilterContract.deployFrom0xArtifactAsync( + artifacts.BalanceThresholdFilter, + provider, + txDefaults, + exchangeInstance.address, + erc721BalanceThresholdAsset.address, + erc721alanceThreshold, + ); + const erc20BalanceThreshold = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 10); + erc20BalanceThresholdFilterInstance = await BalanceThresholdFilterContract.deployFrom0xArtifactAsync( + artifacts.BalanceThresholdFilter, + provider, + txDefaults, + exchangeInstance.address, + erc20BalanceThresholdAsset.address, + erc20BalanceThreshold, + ); + // Default order parameters + defaultOrderParams = { + exchangeAddress: exchangeInstance.address, + feeRecipientAddress, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + makerAssetAmount, + takerAssetAmount, + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), DECIMALS_DEFAULT), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(150), DECIMALS_DEFAULT), + senderAddress: erc721BalanceThresholdFilterInstance.address, + }; + // Create two order factories with valid makers (who meet the threshold balance), and + // one factory for an invalid address (that does not meet the threshold balance) + // Valid order factory #1 + const defaultOrderParams1 = { + makerAddress: validMakerAddress, + ...defaultOrderParams, + }; + orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams1); + // Valid order factory #2 + const defaultOrderParams2 = { + makerAddress: validMakerAddress2, + ...defaultOrderParams, + }; + orderFactory2 = new OrderFactory(secondMakerPrivateKey, defaultOrderParams2); + // Invalid order factory + const defaultNonValidOrderParams = { + makerAddress: invalidAddress, + ...defaultOrderParams, + }; + invalidOrderFactory = new OrderFactory(invalidAddressPrivateKey, defaultNonValidOrderParams); + // Create Balance Thresold Wrappers + erc20TakerBalanceThresholdWrapper = new BalanceThresholdWrapper( + erc20BalanceThresholdFilterInstance, + exchangeInstance, + new TransactionFactory(takerPrivateKey, exchangeInstance.address), + provider, + ); + erc721TakerBalanceThresholdWrapper = new BalanceThresholdWrapper( + erc721BalanceThresholdFilterInstance, + exchangeInstance, + new TransactionFactory(takerPrivateKey, exchangeInstance.address), + provider, + ); + erc721MakerBalanceThresholdWrapper = new BalanceThresholdWrapper( + erc721BalanceThresholdFilterInstance, + exchangeInstance, + new TransactionFactory(makerPrivateKey, exchangeInstance.address), + provider, + ); + erc721NonValidBalanceThresholdWrapper = new BalanceThresholdWrapper( + erc721BalanceThresholdFilterInstance, + exchangeInstance, + new TransactionFactory(invalidAddressPrivateKey, exchangeInstance.address), + provider, + ); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('General Sanity Checks', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both maker/taker when both maker and taker exceed the balance threshold of an ERC20 token', async () => { + const validSignedOrderERC20Sender = await orderFactory.newSignedOrderAsync({ + ...defaultOrderParams, + makerAddress: validMakerAddress, + senderAddress: erc20TakerBalanceThresholdWrapper.getBalanceThresholdAddress(), + }); + // Execute a valid fill + const txReceipt = await erc20TakerBalanceThresholdWrapper.fillOrderAsync( + validSignedOrderERC20Sender, + validTakerAddress, + { takerAssetFillAmount }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if the Exchange transaction function is not supported', async () => { + // Create signed order without the fillOrder function selector + const salt = new BigNumber(0); + const badSelectorHex = '0x00000000'; + const signatureHex = '0x'; + // Call valid forwarder + return expectTransactionFailedAsync( + erc721BalanceThresholdFilterInstance.executeTransaction.sendTransactionAsync( + salt, + validTakerAddress, + badSelectorHex, + signatureHex, + ), + RevertReason.InvalidOrBlockedExchangeSelector, + ); + }); + it('should revert if senderAddress is not set to the valid forwarding contract', async () => { + // Create signed order with incorrect senderAddress + const notBalanceThresholdFilterAddress = zrxToken.address; + const signedOrderWithBadSenderAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: notBalanceThresholdFilterAddress, + }); + // Call valid forwarder + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrderAsync(signedOrderWithBadSenderAddress, validTakerAddress, { + takerAssetFillAmount, + }), + RevertReason.FailedExecution, + ); + }); + }); + + describe('batchFillOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount) + .times(2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.batchFillOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.batchFillOrdersAsync(orders, invalidAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('batchFillOrdersNoThrow', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrdersNoThrowAsync( + orders, + validTakerAddress, + { + takerAssetFillAmounts, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount) + .times(2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.batchFillOrdersNoThrowAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.batchFillOrdersNoThrowAsync(orders, invalidAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('batchFillOrKillOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const txReceipt = await erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync( + orders, + validTakerAddress, + { takerAssetFillAmounts }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const cumulativeTakerAssetFillAmount = takerAssetFillAmount.times(2); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount) + .times(2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount.times(2)), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.times(2).add(takerFeePaid)), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, takerAssetFillAmount]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, invalidAddress, { + takerAssetFillAmounts, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if one takerAssetFillAmount is not fully filled', async () => { + const tooBigTakerAssetFillAmount = validSignedOrder.takerAssetAmount.times(2); + const orders = [validSignedOrder, validSignedOrder2]; + const takerAssetFillAmounts = [takerAssetFillAmount, tooBigTakerAssetFillAmount]; + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.batchFillOrKillOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmounts, + }), + RevertReason.FailedExecution, + ); + }); + }); + + describe('fillOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrderAsync( + validSignedOrder, + validTakerAddress, + { takerAssetFillAmount }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrderAsync(signedOrderWithBadMakerAddress, validTakerAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.fillOrderAsync(validSignedOrder, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('fillOrderNoThrow', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrderNoThrowAsync( + validSignedOrder, + validTakerAddress, + { + takerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrderNoThrowAsync( + signedOrderWithBadMakerAddress, + validTakerAddress, + { takerAssetFillAmount }, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.fillOrderNoThrowAsync(validSignedOrder, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('fillOrKillOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both maker/taker when both maker and taker meet the balance threshold', async () => { + // Execute a valid fill + const takerAssetFillAmount_ = validSignedOrder.takerAssetAmount; + const txReceipt = await erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync( + validSignedOrder, + validTakerAddress, + { takerAssetFillAmount: takerAssetFillAmount_ }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [validSignedOrder.makerAddress, validTakerAddress]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount_ + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid = validSignedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(validSignedOrder.makerAssetAmount); + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(takerAssetFillAmount_), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(takerAssetFillAmount_), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(makerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + it('should revert if maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync( + signedOrderWithBadMakerAddress, + validTakerAddress, + { takerAssetFillAmount }, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.fillOrKillOrderAsync(validSignedOrder, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if takerAssetFillAmount is not fully filled', async () => { + const tooBigTakerAssetFillAmount = validSignedOrder.takerAssetAmount.times(2); + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.fillOrKillOrderAsync(validSignedOrder, validTakerAddress, { + takerAssetFillAmount: tooBigTakerAssetFillAmount, + }), + RevertReason.FailedExecution, + ); + }); + }); + + describe('marketSellOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount); + const txReceipt = await erc721TakerBalanceThresholdWrapper.marketSellOrdersAsync( + orders, + validTakerAddress, + { takerAssetFillAmount: cumulativeTakerAssetFillAmount }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount2 = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid2 = validSignedOrder2.makerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid2 = validSignedOrder2.takerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2); + const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(validSignedOrder.makerFee) + .add(makerFeePaid2) + .add(takerFeePaid), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.marketSellOrdersAsync(orders, validTakerAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.marketSellOrdersAsync(orders, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('marketSellOrdersNoThrow', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount); + const txReceipt = await erc721TakerBalanceThresholdWrapper.marketSellOrdersNoThrowAsync( + orders, + validTakerAddress, + { + takerAssetFillAmount: cumulativeTakerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount2 = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const makerFeePaid2 = validSignedOrder2.makerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid2 = validSignedOrder2.takerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2); + const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(validSignedOrder.makerFee) + .add(makerFeePaid2) + .add(takerFeePaid), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.marketSellOrdersNoThrowAsync(orders, validTakerAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.marketSellOrdersNoThrowAsync(orders, invalidAddress, { + takerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('marketBuyOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount); + const makerAssetFillAmount2 = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2); + const txReceipt = await erc721TakerBalanceThresholdWrapper.marketBuyOrdersAsync(orders, validTakerAddress, { + makerAssetFillAmount: cumulativeMakerAssetFillAmount, + }); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerFeePaid2 = validSignedOrder2.makerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid2 = validSignedOrder2.takerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(validSignedOrder.makerFee) + .add(makerFeePaid2) + .add(takerFeePaid), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + const dummyMakerAssetFillAmount = new BigNumber(0); + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.marketBuyOrdersAsync(orders, validTakerAddress, { + makerAssetFillAmount: dummyMakerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const dummyMakerAssetFillAmount = new BigNumber(0); + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.marketBuyOrdersAsync(orders, invalidAddress, { + makerAssetFillAmount: dummyMakerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('marketBuyOrdersNoThrowAsync', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('should transfer the correct amounts and validate both makers/taker when both makers and taker meet the balance threshold', async () => { + // Execute a valid fill + const orders = [validSignedOrder, validSignedOrder2]; + const cumulativeTakerAssetFillAmount = validSignedOrder.takerAssetAmount.plus(takerAssetFillAmount); + const makerAssetFillAmount2 = takerAssetFillAmount + .times(validSignedOrder.makerAssetAmount) + .dividedToIntegerBy(validSignedOrder.takerAssetAmount); + const cumulativeMakerAssetFillAmount = validSignedOrder.makerAssetAmount.plus(makerAssetFillAmount2); + const txReceipt = await erc721TakerBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync( + orders, + validTakerAddress, + { + makerAssetFillAmount: cumulativeMakerAssetFillAmount, + // HACK(albrow): We need to hardcode the gas estimate here because + // the Geth gas estimator doesn't work with the way we use + // delegatecall and swallow errors. + gas: 600000, + }, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + validSignedOrder.makerAddress, + validSignedOrder2.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerFeePaid2 = validSignedOrder2.makerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid2 = validSignedOrder2.takerFee + .times(makerAssetFillAmount2) + .dividedToIntegerBy(validSignedOrder2.makerAssetAmount); + const takerFeePaid = validSignedOrder.takerFee.plus(takerFeePaid2); + // Maker #1 + expect(newBalances[validMakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultMakerAssetAddress].minus(validSignedOrder.makerAssetAmount), + ); + expect(newBalances[validMakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][defaultTakerAssetAddress].add(validSignedOrder.takerAssetAmount), + ); + expect(newBalances[validMakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress][zrxToken.address].minus(validSignedOrder.makerFee), + ); + // Maker #2 + expect(newBalances[validMakerAddress2][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultMakerAssetAddress].minus(makerAssetFillAmount2), + ); + expect(newBalances[validMakerAddress2][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][defaultTakerAssetAddress].add(takerAssetFillAmount), + ); + expect(newBalances[validMakerAddress2][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validMakerAddress2][zrxToken.address].minus(makerFeePaid2), + ); + // Taker + expect(newBalances[validTakerAddress][defaultTakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultTakerAssetAddress].minus(cumulativeTakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add(cumulativeMakerAssetFillAmount), + ); + expect(newBalances[validTakerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address].minus(takerFeePaid), + ); + // Fee recipient + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(validSignedOrder.makerFee) + .add(makerFeePaid2) + .add(takerFeePaid), + ); + }); + it('should revert if one maker does not meet the balance threshold', async () => { + // Create order set with one non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + makerAddress: invalidAddress, + }); + const orders = [validSignedOrder, signedOrderWithBadMakerAddress]; + // Execute transaction + const dummyMakerAssetFillAmount = new BigNumber(0); + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync(orders, validTakerAddress, { + makerAssetFillAmount: dummyMakerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + const orders = [validSignedOrder, validSignedOrder2]; + const dummyMakerAssetFillAmount = new BigNumber(0); + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.marketBuyOrdersNoThrowAsync(orders, invalidAddress, { + makerAssetFillAmount: dummyMakerAssetFillAmount, + }), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('matchOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('Should transfer correct amounts when both makers and taker meet the balance threshold', async () => { + // Test values/results taken from Match Orders test: + // 'Should transfer correct amounts when right order is fully filled and values pass isRoundingErrorFloor but fail isRoundingErrorCeil' + // Create orders to match + const signedOrderLeft = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(17), 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(98), 0), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + feeRecipientAddress, + }); + const signedOrderRight = await orderFactory2.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0), + makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(1), 18), + feeRecipientAddress, + }); + // Compute expected transfer amounts + const expectedTransferAmounts = { + // Left Maker + amountSoldByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0), + amountBoughtByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0), + feePaidByLeftMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.4705882352941176'), 16), // 76.47% + // Right Maker + amountSoldByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(75), 0), + amountBoughtByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(13), 0), + feePaidByRightMaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100% + // Taker + amountReceivedByTaker: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), 0), + feePaidByTakerLeft: Web3Wrapper.toBaseUnitAmount(new BigNumber('76.5306122448979591'), 16), // 76.53% + feePaidByTakerRight: Web3Wrapper.toBaseUnitAmount(new BigNumber(100), 16), // 100% + }; + const txReceipt = await erc721TakerBalanceThresholdWrapper.matchOrdersAsync( + signedOrderLeft, + signedOrderRight, + validTakerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses = [ + signedOrderLeft.makerAddress, + signedOrderRight.makerAddress, + validTakerAddress, + ]; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check balances + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect( + newBalances[signedOrderLeft.makerAddress][defaultMakerAssetAddress], + 'Checking left maker egress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[signedOrderLeft.makerAddress][defaultMakerAssetAddress].sub( + expectedTransferAmounts.amountSoldByLeftMaker, + ), + ); + expect( + newBalances[signedOrderRight.makerAddress][defaultTakerAssetAddress], + 'Checking right maker ingress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[signedOrderRight.makerAddress][defaultTakerAssetAddress].sub( + expectedTransferAmounts.amountSoldByRightMaker, + ), + ); + expect( + newBalances[validTakerAddress][defaultMakerAssetAddress], + 'Checking taker ingress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[validTakerAddress][defaultMakerAssetAddress].add( + expectedTransferAmounts.amountReceivedByTaker, + ), + ); + expect( + newBalances[signedOrderLeft.makerAddress][defaultTakerAssetAddress], + 'Checking left maker ingress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[signedOrderLeft.makerAddress][defaultTakerAssetAddress].add( + expectedTransferAmounts.amountBoughtByLeftMaker, + ), + ); + expect( + newBalances[signedOrderRight.makerAddress][defaultMakerAssetAddress], + 'Checking right maker egress ERC20 account balance', + ).to.be.bignumber.equal( + erc20Balances[signedOrderRight.makerAddress][defaultMakerAssetAddress].add( + expectedTransferAmounts.amountBoughtByRightMaker, + ), + ); + // Paid fees + expect( + newBalances[signedOrderLeft.makerAddress][zrxToken.address], + 'Checking left maker egress ERC20 account fees', + ).to.be.bignumber.equal( + erc20Balances[signedOrderLeft.makerAddress][zrxToken.address].minus( + expectedTransferAmounts.feePaidByLeftMaker, + ), + ); + expect( + newBalances[signedOrderRight.makerAddress][zrxToken.address], + 'Checking right maker egress ERC20 account fees', + ).to.be.bignumber.equal( + erc20Balances[signedOrderRight.makerAddress][zrxToken.address].minus( + expectedTransferAmounts.feePaidByRightMaker, + ), + ); + expect( + newBalances[validTakerAddress][zrxToken.address], + 'Checking taker egress ERC20 account fees', + ).to.be.bignumber.equal( + erc20Balances[validTakerAddress][zrxToken.address] + .minus(expectedTransferAmounts.feePaidByTakerLeft) + .sub(expectedTransferAmounts.feePaidByTakerRight), + ); + // Received fees + expect( + newBalances[signedOrderLeft.feeRecipientAddress][zrxToken.address], + 'Checking left fee recipient ingress ERC20 account fees', + ).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address] + .add(expectedTransferAmounts.feePaidByLeftMaker) + .add(expectedTransferAmounts.feePaidByRightMaker) + .add(expectedTransferAmounts.feePaidByTakerLeft) + .add(expectedTransferAmounts.feePaidByTakerRight), + ); + }); + it('should revert if left maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.matchOrdersAsync( + validSignedOrder, + signedOrderWithBadMakerAddress, + validTakerAddress, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if right maker does not meet the balance threshold', async () => { + // Create signed order with non-valid maker address + const signedOrderWithBadMakerAddress = await orderFactory.newSignedOrderAsync({ + senderAddress: erc721BalanceThresholdFilterInstance.address, + makerAddress: invalidAddress, + }); + // Execute transaction + return expectTransactionFailedAsync( + erc721TakerBalanceThresholdWrapper.matchOrdersAsync( + signedOrderWithBadMakerAddress, + validSignedOrder, + validTakerAddress, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + it('should revert if taker does not meet the balance threshold', async () => { + return expectTransactionFailedAsync( + erc721NonValidBalanceThresholdWrapper.matchOrdersAsync( + validSignedOrder, + validSignedOrder, + invalidAddress, + ), + RevertReason.AtLeastOneAddressDoesNotMeetBalanceThreshold, + ); + }); + }); + + describe('cancelOrder', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + validSignedOrder = await orderFactory.newSignedOrderAsync(); + validSignedOrder2 = await orderFactory2.newSignedOrderAsync(); + }); + it('Should successfully cancel order if maker meets balance threshold', async () => { + // Verify order is not cancelled + const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + validSignedOrder, + ); + expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + // Cancel + const txReceipt = await erc721MakerBalanceThresholdWrapper.cancelOrderAsync( + validSignedOrder, + validSignedOrder.makerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + validSignedOrder, + ); + expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + }); + it('Should successfully cancel order if maker does not meet balance threshold', async () => { + // Create order where maker does not meet balance threshold + const signedOrderWithBadMakerAddress = await invalidOrderFactory.newSignedOrderAsync({}); + // Verify order is not cancelled + const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrderWithBadMakerAddress, + ); + expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + // Cancel + const txReceipt = await erc721NonValidBalanceThresholdWrapper.cancelOrderAsync( + signedOrderWithBadMakerAddress, + signedOrderWithBadMakerAddress.makerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrderWithBadMakerAddress, + ); + expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + }); + }); + + describe('batchCancelOrders', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + }); + it('Should successfully batch cancel orders if maker meets balance threshold', async () => { + // Create orders to cancel + const validSignedOrders = [ + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + await orderFactory.newSignedOrderAsync(), + ]; + // Verify orders are not cancelled + _.each(validSignedOrders, async signedOrder => { + const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + // Cancel + const txReceipt = await erc721MakerBalanceThresholdWrapper.batchCancelOrdersAsync( + validSignedOrders, + validSignedOrders[0].makerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + _.each(validSignedOrders, async signedOrder => { + const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + }); + }); + it('Should successfully batch cancel order if maker does not meet balance threshold', async () => { + // Create orders to cancel + const invalidSignedOrders = [ + await invalidOrderFactory.newSignedOrderAsync(), + await invalidOrderFactory.newSignedOrderAsync(), + await invalidOrderFactory.newSignedOrderAsync(), + ]; + // Verify orders are not cancelled + _.each(invalidSignedOrders, async signedOrder => { + const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + // Cancel + const txReceipt = await erc721NonValidBalanceThresholdWrapper.batchCancelOrdersAsync( + invalidSignedOrders, + invalidAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + _.each(invalidSignedOrders, async signedOrder => { + const orderInfoAfterCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + }); + }); + }); + + describe('cancelOrdersUpTo', () => { + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + }); + it('Should successfully batch cancel orders if maker meets balance threshold', async () => { + // Create orders to cancel + const validSignedOrders = [ + await orderFactory.newSignedOrderAsync({ salt: new BigNumber(0) }), + await orderFactory.newSignedOrderAsync({ salt: new BigNumber(1) }), + await orderFactory.newSignedOrderAsync({ salt: new BigNumber(2) }), + ]; + // Verify orders are not cancelled + _.each(validSignedOrders, async signedOrder => { + const orderInfoBeforeCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + // Cancel + const cancelOrdersUpToThisSalt = new BigNumber(1); + const txReceipt = await erc721MakerBalanceThresholdWrapper.cancelOrdersUpToAsync( + cancelOrdersUpToThisSalt, + validSignedOrders[0].makerAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + _.each(validSignedOrders, async (signedOrder, salt: number) => { + const orderInfoAfterCancelling = await erc721MakerBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + const saltAsBigNumber = new BigNumber(salt); + if (saltAsBigNumber.lessThanOrEqualTo(cancelOrdersUpToThisSalt)) { + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + } else { + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + } + }); + }); + it('Should successfully batch cancel order if maker does not meet balance threshold', async () => { + // Create orders to cancel + const invalidSignedOrders = [ + await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(0) }), + await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(1) }), + await invalidOrderFactory.newSignedOrderAsync({ salt: new BigNumber(2) }), + ]; + // Verify orders are not cancelled + _.each(invalidSignedOrders, async signedOrder => { + const orderInfoBeforeCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + return expect(orderInfoBeforeCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + }); + // Cancel + const cancelOrdersUpToThisSalt = new BigNumber(1); + const txReceipt = await erc721NonValidBalanceThresholdWrapper.cancelOrdersUpToAsync( + cancelOrdersUpToThisSalt, + invalidAddress, + ); + // Assert validated addresses + const expectedValidatedAddresseses: string[] = []; + await assertValidatedAddressesLog(txReceipt, expectedValidatedAddresseses); + // Check that order was cancelled + _.each(invalidSignedOrders, async (signedOrder, salt: number) => { + const orderInfoAfterCancelling = await erc721NonValidBalanceThresholdWrapper.getOrderInfoAsync( + signedOrder, + ); + const saltAsBigNumber = new BigNumber(salt); + if (saltAsBigNumber.lessThanOrEqualTo(cancelOrdersUpToThisSalt)) { + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.CANCELLED); + } else { + return expect(orderInfoAfterCancelling.orderStatus).to.be.equal(OrderStatus.FILLABLE); + } + }); + }); + }); +}); +// tslint:disable:max-file-line-count +// tslint:enable:no-unnecessary-type-assertion diff --git a/contracts/extensions/test/utils/balance_threshold_wrapper.ts b/contracts/extensions/test/utils/balance_threshold_wrapper.ts new file mode 100644 index 000000000..28a4ef011 --- /dev/null +++ b/contracts/extensions/test/utils/balance_threshold_wrapper.ts @@ -0,0 +1,283 @@ +import { artifacts as protocolArtifacts, ExchangeContract } from '@0x/contracts-protocol'; +import { + FillResults, + formatters, + LogDecoder, + OrderInfo, + orderUtils, + TransactionFactory, +} from '@0x/contracts-test-utils'; +import { artifacts as tokensArtifacts } from '@0x/contracts-tokens'; +import { SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BalanceThresholdFilterContract } from '../../generated-wrappers/balance_threshold_filter'; +import { artifacts } from '../../src/artifacts'; + +export class BalanceThresholdWrapper { + private readonly _balanceThresholdFilter: BalanceThresholdFilterContract; + private readonly _signerTransactionFactory: TransactionFactory; + private readonly _exchange: ExchangeContract; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _logDecoder: LogDecoder; + constructor( + balanceThresholdFilter: BalanceThresholdFilterContract, + exchangeContract: ExchangeContract, + signerTransactionFactory: TransactionFactory, + provider: Provider, + ) { + this._balanceThresholdFilter = balanceThresholdFilter; + this._exchange = exchangeContract; + this._signerTransactionFactory = signerTransactionFactory; + this._web3Wrapper = new Web3Wrapper(provider); + this._logDecoder = new LogDecoder(this._web3Wrapper, { + ...artifacts, + ...tokensArtifacts, + ...protocolArtifacts, + }); + } + public async fillOrderAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrder.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async fillOrKillOrderAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrKillOrder.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async fillOrderNoThrowAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber; gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrderNoThrow.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + const txReceipt = this._executeTransactionAsync(data, from, opts.gas); + return txReceipt; + } + public async batchFillOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[] } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const data = this._exchange.batchFillOrders.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async batchFillOrKillOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[] } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const data = this._exchange.batchFillOrKillOrders.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async batchFillOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmounts?: BigNumber[]; gas?: number } = {}, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchFill(orders, opts.takerAssetFillAmounts); + const data = this._exchange.batchFillOrKillOrders.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmounts, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from, opts.gas); + return txReceipt; + } + public async marketSellOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmount: BigNumber }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount); + const data = this._exchange.marketSellOrders.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmount, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async marketSellOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { takerAssetFillAmount: BigNumber; gas?: number }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketSellOrders(orders, opts.takerAssetFillAmount); + const data = this._exchange.marketSellOrdersNoThrow.getABIEncodedTransactionData( + params.orders, + params.takerAssetFillAmount, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from, opts.gas); + return txReceipt; + } + public async marketBuyOrdersAsync( + orders: SignedOrder[], + from: string, + opts: { makerAssetFillAmount: BigNumber }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount); + const data = this._exchange.marketBuyOrders.getABIEncodedTransactionData( + params.orders, + params.makerAssetFillAmount, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async marketBuyOrdersNoThrowAsync( + orders: SignedOrder[], + from: string, + opts: { makerAssetFillAmount: BigNumber; gas?: number }, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createMarketBuyOrders(orders, opts.makerAssetFillAmount); + const data = this._exchange.marketBuyOrdersNoThrow.getABIEncodedTransactionData( + params.orders, + params.makerAssetFillAmount, + params.signatures, + ); + const txReceipt = this._executeTransactionAsync(data, from, opts.gas); + return txReceipt; + } + public async cancelOrderAsync(signedOrder: SignedOrder, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createCancel(signedOrder); + const data = this._exchange.cancelOrder.getABIEncodedTransactionData(params.order); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async batchCancelOrdersAsync( + orders: SignedOrder[], + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = formatters.createBatchCancel(orders); + const data = this._exchange.batchCancelOrders.getABIEncodedTransactionData(params.orders); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async cancelOrdersUpToAsync(salt: BigNumber, from: string): Promise<TransactionReceiptWithDecodedLogs> { + const data = this._exchange.cancelOrdersUpTo.getABIEncodedTransactionData(salt); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async getTakerAssetFilledAmountAsync(orderHashHex: string): Promise<BigNumber> { + const filledAmount = await this._exchange.filled.callAsync(orderHashHex); + return filledAmount; + } + public async isCancelledAsync(orderHashHex: string): Promise<boolean> { + const isCancelled = await this._exchange.cancelled.callAsync(orderHashHex); + return isCancelled; + } + public async getOrderEpochAsync(makerAddress: string, senderAddress: string): Promise<BigNumber> { + const orderEpoch = await this._exchange.orderEpoch.callAsync(makerAddress, senderAddress); + return orderEpoch; + } + public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> { + const orderInfo = await this._exchange.getOrderInfo.callAsync(signedOrder); + return orderInfo; + } + public async getOrdersInfoAsync(signedOrders: SignedOrder[]): Promise<OrderInfo[]> { + const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[]; + return ordersInfo; + } + public async matchOrdersAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight); + const data = await this._exchange.matchOrders.getABIEncodedTransactionData( + params.left, + params.right, + params.leftSignature, + params.rightSignature, + ); + const txReceipt = this._executeTransactionAsync(data, from); + return txReceipt; + } + public async getFillOrderResultsAsync( + signedOrder: SignedOrder, + from: string, + opts: { takerAssetFillAmount?: BigNumber } = {}, + ): Promise<FillResults> { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const fillResults = await this._exchange.fillOrder.callAsync( + params.order, + params.takerAssetFillAmount, + params.signature, + { from }, + ); + return fillResults; + } + public abiEncodeFillOrder(signedOrder: SignedOrder, opts: { takerAssetFillAmount?: BigNumber } = {}): string { + const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount); + const data = this._exchange.fillOrder.getABIEncodedTransactionData( + params.order, + params.takerAssetFillAmount, + params.signature, + ); + return data; + } + public getBalanceThresholdAddress(): string { + return this._balanceThresholdFilter.address; + } + public getExchangeAddress(): string { + return this._exchange.address; + } + private async _executeTransactionAsync( + abiEncodedExchangeTxData: string, + from: string, + gas?: number, + ): Promise<TransactionReceiptWithDecodedLogs> { + const signedExchangeTx = this._signerTransactionFactory.newSignedTransaction(abiEncodedExchangeTxData); + const txOpts = _.isUndefined(gas) ? { from } : { from, gas }; + const txHash = await this._balanceThresholdFilter.executeTransaction.sendTransactionAsync( + signedExchangeTx.salt, + signedExchangeTx.signerAddress, + signedExchangeTx.data, + signedExchangeTx.signature, + txOpts, + ); + const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return txReceipt; + } +} diff --git a/contracts/extensions/tsconfig.json b/contracts/extensions/tsconfig.json index a4ce1e002..a303e3f5c 100644 --- a/contracts/extensions/tsconfig.json +++ b/contracts/extensions/tsconfig.json @@ -6,6 +6,10 @@ "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], - "files": ["./generated-artifacts/DutchAuction.json", "./generated-artifacts/Forwarder.json"], + "files": [ + "./generated-artifacts/BalanceThresholdFilter.json", + "./generated-artifacts/DutchAuction.json", + "./generated-artifacts/Forwarder.json" + ], "exclude": ["./deploy/solc/solc_bin"] } diff --git a/contracts/interfaces/package.json b/contracts/interfaces/package.json index 4d3e4b7f9..15385a154 100644 --- a/contracts/interfaces/package.json +++ b/contracts/interfaces/package.json @@ -10,7 +10,8 @@ "build": "yarn pre_build && tsc -b", "build:ci": "yarn build", "pre_build": "run-s compile generate_contract_wrappers", - "compile": "sol-compiler --contracts-dir contracts", + "compile": "sol-compiler", + "watch": "sol-compiler -w", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", diff --git a/contracts/libs/compiler.json b/contracts/libs/compiler.json index cf7c52a73..349d3063b 100644 --- a/contracts/libs/compiler.json +++ b/contracts/libs/compiler.json @@ -18,5 +18,14 @@ } } }, - "contracts": ["TestLibs", "LibOrder", "LibMath", "LibFillResults", "LibAbiEncoder", "LibEIP712"] + "contracts": [ + "TestLibs", + "LibOrder", + "LibMath", + "LibFillResults", + "LibAbiEncoder", + "LibEIP712", + "LibAssetProxyErrors", + "LibConstants" + ] } diff --git a/contracts/libs/contracts/libs/LibAddressArray.sol b/contracts/libs/contracts/libs/LibAddressArray.sol new file mode 100644 index 000000000..ccae2ac5f --- /dev/null +++ b/contracts/libs/contracts/libs/LibAddressArray.sol @@ -0,0 +1,84 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + +import "@0x/contracts-utils/contracts/utils/LibBytes/LibBytes.sol"; + + +library LibAddressArray { + + /// @dev Append a new address to an array of addresses. + /// The `addressArray` may need to be reallocated to make space + /// for the new address. Because of this we return the resulting + /// memory location of `addressArray`. + /// @param addressToAppend Address to append. + /// @return Array of addresses: [... addressArray, addressToAppend] + function append(address[] memory addressArray, address addressToAppend) + internal pure + returns (address[]) + { + // Get stats on address array and free memory + uint256 freeMemPtr = 0; + uint256 addressArrayBeginPtr = 0; + uint256 addressArrayEndPtr = 0; + uint256 addressArrayLength = addressArray.length; + uint256 addressArrayMemSizeInBytes = 32 + (32 * addressArrayLength); + assembly { + freeMemPtr := mload(0x40) + addressArrayBeginPtr := addressArray + addressArrayEndPtr := add(addressArray, addressArrayMemSizeInBytes) + } + + // Cases for `freeMemPtr`: + // `freeMemPtr` == `addressArrayEndPtr`: Nothing occupies memory after `addressArray` + // `freeMemPtr` > `addressArrayEndPtr`: Some value occupies memory after `addressArray` + // `freeMemPtr` < `addressArrayEndPtr`: Memory has not been managed properly. + require( + freeMemPtr >= addressArrayEndPtr, + "INVALID_FREE_MEMORY_PTR" + ); + + // If free memory begins at the end of `addressArray` + // then we can append `addressToAppend` directly. + // Otherwise, we must copy the array to free memory + // before appending new values to it. + if (freeMemPtr > addressArrayEndPtr) { + LibBytes.memCopy(freeMemPtr, addressArrayBeginPtr, addressArrayMemSizeInBytes); + assembly { + addressArray := freeMemPtr + addressArrayBeginPtr := addressArray + } + } + + // Append `addressToAppend` + addressArrayLength += 1; + addressArrayMemSizeInBytes += 32; + addressArrayEndPtr = addressArrayBeginPtr + addressArrayMemSizeInBytes; + freeMemPtr = addressArrayEndPtr; + assembly { + // Store new array length + mstore(addressArray, addressArrayLength) + + // Update `freeMemPtr` + mstore(0x40, freeMemPtr) + } + addressArray[addressArrayLength - 1] = addressToAppend; + return addressArray; + } +} diff --git a/contracts/libs/contracts/libs/LibExchangeSelectors.sol b/contracts/libs/contracts/libs/LibExchangeSelectors.sol new file mode 100644 index 000000000..edb4f9cbd --- /dev/null +++ b/contracts/libs/contracts/libs/LibExchangeSelectors.sol @@ -0,0 +1,152 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; + + +contract LibExchangeSelectors { + + // solhint-disable max-line-length + // allowedValidators + bytes4 constant public ALLOWED_VALIDATORS_SELECTOR = 0x7b8e3514; + bytes4 constant public ALLOWED_VALIDATORS_SELECTOR_GENERATOR = bytes4(keccak256("allowedValidators(address,address)")); + + // assetProxies + bytes4 constant public ASSET_PROXIES_SELECTOR = 0x3fd3c997; + bytes4 constant public ASSET_PROXIES_SELECTOR_GENERATOR = bytes4(keccak256("assetProxies(bytes4)")); + + // batchCancelOrders + bytes4 constant public BATCH_CANCEL_ORDERS_SELECTOR = 0x4ac14782; + bytes4 constant public BATCH_CANCEL_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("batchCancelOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[])")); + + // batchFillOrKillOrders + bytes4 constant public BATCH_FILL_OR_KILL_ORDERS_SELECTOR = 0x4d0ae546; + bytes4 constant public BATCH_FILL_OR_KILL_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("batchFillOrKillOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256[],bytes[])")); + + // batchFillOrders + bytes4 constant public BATCH_FILL_ORDERS_SELECTOR = 0x297bb70b; + bytes4 constant public BATCH_FILL_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("batchFillOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256[],bytes[])")); + + // batchFillOrdersNoThrow + bytes4 constant public BATCH_FILL_ORDERS_NO_THROW_SELECTOR = 0x50dde190; + bytes4 constant public BATCH_FILL_ORDERS_NO_THROW_SELECTOR_GENERATOR = bytes4(keccak256("batchFillOrdersNoThrow((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256[],bytes[])")); + + // cancelOrder + bytes4 constant public CANCEL_ORDER_SELECTOR = 0xd46b02c3; + bytes4 constant public CANCEL_ORDER_SELECTOR_GENERATOR = bytes4(keccak256("cancelOrder((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes))")); + + // cancelOrdersUpTo + bytes4 constant public CANCEL_ORDERS_UP_TO_SELECTOR = 0x4f9559b1; + bytes4 constant public CANCEL_ORDERS_UP_TO_SELECTOR_GENERATOR = bytes4(keccak256("cancelOrdersUpTo(uint256)")); + + // cancelled + bytes4 constant public CANCELLED_SELECTOR = 0x2ac12622; + bytes4 constant public CANCELLED_SELECTOR_GENERATOR = bytes4(keccak256("cancelled(bytes32)")); + + // currentContextAddress + bytes4 constant public CURRENT_CONTEXT_ADDRESS_SELECTOR = 0xeea086ba; + bytes4 constant public CURRENT_CONTEXT_ADDRESS_SELECTOR_GENERATOR = bytes4(keccak256("currentContextAddress()")); + + // executeTransaction + bytes4 constant public EXECUTE_TRANSACTION_SELECTOR = 0xbfc8bfce; + bytes4 constant public EXECUTE_TRANSACTION_SELECTOR_GENERATOR = bytes4(keccak256("executeTransaction(uint256,address,bytes,bytes)")); + + // fillOrKillOrder + bytes4 constant public FILL_OR_KILL_ORDER_SELECTOR = 0x64a3bc15; + bytes4 constant public FILL_OR_KILL_ORDER_SELECTOR_GENERATOR = bytes4(keccak256("fillOrKillOrder((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),uint256,bytes)")); + + // fillOrder + bytes4 constant public FILL_ORDER_SELECTOR = 0xb4be83d5; + bytes4 constant public FILL_ORDER_SELECTOR_GENERATOR = bytes4(keccak256("fillOrder((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),uint256,bytes)")); + + // fillOrderNoThrow + bytes4 constant public FILL_ORDER_NO_THROW_SELECTOR = 0x3e228bae; + bytes4 constant public FILL_ORDER_NO_THROW_SELECTOR_GENERATOR = bytes4(keccak256("fillOrderNoThrow((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),uint256,bytes)")); + + // filled + bytes4 constant public FILLED_SELECTOR = 0x288cdc91; + bytes4 constant public FILLED_SELECTOR_GENERATOR = bytes4(keccak256("filled(bytes32)")); + + // getAssetProxy + bytes4 constant public GET_ASSET_PROXY_SELECTOR = 0x60704108; + bytes4 constant public GET_ASSET_PROXY_SELECTOR_GENERATOR = bytes4(keccak256("getAssetProxy(bytes4)")); + + // getOrderInfo + bytes4 constant public GET_ORDER_INFO_SELECTOR = 0xc75e0a81; + bytes4 constant public GET_ORDER_INFO_SELECTOR_GENERATOR = bytes4(keccak256("getOrderInfo((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes))")); + + // getOrdersInfo + bytes4 constant public GET_ORDERS_INFO_SELECTOR = 0x7e9d74dc; + bytes4 constant public GET_ORDERS_INFO_SELECTOR_GENERATOR = bytes4(keccak256("getOrdersInfo((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[])")); + + // isValidSignature + bytes4 constant public IS_VALID_SIGNATURE_SELECTOR = 0x93634702; + bytes4 constant public IS_VALID_SIGNATURE_SELECTOR_GENERATOR = bytes4(keccak256("isValidSignature(bytes32,address,bytes)")); + + // marketBuyOrders + bytes4 constant public MARKET_BUY_ORDERS_SELECTOR = 0xe5fa431b; + bytes4 constant public MARKET_BUY_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("marketBuyOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256,bytes[])")); + + // marketBuyOrdersNoThrow + bytes4 constant public MARKET_BUY_ORDERS_NO_THROW_SELECTOR = 0xa3e20380; + bytes4 constant public MARKET_BUY_ORDERS_NO_THROW_SELECTOR_GENERATOR = bytes4(keccak256("marketBuyOrdersNoThrow((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256,bytes[])")); + + // marketSellOrders + bytes4 constant public MARKET_SELL_ORDERS_SELECTOR = 0x7e1d9808; + bytes4 constant public MARKET_SELL_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("marketSellOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256,bytes[])")); + + // marketSellOrdersNoThrow + bytes4 constant public MARKET_SELL_ORDERS_NO_THROW_SELECTOR = 0xdd1c7d18; + bytes4 constant public MARKET_SELL_ORDERS_NO_THROW_SELECTOR_GENERATOR = bytes4(keccak256("marketSellOrdersNoThrow((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],uint256,bytes[])")); + + // matchOrders + bytes4 constant public MATCH_ORDERS_SELECTOR = 0x3c28d861; + bytes4 constant public MATCH_ORDERS_SELECTOR_GENERATOR = bytes4(keccak256("matchOrders((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),(address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes),bytes,bytes)")); + + // orderEpoch + bytes4 constant public ORDER_EPOCH_SELECTOR = 0xd9bfa73e; + bytes4 constant public ORDER_EPOCH_SELECTOR_GENERATOR = bytes4(keccak256("orderEpoch(address,address)")); + + // owner + bytes4 constant public OWNER_SELECTOR = 0x8da5cb5b; + bytes4 constant public OWNER_SELECTOR_GENERATOR = bytes4(keccak256("owner()")); + + // preSign + bytes4 constant public PRE_SIGN_SELECTOR = 0x3683ef8e; + bytes4 constant public PRE_SIGN_SELECTOR_GENERATOR = bytes4(keccak256("preSign(bytes32,address,bytes)")); + + // preSigned + bytes4 constant public PRE_SIGNED_SELECTOR = 0x82c174d0; + bytes4 constant public PRE_SIGNED_SELECTOR_GENERATOR = bytes4(keccak256("preSigned(bytes32,address)")); + + // registerAssetProxy + bytes4 constant public REGISTER_ASSET_PROXY_SELECTOR = 0xc585bb93; + bytes4 constant public REGISTER_ASSET_PROXY_SELECTOR_GENERATOR = bytes4(keccak256("registerAssetProxy(address)")); + + // setSignatureValidatorApproval + bytes4 constant public SET_SIGNATURE_VALIDATOR_APPROVAL_SELECTOR = 0x77fcce68; + bytes4 constant public SET_SIGNATURE_VALIDATOR_APPROVAL_SELECTOR_GENERATOR = bytes4(keccak256("setSignatureValidatorApproval(address,bool)")); + + // transactions + bytes4 constant public TRANSACTIONS_SELECTOR = 0x642f2eaf; + bytes4 constant public TRANSACTIONS_SELECTOR_GENERATOR = bytes4(keccak256("transactions(bytes32)")); + + // transferOwnership + bytes4 constant public TRANSFER_OWNERSHIP_SELECTOR = 0xf2fde38b; + bytes4 constant public TRANSFER_OWNERSHIP_SELECTOR_GENERATOR = bytes4(keccak256("transferOwnership(address)")); +}
\ No newline at end of file diff --git a/contracts/libs/package.json b/contracts/libs/package.json index fa4b6e523..ce7b97502 100644 --- a/contracts/libs/package.json +++ b/contracts/libs/package.json @@ -19,7 +19,8 @@ "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", - "compile": "sol-compiler --contracts-dir contracts", + "compile": "sol-compiler", + "watch": "sol-compiler -w", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", diff --git a/contracts/multisig/package.json b/contracts/multisig/package.json index b338f67f7..2d7b4aa05 100644 --- a/contracts/multisig/package.json +++ b/contracts/multisig/package.json @@ -19,7 +19,8 @@ "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", - "compile": "sol-compiler --contracts-dir contracts", + "compile": "sol-compiler", + "watch": "sol-compiler -w", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../packages/abi-gen-templates/contract.handlebars --partials '../../packages/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", diff --git a/contracts/multisig/src/index.ts b/contracts/multisig/src/index.ts new file mode 100644 index 000000000..d55f08ea2 --- /dev/null +++ b/contracts/multisig/src/index.ts @@ -0,0 +1,2 @@ +export * from './artifacts'; +export * from './wrappers'; diff --git a/contracts/protocol/CHANGELOG.json b/contracts/protocol/CHANGELOG.json index 5c3798a69..2dca9794a 100644 --- a/contracts/protocol/CHANGELOG.json +++ b/contracts/protocol/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "2.2.0", + "changes": [ + { + "note": "Added LibAddressArray", + "pr": 1383 + } + ] + }, + { "timestamp": 1544741676, "version": "2.1.59", "changes": [ diff --git a/contracts/protocol/package.json b/contracts/protocol/package.json index 838189371..d37a0302a 100644 --- a/contracts/protocol/package.json +++ b/contracts/protocol/package.json @@ -19,7 +19,8 @@ "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", - "compile": "sol-compiler --contracts-dir contracts", + "compile": "sol-compiler", + "watch": "sol-compiler -w", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", @@ -44,7 +45,6 @@ "homepage": "https://github.com/0xProject/0x-monorepo/contracts/protocol/README.md", "devDependencies": { "@0x/abi-gen": "^1.0.19", - "@0x/contracts-test-utils": "^1.0.2", "@0x/dev-utils": "^1.0.21", "@0x/sol-compiler": "^1.1.16", "@0x/sol-cov": "^2.1.16", @@ -75,6 +75,7 @@ "@0x/contracts-interfaces": "^1.0.2", "@0x/contracts-libs": "^1.0.2", "@0x/contracts-multisig": "^1.0.2", + "@0x/contracts-test-utils": "^1.0.2", "@0x/contracts-tokens": "^1.0.2", "@0x/contracts-utils": "^1.0.2", "@0x/order-utils": "^3.0.7", diff --git a/contracts/test-utils/src/types.ts b/contracts/test-utils/src/types.ts index d738fcd4e..1630eab0d 100644 --- a/contracts/test-utils/src/types.ts +++ b/contracts/test-utils/src/types.ts @@ -104,6 +104,7 @@ export enum ContractName { Authorizable = 'Authorizable', Whitelist = 'Whitelist', Forwarder = 'Forwarder', + BalanceThresholdFilter = 'BalanceThresholdFilter', } export interface SignedTransaction { diff --git a/contracts/tokens/package.json b/contracts/tokens/package.json index 6f8a366dd..79afc4820 100644 --- a/contracts/tokens/package.json +++ b/contracts/tokens/package.json @@ -19,7 +19,8 @@ "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", - "compile": "sol-compiler --contracts-dir contracts", + "compile": "sol-compiler", + "watch": "sol-compiler -w", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", diff --git a/contracts/utils/package.json b/contracts/utils/package.json index a776bdfbb..cf94af0f6 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -19,7 +19,8 @@ "test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html", "test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha", "run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit", - "compile": "sol-compiler --contracts-dir contracts", + "compile": "sol-compiler", + "watch": "sol-compiler -w", "clean": "shx rm -rf lib generated-artifacts generated-wrappers", "generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output generated-wrappers --backend ethers", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", diff --git a/packages/json-schemas/schemas/order_watcher_web_socket_request_schema.json b/packages/json-schemas/schemas/order_watcher_web_socket_request_schema.json new file mode 100644 index 000000000..b0c419f94 --- /dev/null +++ b/packages/json-schemas/schemas/order_watcher_web_socket_request_schema.json @@ -0,0 +1,52 @@ +{ + "id": "/orderWatcherWebSocketRequestSchema", + "type": "object", + "definitions": { + "signedOrderParam": { + "type": "object", + "properties": { + "signedOrder": { "$ref": "/signedOrderSchema" } + }, + "required": ["signedOrder"] + }, + "orderHashParam": { + "type": "object", + "properties": { + "orderHash": { "$ref": "/hexSchema" } + }, + "required": ["orderHash"] + } + }, + "oneOf": [ + { + "type": "object", + "properties": { + "id": { "type": "number" }, + "jsonrpc": { "type": "string" }, + "method": { "enum": ["ADD_ORDER"] }, + "params": { "$ref": "#/definitions/signedOrderParam" } + }, + "required": ["id", "jsonrpc", "method", "params"] + }, + { + "type": "object", + "properties": { + "id": { "type": "number" }, + "jsonrpc": { "type": "string" }, + "method": { "enum": ["REMOVE_ORDER"] }, + "params": { "$ref": "#/definitions/orderHashParam" } + }, + "required": ["id", "jsonrpc", "method", "params"] + }, + { + "type": "object", + "properties": { + "id": { "type": "number" }, + "jsonrpc": { "type": "string" }, + "method": { "enum": ["GET_STATS"] }, + "params": {} + }, + "required": ["id", "jsonrpc", "method"] + } + ] +}
\ No newline at end of file diff --git a/packages/json-schemas/schemas/order_watcher_web_socket_utf8_message_schema.json b/packages/json-schemas/schemas/order_watcher_web_socket_utf8_message_schema.json new file mode 100644 index 000000000..154d6d754 --- /dev/null +++ b/packages/json-schemas/schemas/order_watcher_web_socket_utf8_message_schema.json @@ -0,0 +1,10 @@ +{ + "id": "/orderWatcherWebSocketUtf8MessageSchema", + "properties": { + "utf8Data": { "type": "string" } + }, + "required": [ + "utf8Data" + ], + "type": "object" +} diff --git a/packages/json-schemas/src/schemas.ts b/packages/json-schemas/src/schemas.ts index 21a6f424c..050f4e625 100644 --- a/packages/json-schemas/src/schemas.ts +++ b/packages/json-schemas/src/schemas.ts @@ -16,6 +16,8 @@ import * as orderFillOrKillRequestsSchema from '../schemas/order_fill_or_kill_re import * as orderFillRequestsSchema from '../schemas/order_fill_requests_schema.json'; import * as orderHashSchema from '../schemas/order_hash_schema.json'; import * as orderSchema from '../schemas/order_schema.json'; +import * as orderWatcherWebSocketRequestSchema from '../schemas/order_watcher_web_socket_request_schema.json'; +import * as orderWatcherWebSocketUtf8MessageSchema from '../schemas/order_watcher_web_socket_utf8_message_schema.json'; import * as orderBookRequestSchema from '../schemas/orderbook_request_schema.json'; import * as ordersRequestOptsSchema from '../schemas/orders_request_opts_schema.json'; import * as ordersSchema from '../schemas/orders_schema.json'; @@ -66,6 +68,8 @@ export const schemas = { jsNumber, requestOptsSchema, pagedRequestOptsSchema, + orderWatcherWebSocketRequestSchema, + orderWatcherWebSocketUtf8MessageSchema, ordersRequestOptsSchema, orderBookRequestSchema, orderConfigRequestSchema, diff --git a/packages/json-schemas/tsconfig.json b/packages/json-schemas/tsconfig.json index a79d54385..ec573290c 100644 --- a/packages/json-schemas/tsconfig.json +++ b/packages/json-schemas/tsconfig.json @@ -23,6 +23,8 @@ "./schemas/order_schema.json", "./schemas/signed_order_schema.json", "./schemas/orders_schema.json", + "./schemas/order_watcher_web_socket_request_schema.json", + "./schemas/order_watcher_web_socket_utf8_message_schema.json", "./schemas/paginated_collection_schema.json", "./schemas/relayer_api_asset_data_pairs_response_schema.json", "./schemas/relayer_api_asset_data_pairs_schema.json", diff --git a/packages/order-watcher/CHANGELOG.json b/packages/order-watcher/CHANGELOG.json index c1fd8d4a9..304dc45fd 100644 --- a/packages/order-watcher/CHANGELOG.json +++ b/packages/order-watcher/CHANGELOG.json @@ -1,5 +1,15 @@ [ { + "version": "2.3.0", + "changes": [ + { + "note": + "Added a WebSocket interface to OrderWatcher so that it can be used by a client written in any language", + "pr": 1427 + } + ] + }, + { "version": "2.2.8", "changes": [ { diff --git a/packages/order-watcher/README.md b/packages/order-watcher/README.md index c0b99b272..385fe4715 100644 --- a/packages/order-watcher/README.md +++ b/packages/order-watcher/README.md @@ -4,6 +4,9 @@ An order watcher daemon that watches for order validity. #### Read the wiki [article](https://0xproject.com/wiki#0x-OrderWatcher). +OrderWatcher also comes with a WebSocket server to provide language-agnostic access +to order watching functionality. We used the [WebSocket Client and Server Implementation for Node](https://www.npmjs.com/package/websocket). The server sends and receives messages that conform to the [JSON RPC specifications](https://www.jsonrpc.org/specification). + ## Installation **Install** @@ -26,6 +29,91 @@ If your project is in [TypeScript](https://www.typescriptlang.org/), add the fol } ``` +## Using the WebSocket Server + +**Setup** + +**Environmental Variables** +Several environmental variables can be set to configure the server: + +* `ORDER_WATCHER_HTTP_PORT` specifies the port that the http server will listen on + and accept connections from. When this is not set, we default to 8080. + +**Requests** +The server accepts three types of requests: `ADD_ORDER`, `REMOVE_ORDER` and `GET_STATS`. These mirror what the underlying OrderWatcher does. You can read more in the [wiki](https://0xproject.com/wiki#0x-OrderWatcher). Unlike the OrderWatcher, it does not expose any `subscribe` or `unsubscribe` functionality because the WebSocket server keeps a single subscription open for all clients. + +The first step for making a request is establishing a connection with the server. In Javascript: + +``` +var W3CWebSocket = require('websocket').w3cwebsocket; +wsClient = new W3CWebSocket('ws://127.0.0.1:8080'); +``` + +In Python, you could use the [websocket-client library](http://pypi.python.org/pypi/websocket-client/) and run: + +``` +from websocket import create_connection +wsClient = create_connection("ws://127.0.0.1:8080") +``` + +With the connection established, you prepare the payload for your request. The payload is a json object with a format established by the [JSON RPC specification](https://www.jsonrpc.org/specification): + +* `id`: All requests require you to specify a numerical `id`. When the server responds to the request, the response will have the same `id` as the one supplied with your request. +* `jsonrpc`: This is always the string `'2.0'`. +* `method`: This specifies the OrderWatcher method you want to call. I.e., `'ADD_ORDER'`, `'REMOVE_ORDER'` or `'GET_STATS'`. +* `params`: These contain the parameters needed by OrderWatcher to execute the method you called. For `ADD_ORDER`, provide `{ signedOrder: <your signedOrder> }`. For `REMOVE_ORDER`, provide `{ orderHash: <your orderHash> }`. For `GET_STATS`, no parameters are needed, so you may leave this empty. + +Next, convert the payload to a string and send it through the connection. +In Javascript: + +``` +const addOrderPayload = { + id: 1, + jsonrpc: '2.0', + method: 'ADD_ORDER', + params: { signedOrder: <your signedOrder> }, +}; +wsClient.send(JSON.stringify(addOrderPayload)); +``` + +In Python: + +``` +import json +remove_order_payload = { + 'id': 1, + 'jsonrpc': '2.0', + 'method': 'REMOVE_ORDER', + 'params': {'orderHash': '0x6edc16bf37fde79f5012088c33784c730e2f103d9ab1caf73060c386ad107b7e'}, +} +wsClient.send(json.dumps(remove_order_payload)); +``` + +**Response** +The server responds to all requests in a similar format. In the data field, you'll find another object containing the following fields: + +* `id`: The id corresponding to the request that the server is responding to. `UPDATE` responses are not based on any requests so the `id` field is omitted`. +* `jsonrpc`: Always `'2.0'`. +* `method`: The method the server is responding to. Eg. `ADD_ORDER`. When order states change the server may also initiate a response. In this case, method will be listed as `UPDATE`. +* `result`: This field varies based on the method. `UPDATE` responses contain the new order state. `GET_STATS` responses contain the current order count. When there are errors, this field is omitted. +* `error`: When there is an error executing a request, the [JSON RPC](https://www.jsonrpc.org/specification) error object is listed here. When the server responds successfully, this field is omitted. + +In Javascript, the responses can be parsed using the `onmessage` callback: + +``` +wsClient.onmessage = (msg) => { + const responseData = JSON.parse(msg.data); + const method = responseData.method +}; +``` + +In Python, `recv` is a lightweight way to receive a response: + +``` +result = wsClient.recv() +method = result.method +``` + ## Contributing We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository. diff --git a/packages/order-watcher/package.json b/packages/order-watcher/package.json index 499d4cead..16a46294e 100644 --- a/packages/order-watcher/package.json +++ b/packages/order-watcher/package.json @@ -74,7 +74,8 @@ "ethereum-types": "^1.1.4", "ethereumjs-blockstream": "6.0.0", "ethers": "~4.0.4", - "lodash": "^4.17.5" + "lodash": "^4.17.5", + "websocket": "^1.0.25" }, "publishConfig": { "access": "public" diff --git a/packages/order-watcher/src/index.ts b/packages/order-watcher/src/index.ts index 5eeba3e87..e275a0c6a 100644 --- a/packages/order-watcher/src/index.ts +++ b/packages/order-watcher/src/index.ts @@ -1,4 +1,5 @@ export { OrderWatcher } from './order_watcher/order_watcher'; +export { OrderWatcherWebSocketServer } from './order_watcher/order_watcher_web_socket_server'; export { ExpirationWatcher } from './order_watcher/expiration_watcher'; export { diff --git a/packages/order-watcher/src/order_watcher/order_watcher_web_socket_server.ts b/packages/order-watcher/src/order_watcher/order_watcher_web_socket_server.ts new file mode 100644 index 000000000..b75b07603 --- /dev/null +++ b/packages/order-watcher/src/order_watcher/order_watcher_web_socket_server.ts @@ -0,0 +1,200 @@ +import { ContractAddresses } from '@0x/contract-addresses'; +import { schemas } from '@0x/json-schemas'; +import { OrderStateInvalid, OrderStateValid, SignedOrder } from '@0x/types'; +import { BigNumber, logUtils } from '@0x/utils'; +import { Provider } from 'ethereum-types'; +import * as http from 'http'; +import * as WebSocket from 'websocket'; + +import { GetStatsResult, OrderWatcherConfig, OrderWatcherMethod, WebSocketRequest, WebSocketResponse } from '../types'; +import { assert } from '../utils/assert'; + +import { OrderWatcher } from './order_watcher'; + +const DEFAULT_HTTP_PORT = 8080; +const JSON_RPC_VERSION = '2.0'; + +// Wraps the OrderWatcher functionality in a WebSocket server. Motivations: +// 1) Users can watch orders via non-typescript programs. +// 2) Better encapsulation so that users can work +export class OrderWatcherWebSocketServer { + private readonly _orderWatcher: OrderWatcher; + private readonly _httpServer: http.Server; + private readonly _connectionStore: Set<WebSocket.connection>; + private readonly _wsServer: WebSocket.server; + private readonly _isVerbose: boolean; + /** + * Recover types lost when the payload is stringified. + */ + private static _parseSignedOrder(rawRequest: any): SignedOrder { + const bigNumberFields = [ + 'salt', + 'makerFee', + 'takerFee', + 'makerAssetAmount', + 'takerAssetAmount', + 'expirationTimeSeconds', + ]; + for (const field of bigNumberFields) { + rawRequest[field] = new BigNumber(rawRequest[field]); + } + return rawRequest; + } + + /** + * Instantiate a new WebSocket server which provides OrderWatcher functionality + * @param provider Web3 provider to use for JSON RPC calls. + * @param networkId NetworkId to watch orders on. + * @param contractAddresses Optional contract addresses. Defaults to known + * addresses based on networkId. + * @param orderWatcherConfig OrderWatcher configurations. isVerbose sets the verbosity for the WebSocket server aswell. + * @param isVerbose Whether to enable verbose logging. Defaults to true. + */ + constructor( + provider: Provider, + networkId: number, + contractAddresses?: ContractAddresses, + orderWatcherConfig?: Partial<OrderWatcherConfig>, + ) { + this._isVerbose = + orderWatcherConfig !== undefined && orderWatcherConfig.isVerbose !== undefined + ? orderWatcherConfig.isVerbose + : true; + this._orderWatcher = new OrderWatcher(provider, networkId, contractAddresses, orderWatcherConfig); + this._connectionStore = new Set(); + this._httpServer = http.createServer(); + this._wsServer = new WebSocket.server({ + httpServer: this._httpServer, + // Avoid setting autoAcceptConnections to true as it defeats all + // standard cross-origin protection facilities built into the protocol + // and the browser. + // Source: https://www.npmjs.com/package/websocket#server-example + // Also ensures that a request event is emitted by + // the server whenever a new WebSocket request is made. + autoAcceptConnections: false, + }); + + this._wsServer.on('request', async (request: any) => { + // Designed for usage pattern where client and server are run on the same + // machine by the same user. As such, no security checks are in place. + const connection: WebSocket.connection = request.accept(null, request.origin); + this._log(`${new Date()} [Server] Accepted connection from origin ${request.origin}.`); + connection.on('message', this._onMessageCallbackAsync.bind(this, connection)); + connection.on('close', this._onCloseCallback.bind(this, connection)); + this._connectionStore.add(connection); + }); + } + + /** + * Activates the WebSocket server by subscribing to the OrderWatcher and + * starting the WebSocket's HTTP server + */ + public start(): void { + // Have the WebSocket server subscribe to the OrderWatcher to receive updates. + // These updates are then broadcast to clients in the _connectionStore. + this._orderWatcher.subscribe(this._broadcastCallback.bind(this)); + + const port = process.env.ORDER_WATCHER_HTTP_PORT || DEFAULT_HTTP_PORT; + this._httpServer.listen(port, () => { + this._log(`${new Date()} [Server] Listening on port ${port}`); + }); + } + + /** + * Deactivates the WebSocket server by stopping the HTTP server from accepting + * new connections and unsubscribing from the OrderWatcher + */ + public stop(): void { + this._httpServer.close(); + this._orderWatcher.unsubscribe(); + } + + private _log(...args: any[]): void { + if (this._isVerbose) { + logUtils.log(...args); + } + } + + private async _onMessageCallbackAsync(connection: WebSocket.connection, message: any): Promise<void> { + let response: WebSocketResponse; + let id: number | null = null; + try { + assert.doesConformToSchema('message', message, schemas.orderWatcherWebSocketUtf8MessageSchema); + const request: WebSocketRequest = JSON.parse(message.utf8Data); + id = request.id; + assert.doesConformToSchema('request', request, schemas.orderWatcherWebSocketRequestSchema); + assert.isString(request.jsonrpc, JSON_RPC_VERSION); + response = { + id, + jsonrpc: JSON_RPC_VERSION, + method: request.method, + result: await this._routeRequestAsync(request), + }; + } catch (err) { + response = { + id, + jsonrpc: JSON_RPC_VERSION, + method: null, + error: err.toString(), + }; + } + this._log(`${new Date()} [Server] OrderWatcher output: ${JSON.stringify(response)}`); + connection.sendUTF(JSON.stringify(response)); + } + + private _onCloseCallback(connection: WebSocket.connection): void { + this._connectionStore.delete(connection); + this._log(`${new Date()} [Server] Client ${connection.remoteAddress} disconnected.`); + } + + private async _routeRequestAsync(request: WebSocketRequest): Promise<GetStatsResult | undefined> { + this._log(`${new Date()} [Server] Request received: ${request.method}`); + switch (request.method) { + case OrderWatcherMethod.AddOrder: { + const signedOrder: SignedOrder = OrderWatcherWebSocketServer._parseSignedOrder( + request.params.signedOrder, + ); + await this._orderWatcher.addOrderAsync(signedOrder); + break; + } + case OrderWatcherMethod.RemoveOrder: { + this._orderWatcher.removeOrder(request.params.orderHash || 'undefined'); + break; + } + case OrderWatcherMethod.GetStats: { + return this._orderWatcher.getStats(); + } + default: + // Should never reach here. Should be caught by JSON schema check. + throw new Error(`Unexpected default case hit for request.method`); + } + return undefined; + } + + /** + * Broadcasts OrderState changes to ALL connected clients. At the moment, + * we do not support clients subscribing to only a subset of orders. As such, + * Client B will be notified of changes to an order that Client A added. + */ + private _broadcastCallback(err: Error | null, orderState?: OrderStateValid | OrderStateInvalid | undefined): void { + const method = OrderWatcherMethod.Update; + const response = + err === null + ? { + jsonrpc: JSON_RPC_VERSION, + method, + result: orderState, + } + : { + jsonrpc: JSON_RPC_VERSION, + method, + error: { + code: -32000, + message: err.message, + }, + }; + this._connectionStore.forEach((connection: WebSocket.connection) => { + connection.sendUTF(JSON.stringify(response)); + }); + } +} diff --git a/packages/order-watcher/src/types.ts b/packages/order-watcher/src/types.ts index 8078dd971..2b529a939 100644 --- a/packages/order-watcher/src/types.ts +++ b/packages/order-watcher/src/types.ts @@ -1,4 +1,4 @@ -import { OrderState } from '@0x/types'; +import { OrderState, SignedOrder } from '@0x/types'; import { LogEntryEvent } from 'ethereum-types'; export enum OrderWatcherError { @@ -31,3 +31,67 @@ export enum InternalOrderWatcherError { ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY', WethNotInTokenRegistry = 'WETH_NOT_IN_TOKEN_REGISTRY', } + +export enum OrderWatcherMethod { + // Methods initiated by the user. + GetStats = 'GET_STATS', + AddOrder = 'ADD_ORDER', + RemoveOrder = 'REMOVE_ORDER', + // These are spontaneous; they are primarily orderstate changes. + Update = 'UPDATE', + // `subscribe` and `unsubscribe` are methods of OrderWatcher, but we don't + // need to expose them to the WebSocket server user because the user implicitly + // subscribes and unsubscribes by connecting and disconnecting from the server. +} + +// Users have to create a json object of this format and attach it to +// the data field of their WebSocket message to interact with the server. +export type WebSocketRequest = AddOrderRequest | RemoveOrderRequest | GetStatsRequest; + +export interface AddOrderRequest { + id: number; + jsonrpc: string; + method: OrderWatcherMethod.AddOrder; + params: { signedOrder: SignedOrder }; +} + +export interface RemoveOrderRequest { + id: number; + jsonrpc: string; + method: OrderWatcherMethod.RemoveOrder; + params: { orderHash: string }; +} + +export interface GetStatsRequest { + id: number; + jsonrpc: string; + method: OrderWatcherMethod.GetStats; +} + +// Users should expect a json object of this format in the data field +// of the WebSocket messages that the server sends out. +export type WebSocketResponse = SuccessfulWebSocketResponse | ErrorWebSocketResponse; + +export interface SuccessfulWebSocketResponse { + id: number; + jsonrpc: string; + method: OrderWatcherMethod; + result: OrderState | GetStatsResult | undefined; // result is undefined for ADD_ORDER and REMOVE_ORDER +} + +export interface ErrorWebSocketResponse { + id: number | null; + jsonrpc: string; + method: null; + error: JSONRPCError; +} + +export interface JSONRPCError { + code: number; + message: string; + data?: string | object; +} + +export interface GetStatsResult { + orderCount: number; +} diff --git a/packages/order-watcher/test/order_watcher_web_socket_server_test.ts b/packages/order-watcher/test/order_watcher_web_socket_server_test.ts new file mode 100644 index 000000000..578e0de61 --- /dev/null +++ b/packages/order-watcher/test/order_watcher_web_socket_server_test.ts @@ -0,0 +1,308 @@ +import { ContractWrappers } from '@0x/contract-wrappers'; +import { tokenUtils } from '@0x/contract-wrappers/lib/test/utils/token_utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { FillScenarios } from '@0x/fill-scenarios'; +import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; +import { ExchangeContractErrs, OrderStateInvalid, OrderStateValid, SignedOrder } from '@0x/types'; +import { BigNumber, logUtils } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as chai from 'chai'; +import 'mocha'; +import * as WebSocket from 'websocket'; + +import { OrderWatcherWebSocketServer } from '../src/order_watcher/order_watcher_web_socket_server'; +import { AddOrderRequest, OrderWatcherMethod, RemoveOrderRequest } from '../src/types'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { migrateOnceAsync } from './utils/migrate'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +interface WsMessage { + data: string; +} + +describe.only('OrderWatcherWebSocketServer', async () => { + let contractWrappers: ContractWrappers; + let wsServer: OrderWatcherWebSocketServer; + let wsClient: WebSocket.w3cwebsocket; + let wsClientTwo: WebSocket.w3cwebsocket; + let fillScenarios: FillScenarios; + let userAddresses: string[]; + let makerAssetData: string; + let takerAssetData: string; + let makerTokenAddress: string; + let takerTokenAddress: string; + let makerAddress: string; + let takerAddress: string; + let zrxTokenAddress: string; + let signedOrder: SignedOrder; + let orderHash: string; + let addOrderPayload: AddOrderRequest; + let removeOrderPayload: RemoveOrderRequest; + const decimals = constants.ZRX_DECIMALS; + const fillableAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(5), decimals); + + before(async () => { + // Set up constants + const contractAddresses = await migrateOnceAsync(); + await blockchainLifecycle.startAsync(); + const networkId = constants.TESTRPC_NETWORK_ID; + const config = { + networkId, + contractAddresses, + }; + contractWrappers = new ContractWrappers(provider, config); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + zrxTokenAddress = contractAddresses.zrxToken; + [makerAddress, takerAddress] = userAddresses; + [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); + [makerAssetData, takerAssetData] = [ + assetDataUtils.encodeERC20AssetData(makerTokenAddress), + assetDataUtils.encodeERC20AssetData(takerTokenAddress), + ]; + fillScenarios = new FillScenarios( + provider, + userAddresses, + zrxTokenAddress, + contractAddresses.exchange, + contractAddresses.erc20Proxy, + contractAddresses.erc721Proxy, + ); + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerAssetData, + takerAssetData, + makerAddress, + takerAddress, + fillableAmount, + ); + orderHash = orderHashUtils.getOrderHashHex(signedOrder); + addOrderPayload = { + id: 1, + jsonrpc: '2.0', + method: OrderWatcherMethod.AddOrder, + params: { signedOrder }, + }; + removeOrderPayload = { + id: 1, + jsonrpc: '2.0', + method: OrderWatcherMethod.RemoveOrder, + params: { orderHash }, + }; + + // Prepare OrderWatcher WebSocket server + const orderWatcherConfig = { + isVerbose: true, + }; + wsServer = new OrderWatcherWebSocketServer(provider, networkId, contractAddresses, orderWatcherConfig); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + beforeEach(async () => { + wsServer.start(); + await blockchainLifecycle.startAsync(); + wsClient = new WebSocket.w3cwebsocket('ws://127.0.0.1:8080/'); + logUtils.log(`${new Date()} [Client] Connected.`); + }); + afterEach(async () => { + wsClient.close(); + await blockchainLifecycle.revertAsync(); + wsServer.stop(); + logUtils.log(`${new Date()} [Client] Closed.`); + }); + + it('responds to getStats requests correctly', (done: any) => { + const payload = { + id: 1, + jsonrpc: '2.0', + method: 'GET_STATS', + }; + wsClient.onopen = () => wsClient.send(JSON.stringify(payload)); + wsClient.onmessage = (msg: any) => { + const responseData = JSON.parse(msg.data); + expect(responseData.id).to.be.eq(1); + expect(responseData.jsonrpc).to.be.eq('2.0'); + expect(responseData.method).to.be.eq('GET_STATS'); + expect(responseData.result.orderCount).to.be.eq(0); + done(); + }; + }); + + it('throws an error when an invalid method is attempted', async () => { + const invalidMethodPayload = { + id: 1, + jsonrpc: '2.0', + method: 'BAD_METHOD', + }; + wsClient.onopen = () => wsClient.send(JSON.stringify(invalidMethodPayload)); + const errorMsg = await onMessageAsync(wsClient, null); + const errorData = JSON.parse(errorMsg.data); + // tslint:disable-next-line:no-unused-expression + expect(errorData.id).to.be.null; + // tslint:disable-next-line:no-unused-expression + expect(errorData.method).to.be.null; + expect(errorData.jsonrpc).to.be.eq('2.0'); + expect(errorData.error).to.match(/^Error: Expected request to conform to schema/); + }); + + it('throws an error when jsonrpc field missing from request', async () => { + const noJsonRpcPayload = { + id: 1, + method: 'GET_STATS', + }; + wsClient.onopen = () => wsClient.send(JSON.stringify(noJsonRpcPayload)); + const errorMsg = await onMessageAsync(wsClient, null); + const errorData = JSON.parse(errorMsg.data); + // tslint:disable-next-line:no-unused-expression + expect(errorData.method).to.be.null; + expect(errorData.jsonrpc).to.be.eq('2.0'); + expect(errorData.error).to.match(/^Error: Expected request to conform to schema/); + }); + + it('throws an error when we try to add an order without a signedOrder', async () => { + const noSignedOrderAddOrderPayload = { + id: 1, + jsonrpc: '2.0', + method: 'ADD_ORDER', + orderHash: '0x7337e2f2a9aa2ed6afe26edc2df7ad79c3ffa9cf9b81a964f707ea63f5272355', + }; + wsClient.onopen = () => wsClient.send(JSON.stringify(noSignedOrderAddOrderPayload)); + const errorMsg = await onMessageAsync(wsClient, null); + const errorData = JSON.parse(errorMsg.data); + // tslint:disable-next-line:no-unused-expression + expect(errorData.id).to.be.null; + // tslint:disable-next-line:no-unused-expression + expect(errorData.method).to.be.null; + expect(errorData.jsonrpc).to.be.eq('2.0'); + expect(errorData.error).to.match(/^Error: Expected request to conform to schema/); + }); + + it('throws an error when we try to add a bad signedOrder', async () => { + const invalidAddOrderPayload = { + id: 1, + jsonrpc: '2.0', + method: 'ADD_ORDER', + signedOrder: { + makerAddress: '0x0', + }, + }; + wsClient.onopen = () => wsClient.send(JSON.stringify(invalidAddOrderPayload)); + const errorMsg = await onMessageAsync(wsClient, null); + const errorData = JSON.parse(errorMsg.data); + // tslint:disable-next-line:no-unused-expression + expect(errorData.id).to.be.null; + // tslint:disable-next-line:no-unused-expression + expect(errorData.method).to.be.null; + expect(errorData.error).to.match(/^Error: Expected request to conform to schema/); + }); + + it('executes addOrder and removeOrder requests correctly', async () => { + wsClient.onopen = () => wsClient.send(JSON.stringify(addOrderPayload)); + const addOrderMsg = await onMessageAsync(wsClient, OrderWatcherMethod.AddOrder); + const addOrderData = JSON.parse(addOrderMsg.data); + expect(addOrderData.method).to.be.eq('ADD_ORDER'); + expect((wsServer as any)._orderWatcher._orderByOrderHash).to.deep.include({ + [orderHash]: signedOrder, + }); + + const clientOnMessagePromise = onMessageAsync(wsClient, OrderWatcherMethod.RemoveOrder); + wsClient.send(JSON.stringify(removeOrderPayload)); + const removeOrderMsg = await clientOnMessagePromise; + const removeOrderData = JSON.parse(removeOrderMsg.data); + expect(removeOrderData.method).to.be.eq('REMOVE_ORDER'); + expect((wsServer as any)._orderWatcher._orderByOrderHash).to.not.deep.include({ + [orderHash]: signedOrder, + }); + }); + + it('broadcasts orderStateInvalid message when makerAddress allowance set to 0 for watched order', async () => { + // Add the regular order + wsClient.onopen = () => wsClient.send(JSON.stringify(addOrderPayload)); + + // We register the onMessage callback before calling `setProxyAllowanceAsync` which we + // expect will cause a message to be emitted. We do now "await" here, since we want to + // check for messages _after_ calling `setProxyAllowanceAsync` + const clientOnMessagePromise = onMessageAsync(wsClient, OrderWatcherMethod.Update); + + // Set the allowance to 0 + await contractWrappers.erc20Token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, new BigNumber(0)); + + // We now await the `onMessage` promise to check for the message + const orderWatcherUpdateMsg = await clientOnMessagePromise; + const orderWatcherUpdateData = JSON.parse(orderWatcherUpdateMsg.data); + expect(orderWatcherUpdateData.method).to.be.eq('UPDATE'); + const invalidOrderState = orderWatcherUpdateData.result as OrderStateInvalid; + expect(invalidOrderState.isValid).to.be.false(); + expect(invalidOrderState.orderHash).to.be.eq(orderHash); + expect(invalidOrderState.error).to.be.eq(ExchangeContractErrs.InsufficientMakerAllowance); + }); + + it('broadcasts to multiple clients when an order backing ZRX allowance changes', async () => { + // Prepare order + const makerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(2), decimals); + const takerFee = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), decimals); + const nonZeroMakerFeeSignedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerAssetData, + takerAssetData, + makerFee, + takerFee, + makerAddress, + takerAddress, + fillableAmount, + takerAddress, + ); + const nonZeroMakerFeeOrderPayload = { + id: 1, + jsonrpc: '2.0', + method: 'ADD_ORDER', + signedOrder: nonZeroMakerFeeSignedOrder, + }; + + // Set up a second client and have it add the order + wsClientTwo = new WebSocket.w3cwebsocket('ws://127.0.0.1:8080/'); + logUtils.log(`${new Date()} [Client] Connected.`); + wsClientTwo.onopen = () => wsClientTwo.send(JSON.stringify(nonZeroMakerFeeOrderPayload)); + + // Setup the onMessage callbacks, but don't await them yet + const clientOneOnMessagePromise = onMessageAsync(wsClient, OrderWatcherMethod.Update); + const clientTwoOnMessagePromise = onMessageAsync(wsClientTwo, OrderWatcherMethod.Update); + + // Change the allowance + await contractWrappers.erc20Token.setProxyAllowanceAsync(zrxTokenAddress, makerAddress, new BigNumber(0)); + + // Check that both clients receive the emitted event by awaiting the onMessageAsync promises + let updateMsg = await clientOneOnMessagePromise; + let updateData = JSON.parse(updateMsg.data); + let orderState = updateData.result as OrderStateValid; + expect(orderState.isValid).to.be.true(); + expect(orderState.orderRelevantState.makerFeeProxyAllowance).to.be.eq('0'); + + updateMsg = await clientTwoOnMessagePromise; + updateData = JSON.parse(updateMsg.data); + orderState = updateData.result as OrderStateValid; + expect(orderState.isValid).to.be.true(); + expect(orderState.orderRelevantState.makerFeeProxyAllowance).to.be.eq('0'); + + wsClientTwo.close(); + logUtils.log(`${new Date()} [Client] Closed.`); + }); +}); + +// HACK: createFillableSignedOrderAsync is Promise-based, which forces us +// to use Promises instead of the done() callbacks for tests. +// onmessage callback must thus be wrapped as a Promise. +async function onMessageAsync(client: WebSocket.w3cwebsocket, method: string | null): Promise<WsMessage> { + return new Promise<WsMessage>(resolve => { + client.onmessage = (msg: WsMessage) => { + const data = JSON.parse(msg.data); + if (data.method === method) { + resolve(msg); + } + }; + }); +} diff --git a/packages/sol-compiler/CHANGELOG.json b/packages/sol-compiler/CHANGELOG.json index 0a757f519..8548fd73f 100644 --- a/packages/sol-compiler/CHANGELOG.json +++ b/packages/sol-compiler/CHANGELOG.json @@ -1,5 +1,18 @@ [ { + "version": "2.0.0", + "changes": [ + { + "note": "Add sol-compiler watch mode with -w flag", + "pr": 1461 + }, + { + "note": "Make error and warning colouring more visually pleasant and consistent with other compilers", + "pr": 1461 + } + ] + }, + { "version": "1.1.16", "changes": [ { diff --git a/packages/sol-compiler/package.json b/packages/sol-compiler/package.json index 0ad620b1f..86167a603 100644 --- a/packages/sol-compiler/package.json +++ b/packages/sol-compiler/package.json @@ -44,7 +44,9 @@ "devDependencies": { "@0x/dev-utils": "^1.0.21", "@0x/tslint-config": "^2.0.0", + "@types/chokidar": "^1.7.5", "@types/mkdirp": "^0.5.2", + "@types/pluralize": "^0.0.29", "@types/require-from-string": "^1.2.0", "@types/semver": "^5.5.0", "chai": "^4.0.1", @@ -74,10 +76,12 @@ "@0x/web3-wrapper": "^3.2.1", "@types/yargs": "^11.0.0", "chalk": "^2.3.0", + "chokidar": "^2.0.4", "ethereum-types": "^1.1.4", "ethereumjs-util": "^5.1.1", "lodash": "^4.17.5", "mkdirp": "^0.5.1", + "pluralize": "^7.0.0", "require-from-string": "^2.0.1", "semver": "5.5.0", "solc": "^0.4.23", diff --git a/packages/sol-compiler/src/cli.ts b/packages/sol-compiler/src/cli.ts index 0a9db6e05..18cc68aaf 100644 --- a/packages/sol-compiler/src/cli.ts +++ b/packages/sol-compiler/src/cli.ts @@ -25,6 +25,10 @@ const SEPARATOR = ','; type: 'string', description: 'comma separated list of contracts to compile', }) + .option('watch', { + alias: 'w', + default: false, + }) .help().argv; const contracts = _.isUndefined(argv.contracts) ? undefined @@ -37,7 +41,11 @@ const SEPARATOR = ','; contracts, }; const compiler = new Compiler(opts); - await compiler.compileAsync(); + if (argv.watch) { + await compiler.watchAsync(); + } else { + await compiler.compileAsync(); + } })().catch(err => { logUtils.log(err); process.exit(1); diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 85df8209e..d38ccbf39 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -6,26 +6,29 @@ import { NPMResolver, RelativeFSResolver, Resolver, + SpyResolver, URLResolver, } from '@0x/sol-resolver'; -import { fetchAsync, logUtils } from '@0x/utils'; -import chalk from 'chalk'; +import { logUtils } from '@0x/utils'; +import * as chokidar from 'chokidar'; import { CompilerOptions, ContractArtifact, ContractVersionData, StandardOutput } from 'ethereum-types'; -import * as ethUtil from 'ethereumjs-util'; import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; -import * as requireFromString from 'require-from-string'; +import * as pluralize from 'pluralize'; import * as semver from 'semver'; import solc = require('solc'); import { compilerOptionsSchema } from './schemas/compiler_options_schema'; import { binPaths } from './solc/bin_paths'; import { + addHexPrefixToContractBytecode, + compile, createDirIfDoesNotExistAsync, getContractArtifactIfExistsAsync, - getNormalizedErrMsg, - parseDependencies, + getSolcAsync, + getSourcesWithDependencies, + getSourceTreeHash, parseSolidityVersionRange, } from './utils/compiler'; import { constants } from './utils/constants'; @@ -35,7 +38,6 @@ import { utils } from './utils/utils'; type TYPE_ALL_FILES_IDENTIFIER = '*'; const ALL_CONTRACTS_IDENTIFIER = '*'; const ALL_FILES_IDENTIFIER = '*'; -const SOLC_BIN_DIR = path.join(__dirname, '..', '..', 'solc_bin'); const DEFAULT_CONTRACTS_DIR = path.resolve('contracts'); const DEFAULT_ARTIFACTS_DIR = path.resolve('artifacts'); // Solc compiler settings cannot be configured from the commandline. @@ -82,49 +84,6 @@ export class Compiler { private readonly _artifactsDir: string; private readonly _solcVersionIfExists: string | undefined; private readonly _specifiedContracts: string[] | TYPE_ALL_FILES_IDENTIFIER; - private static async _getSolcAsync( - solcVersion: string, - ): Promise<{ solcInstance: solc.SolcInstance; fullSolcVersion: string }> { - const fullSolcVersion = binPaths[solcVersion]; - if (_.isUndefined(fullSolcVersion)) { - throw new Error(`${solcVersion} is not a known compiler version`); - } - const compilerBinFilename = path.join(SOLC_BIN_DIR, fullSolcVersion); - let solcjs: string; - if (await fsWrapper.doesFileExistAsync(compilerBinFilename)) { - solcjs = (await fsWrapper.readFileAsync(compilerBinFilename)).toString(); - } else { - logUtils.warn(`Downloading ${fullSolcVersion}...`); - const url = `${constants.BASE_COMPILER_URL}${fullSolcVersion}`; - const response = await fetchAsync(url); - const SUCCESS_STATUS = 200; - if (response.status !== SUCCESS_STATUS) { - throw new Error(`Failed to load ${fullSolcVersion}`); - } - solcjs = await response.text(); - await fsWrapper.writeFileAsync(compilerBinFilename, solcjs); - } - if (solcjs.length === 0) { - throw new Error('No compiler available'); - } - const solcInstance = solc.setupMethods(requireFromString(solcjs, compilerBinFilename)); - return { solcInstance, fullSolcVersion }; - } - private static _addHexPrefixToContractBytecode(compiledContract: solc.StandardContractOutput): void { - if (!_.isUndefined(compiledContract.evm)) { - if (!_.isUndefined(compiledContract.evm.bytecode) && !_.isUndefined(compiledContract.evm.bytecode.object)) { - compiledContract.evm.bytecode.object = ethUtil.addHexPrefix(compiledContract.evm.bytecode.object); - } - if ( - !_.isUndefined(compiledContract.evm.deployedBytecode) && - !_.isUndefined(compiledContract.evm.deployedBytecode.object) - ) { - compiledContract.evm.deployedBytecode.object = ethUtil.addHexPrefix( - compiledContract.evm.deployedBytecode.object, - ); - } - } - } /** * Instantiates a new instance of the Compiler class. * @param opts Optional compiler options @@ -158,7 +117,7 @@ export class Compiler { */ public async compileAsync(): Promise<void> { await createDirIfDoesNotExistAsync(this._artifactsDir); - await createDirIfDoesNotExistAsync(SOLC_BIN_DIR); + await createDirIfDoesNotExistAsync(constants.SOLC_BIN_DIR); await this._compileContractsAsync(this._getContractNamesToCompile(), true); } /** @@ -173,6 +132,54 @@ export class Compiler { const promisedOutputs = this._compileContractsAsync(this._getContractNamesToCompile(), false); return promisedOutputs; } + public async watchAsync(): Promise<void> { + console.clear(); // tslint:disable-line:no-console + logUtils.logWithTime('Starting compilation in watch mode...'); + const MATCH_NOTHING_REGEX = '^$'; + const IGNORE_DOT_FILES_REGEX = /(^|[\/\\])\../; + // Initially we watch nothing. We'll add the paths later. + const watcher = chokidar.watch(MATCH_NOTHING_REGEX, { ignored: IGNORE_DOT_FILES_REGEX }); + const onFileChangedAsync = async () => { + watcher.unwatch('*'); // Stop watching + try { + await this.compileAsync(); + logUtils.logWithTime('Found 0 errors. Watching for file changes.'); + } catch (err) { + if (err.typeName === 'CompilationError') { + logUtils.logWithTime( + `Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`, + ); + } else { + logUtils.logWithTime('Found errors. Watching for file changes.'); + } + } + + const pathsToWatch = this._getPathsToWatch(); + watcher.add(pathsToWatch); + }; + await onFileChangedAsync(); + watcher.on('change', (changedFilePath: string) => { + console.clear(); // tslint:disable-line:no-console + logUtils.logWithTime('File change detected. Starting incremental compilation...'); + // NOTE: We can't await it here because that's a callback. + // Instead we stop watching inside of it and start it again when we're finished. + onFileChangedAsync(); // tslint:disable-line no-floating-promises + }); + } + private _getPathsToWatch(): string[] { + const contractNames = this._getContractNamesToCompile(); + const spyResolver = new SpyResolver(this._resolver); + for (const contractName of contractNames) { + const contractSource = spyResolver.resolve(contractName); + // NOTE: We ignore the return value here. We don't want to compute the source tree hash. + // We just want to call a SpyResolver on each contracts and it's dependencies and + // this is a convenient way to reuse the existing code that does that. + // We can then get all the relevant paths from the `spyResolver` below. + getSourceTreeHash(spyResolver, contractSource.path); + } + const pathsToWatch = _.uniq(spyResolver.resolvedContractSources.map(cs => cs.absolutePath)); + return pathsToWatch; + } private _getContractNamesToCompile(): string[] { let contractNamesToCompile; if (this._specifiedContracts === ALL_CONTRACTS_IDENTIFIER) { @@ -201,12 +208,14 @@ export class Compiler { for (const contractName of contractNames) { const contractSource = this._resolver.resolve(contractName); + const sourceTreeHashHex = getSourceTreeHash( + this._resolver, + path.join(this._contractsDir, contractSource.path), + ).toString('hex'); const contractData = { contractName, currentArtifactIfExists: await getContractArtifactIfExistsAsync(this._artifactsDir, contractName), - sourceTreeHashHex: `0x${this._getSourceTreeHash( - path.join(this._contractsDir, contractSource.path), - ).toString('hex')}`, + sourceTreeHashHex: `0x${sourceTreeHashHex}`, }; if (!this._shouldCompile(contractData)) { continue; @@ -244,9 +253,8 @@ export class Compiler { }) with Solidity v${solcVersion}...`, ); - const { solcInstance, fullSolcVersion } = await Compiler._getSolcAsync(solcVersion); - - const compilerOutput = this._compile(solcInstance, input.standardInput); + const { solcInstance, fullSolcVersion } = await getSolcAsync(solcVersion); + const compilerOutput = compile(this._resolver, solcInstance, input.standardInput); compilerOutputs.push(compilerOutput); for (const contractPath of input.contractsToCompile) { @@ -259,7 +267,7 @@ export class Compiler { ); } - Compiler._addHexPrefixToContractBytecode(compiledContract); + addHexPrefixToContractBytecode(compiledContract); if (shouldPersist) { await this._persistCompiledContractAsync( @@ -298,10 +306,14 @@ export class Compiler { const compiledContract = compilerOutput.contracts[contractPath][contractName]; // need to gather sourceCodes for this artifact, but compilerOutput.sources (the list of contract modules) - // contains listings for for every contract compiled during the compiler invocation that compiled the contract + // contains listings for every contract compiled during the compiler invocation that compiled the contract // to be persisted, which could include many that are irrelevant to the contract at hand. So, gather up only // the relevant sources: - const { sourceCodes, sources } = this._getSourcesWithDependencies(contractPath, compilerOutput.sources); + const { sourceCodes, sources } = getSourcesWithDependencies( + this._resolver, + contractPath, + compilerOutput.sources, + ); const contractVersion: ContractVersionData = { compilerOutput: compiledContract, @@ -336,130 +348,4 @@ export class Compiler { await fsWrapper.writeFileAsync(currentArtifactPath, artifactString); logUtils.warn(`${contractName} artifact saved!`); } - /** - * For the given @param contractPath, populates JSON objects to be used in the ContractVersionData interface's - * properties `sources` (source code file names mapped to ID numbers) and `sourceCodes` (source code content of - * contracts) for that contract. The source code pointed to by contractPath is read and parsed directly (via - * `this._resolver.resolve().source`), as are its imports, recursively. The ID numbers for @return `sources` are - * taken from the corresponding ID's in @param fullSources, and the content for @return sourceCodes is read from - * disk (via the aforementioned `resolver.source`). - */ - private _getSourcesWithDependencies( - contractPath: string, - fullSources: { [sourceName: string]: { id: number } }, - ): { sourceCodes: { [sourceName: string]: string }; sources: { [sourceName: string]: { id: number } } } { - const sources = { [contractPath]: { id: fullSources[contractPath].id } }; - const sourceCodes = { [contractPath]: this._resolver.resolve(contractPath).source }; - this._recursivelyGatherDependencySources( - contractPath, - sourceCodes[contractPath], - fullSources, - sources, - sourceCodes, - ); - return { sourceCodes, sources }; - } - private _recursivelyGatherDependencySources( - contractPath: string, - contractSource: string, - fullSources: { [sourceName: string]: { id: number } }, - sourcesToAppendTo: { [sourceName: string]: { id: number } }, - sourceCodesToAppendTo: { [sourceName: string]: string }, - ): void { - const importStatementMatches = contractSource.match(/\nimport[^;]*;/g); - if (importStatementMatches === null) { - return; - } - for (const importStatementMatch of importStatementMatches) { - const importPathMatches = importStatementMatch.match(/\"([^\"]*)\"/); - if (importPathMatches === null || importPathMatches.length === 0) { - continue; - } - - let importPath = importPathMatches[1]; - // HACK(ablrow): We have, e.g.: - // - // importPath = "../../utils/LibBytes/LibBytes.sol" - // contractPath = "2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol" - // - // Resolver doesn't understand "../" so we want to pass - // "2.0.0/utils/LibBytes/LibBytes.sol" to resolver. - // - // This hack involves using path.resolve. But path.resolve returns - // absolute directories by default. We trick it into thinking that - // contractPath is a root directory by prepending a '/' and then - // removing the '/' the end. - // - // path.resolve("/a/b/c", ""../../d/e") === "/a/d/e" - // - const lastPathSeparatorPos = contractPath.lastIndexOf('/'); - const contractFolder = lastPathSeparatorPos === -1 ? '' : contractPath.slice(0, lastPathSeparatorPos + 1); - if (importPath.startsWith('.')) { - /** - * Some imports path are relative ("../Token.sol", "./Wallet.sol") - * while others are absolute ("Token.sol", "@0x/contracts/Wallet.sol") - * And we need to append the base path for relative imports. - */ - importPath = path.resolve(`/${contractFolder}`, importPath).replace('/', ''); - } - - if (_.isUndefined(sourcesToAppendTo[importPath])) { - sourcesToAppendTo[importPath] = { id: fullSources[importPath].id }; - sourceCodesToAppendTo[importPath] = this._resolver.resolve(importPath).source; - - this._recursivelyGatherDependencySources( - importPath, - this._resolver.resolve(importPath).source, - fullSources, - sourcesToAppendTo, - sourceCodesToAppendTo, - ); - } - } - } - private _compile(solcInstance: solc.SolcInstance, standardInput: solc.StandardInput): solc.StandardOutput { - const compiled: solc.StandardOutput = JSON.parse( - solcInstance.compileStandardWrapper(JSON.stringify(standardInput), importPath => { - const sourceCodeIfExists = this._resolver.resolve(importPath); - return { contents: sourceCodeIfExists.source }; - }), - ); - if (!_.isUndefined(compiled.errors)) { - const SOLIDITY_WARNING = 'warning'; - const errors = _.filter(compiled.errors, entry => entry.severity !== SOLIDITY_WARNING); - const warnings = _.filter(compiled.errors, entry => entry.severity === SOLIDITY_WARNING); - if (!_.isEmpty(errors)) { - errors.forEach(error => { - const normalizedErrMsg = getNormalizedErrMsg(error.formattedMessage || error.message); - logUtils.warn(chalk.red(normalizedErrMsg)); - }); - throw new Error('Compilation errors encountered'); - } else { - warnings.forEach(warning => { - const normalizedWarningMsg = getNormalizedErrMsg(warning.formattedMessage || warning.message); - logUtils.warn(chalk.yellow(normalizedWarningMsg)); - }); - } - } - return compiled; - } - /** - * Gets the source tree hash for a file and its dependencies. - * @param fileName Name of contract file. - */ - private _getSourceTreeHash(importPath: string): Buffer { - const contractSource = this._resolver.resolve(importPath); - const dependencies = parseDependencies(contractSource); - const sourceHash = ethUtil.sha3(contractSource.source); - if (dependencies.length === 0) { - return sourceHash; - } else { - const dependencySourceTreeHashes = _.map(dependencies, (dependency: string) => - this._getSourceTreeHash(dependency), - ); - const sourceTreeHashesBuffer = Buffer.concat([sourceHash, ...dependencySourceTreeHashes]); - const sourceTreeHash = ethUtil.sha3(sourceTreeHashesBuffer); - return sourceTreeHash; - } - } } diff --git a/packages/sol-compiler/src/utils/compiler.ts b/packages/sol-compiler/src/utils/compiler.ts index cda67a414..db308f2b5 100644 --- a/packages/sol-compiler/src/utils/compiler.ts +++ b/packages/sol-compiler/src/utils/compiler.ts @@ -1,10 +1,18 @@ -import { ContractSource } from '@0x/sol-resolver'; -import { logUtils } from '@0x/utils'; +import { ContractSource, Resolver } from '@0x/sol-resolver'; +import { fetchAsync, logUtils } from '@0x/utils'; +import chalk from 'chalk'; import { ContractArtifact } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import * as path from 'path'; +import * as requireFromString from 'require-from-string'; +import * as solc from 'solc'; +import { binPaths } from '../solc/bin_paths'; + +import { constants } from './constants'; import { fsWrapper } from './fs_wrapper'; +import { CompilationError } from './types'; /** * Gets contract data on network or returns if an artifact does not exist. @@ -106,3 +114,208 @@ export function parseDependencies(contractSource: ContractSource): string[] { }); return dependencies; } + +/** + * Compiles the contracts and prints errors/warnings + * @param resolver Resolver + * @param solcInstance Instance of a solc compiler + * @param standardInput Solidity standard JSON input + */ +export function compile( + resolver: Resolver, + solcInstance: solc.SolcInstance, + standardInput: solc.StandardInput, +): solc.StandardOutput { + const standardInputStr = JSON.stringify(standardInput); + const standardOutputStr = solcInstance.compileStandardWrapper(standardInputStr, importPath => { + const sourceCodeIfExists = resolver.resolve(importPath); + return { contents: sourceCodeIfExists.source }; + }); + const compiled: solc.StandardOutput = JSON.parse(standardOutputStr); + if (!_.isUndefined(compiled.errors)) { + printCompilationErrorsAndWarnings(compiled.errors); + } + return compiled; +} +/** + * Separates errors from warnings, formats the messages and prints them. Throws if there is any compilation error (not warning). + * @param solcErrors The errors field of standard JSON output that contains errors and warnings. + */ +function printCompilationErrorsAndWarnings(solcErrors: solc.SolcError[]): void { + const SOLIDITY_WARNING = 'warning'; + const errors = _.filter(solcErrors, entry => entry.severity !== SOLIDITY_WARNING); + const warnings = _.filter(solcErrors, entry => entry.severity === SOLIDITY_WARNING); + if (!_.isEmpty(errors)) { + errors.forEach(error => { + const normalizedErrMsg = getNormalizedErrMsg(error.formattedMessage || error.message); + logUtils.log(chalk.red('error'), normalizedErrMsg); + }); + throw new CompilationError(errors.length); + } else { + warnings.forEach(warning => { + const normalizedWarningMsg = getNormalizedErrMsg(warning.formattedMessage || warning.message); + logUtils.log(chalk.yellow('warning'), normalizedWarningMsg); + }); + } +} + +/** + * Gets the source tree hash for a file and its dependencies. + * @param fileName Name of contract file. + */ +export function getSourceTreeHash(resolver: Resolver, importPath: string): Buffer { + const contractSource = resolver.resolve(importPath); + const dependencies = parseDependencies(contractSource); + const sourceHash = ethUtil.sha3(contractSource.source); + if (dependencies.length === 0) { + return sourceHash; + } else { + const dependencySourceTreeHashes = _.map(dependencies, (dependency: string) => + getSourceTreeHash(resolver, dependency), + ); + const sourceTreeHashesBuffer = Buffer.concat([sourceHash, ...dependencySourceTreeHashes]); + const sourceTreeHash = ethUtil.sha3(sourceTreeHashesBuffer); + return sourceTreeHash; + } +} + +/** + * For the given @param contractPath, populates JSON objects to be used in the ContractVersionData interface's + * properties `sources` (source code file names mapped to ID numbers) and `sourceCodes` (source code content of + * contracts) for that contract. The source code pointed to by contractPath is read and parsed directly (via + * `resolver.resolve().source`), as are its imports, recursively. The ID numbers for @return `sources` are + * taken from the corresponding ID's in @param fullSources, and the content for @return sourceCodes is read from + * disk (via the aforementioned `resolver.source`). + */ +export function getSourcesWithDependencies( + resolver: Resolver, + contractPath: string, + fullSources: { [sourceName: string]: { id: number } }, +): { sourceCodes: { [sourceName: string]: string }; sources: { [sourceName: string]: { id: number } } } { + const sources = { [contractPath]: { id: fullSources[contractPath].id } }; + const sourceCodes = { [contractPath]: resolver.resolve(contractPath).source }; + recursivelyGatherDependencySources( + resolver, + contractPath, + sourceCodes[contractPath], + fullSources, + sources, + sourceCodes, + ); + return { sourceCodes, sources }; +} + +function recursivelyGatherDependencySources( + resolver: Resolver, + contractPath: string, + contractSource: string, + fullSources: { [sourceName: string]: { id: number } }, + sourcesToAppendTo: { [sourceName: string]: { id: number } }, + sourceCodesToAppendTo: { [sourceName: string]: string }, +): void { + const importStatementMatches = contractSource.match(/\nimport[^;]*;/g); + if (importStatementMatches === null) { + return; + } + for (const importStatementMatch of importStatementMatches) { + const importPathMatches = importStatementMatch.match(/\"([^\"]*)\"/); + if (importPathMatches === null || importPathMatches.length === 0) { + continue; + } + + let importPath = importPathMatches[1]; + // HACK(albrow): We have, e.g.: + // + // importPath = "../../utils/LibBytes/LibBytes.sol" + // contractPath = "2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol" + // + // Resolver doesn't understand "../" so we want to pass + // "2.0.0/utils/LibBytes/LibBytes.sol" to resolver. + // + // This hack involves using path.resolve. But path.resolve returns + // absolute directories by default. We trick it into thinking that + // contractPath is a root directory by prepending a '/' and then + // removing the '/' the end. + // + // path.resolve("/a/b/c", ""../../d/e") === "/a/d/e" + // + const lastPathSeparatorPos = contractPath.lastIndexOf('/'); + const contractFolder = lastPathSeparatorPos === -1 ? '' : contractPath.slice(0, lastPathSeparatorPos + 1); + if (importPath.startsWith('.')) { + /** + * Some imports path are relative ("../Token.sol", "./Wallet.sol") + * while others are absolute ("Token.sol", "@0x/contracts/Wallet.sol") + * And we need to append the base path for relative imports. + */ + importPath = path.resolve(`/${contractFolder}`, importPath).replace('/', ''); + } + + if (_.isUndefined(sourcesToAppendTo[importPath])) { + sourcesToAppendTo[importPath] = { id: fullSources[importPath].id }; + sourceCodesToAppendTo[importPath] = resolver.resolve(importPath).source; + + recursivelyGatherDependencySources( + resolver, + importPath, + resolver.resolve(importPath).source, + fullSources, + sourcesToAppendTo, + sourceCodesToAppendTo, + ); + } + } +} + +/** + * Gets the solidity compiler instance and full version name. If the compiler is already cached - gets it from FS, + * otherwise - fetches it and caches it. + * @param solcVersion The compiler version. e.g. 0.5.0 + */ +export async function getSolcAsync( + solcVersion: string, +): Promise<{ solcInstance: solc.SolcInstance; fullSolcVersion: string }> { + const fullSolcVersion = binPaths[solcVersion]; + if (_.isUndefined(fullSolcVersion)) { + throw new Error(`${solcVersion} is not a known compiler version`); + } + const compilerBinFilename = path.join(constants.SOLC_BIN_DIR, fullSolcVersion); + let solcjs: string; + if (await fsWrapper.doesFileExistAsync(compilerBinFilename)) { + solcjs = (await fsWrapper.readFileAsync(compilerBinFilename)).toString(); + } else { + logUtils.warn(`Downloading ${fullSolcVersion}...`); + const url = `${constants.BASE_COMPILER_URL}${fullSolcVersion}`; + const response = await fetchAsync(url); + const SUCCESS_STATUS = 200; + if (response.status !== SUCCESS_STATUS) { + throw new Error(`Failed to load ${fullSolcVersion}`); + } + solcjs = await response.text(); + await fsWrapper.writeFileAsync(compilerBinFilename, solcjs); + } + if (solcjs.length === 0) { + throw new Error('No compiler available'); + } + const solcInstance = solc.setupMethods(requireFromString(solcjs, compilerBinFilename)); + return { solcInstance, fullSolcVersion }; +} + +/** + * Solidity compiler emits the bytecode without a 0x prefix for a hex. This function fixes it if bytecode is present. + * @param compiledContract The standard JSON output section for a contract. Geth modified in place. + */ +export function addHexPrefixToContractBytecode(compiledContract: solc.StandardContractOutput): void { + if (!_.isUndefined(compiledContract.evm)) { + if (!_.isUndefined(compiledContract.evm.bytecode) && !_.isUndefined(compiledContract.evm.bytecode.object)) { + compiledContract.evm.bytecode.object = ethUtil.addHexPrefix(compiledContract.evm.bytecode.object); + } + if ( + !_.isUndefined(compiledContract.evm.deployedBytecode) && + !_.isUndefined(compiledContract.evm.deployedBytecode.object) + ) { + compiledContract.evm.deployedBytecode.object = ethUtil.addHexPrefix( + compiledContract.evm.deployedBytecode.object, + ); + } + } +} diff --git a/packages/sol-compiler/src/utils/constants.ts b/packages/sol-compiler/src/utils/constants.ts index df2ddb3b2..433897f8a 100644 --- a/packages/sol-compiler/src/utils/constants.ts +++ b/packages/sol-compiler/src/utils/constants.ts @@ -1,5 +1,8 @@ +import * as path from 'path'; + export const constants = { SOLIDITY_FILE_EXTENSION: '.sol', BASE_COMPILER_URL: 'https://ethereum.github.io/solc-bin/bin/', LATEST_ARTIFACT_VERSION: '2.0.0', + SOLC_BIN_DIR: path.join(__dirname, '..', '..', 'solc_bin'), }; diff --git a/packages/sol-compiler/src/utils/types.ts b/packages/sol-compiler/src/utils/types.ts index b211cfcbc..64328899d 100644 --- a/packages/sol-compiler/src/utils/types.ts +++ b/packages/sol-compiler/src/utils/types.ts @@ -29,3 +29,12 @@ export interface Token { } export type DoneCallback = (err?: Error) => void; + +export class CompilationError extends Error { + public errorsCount: number; + public typeName = 'CompilationError'; + constructor(errorsCount: number) { + super('Compilation errors encountered'); + this.errorsCount = errorsCount; + } +} diff --git a/packages/sol-compiler/test/compiler_utils_test.ts b/packages/sol-compiler/test/compiler_utils_test.ts index 4fe7b994e..b8c18110c 100644 --- a/packages/sol-compiler/test/compiler_utils_test.ts +++ b/packages/sol-compiler/test/compiler_utils_test.ts @@ -52,7 +52,7 @@ describe('Compiler utils', () => { const source = await fsWrapper.readFileAsync(path, { encoding: 'utf8', }); - const dependencies = parseDependencies({ source, path }); + const dependencies = parseDependencies({ source, path, absolutePath: path }); const expectedDependencies = [ 'zeppelin-solidity/contracts/token/ERC20/ERC20.sol', 'packages/sol-compiler/lib/test/fixtures/contracts/TokenTransferProxy.sol', @@ -68,7 +68,7 @@ describe('Compiler utils', () => { const source = await fsWrapper.readFileAsync(path, { encoding: 'utf8', }); - expect(parseDependencies({ source, path })).to.be.deep.equal([ + expect(parseDependencies({ source, path, absolutePath: path })).to.be.deep.equal([ 'zeppelin-solidity/contracts/ownership/Ownable.sol', 'zeppelin-solidity/contracts/token/ERC20/ERC20.sol', ]); @@ -77,7 +77,7 @@ describe('Compiler utils', () => { it.skip('correctly parses commented out dependencies', async () => { const path = ''; const source = `// import "./TokenTransferProxy.sol";`; - expect(parseDependencies({ path, source })).to.be.deep.equal([]); + expect(parseDependencies({ path, source, absolutePath: path })).to.be.deep.equal([]); }); }); }); diff --git a/packages/sol-resolver/CHANGELOG.json b/packages/sol-resolver/CHANGELOG.json index 85398e624..74c4d39c5 100644 --- a/packages/sol-resolver/CHANGELOG.json +++ b/packages/sol-resolver/CHANGELOG.json @@ -1,5 +1,18 @@ [ { + "version": "1.2.1", + "changes": [ + { + "note": "Add `absolutePath` to `ContractSource` type", + "pr": 1461 + }, + { + "note": "Add `SpyResolver` that records all resolved contracts data", + "pr": 1461 + } + ] + }, + { "version": "1.1.1", "changes": [ { diff --git a/packages/sol-resolver/src/index.ts b/packages/sol-resolver/src/index.ts index a86053259..f55aca070 100644 --- a/packages/sol-resolver/src/index.ts +++ b/packages/sol-resolver/src/index.ts @@ -5,5 +5,6 @@ export { NPMResolver } from './resolvers/npm_resolver'; export { FSResolver } from './resolvers/fs_resolver'; export { RelativeFSResolver } from './resolvers/relative_fs_resolver'; export { NameResolver } from './resolvers/name_resolver'; +export { SpyResolver } from './resolvers/spy_resolver'; export { EnumerableResolver } from './resolvers/enumerable_resolver'; export { Resolver } from './resolvers/resolver'; diff --git a/packages/sol-resolver/src/resolvers/fs_resolver.ts b/packages/sol-resolver/src/resolvers/fs_resolver.ts index 63fc3448e..86128023d 100644 --- a/packages/sol-resolver/src/resolvers/fs_resolver.ts +++ b/packages/sol-resolver/src/resolvers/fs_resolver.ts @@ -9,10 +9,7 @@ export class FSResolver extends Resolver { public resolveIfExists(importPath: string): ContractSource | undefined { if (fs.existsSync(importPath) && fs.lstatSync(importPath).isFile()) { const fileContent = fs.readFileSync(importPath).toString(); - return { - source: fileContent, - path: importPath, - }; + return { source: fileContent, path: importPath, absolutePath: importPath }; } return undefined; } diff --git a/packages/sol-resolver/src/resolvers/name_resolver.ts b/packages/sol-resolver/src/resolvers/name_resolver.ts index d6ac6a499..aee326fb7 100644 --- a/packages/sol-resolver/src/resolvers/name_resolver.ts +++ b/packages/sol-resolver/src/resolvers/name_resolver.ts @@ -20,10 +20,7 @@ export class NameResolver extends EnumerableResolver { if (contractName === lookupContractName) { const absoluteContractPath = path.join(this._contractsDir, filePath); const source = fs.readFileSync(absoluteContractPath).toString(); - contractSource = { - source, - path: filePath, - }; + contractSource = { source, path: filePath, absolutePath: absoluteContractPath }; return true; } return undefined; @@ -36,10 +33,7 @@ export class NameResolver extends EnumerableResolver { const onFile = (filePath: string) => { const absoluteContractPath = path.join(this._contractsDir, filePath); const source = fs.readFileSync(absoluteContractPath).toString(); - const contractSource = { - source, - path: filePath, - }; + const contractSource = { source, path: filePath, absolutePath: absoluteContractPath }; contractSources.push(contractSource); }; this._traverseContractsDir(this._contractsDir, onFile); diff --git a/packages/sol-resolver/src/resolvers/npm_resolver.ts b/packages/sol-resolver/src/resolvers/npm_resolver.ts index eeb2b5493..3c1d09557 100644 --- a/packages/sol-resolver/src/resolvers/npm_resolver.ts +++ b/packages/sol-resolver/src/resolvers/npm_resolver.ts @@ -32,10 +32,7 @@ export class NPMResolver extends Resolver { const lookupPath = path.join(currentPath, 'node_modules', packagePath, pathWithinPackage); if (fs.existsSync(lookupPath) && fs.lstatSync(lookupPath).isFile()) { const fileContent = fs.readFileSync(lookupPath).toString(); - return { - source: fileContent, - path: lookupPath, - }; + return { source: fileContent, path: importPath, absolutePath: lookupPath }; } currentPath = path.dirname(currentPath); } diff --git a/packages/sol-resolver/src/resolvers/relative_fs_resolver.ts b/packages/sol-resolver/src/resolvers/relative_fs_resolver.ts index ed96040d3..cfff145f9 100644 --- a/packages/sol-resolver/src/resolvers/relative_fs_resolver.ts +++ b/packages/sol-resolver/src/resolvers/relative_fs_resolver.ts @@ -13,13 +13,10 @@ export class RelativeFSResolver extends Resolver { } // tslint:disable-next-line:prefer-function-over-method public resolveIfExists(importPath: string): ContractSource | undefined { - const filePath = path.join(this._contractsDir, importPath); + const filePath = path.resolve(path.join(this._contractsDir, importPath)); if (fs.existsSync(filePath) && !fs.lstatSync(filePath).isDirectory()) { const fileContent = fs.readFileSync(filePath).toString(); - return { - source: fileContent, - path: importPath, - }; + return { source: fileContent, path: importPath, absolutePath: filePath }; } return undefined; } diff --git a/packages/sol-resolver/src/resolvers/spy_resolver.ts b/packages/sol-resolver/src/resolvers/spy_resolver.ts new file mode 100644 index 000000000..5582d771a --- /dev/null +++ b/packages/sol-resolver/src/resolvers/spy_resolver.ts @@ -0,0 +1,25 @@ +import * as _ from 'lodash'; + +import { ContractSource } from '../types'; + +import { Resolver } from './resolver'; + +/** + * This resolver is a passthrough proxy to any resolver that records all the resolved contracts sources. + * You can access them later using the `resolvedContractSources` public field. + */ +export class SpyResolver extends Resolver { + public resolvedContractSources: ContractSource[] = []; + private readonly _resolver: Resolver; + constructor(resolver: Resolver) { + super(); + this._resolver = resolver; + } + public resolveIfExists(importPath: string): ContractSource | undefined { + const contractSourceIfExists = this._resolver.resolveIfExists(importPath); + if (!_.isUndefined(contractSourceIfExists)) { + this.resolvedContractSources.push(contractSourceIfExists); + } + return contractSourceIfExists; + } +} diff --git a/packages/sol-resolver/src/resolvers/url_resolver.ts b/packages/sol-resolver/src/resolvers/url_resolver.ts index 180b0c9f6..ef300e6db 100644 --- a/packages/sol-resolver/src/resolvers/url_resolver.ts +++ b/packages/sol-resolver/src/resolvers/url_resolver.ts @@ -11,10 +11,7 @@ export class URLResolver extends Resolver { if (importPath.startsWith(FILE_URL_PREXIF)) { const filePath = importPath.substr(FILE_URL_PREXIF.length); const fileContent = fs.readFileSync(filePath).toString(); - return { - source: fileContent, - path: importPath, - }; + return { source: fileContent, path: importPath, absolutePath: filePath }; } return undefined; } diff --git a/packages/sol-resolver/src/types.ts b/packages/sol-resolver/src/types.ts index 41492622d..b4ba164c8 100644 --- a/packages/sol-resolver/src/types.ts +++ b/packages/sol-resolver/src/types.ts @@ -1,6 +1,7 @@ export interface ContractSource { source: string; path: string; + absolutePath: string; } export interface ContractSources { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 6b728af71..4470dd501 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -243,6 +243,10 @@ export enum RevertReason { AuctionNotStarted = 'AUCTION_NOT_STARTED', AuctionInvalidBeginTime = 'INVALID_BEGIN_TIME', InvalidAssetData = 'INVALID_ASSET_DATA', + // Balance Threshold Filter + InvalidOrBlockedExchangeSelector = 'INVALID_OR_BLOCKED_EXCHANGE_SELECTOR', + BalanceQueryFailed = 'BALANCE_QUERY_FAILED', + AtLeastOneAddressDoesNotMeetBalanceThreshold = 'AT_LEAST_ONE_ADDRESS_DOES_NOT_MEET_BALANCE_THRESHOLD', } export enum StatusCodes { diff --git a/packages/typescript-typings/tsconfig.json b/packages/typescript-typings/tsconfig.json index 7f0fe2f7a..8ea3bfb0c 100644 --- a/packages/typescript-typings/tsconfig.json +++ b/packages/typescript-typings/tsconfig.json @@ -3,5 +3,6 @@ "compilerOptions": { "outDir": "lib", "rootDir": "." - } + }, + "include": ["types"] } diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index fe66d3f31..605151fb6 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "2.1.0", + "changes": [ + { + "note": "Add `logWithTime` to `logUtils`", + "pr": 1461 + } + ] + }, + { "version": "2.0.8", "changes": [ { diff --git a/packages/utils/package.json b/packages/utils/package.json index a25dc9cff..5ffec049a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -49,6 +49,7 @@ "@types/node": "*", "abortcontroller-polyfill": "^1.1.9", "bignumber.js": "~4.1.0", + "chalk": "^2.4.1", "detect-node": "2.0.3", "ethereum-types": "^1.1.4", "ethereumjs-util": "^5.1.1", diff --git a/packages/utils/src/log_utils.ts b/packages/utils/src/log_utils.ts index 87f8479b5..6d9996c67 100644 --- a/packages/utils/src/log_utils.ts +++ b/packages/utils/src/log_utils.ts @@ -1,3 +1,5 @@ +import chalk from 'chalk'; + export const logUtils = { log(...args: any[]): void { console.log(...args); // tslint:disable-line:no-console @@ -5,4 +7,7 @@ export const logUtils = { warn(...args: any[]): void { console.warn(...args); // tslint:disable-line:no-console }, + logWithTime(arg: string): void { + logUtils.log(`[${chalk.gray(new Date().toLocaleTimeString())}] ${arg}`); + }, }; diff --git a/packages/website/ts/@next/components/animatedChatIcon.tsx b/packages/website/ts/@next/components/animatedChatIcon.tsx index feaa0631f..9a86e244c 100644 --- a/packages/website/ts/@next/components/animatedChatIcon.tsx +++ b/packages/website/ts/@next/components/animatedChatIcon.tsx @@ -4,28 +4,69 @@ import styled, { keyframes } from 'styled-components'; export const AnimatedChatIcon = () => ( <svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg"> <mask id="mask30" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="150" height="150"> - <circle cx="75" cy="75" r="73" fill="#00AE99" stroke="#00AE99" stroke-width="3"/> + <circle cx="75" cy="75" r="73" fill="#00AE99" stroke="#00AE99" stroke-width="3" /> </mask> <g mask="url(#mask30)"> - <circle cx="75" cy="75" r="73" stroke="#00AE99" stroke-width="3"/> + <circle cx="75" cy="75" r="73" stroke="#00AE99" stroke-width="3" /> <Rays> - <path vector-effect="non-scaling-stroke" d="M76 37H137.5" stroke="#00AE99" stroke-width="3"/> - <path vector-effect="non-scaling-stroke" d="M37 73.5L37 12M113 137.5L113 75" stroke="#00AE99" stroke-width="3"/> - <path vector-effect="non-scaling-stroke" d="M13 113H71.5" stroke="#00AE99" stroke-width="3"/> - <path vector-effect="non-scaling-stroke" d="M49.087 47.5264L92.574 4.03932" stroke="#00AE99" stroke-width="3"/> - <path vector-effect="non-scaling-stroke" d="M47.3192 100.913L3.8321 57.4259M146.314 92.4277L102.12 48.2335" stroke="#00AE99" stroke-width="3"/> - <path vector-effect="non-scaling-stroke" d="M58.2793 145.814L101.766 102.327" stroke="#00AE99" stroke-width="3"/> + <path vector-effect="non-scaling-stroke" d="M76 37H137.5" stroke="#00AE99" stroke-width="3" /> + <path + vector-effect="non-scaling-stroke" + d="M37 73.5L37 12M113 137.5L113 75" + stroke="#00AE99" + stroke-width="3" + /> + <path vector-effect="non-scaling-stroke" d="M13 113H71.5" stroke="#00AE99" stroke-width="3" /> + <path + vector-effect="non-scaling-stroke" + d="M49.087 47.5264L92.574 4.03932" + stroke="#00AE99" + stroke-width="3" + /> + <path + vector-effect="non-scaling-stroke" + d="M47.3192 100.913L3.8321 57.4259M146.314 92.4277L102.12 48.2335" + stroke="#00AE99" + stroke-width="3" + /> + <path + vector-effect="non-scaling-stroke" + d="M58.2793 145.814L101.766 102.327" + stroke="#00AE99" + stroke-width="3" + /> </Rays> <Bubble> - <path vector-effect="non-scaling-stroke" d="M113 75C113 85.3064 108.897 94.6546 102.235 101.5C98.4048 105.436 71 132.5 71 132.5V112.792C51.8933 110.793 37 94.6359 37 75C37 54.0132 54.0132 37 75 37C95.9868 37 113 54.0132 113 75Z" stroke="#00AE99" strokeWidth="3"/> + <path + vector-effect="non-scaling-stroke" + d="M113 75C113 85.3064 108.897 94.6546 102.235 101.5C98.4048 105.436 71 132.5 71 132.5V112.792C51.8933 110.793 37 94.6359 37 75C37 54.0132 54.0132 37 75 37C95.9868 37 113 54.0132 113 75Z" + stroke="#00AE99" + strokeWidth="3" + /> </Bubble> - <Dot delay={0} vector-effect="non-scaling-stroke" cx="75" cy="75" r="4" stroke="#00AE99" strokeWidth="3"/> - <Dot delay={4.4} vector-effect="non-scaling-stroke" cx="91" cy="75" r="4" stroke="#00AE99" strokeWidth="3"/> - <Dot delay={-4.6} vector-effect="non-scaling-stroke" cx="59" cy="75" r="4" stroke="#00AE99" strokeWidth="3"/> + <Dot delay={0} vector-effect="non-scaling-stroke" cx="75" cy="75" r="4" stroke="#00AE99" strokeWidth="3" /> + <Dot + delay={4.4} + vector-effect="non-scaling-stroke" + cx="91" + cy="75" + r="4" + stroke="#00AE99" + strokeWidth="3" + /> + <Dot + delay={-4.6} + vector-effect="non-scaling-stroke" + cx="59" + cy="75" + r="4" + stroke="#00AE99" + strokeWidth="3" + /> </g> </svg> ); @@ -57,6 +98,9 @@ const Rays = styled.g` transform-origin: 50% 50%; `; -const Dot = styled.circle<{ delay: number }>` +const Dot = + styled.circle < + { delay: number } > + ` animation: ${fadeInOut} 4s ${props => `${props.delay}s`} infinite; `; diff --git a/packages/website/ts/@next/components/animatedCompassIcon.tsx b/packages/website/ts/@next/components/animatedCompassIcon.tsx index aa0cfd099..5388f95ca 100644 --- a/packages/website/ts/@next/components/animatedCompassIcon.tsx +++ b/packages/website/ts/@next/components/animatedCompassIcon.tsx @@ -4,17 +4,21 @@ import styled, { keyframes } from 'styled-components'; export const AnimatedCompassIcon = () => ( <svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg"> <g> - <circle cx="75" cy="75" r="73" stroke="#00AE99" stroke-width="3"/> - <circle cx="75" cy="75" r="58" stroke="#00AE99" stroke-width="3"/> - <Needle d="M62.9792 62.9792L36.6447 113.355L87.0208 87.0208M62.9792 62.9792L113.355 36.6447L87.0208 87.0208M62.9792 62.9792L87.0208 87.0208" stroke="#00AE99" strokeWidth="3"/> + <circle cx="75" cy="75" r="73" stroke="#00AE99" stroke-width="3" /> + <circle cx="75" cy="75" r="58" stroke="#00AE99" stroke-width="3" /> + <Needle + d="M62.9792 62.9792L36.6447 113.355L87.0208 87.0208M62.9792 62.9792L113.355 36.6447L87.0208 87.0208M62.9792 62.9792L87.0208 87.0208" + stroke="#00AE99" + strokeWidth="3" + /> <Dial> - <path d="M75 2V17M75 133V148" stroke="#00AE99" stroke-width="3"/> - <path d="M2 75L17 75M133 75L148 75" stroke="#00AE99" stroke-width="3"/> - <path d="M11.7801 38.5L24.7705 46M125.229 104L138.22 111.5" stroke="#00AE99" stroke-width="3"/> - <path d="M38.5001 11.7801L46.0001 24.7705M104 125.229L111.5 138.22" stroke="#00AE99" stroke-width="3"/> - <path d="M111.5 11.7801L104 24.7705M46 125.229L38.5 138.22" stroke="#00AE99" stroke-width="3"/> - <path d="M138.22 38.5L125.229 46M24.7705 104L11.7801 111.5" stroke="#00AE99" stroke-width="3"/> + <path d="M75 2V17M75 133V148" stroke="#00AE99" stroke-width="3" /> + <path d="M2 75L17 75M133 75L148 75" stroke="#00AE99" stroke-width="3" /> + <path d="M11.7801 38.5L24.7705 46M125.229 104L138.22 111.5" stroke="#00AE99" stroke-width="3" /> + <path d="M38.5001 11.7801L46.0001 24.7705M104 125.229L111.5 138.22" stroke="#00AE99" stroke-width="3" /> + <path d="M111.5 11.7801L104 24.7705M46 125.229L38.5 138.22" stroke="#00AE99" stroke-width="3" /> + <path d="M138.22 38.5L125.229 46M24.7705 104L11.7801 111.5" stroke="#00AE99" stroke-width="3" /> </Dial> </g> </svg> diff --git a/packages/website/ts/@next/components/blockIconLink.tsx b/packages/website/ts/@next/components/blockIconLink.tsx index 46a267889..bdcc5c29d 100644 --- a/packages/website/ts/@next/components/blockIconLink.tsx +++ b/packages/website/ts/@next/components/blockIconLink.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import {withRouter} from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import styled from 'styled-components'; -import {Button} from 'ts/@next/components/button'; -import {Icon} from 'ts/@next/components/icon'; +import { Button } from 'ts/@next/components/button'; +import { Icon } from 'ts/@next/components/icon'; interface Props { icon?: string; @@ -16,48 +16,26 @@ interface Props { class BaseComponent extends React.PureComponent<Props> { public onClick = (): void => { - const { - linkAction, - linkUrl, - } = this.props; + const { linkAction, linkUrl } = this.props; if (linkAction) { linkAction(); } else { this.props.history.push(linkUrl); } - } + }; public render(): React.ReactNode { - const { - icon, - iconComponent, - linkUrl, - linkAction, - title, - linkLabel, - } = this.props; + const { icon, iconComponent, linkUrl, linkAction, title, linkLabel } = this.props; return ( <Wrap onClick={this.onClick}> <div> - <Icon - name={icon} - component={iconComponent} - size="large" - margin={[0, 0, 'default', 0]} - /> + <Icon name={icon} component={iconComponent} size="large" margin={[0, 0, 'default', 0]} /> - <Title> - {title} - </Title> + <Title>{title}</Title> - <Button - isWithArrow={true} - isTransparent={true} - href={linkUrl} - onClick={linkAction} - > + <Button isWithArrow={true} isTransparent={true} href={linkUrl} onClick={linkAction}> {linkLabel} </Button> </div> diff --git a/packages/website/ts/@next/components/button.tsx b/packages/website/ts/@next/components/button.tsx index fdf396ef0..baca01a14 100644 --- a/packages/website/ts/@next/components/button.tsx +++ b/packages/website/ts/@next/components/button.tsx @@ -23,14 +23,14 @@ interface ButtonInterface { to?: string; onClick?: () => any; theme?: ThemeInterface; - useAnchorTag?: boolean; + shouldUseAnchorTag?: boolean; } export const Button = (props: ButtonInterface) => { - const { children, href, isWithArrow, to, useAnchorTag, target } = props; + const { children, href, isWithArrow, to, shouldUseAnchorTag, target } = props; let linkElem; - if (href || useAnchorTag) { + if (href || shouldUseAnchorTag) { linkElem = 'a'; } if (to) { diff --git a/packages/website/ts/@next/components/definition.tsx b/packages/website/ts/@next/components/definition.tsx index 965466f60..8adef8d54 100644 --- a/packages/website/ts/@next/components/definition.tsx +++ b/packages/website/ts/@next/components/definition.tsx @@ -9,7 +9,7 @@ interface Action { label: string; url?: string; onClick?: () => void; - useAnchorTag?: boolean; + shouldUseAnchorTag?: boolean; } interface Props { @@ -41,7 +41,9 @@ export const Definition = (props: Props) => ( </Heading> {typeof props.description === 'string' ? ( - <Paragraph isMuted={true} size={props.fontSize || 'default'}>{props.description}</Paragraph> + <Paragraph isMuted={true} size={props.fontSize || 'default'}> + {props.description} + </Paragraph> ) : ( <>{props.description}</> )} @@ -55,7 +57,7 @@ export const Definition = (props: Props) => ( onClick={item.onClick} isWithArrow={true} isAccentColor={true} - useAnchorTag={item.useAnchorTag} + shouldUseAnchorTag={item.shouldUseAnchorTag} target="_blank" > {item.label} diff --git a/packages/website/ts/@next/components/footer.tsx b/packages/website/ts/@next/components/footer.tsx index fedae5a1b..b30a0cc5e 100644 --- a/packages/website/ts/@next/components/footer.tsx +++ b/packages/website/ts/@next/components/footer.tsx @@ -2,7 +2,6 @@ import { Link as SmartLink } from '@0x/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; import MediaQuery from 'react-responsive'; -import { Link as ReactRouterLink } from 'react-router-dom'; import styled from 'styled-components'; import { Logo } from 'ts/@next/components/logo'; diff --git a/packages/website/ts/@next/components/hamburger.tsx b/packages/website/ts/@next/components/hamburger.tsx index b5c01a2b0..435d206cd 100644 --- a/packages/website/ts/@next/components/hamburger.tsx +++ b/packages/website/ts/@next/components/hamburger.tsx @@ -16,7 +16,10 @@ export const Hamburger: React.FunctionComponent<Props> = (props: Props) => { ); }; -const StyledHamburger = styled.button<Props>` +const StyledHamburger = + styled.button < + Props > + ` background: none; border: 0; width: 22px; @@ -50,7 +53,9 @@ const StyledHamburger = styled.button<Props>` //transform-origin: 0% 100%; } - ${props => props.isOpen && ` + ${props => + props.isOpen && + ` opacity: 1; transform: rotate(45deg) translate(0, 1px); diff --git a/packages/website/ts/@next/components/hero.tsx b/packages/website/ts/@next/components/hero.tsx index c79e2a6eb..4c8874d3e 100644 --- a/packages/website/ts/@next/components/hero.tsx +++ b/packages/website/ts/@next/components/hero.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import styled from 'styled-components'; -import {addFadeInAnimation} from 'ts/@next/constants/animations'; +import { addFadeInAnimation } from 'ts/@next/constants/animations'; interface Props { title: string; @@ -15,38 +15,6 @@ interface Props { actions?: React.ReactNode; } -export const Hero = (props: Props) => ( - <Section> - <Wrap isCentered={!props.figure} isFullWidth={props.isFullWidth} isCenteredMobile={props.isCenteredMobile}> - {props.figure && - <Content width="400px"> - {props.figure} - </Content> - } - - <Content width={props.maxWidth ? props.maxWidth : (props.figure ? '546px' : '678px')}> - <Title isLarge={props.isLargeTitle} maxWidth={props.maxWidthHeading}> - {props.title} - </Title> - - <Description> - {props.description} - </Description> - - {props.actions && - <ButtonWrap> - {props.actions} - </ButtonWrap> - } - </Content> - </Wrap> - </Section> -); - -Hero.defaultProps = { - isCenteredMobile: true, -}; - const Section = styled.section` padding: 120px 0; @@ -55,26 +23,41 @@ const Section = styled.section` } `; -const Wrap = styled.div<{ isCentered?: boolean; isFullWidth?: boolean; isCenteredMobile?: boolean }>` +interface WrapProps { + isCentered?: boolean; + isFullWidth?: boolean; + isCenteredMobile?: boolean; +} +const Wrap = + styled.div < + WrapProps > + ` width: calc(100% - 60px); margin: 0 auto; @media (min-width: 768px) { - max-width: ${props => !props.isFullWidth ? '895px' : '1136px'}; + max-width: ${props => (!props.isFullWidth ? '895px' : '1136px')}; flex-direction: row-reverse; display: flex; align-items: center; text-align: ${props => props.isCentered && 'center'}; - justify-content: ${props => props.isCentered ? 'center' : 'space-between'}; + justify-content: ${props => (props.isCentered ? 'center' : 'space-between')}; } @media (max-width: 768px) { - text-align: ${props => props.isCenteredMobile ? `center` : 'left'}; + text-align: ${props => (props.isCenteredMobile ? `center` : 'left')}; } `; -const Title = styled.h1<{ isLarge?: any; maxWidth?: string }>` - font-size: ${props => props.isLarge ? '80px' : '50px'}; +interface TitleProps { + isLarge?: any; + maxWidth?: string; +} +const Title = + styled.h1 < + TitleProps > + ` + font-size: ${props => (props.isLarge ? '80px' : '50px')}; font-weight: 300; line-height: 1.1; margin-left: auto; @@ -99,14 +82,15 @@ const Description = styled.p` padding: 0; margin-bottom: 50px; color: ${props => props.theme.introTextColor}; - ${addFadeInAnimation('0.5s', '0.15s')} - - @media (max-width: 1024px) { + ${addFadeInAnimation('0.5s', '0.15s')} @media (max-width: 1024px) { margin-bottom: 30px; } `; -const Content = styled.div<{ width: string }>` +const Content = + styled.div < + { width: string } > + ` width: 100%; @media (min-width: 768px) { @@ -123,10 +107,10 @@ const ButtonWrap = styled.div` } > *:nth-child(1) { - ${addFadeInAnimation('0.6s', '0.3s')} + ${addFadeInAnimation('0.6s', '0.3s')}; } > *:nth-child(2) { - ${addFadeInAnimation('0.6s', '0.4s')} + ${addFadeInAnimation('0.6s', '0.4s')}; } @media (max-width: 500px) { @@ -144,3 +128,25 @@ const ButtonWrap = styled.div` } } `; + +export const Hero: React.StatelessComponent<Props> = (props: Props) => ( + <Section> + <Wrap isCentered={!props.figure} isFullWidth={props.isFullWidth} isCenteredMobile={props.isCenteredMobile}> + {props.figure && <Content width="400px">{props.figure}</Content>} + + <Content width={props.maxWidth ? props.maxWidth : props.figure ? '546px' : '678px'}> + <Title isLarge={props.isLargeTitle} maxWidth={props.maxWidthHeading}> + {props.title} + </Title> + + <Description>{props.description}</Description> + + {props.actions && <ButtonWrap>{props.actions}</ButtonWrap>} + </Content> + </Wrap> + </Section> +); + +Hero.defaultProps = { + isCenteredMobile: true, +}; diff --git a/packages/website/ts/@next/components/heroAnimation.tsx b/packages/website/ts/@next/components/heroAnimation.tsx index 32117c1f8..42956fb6a 100644 --- a/packages/website/ts/@next/components/heroAnimation.tsx +++ b/packages/website/ts/@next/components/heroAnimation.tsx @@ -4,16 +4,49 @@ import styled, { keyframes } from 'styled-components'; export const HeroAnimation = () => ( <Image width="404" height="404" viewBox="0 0 404 404" fill="none" xmlns="http://www.w3.org/2000/svg"> <mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="404" height="404"> - <circle cx="202" cy="202" r="200" fill="#00AE99" stroke="#00AE99" stroke-width="3"/> + <circle cx="202" cy="202" r="200" fill="#00AE99" stroke="#00AE99" stroke-width="3" /> </mask> <g mask="url(#mask0)"> <circle cx="202" cy="202" r="200" stroke="#00AE99" strokeWidth="3" /> - <TopCircle vector-effect="non-scaling-stroke" cx="201.667" cy="68.6667" r="66.6667" stroke="#00AE99" strokeWidth="3"/> - <LeftCircle vector-effect="non-scaling-stroke" cx="68.6667" cy="202.667" r="66.6667" stroke="#00AE99" strokeWidth="3"/> - <Logo vector-effect="non-scaling-stroke" d="M168.17 260.29L167.271 259.089L165.46 260.444L167.413 261.585L168.17 260.29ZM197.32 269.2L197.219 270.696L197.226 270.697L197.32 269.2ZM237.414 258.856L238.22 260.12L238.225 260.117L237.414 258.856ZM252.653 245.439L253.801 246.405L254.55 245.515L253.874 244.568L252.653 245.439ZM241.096 229.872L242.285 228.958L242.281 228.952L242.276 228.946L241.096 229.872ZM237.72 225.571L238.901 224.645L237.582 222.965L236.449 224.775L237.72 225.571ZM219.719 241.445L218.672 242.519L219.418 243.246L220.36 242.801L219.719 241.445ZM208.264 230.282L209.311 229.207L208.392 228.312L207.365 229.081L208.264 230.282ZM143.827 169.549L145.02 168.64L143.647 166.838L142.524 168.806L143.827 169.549ZM135.133 198.43L133.637 198.329L133.636 198.337L135.133 198.43ZM145.464 238.577L144.201 239.388L145.464 238.577ZM158.862 253.837L157.895 254.984L158.786 255.736L159.735 255.057L158.862 253.837ZM174.409 242.264L175.324 243.453L175.33 243.448L175.336 243.443L174.409 242.264ZM178.705 238.885L179.632 240.064L181.287 238.761L179.516 237.623L178.705 238.885ZM162.851 220.757L161.78 219.707L161.049 220.452L161.495 221.397L162.851 220.757ZM174.102 209.286L175.173 210.337L176.082 209.41L175.295 208.377L174.102 209.286ZM235.163 145.072L236.036 146.292L237.92 144.945L235.92 143.777L235.163 145.072ZM206.014 136.162L205.91 137.658L205.913 137.658L206.014 136.162ZM165.817 146.506L166.629 147.767L166.632 147.765L165.817 146.506ZM150.578 159.922L149.43 158.956L148.681 159.846L149.357 160.793L150.578 159.922ZM162.135 175.489L160.946 176.403L160.951 176.409L160.955 176.415L162.135 175.489ZM165.511 179.791L164.331 180.717L165.634 182.378L166.773 180.6L165.511 179.791ZM183.614 163.916L184.655 162.836L183.913 162.122L182.98 162.557L183.614 163.916ZM194.354 174.26L193.313 175.341L194.212 176.206L195.226 175.48L194.354 174.26ZM259.608 235.505L258.411 236.409L259.789 238.233L260.914 236.243L259.608 235.505ZM268.2 206.931L269.696 207.033L269.697 207.024L268.2 206.931ZM257.87 166.784L259.135 165.979L259.132 165.974L257.87 166.784ZM244.471 151.524L245.439 150.378L244.547 149.625L243.598 150.304L244.471 151.524ZM228.924 163.097L228.009 161.909L228.003 161.913L227.997 161.918L228.924 163.097ZM224.629 166.477L223.701 165.298L222.034 166.609L223.826 167.744L224.629 166.477ZM240.584 184.604L239.228 185.244L239.235 185.259L239.242 185.274L240.584 184.604ZM240.687 184.809L241.767 185.849L242.502 185.086L242.029 184.139L240.687 184.809ZM229.845 196.075L228.764 195.035L227.877 195.957L228.648 196.979L229.845 196.075ZM167.413 261.585C176.201 266.718 186.346 269.964 197.219 270.696L197.421 267.703C187.019 267.002 177.321 263.898 168.926 258.994L167.413 261.585ZM197.226 270.697C212.283 271.639 226.405 267.659 238.22 260.12L236.607 257.591C225.307 264.8 211.813 268.604 197.413 267.703L197.226 270.697ZM238.225 260.117C244.08 256.348 249.307 251.742 253.801 246.405L251.506 244.473C247.204 249.583 242.203 253.989 236.602 257.594L238.225 260.117ZM253.874 244.568C250.283 239.533 246.385 234.295 242.285 228.958L239.906 230.786C243.989 236.1 247.864 241.309 251.432 246.31L253.874 244.568ZM242.276 228.946C241.713 228.229 241.151 227.512 240.588 226.795C240.026 226.078 239.463 225.362 238.901 224.645L236.54 226.497C237.103 227.213 237.665 227.93 238.228 228.647C238.791 229.364 239.353 230.081 239.916 230.798L242.276 228.946ZM236.449 224.775C232.311 231.384 226.193 236.725 219.078 240.089L220.36 242.801C227.974 239.201 234.538 233.481 238.992 226.367L236.449 224.775ZM220.766 240.371L209.311 229.207L207.217 231.356L218.672 242.519L220.766 240.371ZM207.365 229.081L167.271 259.089L169.069 261.49L209.163 231.483L207.365 229.081ZM142.524 168.806C137.505 177.601 134.368 187.549 133.637 198.329L136.63 198.532C137.33 188.214 140.33 178.703 145.13 170.293L142.524 168.806ZM133.636 198.337C132.696 213.409 136.668 227.654 144.201 239.388L146.726 237.767C139.531 226.56 135.73 212.947 136.63 198.524L133.636 198.337ZM144.201 239.388C147.965 245.25 152.565 250.484 157.895 254.984L159.83 252.691C154.727 248.383 150.327 243.376 146.726 237.767L144.201 239.388ZM159.735 255.057C164.764 251.461 169.994 247.558 175.324 243.453L173.494 241.076C168.187 245.164 162.985 249.045 157.99 252.617L159.735 255.057ZM175.336 243.443C176.768 242.317 178.2 241.19 179.632 240.064L177.777 237.706C176.345 238.832 174.913 239.959 173.481 241.086L175.336 243.443ZM179.516 237.623C172.904 233.374 167.568 227.241 164.208 220.117L161.495 221.397C165.09 229.021 170.8 235.588 177.894 240.147L179.516 237.623ZM163.922 221.807L175.173 210.337L173.031 208.236L161.78 219.707L163.922 221.807ZM175.295 208.377L145.02 168.64L142.634 170.458L172.909 210.196L175.295 208.377ZM235.92 143.777C227.132 138.643 216.987 135.398 206.114 134.665L205.913 137.658C216.315 138.359 226.012 141.463 234.407 146.367L235.92 143.777ZM206.118 134.665C191.055 133.618 176.824 137.599 165.003 145.246L166.632 147.765C177.926 140.459 191.515 136.657 205.91 137.658L206.118 134.665ZM165.006 145.244C159.151 149.013 153.924 153.619 149.43 158.956L151.725 160.888C156.027 155.779 161.028 151.372 166.629 147.767L165.006 145.244ZM149.357 160.793C152.948 165.828 156.846 171.066 160.946 176.403L163.325 174.575C159.242 169.261 155.367 164.052 151.799 159.051L149.357 160.793ZM160.955 176.415C161.518 177.132 162.08 177.849 162.643 178.566C163.205 179.283 163.768 180 164.331 180.717L166.691 178.865C166.128 178.148 165.566 177.431 165.003 176.714C164.441 175.997 163.878 175.28 163.315 174.563L160.955 176.415ZM166.773 180.6C171.021 173.973 177.044 168.635 184.248 165.276L182.98 162.557C175.251 166.161 168.796 171.885 164.248 178.981L166.773 180.6ZM182.574 164.997L193.313 175.341L195.394 173.18L184.655 162.836L182.574 164.997ZM195.226 175.48L236.036 146.292L234.291 143.852L193.481 173.04L195.226 175.48ZM260.914 236.243C265.827 227.556 268.964 217.713 269.696 207.033L266.703 206.828C266.003 217.042 263.004 226.453 258.303 234.767L260.914 236.243ZM269.697 207.024C270.638 191.949 266.663 177.81 259.135 165.979L256.604 167.589C263.804 178.904 267.603 192.417 266.703 206.837L269.697 207.024ZM259.132 165.974C255.368 160.111 250.769 154.878 245.439 150.378L243.503 152.67C248.606 156.978 253.007 161.986 256.607 167.594L259.132 165.974ZM243.598 150.304C238.57 153.901 233.339 157.803 228.009 161.909L229.84 164.285C235.147 160.197 240.349 156.316 245.344 152.744L243.598 150.304ZM227.997 161.918C227.281 162.481 226.565 163.045 225.849 163.608C225.133 164.171 224.417 164.734 223.701 165.298L225.556 167.656C226.272 167.092 226.988 166.529 227.704 165.966C228.42 165.402 229.136 164.839 229.852 164.276L227.997 161.918ZM223.826 167.744C230.535 171.992 235.869 178.121 239.228 185.244L241.941 183.964C238.345 176.339 232.632 169.769 225.431 165.209L223.826 167.744ZM239.242 185.274L239.345 185.479L242.029 184.139L241.926 183.934L239.242 185.274ZM239.606 183.769L228.764 195.035L230.926 197.115L241.767 185.849L239.606 183.769ZM228.648 196.979L258.411 236.409L260.806 234.601L231.042 195.171L228.648 196.979Z" fill="#00AE99"/> - <Rectangle vector-effect="non-scaling-stroke" d="M269 135V268.333H442V135H269Z" stroke="#00AE99" strokeWidth="3"/> - <Square vector-effect="non-scaling-stroke" d="M339.64 269.64L270 339.281L343.913 413.194L413.554 343.554L339.64 269.64Z" stroke="#00AE99" strokeWidth="3"/> - <Oblong vector-effect="non-scaling-stroke" d="M202.5 269C202.5 269 269 269 269 335.5C269 402 202.5 402 202.5 402H-6.5C-6.5 402 -77 402 -77 335.5C-77 269 -6.5 269 -6.5 269H202.5Z" stroke="#00AE99" strokeWidth="3"/> + <TopCircle + vector-effect="non-scaling-stroke" + cx="201.667" + cy="68.6667" + r="66.6667" + stroke="#00AE99" + strokeWidth="3" + /> + <LeftCircle + vector-effect="non-scaling-stroke" + cx="68.6667" + cy="202.667" + r="66.6667" + stroke="#00AE99" + strokeWidth="3" + /> + <Logo + vector-effect="non-scaling-stroke" + d="M168.17 260.29L167.271 259.089L165.46 260.444L167.413 261.585L168.17 260.29ZM197.32 269.2L197.219 270.696L197.226 270.697L197.32 269.2ZM237.414 258.856L238.22 260.12L238.225 260.117L237.414 258.856ZM252.653 245.439L253.801 246.405L254.55 245.515L253.874 244.568L252.653 245.439ZM241.096 229.872L242.285 228.958L242.281 228.952L242.276 228.946L241.096 229.872ZM237.72 225.571L238.901 224.645L237.582 222.965L236.449 224.775L237.72 225.571ZM219.719 241.445L218.672 242.519L219.418 243.246L220.36 242.801L219.719 241.445ZM208.264 230.282L209.311 229.207L208.392 228.312L207.365 229.081L208.264 230.282ZM143.827 169.549L145.02 168.64L143.647 166.838L142.524 168.806L143.827 169.549ZM135.133 198.43L133.637 198.329L133.636 198.337L135.133 198.43ZM145.464 238.577L144.201 239.388L145.464 238.577ZM158.862 253.837L157.895 254.984L158.786 255.736L159.735 255.057L158.862 253.837ZM174.409 242.264L175.324 243.453L175.33 243.448L175.336 243.443L174.409 242.264ZM178.705 238.885L179.632 240.064L181.287 238.761L179.516 237.623L178.705 238.885ZM162.851 220.757L161.78 219.707L161.049 220.452L161.495 221.397L162.851 220.757ZM174.102 209.286L175.173 210.337L176.082 209.41L175.295 208.377L174.102 209.286ZM235.163 145.072L236.036 146.292L237.92 144.945L235.92 143.777L235.163 145.072ZM206.014 136.162L205.91 137.658L205.913 137.658L206.014 136.162ZM165.817 146.506L166.629 147.767L166.632 147.765L165.817 146.506ZM150.578 159.922L149.43 158.956L148.681 159.846L149.357 160.793L150.578 159.922ZM162.135 175.489L160.946 176.403L160.951 176.409L160.955 176.415L162.135 175.489ZM165.511 179.791L164.331 180.717L165.634 182.378L166.773 180.6L165.511 179.791ZM183.614 163.916L184.655 162.836L183.913 162.122L182.98 162.557L183.614 163.916ZM194.354 174.26L193.313 175.341L194.212 176.206L195.226 175.48L194.354 174.26ZM259.608 235.505L258.411 236.409L259.789 238.233L260.914 236.243L259.608 235.505ZM268.2 206.931L269.696 207.033L269.697 207.024L268.2 206.931ZM257.87 166.784L259.135 165.979L259.132 165.974L257.87 166.784ZM244.471 151.524L245.439 150.378L244.547 149.625L243.598 150.304L244.471 151.524ZM228.924 163.097L228.009 161.909L228.003 161.913L227.997 161.918L228.924 163.097ZM224.629 166.477L223.701 165.298L222.034 166.609L223.826 167.744L224.629 166.477ZM240.584 184.604L239.228 185.244L239.235 185.259L239.242 185.274L240.584 184.604ZM240.687 184.809L241.767 185.849L242.502 185.086L242.029 184.139L240.687 184.809ZM229.845 196.075L228.764 195.035L227.877 195.957L228.648 196.979L229.845 196.075ZM167.413 261.585C176.201 266.718 186.346 269.964 197.219 270.696L197.421 267.703C187.019 267.002 177.321 263.898 168.926 258.994L167.413 261.585ZM197.226 270.697C212.283 271.639 226.405 267.659 238.22 260.12L236.607 257.591C225.307 264.8 211.813 268.604 197.413 267.703L197.226 270.697ZM238.225 260.117C244.08 256.348 249.307 251.742 253.801 246.405L251.506 244.473C247.204 249.583 242.203 253.989 236.602 257.594L238.225 260.117ZM253.874 244.568C250.283 239.533 246.385 234.295 242.285 228.958L239.906 230.786C243.989 236.1 247.864 241.309 251.432 246.31L253.874 244.568ZM242.276 228.946C241.713 228.229 241.151 227.512 240.588 226.795C240.026 226.078 239.463 225.362 238.901 224.645L236.54 226.497C237.103 227.213 237.665 227.93 238.228 228.647C238.791 229.364 239.353 230.081 239.916 230.798L242.276 228.946ZM236.449 224.775C232.311 231.384 226.193 236.725 219.078 240.089L220.36 242.801C227.974 239.201 234.538 233.481 238.992 226.367L236.449 224.775ZM220.766 240.371L209.311 229.207L207.217 231.356L218.672 242.519L220.766 240.371ZM207.365 229.081L167.271 259.089L169.069 261.49L209.163 231.483L207.365 229.081ZM142.524 168.806C137.505 177.601 134.368 187.549 133.637 198.329L136.63 198.532C137.33 188.214 140.33 178.703 145.13 170.293L142.524 168.806ZM133.636 198.337C132.696 213.409 136.668 227.654 144.201 239.388L146.726 237.767C139.531 226.56 135.73 212.947 136.63 198.524L133.636 198.337ZM144.201 239.388C147.965 245.25 152.565 250.484 157.895 254.984L159.83 252.691C154.727 248.383 150.327 243.376 146.726 237.767L144.201 239.388ZM159.735 255.057C164.764 251.461 169.994 247.558 175.324 243.453L173.494 241.076C168.187 245.164 162.985 249.045 157.99 252.617L159.735 255.057ZM175.336 243.443C176.768 242.317 178.2 241.19 179.632 240.064L177.777 237.706C176.345 238.832 174.913 239.959 173.481 241.086L175.336 243.443ZM179.516 237.623C172.904 233.374 167.568 227.241 164.208 220.117L161.495 221.397C165.09 229.021 170.8 235.588 177.894 240.147L179.516 237.623ZM163.922 221.807L175.173 210.337L173.031 208.236L161.78 219.707L163.922 221.807ZM175.295 208.377L145.02 168.64L142.634 170.458L172.909 210.196L175.295 208.377ZM235.92 143.777C227.132 138.643 216.987 135.398 206.114 134.665L205.913 137.658C216.315 138.359 226.012 141.463 234.407 146.367L235.92 143.777ZM206.118 134.665C191.055 133.618 176.824 137.599 165.003 145.246L166.632 147.765C177.926 140.459 191.515 136.657 205.91 137.658L206.118 134.665ZM165.006 145.244C159.151 149.013 153.924 153.619 149.43 158.956L151.725 160.888C156.027 155.779 161.028 151.372 166.629 147.767L165.006 145.244ZM149.357 160.793C152.948 165.828 156.846 171.066 160.946 176.403L163.325 174.575C159.242 169.261 155.367 164.052 151.799 159.051L149.357 160.793ZM160.955 176.415C161.518 177.132 162.08 177.849 162.643 178.566C163.205 179.283 163.768 180 164.331 180.717L166.691 178.865C166.128 178.148 165.566 177.431 165.003 176.714C164.441 175.997 163.878 175.28 163.315 174.563L160.955 176.415ZM166.773 180.6C171.021 173.973 177.044 168.635 184.248 165.276L182.98 162.557C175.251 166.161 168.796 171.885 164.248 178.981L166.773 180.6ZM182.574 164.997L193.313 175.341L195.394 173.18L184.655 162.836L182.574 164.997ZM195.226 175.48L236.036 146.292L234.291 143.852L193.481 173.04L195.226 175.48ZM260.914 236.243C265.827 227.556 268.964 217.713 269.696 207.033L266.703 206.828C266.003 217.042 263.004 226.453 258.303 234.767L260.914 236.243ZM269.697 207.024C270.638 191.949 266.663 177.81 259.135 165.979L256.604 167.589C263.804 178.904 267.603 192.417 266.703 206.837L269.697 207.024ZM259.132 165.974C255.368 160.111 250.769 154.878 245.439 150.378L243.503 152.67C248.606 156.978 253.007 161.986 256.607 167.594L259.132 165.974ZM243.598 150.304C238.57 153.901 233.339 157.803 228.009 161.909L229.84 164.285C235.147 160.197 240.349 156.316 245.344 152.744L243.598 150.304ZM227.997 161.918C227.281 162.481 226.565 163.045 225.849 163.608C225.133 164.171 224.417 164.734 223.701 165.298L225.556 167.656C226.272 167.092 226.988 166.529 227.704 165.966C228.42 165.402 229.136 164.839 229.852 164.276L227.997 161.918ZM223.826 167.744C230.535 171.992 235.869 178.121 239.228 185.244L241.941 183.964C238.345 176.339 232.632 169.769 225.431 165.209L223.826 167.744ZM239.242 185.274L239.345 185.479L242.029 184.139L241.926 183.934L239.242 185.274ZM239.606 183.769L228.764 195.035L230.926 197.115L241.767 185.849L239.606 183.769ZM228.648 196.979L258.411 236.409L260.806 234.601L231.042 195.171L228.648 196.979Z" + fill="#00AE99" + /> + <Rectangle + vector-effect="non-scaling-stroke" + d="M269 135V268.333H442V135H269Z" + stroke="#00AE99" + strokeWidth="3" + /> + <Square + vector-effect="non-scaling-stroke" + d="M339.64 269.64L270 339.281L343.913 413.194L413.554 343.554L339.64 269.64Z" + stroke="#00AE99" + strokeWidth="3" + /> + <Oblong + vector-effect="non-scaling-stroke" + d="M202.5 269C202.5 269 269 269 269 335.5C269 402 202.5 402 202.5 402H-6.5C-6.5 402 -77 402 -77 335.5C-77 269 -6.5 269 -6.5 269H202.5Z" + stroke="#00AE99" + strokeWidth="3" + /> </g> </Image> ); diff --git a/packages/website/ts/@next/components/heroImage.tsx b/packages/website/ts/@next/components/heroImage.tsx index 956218083..af7c055ac 100644 --- a/packages/website/ts/@next/components/heroImage.tsx +++ b/packages/website/ts/@next/components/heroImage.tsx @@ -5,11 +5,7 @@ interface Props { image: React.ReactNode; } -export const LandingAnimation = (props: Props) => ( - <Wrap> - {props.image} - </Wrap> -); +export const LandingAnimation = (props: Props) => <Wrap>{props.image}</Wrap>; const Wrap = styled.figure` display: inline-block; diff --git a/packages/website/ts/@next/components/icon.tsx b/packages/website/ts/@next/components/icon.tsx index d9632a3c7..fc9d488f9 100644 --- a/packages/website/ts/@next/components/icon.tsx +++ b/packages/website/ts/@next/components/icon.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import Loadable from 'react-loadable'; import styled from 'styled-components'; -import {Paragraph} from 'ts/@next/components/text'; -import {getCSSPadding, PaddingInterface} from 'ts/@next/constants/utilities'; +import { Paragraph } from 'ts/@next/components/text'; +import { getCSSPadding, PaddingInterface } from 'ts/@next/constants/utilities'; interface IconProps extends PaddingInterface { name?: string; @@ -14,7 +14,7 @@ interface IconProps extends PaddingInterface { export const Icon: React.FunctionComponent<IconProps> = (props: IconProps) => { if (props.name && !props.component) { const IconSVG = Loadable({ - loader: async () => import(/* webpackChunkName: "icon" */`ts/@next/icons/illustrations/${props.name}.svg`), + loader: async () => import(/* webpackChunkName: "icon" */ `ts/@next/icons/illustrations/${props.name}.svg`), loading: () => <Paragraph>Loading</Paragraph>, }); @@ -26,17 +26,16 @@ export const Icon: React.FunctionComponent<IconProps> = (props: IconProps) => { } if (props.component) { - return ( - <StyledIcon {...props}> - {props.component} - </StyledIcon> - ); + return <StyledIcon {...props}>{props.component}</StyledIcon>; } return null; }; -export const InlineIconWrap = styled.div<PaddingInterface>` +export const InlineIconWrap = + styled.div < + PaddingInterface > + ` margin: ${props => getCSSPadding(props.margin)}; display: flex; align-items: center; @@ -55,7 +54,10 @@ const _getSize = (size: string | number = 'small'): string => { return `${size}px`; }; -const StyledIcon = styled.figure<IconProps>` +const StyledIcon = + styled.figure < + IconProps > + ` width: ${props => _getSize(props.size)}; height: ${props => _getSize(props.size)}; margin: ${props => getCSSPadding(props.margin)}; diff --git a/packages/website/ts/@next/components/image.tsx b/packages/website/ts/@next/components/image.tsx index 34520b619..65b2a9705 100644 --- a/packages/website/ts/@next/components/image.tsx +++ b/packages/website/ts/@next/components/image.tsx @@ -9,11 +9,12 @@ interface Props { } const ImageClass: React.FunctionComponent<Props> = (props: Props) => { - return ( - <img {...props} /> - ); + return <img {...props} />; }; -export const Image = styled(ImageClass)<Props>` +export const Image = + styled(ImageClass) < + Props > + ` margin: ${props => props.isCentered && `0 auto`}; `; diff --git a/packages/website/ts/@next/components/layout.tsx b/packages/website/ts/@next/components/layout.tsx index 358120adc..770ee159c 100644 --- a/packages/website/ts/@next/components/layout.tsx +++ b/packages/website/ts/@next/components/layout.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import {getCSSPadding, PADDING_SIZES, PaddingInterface} from 'ts/@next/constants/utilities'; +import { getCSSPadding, PADDING_SIZES, PaddingInterface } from 'ts/@next/constants/utilities'; interface WrapWidths { default: string; @@ -51,8 +51,8 @@ export interface WrapStickyInterface { const _getColumnWidth = (args: GetColWidthArgs): string => { const { span = 1, columns } = args; - const percentWidth = (span / columns) * 100; - const gutterDiff = (GUTTER * (columns - 1)) / columns; + const percentWidth = span / columns * 100; + const gutterDiff = GUTTER * (columns - 1) / columns; return `calc(${percentWidth}% - ${gutterDiff}px)`; }; @@ -87,8 +87,11 @@ export const Main = styled.main` // passing a asElement (same patter nas Heading) so we dont have to // make a const on every route to withComponent-size it. // just <Section asElement?="div/section/footer/header/whatever" /> ? -export const Section = styled.section<SectionProps>` - width: ${props => props.isFullWidth ? `calc(100% + ${GUTTER * 2}px)` : '100%'}; +export const Section = + styled.section < + SectionProps > + ` + width: ${props => (props.isFullWidth ? `calc(100% + ${GUTTER * 2}px)` : '100%')}; padding: ${props => !props.isNoPadding && (props.isPadLarge ? `${PADDING_SIZES.large}` : PADDING_SIZES.default)}; background-color: ${props => props.bgColor}; position: ${props => props.isRelative && 'relative'}; @@ -102,11 +105,15 @@ export const Section = styled.section<SectionProps>` @media (max-width: ${BREAKPOINTS.mobile}) { margin-bottom: ${props => !props.isNoMargin && `${GUTTER / 2}px`}; - padding: ${props => props.isPadLarge ? `${PADDING_SIZES.large} ${PADDING_SIZES.default}` : PADDING_SIZES.default}; + padding: ${props => + props.isPadLarge ? `${PADDING_SIZES.large} ${PADDING_SIZES.default}` : PADDING_SIZES.default}; } `; -const WrapBase = styled.div<WrapProps>` +const WrapBase = + styled.div < + WrapProps > + ` max-width: ${props => WRAPPER_WIDTHS[props.width || 'default']}; padding: ${props => props.padding && getCSSPadding(props.padding)}; background-color: ${props => props.bgColor}; @@ -130,7 +137,10 @@ export const WrapCentered = styled(WrapBase)` text-align: center; `; -export const WrapSticky = styled.div<WrapStickyInterface>` +export const WrapSticky = + styled.div < + WrapStickyInterface > + ` position: sticky; top: ${props => props.offsetTop || '60px'}; `; @@ -138,16 +148,21 @@ export const WrapSticky = styled.div<WrapStickyInterface>` export const WrapGrid = styled(WrapBase)` display: flex; flex-wrap: ${props => props.isWrapped && `wrap`}; - justify-content: ${props => props.isCentered ? `center` : 'space-between'}; + justify-content: ${props => (props.isCentered ? `center` : 'space-between')}; `; -export const Column = styled.div<ColumnProps>` +export const Column = + styled.div < + ColumnProps > + ` background-color: ${props => props.bgColor}; flex-grow: ${props => props.isFlexGrow && 1}; @media (min-width: ${BREAKPOINTS.mobile}) { - padding: ${props => !props.isNoPadding && (props.isPadLarge ? `${PADDING_SIZES.large} ${PADDING_SIZES.default}` : PADDING_SIZES.default)}; - width: ${props => props.colWidth ? COLUMN_WIDTHS[props.colWidth] : '100%'}; + padding: ${props => + !props.isNoPadding && + (props.isPadLarge ? `${PADDING_SIZES.large} ${PADDING_SIZES.default}` : PADDING_SIZES.default)}; + width: ${props => (props.colWidth ? COLUMN_WIDTHS[props.colWidth] : '100%')}; } @media (max-width: ${BREAKPOINTS.mobile}) { diff --git a/packages/website/ts/@next/components/logo.tsx b/packages/website/ts/@next/components/logo.tsx index 2423f07b5..227d48ee0 100644 --- a/packages/website/ts/@next/components/logo.tsx +++ b/packages/website/ts/@next/components/logo.tsx @@ -23,7 +23,10 @@ const StyledLogo = styled.div` } `; -const Icon = styled(LogoIcon)<LogoInterface>` +const Icon = + styled(LogoIcon) < + LogoInterface > + ` flex-shrink: 0; path { diff --git a/packages/website/ts/@next/components/modals/input.tsx b/packages/website/ts/@next/components/modals/input.tsx index d4d9206a2..d4d53402a 100644 --- a/packages/website/ts/@next/components/modals/input.tsx +++ b/packages/website/ts/@next/components/modals/input.tsx @@ -15,10 +15,6 @@ interface InputProps { isErrors?: boolean; } -interface LabelProps { - string: boolean; -} - interface ErrorProps { [key: string]: string; } @@ -47,7 +43,7 @@ Input.defaultProps = { const StyledInput = styled.input` appearance: none; background-color: #fff; - border: 1px solid #D5D5D5; + border: 1px solid #d5d5d5; color: #000; font-size: 1.294117647rem; padding: 16px 15px 14px; @@ -59,11 +55,14 @@ const StyledInput = styled.input` border-color: ${(props: InputProps) => props.isErrors && `#FD0000`}; &::placeholder { - color: #C3C3C3; + color: #c3c3c3; } `; -const InputWrapper = styled.div<InputProps>` +const InputWrapper = + styled.div < + InputProps > + ` position: relative; flex-grow: ${props => props.width === InputWidth.Full && 1}; width: ${props => props.width === InputWidth.Half && `calc(50% - 15px)`}; @@ -83,8 +82,8 @@ const Label = styled.label` `; const Error = styled.span` - color: #FD0000; - font-size: .833333333rem; + color: #fd0000; + font-size: 0.833333333rem; line-height: 1em; display: inline-block; position: absolute; diff --git a/packages/website/ts/@next/components/modals/modal_contact.tsx b/packages/website/ts/@next/components/modals/modal_contact.tsx index 69250fad1..b97baf5e7 100644 --- a/packages/website/ts/@next/components/modals/modal_contact.tsx +++ b/packages/website/ts/@next/components/modals/modal_contact.tsx @@ -161,6 +161,8 @@ export class ModalContact extends React.Component<Props> { this.setState({ ...this.state, errors: [], isSubmitting: true }); try { + // Disabling no-unbound method b/c no reason for _.isEmpty to be bound + // tslint:disable:no-unbound-method const response = await fetch('https://website-api.0xproject.com/leads', { method: 'post', mode: 'cors', @@ -185,6 +187,7 @@ export class ModalContact extends React.Component<Props> { } } private _parseErrors(errors: ErrorResponseProps[]): ErrorProps { + const initialValue: {} = {}; return _.reduce( errors, (hash: ErrorProps, error: ErrorResponseProps) => { @@ -194,7 +197,7 @@ export class ModalContact extends React.Component<Props> { return hash; }, - {}, + initialValue, ); } } diff --git a/packages/website/ts/@next/components/newLayout.tsx b/packages/website/ts/@next/components/newLayout.tsx index edb236576..28e7653c5 100644 --- a/packages/website/ts/@next/components/newLayout.tsx +++ b/packages/website/ts/@next/components/newLayout.tsx @@ -49,14 +49,15 @@ export interface ColumnProps { export const Section: React.FunctionComponent<SectionProps> = (props: SectionProps) => { return ( <SectionBase {...props}> - <Wrap {...props}> - {props.children} - </Wrap> + <Wrap {...props}>{props.children}</Wrap> </SectionBase> ); }; -export const Column = styled.div<ColumnProps>` +export const Column = + styled.div < + ColumnProps > + ` width: ${props => props.width}; max-width: ${props => props.maxWidth}; padding: ${props => props.padding}; @@ -70,7 +71,10 @@ export const Column = styled.div<ColumnProps>` } `; -export const FlexWrap = styled.div<FlexProps>` +export const FlexWrap = + styled.div < + FlexProps > + ` max-width: 1500px; margin: 0 auto; padding: ${props => props.padding}; @@ -81,12 +85,18 @@ export const FlexWrap = styled.div<FlexProps>` } `; -export const WrapSticky = styled.div<WrapProps>` +export const WrapSticky = + styled.div < + WrapProps > + ` position: sticky; top: ${props => props.offsetTop || '60px'}; `; -const SectionBase = styled.section<SectionProps>` +const SectionBase = + styled.section < + SectionProps > + ` width: ${props => !props.isFullWidth && 'calc(100% - 60px)'}; max-width: 1500px; margin: 0 auto; @@ -100,7 +110,10 @@ const SectionBase = styled.section<SectionProps>` } `; -const Wrap = styled(FlexWrap)<WrapProps>` +const Wrap = + styled(FlexWrap) < + WrapProps > + ` width: ${props => props.wrapWidth || 'calc(100% - 60px)'}; width: ${props => props.bgColor && 'calc(100% - 60px)'}; max-width: ${props => !props.isFullWidth && (props.maxWidth || '895px')}; @@ -108,10 +121,13 @@ const Wrap = styled(FlexWrap)<WrapProps>` margin: 0 auto; `; -export const WrapGrid = styled(Wrap)<WrapProps>` +export const WrapGrid = + styled(Wrap) < + WrapProps > + ` display: flex; flex-wrap: ${props => props.isWrapped && `wrap`}; - justify-content: ${props => props.isCentered ? `center` : 'space-between'}; + justify-content: ${props => (props.isCentered ? `center` : 'space-between')}; @media (max-width: 768px) { width: 100%; diff --git a/packages/website/ts/@next/components/newsletter_form.tsx b/packages/website/ts/@next/components/newsletter_form.tsx index eef496982..ce6b04993 100644 --- a/packages/website/ts/@next/components/newsletter_form.tsx +++ b/packages/website/ts/@next/components/newsletter_form.tsx @@ -91,7 +91,7 @@ class Form extends React.Component<FormProps> { } try { - const response = await fetch('https://website-api.0x.org/newsletter_subscriber/substack', { + await fetch('https://website-api.0x.org/newsletter_subscriber/substack', { method: 'post', mode: 'cors', headers: { diff --git a/packages/website/ts/@next/components/sections/landing/about.tsx b/packages/website/ts/@next/components/sections/landing/about.tsx index 87a0fe562..7b51b0d13 100644 --- a/packages/website/ts/@next/components/sections/landing/about.tsx +++ b/packages/website/ts/@next/components/sections/landing/about.tsx @@ -57,11 +57,11 @@ const Figure = (props: FigureProps) => ( ); const DeveloperLink = styled(Button)` - @media (max-width: 500px) { - && { - white-space: pre-wrap; - line-height: 1.3; - } + @media (max-width: 500px) { + && { + white-space: pre-wrap; + line-height: 1.3; + } } `; diff --git a/packages/website/ts/@next/components/sections/landing/clients.tsx b/packages/website/ts/@next/components/sections/landing/clients.tsx index e411feeb0..a7a526818 100644 --- a/packages/website/ts/@next/components/sections/landing/clients.tsx +++ b/packages/website/ts/@next/components/sections/landing/clients.tsx @@ -1,9 +1,9 @@ import * as _ from 'lodash'; import * as React from 'react'; import styled from 'styled-components'; -import {Heading} from 'ts/@next/components/text'; +import { Heading } from 'ts/@next/components/text'; -import {Section, WrapGrid} from 'ts/@next/components/newLayout'; +import { Section, WrapGrid } from 'ts/@next/components/newLayout'; interface ProjectLogo { name: string; @@ -58,16 +58,11 @@ const projects: ProjectLogo[] = [ export const SectionLandingClients = () => ( <Section isTextCentered={true}> - <Heading size="small"> - Join the growing number of projects developing on 0x - </Heading> + <Heading size="small">Join the growing number of projects developing on 0x</Heading> <WrapGrid isWrapped={true}> {_.map(projects, (item: ProjectLogo, index) => ( - <StyledProject - key={`client-${index}`} - isOnMobile={item.persistOnMobile} - > + <StyledProject key={`client-${index}`} isOnMobile={item.persistOnMobile}> <img src={item.imageUrl} alt={item.name} /> </StyledProject> ))} @@ -75,7 +70,10 @@ export const SectionLandingClients = () => ( </Section> ); -const StyledProject = styled.div<StyledProjectInterface>` +const StyledProject = + styled.div < + StyledProjectInterface > + ` flex-shrink: 0; img { diff --git a/packages/website/ts/@next/components/sections/landing/hero.tsx b/packages/website/ts/@next/components/sections/landing/hero.tsx index 85290d1c6..cf67ad66d 100644 --- a/packages/website/ts/@next/components/sections/landing/hero.tsx +++ b/packages/website/ts/@next/components/sections/landing/hero.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import {Button} from 'ts/@next/components/button'; -import {Hero} from 'ts/@next/components/hero'; -import {LandingAnimation} from 'ts/@next/components/heroImage'; +import { Button } from 'ts/@next/components/button'; +import { Hero } from 'ts/@next/components/hero'; +import { LandingAnimation } from 'ts/@next/components/heroImage'; -import {HeroAnimation} from 'ts/@next/components/heroAnimation'; +import { HeroAnimation } from 'ts/@next/components/heroAnimation'; import { WebsitePaths } from 'ts/types'; export const SectionLandingHero = () => ( diff --git a/packages/website/ts/@next/components/separator.tsx b/packages/website/ts/@next/components/separator.tsx index ccc79aedf..0b8b8d766 100644 --- a/packages/website/ts/@next/components/separator.tsx +++ b/packages/website/ts/@next/components/separator.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; export const Separator = styled.hr` - background: #EAEAEA; + background: #eaeaea; height: 1px; border: 0; `; diff --git a/packages/website/ts/@next/components/siteWrap.tsx b/packages/website/ts/@next/components/siteWrap.tsx index db91fe08a..75cb9a268 100644 --- a/packages/website/ts/@next/components/siteWrap.tsx +++ b/packages/website/ts/@next/components/siteWrap.tsx @@ -115,13 +115,10 @@ export class SiteWrap extends React.Component<Props, State> { this.setState({ isMobileNavOpen: !this.state.isMobileNavOpen, }); - } + }; public render(): React.ReactNode { - const { - children, - theme = 'dark', - } = this.props; + const { children, theme = 'dark' } = this.props; const { isMobileNavOpen } = this.state; const currentTheme = GLOBAL_THEMES[theme]; @@ -131,16 +128,11 @@ export class SiteWrap extends React.Component<Props, State> { <> <GlobalStyles /> - <Header - isNavToggled={isMobileNavOpen} - toggleMobileNav={this.toggleMobileNav} - /> + <Header isNavToggled={isMobileNavOpen} toggleMobileNav={this.toggleMobileNav} /> - <Main isNavToggled={isMobileNavOpen}> - {children} - </Main> + <Main isNavToggled={isMobileNavOpen}>{children}</Main> - <Footer/> + <Footer /> </> </ThemeProvider> </> @@ -148,7 +140,10 @@ export class SiteWrap extends React.Component<Props, State> { } } -const Main = styled.main<MainProps>` +const Main = + styled.main < + MainProps > + ` transition: transform 0.5s, opacity 0.5s; opacity: ${props => props.isNavToggled && '0.5'}; `; diff --git a/packages/website/ts/@next/components/slider/slider.tsx b/packages/website/ts/@next/components/slider/slider.tsx index 10bbbf609..33a352b9f 100644 --- a/packages/website/ts/@next/components/slider/slider.tsx +++ b/packages/website/ts/@next/components/slider/slider.tsx @@ -7,8 +7,7 @@ import { colors } from 'ts/style/colors'; import { Icon } from 'ts/@next/components/icon'; import { Heading, Paragraph } from 'ts/@next/components/text'; -interface SliderProps { -} +interface SliderProps {} interface SlideProps { icon: string; @@ -20,7 +19,8 @@ interface SlideProps { const flickityOptions = { initialIndex: 0, cellAlign: 'left', - arrowShape: 'M0 50.766L42.467 93.58l5.791-5.839-32.346-32.61H100V46.84H15.48L50.2 11.838 44.409 6 5.794 44.93l-.003-.003z', + arrowShape: + 'M0 50.766L42.467 93.58l5.791-5.839-32.346-32.61H100V46.84H15.48L50.2 11.838 44.409 6 5.794 44.93l-.003-.003z', prevNextButtons: true, }; @@ -33,7 +33,9 @@ export const Slide: React.StatelessComponent<SlideProps> = (props: SlideProps) = <Icon name={icon} size="large" /> </SlideHead> <SlideContent> - <Heading asElement="h4" size="small" marginBottom="15px">{heading}</Heading> + <Heading asElement="h4" size="small" marginBottom="15px"> + {heading} + </Heading> <Paragraph isMuted={true}>{text}</Paragraph> </SlideContent> </StyledSlide> @@ -93,7 +95,7 @@ const StyledSlider = styled.div` top: calc(50% - 37px); border: 0; padding: 0; - transition: background-color .40s ease-in-out, visibility .40s ease-in-out, opacity .40s ease-in-out; + transition: background-color 0.4s ease-in-out, visibility 0.4s ease-in-out, opacity 0.4s ease-in-out; &:disabled { opacity: 0; @@ -130,7 +132,7 @@ const StyledSlide = styled.div` height: 520px; flex: 0 0 auto; opacity: 0.3; - transition: opacity .40s ease-in-out; + transition: opacity 0.4s ease-in-out; & + & { margin-left: 30px; diff --git a/packages/website/ts/@next/components/text.tsx b/packages/website/ts/@next/components/text.tsx index a687bca38..9f6ed9e7a 100644 --- a/packages/website/ts/@next/components/text.tsx +++ b/packages/website/ts/@next/components/text.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styled from 'styled-components'; -import {getCSSPadding, PaddingInterface} from 'ts/@next/constants/utilities'; +import { getCSSPadding, PaddingInterface } from 'ts/@next/constants/utilities'; interface BaseTextInterface extends PaddingInterface { size?: 'default' | 'medium' | 'large' | 'small' | number; @@ -9,7 +9,7 @@ interface BaseTextInterface extends PaddingInterface { } interface HeadingProps extends BaseTextInterface { - asElement?: 'h1'| 'h2'| 'h3'| 'h4'; + asElement?: 'h1' | 'h2' | 'h3' | 'h4'; maxWidth?: string; fontWeight?: string; isCentered?: boolean; @@ -27,38 +27,33 @@ interface ParagraphProps extends BaseTextInterface { fontWeight?: string | number; } -const StyledHeading = styled.h1<HeadingProps>` +const StyledHeading = + styled.h1 < + HeadingProps > + ` max-width: ${props => props.maxWidth}; color: ${props => props.color || props.theme.textColor}; display: ${props => props.isFlex && `inline-flex`}; align-items: center; justify-content: ${props => props.isFlex && `space-between`}; - font-size: ${props => typeof props.size === 'string' ? `var(--${props.size || 'default'}Heading)` : `${props.size}px`}; + font-size: ${props => + typeof props.size === 'string' ? `var(--${props.size || 'default'}Heading)` : `${props.size}px`}; line-height: ${props => `var(--${props.size || 'default'}HeadingHeight)`}; text-align: ${props => props.isCentered && 'center'}; padding: ${props => props.padding && getCSSPadding(props.padding)}; margin-left: ${props => props.isCentered && 'auto'}; margin-right: ${props => props.isCentered && 'auto'}; margin-bottom: ${props => !props.isNoMargin && (props.marginBottom || '30px')}; - opacity: ${props => typeof props.isMuted === 'boolean' ? 0.75 : props.isMuted}; - font-weight: ${props => props.fontWeight ? props.fontWeight : (['h4'].includes(props.asElement) ? 400 : 300)}; + opacity: ${props => (typeof props.isMuted === 'boolean' ? 0.75 : props.isMuted)}; + font-weight: ${props => (props.fontWeight ? props.fontWeight : ['h4'].includes(props.asElement) ? 400 : 300)}; width: ${props => props.isFlex && `100%`}; `; export const Heading: React.StatelessComponent<HeadingProps> = props => { - const { - asElement = 'h1', - children, - } = props; + const { asElement = 'h1', children } = props; const Component = StyledHeading.withComponent(asElement); - return ( - <Component - {...props} - > - {children} - </Component> - ); + return <Component {...props}>{children}</Component>; }; Heading.defaultProps = { @@ -69,14 +64,17 @@ Heading.defaultProps = { // Note: this would be useful to be implemented the same way was "Heading" // and be more generic. e.g. <Text /> with a props asElement so we can use it // for literally anything = -export const Paragraph = styled.p<ParagraphProps>` +export const Paragraph = + styled.p < + ParagraphProps > + ` font-size: ${props => `var(--${props.size || 'default'}Paragraph)`}; font-weight: ${props => props.fontWeight || 300}; margin-bottom: ${props => !props.isNoMargin && (props.marginBottom || '30px')}; padding: ${props => props.padding && getCSSPadding(props.padding)}; color: ${props => props.color || props.theme.paragraphColor}; - opacity: ${props => typeof props.isMuted === 'boolean' ? 0.75 : props.isMuted}; - text-align: ${props => props.textAlign ? props.textAlign : props.isCentered && 'center'}; + opacity: ${props => (typeof props.isMuted === 'boolean' ? 0.75 : props.isMuted)}; + text-align: ${props => (props.textAlign ? props.textAlign : props.isCentered && 'center')}; line-height: 1.4; `; diff --git a/packages/website/ts/@next/constants/globalStyle.tsx b/packages/website/ts/@next/constants/globalStyle.tsx index bf168d344..b095fafb5 100644 --- a/packages/website/ts/@next/constants/globalStyle.tsx +++ b/packages/website/ts/@next/constants/globalStyle.tsx @@ -1,5 +1,5 @@ -import {createGlobalStyle, withTheme} from 'styled-components'; -import {cssReset} from 'ts/@next/constants/cssReset'; +import { createGlobalStyle, withTheme } from 'styled-components'; +import { cssReset } from 'ts/@next/constants/cssReset'; export interface GlobalStyle { theme: { @@ -10,7 +10,10 @@ export interface GlobalStyle { }; } -const GlobalStyles = withTheme(createGlobalStyle<GlobalStyle> ` +const GlobalStyles = withTheme( + createGlobalStyle < + GlobalStyle > + ` ${cssReset}; html { @@ -100,6 +103,7 @@ const GlobalStyles = withTheme(createGlobalStyle<GlobalStyle> ` img + p { padding-top: 30px; } -`); +`, +); export { GlobalStyles }; diff --git a/packages/website/ts/@next/constants/utilities.tsx b/packages/website/ts/@next/constants/utilities.tsx index 0d626c91b..ee5c5b4ce 100644 --- a/packages/website/ts/@next/constants/utilities.tsx +++ b/packages/website/ts/@next/constants/utilities.tsx @@ -8,9 +8,9 @@ interface PaddingSizes { } export const PADDING_SIZES: PaddingSizes = { - 'default': '30px', - 'large': '60px', - 'small': '15px', + default: '30px', + large: '60px', + small: '15px', }; export const getCSSPadding = (value: number | Array<string | number> = 0): string => { diff --git a/packages/website/ts/@next/pages/community.tsx b/packages/website/ts/@next/pages/community.tsx index a02e7e6fd..eb3e7210d 100644 --- a/packages/website/ts/@next/pages/community.tsx +++ b/packages/website/ts/@next/pages/community.tsx @@ -98,14 +98,12 @@ export class NextCommunity extends React.Component { Community </Heading> <Paragraph size="medium" isCentered={true} isMuted={true} marginBottom="0"> - The 0x community is a global, passionate group of crypto developers and enthusiasts. The official channels below provide a great forum for connecting and engaging with the community. + The 0x community is a global, passionate group of crypto developers and enthusiasts. The + official channels below provide a great forum for connecting and engaging with the + community. </Paragraph> <LinkWrap> - <Button - to="#" - isWithArrow={true} - isAccentColor={true} - > + <Button to="#" isWithArrow={true} isAccentColor={true}> Join the 0x community </Button> </LinkWrap> @@ -113,7 +111,13 @@ export class NextCommunity extends React.Component { </Section> <Section isFullWidth={true}> - <WrapGrid isTextCentered={true} isWrapped={true} isFullWidth={false} isCentered={false} maxWidth="1151px"> + <WrapGrid + isTextCentered={true} + isWrapped={true} + isFullWidth={false} + isCentered={false} + maxWidth="1151px" + > {_.map(communityLinks, (link: CommunityLinkProps, index: number) => ( <CommunityLink key={`cl-${index}`} @@ -126,32 +130,37 @@ export class NextCommunity extends React.Component { </WrapGrid> </Section> - <EventsWrapper bgColor={colors.backgroundLight} isFullWidth={true} isCentered={true} isTextCentered={true}> + <EventsWrapper + bgColor={colors.backgroundLight} + isFullWidth={true} + isCentered={true} + isTextCentered={true} + > <Column maxWidth="720px"> <Heading size="medium" asElement="h2" isCentered={true} maxWidth="507px" marginBottom="30px"> Upcoming Events </Heading> <Paragraph size="medium" isCentered={true} isMuted={true}> - 0x meetups happen all over the world on a monthly basis and are hosted by devoted members of the community. Want to host a meetup in your city? Reach out for help finding a venue, connecting with local 0x mentors, and promoting your events. + 0x meetups happen all over the world on a monthly basis and are hosted by devoted members of + the community. Want to host a meetup in your city? Reach out for help finding a venue, + connecting with local 0x mentors, and promoting your events. </Paragraph> <LinkWrap> - <Button - to="#" - isWithArrow={true} - isAccentColor={true} - > + <Button to="#" isWithArrow={true} isAccentColor={true}> Get in Touch </Button> - <Button - to="#" - isWithArrow={true} - isAccentColor={true} - > + <Button to="#" isWithArrow={true} isAccentColor={true}> Join Newsletter </Button> </LinkWrap> </Column> - <WrapGrid isTextCentered={true} isWrapped={true} isFullWidth={false} isCentered={false} maxWidth="1149px"> + <WrapGrid + isTextCentered={true} + isWrapped={true} + isFullWidth={false} + isCentered={false} + maxWidth="1149px" + > {_.map(events, (ev: EventProps, index: number) => ( <Event key={`event-${index}`} @@ -177,17 +186,17 @@ export class NextCommunity extends React.Component { public _onOpenContactModal = (): void => { this.setState({ isContactModalOpen: true }); - } + }; public _onDismissContactModal = (): void => { this.setState({ isContactModalOpen: false }); - } + }; } const Event: React.FunctionComponent<EventProps> = (event: EventProps) => ( <StyledEvent> <EventIcon name="logo-mark" size={30} margin={0} /> - <EventImage src={event.imageUrl} alt=""/> + <EventImage src={event.imageUrl} alt="" /> <EventContent> <Heading color={colors.white} size="small" marginBottom="0"> {event.title} @@ -195,11 +204,7 @@ const Event: React.FunctionComponent<EventProps> = (event: EventProps) => ( <Paragraph color={colors.white} isMuted={0.65}> {event.date} </Paragraph> - <Button - color={colors.white} - href={event.signupUrl} - isWithArrow={true} - > + <Button color={colors.white} href={event.signupUrl} isWithArrow={true}> Sign Up </Button> </EventContent> diff --git a/packages/website/ts/@next/pages/ecosystem.tsx b/packages/website/ts/@next/pages/ecosystem.tsx index ab73cc52f..3d3e219a2 100644 --- a/packages/website/ts/@next/pages/ecosystem.tsx +++ b/packages/website/ts/@next/pages/ecosystem.tsx @@ -69,7 +69,7 @@ export const NextEcosystem = () => ( href={constants.URL_ECOSYSTEM_APPLY} isWithArrow={true} isAccentColor={true} - useAnchorTag={true} + shouldUseAnchorTag={true} > Apply now </Button> @@ -77,7 +77,7 @@ export const NextEcosystem = () => ( href={constants.URL_ECOSYSTEM_BLOG_POST} isWithArrow={true} isAccentColor={true} - useAnchorTag={true} + shouldUseAnchorTag={true} target="_blank" > Learn More diff --git a/packages/website/ts/@next/pages/instant.tsx b/packages/website/ts/@next/pages/instant.tsx index 94633116f..8b3a417a9 100644 --- a/packages/website/ts/@next/pages/instant.tsx +++ b/packages/website/ts/@next/pages/instant.tsx @@ -3,19 +3,18 @@ import * as _ from 'lodash'; import * as React from 'react'; import styled, { keyframes } from 'styled-components'; -import { colors } from 'ts/style/colors'; - import { Banner } from 'ts/@next/components/banner'; -import { Hero } from 'ts/@next/components/hero'; - import { Button } from 'ts/@next/components/button'; import { Definition } from 'ts/@next/components/definition'; +import { Hero } from 'ts/@next/components/hero'; import { Section, SectionProps } from 'ts/@next/components/newLayout'; import { SiteWrap } from 'ts/@next/components/siteWrap'; import { Heading, Paragraph } from 'ts/@next/components/text'; import { Configurator } from 'ts/@next/pages/instant/configurator'; +import { colors } from 'ts/style/colors'; import { WebsitePaths } from 'ts/types'; import { utils } from 'ts/utils/utils'; + import { ModalContact } from '../components/modals/modal_contact'; const CONFIGURATOR_MIN_WIDTH_PX = 1050; @@ -39,7 +38,7 @@ const featuresData = [ { label: 'Get Started', onClick: getStartedClick, - useAnchorTag: true, + shouldUseAnchorTag: true, }, { label: 'Explore the Docs', diff --git a/packages/website/ts/@next/pages/instant/code_demo.tsx b/packages/website/ts/@next/pages/instant/code_demo.tsx index 04556123e..4a3022df5 100644 --- a/packages/website/ts/@next/pages/instant/code_demo.tsx +++ b/packages/website/ts/@next/pages/instant/code_demo.tsx @@ -22,7 +22,7 @@ const CustomPre = styled.pre` border: none; } code:first-of-type { - background-color: #060D0D !important; + background-color: #060d0d !important; color: #999; min-height: 100%; text-align: center; @@ -161,9 +161,7 @@ export class CodeDemo extends React.Component<CodeDemoProps, CodeDemoState> { <Container position="relative" height="100%"> <Container position="absolute" top="10px" right="10px" zIndex={zIndex.overlay - 1}> <CopyToClipboard text={this.props.children} onCopy={this._handleCopyClick}> - <StyledButton> - {copyButtonText} - </StyledButton> + <StyledButton>{copyButtonText}</StyledButton> </CopyToClipboard> </Container> <SyntaxHighlighter language="html" style={customStyle} showLineNumbers={true} PreTag={CustomPre}> diff --git a/packages/website/ts/@next/pages/instant/config_generator.tsx b/packages/website/ts/@next/pages/instant/config_generator.tsx index a1263da58..d4497ac92 100644 --- a/packages/website/ts/@next/pages/instant/config_generator.tsx +++ b/packages/website/ts/@next/pages/instant/config_generator.tsx @@ -65,7 +65,7 @@ export class ConfigGenerator extends React.Component<ConfigGeneratorProps, Confi <Container minWidth="350px"> <ConfigGeneratorSection title="Liquidity Source"> <Select - includeEmpty={false} + shouldIncludeEmpty={false} id="" value={value.orderSource} items={this._generateItems()} diff --git a/packages/website/ts/@next/pages/instant/config_generator_address_input.tsx b/packages/website/ts/@next/pages/instant/config_generator_address_input.tsx index 23cdfcf7f..9b0e9b1d1 100644 --- a/packages/website/ts/@next/pages/instant/config_generator_address_input.tsx +++ b/packages/website/ts/@next/pages/instant/config_generator_address_input.tsx @@ -43,11 +43,7 @@ export class ConfigGeneratorAddressInput extends React.Component< const hasError = !_.isEmpty(errMsg); return ( <Container height="80px"> - <Input - value={this.props.value} - onChange={this._handleChange} - placeholder="0xe99...aa8da4" - /> + <Input value={this.props.value} onChange={this._handleChange} placeholder="0xe99...aa8da4" /> <Container marginTop="5px" isHidden={!hasError} height="25px"> <Paragraph size="small" isNoMargin={true}> {errMsg} diff --git a/packages/website/ts/@next/pages/instant/fee_percentage_slider.tsx b/packages/website/ts/@next/pages/instant/fee_percentage_slider.tsx index 512ae06b4..e9f8ba83b 100644 --- a/packages/website/ts/@next/pages/instant/fee_percentage_slider.tsx +++ b/packages/website/ts/@next/pages/instant/fee_percentage_slider.tsx @@ -58,7 +58,7 @@ const StyledSlider = styled(SliderWithTooltip)` top: 7px; &:after { border: solid transparent; - content: " "; + content: ' '; height: 0; width: 0; position: absolute; diff --git a/packages/website/ts/@next/pages/instant/select.tsx b/packages/website/ts/@next/pages/instant/select.tsx index f5b5e60c8..d4146cfb0 100644 --- a/packages/website/ts/@next/pages/instant/select.tsx +++ b/packages/website/ts/@next/pages/instant/select.tsx @@ -13,21 +13,21 @@ interface SelectProps { items: SelectItemConfig[]; emptyText?: string; onChange?: (ev: React.ChangeEvent<HTMLSelectElement>) => void; - includeEmpty: boolean; + shouldIncludeEmpty: boolean; } export const Select: React.FunctionComponent<SelectProps> = ({ value, id, items, - includeEmpty, + shouldIncludeEmpty, emptyText, onChange, }) => { return ( <Container> <StyledSelect id={id} onChange={onChange}> - {includeEmpty && <option value="">{emptyText}</option>} + {shouldIncludeEmpty && <option value="">{emptyText}</option>} {items.map((item, index) => ( <option key={`${id}-item-${index}`} @@ -48,7 +48,7 @@ export const Select: React.FunctionComponent<SelectProps> = ({ Select.defaultProps = { emptyText: 'Select...', - includeEmpty: true, + shouldIncludeEmpty: true, }; const Container = styled.div` diff --git a/packages/website/ts/@next/pages/landing.tsx b/packages/website/ts/@next/pages/landing.tsx index 8696cf022..ae560e8e3 100644 --- a/packages/website/ts/@next/pages/landing.tsx +++ b/packages/website/ts/@next/pages/landing.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import {SiteWrap} from 'ts/@next/components/siteWrap'; +import { SiteWrap } from 'ts/@next/components/siteWrap'; -import {SectionLandingAbout} from 'ts/@next/components/sections/landing/about'; -import {SectionLandingClients} from 'ts/@next/components/sections/landing/clients'; -import {SectionLandingCta} from 'ts/@next/components/sections/landing/cta'; -import {SectionLandingHero} from 'ts/@next/components/sections/landing/hero'; +import { SectionLandingAbout } from 'ts/@next/components/sections/landing/about'; +import { SectionLandingClients } from 'ts/@next/components/sections/landing/clients'; +import { SectionLandingCta } from 'ts/@next/components/sections/landing/cta'; +import { SectionLandingHero } from 'ts/@next/components/sections/landing/hero'; import { ModalContact } from 'ts/@next/components/modals/modal_contact'; @@ -21,7 +21,7 @@ export class NextLanding extends React.Component<Props> { isContactModalOpen: false, }; public render(): React.ReactNode { - return ( + return ( <SiteWrap theme="dark"> <SectionLandingHero /> <SectionLandingAbout /> @@ -34,9 +34,9 @@ export class NextLanding extends React.Component<Props> { public _onOpenContactModal = (): void => { this.setState({ isContactModalOpen: true }); - } + }; public _onDismissContactModal = (): void => { this.setState({ isContactModalOpen: false }); - } + }; } diff --git a/packages/website/ts/@next/pages/market_maker.tsx b/packages/website/ts/@next/pages/market_maker.tsx index 37a25f0ac..e2d3c75c4 100644 --- a/packages/website/ts/@next/pages/market_maker.tsx +++ b/packages/website/ts/@next/pages/market_maker.tsx @@ -1,25 +1,20 @@ import * as _ from 'lodash'; import * as React from 'react'; -import { colors } from 'ts/style/colors'; - import { Banner } from 'ts/@next/components/banner'; import { Button } from 'ts/@next/components/button'; import { Definition } from 'ts/@next/components/definition'; import { Hero } from 'ts/@next/components/hero'; -import { Icon } from 'ts/@next/components/icon'; -import { SiteWrap } from 'ts/@next/components/siteWrap'; - import { ModalContact } from 'ts/@next/components/modals/modal_contact'; -import {Section} from 'ts/@next/components/newLayout'; - -import { WebsitePaths } from 'ts/types'; +import { Section } from 'ts/@next/components/newLayout'; +import { SiteWrap } from 'ts/@next/components/siteWrap'; const offersData = [ { icon: 'supportForAllEthereumStandards', title: 'Comprehensive Tutorials', - description: 'Stay on the bleeding edge of crypto by learning how to market make on decentralized exchanges. The network of 0x relayers provides market makers a first-mover advantage to capture larger spreads, arbitrage markets, and access a long-tail of new tokens not currently listed on centralized exchanges.', + description: + 'Stay on the bleeding edge of crypto by learning how to market make on decentralized exchanges. The network of 0x relayers provides market makers a first-mover advantage to capture larger spreads, arbitrage markets, and access a long-tail of new tokens not currently listed on centralized exchanges.', }, { icon: 'generateRevenueForYourBusiness-large', @@ -34,7 +29,8 @@ const offersData = [ { icon: 'getInTouch', title: 'Personalized Support', - description: 'The 0x MM Success Manager will walk you through how to read 0x order types, spin up an Ethereum node, set up your MM bot, and execute trades on the blockchain. We are more than happy to promptly answer your questions and give you complete onboarding assistance.', + description: + 'The 0x MM Success Manager will walk you through how to read 0x order types, spin up an Ethereum node, set up your MM bot, and execute trades on the blockchain. We are more than happy to promptly answer your questions and give you complete onboarding assistance.', }, ]; @@ -53,14 +49,10 @@ export class NextMarketMaker extends React.Component { isCenteredMobile={false} title="Bring liquidity to the exchanges of the future" description="Market makers (MMs) are important stakeholders in the 0x ecosystem. The Market Making Program provides a set of resources that help onboard MMs bring liquidity to the 0x network. The program includes tutorials, a robust data platform, trade compensation, and 1:1 support from our MM Success Manager." - actions={<HeroActions/>} + actions={<HeroActions />} /> - <Section - bgColor="light" - isFlex={true} - maxWidth="1170px" - > + <Section bgColor="light" isFlex={true} maxWidth="1170px"> <Definition title="Secure" titleSize="small" @@ -90,17 +82,17 @@ export class NextMarketMaker extends React.Component { </Section> <Section> - {_.map(offersData, (item, index) => ( - <Definition - key={`offers-${index}`} - icon={item.icon} - title={item.title} - description={item.description} - isInlineIcon={true} - iconSize={240} - fontSize="medium" - /> - ))} + {_.map(offersData, (item, index) => ( + <Definition + key={`offers-${index}`} + icon={item.icon} + title={item.title} + description={item.description} + isInlineIcon={true} + iconSize={240} + fontSize="medium" + /> + ))} </Section> <Banner @@ -116,11 +108,11 @@ export class NextMarketMaker extends React.Component { public _onOpenContactModal = (): void => { this.setState({ isContactModalOpen: true }); - } + }; public _onDismissContactModal = (): void => { this.setState({ isContactModalOpen: false }); - } + }; } const HeroActions = () => ( diff --git a/packages/website/ts/@next/pages/why.tsx b/packages/website/ts/@next/pages/why.tsx index 9c3c4d0a2..73195f31c 100644 --- a/packages/website/ts/@next/pages/why.tsx +++ b/packages/website/ts/@next/pages/why.tsx @@ -1,20 +1,18 @@ import * as _ from 'lodash'; import * as React from 'react'; import ScrollableAnchor, { configureAnchors } from 'react-scrollable-anchor'; - import styled from 'styled-components'; -import {Hero} from 'ts/@next/components/hero'; - import { Banner } from 'ts/@next/components/banner'; import { Button } from 'ts/@next/components/button'; -import {Definition} from 'ts/@next/components/definition'; -import {Column, Section, WrapSticky} from 'ts/@next/components/newLayout'; +import { Definition } from 'ts/@next/components/definition'; +import { Hero } from 'ts/@next/components/hero'; +import { Column, Section, WrapSticky } from 'ts/@next/components/newLayout'; import { SiteWrap } from 'ts/@next/components/siteWrap'; import { Slide, Slider } from 'ts/@next/components/slider/slider'; +import { Heading } from 'ts/@next/components/text'; import { ModalContact } from '../components/modals/modal_contact'; -import { Heading } from 'ts/@next/components/text'; const offersData = [ { @@ -48,7 +46,8 @@ const functionalityData = [ { icon: 'buildBusiness', title: 'Build a Business', - description: 'Monetize your product by taking fees on each transaction and join a growing number of relayers in the 0x ecosystem.', + description: + 'Monetize your product by taking fees on each transaction and join a growing number of relayers in the 0x ecosystem.', }, ]; @@ -56,27 +55,32 @@ const useCaseSlides = [ { icon: 'gamingAndCollectibles', title: 'Games & Collectibles', - description: 'Artists and game makers are tokenizing digital art and in-game items known as non-fungible tokens (NFTs). 0x enables these creators to add exchange functionality by providing the ability to build marketplaces for NFT trading.', + description: + 'Artists and game makers are tokenizing digital art and in-game items known as non-fungible tokens (NFTs). 0x enables these creators to add exchange functionality by providing the ability to build marketplaces for NFT trading.', }, { icon: 'predictionMarkets', title: 'Prediction Markets', - description: 'Decentralized prediction markets and cryptodervivative platforms generate sets of tokens that represent a financial stake in the outcomes of events. 0x allows these tokens to be instantly tradable in liquid markets.', + description: + 'Decentralized prediction markets and cryptodervivative platforms generate sets of tokens that represent a financial stake in the outcomes of events. 0x allows these tokens to be instantly tradable in liquid markets.', }, { icon: 'orderBooks', title: 'Order Books', - description: 'There are thousands of decentralized apps and protocols that have native utility tokens. 0x provides professional exchanges with the ability to host order books and facilitates the exchange of these assets.', + description: + 'There are thousands of decentralized apps and protocols that have native utility tokens. 0x provides professional exchanges with the ability to host order books and facilitates the exchange of these assets.', }, { icon: 'decentralisedLoans', title: 'Decentralized Loans', - description: 'Efficient lending requires liquid markets where investors can buy and re-sell loans. 0x enables an ecosystem of lenders to self-organize and efficiently determine market prices for all outstanding loans.', + description: + 'Efficient lending requires liquid markets where investors can buy and re-sell loans. 0x enables an ecosystem of lenders to self-organize and efficiently determine market prices for all outstanding loans.', }, { icon: 'stableTokens', title: 'Stable Tokens', - description: 'Novel economic constructs such as stable coins require efficient, liquid markets to succeed. 0x will facilitate the underlying economic mechanisms that allow these tokens to remain stable.', + description: + 'Novel economic constructs such as stable coins require efficient, liquid markets to succeed. 0x will facilitate the underlying economic mechanisms that allow these tokens to remain stable.', }, ]; @@ -87,27 +91,20 @@ export class NextWhy extends React.Component { isContactModalOpen: false, }; public render(): React.ReactNode { + const buildAction = ( + <Button href="/docs" isWithArrow={true} isAccentColor={true}> + Build on 0x + </Button> + ); return ( <SiteWrap theme="dark"> <Hero title="The exchange layer for the crypto economy" description="The world's assets are becoming tokenized on public blockchains. 0x Protocol is free, open-source infrastracture that developers and businesses utilize to build products that enable the purchasing and trading of crypto tokens." - actions={ - <Button - href="/docs" - isWithArrow={true} - isAccentColor={true} - > - Build on 0x - </Button> - } + actions={buildAction} /> - <Section - bgColor="dark" - isFlex={true} - maxWidth="1170px" - > + <Section bgColor="dark" isFlex={true} maxWidth="1170px"> <Definition title="Support for all Ethereum Standards" titleSize="small" @@ -136,20 +133,22 @@ export class NextWhy extends React.Component { /> </Section> - <Section maxWidth="1170px" isFlex={true} isFullWidth={true}> - <Column> - <NavStickyWrap offsetTop="130px"> - <ChapterLink href="#benefits">Benefits</ChapterLink> - <ChapterLink href="#cases">Use Cases</ChapterLink> - <ChapterLink href="#functionality">Features</ChapterLink> - </NavStickyWrap> - </Column> + <Section maxWidth="1170px" isFlex={true} isFullWidth={true}> + <Column> + <NavStickyWrap offsetTop="130px"> + <ChapterLink href="#benefits">Benefits</ChapterLink> + <ChapterLink href="#cases">Use Cases</ChapterLink> + <ChapterLink href="#functionality">Features</ChapterLink> + </NavStickyWrap> + </Column> <Column width="55%" maxWidth="826px"> <Column width="100%" maxWidth="560px" padding="0 30px 0 0"> <ScrollableAnchor id="benefits"> <SectionWrap> - <SectionTitle size="medium" marginBottom="60px" isNoBorder={true}>What 0x offers</SectionTitle> + <SectionTitle size="medium" marginBottom="60px" isNoBorder={true}> + What 0x offers + </SectionTitle> {_.map(offersData, (item, index) => ( <Definition @@ -166,7 +165,9 @@ export class NextWhy extends React.Component { <ScrollableAnchor id="cases"> <SectionWrap isNotRelative={true}> - <SectionTitle size="medium" marginBottom="60px">Use Cases</SectionTitle> + <SectionTitle size="medium" marginBottom="60px"> + Use Cases + </SectionTitle> <Slider> {_.map(useCaseSlides, (item, index) => ( <Slide @@ -182,7 +183,9 @@ export class NextWhy extends React.Component { <ScrollableAnchor id="functionality"> <SectionWrap> - <SectionTitle size="medium" marginBottom="60px">Exchange Functionality</SectionTitle> + <SectionTitle size="medium" marginBottom="60px"> + Exchange Functionality + </SectionTitle> {_.map(functionalityData, (item, index) => ( <Definition @@ -198,33 +201,36 @@ export class NextWhy extends React.Component { </ScrollableAnchor> </Column> </Column> - </Section> - - <Banner - heading="Ready to get started?" - subline="Dive into our docs, or contact us if needed" - mainCta={{ text: 'Get Started', href: '/docs' }} - secondaryCta={{ text: 'Get in Touch', onClick: this._onOpenContactModal.bind(this) }} - /> - <ModalContact isOpen={this.state.isContactModalOpen} onDismiss={this._onDismissContactModal} /> + </Section> + + <Banner + heading="Ready to get started?" + subline="Dive into our docs, or contact us if needed" + mainCta={{ text: 'Get Started', href: '/docs' }} + secondaryCta={{ text: 'Get in Touch', onClick: this._onOpenContactModal.bind(this) }} + /> + <ModalContact isOpen={this.state.isContactModalOpen} onDismiss={this._onDismissContactModal} /> </SiteWrap> ); } public _onOpenContactModal = (): void => { this.setState({ isContactModalOpen: true }); - } + }; public _onDismissContactModal = (): void => { this.setState({ isContactModalOpen: false }); - } + }; } interface SectionProps { isNotRelative?: boolean; } -const SectionWrap = styled.div<SectionProps>` +const SectionWrap = + styled.div < + SectionProps > + ` position: ${props => !props.isNotRelative && 'relative'}; & + & { @@ -247,10 +253,18 @@ const SectionWrap = styled.div<SectionProps>` } `; -const SectionTitle = styled(Heading)<{ isNoBorder?: boolean }>` +interface SectionTitleProps { + isNoBorder?: boolean; +} +const SectionTitle = + styled(Heading) < + SectionTitleProps > + ` position: relative; - ${props => !props.isNoBorder && ` + ${props => + !props.isNoBorder && + ` &:before { content: ''; width: 100vw; diff --git a/packages/website/ts/components/ui/ease_up_from_bottom_animation.tsx b/packages/website/ts/components/ui/ease_up_from_bottom_animation.tsx index 176c9410c..ba141c01e 100644 --- a/packages/website/ts/components/ui/ease_up_from_bottom_animation.tsx +++ b/packages/website/ts/components/ui/ease_up_from_bottom_animation.tsx @@ -1,4 +1,3 @@ -import * as React from 'react'; import { css, keyframes, styled } from 'ts/style/theme'; const appearFromBottomFrames = keyframes` diff --git a/packages/website/ts/globals.d.ts b/packages/website/ts/globals.d.ts index 9addab335..b953a319f 100644 --- a/packages/website/ts/globals.d.ts +++ b/packages/website/ts/globals.d.ts @@ -8,7 +8,7 @@ declare module 'react-flickity-component'; declare module 'react-anchor-link-smooth-scroll'; declare module 'react-responsive'; declare module 'react-scrollable-anchor'; -declare module 'react-headroom' +declare module 'react-headroom'; declare module '*.json' { const json: any; @@ -18,9 +18,7 @@ declare module '*.json' { } declare module '*.svg' { - //const svg: any; - //export default svg; - import {PureComponent, SVGProps} from "react"; + import { PureComponent, SVGProps } from 'react'; export default class extends PureComponent<SVGProps<SVGSVGElement>> {} } diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index abd8bc2ef..3f0c1c28c 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -4,13 +4,8 @@ import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom'; import { MetaTags } from 'ts/components/meta_tags'; -import { About } from 'ts/containers/about'; import { DocsHome } from 'ts/containers/docs_home'; import { FAQ } from 'ts/containers/faq'; -import { Instant } from 'ts/containers/instant'; -import { Jobs } from 'ts/containers/jobs'; -import { Landing } from 'ts/containers/landing'; // Note(ez): When we're done we omit all old site imports -import { LaunchKit } from 'ts/containers/launch_kit'; import { NotFound } from 'ts/containers/not_found'; import { Wiki } from 'ts/containers/wiki'; import { createLazyComponent } from 'ts/lazy_component'; @@ -25,12 +20,10 @@ import { NextAboutJobs } from 'ts/@next/pages/about/jobs'; import { NextAboutMission } from 'ts/@next/pages/about/mission'; import { NextAboutPress } from 'ts/@next/pages/about/press'; import { NextAboutTeam } from 'ts/@next/pages/about/team'; -import { NextCommunity } from 'ts/@next/pages/community'; import { NextEcosystem } from 'ts/@next/pages/ecosystem'; import { Next0xInstant } from 'ts/@next/pages/instant'; import { NextLanding } from 'ts/@next/pages/landing'; import { NextLaunchKit } from 'ts/@next/pages/launch_kit'; -import { NextMarketMaker } from 'ts/@next/pages/market_maker'; import { NextWhy } from 'ts/@next/pages/why'; // Check if we've introduced an update that requires us to clear the tradeHistory local storage entries @@ -104,15 +97,19 @@ render( <div> <Switch> {/* Next (new site) routes */} - <Route exact path="/" component={NextLanding as any} /> - <Route exact path={WebsitePaths.Why} component={NextWhy as any} /> - <Route exact path={WebsitePaths.Instant} component={Next0xInstant as any} /> - <Route exact path={WebsitePaths.LaunchKit} component={NextLaunchKit as any} /> - <Route exact path={WebsitePaths.Ecosystem} component={NextEcosystem as any} /> - <Route exact path={WebsitePaths.AboutMission} component={NextAboutMission as any} /> - <Route exact path={WebsitePaths.AboutTeam} component={NextAboutTeam as any} /> - <Route exact path={WebsitePaths.AboutPress} component={NextAboutPress as any} /> - <Route exact path={WebsitePaths.AboutJobs} component={NextAboutJobs as any} /> + <Route exact={true} path="/" component={NextLanding as any} /> + <Route exact={true} path={WebsitePaths.Why} component={NextWhy as any} /> + <Route exact={true} path={WebsitePaths.Instant} component={Next0xInstant as any} /> + <Route exact={true} path={WebsitePaths.LaunchKit} component={NextLaunchKit as any} /> + <Route exact={true} path={WebsitePaths.Ecosystem} component={NextEcosystem as any} /> + <Route + exact={true} + path={WebsitePaths.AboutMission} + component={NextAboutMission as any} + /> + <Route exact={true} path={WebsitePaths.AboutTeam} component={NextAboutTeam as any} /> + <Route exact={true} path={WebsitePaths.AboutPress} component={NextAboutPress as any} /> + <Route exact={true} path={WebsitePaths.AboutJobs} component={NextAboutJobs as any} /> {/* Note(ez): We remove/replace all old routes with next routes once we're ready to put a ring on it. for now let's keep em there for reference diff --git a/packages/website/ts/pages/documentation/docs_home.tsx b/packages/website/ts/pages/documentation/docs_home.tsx index c52d7bd8b..fd3932bfa 100644 --- a/packages/website/ts/pages/documentation/docs_home.tsx +++ b/packages/website/ts/pages/documentation/docs_home.tsx @@ -100,6 +100,14 @@ const CATEGORY_TO_PACKAGES: ObjectMap<Package[]> = { }, }, { + description: 'A Python Standard Relayer API client', + link: { + title: '0x-sra-client.py', + to: 'https://pypi.org/project/0x-sra-client/', + shouldOpenInNewTab: true, + }, + }, + { description: 'An http & websocket client for interacting with relayers that have implemented the [Standard Relayer API](https://github.com/0xProject/standard-relayer-api)', link: { diff --git a/packages/website/tsconfig.json b/packages/website/tsconfig.json index 67573971f..96bba7bbf 100644 --- a/packages/website/tsconfig.json +++ b/packages/website/tsconfig.json @@ -23,10 +23,7 @@ "awesomeTypescriptLoaderOptions": { "useCache": true, "errorsAsWarnings": true, - "reportFiles": [ - "./ts/**/*" - ] - + "reportFiles": ["./ts/**/*"] }, "include": ["./ts/**/*"] } @@ -1299,6 +1299,14 @@ version "0.22.9" resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.9.tgz#b5990152604c2ada749b7f88cab3476f21f39d7b" +"@types/chokidar@^1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@types/chokidar/-/chokidar-1.7.5.tgz#1fa78c8803e035bed6d98e6949e514b133b0c9b6" + integrity sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ== + dependencies: + "@types/events" "*" + "@types/node" "*" + "@types/compare-versions@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/compare-versions/-/compare-versions-3.0.0.tgz#4a45dffe0ebbc00d0f2daef8a0e96ffc66cf5955" @@ -1550,6 +1558,11 @@ version "2.0.0" resolved "https://registry.yarnpkg.com/@types/p-limit/-/p-limit-2.0.0.tgz#c076b7daa9163108a35899ea6a9d927526943ea2" +"@types/pluralize@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.29.tgz#6ffa33ed1fc8813c469b859681d09707eb40d03c" + integrity sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA== + "@types/prop-types@*": version "15.5.5" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.5.tgz#17038dd322c2325f5da650a94d5f9974943625e3" @@ -4149,6 +4162,26 @@ chokidar@^2.0.0, chokidar@^2.0.2: optionalDependencies: fsevents "^1.1.2" +chokidar@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" + integrity sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + lodash.debounce "^4.0.8" + normalize-path "^2.1.1" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + upath "^1.0.5" + optionalDependencies: + fsevents "^1.2.2" + chownr@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" @@ -7237,7 +7270,7 @@ fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -fsevents@^1.0.0, fsevents@^1.2.3: +fsevents@^1.0.0, fsevents@^1.2.2, fsevents@^1.2.3: version "1.2.4" resolved "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" dependencies: @@ -10197,6 +10230,11 @@ lodash.camelcase@^3.0.1: dependencies: lodash._createcompounder "^3.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + lodash.deburr@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-3.2.0.tgz#6da8f54334a366a7cf4c4c76ef8d80aa1b365ed5" @@ -12326,6 +12364,7 @@ pkginfo@0.x.x: pluralize@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== pn@^1.1.0: version "1.1.0" @@ -16534,6 +16573,11 @@ upath@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d" +upath@^1.0.5: + version "1.1.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" + integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw== + update-notifier@^2.3.0, update-notifier@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" |