diff --git a/.gitignore b/.gitignore index 9e43f20b04..0e483dc0ad 100644 --- a/.gitignore +++ b/.gitignore @@ -79,13 +79,13 @@ packages/react-docs/example/public/bundle* packages/testnet-faucets/server/ # generated contract artifacts/ -packages/contracts/generated-artifacts/ +contracts/core/generated-artifacts/ packages/sol-cov/test/fixtures/artifacts/ packages/metacoin/artifacts/ # generated contract wrappers packages/abi-gen-wrappers/wrappers -packages/contracts/generated-wrappers/ +contracts/core/generated-wrappers/ packages/metacoin/src/contract_wrappers # solc-bin in sol-compiler diff --git a/.prettierignore b/.prettierignore index db389bdb91..9dbb83e27b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,7 @@ lib .nyc_output -/packages/contracts/generated-wrappers -/packages/contracts/generated-artifacts +/contracts/core/generated-wrappers +/contracts/core/generated-artifacts /packages/abi-gen-wrappers/src/generated-wrappers /packages/contract-artifacts/artifacts /python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts diff --git a/CODEOWNERS b/CODEOWNERS index 3cf75fb2df..346e42ac03 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,4 +32,4 @@ packages/web3-wrapper/ @LogvinovLeon @fabioberger python-packages/ @feuGeneA # Protocol/smart contracts -packages/contracts/test/ @albrow +contracts/core/test/ @albrow diff --git a/README.md b/README.md index 4212993719..5e99b47883 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Visit our [developer portal](https://0xproject.com/docs/order-utils) for a compr | Package | Description | | -------------------------------------------------- | ---------------------------------------------------------------- | -| [`@0x/contracts`](/packages/contracts) | 0x protocol solidity smart contracts & tests | +| [`@0x/contracts`](/contracts/core) | 0x protocol solidity smart contracts & tests | | [`@0x/testnet-faucets`](/packages/testnet-faucets) | A faucet micro-service that dispenses test ERC20 tokens or Ether | | [`@0x/website`](/packages/website) | 0x website | diff --git a/packages/contracts/.solhint.json b/contracts/core/.solhint.json similarity index 100% rename from packages/contracts/.solhint.json rename to contracts/core/.solhint.json diff --git a/packages/contracts/.solhintignore b/contracts/core/.solhintignore similarity index 100% rename from packages/contracts/.solhintignore rename to contracts/core/.solhintignore diff --git a/packages/contracts/CHANGELOG.json b/contracts/core/CHANGELOG.json similarity index 94% rename from packages/contracts/CHANGELOG.json rename to contracts/core/CHANGELOG.json index 00f94c83b3..7dfa069902 100644 --- a/packages/contracts/CHANGELOG.json +++ b/contracts/core/CHANGELOG.json @@ -1,4 +1,14 @@ [ + { + "name": "MultiAssetProxy", + "version": "1.0.0", + "changes": [ + { + "note": "Add MultiAssetProxy implementation", + "pr": 1224 + } + ] + }, { "name": "OrderValidator", "version": "1.0.1", diff --git a/packages/contracts/README.md b/contracts/core/README.md similarity index 100% rename from packages/contracts/README.md rename to contracts/core/README.md diff --git a/packages/contracts/compiler.json b/contracts/core/compiler.json similarity index 98% rename from packages/contracts/compiler.json rename to contracts/core/compiler.json index af3980b4ea..c824e4645a 100644 --- a/packages/contracts/compiler.json +++ b/contracts/core/compiler.json @@ -38,6 +38,7 @@ "IValidator", "IWallet", "MixinAuthorizable", + "MultiAssetProxy", "MultiSigWallet", "MultiSigWalletWithTimeLock", "OrderValidator", diff --git a/packages/contracts/contracts/examples/ExchangeWrapper/ExchangeWrapper.sol b/contracts/core/contracts/examples/ExchangeWrapper/ExchangeWrapper.sol similarity index 100% rename from packages/contracts/contracts/examples/ExchangeWrapper/ExchangeWrapper.sol rename to contracts/core/contracts/examples/ExchangeWrapper/ExchangeWrapper.sol diff --git a/packages/contracts/contracts/examples/Validator/Validator.sol b/contracts/core/contracts/examples/Validator/Validator.sol similarity index 100% rename from packages/contracts/contracts/examples/Validator/Validator.sol rename to contracts/core/contracts/examples/Validator/Validator.sol diff --git a/packages/contracts/contracts/examples/Wallet/Wallet.sol b/contracts/core/contracts/examples/Wallet/Wallet.sol similarity index 100% rename from packages/contracts/contracts/examples/Wallet/Wallet.sol rename to contracts/core/contracts/examples/Wallet/Wallet.sol diff --git a/packages/contracts/contracts/examples/Whitelist/Whitelist.sol b/contracts/core/contracts/examples/Whitelist/Whitelist.sol similarity index 100% rename from packages/contracts/contracts/examples/Whitelist/Whitelist.sol rename to contracts/core/contracts/examples/Whitelist/Whitelist.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/Forwarder.sol b/contracts/core/contracts/extensions/Forwarder/Forwarder.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/Forwarder.sol rename to contracts/core/contracts/extensions/Forwarder/Forwarder.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/MixinAssets.sol b/contracts/core/contracts/extensions/Forwarder/MixinAssets.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/MixinAssets.sol rename to contracts/core/contracts/extensions/Forwarder/MixinAssets.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol b/contracts/core/contracts/extensions/Forwarder/MixinExchangeWrapper.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol rename to contracts/core/contracts/extensions/Forwarder/MixinExchangeWrapper.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/MixinForwarderCore.sol b/contracts/core/contracts/extensions/Forwarder/MixinForwarderCore.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/MixinForwarderCore.sol rename to contracts/core/contracts/extensions/Forwarder/MixinForwarderCore.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/MixinWeth.sol b/contracts/core/contracts/extensions/Forwarder/MixinWeth.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/MixinWeth.sol rename to contracts/core/contracts/extensions/Forwarder/MixinWeth.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/interfaces/IAssets.sol b/contracts/core/contracts/extensions/Forwarder/interfaces/IAssets.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/interfaces/IAssets.sol rename to contracts/core/contracts/extensions/Forwarder/interfaces/IAssets.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/interfaces/IForwarder.sol b/contracts/core/contracts/extensions/Forwarder/interfaces/IForwarder.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/interfaces/IForwarder.sol rename to contracts/core/contracts/extensions/Forwarder/interfaces/IForwarder.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/interfaces/IForwarderCore.sol b/contracts/core/contracts/extensions/Forwarder/interfaces/IForwarderCore.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/interfaces/IForwarderCore.sol rename to contracts/core/contracts/extensions/Forwarder/interfaces/IForwarderCore.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/libs/LibConstants.sol b/contracts/core/contracts/extensions/Forwarder/libs/LibConstants.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/libs/LibConstants.sol rename to contracts/core/contracts/extensions/Forwarder/libs/LibConstants.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/libs/LibForwarderErrors.sol b/contracts/core/contracts/extensions/Forwarder/libs/LibForwarderErrors.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/libs/LibForwarderErrors.sol rename to contracts/core/contracts/extensions/Forwarder/libs/LibForwarderErrors.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/mixins/MAssets.sol b/contracts/core/contracts/extensions/Forwarder/mixins/MAssets.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/mixins/MAssets.sol rename to contracts/core/contracts/extensions/Forwarder/mixins/MAssets.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/mixins/MExchangeWrapper.sol b/contracts/core/contracts/extensions/Forwarder/mixins/MExchangeWrapper.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/mixins/MExchangeWrapper.sol rename to contracts/core/contracts/extensions/Forwarder/mixins/MExchangeWrapper.sol diff --git a/packages/contracts/contracts/extensions/Forwarder/mixins/MWeth.sol b/contracts/core/contracts/extensions/Forwarder/mixins/MWeth.sol similarity index 100% rename from packages/contracts/contracts/extensions/Forwarder/mixins/MWeth.sol rename to contracts/core/contracts/extensions/Forwarder/mixins/MWeth.sol diff --git a/packages/contracts/contracts/extensions/OrderValidator/OrderValidator.sol b/contracts/core/contracts/extensions/OrderValidator/OrderValidator.sol similarity index 100% rename from packages/contracts/contracts/extensions/OrderValidator/OrderValidator.sol rename to contracts/core/contracts/extensions/OrderValidator/OrderValidator.sol diff --git a/packages/contracts/contracts/multisig/MultiSigWallet.sol b/contracts/core/contracts/multisig/MultiSigWallet.sol similarity index 100% rename from packages/contracts/contracts/multisig/MultiSigWallet.sol rename to contracts/core/contracts/multisig/MultiSigWallet.sol diff --git a/packages/contracts/contracts/multisig/MultiSigWalletWithTimeLock.sol b/contracts/core/contracts/multisig/MultiSigWalletWithTimeLock.sol similarity index 100% rename from packages/contracts/contracts/multisig/MultiSigWalletWithTimeLock.sol rename to contracts/core/contracts/multisig/MultiSigWalletWithTimeLock.sol diff --git a/packages/contracts/contracts/protocol/AssetProxy/ERC20Proxy.sol b/contracts/core/contracts/protocol/AssetProxy/ERC20Proxy.sol similarity index 100% rename from packages/contracts/contracts/protocol/AssetProxy/ERC20Proxy.sol rename to contracts/core/contracts/protocol/AssetProxy/ERC20Proxy.sol diff --git a/packages/contracts/contracts/protocol/AssetProxy/ERC721Proxy.sol b/contracts/core/contracts/protocol/AssetProxy/ERC721Proxy.sol similarity index 100% rename from packages/contracts/contracts/protocol/AssetProxy/ERC721Proxy.sol rename to contracts/core/contracts/protocol/AssetProxy/ERC721Proxy.sol diff --git a/packages/contracts/contracts/protocol/AssetProxy/MixinAuthorizable.sol b/contracts/core/contracts/protocol/AssetProxy/MixinAuthorizable.sol similarity index 100% rename from packages/contracts/contracts/protocol/AssetProxy/MixinAuthorizable.sol rename to contracts/core/contracts/protocol/AssetProxy/MixinAuthorizable.sol diff --git a/contracts/core/contracts/protocol/AssetProxy/MultiAssetProxy.sol b/contracts/core/contracts/protocol/AssetProxy/MultiAssetProxy.sol new file mode 100644 index 0000000000..42231e73b0 --- /dev/null +++ b/contracts/core/contracts/protocol/AssetProxy/MultiAssetProxy.sol @@ -0,0 +1,300 @@ +/* + + 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 "../Exchange/MixinAssetProxyDispatcher.sol"; +import "./MixinAuthorizable.sol"; + + +contract MultiAssetProxy is + MixinAssetProxyDispatcher, + MixinAuthorizable +{ + // Id of this proxy. + bytes4 constant internal PROXY_ID = bytes4(keccak256("MultiAsset(uint256[],bytes[])")); + + // solhint-disable-next-line payable-fallback + function () + external + { + assembly { + // The first 4 bytes of calldata holds the function selector + let selector := and(calldataload(0), 0xffffffff00000000000000000000000000000000000000000000000000000000) + + // `transferFrom` will be called with the following parameters: + // assetData Encoded byte array. + // from Address to transfer asset from. + // to Address to transfer asset to. + // amount Amount of asset to transfer. + // bytes4(keccak256("transferFrom(bytes,address,address,uint256)")) = 0xa85e59e4 + if eq(selector, 0xa85e59e400000000000000000000000000000000000000000000000000000000) { + + // To lookup a value in a mapping, we load from the storage location keccak256(k, p), + // where k is the key left padded to 32 bytes and p is the storage slot + mstore(0, caller) + mstore(32, authorized_slot) + + // Revert if authorized[msg.sender] == false + if iszero(sload(keccak256(0, 64))) { + // Revert with `Error("SENDER_NOT_AUTHORIZED")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001553454e4445525f4e4f545f415554484f52495a454400000000000000) + mstore(96, 0) + revert(0, 100) + } + + // `transferFrom`. + // The function is marked `external`, so no abi decoding is done for + // us. Instead, we expect the `calldata` memory to contain the + // following: + // + // | Area | Offset | Length | Contents | + // |----------|--------|---------|-------------------------------------| + // | Header | 0 | 4 | function selector | + // | Params | | 4 * 32 | function parameters: | + // | | 4 | | 1. offset to assetData (*) | + // | | 36 | | 2. from | + // | | 68 | | 3. to | + // | | 100 | | 4. amount | + // | Data | | | assetData: | + // | | 132 | 32 | assetData Length | + // | | 164 | ** | assetData Contents | + // + // (*): offset is computed from start of function parameters, so offset + // by an additional 4 bytes in the calldata. + // + // (**): see table below to compute length of assetData Contents + // + // WARNING: The ABIv2 specification allows additional padding between + // the Params and Data section. This will result in a larger + // offset to assetData. + + // Load offset to `assetData` + let assetDataOffset := calldataload(4) + + // Asset data itself is encoded as follows: + // + // | Area | Offset | Length | Contents | + // |----------|-------------|---------|-------------------------------------| + // | Header | 0 | 4 | assetProxyId | + // | Params | | 2 * 32 | function parameters: | + // | | 4 | | 1. offset to amounts (*) | + // | | 36 | | 2. offset to nestedAssetData (*) | + // | Data | | | amounts: | + // | | 68 | 32 | amounts Length | + // | | 100 | a | amounts Contents | + // | | | | nestedAssetData: | + // | | 100 + a | 32 | nestedAssetData Length | + // | | 132 + a | b | nestedAssetData Contents (offsets) | + // | | 132 + a + b | | nestedAssetData[0, ..., len] | + + // In order to find the offset to `amounts`, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + let amountsOffset := calldataload(add(assetDataOffset, 40)) + + // In order to find the offset to `nestedAssetData`, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + // + 32 (amounts offset) + let nestedAssetDataOffset := calldataload(add(assetDataOffset, 72)) + + // In order to find the start of the `amounts` contents, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + // + amountsOffset + // + 32 (amounts len) + let amountsContentsStart := add(assetDataOffset, add(amountsOffset, 72)) + + // Load number of elements in `amounts` + let amountsLen := calldataload(sub(amountsContentsStart, 32)) + + // In order to find the start of the `nestedAssetData` contents, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + // + nestedAssetDataOffset + // + 32 (nestedAssetData len) + let nestedAssetDataContentsStart := add(assetDataOffset, add(nestedAssetDataOffset, 72)) + + // Load number of elements in `nestedAssetData` + let nestedAssetDataLen := calldataload(sub(nestedAssetDataContentsStart, 32)) + + // Revert if number of elements in `amounts` differs from number of elements in `nestedAssetData` + if iszero(eq(amountsLen, nestedAssetDataLen)) { + // Revert with `Error("LENGTH_MISMATCH")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000000f4c454e4754485f4d49534d4154434800000000000000000000000000) + mstore(96, 0) + revert(0, 100) + } + + // Copy `transferFrom` selector, offset to `assetData`, `from`, and `to` from calldata to memory + calldatacopy( + 0, // memory can safely be overwritten from beginning + 0, // start of calldata + 100 // length of selector (4) and 3 params (32 * 3) + ) + + // Overwrite existing offset to `assetData` with our own + mstore(4, 128) + + // Load `amount` + let amount := calldataload(100) + + // Calculate number of bytes in `amounts` contents + let amountsByteLen := mul(amountsLen, 32) + + // Initialize `assetProxyId` and `assetProxy` to 0 + let assetProxyId := 0 + let assetProxy := 0 + + // Loop through `amounts` and `nestedAssetData`, calling `transferFrom` for each respective element + for {let i := 0} lt(i, amountsByteLen) {i := add(i, 32)} { + + // Calculate the total amount + let amountsElement := calldataload(add(amountsContentsStart, i)) + let totalAmount := mul(amountsElement, amount) + + // Revert if multiplication resulted in an overflow + if iszero(eq(div(totalAmount, amount), amountsElement)) { + // Revert with `Error("UINT256_OVERFLOW")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001055494e543235365f4f564552464c4f57000000000000000000000000) + mstore(96, 0) + revert(0, 100) + } + + // Write `totalAmount` to memory + mstore(100, totalAmount) + + // Load offset to `nestedAssetData[i]` + let nestedAssetDataElementOffset := calldataload(add(nestedAssetDataContentsStart, i)) + + // In order to find the start of the `nestedAssetData[i]` contents, we must add: + // 4 (function selector) + // + assetDataOffset + // + 32 (assetData len) + // + 4 (assetProxyId) + // + nestedAssetDataOffset + // + 32 (nestedAssetData len) + // + nestedAssetDataElementOffset + // + 32 (nestedAssetDataElement len) + let nestedAssetDataElementContentsStart := add(assetDataOffset, add(nestedAssetDataOffset, add(nestedAssetDataElementOffset, 104))) + + // Load length of `nestedAssetData[i]` + let nestedAssetDataElementLenStart := sub(nestedAssetDataElementContentsStart, 32) + let nestedAssetDataElementLen := calldataload(nestedAssetDataElementLenStart) + + // Revert if the `nestedAssetData` does not contain a 4 byte `assetProxyId` + if lt(nestedAssetDataElementLen, 4) { + // Revert with `Error("LENGTH_GREATER_THAN_3_REQUIRED")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001e4c454e4754485f475245415445525f5448414e5f335f524551554952) + mstore(96, 0x4544000000000000000000000000000000000000000000000000000000000000) + revert(0, 100) + } + + // Load AssetProxy id + let currentAssetProxyId := and( + calldataload(nestedAssetDataElementContentsStart), + 0xffffffff00000000000000000000000000000000000000000000000000000000 + ) + + // Only load `assetProxy` if `currentAssetProxyId` does not equal `assetProxyId` + // We do not need to check if `currentAssetProxyId` is 0 since `assetProxy` is also initialized to 0 + if iszero(eq(currentAssetProxyId, assetProxyId)) { + // Update `assetProxyId` + assetProxyId := currentAssetProxyId + // To lookup a value in a mapping, we load from the storage location keccak256(k, p), + // where k is the key left padded to 32 bytes and p is the storage slot + mstore(132, assetProxyId) + mstore(164, assetProxies_slot) + assetProxy := sload(keccak256(132, 64)) + } + + // Revert if AssetProxy with given id does not exist + if iszero(assetProxy) { + // Revert with `Error("ASSET_PROXY_DOES_NOT_EXIST")` + mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + mstore(32, 0x0000002000000000000000000000000000000000000000000000000000000000) + mstore(64, 0x0000001a41535345545f50524f58595f444f45535f4e4f545f45584953540000) + mstore(96, 0) + revert(0, 100) + } + + // Copy `nestedAssetData[i]` from calldata to memory + calldatacopy( + 132, // memory slot after `amounts[i]` + nestedAssetDataElementLenStart, // location of `nestedAssetData[i]` in calldata + add(nestedAssetDataElementLen, 32) // `nestedAssetData[i].length` plus 32 byte length + ) + + // call `assetProxy.transferFrom` + let success := call( + gas, // forward all gas + assetProxy, // call address of asset proxy + 0, // don't send any ETH + 0, // pointer to start of input + add(164, nestedAssetDataElementLen), // length of input + 0, // write output over memory that won't be reused + 0 // don't copy output to memory + ) + + // Revert with reason given by AssetProxy if `transferFrom` call failed + if iszero(success) { + returndatacopy( + 0, // copy to memory at 0 + 0, // copy from return data at 0 + returndatasize() // copy all return data + ) + revert(0, returndatasize()) + } + } + + // Return if no `transferFrom` calls reverted + return(0, 0) + } + + // Revert if undefined function is called + revert(0, 0) + } + } + + /// @dev Gets the proxy id associated with the proxy address. + /// @return Proxy id. + function getProxyId() + external + pure + returns (bytes4) + { + return PROXY_ID; + } +} diff --git a/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol b/contracts/core/contracts/protocol/AssetProxy/interfaces/IAssetData.sol similarity index 82% rename from packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol rename to contracts/core/contracts/protocol/AssetProxy/interfaces/IAssetData.sol index 3e76e38dd0..e2da68919f 100644 --- a/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol +++ b/contracts/core/contracts/protocol/AssetProxy/interfaces/IAssetData.sol @@ -18,6 +18,7 @@ // solhint-disable pragma solidity 0.4.24; +pragma experimental ABIEncoderV2; // @dev Interface of the asset proxy's assetData. @@ -26,15 +27,18 @@ pragma solidity 0.4.24; interface IAssetData { function ERC20Token(address tokenContract) - external - pure; + external; function ERC721Token( address tokenContract, - uint256 tokenId, - bytes receiverData + uint256 tokenId ) - external - pure; + external; + + function MultiAsset( + uint256[] amounts, + bytes[] nestedAssetData + ) + external; } diff --git a/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetProxy.sol b/contracts/core/contracts/protocol/AssetProxy/interfaces/IAssetProxy.sol similarity index 100% rename from packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetProxy.sol rename to contracts/core/contracts/protocol/AssetProxy/interfaces/IAssetProxy.sol diff --git a/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAuthorizable.sol b/contracts/core/contracts/protocol/AssetProxy/interfaces/IAuthorizable.sol similarity index 100% rename from packages/contracts/contracts/protocol/AssetProxy/interfaces/IAuthorizable.sol rename to contracts/core/contracts/protocol/AssetProxy/interfaces/IAuthorizable.sol diff --git a/packages/contracts/contracts/protocol/AssetProxy/libs/LibAssetProxyErrors.sol b/contracts/core/contracts/protocol/AssetProxy/libs/LibAssetProxyErrors.sol similarity index 100% rename from packages/contracts/contracts/protocol/AssetProxy/libs/LibAssetProxyErrors.sol rename to contracts/core/contracts/protocol/AssetProxy/libs/LibAssetProxyErrors.sol diff --git a/packages/contracts/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol b/contracts/core/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol similarity index 100% rename from packages/contracts/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol rename to contracts/core/contracts/protocol/AssetProxy/mixins/MAuthorizable.sol diff --git a/packages/contracts/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol b/contracts/core/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol similarity index 100% rename from packages/contracts/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol rename to contracts/core/contracts/protocol/AssetProxyOwner/AssetProxyOwner.sol diff --git a/packages/contracts/contracts/protocol/Exchange/Exchange.sol b/contracts/core/contracts/protocol/Exchange/Exchange.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/Exchange.sol rename to contracts/core/contracts/protocol/Exchange/Exchange.sol diff --git a/packages/contracts/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol b/contracts/core/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol rename to contracts/core/contracts/protocol/Exchange/MixinAssetProxyDispatcher.sol diff --git a/packages/contracts/contracts/protocol/Exchange/MixinExchangeCore.sol b/contracts/core/contracts/protocol/Exchange/MixinExchangeCore.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/MixinExchangeCore.sol rename to contracts/core/contracts/protocol/Exchange/MixinExchangeCore.sol diff --git a/packages/contracts/contracts/protocol/Exchange/MixinMatchOrders.sol b/contracts/core/contracts/protocol/Exchange/MixinMatchOrders.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/MixinMatchOrders.sol rename to contracts/core/contracts/protocol/Exchange/MixinMatchOrders.sol diff --git a/packages/contracts/contracts/protocol/Exchange/MixinSignatureValidator.sol b/contracts/core/contracts/protocol/Exchange/MixinSignatureValidator.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/MixinSignatureValidator.sol rename to contracts/core/contracts/protocol/Exchange/MixinSignatureValidator.sol diff --git a/packages/contracts/contracts/protocol/Exchange/MixinTransactions.sol b/contracts/core/contracts/protocol/Exchange/MixinTransactions.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/MixinTransactions.sol rename to contracts/core/contracts/protocol/Exchange/MixinTransactions.sol diff --git a/packages/contracts/contracts/protocol/Exchange/MixinWrapperFunctions.sol b/contracts/core/contracts/protocol/Exchange/MixinWrapperFunctions.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/MixinWrapperFunctions.sol rename to contracts/core/contracts/protocol/Exchange/MixinWrapperFunctions.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol b/contracts/core/contracts/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/IExchange.sol b/contracts/core/contracts/protocol/Exchange/interfaces/IExchange.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/IExchange.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/IExchange.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/IExchangeCore.sol b/contracts/core/contracts/protocol/Exchange/interfaces/IExchangeCore.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/IExchangeCore.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/IExchangeCore.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/IMatchOrders.sol b/contracts/core/contracts/protocol/Exchange/interfaces/IMatchOrders.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/IMatchOrders.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/IMatchOrders.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/ISignatureValidator.sol b/contracts/core/contracts/protocol/Exchange/interfaces/ISignatureValidator.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/ISignatureValidator.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/ISignatureValidator.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/ITransactions.sol b/contracts/core/contracts/protocol/Exchange/interfaces/ITransactions.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/ITransactions.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/ITransactions.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/IValidator.sol b/contracts/core/contracts/protocol/Exchange/interfaces/IValidator.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/IValidator.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/IValidator.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/IWallet.sol b/contracts/core/contracts/protocol/Exchange/interfaces/IWallet.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/IWallet.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/IWallet.sol diff --git a/packages/contracts/contracts/protocol/Exchange/interfaces/IWrapperFunctions.sol b/contracts/core/contracts/protocol/Exchange/interfaces/IWrapperFunctions.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/interfaces/IWrapperFunctions.sol rename to contracts/core/contracts/protocol/Exchange/interfaces/IWrapperFunctions.sol diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibAbiEncoder.sol b/contracts/core/contracts/protocol/Exchange/libs/LibAbiEncoder.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/libs/LibAbiEncoder.sol rename to contracts/core/contracts/protocol/Exchange/libs/LibAbiEncoder.sol diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibConstants.sol b/contracts/core/contracts/protocol/Exchange/libs/LibConstants.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/libs/LibConstants.sol rename to contracts/core/contracts/protocol/Exchange/libs/LibConstants.sol diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibEIP712.sol b/contracts/core/contracts/protocol/Exchange/libs/LibEIP712.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/libs/LibEIP712.sol rename to contracts/core/contracts/protocol/Exchange/libs/LibEIP712.sol diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibExchangeErrors.sol b/contracts/core/contracts/protocol/Exchange/libs/LibExchangeErrors.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/libs/LibExchangeErrors.sol rename to contracts/core/contracts/protocol/Exchange/libs/LibExchangeErrors.sol diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibFillResults.sol b/contracts/core/contracts/protocol/Exchange/libs/LibFillResults.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/libs/LibFillResults.sol rename to contracts/core/contracts/protocol/Exchange/libs/LibFillResults.sol diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibMath.sol b/contracts/core/contracts/protocol/Exchange/libs/LibMath.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/libs/LibMath.sol rename to contracts/core/contracts/protocol/Exchange/libs/LibMath.sol diff --git a/packages/contracts/contracts/protocol/Exchange/libs/LibOrder.sol b/contracts/core/contracts/protocol/Exchange/libs/LibOrder.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/libs/LibOrder.sol rename to contracts/core/contracts/protocol/Exchange/libs/LibOrder.sol diff --git a/packages/contracts/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol b/contracts/core/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol rename to contracts/core/contracts/protocol/Exchange/mixins/MAssetProxyDispatcher.sol diff --git a/packages/contracts/contracts/protocol/Exchange/mixins/MExchangeCore.sol b/contracts/core/contracts/protocol/Exchange/mixins/MExchangeCore.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/mixins/MExchangeCore.sol rename to contracts/core/contracts/protocol/Exchange/mixins/MExchangeCore.sol diff --git a/packages/contracts/contracts/protocol/Exchange/mixins/MMatchOrders.sol b/contracts/core/contracts/protocol/Exchange/mixins/MMatchOrders.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/mixins/MMatchOrders.sol rename to contracts/core/contracts/protocol/Exchange/mixins/MMatchOrders.sol diff --git a/packages/contracts/contracts/protocol/Exchange/mixins/MSignatureValidator.sol b/contracts/core/contracts/protocol/Exchange/mixins/MSignatureValidator.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/mixins/MSignatureValidator.sol rename to contracts/core/contracts/protocol/Exchange/mixins/MSignatureValidator.sol diff --git a/packages/contracts/contracts/protocol/Exchange/mixins/MTransactions.sol b/contracts/core/contracts/protocol/Exchange/mixins/MTransactions.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/mixins/MTransactions.sol rename to contracts/core/contracts/protocol/Exchange/mixins/MTransactions.sol diff --git a/packages/contracts/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol b/contracts/core/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol similarity index 100% rename from packages/contracts/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol rename to contracts/core/contracts/protocol/Exchange/mixins/MWrapperFunctions.sol diff --git a/packages/contracts/contracts/test/DummyERC20Token/DummyERC20Token.sol b/contracts/core/contracts/test/DummyERC20Token/DummyERC20Token.sol similarity index 100% rename from packages/contracts/contracts/test/DummyERC20Token/DummyERC20Token.sol rename to contracts/core/contracts/test/DummyERC20Token/DummyERC20Token.sol diff --git a/packages/contracts/contracts/test/DummyERC20Token/DummyMultipleReturnERC20Token.sol b/contracts/core/contracts/test/DummyERC20Token/DummyMultipleReturnERC20Token.sol similarity index 100% rename from packages/contracts/contracts/test/DummyERC20Token/DummyMultipleReturnERC20Token.sol rename to contracts/core/contracts/test/DummyERC20Token/DummyMultipleReturnERC20Token.sol diff --git a/packages/contracts/contracts/test/DummyERC20Token/DummyNoReturnERC20Token.sol b/contracts/core/contracts/test/DummyERC20Token/DummyNoReturnERC20Token.sol similarity index 100% rename from packages/contracts/contracts/test/DummyERC20Token/DummyNoReturnERC20Token.sol rename to contracts/core/contracts/test/DummyERC20Token/DummyNoReturnERC20Token.sol diff --git a/packages/contracts/contracts/test/DummyERC721Receiver/DummyERC721Receiver.sol b/contracts/core/contracts/test/DummyERC721Receiver/DummyERC721Receiver.sol similarity index 100% rename from packages/contracts/contracts/test/DummyERC721Receiver/DummyERC721Receiver.sol rename to contracts/core/contracts/test/DummyERC721Receiver/DummyERC721Receiver.sol diff --git a/packages/contracts/contracts/test/DummyERC721Receiver/InvalidERC721Receiver.sol b/contracts/core/contracts/test/DummyERC721Receiver/InvalidERC721Receiver.sol similarity index 100% rename from packages/contracts/contracts/test/DummyERC721Receiver/InvalidERC721Receiver.sol rename to contracts/core/contracts/test/DummyERC721Receiver/InvalidERC721Receiver.sol diff --git a/packages/contracts/contracts/test/DummyERC721Token/DummyERC721Token.sol b/contracts/core/contracts/test/DummyERC721Token/DummyERC721Token.sol similarity index 100% rename from packages/contracts/contracts/test/DummyERC721Token/DummyERC721Token.sol rename to contracts/core/contracts/test/DummyERC721Token/DummyERC721Token.sol diff --git a/packages/contracts/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol b/contracts/core/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol similarity index 89% rename from packages/contracts/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol rename to contracts/core/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol index 99dd47a785..aa3bf9ab83 100644 --- a/packages/contracts/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol +++ b/contracts/core/contracts/test/ReentrantERC20Token/ReentrantERC20Token.sol @@ -92,53 +92,53 @@ contract ReentrantERC20Token is LibOrder.Order[] memory orders; uint256[] memory takerAssetFillAmounts; bytes[] memory signatures; - bytes memory calldata; + bytes memory callData; - // Create calldata for function that corresponds to currentFunctionId + // Create callData for function that corresponds to currentFunctionId if (currentFunctionId == uint8(ExchangeFunction.FILL_ORDER)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.fillOrder.selector, order, 0, signature ); } else if (currentFunctionId == uint8(ExchangeFunction.FILL_OR_KILL_ORDER)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.fillOrKillOrder.selector, order, 0, signature ); } else if (currentFunctionId == uint8(ExchangeFunction.BATCH_FILL_ORDERS)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.batchFillOrders.selector, orders, takerAssetFillAmounts, signatures ); } else if (currentFunctionId == uint8(ExchangeFunction.BATCH_FILL_OR_KILL_ORDERS)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.batchFillOrKillOrders.selector, orders, takerAssetFillAmounts, signatures ); } else if (currentFunctionId == uint8(ExchangeFunction.MARKET_BUY_ORDERS)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.marketBuyOrders.selector, orders, 0, signatures ); } else if (currentFunctionId == uint8(ExchangeFunction.MARKET_SELL_ORDERS)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.marketSellOrders.selector, orders, 0, signatures ); } else if (currentFunctionId == uint8(ExchangeFunction.MATCH_ORDERS)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.matchOrders.selector, order, order, @@ -146,22 +146,22 @@ contract ReentrantERC20Token is signature ); } else if (currentFunctionId == uint8(ExchangeFunction.CANCEL_ORDER)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.cancelOrder.selector, order ); } else if (currentFunctionId == uint8(ExchangeFunction.BATCH_CANCEL_ORDERS)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.batchCancelOrders.selector, orders ); } else if (currentFunctionId == uint8(ExchangeFunction.CANCEL_ORDERS_UP_TO)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.cancelOrdersUpTo.selector, 0 ); } else if (currentFunctionId == uint8(ExchangeFunction.SET_SIGNATURE_VALIDATOR_APPROVAL)) { - calldata = abi.encodeWithSelector( + callData = abi.encodeWithSelector( EXCHANGE.setSignatureValidatorApproval.selector, address(0), false @@ -169,7 +169,7 @@ contract ReentrantERC20Token is } // Call Exchange function, swallow error - address(EXCHANGE).call(calldata); + address(EXCHANGE).call(callData); // Revert reason is 100 bytes bytes memory returnData = new bytes(100); diff --git a/packages/contracts/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol b/contracts/core/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol similarity index 100% rename from packages/contracts/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol rename to contracts/core/contracts/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol diff --git a/packages/contracts/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol b/contracts/core/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol similarity index 100% rename from packages/contracts/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol rename to contracts/core/contracts/test/TestAssetProxyOwner/TestAssetProxyOwner.sol diff --git a/packages/contracts/contracts/test/TestConstants/TestConstants.sol b/contracts/core/contracts/test/TestConstants/TestConstants.sol similarity index 100% rename from packages/contracts/contracts/test/TestConstants/TestConstants.sol rename to contracts/core/contracts/test/TestConstants/TestConstants.sol diff --git a/packages/contracts/contracts/test/TestExchangeInternals/TestExchangeInternals.sol b/contracts/core/contracts/test/TestExchangeInternals/TestExchangeInternals.sol similarity index 100% rename from packages/contracts/contracts/test/TestExchangeInternals/TestExchangeInternals.sol rename to contracts/core/contracts/test/TestExchangeInternals/TestExchangeInternals.sol diff --git a/packages/contracts/contracts/test/TestLibBytes/TestLibBytes.sol b/contracts/core/contracts/test/TestLibBytes/TestLibBytes.sol similarity index 100% rename from packages/contracts/contracts/test/TestLibBytes/TestLibBytes.sol rename to contracts/core/contracts/test/TestLibBytes/TestLibBytes.sol diff --git a/packages/contracts/contracts/test/TestLibs/TestLibs.sol b/contracts/core/contracts/test/TestLibs/TestLibs.sol similarity index 100% rename from packages/contracts/contracts/test/TestLibs/TestLibs.sol rename to contracts/core/contracts/test/TestLibs/TestLibs.sol diff --git a/packages/contracts/contracts/test/TestSignatureValidator/TestSignatureValidator.sol b/contracts/core/contracts/test/TestSignatureValidator/TestSignatureValidator.sol similarity index 100% rename from packages/contracts/contracts/test/TestSignatureValidator/TestSignatureValidator.sol rename to contracts/core/contracts/test/TestSignatureValidator/TestSignatureValidator.sol diff --git a/packages/contracts/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol b/contracts/core/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol similarity index 100% rename from packages/contracts/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol rename to contracts/core/contracts/test/TestStaticCallReceiver/TestStaticCallReceiver.sol diff --git a/packages/contracts/contracts/tokens/ERC20Token/ERC20Token.sol b/contracts/core/contracts/tokens/ERC20Token/ERC20Token.sol similarity index 100% rename from packages/contracts/contracts/tokens/ERC20Token/ERC20Token.sol rename to contracts/core/contracts/tokens/ERC20Token/ERC20Token.sol diff --git a/packages/contracts/contracts/tokens/ERC20Token/IERC20Token.sol b/contracts/core/contracts/tokens/ERC20Token/IERC20Token.sol similarity index 100% rename from packages/contracts/contracts/tokens/ERC20Token/IERC20Token.sol rename to contracts/core/contracts/tokens/ERC20Token/IERC20Token.sol diff --git a/packages/contracts/contracts/tokens/ERC20Token/MintableERC20Token.sol b/contracts/core/contracts/tokens/ERC20Token/MintableERC20Token.sol similarity index 100% rename from packages/contracts/contracts/tokens/ERC20Token/MintableERC20Token.sol rename to contracts/core/contracts/tokens/ERC20Token/MintableERC20Token.sol diff --git a/packages/contracts/contracts/tokens/ERC20Token/UnlimitedAllowanceERC20Token.sol b/contracts/core/contracts/tokens/ERC20Token/UnlimitedAllowanceERC20Token.sol similarity index 100% rename from packages/contracts/contracts/tokens/ERC20Token/UnlimitedAllowanceERC20Token.sol rename to contracts/core/contracts/tokens/ERC20Token/UnlimitedAllowanceERC20Token.sol diff --git a/packages/contracts/contracts/tokens/ERC721Token/ERC721Token.sol b/contracts/core/contracts/tokens/ERC721Token/ERC721Token.sol similarity index 100% rename from packages/contracts/contracts/tokens/ERC721Token/ERC721Token.sol rename to contracts/core/contracts/tokens/ERC721Token/ERC721Token.sol diff --git a/packages/contracts/contracts/tokens/ERC721Token/IERC721Receiver.sol b/contracts/core/contracts/tokens/ERC721Token/IERC721Receiver.sol similarity index 100% rename from packages/contracts/contracts/tokens/ERC721Token/IERC721Receiver.sol rename to contracts/core/contracts/tokens/ERC721Token/IERC721Receiver.sol diff --git a/packages/contracts/contracts/tokens/ERC721Token/IERC721Token.sol b/contracts/core/contracts/tokens/ERC721Token/IERC721Token.sol similarity index 100% rename from packages/contracts/contracts/tokens/ERC721Token/IERC721Token.sol rename to contracts/core/contracts/tokens/ERC721Token/IERC721Token.sol diff --git a/packages/contracts/contracts/tokens/ERC721Token/MintableERC721Token.sol b/contracts/core/contracts/tokens/ERC721Token/MintableERC721Token.sol similarity index 100% rename from packages/contracts/contracts/tokens/ERC721Token/MintableERC721Token.sol rename to contracts/core/contracts/tokens/ERC721Token/MintableERC721Token.sol diff --git a/packages/contracts/contracts/tokens/EtherToken/IEtherToken.sol b/contracts/core/contracts/tokens/EtherToken/IEtherToken.sol similarity index 100% rename from packages/contracts/contracts/tokens/EtherToken/IEtherToken.sol rename to contracts/core/contracts/tokens/EtherToken/IEtherToken.sol diff --git a/packages/contracts/contracts/tokens/EtherToken/WETH9.sol b/contracts/core/contracts/tokens/EtherToken/WETH9.sol similarity index 100% rename from packages/contracts/contracts/tokens/EtherToken/WETH9.sol rename to contracts/core/contracts/tokens/EtherToken/WETH9.sol diff --git a/packages/contracts/contracts/tokens/ZRXToken/ERC20Token_v1.sol b/contracts/core/contracts/tokens/ZRXToken/ERC20Token_v1.sol similarity index 100% rename from packages/contracts/contracts/tokens/ZRXToken/ERC20Token_v1.sol rename to contracts/core/contracts/tokens/ZRXToken/ERC20Token_v1.sol diff --git a/packages/contracts/contracts/tokens/ZRXToken/Token_v1.sol b/contracts/core/contracts/tokens/ZRXToken/Token_v1.sol similarity index 100% rename from packages/contracts/contracts/tokens/ZRXToken/Token_v1.sol rename to contracts/core/contracts/tokens/ZRXToken/Token_v1.sol diff --git a/packages/contracts/contracts/tokens/ZRXToken/UnlimitedAllowanceToken_v1.sol b/contracts/core/contracts/tokens/ZRXToken/UnlimitedAllowanceToken_v1.sol similarity index 100% rename from packages/contracts/contracts/tokens/ZRXToken/UnlimitedAllowanceToken_v1.sol rename to contracts/core/contracts/tokens/ZRXToken/UnlimitedAllowanceToken_v1.sol diff --git a/packages/contracts/contracts/tokens/ZRXToken/ZRXToken.sol b/contracts/core/contracts/tokens/ZRXToken/ZRXToken.sol similarity index 100% rename from packages/contracts/contracts/tokens/ZRXToken/ZRXToken.sol rename to contracts/core/contracts/tokens/ZRXToken/ZRXToken.sol diff --git a/packages/contracts/contracts/utils/LibBytes/LibBytes.sol b/contracts/core/contracts/utils/LibBytes/LibBytes.sol similarity index 100% rename from packages/contracts/contracts/utils/LibBytes/LibBytes.sol rename to contracts/core/contracts/utils/LibBytes/LibBytes.sol diff --git a/packages/contracts/contracts/utils/Ownable/IOwnable.sol b/contracts/core/contracts/utils/Ownable/IOwnable.sol similarity index 100% rename from packages/contracts/contracts/utils/Ownable/IOwnable.sol rename to contracts/core/contracts/utils/Ownable/IOwnable.sol diff --git a/packages/contracts/contracts/utils/Ownable/Ownable.sol b/contracts/core/contracts/utils/Ownable/Ownable.sol similarity index 100% rename from packages/contracts/contracts/utils/Ownable/Ownable.sol rename to contracts/core/contracts/utils/Ownable/Ownable.sol diff --git a/packages/contracts/contracts/utils/ReentrancyGuard/ReentrancyGuard.sol b/contracts/core/contracts/utils/ReentrancyGuard/ReentrancyGuard.sol similarity index 100% rename from packages/contracts/contracts/utils/ReentrancyGuard/ReentrancyGuard.sol rename to contracts/core/contracts/utils/ReentrancyGuard/ReentrancyGuard.sol diff --git a/packages/contracts/contracts/utils/SafeMath/SafeMath.sol b/contracts/core/contracts/utils/SafeMath/SafeMath.sol similarity index 100% rename from packages/contracts/contracts/utils/SafeMath/SafeMath.sol rename to contracts/core/contracts/utils/SafeMath/SafeMath.sol diff --git a/packages/contracts/package.json b/contracts/core/package.json similarity index 80% rename from packages/contracts/package.json rename to contracts/core/package.json index 6f031fcee8..b20b0ef3f7 100644 --- a/packages/contracts/package.json +++ b/contracts/core/package.json @@ -19,7 +19,8 @@ "test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov", "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", + "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", "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", @@ -32,7 +33,8 @@ "lint-contracts": "solhint contracts/**/**/**/**/*.sol" }, "config": { - "abis": "generated-artifacts/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyMultipleReturnERC20Token|DummyNoReturnERC20Token|ERC20Token|ERC20Proxy|ERC721Token|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|ReentrantERC20Token|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|TestStaticCallReceiver|Validator|Wallet|Whitelist|WETH9|ZRXToken).json" + "abis": + "generated-artifacts/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyMultipleReturnERC20Token|DummyNoReturnERC20Token|ERC20Token|ERC20Proxy|ERC721Token|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiAssetProxy|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|ReentrantERC20Token|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|TestStaticCallReceiver|Validator|Wallet|Whitelist|WETH9|ZRXToken).json" }, "repository": { "type": "git", @@ -43,7 +45,7 @@ "bugs": { "url": "https://github.com/0xProject/0x-monorepo/issues" }, - "homepage": "https://github.com/0xProject/0x-monorepo/packages/contracts/README.md", + "homepage": "https://github.com/0xProject/0x-monorepo/contracts/core/README.md", "devDependencies": { "@0x/abi-gen": "^1.0.17", "@0x/dev-utils": "^1.0.19", diff --git a/packages/contracts/src/artifacts/index.ts b/contracts/core/src/artifacts/index.ts similarity index 97% rename from packages/contracts/src/artifacts/index.ts rename to contracts/core/src/artifacts/index.ts index c30972a91b..97c1b62092 100644 --- a/packages/contracts/src/artifacts/index.ts +++ b/contracts/core/src/artifacts/index.ts @@ -19,6 +19,7 @@ import * as InvalidERC721Receiver from '../../generated-artifacts/InvalidERC721R import * as IValidator from '../../generated-artifacts/IValidator.json'; import * as IWallet from '../../generated-artifacts/IWallet.json'; import * as MixinAuthorizable from '../../generated-artifacts/MixinAuthorizable.json'; +import * as MultiAssetProxy from '../../generated-artifacts/MultiAssetProxy.json'; import * as MultiSigWallet from '../../generated-artifacts/MultiSigWallet.json'; import * as MultiSigWalletWithTimeLock from '../../generated-artifacts/MultiSigWalletWithTimeLock.json'; import * as OrderValidator from '../../generated-artifacts/OrderValidator.json'; @@ -57,6 +58,7 @@ export const artifacts = { IWallet: IWallet as ContractArtifact, InvalidERC721Receiver: InvalidERC721Receiver as ContractArtifact, MixinAuthorizable: MixinAuthorizable as ContractArtifact, + MultiAssetProxy: MultiAssetProxy as ContractArtifact, MultiSigWallet: MultiSigWallet as ContractArtifact, MultiSigWalletWithTimeLock: MultiSigWalletWithTimeLock as ContractArtifact, OrderValidator: OrderValidator as ContractArtifact, diff --git a/packages/contracts/src/wrappers/index.ts b/contracts/core/src/wrappers/index.ts similarity index 100% rename from packages/contracts/src/wrappers/index.ts rename to contracts/core/src/wrappers/index.ts diff --git a/packages/contracts/test/asset_proxy/authorizable.ts b/contracts/core/test/asset_proxy/authorizable.ts similarity index 100% rename from packages/contracts/test/asset_proxy/authorizable.ts rename to contracts/core/test/asset_proxy/authorizable.ts diff --git a/contracts/core/test/asset_proxy/proxies.ts b/contracts/core/test/asset_proxy/proxies.ts new file mode 100644 index 0000000000..8fa1e602a2 --- /dev/null +++ b/contracts/core/test/asset_proxy/proxies.ts @@ -0,0 +1,1246 @@ +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { RevertReason } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token'; +import { DummyERC721ReceiverContract } from '../../generated-wrappers/dummy_erc721_receiver'; +import { DummyERC721TokenContract } from '../../generated-wrappers/dummy_erc721_token'; +import { DummyMultipleReturnERC20TokenContract } from '../../generated-wrappers/dummy_multiple_return_erc20_token'; +import { DummyNoReturnERC20TokenContract } from '../../generated-wrappers/dummy_no_return_erc20_token'; +import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy'; +import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy'; +import { IAssetDataContract } from '../../generated-wrappers/i_asset_data'; +import { IAssetProxyContract } from '../../generated-wrappers/i_asset_proxy'; +import { MultiAssetProxyContract } from '../../generated-wrappers/multi_asset_proxy'; +import { artifacts } from '../../src/artifacts'; +import { expectTransactionFailedAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { constants } from '../utils/constants'; +import { ERC20Wrapper } from '../utils/erc20_wrapper'; +import { ERC721Wrapper } from '../utils/erc721_wrapper'; +import { LogDecoder } from '../utils/log_decoder'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const assetProxyInterface = new IAssetProxyContract( + artifacts.IAssetProxy.compilerOutput.abi, + constants.NULL_ADDRESS, + provider, +); +const assetDataInterface = new IAssetDataContract( + artifacts.IAssetData.compilerOutput.abi, + constants.NULL_ADDRESS, + provider, +); + +// tslint:disable:no-unnecessary-type-assertion +describe('Asset Transfer Proxies', () => { + let owner: string; + let notAuthorized: string; + let authorized: string; + let fromAddress: string; + let toAddress: string; + + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let erc721TokenA: DummyERC721TokenContract; + let erc721TokenB: DummyERC721TokenContract; + let erc721Receiver: DummyERC721ReceiverContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; + let noReturnErc20Token: DummyNoReturnERC20TokenContract; + let multipleReturnErc20Token: DummyMultipleReturnERC20TokenContract; + let multiAssetProxy: MultiAssetProxyContract; + + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; + let erc721AFromTokenId: BigNumber; + let erc721BFromTokenId: BigNumber; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, notAuthorized, authorized, fromAddress, toAddress] = _.slice(accounts, 0, 5)); + + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + + // Deploy AssetProxies + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync( + artifacts.MultiAssetProxy, + provider, + txDefaults, + ); + + // Configure ERC20Proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Configure ERC721Proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Configure MultiAssetProxy + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(authorized, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Deploy and configure ERC20 tokens + const numDummyErc20ToDeploy = 2; + [erc20TokenA, erc20TokenB] = await erc20Wrapper.deployDummyTokensAsync( + numDummyErc20ToDeploy, + constants.DUMMY_TOKEN_DECIMALS, + ); + noReturnErc20Token = await DummyNoReturnERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyNoReturnERC20Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + multipleReturnErc20Token = await DummyMultipleReturnERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyMultipleReturnERC20Token, + provider, + txDefaults, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + + await erc20Wrapper.setBalancesAndAllowancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await noReturnErc20Token.setBalance.sendTransactionAsync(fromAddress, constants.INITIAL_ERC20_BALANCE), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await noReturnErc20Token.approve.sendTransactionAsync( + erc20Proxy.address, + constants.INITIAL_ERC20_ALLOWANCE, + { from: fromAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multipleReturnErc20Token.setBalance.sendTransactionAsync( + fromAddress, + constants.INITIAL_ERC20_BALANCE, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multipleReturnErc20Token.approve.sendTransactionAsync( + erc20Proxy.address, + constants.INITIAL_ERC20_ALLOWANCE, + { from: fromAddress }, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Deploy and configure ERC721 tokens and receiver + [erc721TokenA, erc721TokenB] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync( + artifacts.DummyERC721Receiver, + provider, + txDefaults, + ); + + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721AFromTokenId = erc721Balances[fromAddress][erc721TokenA.address][0]; + erc721BFromTokenId = erc721Balances[fromAddress][erc721TokenB.address][0]; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('ERC20Proxy', () => { + it('should revert if undefined function is called', async () => { + const undefinedSelector = '0x01020304'; + await expectTransactionFailedWithoutReasonAsync( + web3Wrapper.sendTransactionAsync({ + from: owner, + to: erc20Proxy.address, + value: constants.ZERO_AMOUNT, + data: undefinedSelector, + }), + ); + }); + it('should have an id of 0xf47261b0', async () => { + const proxyId = await erc20Proxy.getProxyId.callAsync(); + const expectedProxyId = '0xf47261b0'; + expect(proxyId).to.equal(expectedProxyId); + }); + describe('transferFrom', () => { + it('should successfully transfer tokens', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // Perform a transfer from fromAddress to toAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(amount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(amount), + ); + }); + + it('should successfully transfer tokens that do not return a value', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address); + // Perform a transfer from fromAddress to toAddress + const initialFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress); + const initialToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress); + const newToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress); + expect(newFromBalance).to.be.bignumber.equal(initialFromBalance.minus(amount)); + expect(newToBalance).to.be.bignumber.equal(initialToBalance.plus(amount)); + }); + + it('should successfully transfer tokens and ignore extra assetData', async () => { + // Construct ERC20 asset data + const extraData = '0102030405060708'; + const encodedAssetData = `${assetDataUtils.encodeERC20AssetData(erc20TokenA.address)}${extraData}`; + // Perform a transfer from fromAddress to toAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(amount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(amount), + ); + }); + + it('should do nothing if transferring 0 amount of a token', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // Perform a transfer from fromAddress to toAddress + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const amount = new BigNumber(0); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address], + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address], + ); + }); + + it('should revert if allowances are too low', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // Create allowance less than transfer amount. Set allowance on proxy. + const allowance = new BigNumber(0); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, allowance, { + from: fromAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + // Perform a transfer; expect this to fail. + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: authorized, + }), + RevertReason.TransferFailed, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.deep.equal(erc20Balances); + }); + + it('should revert if allowances are too low and token does not return a value', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address); + // Create allowance less than transfer amount. Set allowance on proxy. + const allowance = new BigNumber(0); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await noReturnErc20Token.approve.sendTransactionAsync(erc20Proxy.address, allowance, { + from: fromAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const initialFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress); + const initialToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress); + // Perform a transfer; expect this to fail. + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: authorized, + }), + RevertReason.TransferFailed, + ); + const newFromBalance = await noReturnErc20Token.balanceOf.callAsync(fromAddress); + const newToBalance = await noReturnErc20Token.balanceOf.callAsync(toAddress); + expect(newFromBalance).to.be.bignumber.equal(initialFromBalance); + expect(newToBalance).to.be.bignumber.equal(initialToBalance); + }); + + it('should revert if caller is not authorized', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // Perform a transfer from fromAddress to toAddress + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: notAuthorized, + }), + RevertReason.SenderNotAuthorized, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.deep.equal(erc20Balances); + }); + + it('should revert if token returns more than 32 bytes', async () => { + // Construct ERC20 asset data + const encodedAssetData = assetDataUtils.encodeERC20AssetData(multipleReturnErc20Token.address); + const amount = new BigNumber(10); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + const initialFromBalance = await multipleReturnErc20Token.balanceOf.callAsync(fromAddress); + const initialToBalance = await multipleReturnErc20Token.balanceOf.callAsync(toAddress); + // Perform a transfer; expect this to fail. + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc20Proxy.address, + data, + from: authorized, + }), + RevertReason.TransferFailed, + ); + const newFromBalance = await multipleReturnErc20Token.balanceOf.callAsync(fromAddress); + const newToBalance = await multipleReturnErc20Token.balanceOf.callAsync(toAddress); + expect(newFromBalance).to.be.bignumber.equal(initialFromBalance); + expect(newToBalance).to.be.bignumber.equal(initialToBalance); + }); + }); + }); + + describe('ERC721Proxy', () => { + it('should revert if undefined function is called', async () => { + const undefinedSelector = '0x01020304'; + await expectTransactionFailedWithoutReasonAsync( + web3Wrapper.sendTransactionAsync({ + from: owner, + to: erc721Proxy.address, + value: constants.ZERO_AMOUNT, + data: undefinedSelector, + }), + ); + }); + it('should have an id of 0x02571792', async () => { + const proxyId = await erc721Proxy.getProxyId.callAsync(); + const expectedProxyId = '0x02571792'; + expect(proxyId).to.equal(expectedProxyId); + }); + describe('transferFrom', () => { + it('should successfully transfer tokens', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + // Verify pre-condition + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.bignumber.equal(toAddress); + }); + + it('should successfully transfer tokens and ignore extra assetData', async () => { + // Construct ERC721 asset data + const extraData = '0102030405060708'; + const encodedAssetData = `${assetDataUtils.encodeERC721AssetData( + erc721TokenA.address, + erc721AFromTokenId, + )}${extraData}`; + // Verify pre-condition + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Verify transfer was successful + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.bignumber.equal(toAddress); + }); + + it('should not call onERC721Received when transferring to a smart contract', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + // Verify pre-condition + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + erc721Receiver.address, + amount, + ); + const logDecoder = new LogDecoder(web3Wrapper); + const tx = await logDecoder.getTxWithDecodedLogsAsync( + await web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: authorized, + gas: constants.MAX_TRANSFER_FROM_GAS, + }), + ); + // Verify that no log was emitted by erc721 receiver + expect(tx.logs.length).to.be.equal(1); + // Verify transfer was successful + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.bignumber.equal(erc721Receiver.address); + }); + + it('should revert if transferring 0 amount of a token', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + // Verify pre-condition + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const amount = new BigNumber(0); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: authorized, + }), + RevertReason.InvalidAmount, + ); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); + }); + + it('should revert if transferring > 1 amount of a token', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + // Verify pre-condition + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const amount = new BigNumber(500); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: authorized, + }), + RevertReason.InvalidAmount, + ); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); + }); + + it('should revert if allowances are too low', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + // Verify pre-condition + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Remove transfer approval for fromAddress. + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721TokenA.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721AFromTokenId, { + from: fromAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + // Perform a transfer; expect this to fail. + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: authorized, + }), + RevertReason.TransferFailed, + ); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); + }); + + it('should revert if caller is not authorized', async () => { + // Construct ERC721 asset data + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + // Verify pre-condition + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Perform a transfer from fromAddress to toAddress + const amount = new BigNumber(1); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + encodedAssetData, + fromAddress, + toAddress, + amount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: erc721Proxy.address, + data, + from: notAuthorized, + }), + RevertReason.SenderNotAuthorized, + ); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); + }); + }); + }); + describe('MultiAssetProxy', () => { + it('should revert if undefined function is called', async () => { + const undefinedSelector = '0x01020304'; + await expectTransactionFailedWithoutReasonAsync( + web3Wrapper.sendTransactionAsync({ + from: owner, + to: multiAssetProxy.address, + value: constants.ZERO_AMOUNT, + data: undefinedSelector, + }), + ); + }); + it('should have an id of 0x94cfcdd7', async () => { + const proxyId = await multiAssetProxy.getProxyId.callAsync(); + // first 4 bytes of `keccak256('MultiAsset(uint256[],bytes[])')` + const expectedProxyId = '0x94cfcdd7'; + expect(proxyId).to.equal(expectedProxyId); + }); + describe('transferFrom', () => { + it('should transfer a single ERC20 token', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const amounts = [erc20Amount]; + const nestedAssetData = [erc20AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalAmount = inputAmount.times(erc20Amount); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalAmount), + ); + }); + it('should successfully transfer multiple of the same ERC20 token', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount1 = new BigNumber(10); + const erc20Amount2 = new BigNumber(20); + const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const amounts = [erc20Amount1, erc20Amount2]; + const nestedAssetData = [erc20AssetData1, erc20AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalAmount = inputAmount.times(erc20Amount1).plus(inputAmount.times(erc20Amount2)); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalAmount), + ); + }); + it('should successfully transfer multiple different ERC20 tokens', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount1 = new BigNumber(10); + const erc20Amount2 = new BigNumber(20); + const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address); + const amounts = [erc20Amount1, erc20Amount2]; + const nestedAssetData = [erc20AssetData1, erc20AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalErc20AAmount = inputAmount.times(erc20Amount1); + const totalErc20BAmount = inputAmount.times(erc20Amount2); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount), + ); + expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount), + ); + expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount), + ); + }); + it('should transfer a single ERC721 token', async () => { + const inputAmount = new BigNumber(1); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc721Amount]; + const nestedAssetData = [erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.equal(toAddress); + }); + it('should successfully transfer multiple of the same ERC721 token', async () => { + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + const erc721AFromTokenId2 = erc721Balances[fromAddress][erc721TokenA.address][1]; + const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const erc721AssetData2 = assetDataUtils.encodeERC721AssetData( + erc721TokenA.address, + erc721AFromTokenId2, + ); + const inputAmount = new BigNumber(1); + const erc721Amount = new BigNumber(1); + const amounts = [erc721Amount, erc721Amount]; + const nestedAssetData = [erc721AssetData1, erc721AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset1).to.be.equal(fromAddress); + const ownerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2); + expect(ownerFromAsset2).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + gas: constants.MAX_TRANSFER_FROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + const newOwnerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2); + expect(newOwnerFromAsset1).to.be.equal(toAddress); + expect(newOwnerFromAsset2).to.be.equal(toAddress); + }); + it('should successfully transfer multiple different ERC721 tokens', async () => { + const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const erc721AssetData2 = assetDataUtils.encodeERC721AssetData(erc721TokenB.address, erc721BFromTokenId); + const inputAmount = new BigNumber(1); + const erc721Amount = new BigNumber(1); + const amounts = [erc721Amount, erc721Amount]; + const nestedAssetData = [erc721AssetData1, erc721AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset1).to.be.equal(fromAddress); + const ownerFromAsset2 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId); + expect(ownerFromAsset2).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + gas: constants.MAX_TRANSFER_FROM_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + const newOwnerFromAsset2 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId); + expect(newOwnerFromAsset1).to.be.equal(toAddress); + expect(newOwnerFromAsset2).to.be.equal(toAddress); + }); + it('should successfully transfer a combination of ERC20 and ERC721 tokens', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalAmount = inputAmount.times(erc20Amount); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalAmount), + ); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.equal(toAddress); + }); + it('should successfully transfer tokens and ignore extra assetData', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const extraData = '0102030405060708'; + const assetData = `${assetDataInterface.MultiAsset.getABIEncodedTransactionData( + amounts, + nestedAssetData, + )}${extraData}`; + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalAmount = inputAmount.times(erc20Amount); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalAmount), + ); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.equal(toAddress); + }); + it('should successfully transfer correct amounts when the `amount` > 1', async () => { + const inputAmount = new BigNumber(100); + const erc20Amount1 = new BigNumber(10); + const erc20Amount2 = new BigNumber(20); + const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address); + const amounts = [erc20Amount1, erc20Amount2]; + const nestedAssetData = [erc20AssetData1, erc20AssetData2]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalErc20AAmount = inputAmount.times(erc20Amount1); + const totalErc20BAmount = inputAmount.times(erc20Amount2); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount), + ); + expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount), + ); + expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount), + ); + }); + it('should successfully transfer a large amount of tokens', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount1 = new BigNumber(10); + const erc20Amount2 = new BigNumber(20); + const erc20AssetData1 = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc20AssetData2 = assetDataUtils.encodeERC20AssetData(erc20TokenB.address); + const erc721Amount = new BigNumber(1); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + const erc721AFromTokenId2 = erc721Balances[fromAddress][erc721TokenA.address][1]; + const erc721BFromTokenId2 = erc721Balances[fromAddress][erc721TokenB.address][1]; + const erc721AssetData1 = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const erc721AssetData2 = assetDataUtils.encodeERC721AssetData( + erc721TokenA.address, + erc721AFromTokenId2, + ); + const erc721AssetData3 = assetDataUtils.encodeERC721AssetData(erc721TokenB.address, erc721BFromTokenId); + const erc721AssetData4 = assetDataUtils.encodeERC721AssetData( + erc721TokenB.address, + erc721BFromTokenId2, + ); + const amounts = [erc721Amount, erc20Amount1, erc721Amount, erc20Amount2, erc721Amount, erc721Amount]; + const nestedAssetData = [ + erc721AssetData1, + erc20AssetData1, + erc721AssetData2, + erc20AssetData2, + erc721AssetData3, + erc721AssetData4, + ]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + const ownerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset1).to.be.equal(fromAddress); + const ownerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2); + expect(ownerFromAsset2).to.be.equal(fromAddress); + const ownerFromAsset3 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId); + expect(ownerFromAsset3).to.be.equal(fromAddress); + const ownerFromAsset4 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId2); + expect(ownerFromAsset4).to.be.equal(fromAddress); + const erc20Balances = await erc20Wrapper.getBalancesAsync(); + await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + gas: constants.MAX_EXECUTE_TRANSACTION_GAS, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const newOwnerFromAsset1 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + const newOwnerFromAsset2 = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId2); + const newOwnerFromAsset3 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId); + const newOwnerFromAsset4 = await erc721TokenB.ownerOf.callAsync(erc721BFromTokenId2); + expect(newOwnerFromAsset1).to.be.equal(toAddress); + expect(newOwnerFromAsset2).to.be.equal(toAddress); + expect(newOwnerFromAsset3).to.be.equal(toAddress); + expect(newOwnerFromAsset4).to.be.equal(toAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const totalErc20AAmount = inputAmount.times(erc20Amount1); + const totalErc20BAmount = inputAmount.times(erc20Amount2); + expect(newBalances[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(totalErc20AAmount), + ); + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address].add(totalErc20AAmount), + ); + expect(newBalances[fromAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenB.address].minus(totalErc20BAmount), + ); + expect(newBalances[toAddress][erc20TokenB.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenB.address].add(totalErc20BAmount), + ); + }); + it('should revert if a single transfer fails', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // 2 is an invalid erc721 amount + const erc721Amount = new BigNumber(2); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.InvalidAmount, + ); + }); + it('should revert if an AssetProxy is not registered', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const invalidProxyId = '0x12345678'; + const invalidErc721AssetData = `${invalidProxyId}${erc721AssetData.slice(10)}`; + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, invalidErc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.AssetProxyDoesNotExist, + ); + }); + it('should revert if the length of `amounts` does not match the length of `nestedAssetData`', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.LengthMismatch, + ); + }); + it('should revert if amounts multiplication results in an overflow', async () => { + const inputAmount = new BigNumber(2).pow(128); + const erc20Amount = new BigNumber(2).pow(128); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const amounts = [erc20Amount]; + const nestedAssetData = [erc20AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.Uint256Overflow, + ); + }); + it('should revert if an element of `nestedAssetData` is < 4 bytes long', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = '0x123456'; + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: authorized, + }), + RevertReason.LengthGreaterThan3Required, + ); + }); + it('should revert if caller is not authorized', async () => { + const inputAmount = new BigNumber(1); + const erc20Amount = new BigNumber(10); + const erc20AssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + const erc721Amount = new BigNumber(1); + const erc721AssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); + const amounts = [erc20Amount, erc721Amount]; + const nestedAssetData = [erc20AssetData, erc721AssetData]; + const assetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData(amounts, nestedAssetData); + const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( + assetData, + fromAddress, + toAddress, + inputAmount, + ); + await expectTransactionFailedAsync( + web3Wrapper.sendTransactionAsync({ + to: multiAssetProxy.address, + data, + from: notAuthorized, + }), + RevertReason.SenderNotAuthorized, + ); + }); + }); + }); +}); +// tslint:enable:no-unnecessary-type-assertion +// tslint:disable:max-file-line-count diff --git a/packages/contracts/test/exchange/core.ts b/contracts/core/test/exchange/core.ts similarity index 72% rename from packages/contracts/test/exchange/core.ts rename to contracts/core/test/exchange/core.ts index fc8dc5346a..9159b0d8f5 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/contracts/core/test/exchange/core.ts @@ -14,6 +14,8 @@ import { DummyNoReturnERC20TokenContract } from '../../generated-wrappers/dummy_ import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy'; import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy'; import { ExchangeCancelEventArgs, ExchangeContract } from '../../generated-wrappers/exchange'; +import { IAssetDataContract } from '../../generated-wrappers/i_asset_data'; +import { MultiAssetProxyContract } from '../../generated-wrappers/multi_asset_proxy'; import { ReentrantERC20TokenContract } from '../../generated-wrappers/reentrant_erc20_token'; import { TestStaticCallReceiverContract } from '../../generated-wrappers/test_static_call_receiver'; import { artifacts } from '../../src/artifacts'; @@ -31,6 +33,11 @@ import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); +const assetDataInterface = new IAssetDataContract( + artifacts.IAssetData.compilerOutput.abi, + constants.NULL_ADDRESS, + provider, +); // tslint:disable:no-unnecessary-type-assertion describe('Exchange core', () => { let makerAddress: string; @@ -47,6 +54,7 @@ describe('Exchange core', () => { let exchange: ExchangeContract; let erc20Proxy: ERC20ProxyContract; let erc721Proxy: ERC721ProxyContract; + let multiAssetProxy: MultiAssetProxyContract; let maliciousWallet: TestStaticCallReceiverContract; let maliciousValidator: TestStaticCallReceiverContract; @@ -76,44 +84,26 @@ describe('Exchange core', () => { erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + // Deploy AssetProxies, Exchange, tokens, and malicious contracts + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync( + artifacts.MultiAssetProxy, + provider, + txDefaults, + ); const numDummyErc20ToDeploy = 3; [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS, ); - erc20Proxy = await erc20Wrapper.deployProxyAsync(); - await erc20Wrapper.setBalancesAndAllowancesAsync(); - [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); - erc721Proxy = await erc721Wrapper.deployProxyAsync(); - await erc721Wrapper.setBalancesAndAllowancesAsync(); - const erc721Balances = await erc721Wrapper.getBalancesAsync(); - erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; - erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address]; - exchange = await ExchangeContract.deployFrom0xArtifactAsync( artifacts.Exchange, provider, txDefaults, assetDataUtils.encodeERC20AssetData(zrxToken.address), ); - exchangeWrapper = new ExchangeWrapper(exchange, provider); - await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); - await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); - - await web3Wrapper.awaitTransactionSuccessAsync( - await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { - from: owner, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { - from: owner, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync( artifacts.TestStaticCallReceiver, provider, @@ -126,9 +116,72 @@ describe('Exchange core', () => { exchange.address, ); + // Configure ERC20Proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Configure ERC721Proxy + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(multiAssetProxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Configure MultiAssetProxy + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.registerAssetProxy.sendTransactionAsync(erc721Proxy.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + // Configure Exchange + exchangeWrapper = new ExchangeWrapper(exchange, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(multiAssetProxy.address, owner); + + // Configure ERC20 tokens + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + // Configure ERC721 tokens + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; + erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address]; + + // Configure order defaults defaultMakerAssetAddress = erc20TokenA.address; defaultTakerAssetAddress = erc20TokenB.address; - const defaultOrderParams = { ...constants.STATIC_ORDER_PARAMS, exchangeAddress: exchange.address, @@ -707,6 +760,292 @@ describe('Exchange core', () => { }); }); + describe('Testing exchange of multiple assets', () => { + it('should allow multiple assets to be exchanged for a single asset', async () => { + const makerAmounts = [new BigNumber(10), new BigNumber(20)]; + const makerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + assetDataUtils.encodeERC20AssetData(erc20TokenB.address), + ]; + const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + makerAmounts, + makerNestedAssetData, + ); + const makerAssetAmount = new BigNumber(1); + const takerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const takerAssetAmount = new BigNumber(10); + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + takerAssetData, + makerAssetAmount, + takerAssetAmount, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }); + + const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + + const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal( + initialMakerBalanceA.minus(makerAmounts[0].times(makerAssetAmount)), + ); + expect(finalMakerBalanceB).to.be.bignumber.equal( + initialMakerBalanceB.minus(makerAmounts[1].times(makerAssetAmount)), + ); + expect(finalMakerZrxBalance).to.be.bignumber.equal(initialMakerZrxBalance.plus(takerAssetAmount)); + expect(finalTakerBalanceA).to.be.bignumber.equal( + initialTakerBalanceA.plus(makerAmounts[0].times(makerAssetAmount)), + ); + expect(finalTakerBalanceB).to.be.bignumber.equal( + initialTakerBalanceB.plus(makerAmounts[1].times(makerAssetAmount)), + ); + expect(finalTakerZrxBalance).to.be.bignumber.equal(initialTakerZrxBalance.minus(takerAssetAmount)); + }); + it('should allow multiple assets to be exchanged for multiple assets', async () => { + const makerAmounts = [new BigNumber(10), new BigNumber(20)]; + const makerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + assetDataUtils.encodeERC20AssetData(erc20TokenB.address), + ]; + const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + makerAmounts, + makerNestedAssetData, + ); + const makerAssetAmount = new BigNumber(1); + const takerAmounts = [new BigNumber(10), new BigNumber(1)]; + const takerAssetId = erc721TakerAssetIds[0]; + const takerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(zrxToken.address), + assetDataUtils.encodeERC721AssetData(erc721Token.address, takerAssetId), + ]; + const takerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + takerAmounts, + takerNestedAssetData, + ); + const takerAssetAmount = new BigNumber(1); + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + takerAssetData, + makerAssetAmount, + takerAssetAmount, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }); + + const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const initialOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + expect(initialOwnerTakerAsset).to.be.bignumber.equal(takerAddress); + + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + + const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + const finalOwnerTakerAsset = await erc721Token.ownerOf.callAsync(takerAssetId); + + expect(finalMakerBalanceA).to.be.bignumber.equal( + initialMakerBalanceA.minus(makerAmounts[0].times(makerAssetAmount)), + ); + expect(finalMakerBalanceB).to.be.bignumber.equal( + initialMakerBalanceB.minus(makerAmounts[1].times(makerAssetAmount)), + ); + expect(finalMakerZrxBalance).to.be.bignumber.equal( + initialMakerZrxBalance.plus(takerAmounts[0].times(takerAssetAmount)), + ); + expect(finalTakerBalanceA).to.be.bignumber.equal( + initialTakerBalanceA.plus(makerAmounts[0].times(makerAssetAmount)), + ); + expect(finalTakerBalanceB).to.be.bignumber.equal( + initialTakerBalanceB.plus(makerAmounts[1].times(makerAssetAmount)), + ); + expect(finalTakerZrxBalance).to.be.bignumber.equal( + initialTakerZrxBalance.minus(takerAmounts[0].times(takerAssetAmount)), + ); + expect(finalOwnerTakerAsset).to.be.equal(makerAddress); + }); + it('should allow an order selling multiple assets to be partially filled', async () => { + const makerAmounts = [new BigNumber(10), new BigNumber(20)]; + const makerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + assetDataUtils.encodeERC20AssetData(erc20TokenB.address), + ]; + const makerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + makerAmounts, + makerNestedAssetData, + ); + const makerAssetAmount = new BigNumber(30); + const takerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const takerAssetAmount = new BigNumber(10); + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + takerAssetData, + makerAssetAmount, + takerAssetAmount, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }); + + const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + const takerAssetFillAmount = takerAssetAmount.dividedToIntegerBy(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount, + }); + + const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal( + initialMakerBalanceA.minus( + makerAmounts[0].times( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalMakerBalanceB).to.be.bignumber.equal( + initialMakerBalanceB.minus( + makerAmounts[1].times( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalMakerZrxBalance).to.be.bignumber.equal( + initialMakerZrxBalance.plus( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ); + expect(finalTakerBalanceA).to.be.bignumber.equal( + initialTakerBalanceA.plus( + makerAmounts[0].times( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalTakerBalanceB).to.be.bignumber.equal( + initialTakerBalanceB.plus( + makerAmounts[1].times( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalTakerZrxBalance).to.be.bignumber.equal( + initialTakerZrxBalance.minus( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ); + }); + it('should allow an order buying multiple assets to be partially filled', async () => { + const takerAmounts = [new BigNumber(10), new BigNumber(20)]; + const takerNestedAssetData = [ + assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + assetDataUtils.encodeERC20AssetData(erc20TokenB.address), + ]; + const takerAssetData = assetDataInterface.MultiAsset.getABIEncodedTransactionData( + takerAmounts, + takerNestedAssetData, + ); + const takerAssetAmount = new BigNumber(30); + const makerAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + const makerAssetAmount = new BigNumber(10); + signedOrder = await orderFactory.newSignedOrderAsync({ + makerAssetData, + takerAssetData, + makerAssetAmount, + takerAssetAmount, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }); + + const initialMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const initialMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const initialMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const initialTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const initialTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const initialTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + const takerAssetFillAmount = takerAssetAmount.dividedToIntegerBy(2); + await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress, { + takerAssetFillAmount, + }); + + const finalMakerBalanceA = await erc20TokenA.balanceOf.callAsync(makerAddress); + const finalMakerBalanceB = await erc20TokenB.balanceOf.callAsync(makerAddress); + const finalMakerZrxBalance = await zrxToken.balanceOf.callAsync(makerAddress); + const finalTakerBalanceA = await erc20TokenA.balanceOf.callAsync(takerAddress); + const finalTakerBalanceB = await erc20TokenB.balanceOf.callAsync(takerAddress); + const finalTakerZrxBalance = await zrxToken.balanceOf.callAsync(takerAddress); + + expect(finalMakerBalanceA).to.be.bignumber.equal( + initialMakerBalanceA.plus( + takerAmounts[0].times( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalMakerBalanceB).to.be.bignumber.equal( + initialMakerBalanceB.plus( + takerAmounts[1].times( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalMakerZrxBalance).to.be.bignumber.equal( + initialMakerZrxBalance.minus( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ); + expect(finalTakerBalanceA).to.be.bignumber.equal( + initialTakerBalanceA.minus( + takerAmounts[0].times( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalTakerBalanceB).to.be.bignumber.equal( + initialTakerBalanceB.minus( + takerAmounts[1].times( + takerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ), + ); + expect(finalTakerZrxBalance).to.be.bignumber.equal( + initialTakerZrxBalance.plus( + makerAssetAmount.times(takerAssetFillAmount).dividedToIntegerBy(takerAssetAmount), + ), + ); + }); + }); + describe('getOrderInfo', () => { beforeEach(async () => { signedOrder = await orderFactory.newSignedOrderAsync(); diff --git a/packages/contracts/test/exchange/dispatcher.ts b/contracts/core/test/exchange/dispatcher.ts similarity index 100% rename from packages/contracts/test/exchange/dispatcher.ts rename to contracts/core/test/exchange/dispatcher.ts diff --git a/packages/contracts/test/exchange/fill_order.ts b/contracts/core/test/exchange/fill_order.ts similarity index 100% rename from packages/contracts/test/exchange/fill_order.ts rename to contracts/core/test/exchange/fill_order.ts diff --git a/packages/contracts/test/exchange/internal.ts b/contracts/core/test/exchange/internal.ts similarity index 100% rename from packages/contracts/test/exchange/internal.ts rename to contracts/core/test/exchange/internal.ts diff --git a/packages/contracts/test/exchange/libs.ts b/contracts/core/test/exchange/libs.ts similarity index 100% rename from packages/contracts/test/exchange/libs.ts rename to contracts/core/test/exchange/libs.ts diff --git a/packages/contracts/test/exchange/match_orders.ts b/contracts/core/test/exchange/match_orders.ts similarity index 100% rename from packages/contracts/test/exchange/match_orders.ts rename to contracts/core/test/exchange/match_orders.ts diff --git a/packages/contracts/test/exchange/signature_validator.ts b/contracts/core/test/exchange/signature_validator.ts similarity index 100% rename from packages/contracts/test/exchange/signature_validator.ts rename to contracts/core/test/exchange/signature_validator.ts diff --git a/packages/contracts/test/exchange/transactions.ts b/contracts/core/test/exchange/transactions.ts similarity index 100% rename from packages/contracts/test/exchange/transactions.ts rename to contracts/core/test/exchange/transactions.ts diff --git a/packages/contracts/test/exchange/wrapper.ts b/contracts/core/test/exchange/wrapper.ts similarity index 100% rename from packages/contracts/test/exchange/wrapper.ts rename to contracts/core/test/exchange/wrapper.ts diff --git a/packages/contracts/test/extensions/forwarder.ts b/contracts/core/test/extensions/forwarder.ts similarity index 100% rename from packages/contracts/test/extensions/forwarder.ts rename to contracts/core/test/extensions/forwarder.ts diff --git a/packages/contracts/test/extensions/order_validator.ts b/contracts/core/test/extensions/order_validator.ts similarity index 100% rename from packages/contracts/test/extensions/order_validator.ts rename to contracts/core/test/extensions/order_validator.ts diff --git a/packages/contracts/test/global_hooks.ts b/contracts/core/test/global_hooks.ts similarity index 100% rename from packages/contracts/test/global_hooks.ts rename to contracts/core/test/global_hooks.ts diff --git a/packages/contracts/test/libraries/lib_bytes.ts b/contracts/core/test/libraries/lib_bytes.ts similarity index 100% rename from packages/contracts/test/libraries/lib_bytes.ts rename to contracts/core/test/libraries/lib_bytes.ts diff --git a/packages/contracts/test/multisig/asset_proxy_owner.ts b/contracts/core/test/multisig/asset_proxy_owner.ts similarity index 100% rename from packages/contracts/test/multisig/asset_proxy_owner.ts rename to contracts/core/test/multisig/asset_proxy_owner.ts diff --git a/packages/contracts/test/multisig/multi_sig_with_time_lock.ts b/contracts/core/test/multisig/multi_sig_with_time_lock.ts similarity index 100% rename from packages/contracts/test/multisig/multi_sig_with_time_lock.ts rename to contracts/core/test/multisig/multi_sig_with_time_lock.ts diff --git a/packages/contracts/test/tokens/erc721_token.ts b/contracts/core/test/tokens/erc721_token.ts similarity index 100% rename from packages/contracts/test/tokens/erc721_token.ts rename to contracts/core/test/tokens/erc721_token.ts diff --git a/packages/contracts/test/tokens/unlimited_allowance_token.ts b/contracts/core/test/tokens/unlimited_allowance_token.ts similarity index 100% rename from packages/contracts/test/tokens/unlimited_allowance_token.ts rename to contracts/core/test/tokens/unlimited_allowance_token.ts diff --git a/packages/contracts/test/tokens/weth9.ts b/contracts/core/test/tokens/weth9.ts similarity index 100% rename from packages/contracts/test/tokens/weth9.ts rename to contracts/core/test/tokens/weth9.ts diff --git a/packages/contracts/test/tokens/zrx_token.ts b/contracts/core/test/tokens/zrx_token.ts similarity index 100% rename from packages/contracts/test/tokens/zrx_token.ts rename to contracts/core/test/tokens/zrx_token.ts diff --git a/packages/contracts/test/tutorials/arbitrage.ts b/contracts/core/test/tutorials/arbitrage.ts similarity index 100% rename from packages/contracts/test/tutorials/arbitrage.ts rename to contracts/core/test/tutorials/arbitrage.ts diff --git a/packages/contracts/test/utils/abstract_asset_wrapper.ts b/contracts/core/test/utils/abstract_asset_wrapper.ts similarity index 100% rename from packages/contracts/test/utils/abstract_asset_wrapper.ts rename to contracts/core/test/utils/abstract_asset_wrapper.ts diff --git a/packages/contracts/test/utils/address_utils.ts b/contracts/core/test/utils/address_utils.ts similarity index 100% rename from packages/contracts/test/utils/address_utils.ts rename to contracts/core/test/utils/address_utils.ts diff --git a/packages/contracts/test/utils/assertions.ts b/contracts/core/test/utils/assertions.ts similarity index 100% rename from packages/contracts/test/utils/assertions.ts rename to contracts/core/test/utils/assertions.ts diff --git a/packages/contracts/test/utils/asset_wrapper.ts b/contracts/core/test/utils/asset_wrapper.ts similarity index 100% rename from packages/contracts/test/utils/asset_wrapper.ts rename to contracts/core/test/utils/asset_wrapper.ts diff --git a/packages/contracts/test/utils/block_timestamp.ts b/contracts/core/test/utils/block_timestamp.ts similarity index 100% rename from packages/contracts/test/utils/block_timestamp.ts rename to contracts/core/test/utils/block_timestamp.ts diff --git a/packages/contracts/test/utils/chai_setup.ts b/contracts/core/test/utils/chai_setup.ts similarity index 100% rename from packages/contracts/test/utils/chai_setup.ts rename to contracts/core/test/utils/chai_setup.ts diff --git a/packages/contracts/test/utils/combinatorial_utils.ts b/contracts/core/test/utils/combinatorial_utils.ts similarity index 100% rename from packages/contracts/test/utils/combinatorial_utils.ts rename to contracts/core/test/utils/combinatorial_utils.ts diff --git a/packages/contracts/test/utils/constants.ts b/contracts/core/test/utils/constants.ts similarity index 98% rename from packages/contracts/test/utils/constants.ts rename to contracts/core/test/utils/constants.ts index cd21330e9d..d2c3ab512c 100644 --- a/packages/contracts/test/utils/constants.ts +++ b/contracts/core/test/utils/constants.ts @@ -35,7 +35,7 @@ export const constants = { DUMMY_TOKEN_TOTAL_SUPPLY: new BigNumber(0), NULL_BYTES: '0x', NUM_DUMMY_ERC20_TO_DEPLOY: 3, - NUM_DUMMY_ERC721_TO_DEPLOY: 1, + NUM_DUMMY_ERC721_TO_DEPLOY: 2, NUM_ERC721_TOKENS_TO_MINT: 2, NULL_ADDRESS: '0x0000000000000000000000000000000000000000', UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), diff --git a/packages/contracts/test/utils/coverage.ts b/contracts/core/test/utils/coverage.ts similarity index 100% rename from packages/contracts/test/utils/coverage.ts rename to contracts/core/test/utils/coverage.ts diff --git a/packages/contracts/test/utils/erc20_wrapper.ts b/contracts/core/test/utils/erc20_wrapper.ts similarity index 100% rename from packages/contracts/test/utils/erc20_wrapper.ts rename to contracts/core/test/utils/erc20_wrapper.ts diff --git a/packages/contracts/test/utils/erc721_wrapper.ts b/contracts/core/test/utils/erc721_wrapper.ts similarity index 97% rename from packages/contracts/test/utils/erc721_wrapper.ts rename to contracts/core/test/utils/erc721_wrapper.ts index 3ef4e701d2..e9da553d0a 100644 --- a/packages/contracts/test/utils/erc721_wrapper.ts +++ b/contracts/core/test/utils/erc721_wrapper.ts @@ -29,7 +29,8 @@ export class ERC721Wrapper { this._contractOwnerAddress = contractOwnerAddress; } public async deployDummyTokensAsync(): Promise { - for (let i = 0; i < constants.NUM_DUMMY_ERC721_TO_DEPLOY; i++) { + // tslint:disable-next-line:no-unused-variable + for (const i of _.times(constants.NUM_DUMMY_ERC721_TO_DEPLOY)) { this._dummyTokenContracts.push( await DummyERC721TokenContract.deployFrom0xArtifactAsync( artifacts.DummyERC721Token, @@ -61,7 +62,8 @@ export class ERC721Wrapper { this._initialTokenIdsByOwner = {}; for (const dummyTokenContract of this._dummyTokenContracts) { for (const tokenOwnerAddress of this._tokenOwnerAddresses) { - for (let i = 0; i < constants.NUM_ERC721_TOKENS_TO_MINT; i++) { + // tslint:disable-next-line:no-unused-variable + for (const i of _.times(constants.NUM_ERC721_TOKENS_TO_MINT)) { const tokenId = generatePseudoRandomSalt(); await this.mintAsync(dummyTokenContract.address, tokenId, tokenOwnerAddress); if (_.isUndefined(this._initialTokenIdsByOwner[tokenOwnerAddress])) { diff --git a/packages/contracts/test/utils/exchange_wrapper.ts b/contracts/core/test/utils/exchange_wrapper.ts similarity index 100% rename from packages/contracts/test/utils/exchange_wrapper.ts rename to contracts/core/test/utils/exchange_wrapper.ts diff --git a/packages/contracts/test/utils/fill_order_combinatorial_utils.ts b/contracts/core/test/utils/fill_order_combinatorial_utils.ts similarity index 100% rename from packages/contracts/test/utils/fill_order_combinatorial_utils.ts rename to contracts/core/test/utils/fill_order_combinatorial_utils.ts diff --git a/packages/contracts/test/utils/formatters.ts b/contracts/core/test/utils/formatters.ts similarity index 100% rename from packages/contracts/test/utils/formatters.ts rename to contracts/core/test/utils/formatters.ts diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/contracts/core/test/utils/forwarder_wrapper.ts similarity index 100% rename from packages/contracts/test/utils/forwarder_wrapper.ts rename to contracts/core/test/utils/forwarder_wrapper.ts diff --git a/packages/contracts/test/utils/log_decoder.ts b/contracts/core/test/utils/log_decoder.ts similarity index 100% rename from packages/contracts/test/utils/log_decoder.ts rename to contracts/core/test/utils/log_decoder.ts diff --git a/packages/contracts/test/utils/match_order_tester.ts b/contracts/core/test/utils/match_order_tester.ts similarity index 100% rename from packages/contracts/test/utils/match_order_tester.ts rename to contracts/core/test/utils/match_order_tester.ts diff --git a/packages/contracts/test/utils/multi_sig_wrapper.ts b/contracts/core/test/utils/multi_sig_wrapper.ts similarity index 100% rename from packages/contracts/test/utils/multi_sig_wrapper.ts rename to contracts/core/test/utils/multi_sig_wrapper.ts diff --git a/packages/contracts/test/utils/order_factory.ts b/contracts/core/test/utils/order_factory.ts similarity index 100% rename from packages/contracts/test/utils/order_factory.ts rename to contracts/core/test/utils/order_factory.ts diff --git a/packages/contracts/test/utils/order_factory_from_scenario.ts b/contracts/core/test/utils/order_factory_from_scenario.ts similarity index 100% rename from packages/contracts/test/utils/order_factory_from_scenario.ts rename to contracts/core/test/utils/order_factory_from_scenario.ts diff --git a/packages/contracts/test/utils/order_utils.ts b/contracts/core/test/utils/order_utils.ts similarity index 100% rename from packages/contracts/test/utils/order_utils.ts rename to contracts/core/test/utils/order_utils.ts diff --git a/packages/contracts/test/utils/profiler.ts b/contracts/core/test/utils/profiler.ts similarity index 100% rename from packages/contracts/test/utils/profiler.ts rename to contracts/core/test/utils/profiler.ts diff --git a/packages/contracts/test/utils/revert_trace.ts b/contracts/core/test/utils/revert_trace.ts similarity index 100% rename from packages/contracts/test/utils/revert_trace.ts rename to contracts/core/test/utils/revert_trace.ts diff --git a/packages/contracts/test/utils/signing_utils.ts b/contracts/core/test/utils/signing_utils.ts similarity index 100% rename from packages/contracts/test/utils/signing_utils.ts rename to contracts/core/test/utils/signing_utils.ts diff --git a/packages/contracts/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts b/contracts/core/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts similarity index 100% rename from packages/contracts/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts rename to contracts/core/test/utils/simple_asset_balance_and_proxy_allowance_fetcher.ts diff --git a/packages/contracts/test/utils/simple_order_filled_cancelled_fetcher.ts b/contracts/core/test/utils/simple_order_filled_cancelled_fetcher.ts similarity index 100% rename from packages/contracts/test/utils/simple_order_filled_cancelled_fetcher.ts rename to contracts/core/test/utils/simple_order_filled_cancelled_fetcher.ts diff --git a/packages/contracts/test/utils/test_with_reference.ts b/contracts/core/test/utils/test_with_reference.ts similarity index 100% rename from packages/contracts/test/utils/test_with_reference.ts rename to contracts/core/test/utils/test_with_reference.ts diff --git a/packages/contracts/test/utils/transaction_factory.ts b/contracts/core/test/utils/transaction_factory.ts similarity index 100% rename from packages/contracts/test/utils/transaction_factory.ts rename to contracts/core/test/utils/transaction_factory.ts diff --git a/packages/contracts/test/utils/type_encoding_utils.ts b/contracts/core/test/utils/type_encoding_utils.ts similarity index 100% rename from packages/contracts/test/utils/type_encoding_utils.ts rename to contracts/core/test/utils/type_encoding_utils.ts diff --git a/packages/contracts/test/utils/types.ts b/contracts/core/test/utils/types.ts similarity index 100% rename from packages/contracts/test/utils/types.ts rename to contracts/core/test/utils/types.ts diff --git a/packages/contracts/test/utils/web3_wrapper.ts b/contracts/core/test/utils/web3_wrapper.ts similarity index 100% rename from packages/contracts/test/utils/web3_wrapper.ts rename to contracts/core/test/utils/web3_wrapper.ts diff --git a/packages/contracts/test/utils_test/test_with_reference.ts b/contracts/core/test/utils_test/test_with_reference.ts similarity index 100% rename from packages/contracts/test/utils_test/test_with_reference.ts rename to contracts/core/test/utils_test/test_with_reference.ts diff --git a/packages/contracts/tsconfig.json b/contracts/core/tsconfig.json similarity index 97% rename from packages/contracts/tsconfig.json rename to contracts/core/tsconfig.json index 8b29365cc1..e0f85079a5 100644 --- a/packages/contracts/tsconfig.json +++ b/contracts/core/tsconfig.json @@ -26,6 +26,7 @@ "./generated-artifacts/IWallet.json", "./generated-artifacts/InvalidERC721Receiver.json", "./generated-artifacts/MixinAuthorizable.json", + "./generated-artifacts/MultiAssetProxy.json", "./generated-artifacts/MultiSigWallet.json", "./generated-artifacts/MultiSigWalletWithTimeLock.json", "./generated-artifacts/OrderValidator.json", diff --git a/packages/contracts/tslint.json b/contracts/core/tslint.json similarity index 100% rename from packages/contracts/tslint.json rename to contracts/core/tslint.json diff --git a/lerna.json b/lerna.json index e2147be85b..fa9ef5aff7 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "3.0.0-beta.23", - "packages": ["packages/*"], + "packages": ["packages/*", "contracts/*"], "version": "independent", "command": { "publish": { diff --git a/package.json b/package.json index 7307bea5d0..81208dcd2d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "node": ">=6.12" }, "workspaces": [ - "packages/*" + "packages/*", + "contracts/*" ], "scripts": { "ganache": "ganache-cli -p 8545 --gasLimit 10000000 --networkId 50 -m \"${npm_package_config_mnemonic}\"", diff --git a/packages/contracts/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts deleted file mode 100644 index b8305993ee..0000000000 --- a/packages/contracts/test/asset_proxy/proxies.ts +++ /dev/null @@ -1,629 +0,0 @@ -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { assetDataUtils } from '@0x/order-utils'; -import { RevertReason } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import * as _ from 'lodash'; - -import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token'; -import { DummyERC721ReceiverContract } from '../../generated-wrappers/dummy_erc721_receiver'; -import { DummyERC721TokenContract } from '../../generated-wrappers/dummy_erc721_token'; -import { DummyMultipleReturnERC20TokenContract } from '../../generated-wrappers/dummy_multiple_return_erc20_token'; -import { DummyNoReturnERC20TokenContract } from '../../generated-wrappers/dummy_no_return_erc20_token'; -import { ERC20ProxyContract } from '../../generated-wrappers/erc20_proxy'; -import { ERC721ProxyContract } from '../../generated-wrappers/erc721_proxy'; -import { IAssetProxyContract } from '../../generated-wrappers/i_asset_proxy'; -import { artifacts } from '../../src/artifacts'; -import { expectTransactionFailedAsync, expectTransactionFailedWithoutReasonAsync } from '../utils/assertions'; -import { chaiSetup } from '../utils/chai_setup'; -import { constants } from '../utils/constants'; -import { ERC20Wrapper } from '../utils/erc20_wrapper'; -import { ERC721Wrapper } from '../utils/erc721_wrapper'; -import { LogDecoder } from '../utils/log_decoder'; -import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const assetProxyInterface = new IAssetProxyContract( - artifacts.IAssetProxy.compilerOutput.abi, - constants.NULL_ADDRESS, - provider, -); - -// tslint:disable:no-unnecessary-type-assertion -describe('Asset Transfer Proxies', () => { - let owner: string; - let notAuthorized: string; - let exchangeAddress: string; - let makerAddress: string; - let takerAddress: string; - - let zrxToken: DummyERC20TokenContract; - let erc721Token: DummyERC721TokenContract; - let erc721Receiver: DummyERC721ReceiverContract; - let erc20Proxy: ERC20ProxyContract; - let erc721Proxy: ERC721ProxyContract; - let noReturnErc20Token: DummyNoReturnERC20TokenContract; - let multipleReturnErc20Token: DummyMultipleReturnERC20TokenContract; - - let erc20Wrapper: ERC20Wrapper; - let erc721Wrapper: ERC721Wrapper; - let erc721MakerTokenId: BigNumber; - - before(async () => { - await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - before(async () => { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - const usedAddresses = ([owner, notAuthorized, exchangeAddress, makerAddress, takerAddress] = _.slice( - accounts, - 0, - 5, - )); - - erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); - erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); - - const numDummyErc20ToDeploy = 1; - [zrxToken] = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS); - erc20Proxy = await erc20Wrapper.deployProxyAsync(); - await erc20Wrapper.setBalancesAndAllowancesAsync(); - await web3Wrapper.awaitTransactionSuccessAsync( - await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { - from: owner, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - - [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); - erc721Proxy = await erc721Wrapper.deployProxyAsync(); - await erc721Wrapper.setBalancesAndAllowancesAsync(); - const erc721Balances = await erc721Wrapper.getBalancesAsync(); - erc721MakerTokenId = erc721Balances[makerAddress][erc721Token.address][0]; - await web3Wrapper.awaitTransactionSuccessAsync( - await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { - from: owner, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync( - artifacts.DummyERC721Receiver, - provider, - txDefaults, - ); - noReturnErc20Token = await DummyNoReturnERC20TokenContract.deployFrom0xArtifactAsync( - artifacts.DummyNoReturnERC20Token, - provider, - txDefaults, - constants.DUMMY_TOKEN_NAME, - constants.DUMMY_TOKEN_SYMBOL, - constants.DUMMY_TOKEN_DECIMALS, - constants.DUMMY_TOKEN_TOTAL_SUPPLY, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await noReturnErc20Token.setBalance.sendTransactionAsync(makerAddress, constants.INITIAL_ERC20_BALANCE), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await noReturnErc20Token.approve.sendTransactionAsync( - erc20Proxy.address, - constants.INITIAL_ERC20_ALLOWANCE, - { from: makerAddress }, - ), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - multipleReturnErc20Token = await DummyMultipleReturnERC20TokenContract.deployFrom0xArtifactAsync( - artifacts.DummyMultipleReturnERC20Token, - provider, - txDefaults, - constants.DUMMY_TOKEN_NAME, - constants.DUMMY_TOKEN_SYMBOL, - constants.DUMMY_TOKEN_DECIMALS, - constants.DUMMY_TOKEN_TOTAL_SUPPLY, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await multipleReturnErc20Token.setBalance.sendTransactionAsync( - makerAddress, - constants.INITIAL_ERC20_BALANCE, - ), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await multipleReturnErc20Token.approve.sendTransactionAsync( - erc20Proxy.address, - constants.INITIAL_ERC20_ALLOWANCE, - { from: makerAddress }, - ), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('Transfer Proxy - ERC20', () => { - it('should revert if undefined function is called', async () => { - const undefinedSelector = '0x01020304'; - await expectTransactionFailedWithoutReasonAsync( - web3Wrapper.sendTransactionAsync({ - from: owner, - to: erc20Proxy.address, - value: constants.ZERO_AMOUNT, - data: undefinedSelector, - }), - ); - }); - describe('transferFrom', () => { - it('should successfully transfer tokens', async () => { - // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress - const erc20Balances = await erc20Wrapper.getBalancesAsync(); - const amount = new BigNumber(10); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await web3Wrapper.sendTransactionAsync({ - to: erc20Proxy.address, - data, - from: exchangeAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - // Verify transfer was successful - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][zrxToken.address].minus(amount), - ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address].add(amount), - ); - }); - - it('should successfully transfer tokens that do not return a value', async () => { - // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address); - // Perform a transfer from makerAddress to takerAddress - const initialMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); - const amount = new BigNumber(10); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await web3Wrapper.sendTransactionAsync({ - to: erc20Proxy.address, - data, - from: exchangeAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - // Verify transfer was successful - const newMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const newTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); - expect(newMakerBalance).to.be.bignumber.equal(initialMakerBalance.minus(amount)); - expect(newTakerBalance).to.be.bignumber.equal(initialTakerBalance.plus(amount)); - }); - - it('should successfully transfer tokens and ignore extra assetData', async () => { - // Construct ERC20 asset data - const extraData = '0102030405060708'; - const encodedAssetData = `${assetDataUtils.encodeERC20AssetData(zrxToken.address)}${extraData}`; - // Perform a transfer from makerAddress to takerAddress - const erc20Balances = await erc20Wrapper.getBalancesAsync(); - const amount = new BigNumber(10); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await web3Wrapper.sendTransactionAsync({ - to: erc20Proxy.address, - data, - from: exchangeAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - // Verify transfer was successful - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][zrxToken.address].minus(amount), - ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address].add(amount), - ); - }); - - it('should do nothing if transferring 0 amount of a token', async () => { - // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress - const erc20Balances = await erc20Wrapper.getBalancesAsync(); - const amount = new BigNumber(0); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await web3Wrapper.sendTransactionAsync({ - to: erc20Proxy.address, - data, - from: exchangeAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - // Verify transfer was successful - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][zrxToken.address], - ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address], - ); - }); - - it('should throw if allowances are too low', async () => { - // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Create allowance less than transfer amount. Set allowance on proxy. - const allowance = new BigNumber(0); - const amount = new BigNumber(10); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, allowance, { - from: makerAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - const erc20Balances = await erc20Wrapper.getBalancesAsync(); - // Perform a transfer; expect this to fail. - await expectTransactionFailedAsync( - web3Wrapper.sendTransactionAsync({ - to: erc20Proxy.address, - data, - from: exchangeAddress, - }), - RevertReason.TransferFailed, - ); - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances).to.deep.equal(erc20Balances); - }); - - it('should throw if allowances are too low and token does not return a value', async () => { - // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(noReturnErc20Token.address); - // Create allowance less than transfer amount. Set allowance on proxy. - const allowance = new BigNumber(0); - const amount = new BigNumber(10); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await noReturnErc20Token.approve.sendTransactionAsync(erc20Proxy.address, allowance, { - from: makerAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - const initialMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); - // Perform a transfer; expect this to fail. - await expectTransactionFailedAsync( - web3Wrapper.sendTransactionAsync({ - to: erc20Proxy.address, - data, - from: exchangeAddress, - }), - RevertReason.TransferFailed, - ); - const newMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const newTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); - expect(newMakerBalance).to.be.bignumber.equal(initialMakerBalance); - expect(newTakerBalance).to.be.bignumber.equal(initialTakerBalance); - }); - - it('should throw if requesting address is not authorized', async () => { - // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress - const amount = new BigNumber(10); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - const erc20Balances = await erc20Wrapper.getBalancesAsync(); - await expectTransactionFailedAsync( - web3Wrapper.sendTransactionAsync({ - to: erc20Proxy.address, - data, - from: notAuthorized, - }), - RevertReason.SenderNotAuthorized, - ); - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances).to.deep.equal(erc20Balances); - }); - - it('should throw if token returns more than 32 bytes', async () => { - // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(multipleReturnErc20Token.address); - const amount = new BigNumber(10); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - const initialMakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(takerAddress); - // Perform a transfer; expect this to fail. - await expectTransactionFailedAsync( - web3Wrapper.sendTransactionAsync({ - to: erc20Proxy.address, - data, - from: exchangeAddress, - }), - RevertReason.TransferFailed, - ); - const newMakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(makerAddress); - const newTakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(takerAddress); - expect(newMakerBalance).to.be.bignumber.equal(initialMakerBalance); - expect(newTakerBalance).to.be.bignumber.equal(initialTakerBalance); - }); - }); - - it('should have an id of 0xf47261b0', async () => { - const proxyId = await erc20Proxy.getProxyId.callAsync(); - const expectedProxyId = '0xf47261b0'; - expect(proxyId).to.equal(expectedProxyId); - }); - }); - - describe('Transfer Proxy - ERC721', () => { - it('should revert if undefined function is called', async () => { - const undefinedSelector = '0x01020304'; - await expectTransactionFailedWithoutReasonAsync( - web3Wrapper.sendTransactionAsync({ - from: owner, - to: erc721Proxy.address, - value: constants.ZERO_AMOUNT, - data: undefinedSelector, - }), - ); - }); - describe('transferFrom', () => { - it('should successfully transfer tokens', async () => { - // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress - const amount = new BigNumber(1); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await web3Wrapper.sendTransactionAsync({ - to: erc721Proxy.address, - data, - from: exchangeAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - // Verify transfer was successful - const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); - }); - - it('should successfully transfer tokens and ignore extra assetData', async () => { - // Construct ERC721 asset data - const extraData = '0102030405060708'; - const encodedAssetData = `${assetDataUtils.encodeERC721AssetData( - erc721Token.address, - erc721MakerTokenId, - )}${extraData}`; - // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress - const amount = new BigNumber(1); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await web3Wrapper.sendTransactionAsync({ - to: erc721Proxy.address, - data, - from: exchangeAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - // Verify transfer was successful - const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); - }); - - it('should not call onERC721Received when transferring to a smart contract', async () => { - // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress - const amount = new BigNumber(1); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - erc721Receiver.address, - amount, - ); - const logDecoder = new LogDecoder(web3Wrapper); - const tx = await logDecoder.getTxWithDecodedLogsAsync( - await web3Wrapper.sendTransactionAsync({ - to: erc721Proxy.address, - data, - from: exchangeAddress, - gas: constants.MAX_TRANSFER_FROM_GAS, - }), - ); - // Verify that no log was emitted by erc721 receiver - expect(tx.logs.length).to.be.equal(1); - // Verify transfer was successful - const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(erc721Receiver.address); - }); - - it('should throw if transferring 0 amount of a token', async () => { - // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress - const amount = new BigNumber(0); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await expectTransactionFailedAsync( - web3Wrapper.sendTransactionAsync({ - to: erc721Proxy.address, - data, - from: exchangeAddress, - }), - RevertReason.InvalidAmount, - ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); - }); - - it('should throw if transferring > 1 amount of a token', async () => { - // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress - const amount = new BigNumber(500); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await expectTransactionFailedAsync( - web3Wrapper.sendTransactionAsync({ - to: erc721Proxy.address, - data, - from: exchangeAddress, - }), - RevertReason.InvalidAmount, - ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); - }); - - it('should throw if allowances are too low', async () => { - // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Remove transfer approval for makerAddress. - await web3Wrapper.awaitTransactionSuccessAsync( - await erc721Token.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721MakerTokenId, { - from: makerAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - // Perform a transfer; expect this to fail. - const amount = new BigNumber(1); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await expectTransactionFailedAsync( - web3Wrapper.sendTransactionAsync({ - to: erc721Proxy.address, - data, - from: exchangeAddress, - }), - RevertReason.TransferFailed, - ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); - }); - - it('should throw if requesting address is not authorized', async () => { - // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); - // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress - const amount = new BigNumber(1); - const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( - encodedAssetData, - makerAddress, - takerAddress, - amount, - ); - await expectTransactionFailedAsync( - web3Wrapper.sendTransactionAsync({ - to: erc721Proxy.address, - data, - from: notAuthorized, - }), - RevertReason.SenderNotAuthorized, - ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); - }); - }); - - it('should have an id of 0x02571792', async () => { - const proxyId = await erc721Proxy.getProxyId.callAsync(); - const expectedProxyId = '0x02571792'; - expect(proxyId).to.equal(expectedProxyId); - }); - }); -}); -// tslint:enable:no-unnecessary-type-assertion -// tslint:disable:max-file-line-count diff --git a/packages/ethereum-types/src/index.ts b/packages/ethereum-types/src/index.ts index eff38711aa..9430fdc98b 100644 --- a/packages/ethereum-types/src/index.ts +++ b/packages/ethereum-types/src/index.ts @@ -283,6 +283,11 @@ export interface RawLogEntry { export enum SolidityTypes { Address = 'address', + Bool = 'bool', + Bytes = 'bytes', + Int = 'int', + String = 'string', + Tuple = 'tuple', Uint256 = 'uint256', Uint8 = 'uint8', Uint = 'uint', diff --git a/packages/instant/.dogfood.discharge.json b/packages/instant/.dogfood.discharge.json index ca36b3861d..5d6a09a16f 100644 --- a/packages/instant/.dogfood.discharge.json +++ b/packages/instant/.dogfood.discharge.json @@ -1,12 +1,12 @@ { "domain": "0x-instant-dogfood", - "build_command": "WEBPACK_OUTPUT_PATH=public yarn build:umd:prod", + "build_command": "WEBPACK_OUTPUT_PATH=public dotenv yarn build --env.discharge_target=dogfood", "upload_directory": "public", "index_key": "index.html", "error_key": "index.html", "trailing_slashes": true, "cache": 3600, - "aws_profile": "default", + "aws_profile": "0xproject", "aws_region": "us-east-1", "cdn": false, "dns_configured": true diff --git a/packages/instant/.env_example b/packages/instant/.env_example new file mode 100644 index 0000000000..ebbbebc06e --- /dev/null +++ b/packages/instant/.env_example @@ -0,0 +1,4 @@ +INSTANT_ROLLBAR_PUBLISH_TOKEN= +INSTANT_ROLLBAR_CLIENT_TOKEN= +INSTANT_HEAP_ANALYTICS_ID_PRODUCTION= +INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT= \ No newline at end of file diff --git a/packages/instant/.gitignore b/packages/instant/.gitignore index a99cea187d..2e65f192d4 100644 --- a/packages/instant/.gitignore +++ b/packages/instant/.gitignore @@ -1,3 +1,4 @@ public/instant.js public/instant.js.map -umd/* \ No newline at end of file +umd/* +.env \ No newline at end of file diff --git a/packages/instant/.npmignore b/packages/instant/.npmignore index a4f7810c03..563923652e 100644 --- a/packages/instant/.npmignore +++ b/packages/instant/.npmignore @@ -2,4 +2,5 @@ * */ !lib/**/* -!umd/**/* \ No newline at end of file +!umd/**/* +.env \ No newline at end of file diff --git a/packages/instant/.production.discharge.json b/packages/instant/.production.discharge.json new file mode 100644 index 0000000000..947f68b1ae --- /dev/null +++ b/packages/instant/.production.discharge.json @@ -0,0 +1,13 @@ +{ + "domain": "instant.0xproject.com", + "build_command": "dotenv yarn build --env.discharge_target=production", + "upload_directory": "umd", + "index_key": "instant.js", + "error_key": "404.html", + "trailing_slashes": true, + "cache": 3600, + "aws_profile": "0xproject", + "aws_region": "us-east-1", + "cdn": true, + "dns_configured": true +} diff --git a/packages/instant/.staging.discharge.json b/packages/instant/.staging.discharge.json index c917a650b6..bd5f28ba84 100644 --- a/packages/instant/.staging.discharge.json +++ b/packages/instant/.staging.discharge.json @@ -1,12 +1,12 @@ { "domain": "0x-instant-staging", - "build_command": "WEBPACK_OUTPUT_PATH=public yarn build:umd:prod", + "build_command": "dotenv WEBPACK_OUTPUT_PATH=public yarn build --env.discharge_target=staging", "upload_directory": "public", "index_key": "index.html", "error_key": "index.html", "trailing_slashes": true, "cache": 3600, - "aws_profile": "default", + "aws_profile": "0xproject", "aws_region": "us-east-1", "cdn": false, "dns_configured": true diff --git a/packages/instant/README.md b/packages/instant/README.md index b83a10508c..2092b45d97 100644 --- a/packages/instant/README.md +++ b/packages/instant/README.md @@ -2,39 +2,11 @@ ## Installation -```bash -yarn add @0x/instant -``` - -**Import** - -**CommonJS module** - -```typescript -import { ZeroExInstant } from '@0x/instant'; -``` - -or - -```javascript -var ZeroExInstant = require('@0x/instant').ZeroExInstant; -``` - -If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`: - -```json -"compilerOptions": { - "typeRoots": ["node_modules/@0x/typescript-typings/types", "node_modules/@types"], -} -``` - -**UMD Module** - -The package is also available as a UMD module named `zeroExInstant`. +The package is available as a UMD module named `zeroExInstant` at https://instant.0xproject.com/instant.js. ```html - +
@@ -48,23 +20,33 @@ The package is also available as a UMD module named `zeroExInstant`. ## Deploying -You can deploy a work-in-progress version of 0x Instant at http://0x-instant-dogfood.s3-website-us-east-1.amazonaws.com for easy sharing. +To run any of the following commands you need to configure your `.env` file. There is an example `.env_example` file to show you what values are required. -To build and deploy the site run +You can deploy a work-in-progress version of 0x Instant at http://0x-instant-dogfood.s3-website-us-east-1.amazonaws.com/instant.js for easy sharing. + +To build and deploy the bundle run ``` yarn deploy_dogfood ``` -We also have a staging bucket that is to be updated less frequently can be used to share instant externally: http://0x-instant-staging.s3-website-us-east-1.amazonaws.com/ +We also have a staging bucket that is to be updated less frequently can be used to share a beta version of instant externally: http://0x-instant-staging.s3-website-us-east-1.amazonaws.com/instant.js -To build and deploy to this bucket, run +To build and deploy to this bundle, run ``` yarn deploy_staging ``` -**NOTE: On deploying the site, it will say the site is available at a non-existent URL. Please ignore and use the (now updated) URL above.** +Finally, we have our live production bundle that is only meant to be updated with stable, polished releases: https://instant.0xproject.com/instant.js + +To build and deploy to this bundle, run + +``` +yarn deploy_production +``` + +**NOTE: On deploying the site to staging and dogfood, it will say the site is available at a non-existent URL. Please ignore and use the (now updated) URL above.** ## Contributing diff --git a/packages/instant/package.json b/packages/instant/package.json index c75e2408df..7d0bf6beca 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -6,15 +6,10 @@ }, "private": true, "description": "0x Instant React Component", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "umd/instant.js", "scripts": { - "build": "yarn build:all", - "build:all": "run-p build:umd:prod build:commonjs", - "build:umd:prod": "webpack --mode production", - "build:commonjs": "tsc -b", + "build": "webpack --mode production", "build:ci": "yarn build", - "watch_without_deps": "tsc -w", "dev": "webpack-dev-server --mode development", "lint": "tslint --format stylish --project .", "test": "jest", @@ -24,6 +19,7 @@ "clean": "shx rm -rf lib coverage scripts", "deploy_dogfood": "discharge deploy -c .dogfood.discharge.json", "deploy_staging": "discharge deploy -c .staging.discharge.json", + "deploy_production": "discharge deploy -c .production.discharge.json", "manual:postpublish": "yarn build; node ./scripts/postpublish.js" }, "config": { @@ -64,6 +60,7 @@ "react-redux": "^5.0.7", "redux": "^4.0.0", "redux-devtools-extension": "^2.13.5", + "rollbar": "^2.5.0", "styled-components": "^4.0.2", "ts-optchain": "^0.1.1" }, @@ -81,6 +78,7 @@ "@types/redux": "^3.6.0", "@types/styled-components": "^4.0.1", "awesome-typescript-loader": "^5.2.1", + "dotenv-cli": "^1.4.0", "enzyme": "^3.6.0", "enzyme-adapter-react-16": "^1.5.0", "ip": "^1.1.5", @@ -88,7 +86,9 @@ "make-promises-safe": "^1.1.0", "npm-run-all": "^4.1.2", "nyc": "^11.0.1", + "rollbar-sourcemap-webpack-plugin": "^2.4.0", "shx": "^0.2.2", + "source-map-loader": "^0.2.4", "svg-react-loader": "^0.4.6", "ts-jest": "^23.10.3", "tslint": "5.11.0", @@ -99,6 +99,6 @@ "webpack-dev-server": "^3.1.9" }, "publishConfig": { - "access": "public" + "access": "private" } } diff --git a/packages/instant/public/index.html b/packages/instant/public/index.html index df39994ef0..d10618c58f 100644 --- a/packages/instant/public/index.html +++ b/packages/instant/public/index.html @@ -175,6 +175,7 @@ defaultSelectedAssetData: queryParams.getQueryParamValue('defaultSelectedAssetData'), affiliateInfo: affiliateInfoOverride, shouldDisablePushToHistory: !!queryParams.getQueryParamValue('shouldDisablePushToHistory'), + walletDisplayName: queryParams.getQueryParamValue('walletDisplayName') || undefined, }; return renderOptionsOverrides; }; diff --git a/packages/instant/src/assets/icons/zrx.svg b/packages/instant/src/assets/icons/zrx.svg index 07518f551c..da623710b3 100644 --- a/packages/instant/src/assets/icons/zrx.svg +++ b/packages/instant/src/assets/icons/zrx.svg @@ -1,3 +1,6 @@ - - + + + + + diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index eeb42d6fc4..1489b94d40 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -1,4 +1,5 @@ import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { AssetProxyId } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; @@ -7,7 +8,7 @@ import { oc } from 'ts-optchain'; import { WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants'; import { ColorOption } from '../style/theme'; -import { AffiliateInfo, ZeroExInstantError } from '../types'; +import { AffiliateInfo, Asset, ZeroExInstantError } from '../types'; import { analytics } from '../util/analytics'; import { gasPriceEstimator } from '../util/gas_price_estimator'; import { util } from '../util/util'; @@ -21,6 +22,7 @@ export interface BuyButtonProps { assetBuyer: AssetBuyer; web3Wrapper: Web3Wrapper; affiliateInfo?: AffiliateInfo; + selectedAsset?: Asset; onValidationPending: (buyQuote: BuyQuote) => void; onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; onSignatureDenied: (buyQuote: BuyQuote) => void; @@ -36,8 +38,12 @@ export class BuyButton extends React.Component { onBuyFailure: util.boundNoop, }; public render(): React.ReactNode { - const { buyQuote, accountAddress } = this.props; + const { buyQuote, accountAddress, selectedAsset } = this.props; const shouldDisableButton = _.isUndefined(buyQuote) || _.isUndefined(accountAddress); + const buttonText = + !_.isUndefined(selectedAsset) && selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20 + ? `Buy ${selectedAsset.metaData.symbol.toUpperCase()}` + : 'Buy Now'; return ( ); } diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx index e563bec73d..833818900b 100644 --- a/packages/instant/src/components/buy_order_state_buttons.tsx +++ b/packages/instant/src/components/buy_order_state_buttons.tsx @@ -4,7 +4,7 @@ import { Web3Wrapper } from '@0x/web3-wrapper'; import * as React from 'react'; import { ColorOption } from '../style/theme'; -import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types'; +import { AffiliateInfo, Asset, OrderProcessState, ZeroExInstantError } from '../types'; import { BuyButton } from './buy_button'; import { PlacingOrderButton } from './placing_order_button'; @@ -21,6 +21,7 @@ export interface BuyOrderStateButtonProps { assetBuyer: AssetBuyer; web3Wrapper: Web3Wrapper; affiliateInfo?: AffiliateInfo; + selectedAsset?: Asset; onViewTransaction: () => void; onValidationPending: (buyQuote: BuyQuote) => void; onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; @@ -60,6 +61,7 @@ export const BuyOrderStateButtons: React.StatelessComponent { public state: ERC20TokenSelectorState = { - searchQuery: undefined, + searchQuery: '', }; public render(): React.ReactNode { const { tokens, onTokenSelect } = this.props; @@ -62,10 +62,10 @@ export class ERC20TokenSelector extends React.Component }; private readonly _isTokenQueryMatch = (token: ERC20Asset): boolean => { const { searchQuery } = this.state; - if (_.isUndefined(searchQuery)) { + const searchQueryLowerCase = searchQuery.toLowerCase().trim(); + if (searchQueryLowerCase === '') { return true; } - const searchQueryLowerCase = searchQuery.toLowerCase(); const tokenName = token.metaData.name.toLowerCase(); const tokenSymbol = token.metaData.symbol.toLowerCase(); return _.startsWith(tokenSymbol, searchQueryLowerCase) || _.startsWith(tokenName, searchQueryLowerCase); diff --git a/packages/instant/src/components/install_wallet_panel_content.tsx b/packages/instant/src/components/install_wallet_panel_content.tsx index 88c26f59cf..481d82da0c 100644 --- a/packages/instant/src/components/install_wallet_panel_content.tsx +++ b/packages/instant/src/components/install_wallet_panel_content.tsx @@ -8,7 +8,9 @@ import { } from '../constants'; import { ColorOption } from '../style/theme'; import { Browser } from '../types'; +import { analytics } from '../util/analytics'; import { envUtil } from '../util/env'; +import { util } from '../util/util'; import { MetaMaskLogo } from './meta_mask_logo'; import { StandardPanelContent, StandardPanelContentProps } from './standard_panel_content'; @@ -45,6 +47,10 @@ export class InstallWalletPanelContent extends React.Component { + analytics.trackInstallWalletModalClickedGet(); + util.createOpenUrlInNewWindow(actionUrl)(); + }; return { image: , title: 'Install MetaMask', @@ -52,10 +58,11 @@ export class InstallWalletPanelContent extends React.Component { private readonly _renderEthAmount = (): React.ReactNode => { return ( - + {format.ethBaseUnitAmount( this.props.totalEthBaseUnitAmount, 4, @@ -119,7 +126,7 @@ export class InstantHeading extends React.Component { private readonly _renderDollarAmount = (): React.ReactNode => { return ( - + {format.ethBaseUnitAmountInUsd( this.props.totalEthBaseUnitAmount, this.props.ethUsdPrice, diff --git a/packages/instant/src/components/payment_method.tsx b/packages/instant/src/components/payment_method.tsx index ebcd62f35b..4efe5b28e2 100644 --- a/packages/instant/src/components/payment_method.tsx +++ b/packages/instant/src/components/payment_method.tsx @@ -18,7 +18,7 @@ import { WalletPrompt } from './wallet_prompt'; export interface PaymentMethodProps { account: Account; network: Network; - walletName: string; + walletDisplayName: string; onInstallWalletClick: () => void; onUnlockWalletClick: () => void; } @@ -26,7 +26,7 @@ export interface PaymentMethodProps { export class PaymentMethod extends React.Component { public render(): React.ReactNode { return ( - + { if (account.state === AccountState.Ready || account.state === AccountState.Locked) { const circleColor: ColorOption = account.state === AccountState.Ready ? ColorOption.green : ColorOption.red; return ( - + - - - {this.props.walletName} + + + {this.props.walletDisplayName} @@ -83,8 +83,7 @@ export class PaymentMethod extends React.Component { const colors = { primaryColor, secondaryColor }; switch (account.state) { case AccountState.Loading: - // Just take up the same amount of space as the other states. - return ; + return null; case AccountState.Locked: return ( { image={} {...colors} > - Please Unlock {this.props.walletName} + Please Unlock {this.props.walletDisplayName} ); case AccountState.None: diff --git a/packages/instant/src/components/scaling_input.tsx b/packages/instant/src/components/scaling_input.tsx index 129162a749..791692257f 100644 --- a/packages/instant/src/components/scaling_input.tsx +++ b/packages/instant/src/components/scaling_input.tsx @@ -98,6 +98,12 @@ export class ScalingInput extends React.Component = props => ( - - - - + + + + ); diff --git a/packages/instant/src/components/standard_panel_content.tsx b/packages/instant/src/components/standard_panel_content.tsx index 582b3318e0..79b7bff24b 100644 --- a/packages/instant/src/components/standard_panel_content.tsx +++ b/packages/instant/src/components/standard_panel_content.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { ColorOption } from '../style/theme'; +import { util } from '../util/util'; import { Container } from './ui/container'; import { Flex } from './ui/flex'; @@ -9,6 +10,7 @@ import { Text } from './ui/text'; export interface MoreInfoSettings { text: string; href: string; + onClick?: () => void; } export interface StandardPanelContentProps { @@ -21,6 +23,15 @@ export interface StandardPanelContentProps { const SPACING_BETWEEN_PX = '20px'; +const onMoreInfoClick = (href: string, onClick?: () => void) => { + return () => { + if (onClick) { + onClick(); + } + util.createOpenUrlInNewWindow(href)(); + }; +}; + export const StandardPanelContent: React.StatelessComponent = ({ image, title, @@ -50,7 +61,7 @@ export const StandardPanelContent: React.StatelessComponent {moreInfoSettings.text} diff --git a/packages/instant/src/components/standard_sliding_panel.tsx b/packages/instant/src/components/standard_sliding_panel.tsx index f587ff79a9..9f517d273c 100644 --- a/packages/instant/src/components/standard_sliding_panel.tsx +++ b/packages/instant/src/components/standard_sliding_panel.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { SlideAnimationState, StandardSlidingPanelContent, StandardSlidingPanelSettings } from '../types'; +import { StandardSlidingPanelContent, StandardSlidingPanelSettings } from '../types'; import { InstallWalletPanelContent } from './install_wallet_panel_content'; import { SlidingPanel } from './sliding_panel'; diff --git a/packages/instant/src/components/timed_progress_bar.tsx b/packages/instant/src/components/timed_progress_bar.tsx index 8465b9cd0a..fb3927088f 100644 --- a/packages/instant/src/components/timed_progress_bar.tsx +++ b/packages/instant/src/components/timed_progress_bar.tsx @@ -1,8 +1,9 @@ import * as _ from 'lodash'; +import { transparentize } from 'polished'; import * as React from 'react'; import { PROGRESS_FINISH_ANIMATION_TIME_MS, PROGRESS_STALL_AT_WIDTH } from '../constants'; -import { ColorOption, css, keyframes, styled } from '../style/theme'; +import { ColorOption, css, keyframes, styled, ThemeConsumer } from '../style/theme'; import { Container } from './ui/container'; @@ -93,8 +94,16 @@ export interface ProgressBarProps extends ProgressProps {} export const ProgressBar: React.ComponentType> = React.forwardRef( (props, ref) => ( - - - + + {theme => ( + + + + )} + ), ); diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index 4dafe13862..636eb8fc9a 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -27,7 +27,9 @@ export interface ContainerProps { borderBottom?: string; className?: string; backgroundColor?: ColorOption; + rawBackgroundColor?: string; hasBoxShadow?: boolean; + isHidden?: boolean; zIndex?: number; whiteSpace?: string; opacity?: number; @@ -38,6 +40,16 @@ export interface ContainerProps { flexGrow?: string | number; } +const getBackgroundColor = (theme: any, backgroundColor?: ColorOption, rawBackgroundColor?: string): string => { + if (backgroundColor) { + return theme[backgroundColor] as string; + } + if (rawBackgroundColor) { + return rawBackgroundColor; + } + return 'none'; +}; + export const Container = styled.div < ContainerProps > @@ -65,12 +77,14 @@ export const Container = ${props => cssRuleIfExists(props, 'opacity')} ${props => cssRuleIfExists(props, 'cursor')} ${props => cssRuleIfExists(props, 'overflow')} + ${props => (props.overflow === 'scroll' ? `-webkit-overflow-scrolling: touch` : '')}; ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; ${props => props.display && stylesForMedia('display', props.display)} ${props => props.width && stylesForMedia('width', props.width)} ${props => props.height && stylesForMedia('height', props.height)} ${props => props.borderRadius && stylesForMedia('border-radius', props.borderRadius)} - background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; + ${props => (props.isHidden ? 'visibility: hidden;' : '')} + background-color: ${props => getBackgroundColor(props.theme, props.backgroundColor, props.rawBackgroundColor)}; border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')}; &:hover { ${props => diff --git a/packages/instant/src/components/ui/input.tsx b/packages/instant/src/components/ui/input.tsx index 1ea5d8fe16..863c970ef7 100644 --- a/packages/instant/src/components/ui/input.tsx +++ b/packages/instant/src/components/ui/input.tsx @@ -10,6 +10,7 @@ export interface InputProps { fontSize?: string; fontColor?: ColorOption; placeholder?: string; + type?: string; onChange?: (event: React.ChangeEvent) => void; } diff --git a/packages/instant/src/components/ui/overlay.tsx b/packages/instant/src/components/ui/overlay.tsx index f67d6fb2fa..7d311dc2f3 100644 --- a/packages/instant/src/components/ui/overlay.tsx +++ b/packages/instant/src/components/ui/overlay.tsx @@ -33,7 +33,7 @@ export const Overlay = Overlay.defaultProps = { zIndex: zIndex.overlayDefault, - backgroundColor: generateOverlayBlack(0.6), + backgroundColor: generateOverlayBlack(0.9), }; Overlay.displayName = 'Overlay'; diff --git a/packages/instant/src/components/ui/text.tsx b/packages/instant/src/components/ui/text.tsx index 8e573d7b94..2824777588 100644 --- a/packages/instant/src/components/ui/text.tsx +++ b/packages/instant/src/components/ui/text.tsx @@ -1,4 +1,3 @@ -import { darken } from 'polished'; import * as React from 'react'; import { ColorOption, styled } from '../../style/theme'; @@ -31,7 +30,7 @@ export const Text: React.StatelessComponent = ({ href, onClick, ...re return ; }; -const darkenOnHoverAmount = 0.3; +const opacityOnHoverAmount = 0.5; export const StyledText = styled.div < TextProps > @@ -56,8 +55,7 @@ export const StyledText = ${props => (props.textAlign ? `text-align: ${props.textAlign}` : '')}; ${props => (props.width ? `width: ${props.width}` : '')}; &:hover { - ${props => - props.onClick ? `color: ${darken(darkenOnHoverAmount, props.theme[props.fontColor || 'white'])}` : ''}; + ${props => (props.onClick ? `opacity: ${opacityOnHoverAmount};` : '')}; } } `; diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index a57dd7993a..bdc5316177 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -11,10 +11,18 @@ import { asyncData } from '../redux/async_data'; import { DEFAULT_STATE, DefaultState, State } from '../redux/reducer'; import { store, Store } from '../redux/store'; import { fonts } from '../style/fonts'; -import { AccountState, AffiliateInfo, AssetMetaData, Network, ZeroExInstantBaseConfig } from '../types'; +import { + AccountState, + AffiliateInfo, + AssetMetaData, + Network, + QuoteFetchOrigin, + ZeroExInstantBaseConfig, +} from '../types'; import { analytics, disableAnalytics } from '../util/analytics'; import { assetUtils } from '../util/asset'; import { errorFlasher } from '../util/error_flasher'; +import { setupRollbar } from '../util/error_reporter'; import { gasPriceEstimator } from '../util/gas_price_estimator'; import { Heartbeater } from '../util/heartbeater'; import { generateAccountHeartbeater, generateBuyQuoteHeartbeater } from '../util/heartbeater_factory'; @@ -50,6 +58,7 @@ export class ZeroExInstantProvider extends React.Component ({ network: state.network, providerState: state.providerState, + walletDisplayName: state.walletDisplayName, }); const mapDispatchToProps = ( @@ -56,27 +58,32 @@ const mergeProps = ( ...ownProps, network: connectedState.network, account: connectedState.providerState.account, - walletName: connectedState.providerState.name, + walletDisplayName: connectedState.walletDisplayName || connectedState.providerState.name, onUnlockWalletClick: () => connectedDispatch.unlockWalletAndDispatchToStore(connectedState.providerState), onInstallWalletClick: () => { const isMobile = envUtil.isMobileOperatingSystem(); - if (!isMobile) { + const walletSuggestion: WalletSuggestion = isMobile + ? WalletSuggestion.CoinbaseWallet + : WalletSuggestion.MetaMask; + + analytics.trackInstallWalletClicked(walletSuggestion); + if (walletSuggestion === WalletSuggestion.MetaMask) { connectedDispatch.openInstallWalletPanel(); - return; + } else { + const operatingSystem = envUtil.getOperatingSystem(); + let url = COINBASE_WALLET_SITE_URL; + switch (operatingSystem) { + case OperatingSystem.Android: + url = COINBASE_WALLET_ANDROID_APP_STORE_URL; + break; + case OperatingSystem.iOS: + url = COINBASE_WALLET_IOS_APP_STORE_URL; + break; + default: + break; + } + window.open(url, '_blank'); } - const operatingSystem = envUtil.getOperatingSystem(); - let url = COINBASE_WALLET_SITE_URL; - switch (operatingSystem) { - case OperatingSystem.Android: - url = COINBASE_WALLET_ANDROID_APP_STORE_URL; - break; - case OperatingSystem.iOS: - url = COINBASE_WALLET_IOS_APP_STORE_URL; - break; - default: - break; - } - window.open(url, '_blank'); }, }); diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx index b7cfdb5044..0d4349124a 100644 --- a/packages/instant/src/containers/latest_error.tsx +++ b/packages/instant/src/containers/latest_error.tsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { SlidingError } from '../components/sliding_error'; +import { Container } from '../components/ui/container'; import { Overlay } from '../components/ui/overlay'; import { Action } from '../redux/actions'; import { State } from '../redux/reducer'; @@ -23,7 +24,12 @@ export interface LatestErrorComponentProps { export const LatestErrorComponent: React.StatelessComponent = props => { if (!props.latestErrorMessage) { - return
; + // Render a hidden SlidingError such that instant does not move when a real error is rendered. + return ( + + + + ); } return ( diff --git a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts index 610335243e..80943a96f6 100644 --- a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts +++ b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts @@ -9,7 +9,8 @@ import { Dispatch } from 'redux'; import { BuyOrderStateButtons } from '../components/buy_order_state_buttons'; import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; -import { AccountState, AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types'; +import { AccountState, AffiliateInfo, Asset, OrderProcessState, ZeroExInstantError } from '../types'; +import { analytics } from '../util/analytics'; import { errorFlasher } from '../util/error_flasher'; import { etherscanUtil } from '../util/etherscan'; @@ -21,6 +22,7 @@ interface ConnectedState { assetBuyer: AssetBuyer; web3Wrapper: Web3Wrapper; affiliateInfo?: AffiliateInfo; + selectedAsset?: Asset; onViewTransaction: () => void; } @@ -40,6 +42,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButt const account = state.providerState.account; const accountAddress = account.state === AccountState.Ready ? account.address : undefined; const accountEthBalanceInWei = account.state === AccountState.Ready ? account.ethBalanceInWei : undefined; + const selectedAsset = state.selectedAsset; return { accountAddress, accountEthBalanceInWei, @@ -48,6 +51,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButt web3Wrapper, buyQuote: state.latestBuyQuote, affiliateInfo: state.affiliateInfo, + selectedAsset, onViewTransaction: () => { if ( state.buyOrderState.processState === OrderProcessState.Processing || @@ -59,6 +63,8 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButt assetBuyer.networkId, ); if (etherscanUrl) { + analytics.trackTransactionViewed(state.buyOrderState.processState); + window.open(etherscanUrl, '_blank'); return; } diff --git a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts index a39bc46a24..cb9df527eb 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -10,7 +10,7 @@ import { ERC20AssetAmountInput, ERC20AssetAmountInputProps } from '../components import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; -import { AffiliateInfo, ERC20Asset, Omit, OrderProcessState } from '../types'; +import { AffiliateInfo, ERC20Asset, Omit, OrderProcessState, QuoteFetchOrigin } from '../types'; import { buyQuoteUpdater } from '../util/buy_quote_updater'; export interface SelectedERC20AssetAmountInputProps { @@ -88,7 +88,7 @@ const mapDispatchToProps = ( // even if it's debounced, give them the illusion it's loading dispatch(actions.setQuoteRequestStatePending()); // tslint:disable-next-line:no-floating-promises - debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value, { + debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value, QuoteFetchOrigin.Manual, { setPending: true, dispatchErrors: true, affiliateInfo, diff --git a/packages/instant/src/data/asset_meta_data_map.ts b/packages/instant/src/data/asset_meta_data_map.ts index b24c9c83d6..0553be7f5c 100644 --- a/packages/instant/src/data/asset_meta_data_map.ts +++ b/packages/instant/src/data/asset_meta_data_map.ts @@ -83,14 +83,14 @@ export const assetMetaDataMap: ObjectMap = { '0xf47261b0000000000000000000000000e0b7927c4af23765cb51314a0e0521a9645f0e2a': { assetProxyId: AssetProxyId.ERC20, decimals: 9, - primaryColor: '#DEB564', + primaryColor: '#E1AA3E', symbol: 'dgd', name: 'DigixDao', }, '0xf47261b00000000000000000000000004f3afec4e5a3f2a6a1a411def7d7dfe50ee057bf': { assetProxyId: AssetProxyId.ERC20, decimals: 9, - primaryColor: '#DEB564', + primaryColor: '#E1AA3E', symbol: 'dgx', name: 'Digix Gold Token', }, diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index 3a8694d6a8..b92fa3a7ca 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -4,6 +4,7 @@ import * as ReactDOM from 'react-dom'; import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR, INJECTED_DIV_CLASS, INJECTED_DIV_ID } from './constants'; import { ZeroExInstantOverlay, ZeroExInstantOverlayProps } from './index'; +import { analytics } from './util/analytics'; import { assert } from './util/assert'; import { util } from './util/util'; @@ -38,6 +39,9 @@ const validateInstantRenderConfig = (config: ZeroExInstantConfig, selector: stri if (!_.isUndefined(config.provider)) { assert.isWeb3Provider('provider', config.provider); } + if (!_.isUndefined(config.walletDisplayName)) { + assert.isString('walletDisplayName', config.walletDisplayName); + } if (!_.isUndefined(config.shouldDisablePushToHistory)) { assert.isBoolean('shouldDisablePushToHistory', config.shouldDisablePushToHistory); } @@ -57,6 +61,7 @@ const renderInstant = (config: ZeroExInstantConfig, selector: string) => { injectedDiv.setAttribute('class', INJECTED_DIV_CLASS); appendTo.appendChild(injectedDiv); const closeInstant = () => { + analytics.trackInstantClosed(); if (!_.isUndefined(config.onClose)) { config.onClose(); } @@ -89,12 +94,12 @@ export const render = (config: ZeroExInstantConfig, selector: string = DEFAULT_Z // If the integrator defined a popstate handler, save it to __zeroExInstantIntegratorsPopStateHandler // unless we have already done so on a previous render. const anyWindow = window as any; - if (window.onpopstate && !anyWindow.__zeroExInstantIntegratorsPopStateHandler) { - anyWindow.__zeroExInstantIntegratorsPopStateHandler = window.onpopstate.bind(window); - } - const integratorsOnPopStateHandler = anyWindow.__zeroExInstantIntegratorsPopStateHandler || util.boundNoop; + const popStateExistsAndNotSetPreviously = window.onpopstate && !anyWindow.__zeroExInstantIntegratorsPopStateHandler; + anyWindow.__zeroExInstantIntegratorsPopStateHandler = popStateExistsAndNotSetPreviously + ? anyWindow.onpopstate.bind(window) + : util.boundNoop; const onPopStateHandler = (e: PopStateEvent) => { - integratorsOnPopStateHandler(e); + anyWindow.__zeroExInstantIntegratorsPopStateHandler(e); const newState = e.state; if (newState && newState.zeroExInstantShowing) { // We have returned to a history state that expects instant to be rendered. @@ -110,3 +115,7 @@ export const render = (config: ZeroExInstantConfig, selector: string = DEFAULT_Z }; window.onpopstate = onPopStateHandler; }; + +// Write version info to the exported object for debugging +export const GIT_SHA = process.env.GIT_SHA; +export const NPM_VERSION = process.env.NPM_PACKAGE_VERSION; diff --git a/packages/instant/src/redux/analytics_middleware.ts b/packages/instant/src/redux/analytics_middleware.ts index 8aa76eb776..47876ca2dc 100644 --- a/packages/instant/src/redux/analytics_middleware.ts +++ b/packages/instant/src/redux/analytics_middleware.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import { Middleware } from 'redux'; import { ETH_DECIMALS } from '../constants'; -import { Account, AccountState } from '../types'; +import { AccountState, StandardSlidingPanelContent } from '../types'; import { analytics } from '../util/analytics'; import { Action, ActionTypes } from './actions'; @@ -77,6 +77,18 @@ export const analyticsMiddleware: Middleware = store => next => middlewareAction }); } break; + case ActionTypes.OPEN_STANDARD_SLIDING_PANEL: + const openSlidingContent = curState.standardSlidingPanelSettings.content; + if (openSlidingContent === StandardSlidingPanelContent.InstallWallet) { + analytics.trackInstallWalletModalOpened(); + } + break; + case ActionTypes.CLOSE_STANDARD_SLIDING_PANEL: + const closeSlidingContent = curState.standardSlidingPanelSettings.content; + if (closeSlidingContent === StandardSlidingPanelContent.InstallWallet) { + analytics.trackInstallWalletModalClosed(); + } + break; } return nextAction; diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 6feb760e7f..18f671cd77 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -4,12 +4,13 @@ import * as _ from 'lodash'; import { Dispatch } from 'redux'; import { BIG_NUMBER_ZERO } from '../constants'; -import { AccountState, ERC20Asset, OrderProcessState, ProviderState } from '../types'; +import { AccountState, ERC20Asset, OrderProcessState, ProviderState, QuoteFetchOrigin } from '../types'; import { analytics } from '../util/analytics'; import { assetUtils } from '../util/asset'; import { buyQuoteUpdater } from '../util/buy_quote_updater'; import { coinbaseApi } from '../util/coinbase_api'; import { errorFlasher } from '../util/error_flasher'; +import { errorReporter } from '../util/error_reporter'; import { actions } from './actions'; import { State } from './reducer'; @@ -23,6 +24,7 @@ export const asyncData = { const errorMessage = 'Error fetching ETH/USD price'; errorFlasher.flashNewErrorMessage(dispatch, errorMessage); dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO)); + errorReporter.report(e); } }, fetchAvailableAssetDatasAndDispatchToStore: async (state: State, dispatch: Dispatch) => { @@ -37,6 +39,7 @@ export const asyncData = { errorFlasher.flashNewErrorMessage(dispatch, errorMessage); // On error, just specify that none are available dispatch(actions.setAvailableAssets([])); + errorReporter.report(e); } }, fetchAccountInfoAndDispatchToStore: async ( @@ -77,6 +80,7 @@ export const asyncData = { const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address); dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei })); } catch (e) { + errorReporter.report(e); // leave balance as is return; } @@ -84,6 +88,7 @@ export const asyncData = { fetchCurrentBuyQuoteAndDispatchToStore: async ( state: State, dispatch: Dispatch, + fetchOrigin: QuoteFetchOrigin, options: { updateSilently: boolean }, ) => { const { buyOrderState, providerState, selectedAsset, selectedAssetUnitAmount, affiliateInfo } = state; @@ -99,7 +104,12 @@ export const asyncData = { dispatch, selectedAsset as ERC20Asset, selectedAssetUnitAmount, - { setPending: !options.updateSilently, dispatchErrors: !options.updateSilently, affiliateInfo }, + fetchOrigin, + { + setPending: !options.updateSilently, + dispatchErrors: !options.updateSilently, + affiliateInfo, + }, ); } }, diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index dfc2b89f30..a9a407b7d6 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -49,6 +49,7 @@ interface OptionalState { latestBuyQuote: BuyQuote; latestErrorMessage: string; affiliateInfo: AffiliateInfo; + walletDisplayName: string; } export type State = DefaultState & PropsDerivedState & Partial; diff --git a/packages/instant/src/style/theme.ts b/packages/instant/src/style/theme.ts index a0751286b8..71e4b70521 100644 --- a/packages/instant/src/style/theme.ts +++ b/packages/instant/src/style/theme.ts @@ -1,6 +1,14 @@ import * as styledComponents from 'styled-components'; -const { default: styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider } = styledComponents; +const { + default: styled, + css, + keyframes, + withTheme, + createGlobalStyle, + ThemeConsumer, + ThemeProvider, +} = styledComponents; export type Theme = { [key in ColorOption]: string }; @@ -45,4 +53,4 @@ export const generateOverlayBlack = (opacity = 0.6) => { return `rgba(0, 0, 0, ${opacity})`; }; -export { styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider }; +export { styled, css, keyframes, withTheme, createGlobalStyle, ThemeConsumer, ThemeProvider }; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 233390bf5a..e65961e953 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -21,6 +21,11 @@ export enum OrderProcessState { Failure = 'FAILURE', } +export enum QuoteFetchOrigin { + Manual = 'Manual', + Heartbeat = 'Heartbeat', +} + export interface SimulatedProgress { startTimeUnix: number; expectedEndTimeUnix: number; @@ -149,6 +154,11 @@ export enum Browser { Other = 'OTHER', } +export enum WalletSuggestion { + CoinbaseWallet = 'Coinbase Wallet', + MetaMask = 'MetaMask', +} + export enum OperatingSystem { Android = 'ANDROID', iOS = 'IOS', @@ -174,6 +184,7 @@ export interface ZeroExInstantRequiredBaseConfig { export interface ZeroExInstantOptionalBaseConfig { provider: Provider; + walletDisplayName: string; availableAssetDatas: string[]; defaultAssetBuyAmount: number; defaultSelectedAssetData: string; diff --git a/packages/instant/src/util/analytics.ts b/packages/instant/src/util/analytics.ts index 5bc9bb385f..e625824ef4 100644 --- a/packages/instant/src/util/analytics.ts +++ b/packages/instant/src/util/analytics.ts @@ -1,7 +1,18 @@ import { BuyQuote } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { AffiliateInfo, Asset, Network, OrderSource, ProviderState } from '../types'; +import { INSTANT_DISCHARGE_TARGET } from '../constants'; +import { + AffiliateInfo, + Asset, + Network, + OrderProcessState, + OrderSource, + ProviderState, + QuoteFetchOrigin, + WalletSuggestion, +} from '../types'; import { EventProperties, heapUtil } from './heap'; @@ -18,6 +29,7 @@ export const evaluateIfEnabled = (fnCall: () => void) => { enum EventNames { INSTANT_OPENED = 'Instant - Opened', + INSTANT_CLOSED = 'Instant - Closed', ACCOUNT_LOCKED = 'Account - Locked', ACCOUNT_READY = 'Account - Ready', ACCOUNT_UNLOCK_REQUESTED = 'Account - Unlock Requested', @@ -33,10 +45,18 @@ enum EventNames { BUY_TX_SUBMITTED = 'Buy - Tx Submitted', BUY_TX_SUCCEEDED = 'Buy - Tx Succeeded', BUY_TX_FAILED = 'Buy - Tx Failed', + INSTALL_WALLET_CLICKED = 'Install Wallet - Clicked', + INSTALL_WALLET_MODAL_OPENED = 'Install Wallet - Modal - Opened', + INSTALL_WALLET_MODAL_CLICKED_EXPLANATION = 'Install Wallet - Modal - Clicked Explanation', + INSTALL_WALLET_MODAL_CLICKED_GET = 'Install Wallet - Modal - Clicked Get', + INSTALL_WALLET_MODAL_CLOSED = 'Install Wallet - Modal - Closed', TOKEN_SELECTOR_OPENED = 'Token Selector - Opened', TOKEN_SELECTOR_CLOSED = 'Token Selector - Closed', TOKEN_SELECTOR_CHOSE = 'Token Selector - Chose', TOKEN_SELECTOR_SEARCHED = 'Token Selector - Searched', + TRANSACTION_VIEWED = 'Transaction - Viewed', + QUOTE_FETCHED = 'Quote - Fetched', + QUOTE_ERROR = 'Quote - Error', } const track = (eventName: EventNames, eventProperties: EventProperties = {}): void => { @@ -84,6 +104,7 @@ export interface AnalyticsEventOptions { providerName?: string; gitSha?: string; npmVersion?: string; + instantEnvironment?: string; orderSource?: string; affiliateAddress?: string; affiliateFeePercent?: number; @@ -129,10 +150,12 @@ export const analytics = { affiliateFeePercent, selectedAssetName: selectedAsset ? selectedAsset.metaData.name : 'none', selectedAssetData: selectedAsset ? selectedAsset.assetData : 'none', + instantEnvironment: INSTANT_DISCHARGE_TARGET || `Local ${process.env.NODE_ENV}`, }; return eventOptions; }, trackInstantOpened: trackingEventFnWithoutPayload(EventNames.INSTANT_OPENED), + trackInstantClosed: trackingEventFnWithoutPayload(EventNames.INSTANT_CLOSED), trackAccountLocked: trackingEventFnWithoutPayload(EventNames.ACCOUNT_LOCKED), trackAccountReady: (address: string) => trackingEventFnWithPayload(EventNames.ACCOUNT_READY)({ address }), trackAccountUnlockRequested: trackingEventFnWithoutPayload(EventNames.ACCOUNT_UNLOCK_REQUESTED), @@ -170,6 +193,14 @@ export const analytics = { expectedTxTimeMs: expectedEndTimeUnix - startTimeUnix, actualTxTimeMs: new Date().getTime() - startTimeUnix, }), + trackInstallWalletClicked: (walletSuggestion: WalletSuggestion) => + trackingEventFnWithPayload(EventNames.INSTALL_WALLET_CLICKED)({ walletSuggestion }), + trackInstallWalletModalClickedExplanation: trackingEventFnWithoutPayload( + EventNames.INSTALL_WALLET_MODAL_CLICKED_EXPLANATION, + ), + trackInstallWalletModalClickedGet: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_CLICKED_GET), + trackInstallWalletModalOpened: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_OPENED), + trackInstallWalletModalClosed: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_CLOSED), trackTokenSelectorOpened: trackingEventFnWithoutPayload(EventNames.TOKEN_SELECTOR_OPENED), trackTokenSelectorClosed: (closedVia: TokenSelectorClosedVia) => trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_CLOSED)({ closedVia }), @@ -177,4 +208,18 @@ export const analytics = { trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_CHOSE)(payload), trackTokenSelectorSearched: (searchText: string) => trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_SEARCHED)({ searchText }), + trackTransactionViewed: (orderProcesState: OrderProcessState) => + trackingEventFnWithPayload(EventNames.TRANSACTION_VIEWED)({ orderState: orderProcesState }), + trackQuoteFetched: (buyQuote: BuyQuote, fetchOrigin: QuoteFetchOrigin) => + trackingEventFnWithPayload(EventNames.QUOTE_FETCHED)({ + ...buyQuoteEventProperties(buyQuote), + fetchOrigin, + }), + trackQuoteError: (errorMessage: string, assetBuyAmount: BigNumber, fetchOrigin: QuoteFetchOrigin) => { + trackingEventFnWithPayload(EventNames.QUOTE_ERROR)({ + errorMessage, + assetBuyAmount: assetBuyAmount.toString(), + fetchOrigin, + }); + }, }; diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts index 40560d3eb1..08f3642e36 100644 --- a/packages/instant/src/util/asset.ts +++ b/packages/instant/src/util/asset.ts @@ -1,3 +1,4 @@ +import { AssetBuyerError } from '@0x/asset-buyer'; import { AssetProxyId, ObjectMap } from '@0x/types'; import * as _ from 'lodash'; @@ -106,4 +107,20 @@ export const assetUtils = { ); return _.compact(erc20sOrUndefined); }, + assetBuyerErrorMessage: (asset: ERC20Asset, error: Error): string | undefined => { + if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { + const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); + return `Not enough ${assetName} available`; + } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { + return 'Not enough ZRX available'; + } else if ( + error.message === AssetBuyerError.StandardRelayerApiError || + error.message.startsWith(AssetBuyerError.AssetUnavailable) + ) { + const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); + return `${assetName} is currently unavailable`; + } + + return undefined; + }, }; diff --git a/packages/instant/src/util/buy_quote_updater.ts b/packages/instant/src/util/buy_quote_updater.ts index 2fd16d7818..4229f2735e 100644 --- a/packages/instant/src/util/buy_quote_updater.ts +++ b/packages/instant/src/util/buy_quote_updater.ts @@ -1,4 +1,4 @@ -import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; @@ -6,9 +6,11 @@ import { Dispatch } from 'redux'; import { oc } from 'ts-optchain'; import { Action, actions } from '../redux/actions'; -import { AffiliateInfo, ERC20Asset } from '../types'; +import { AffiliateInfo, ERC20Asset, QuoteFetchOrigin } from '../types'; +import { analytics } from '../util/analytics'; import { assetUtils } from '../util/asset'; import { errorFlasher } from '../util/error_flasher'; +import { errorReporter } from '../util/error_reporter'; export const buyQuoteUpdater = { updateBuyQuoteAsync: async ( @@ -16,7 +18,12 @@ export const buyQuoteUpdater = { dispatch: Dispatch, asset: ERC20Asset, assetUnitAmount: BigNumber, - options: { setPending: boolean; dispatchErrors: boolean; affiliateInfo?: AffiliateInfo }, + fetchOrigin: QuoteFetchOrigin, + options: { + setPending: boolean; + dispatchErrors: boolean; + affiliateInfo?: AffiliateInfo; + }, ): Promise => { // get a new buy quote. const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetUnitAmount, asset.metaData.decimals); @@ -29,34 +36,24 @@ export const buyQuoteUpdater = { try { newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage }); } catch (error) { + const errorMessage = assetUtils.assetBuyerErrorMessage(asset, error); + + if (_.isUndefined(errorMessage)) { + // This is an unknown error, report it to rollbar + errorReporter.report(error); + } + if (options.dispatchErrors) { dispatch(actions.setQuoteRequestStateFailure()); - let errorMessage; - if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { - const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); - errorMessage = `Not enough ${assetName} available`; - } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { - errorMessage = 'Not enough ZRX available'; - } else if ( - error.message === AssetBuyerError.StandardRelayerApiError || - error.message.startsWith(AssetBuyerError.AssetUnavailable) - ) { - const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); - errorMessage = `${assetName} is currently unavailable`; - } - if (!_.isUndefined(errorMessage)) { - errorFlasher.flashNewErrorMessage(dispatch, errorMessage); - } else { - throw error; - } + analytics.trackQuoteError(error.message ? error.message : 'other', baseUnitValue, fetchOrigin); + errorFlasher.flashNewErrorMessage(dispatch, errorMessage || 'Error fetching price, please try again'); } - // TODO: report to error reporter on else - return; } // We have a successful new buy quote errorFlasher.clearError(dispatch); // invalidate the last buy quote. dispatch(actions.updateLatestBuyQuote(newBuyQuote)); + analytics.trackQuoteFetched(newBuyQuote, fetchOrigin); }, }; diff --git a/packages/instant/src/util/error_reporter.ts b/packages/instant/src/util/error_reporter.ts new file mode 100644 index 0000000000..3ec7b6daaf --- /dev/null +++ b/packages/instant/src/util/error_reporter.ts @@ -0,0 +1,62 @@ +import { logUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { HOST_DOMAINS, INSTANT_DISCHARGE_TARGET, ROLLBAR_CLIENT_TOKEN, ROLLBAR_ENABLED } from '../constants'; + +// Import version of Rollbar designed for embedded components +// See https://docs.rollbar.com/docs/using-rollbarjs-inside-an-embedded-component +// tslint:disable-next-line:no-var-requires +const Rollbar = require('rollbar/dist/rollbar.noconflict.umd'); + +let rollbar: any; +// Configures rollbar and sets up error catching +export const setupRollbar = (): any => { + if (_.isUndefined(rollbar) && ROLLBAR_CLIENT_TOKEN && ROLLBAR_ENABLED) { + rollbar = new Rollbar({ + accessToken: ROLLBAR_CLIENT_TOKEN, + captureUncaught: true, + captureUnhandledRejections: true, + enabled: true, + itemsPerMinute: 10, + maxItems: 500, + payload: { + environment: INSTANT_DISCHARGE_TARGET || `Local ${process.env.NODE_ENV}`, + client: { + javascript: { + source_map_enabled: true, + code_version: process.env.GIT_SHA, + guess_uncaught_frames: true, + }, + }, + }, + hostWhiteList: HOST_DOMAINS, + uncaughtErrorLevel: 'error', + ignoredMessages: [ + // Errors from the third-party scripts + 'Script error', + // Network errors or ad-blockers + 'TypeError: Failed to fetch', + 'Exchange has not been deployed to detected network (network/artifact mismatch)', + // Source: https://groups.google.com/a/chromium.org/forum/#!topic/chromium-discuss/7VU0_VvC7mE + "undefined is not an object (evaluating '__gCrWeb.autofill.extractForms')", + // Source: http://stackoverflow.com/questions/43399818/securityerror-from-facebook-and-cross-domain-messaging + 'SecurityError (DOM Exception 18)', + ], + }); + } +}; + +export const errorReporter = { + report(err: Error): void { + if (!rollbar) { + logUtils.log('Not reporting to rollbar because not configured', err); + return; + } + + rollbar.error(err, (rollbarErr: Error) => { + if (rollbarErr) { + logUtils.log(`Error reporting to rollbar, ignoring: ${rollbarErr}`); + } + }); + }, +}; diff --git a/packages/instant/src/util/gas_price_estimator.ts b/packages/instant/src/util/gas_price_estimator.ts index 6b15809a39..332c8d00a5 100644 --- a/packages/instant/src/util/gas_price_estimator.ts +++ b/packages/instant/src/util/gas_price_estimator.ts @@ -7,6 +7,8 @@ import { GWEI_IN_WEI, } from '../constants'; +import { errorReporter } from './error_reporter'; + interface EthGasStationResult { average: number; fastestWait: number; @@ -42,8 +44,9 @@ export class GasPriceEstimator { let fetchedAmount: GasInfo | undefined; try { fetchedAmount = await fetchFastAmountInWeiAsync(); - } catch { + } catch (e) { fetchedAmount = undefined; + errorReporter.report(e); } if (fetchedAmount) { diff --git a/packages/instant/src/util/heap.ts b/packages/instant/src/util/heap.ts index 7c53c9918f..279ff3059a 100644 --- a/packages/instant/src/util/heap.ts +++ b/packages/instant/src/util/heap.ts @@ -5,6 +5,7 @@ import * as _ from 'lodash'; import { HEAP_ANALYTICS_ID } from '../constants'; import { AnalyticsEventOptions, AnalyticsUserOptions } from './analytics'; +import { errorReporter } from './error_reporter'; export type EventProperties = ObjectMap; @@ -107,8 +108,8 @@ export const heapUtil = { heapFunctionCall(curHeap); } catch (e) { // We never want analytics to crash our React component - // TODO(sk): error reporter here logUtils.log('Analytics error', e); + errorReporter.report(e); } } }, diff --git a/packages/instant/src/util/heartbeater_factory.ts b/packages/instant/src/util/heartbeater_factory.ts index 2b852fb0d5..cf29bf3ea5 100644 --- a/packages/instant/src/util/heartbeater_factory.ts +++ b/packages/instant/src/util/heartbeater_factory.ts @@ -1,5 +1,6 @@ import { asyncData } from '../redux/async_data'; import { Store } from '../redux/store'; +import { QuoteFetchOrigin } from '../types'; import { Heartbeater } from './heartbeater'; @@ -17,8 +18,13 @@ export const generateAccountHeartbeater = (options: HeartbeatFactoryOptions): He export const generateBuyQuoteHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => { const { store, shouldPerformImmediatelyOnStart } = options; return new Heartbeater(async () => { - await asyncData.fetchCurrentBuyQuoteAndDispatchToStore(store.getState(), store.dispatch, { - updateSilently: true, - }); + await asyncData.fetchCurrentBuyQuoteAndDispatchToStore( + store.getState(), + store.dispatch, + QuoteFetchOrigin.Heartbeat, + { + updateSilently: true, + }, + ); }, shouldPerformImmediatelyOnStart); }; diff --git a/packages/instant/test/util/asset.test.ts b/packages/instant/test/util/asset.test.ts index 4229b24ed2..fc4e4e2e47 100644 --- a/packages/instant/test/util/asset.test.ts +++ b/packages/instant/test/util/asset.test.ts @@ -1,6 +1,7 @@ +import { AssetBuyerError } from '@0x/asset-buyer'; import { AssetProxyId, ObjectMap } from '@0x/types'; -import { Asset, AssetMetaData, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types'; +import { Asset, AssetMetaData, ERC20Asset, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types'; import { assetUtils } from '../../src/util/asset'; const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; @@ -11,7 +12,7 @@ const ZRX_META_DATA: ERC20AssetMetaData = { decimals: 18, name: '0x', }; -const ZRX_ASSET: Asset = { +const ZRX_ASSET: ERC20Asset = { assetData: ZRX_ASSET_DATA, metaData: ZRX_META_DATA, }; @@ -45,4 +46,32 @@ describe('assetDataUtil', () => { ).toThrowError(ZeroExInstantError.AssetMetaDataNotAvailable); }); }); + describe('assetBuyerErrorMessage', () => { + it('should return message for InsufficientAssetLiquidity', () => { + const insufficientAssetError = new Error(AssetBuyerError.InsufficientAssetLiquidity); + expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, insufficientAssetError)).toEqual( + 'Not enough ZRX available', + ); + }); + it('should return message for InsufficientAssetLiquidity', () => { + const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity); + expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, insufficientZrxError)).toEqual( + 'Not enough ZRX available', + ); + }); + it('should message for StandardRelayerApiError', () => { + const standardRelayerError = new Error(AssetBuyerError.StandardRelayerApiError); + expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, standardRelayerError)).toEqual( + 'ZRX is currently unavailable', + ); + }); + it('should return error for AssetUnavailable error', () => { + const assetUnavailableError = new Error( + `${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET_DATA}`, + ); + expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, assetUnavailableError)).toEqual( + 'ZRX is currently unavailable', + ); + }); + }); }); diff --git a/packages/instant/tsconfig.json b/packages/instant/tsconfig.json index 14b0ad8f79..2b3c11c9f7 100644 --- a/packages/instant/tsconfig.json +++ b/packages/instant/tsconfig.json @@ -5,8 +5,10 @@ "rootDir": "src", "jsx": "react", "noImplicitAny": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "declaration": false, + "declarationMap": false, + "composite": false }, - "include": ["./src/**/*"], - "exclude": ["./src/index.umd.ts"] + "include": ["./src/**/*"] } diff --git a/packages/instant/webpack.config.js b/packages/instant/webpack.config.js index 41276809c9..a1db01db9f 100644 --- a/packages/instant/webpack.config.js +++ b/packages/instant/webpack.config.js @@ -1,32 +1,117 @@ const childProcess = require('child_process'); const ip = require('ip'); const path = require('path'); +const RollbarSourceMapPlugin = require('rollbar-sourcemap-webpack-plugin'); const webpack = require('webpack'); -// The common js bundle (not this one) is built using tsc. -// The umd bundle (this one) has a different entrypoint. - const GIT_SHA = childProcess .execSync('git rev-parse HEAD') .toString() .trim(); -const HEAP_PRODUCTION_ENV_VAR_NAME = 'INSTANT_HEAP_ANALYTICS_ID_PRODUCTION'; -const HEAP_DEVELOPMENT_ENV_VAR_NAME = 'INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT'; -const getHeapAnalyticsId = modeName => { - if (modeName === 'production') { - return process.env[HEAP_PRODUCTION_ENV_VAR_NAME]; - } - - if (modeName === 'development') { - return process.env[HEAP_DEVELOPMENT_ENV_VAR_NAME]; - } - - return undefined; +const DISCHARGE_TARGETS_THAT_REQUIRED_HEAP = ['production', 'staging', 'dogfood']; +const getHeapConfigForDischargeTarget = dischargeTarget => { + return { + heapAnalyticsIdEnvName: + dischargeTarget === 'production' + ? 'INSTANT_HEAP_ANALYTICS_ID_PRODUCTION' + : 'INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT', + heapAnalyticsIdRequired: DISCHARGE_TARGETS_THAT_REQUIRED_HEAP.includes(dischargeTarget), + }; }; -module.exports = (env, argv) => { +const DISCHARGE_TARGETS_THAT_REQUIRE_ROLLBAR = ['production', 'staging', 'dogfood']; +const getRollbarConfigForDischargeTarget = dischargeTarget => { + if (DISCHARGE_TARGETS_THAT_REQUIRE_ROLLBAR.includes(dischargeTarget)) { + const rollbarSourceMapPublicPath = + dischargeTarget === 'production' + ? 'https://instant.0xproject.com' + : `http://0x-instant-${dischargeTarget}.s3-website-us-east-1.amazonaws.com`; + + return { + rollbarSourceMapPublicPath, + rollbarRequired: true, + }; + } + + return { + rollbarRequired: false, + }; +}; + +const ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME = 'INSTANT_ROLLBAR_CLIENT_TOKEN'; +const ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME = 'INSTANT_ROLLBAR_PUBLISH_TOKEN'; +const getRollbarTokens = (dischargeTarget, rollbarRequired) => { + const clientToken = process.env[ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME]; + const publishToken = process.env[ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME]; + + if (rollbarRequired) { + if (!clientToken) { + throw new Error( + `Rollbar client token required for ${dischargeTarget}, please set env var ${ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME}`, + ); + } + if (!publishToken) { + throw new Error( + `Rollbar publish token required for ${dischargeTarget}, please set env var ${ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME}`, + ); + } + } + + return { clientToken, publishToken }; +}; + +const generateConfig = (dischargeTarget, heapConfigOptions, rollbarConfigOptions, nodeEnv) => { const outputPath = process.env.WEBPACK_OUTPUT_PATH || 'umd'; + + const { heapAnalyticsIdEnvName, heapAnalyticsIdRequired } = heapConfigOptions; + const heapAnalyticsId = process.env[heapAnalyticsIdEnvName]; + if (heapAnalyticsIdRequired && !heapAnalyticsId) { + throw new Error( + `Must define heap analytics id in ENV var ${heapAnalyticsIdEnvName} when building for ${dischargeTarget}`, + ); + } + + const rollbarTokens = getRollbarTokens(dischargeTarget, rollbarConfigOptions.rollbarRequired); + const rollbarEnabled = + rollbarTokens.clientToken && (nodeEnv !== 'development' || process.env.INSTANT_ROLLBAR_FORCE_DEVELOPMENT); + + let rollbarPlugin; + if (rollbarConfigOptions.rollbarRequired) { + if (!rollbarEnabled || !rollbarTokens.publishToken || !rollbarConfigOptions.rollbarSourceMapPublicPath) { + throw new Error(`Rollbar required for ${dischargeTarget} but not configured`); + } + rollbarPlugin = new RollbarSourceMapPlugin({ + accessToken: rollbarTokens.publishToken, + version: GIT_SHA, + publicPath: rollbarConfigOptions.rollbarSourceMapPublicPath, + }); + } + + const envVars = { + GIT_SHA: JSON.stringify(GIT_SHA), + NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version), + ROLLBAR_ENABLED: rollbarEnabled, + }; + if (dischargeTarget) { + envVars.INSTANT_DISCHARGE_TARGET = JSON.stringify(dischargeTarget); + } + if (heapAnalyticsId) { + envVars.HEAP_ANALYTICS_ID = JSON.stringify(heapAnalyticsId); + } + if (rollbarTokens.clientToken) { + envVars.ROLLBAR_CLIENT_TOKEN = JSON.stringify(rollbarTokens.clientToken); + } + + const plugins = [ + new webpack.DefinePlugin({ + 'process.env': envVars, + }), + ]; + if (rollbarPlugin) { + plugins.push(rollbarPlugin); + } + const config = { entry: { instant: './src/index.umd.ts', @@ -37,15 +122,7 @@ module.exports = (env, argv) => { library: 'zeroExInstant', libraryTarget: 'umd', }, - plugins: [ - new webpack.DefinePlugin({ - 'process.env': { - GIT_SHA: JSON.stringify(GIT_SHA), - HEAP_ANALYTICS_ID: getHeapAnalyticsId(argv.mode), - NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version), - }, - }), - ], + plugins, devtool: 'source-map', resolve: { extensions: ['.js', '.json', '.ts', '.tsx'], @@ -60,6 +137,15 @@ module.exports = (env, argv) => { test: /\.svg$/, loader: 'svg-react-loader', }, + { + test: /\.js$/, + loader: 'source-map-loader', + exclude: [ + // instead of /\/node_modules\// + path.join(process.cwd(), 'node_modules'), + path.join(process.cwd(), '../..', 'node_modules'), + ], + }, ], }, devServer: { @@ -79,3 +165,10 @@ module.exports = (env, argv) => { }; return config; }; + +module.exports = (env, argv) => { + const dischargeTarget = env ? env.discharge_target : undefined; + const heapConfigOptions = getHeapConfigForDischargeTarget(dischargeTarget); + const rollbarConfigOptions = getRollbarConfigForDischargeTarget(dischargeTarget); + return generateConfig(dischargeTarget, heapConfigOptions, rollbarConfigOptions, argv.mode); +}; diff --git a/packages/monorepo-scripts/src/prepublish_checks.ts b/packages/monorepo-scripts/src/prepublish_checks.ts index 60cdccf1d5..36e61714b8 100644 --- a/packages/monorepo-scripts/src/prepublish_checks.ts +++ b/packages/monorepo-scripts/src/prepublish_checks.ts @@ -17,7 +17,6 @@ async function prepublishChecksAsync(): Promise { await checkChangelogFormatAsync(updatedPublicPackages); await checkGitTagsForNextVersionAndDeleteIfExistAsync(updatedPublicPackages); await checkPublishRequiredSetupAsync(); - checkRequiredEnvVariables(); } async function checkGitTagsForNextVersionAndDeleteIfExistAsync(updatedPublicPackages: Package[]): Promise { @@ -184,16 +183,6 @@ async function checkPublishRequiredSetupAsync(): Promise { } } -const checkRequiredEnvVariables = () => { - utils.log('Checking required environment variables...'); - const requiredEnvVars = ['INSTANT_HEAP_ANALYTICS_ID_PRODUCTION']; - requiredEnvVars.forEach(requiredEnvVarName => { - if (_.isUndefined(process.env[requiredEnvVarName])) { - throw new Error(`Must have ${requiredEnvVarName} set`); - } - }); -}; - prepublishChecksAsync().catch(err => { utils.log(err); process.exit(1); diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 0b32b60f04..53b24aff0b 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.4.0", + "changes": [ + { + "note": "Add `LengthMismatch` and `LengthGreaterThan3Required` revert reasons", + "pr": 1224 + } + ] + }, { "version": "1.3.0", "changes": [ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1a86f45e6a..26d8f8e226 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -195,6 +195,7 @@ export enum RevertReason { FailedExecution = 'FAILED_EXECUTION', AssetProxyAlreadyExists = 'ASSET_PROXY_ALREADY_EXISTS', LengthGreaterThan0Required = 'LENGTH_GREATER_THAN_0_REQUIRED', + LengthGreaterThan3Required = 'LENGTH_GREATER_THAN_3_REQUIRED', LengthGreaterThan131Required = 'LENGTH_GREATER_THAN_131_REQUIRED', Length0Required = 'LENGTH_0_REQUIRED', Length65Required = 'LENGTH_65_REQUIRED', @@ -209,6 +210,7 @@ export enum RevertReason { MakerNotWhitelisted = 'MAKER_NOT_WHITELISTED', TakerNotWhitelisted = 'TAKER_NOT_WHITELISTED', AssetProxyDoesNotExist = 'ASSET_PROXY_DOES_NOT_EXIST', + LengthMismatch = 'LENGTH_MISMATCH', LibBytesGreaterThanZeroLengthRequired = 'GREATER_THAN_ZERO_LENGTH_REQUIRED', LibBytesGreaterOrEqualTo4LengthRequired = 'GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED', LibBytesGreaterOrEqualTo20LengthRequired = 'GREATER_OR_EQUAL_TO_20_LENGTH_REQUIRED', diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index 8c6fb124f4..08801a8916 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -1,4 +1,14 @@ [ + { + "timestamp": 1543448882, + "version": "2.0.7", + "changes": [ + { + "note": + "Optimized ABI Encoder/Decoder. Generates compressed calldata to save gas. Generates human-readable calldata to aid development." + } + ] + }, { "timestamp": 1542821676, "version": "2.0.6", diff --git a/packages/utils/package.json b/packages/utils/package.json index 8dc1f07392..1f4d85843b 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -33,6 +33,9 @@ "@types/lodash": "4.14.104", "@types/mocha": "^2.2.42", "chai": "^4.0.1", + "chai-as-promised": "^7.1.0", + "chai-bignumber": "^2.0.1", + "dirty-chai": "^2.0.1", "make-promises-safe": "^1.1.0", "mocha": "^4.1.0", "npm-run-all": "^4.1.2", @@ -57,4 +60,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/utils/src/abi_encoder/abstract_data_types/data_type.ts b/packages/utils/src/abi_encoder/abstract_data_types/data_type.ts new file mode 100644 index 0000000000..13cc87e2a6 --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/data_type.ts @@ -0,0 +1,58 @@ +import { DataItem } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { Calldata } from '../calldata/calldata'; +import { CalldataBlock } from '../calldata/calldata_block'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; +import { DecodingRules, EncodingRules } from '../utils/rules'; + +import { DataTypeFactory } from './interfaces'; + +export abstract class DataType { + private readonly _dataItem: DataItem; + private readonly _factory: DataTypeFactory; + + constructor(dataItem: DataItem, factory: DataTypeFactory) { + this._dataItem = dataItem; + this._factory = factory; + } + + public getDataItem(): DataItem { + return this._dataItem; + } + + public getFactory(): DataTypeFactory { + return this._factory; + } + + public encode(value: any, rules?: EncodingRules, selector?: string): string { + const rules_ = _.isUndefined(rules) ? constants.DEFAULT_ENCODING_RULES : rules; + const calldata = new Calldata(rules_); + if (!_.isUndefined(selector)) { + calldata.setSelector(selector); + } + const block = this.generateCalldataBlock(value); + calldata.setRoot(block); + const encodedCalldata = calldata.toString(); + return encodedCalldata; + } + + public decode(calldata: string, rules?: DecodingRules, selector?: string): any { + if (!_.isUndefined(selector) && !_.startsWith(calldata, selector)) { + throw new Error( + `Tried to decode calldata, but it was missing the function selector. Expected prefix '${selector}'. Got '${calldata}'.`, + ); + } + const hasSelector = !_.isUndefined(selector); + const rawCalldata = new RawCalldata(calldata, hasSelector); + const rules_ = _.isUndefined(rules) ? constants.DEFAULT_DECODING_RULES : rules; + const value = this.generateValue(rawCalldata, rules_); + return value; + } + + public abstract generateCalldataBlock(value: any, parentBlock?: CalldataBlock): CalldataBlock; + public abstract generateValue(calldata: RawCalldata, rules: DecodingRules): any; + public abstract getSignature(): string; + public abstract isStatic(): boolean; +} diff --git a/packages/utils/src/abi_encoder/abstract_data_types/interfaces.ts b/packages/utils/src/abi_encoder/abstract_data_types/interfaces.ts new file mode 100644 index 0000000000..2f2f60871e --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/interfaces.ts @@ -0,0 +1,19 @@ +import { DataItem } from 'ethereum-types'; + +import { RawCalldata } from '../calldata/raw_calldata'; + +import { DataType } from './data_type'; + +export interface DataTypeFactory { + create: (dataItem: DataItem, parentDataType?: DataType) => DataType; +} + +export interface DataTypeStaticInterface { + matchType: (type: string) => boolean; + encodeValue: (value: any) => Buffer; + decodeValue: (rawCalldata: RawCalldata) => any; +} + +export interface MemberIndexByName { + [key: string]: number; +} diff --git a/packages/utils/src/abi_encoder/abstract_data_types/types/blob.ts b/packages/utils/src/abi_encoder/abstract_data_types/types/blob.ts new file mode 100644 index 0000000000..a091e55b9d --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/types/blob.ts @@ -0,0 +1,40 @@ +import { DataItem } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BlobCalldataBlock } from '../../calldata/blocks/blob'; +import { CalldataBlock } from '../../calldata/calldata_block'; +import { RawCalldata } from '../../calldata/raw_calldata'; +import { DecodingRules } from '../../utils/rules'; + +import { DataType } from '../data_type'; +import { DataTypeFactory } from '../interfaces'; + +export abstract class AbstractBlobDataType extends DataType { + protected _sizeKnownAtCompileTime: boolean; + + public constructor(dataItem: DataItem, factory: DataTypeFactory, sizeKnownAtCompileTime: boolean) { + super(dataItem, factory); + this._sizeKnownAtCompileTime = sizeKnownAtCompileTime; + } + + public generateCalldataBlock(value: any, parentBlock?: CalldataBlock): BlobCalldataBlock { + const encodedValue = this.encodeValue(value); + const name = this.getDataItem().name; + const signature = this.getSignature(); + const parentName = _.isUndefined(parentBlock) ? '' : parentBlock.getName(); + const block = new BlobCalldataBlock(name, signature, parentName, encodedValue); + return block; + } + + public generateValue(calldata: RawCalldata, rules: DecodingRules): any { + const value = this.decodeValue(calldata); + return value; + } + + public isStatic(): boolean { + return this._sizeKnownAtCompileTime; + } + + public abstract encodeValue(value: any): Buffer; + public abstract decodeValue(calldata: RawCalldata): any; +} diff --git a/packages/utils/src/abi_encoder/abstract_data_types/types/pointer.ts b/packages/utils/src/abi_encoder/abstract_data_types/types/pointer.ts new file mode 100644 index 0000000000..0f3c55280c --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/types/pointer.ts @@ -0,0 +1,54 @@ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { PointerCalldataBlock } from '../../calldata/blocks/pointer'; +import { CalldataBlock } from '../../calldata/calldata_block'; +import { RawCalldata } from '../../calldata/raw_calldata'; +import { constants } from '../../utils/constants'; +import { DecodingRules } from '../../utils/rules'; + +import { DataType } from '../data_type'; +import { DataTypeFactory } from '../interfaces'; + +export abstract class AbstractPointerDataType extends DataType { + protected _destination: DataType; + protected _parent: DataType; + + public constructor(dataItem: DataItem, factory: DataTypeFactory, destination: DataType, parent: DataType) { + super(dataItem, factory); + this._destination = destination; + this._parent = parent; + } + + public generateCalldataBlock(value: any, parentBlock?: CalldataBlock): PointerCalldataBlock { + if (_.isUndefined(parentBlock)) { + throw new Error(`DependentDataType requires a parent block to generate its block`); + } + const destinationBlock = this._destination.generateCalldataBlock(value, parentBlock); + const name = this.getDataItem().name; + const signature = this.getSignature(); + const parentName = parentBlock.getName(); + const block = new PointerCalldataBlock(name, signature, parentName, destinationBlock, parentBlock); + return block; + } + + public generateValue(calldata: RawCalldata, rules: DecodingRules): any { + const destinationOffsetBuf = calldata.popWord(); + const destinationOffsetHex = ethUtil.bufferToHex(destinationOffsetBuf); + const destinationOffsetRelative = parseInt(destinationOffsetHex, constants.HEX_BASE); + const destinationOffsetAbsolute = calldata.toAbsoluteOffset(destinationOffsetRelative); + const currentOffset = calldata.getOffset(); + calldata.setOffset(destinationOffsetAbsolute); + const value = this._destination.generateValue(calldata, rules); + calldata.setOffset(currentOffset); + return value; + } + + // Disable prefer-function-over-method for inherited abstract method. + /* tslint:disable prefer-function-over-method */ + public isStatic(): boolean { + return true; + } + /* tslint:enable prefer-function-over-method */ +} diff --git a/packages/utils/src/abi_encoder/abstract_data_types/types/set.ts b/packages/utils/src/abi_encoder/abstract_data_types/types/set.ts new file mode 100644 index 0000000000..089d046596 --- /dev/null +++ b/packages/utils/src/abi_encoder/abstract_data_types/types/set.ts @@ -0,0 +1,218 @@ +import { DataItem } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../../configured_bignumber'; +import { SetCalldataBlock } from '../../calldata/blocks/set'; +import { CalldataBlock } from '../../calldata/calldata_block'; +import { RawCalldata } from '../../calldata/raw_calldata'; +import { constants } from '../../utils/constants'; +import { DecodingRules } from '../../utils/rules'; + +import { DataType } from '../data_type'; +import { DataTypeFactory, MemberIndexByName } from '../interfaces'; + +import { AbstractPointerDataType } from './pointer'; + +export abstract class AbstractSetDataType extends DataType { + protected readonly _arrayLength: number | undefined; + protected readonly _arrayElementType: string | undefined; + private readonly _memberIndexByName: MemberIndexByName; + private readonly _members: DataType[]; + private readonly _isArray: boolean; + + public constructor( + dataItem: DataItem, + factory: DataTypeFactory, + isArray: boolean = false, + arrayLength?: number, + arrayElementType?: string, + ) { + super(dataItem, factory); + this._memberIndexByName = {}; + this._members = []; + this._isArray = isArray; + this._arrayLength = arrayLength; + this._arrayElementType = arrayElementType; + if (isArray && !_.isUndefined(arrayLength)) { + [this._members, this._memberIndexByName] = this._createMembersWithLength(dataItem, arrayLength); + } else if (!isArray) { + [this._members, this._memberIndexByName] = this._createMembersWithKeys(dataItem); + } + } + + public generateCalldataBlock(value: any[] | object, parentBlock?: CalldataBlock): SetCalldataBlock { + const block = + value instanceof Array + ? this._generateCalldataBlockFromArray(value, parentBlock) + : this._generateCalldataBlockFromObject(value, parentBlock); + return block; + } + + public generateValue(calldata: RawCalldata, rules: DecodingRules): any[] | object { + let members = this._members; + // Case 1: This is an array of undefined length, which means that `this._members` was not + // populated in the constructor. So we must construct the set of members now. + if (this._isArray && _.isUndefined(this._arrayLength)) { + const arrayLengthBuf = calldata.popWord(); + const arrayLengthHex = ethUtil.bufferToHex(arrayLengthBuf); + const arrayLength = new BigNumber(arrayLengthHex, constants.HEX_BASE); + [members] = this._createMembersWithLength(this.getDataItem(), arrayLength.toNumber()); + } + // Create a new scope in the calldata, before descending into the members of this set. + calldata.startScope(); + let value: any[] | object; + if (rules.structsAsObjects && !this._isArray) { + // Construct an object with values for each member of the set. + value = {}; + _.each(this._memberIndexByName, (idx: number, key: string) => { + const member = this._members[idx]; + const memberValue = member.generateValue(calldata, rules); + (value as { [key: string]: any })[key] = memberValue; + }); + } else { + // Construct an array with values for each member of the set. + value = []; + _.each(members, (member: DataType, idx: number) => { + const memberValue = member.generateValue(calldata, rules); + (value as any[]).push(memberValue); + }); + } + // Close this scope and return tetheh value. + calldata.endScope(); + return value; + } + + public isStatic(): boolean { + // An array with an undefined length is never static. + if (this._isArray && _.isUndefined(this._arrayLength)) { + return false; + } + // If any member of the set is a pointer then the set is not static. + const dependentMember = _.find(this._members, (member: DataType) => { + return member instanceof AbstractPointerDataType; + }); + const isStatic = _.isUndefined(dependentMember); + return isStatic; + } + + protected _generateCalldataBlockFromArray(value: any[], parentBlock?: CalldataBlock): SetCalldataBlock { + // Sanity check: if the set has a defined length then `value` must have the same length. + if (!_.isUndefined(this._arrayLength) && value.length !== this._arrayLength) { + throw new Error( + `Expected array of ${JSON.stringify( + this._arrayLength, + )} elements, but got array of length ${JSON.stringify(value.length)}`, + ); + } + // Create a new calldata block for this set. + const parentName = _.isUndefined(parentBlock) ? '' : parentBlock.getName(); + const block = new SetCalldataBlock(this.getDataItem().name, this.getSignature(), parentName); + // If this set has an undefined length then set its header to be the number of elements. + let members = this._members; + if (this._isArray && _.isUndefined(this._arrayLength)) { + [members] = this._createMembersWithLength(this.getDataItem(), value.length); + const lenBuf = ethUtil.setLengthLeft( + ethUtil.toBuffer(`0x${value.length.toString(constants.HEX_BASE)}`), + constants.EVM_WORD_WIDTH_IN_BYTES, + ); + block.setHeader(lenBuf); + } + // Create blocks for members of set. + const memberCalldataBlocks: CalldataBlock[] = []; + _.each(members, (member: DataType, idx: number) => { + const memberBlock = member.generateCalldataBlock(value[idx], block); + memberCalldataBlocks.push(memberBlock); + }); + block.setMembers(memberCalldataBlocks); + return block; + } + + protected _generateCalldataBlockFromObject(obj: object, parentBlock?: CalldataBlock): SetCalldataBlock { + // Create a new calldata block for this set. + const parentName = _.isUndefined(parentBlock) ? '' : parentBlock.getName(); + const block = new SetCalldataBlock(this.getDataItem().name, this.getSignature(), parentName); + // Create blocks for members of set. + const memberCalldataBlocks: CalldataBlock[] = []; + const childMap = _.cloneDeep(this._memberIndexByName); + _.forOwn(obj, (value: any, key: string) => { + if (!(key in childMap)) { + throw new Error( + `Could not assign tuple to object: unrecognized key '${key}' in object ${this.getDataItem().name}`, + ); + } + const memberBlock = this._members[this._memberIndexByName[key]].generateCalldataBlock(value, block); + memberCalldataBlocks.push(memberBlock); + delete childMap[key]; + }); + // Sanity check that all members have been included. + if (Object.keys(childMap).length !== 0) { + throw new Error(`Could not assign tuple to object: missing keys ${Object.keys(childMap)}`); + } + // Associate member blocks with Set block. + block.setMembers(memberCalldataBlocks); + return block; + } + + protected _computeSignatureOfMembers(): string { + // Compute signature of members + let signature = `(`; + _.each(this._members, (member: DataType, i: number) => { + signature += member.getSignature(); + if (i < this._members.length - 1) { + signature += ','; + } + }); + signature += ')'; + return signature; + } + + private _createMembersWithKeys(dataItem: DataItem): [DataType[], MemberIndexByName] { + // Sanity check + if (_.isUndefined(dataItem.components)) { + throw new Error( + `Tried to create a set using key/value pairs, but no components were defined by the input DataItem '${ + dataItem.name + }'.`, + ); + } + // Create one member for each component of `dataItem` + const members: DataType[] = []; + const memberIndexByName: MemberIndexByName = {}; + _.each(dataItem.components, (memberItem: DataItem) => { + const childDataItem: DataItem = { + type: memberItem.type, + name: `${dataItem.name}.${memberItem.name}`, + }; + const components = memberItem.components; + if (!_.isUndefined(components)) { + childDataItem.components = components; + } + const child = this.getFactory().create(childDataItem, this); + memberIndexByName[memberItem.name] = members.length; + members.push(child); + }); + return [members, memberIndexByName]; + } + + private _createMembersWithLength(dataItem: DataItem, length: number): [DataType[], MemberIndexByName] { + // Create `length` members, deriving the type from `dataItem` + const members: DataType[] = []; + const memberIndexByName: MemberIndexByName = {}; + const range = _.range(length); + _.each(range, (idx: number) => { + const memberDataItem: DataItem = { + type: _.isUndefined(this._arrayElementType) ? '' : this._arrayElementType, + name: `${dataItem.name}[${idx.toString(constants.DEC_BASE)}]`, + }; + const components = dataItem.components; + if (!_.isUndefined(components)) { + memberDataItem.components = components; + } + const memberType = this.getFactory().create(memberDataItem, this); + memberIndexByName[idx.toString(constants.DEC_BASE)] = members.length; + members.push(memberType); + }); + return [members, memberIndexByName]; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/blocks/blob.ts b/packages/utils/src/abi_encoder/calldata/blocks/blob.ts new file mode 100644 index 0000000000..219ea6c612 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/blocks/blob.ts @@ -0,0 +1,20 @@ +import { CalldataBlock } from '../calldata_block'; + +export class BlobCalldataBlock extends CalldataBlock { + private readonly _blob: Buffer; + + constructor(name: string, signature: string, parentName: string, blob: Buffer) { + const headerSizeInBytes = 0; + const bodySizeInBytes = blob.byteLength; + super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes); + this._blob = blob; + } + + public toBuffer(): Buffer { + return this._blob; + } + + public getRawData(): Buffer { + return this._blob; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/blocks/pointer.ts b/packages/utils/src/abi_encoder/calldata/blocks/pointer.ts new file mode 100644 index 0000000000..72d6a31730 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/blocks/pointer.ts @@ -0,0 +1,61 @@ +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { constants } from '../../utils/constants'; + +import { CalldataBlock } from '../calldata_block'; + +export class PointerCalldataBlock extends CalldataBlock { + public static readonly RAW_DATA_START = new Buffer('<'); + public static readonly RAW_DATA_END = new Buffer('>'); + private static readonly _DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32; + private static readonly _EMPTY_HEADER_SIZE = 0; + private readonly _parent: CalldataBlock; + private readonly _dependency: CalldataBlock; + private _aliasFor: CalldataBlock | undefined; + + constructor(name: string, signature: string, parentName: string, dependency: CalldataBlock, parent: CalldataBlock) { + const headerSizeInBytes = PointerCalldataBlock._EMPTY_HEADER_SIZE; + const bodySizeInBytes = PointerCalldataBlock._DEPENDENT_PAYLOAD_SIZE_IN_BYTES; + super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes); + this._parent = parent; + this._dependency = dependency; + this._aliasFor = undefined; + } + + public toBuffer(): Buffer { + const destinationOffset = !_.isUndefined(this._aliasFor) + ? this._aliasFor.getOffsetInBytes() + : this._dependency.getOffsetInBytes(); + const parentOffset = this._parent.getOffsetInBytes(); + const parentHeaderSize = this._parent.getHeaderSizeInBytes(); + const pointer: number = destinationOffset - (parentOffset + parentHeaderSize); + const pointerHex = `0x${pointer.toString(constants.HEX_BASE)}`; + const pointerBuf = ethUtil.toBuffer(pointerHex); + const pointerBufPadded = ethUtil.setLengthLeft(pointerBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + return pointerBufPadded; + } + + public getDependency(): CalldataBlock { + return this._dependency; + } + + public setAlias(block: CalldataBlock): void { + this._aliasFor = block; + this._setName(`${this.getName()} (alias for ${block.getName()})`); + } + + public getAlias(): CalldataBlock | undefined { + return this._aliasFor; + } + + public getRawData(): Buffer { + const dependencyRawData = this._dependency.getRawData(); + const rawDataComponents: Buffer[] = []; + rawDataComponents.push(PointerCalldataBlock.RAW_DATA_START); + rawDataComponents.push(dependencyRawData); + rawDataComponents.push(PointerCalldataBlock.RAW_DATA_END); + const rawData = Buffer.concat(rawDataComponents); + return rawData; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/blocks/set.ts b/packages/utils/src/abi_encoder/calldata/blocks/set.ts new file mode 100644 index 0000000000..d1abc4986e --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/blocks/set.ts @@ -0,0 +1,47 @@ +import * as _ from 'lodash'; + +import { CalldataBlock } from '../calldata_block'; + +export class SetCalldataBlock extends CalldataBlock { + private _header: Buffer | undefined; + private _members: CalldataBlock[]; + + constructor(name: string, signature: string, parentName: string) { + super(name, signature, parentName, 0, 0); + this._members = []; + this._header = undefined; + } + + public getRawData(): Buffer { + const rawDataComponents: Buffer[] = []; + if (!_.isUndefined(this._header)) { + rawDataComponents.push(this._header); + } + _.each(this._members, (member: CalldataBlock) => { + const memberBuffer = member.getRawData(); + rawDataComponents.push(memberBuffer); + }); + const rawData = Buffer.concat(rawDataComponents); + return rawData; + } + + public setMembers(members: CalldataBlock[]): void { + this._members = members; + } + + public setHeader(header: Buffer): void { + this._setHeaderSize(header.byteLength); + this._header = header; + } + + public toBuffer(): Buffer { + if (!_.isUndefined(this._header)) { + return this._header; + } + return new Buffer(''); + } + + public getMembers(): CalldataBlock[] { + return this._members; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/calldata.ts b/packages/utils/src/abi_encoder/calldata/calldata.ts new file mode 100644 index 0000000000..5f3eee94a6 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/calldata.ts @@ -0,0 +1,243 @@ +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { constants } from '../utils/constants'; +import { EncodingRules } from '../utils/rules'; + +import { PointerCalldataBlock } from './blocks/pointer'; +import { SetCalldataBlock } from './blocks/set'; +import { CalldataBlock } from './calldata_block'; +import { CalldataIterator, ReverseCalldataIterator } from './iterator'; + +export class Calldata { + private readonly _rules: EncodingRules; + private _selector: string; + private _root: CalldataBlock | undefined; + + public constructor(rules: EncodingRules) { + this._rules = rules; + this._selector = ''; + this._root = undefined; + } + /** + * Sets the root calldata block. This block usually corresponds to a Method. + */ + public setRoot(block: CalldataBlock): void { + this._root = block; + } + /** + * Sets the selector to be prepended onto the calldata. + * If the root block was created by a Method then a selector will likely be set. + */ + public setSelector(selector: string): void { + if (!_.startsWith(selector, '0x')) { + throw new Error(`Expected selector to be hex. Missing prefix '0x'`); + } else if (selector.length !== constants.HEX_SELECTOR_LENGTH_IN_CHARS) { + throw new Error(`Invalid selector '${selector}'`); + } + this._selector = selector; + } + /** + * Iterates through the calldata blocks, starting from the root block, to construct calldata as a hex string. + * If the `optimize` flag is set then this calldata will be condensed, to save gas. + * If the `annotate` flag is set then this will return human-readable calldata. + * If the `annotate` flag is *not* set then this will return EVM-compatible calldata. + */ + public toString(): string { + // Sanity check: root block must be set + if (_.isUndefined(this._root)) { + throw new Error('expected root'); + } + // Optimize, if flag set + if (this._rules.optimize) { + this._optimize(); + } + // Set offsets + const iterator = new CalldataIterator(this._root); + let offset = 0; + for (const block of iterator) { + block.setOffset(offset); + offset += block.getSizeInBytes(); + } + // Generate hex string + const hexString = this._rules.annotate ? this._toHumanReadableCallData() : this._toEvmCompatibeCallDataHex(); + return hexString; + } + /** + * There are three types of calldata blocks: Blob, Set and Pointer. + * Scenarios arise where distinct pointers resolve to identical values. + * We optimize by keeping only one such instance of the identical value, and redirecting all pointers here. + * We keep the last such duplicate value because pointers can only be positive (they cannot point backwards). + * + * Example #1: + * function f(string[], string[]) + * f(["foo", "bar", "blitz"], ["foo", "bar", "blitz"]) + * The array ["foo", "bar", "blitz"] will only be included in the calldata once. + * + * Example #2: + * function f(string[], string) + * f(["foo", "bar", "blitz"], "foo") + * The string "foo" will only be included in the calldata once. + * + * Example #3: + * function f((string, uint, bytes), string, uint, bytes) + * f(("foo", 5, "0x05"), "foo", 5, "0x05") + * The string "foo" and bytes "0x05" will only be included in the calldata once. + * The duplicate `uint 5` values cannot be optimized out because they are static values (no pointer points to them). + * + * @TODO #1: + * This optimization strategy handles blocks that are exact duplicates of one another. + * But what if some block is a combination of two other blocks? Or a subset of another block? + * This optimization problem is not much different from the current implemetation. + * Instead of tracking "observed" hashes, at each node we would simply do pattern-matching on the calldata. + * This strategy would be applied after assigning offsets to the tree, rather than before (as in this strategy). + * Note that one consequence of this strategy is pointers may resolve to offsets that are not word-aligned. + * This shouldn't be a problem but further investigation should be done. + * + * @TODO #2: + * To be done as a follow-up to @TODO #1. + * Since we optimize from the bottom-up, we could be affecting the outcome of a later potential optimization. + * For example, what if by removing one duplicate value we miss out on optimizing another block higher in the tree. + * To handle this case, at each node we can store a candidate optimization in a priority queue (sorted by calldata size). + * At the end of traversing the tree, the candidate at the front of the queue will be the most optimal output. + * + */ + private _optimize(): void { + // Step 1/1 Create a reverse iterator (starts from the end of the calldata to the beginning) + if (_.isUndefined(this._root)) { + throw new Error('expected root'); + } + const iterator = new ReverseCalldataIterator(this._root); + // Step 2/2 Iterate over each block, keeping track of which blocks have been seen and pruning redundant blocks. + const blocksByHash: { [key: string]: CalldataBlock } = {}; + for (const block of iterator) { + // If a block is a pointer and its value has already been observed, then update + // the pointer to resolve to the existing value. + if (block instanceof PointerCalldataBlock) { + const dependencyBlockHashBuf = block.getDependency().computeHash(); + const dependencyBlockHash = ethUtil.bufferToHex(dependencyBlockHashBuf); + if (dependencyBlockHash in blocksByHash) { + const blockWithSameHash = blocksByHash[dependencyBlockHash]; + if (blockWithSameHash !== block.getDependency()) { + block.setAlias(blockWithSameHash); + } + } + continue; + } + // This block has not been seen. Record its hash. + const blockHashBuf = block.computeHash(); + const blockHash = ethUtil.bufferToHex(blockHashBuf); + if (!(blockHash in blocksByHash)) { + blocksByHash[blockHash] = block; + } + } + } + private _toEvmCompatibeCallDataHex(): string { + // Sanity check: must have a root block. + if (_.isUndefined(this._root)) { + throw new Error('expected root'); + } + // Construct an array of buffers (one buffer for each block). + const selectorBuffer = ethUtil.toBuffer(this._selector); + const valueBufs: Buffer[] = [selectorBuffer]; + const iterator = new CalldataIterator(this._root); + for (const block of iterator) { + valueBufs.push(block.toBuffer()); + } + // Create hex from buffer array. + const combinedBuffers = Buffer.concat(valueBufs); + const hexValue = ethUtil.bufferToHex(combinedBuffers); + return hexValue; + } + /** + * Returns human-readable calldata. + * + * Example: + * simpleFunction(string[], string[]) + * strings = ["Hello", "World"] + * simpleFunction(strings, strings) + * + * Output: + * 0xbb4f12e3 + * ### simpleFunction + * 0x0 0000000000000000000000000000000000000000000000000000000000000040 ptr (alias for array2) + * 0x20 0000000000000000000000000000000000000000000000000000000000000040 ptr + * + * 0x40 0000000000000000000000000000000000000000000000000000000000000002 ### array2 + * 0x60 0000000000000000000000000000000000000000000000000000000000000040 ptr + * 0x80 0000000000000000000000000000000000000000000000000000000000000080 ptr + * 0xa0 0000000000000000000000000000000000000000000000000000000000000005 array2[0] + * 0xc0 48656c6c6f000000000000000000000000000000000000000000000000000000 + * 0xe0 0000000000000000000000000000000000000000000000000000000000000005 array2[1] + * 0x100 576f726c64000000000000000000000000000000000000000000000000000000 + */ + private _toHumanReadableCallData(): string { + // Sanity check: must have a root block. + if (_.isUndefined(this._root)) { + throw new Error('expected root'); + } + // Constants for constructing annotated string + const offsetPadding = 10; + const valuePadding = 74; + const namePadding = 80; + const evmWordStartIndex = 0; + const emptySize = 0; + // Construct annotated calldata + let hexValue = `${this._selector}`; + let offset = 0; + const functionName: string = this._root.getName(); + const iterator = new CalldataIterator(this._root); + for (const block of iterator) { + // Process each block 1 word at a time + const size = block.getSizeInBytes(); + const name = block.getName(); + const parentName = block.getParentName(); + const prettyName = name.replace(`${parentName}.`, '').replace(`${functionName}.`, ''); + // Resulting line will be + let offsetStr = ''; + let valueStr = ''; + let nameStr = ''; + let lineStr = ''; + if (size === emptySize) { + // This is a Set block with no header. + // For example, a tuple or an array with a defined length. + offsetStr = ' '.repeat(offsetPadding); + valueStr = ' '.repeat(valuePadding); + nameStr = `### ${prettyName.padEnd(namePadding)}`; + lineStr = `\n${offsetStr}${valueStr}${nameStr}`; + } else { + // This block has at least one word of value. + offsetStr = `0x${offset.toString(constants.HEX_BASE)}`.padEnd(offsetPadding); + valueStr = ethUtil + .stripHexPrefix( + ethUtil.bufferToHex( + block.toBuffer().slice(evmWordStartIndex, constants.EVM_WORD_WIDTH_IN_BYTES), + ), + ) + .padEnd(valuePadding); + if (block instanceof SetCalldataBlock) { + nameStr = `### ${prettyName.padEnd(namePadding)}`; + lineStr = `\n${offsetStr}${valueStr}${nameStr}`; + } else { + nameStr = ` ${prettyName.padEnd(namePadding)}`; + lineStr = `${offsetStr}${valueStr}${nameStr}`; + } + } + // This block has a value that is more than 1 word. + for (let j = constants.EVM_WORD_WIDTH_IN_BYTES; j < size; j += constants.EVM_WORD_WIDTH_IN_BYTES) { + offsetStr = `0x${(offset + j).toString(constants.HEX_BASE)}`.padEnd(offsetPadding); + valueStr = ethUtil + .stripHexPrefix( + ethUtil.bufferToHex(block.toBuffer().slice(j, j + constants.EVM_WORD_WIDTH_IN_BYTES)), + ) + .padEnd(valuePadding); + nameStr = ' '.repeat(namePadding); + lineStr = `${lineStr}\n${offsetStr}${valueStr}${nameStr}`; + } + // Append to hex value + hexValue = `${hexValue}\n${lineStr}`; + offset += size; + } + return hexValue; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/calldata_block.ts b/packages/utils/src/abi_encoder/calldata/calldata_block.ts new file mode 100644 index 0000000000..35bd994e53 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/calldata_block.ts @@ -0,0 +1,77 @@ +import * as ethUtil from 'ethereumjs-util'; + +export abstract class CalldataBlock { + private readonly _signature: string; + private readonly _parentName: string; + private _name: string; + private _offsetInBytes: number; + private _headerSizeInBytes: number; + private _bodySizeInBytes: number; + + constructor( + name: string, + signature: string, + parentName: string, + headerSizeInBytes: number, + bodySizeInBytes: number, + ) { + this._name = name; + this._signature = signature; + this._parentName = parentName; + this._offsetInBytes = 0; + this._headerSizeInBytes = headerSizeInBytes; + this._bodySizeInBytes = bodySizeInBytes; + } + + protected _setHeaderSize(headerSizeInBytes: number): void { + this._headerSizeInBytes = headerSizeInBytes; + } + + protected _setBodySize(bodySizeInBytes: number): void { + this._bodySizeInBytes = bodySizeInBytes; + } + + protected _setName(name: string): void { + this._name = name; + } + + public getName(): string { + return this._name; + } + + public getParentName(): string { + return this._parentName; + } + + public getSignature(): string { + return this._signature; + } + public getHeaderSizeInBytes(): number { + return this._headerSizeInBytes; + } + + public getBodySizeInBytes(): number { + return this._bodySizeInBytes; + } + + public getSizeInBytes(): number { + return this.getHeaderSizeInBytes() + this.getBodySizeInBytes(); + } + + public getOffsetInBytes(): number { + return this._offsetInBytes; + } + + public setOffset(offsetInBytes: number): void { + this._offsetInBytes = offsetInBytes; + } + + public computeHash(): Buffer { + const rawData = this.getRawData(); + const hash = ethUtil.sha3(rawData); + return hash; + } + + public abstract toBuffer(): Buffer; + public abstract getRawData(): Buffer; +} diff --git a/packages/utils/src/abi_encoder/calldata/iterator.ts b/packages/utils/src/abi_encoder/calldata/iterator.ts new file mode 100644 index 0000000000..333b32b4fc --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/iterator.ts @@ -0,0 +1,114 @@ +/* tslint:disable max-classes-per-file */ +import * as _ from 'lodash'; + +import { Queue } from '../utils/queue'; + +import { BlobCalldataBlock } from './blocks/blob'; +import { PointerCalldataBlock } from './blocks/pointer'; +import { SetCalldataBlock } from './blocks/set'; +import { CalldataBlock } from './calldata_block'; + +/** + * Iterator class for Calldata Blocks. Blocks follows the order + * they should be put into calldata that is passed to he EVM. + * + * Example #1: + * Let root = Set { + * Blob{} A, + * Pointer { + * Blob{} a + * } B, + * Blob{} C + * } + * It will iterate as follows: [A, B, C, B.a] + * + * Example #2: + * Let root = Set { + * Blob{} A, + * Pointer { + * Blob{} a + * Pointer { + * Blob{} b + * } + * } B, + * Pointer { + * Blob{} c + * } C + * } + * It will iterate as follows: [A, B, C, B.a, B.b, C.c] + */ +abstract class BaseIterator implements Iterable { + protected readonly _root: CalldataBlock; + protected readonly _queue: Queue; + + private static _createQueue(block: CalldataBlock): Queue { + const queue = new Queue(); + // Base case + if (!(block instanceof SetCalldataBlock)) { + queue.pushBack(block); + return queue; + } + // This is a set; add members + const set = block; + _.eachRight(set.getMembers(), (member: CalldataBlock) => { + queue.mergeFront(BaseIterator._createQueue(member)); + }); + // Add children + _.each(set.getMembers(), (member: CalldataBlock) => { + // Traverse child if it is a unique pointer. + // A pointer that is an alias for another pointer is ignored. + if (member instanceof PointerCalldataBlock && _.isUndefined(member.getAlias())) { + const dependency = member.getDependency(); + queue.mergeBack(BaseIterator._createQueue(dependency)); + } + }); + // Put set block at the front of the queue + queue.pushFront(set); + return queue; + } + + public constructor(root: CalldataBlock) { + this._root = root; + this._queue = BaseIterator._createQueue(root); + } + + public [Symbol.iterator](): { next: () => IteratorResult } { + return { + next: () => { + const nextBlock = this.nextBlock(); + if (!_.isUndefined(nextBlock)) { + return { + value: nextBlock, + done: false, + }; + } + return { + done: true, + value: new BlobCalldataBlock('', '', '', new Buffer('')), + }; + }, + }; + } + + public abstract nextBlock(): CalldataBlock | undefined; +} + +export class CalldataIterator extends BaseIterator { + public constructor(root: CalldataBlock) { + super(root); + } + + public nextBlock(): CalldataBlock | undefined { + return this._queue.popFront(); + } +} + +export class ReverseCalldataIterator extends BaseIterator { + public constructor(root: CalldataBlock) { + super(root); + } + + public nextBlock(): CalldataBlock | undefined { + return this._queue.popBack(); + } +} diff --git a/packages/utils/src/abi_encoder/calldata/raw_calldata.ts b/packages/utils/src/abi_encoder/calldata/raw_calldata.ts new file mode 100644 index 0000000000..189841989e --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/raw_calldata.ts @@ -0,0 +1,82 @@ +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { constants } from '../utils/constants'; +import { Queue } from '../utils/queue'; + +export class RawCalldata { + private static readonly _INITIAL_OFFSET = 0; + private readonly _value: Buffer; + private readonly _selector: string; + private readonly _scopes: Queue; + private _offset: number; + + public constructor(value: string | Buffer, hasSelector: boolean = true) { + // Sanity check + if (typeof value === 'string' && !_.startsWith(value, '0x')) { + throw new Error(`Expected raw calldata to start with '0x'`); + } + // Construct initial values + this._value = ethUtil.toBuffer(value); + this._selector = '0x'; + this._scopes = new Queue(); + this._scopes.pushBack(RawCalldata._INITIAL_OFFSET); + this._offset = RawCalldata._INITIAL_OFFSET; + // If there's a selector then slice it + if (hasSelector) { + const selectorBuf = this._value.slice(constants.HEX_SELECTOR_LENGTH_IN_BYTES); + this._value = this._value.slice(constants.HEX_SELECTOR_LENGTH_IN_BYTES); + this._selector = ethUtil.bufferToHex(selectorBuf); + } + } + + public popBytes(lengthInBytes: number): Buffer { + const value = this._value.slice(this._offset, this._offset + lengthInBytes); + this.setOffset(this._offset + lengthInBytes); + return value; + } + + public popWord(): Buffer { + const wordInBytes = 32; + return this.popBytes(wordInBytes); + } + + public popWords(length: number): Buffer { + const wordInBytes = 32; + return this.popBytes(length * wordInBytes); + } + + public readBytes(from: number, to: number): Buffer { + const value = this._value.slice(from, to); + return value; + } + + public setOffset(offsetInBytes: number): void { + this._offset = offsetInBytes; + } + + public startScope(): void { + this._scopes.pushFront(this._offset); + } + + public endScope(): void { + this._scopes.popFront(); + } + + public getOffset(): number { + return this._offset; + } + + public toAbsoluteOffset(relativeOffset: number): number { + const scopeOffset = this._scopes.peekFront(); + if (_.isUndefined(scopeOffset)) { + throw new Error(`Tried to access undefined scope.`); + } + const absoluteOffset = relativeOffset + scopeOffset; + return absoluteOffset; + } + + public getSelector(): string { + return this._selector; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_type_factory.ts b/packages/utils/src/abi_encoder/evm_data_type_factory.ts new file mode 100644 index 0000000000..4cc124e0a7 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_type_factory.ts @@ -0,0 +1,132 @@ +/* tslint:disable max-classes-per-file */ +import { DataItem, MethodAbi } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DataType } from './abstract_data_types/data_type'; +import { DataTypeFactory } from './abstract_data_types/interfaces'; +import { AddressDataType } from './evm_data_types/address'; +import { ArrayDataType } from './evm_data_types/array'; +import { BoolDataType } from './evm_data_types/bool'; +import { DynamicBytesDataType } from './evm_data_types/dynamic_bytes'; +import { IntDataType } from './evm_data_types/int'; +import { MethodDataType } from './evm_data_types/method'; +import { PointerDataType } from './evm_data_types/pointer'; +import { StaticBytesDataType } from './evm_data_types/static_bytes'; +import { StringDataType } from './evm_data_types/string'; +import { TupleDataType } from './evm_data_types/tuple'; +import { UIntDataType } from './evm_data_types/uint'; + +export class Address extends AddressDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Bool extends BoolDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Int extends IntDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class UInt extends UIntDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class StaticBytes extends StaticBytesDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class DynamicBytes extends DynamicBytesDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class String extends StringDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Pointer extends PointerDataType { + public constructor(destDataType: DataType, parentDataType: DataType) { + super(destDataType, parentDataType, EvmDataTypeFactory.getInstance()); + } +} + +export class Tuple extends TupleDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Array extends ArrayDataType { + public constructor(dataItem: DataItem) { + super(dataItem, EvmDataTypeFactory.getInstance()); + } +} + +export class Method extends MethodDataType { + public constructor(abi: MethodAbi) { + super(abi, EvmDataTypeFactory.getInstance()); + } +} + +/* tslint:disable no-construct */ +export class EvmDataTypeFactory implements DataTypeFactory { + private static _instance: DataTypeFactory; + + public static getInstance(): DataTypeFactory { + if (!EvmDataTypeFactory._instance) { + EvmDataTypeFactory._instance = new EvmDataTypeFactory(); + } + return EvmDataTypeFactory._instance; + } + + /* tslint:disable prefer-function-over-method */ + public create(dataItem: DataItem, parentDataType?: DataType): DataType { + // Create data type + let dataType: undefined | DataType; + if (Array.matchType(dataItem.type)) { + dataType = new Array(dataItem); + } else if (Address.matchType(dataItem.type)) { + dataType = new Address(dataItem); + } else if (Bool.matchType(dataItem.type)) { + dataType = new Bool(dataItem); + } else if (Int.matchType(dataItem.type)) { + dataType = new Int(dataItem); + } else if (UInt.matchType(dataItem.type)) { + dataType = new UInt(dataItem); + } else if (StaticBytes.matchType(dataItem.type)) { + dataType = new StaticBytes(dataItem); + } else if (Tuple.matchType(dataItem.type)) { + dataType = new Tuple(dataItem); + } else if (DynamicBytes.matchType(dataItem.type)) { + dataType = new DynamicBytes(dataItem); + } else if (String.matchType(dataItem.type)) { + dataType = new String(dataItem); + } + // @TODO: DataTypeement Fixed/UFixed types + if (_.isUndefined(dataType)) { + throw new Error(`Unrecognized data type: '${dataItem.type}'`); + } else if (!_.isUndefined(parentDataType) && !dataType.isStatic()) { + const pointerToDataType = new Pointer(dataType, parentDataType); + return pointerToDataType; + } + return dataType; + } + /* tslint:enable prefer-function-over-method */ + + private constructor() {} +} +/* tslint:enable no-construct */ diff --git a/packages/utils/src/abi_encoder/evm_data_types/address.ts b/packages/utils/src/abi_encoder/evm_data_types/address.ts new file mode 100644 index 0000000000..88846b1fa4 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/address.ts @@ -0,0 +1,49 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class AddressDataType extends AbstractBlobDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + private static readonly _ADDRESS_SIZE_IN_BYTES = 20; + private static readonly _DECODED_ADDRESS_OFFSET_IN_BYTES = constants.EVM_WORD_WIDTH_IN_BYTES - + AddressDataType._ADDRESS_SIZE_IN_BYTES; + + public static matchType(type: string): boolean { + return type === SolidityTypes.Address; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, AddressDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!AddressDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Address with bad input: ${dataItem}`); + } + } + + // Disable prefer-function-over-method for inherited abstract methods. + /* tslint:disable prefer-function-over-method */ + public encodeValue(value: string): Buffer { + if (!ethUtil.isValidAddress(value)) { + throw new Error(`Invalid address: '${value}'`); + } + const valueBuf = ethUtil.toBuffer(value); + const encodedValueBuf = ethUtil.setLengthLeft(valueBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + return encodedValueBuf; + } + + public decodeValue(calldata: RawCalldata): string { + const valueBufPadded = calldata.popWord(); + const valueBuf = valueBufPadded.slice(AddressDataType._DECODED_ADDRESS_OFFSET_IN_BYTES); + const value = ethUtil.bufferToHex(valueBuf); + return value; + } + + public getSignature(): string { + return SolidityTypes.Address; + } + /* tslint:enable prefer-function-over-method */ +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/array.ts b/packages/utils/src/abi_encoder/evm_data_types/array.ts new file mode 100644 index 0000000000..7595cb667b --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/array.ts @@ -0,0 +1,64 @@ +import { DataItem } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractSetDataType } from '../abstract_data_types/types/set'; +import { constants } from '../utils/constants'; + +export class ArrayDataType extends AbstractSetDataType { + private static readonly _MATCHER = RegExp('^(.+)\\[([0-9]*)\\]$'); + private readonly _arraySignature: string; + private readonly _elementType: string; + + public static matchType(type: string): boolean { + return ArrayDataType._MATCHER.test(type); + } + + private static _decodeElementTypeAndLengthFromType(type: string): [string, undefined | number] { + const matches = ArrayDataType._MATCHER.exec(type); + if (_.isNull(matches) || matches.length !== 3) { + throw new Error(`Could not parse array: ${type}`); + } else if (_.isUndefined(matches[1])) { + throw new Error(`Could not parse array type: ${type}`); + } else if (_.isUndefined(matches[2])) { + throw new Error(`Could not parse array length: ${type}`); + } + const arrayElementType = matches[1]; + const arrayLength = _.isEmpty(matches[2]) ? undefined : parseInt(matches[2], constants.DEC_BASE); + return [arrayElementType, arrayLength]; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + // Construct parent + const isArray = true; + const [arrayElementType, arrayLength] = ArrayDataType._decodeElementTypeAndLengthFromType(dataItem.type); + super(dataItem, dataTypeFactory, isArray, arrayLength, arrayElementType); + // Set array properties + this._elementType = arrayElementType; + this._arraySignature = this._computeSignature(); + } + + public getSignature(): string { + return this._arraySignature; + } + + private _computeSignature(): string { + // Compute signature for a single array element + const elementDataItem: DataItem = { + type: this._elementType, + name: 'N/A', + }; + const elementComponents = this.getDataItem().components; + if (!_.isUndefined(elementComponents)) { + elementDataItem.components = elementComponents; + } + const elementDataType = this.getFactory().create(elementDataItem); + const elementSignature = elementDataType.getSignature(); + // Construct signature for array of type `element` + if (_.isUndefined(this._arrayLength)) { + return `${elementSignature}[]`; + } else { + return `${elementSignature}[${this._arrayLength}]`; + } + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/bool.ts b/packages/utils/src/abi_encoder/evm_data_types/bool.ts new file mode 100644 index 0000000000..d713d5a942 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/bool.ts @@ -0,0 +1,53 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../configured_bignumber'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class BoolDataType extends AbstractBlobDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + + public static matchType(type: string): boolean { + return type === SolidityTypes.Bool; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, BoolDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!BoolDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Bool with bad input: ${dataItem}`); + } + } + + // Disable prefer-function-over-method for inherited abstract methods. + /* tslint:disable prefer-function-over-method */ + public encodeValue(value: boolean): Buffer { + const encodedValue = value ? '0x1' : '0x0'; + const encodedValueBuf = ethUtil.setLengthLeft( + ethUtil.toBuffer(encodedValue), + constants.EVM_WORD_WIDTH_IN_BYTES, + ); + return encodedValueBuf; + } + + public decodeValue(calldata: RawCalldata): boolean { + const valueBuf = calldata.popWord(); + const valueHex = ethUtil.bufferToHex(valueBuf); + const valueNumber = new BigNumber(valueHex, constants.HEX_BASE); + if (!(valueNumber.equals(0) || valueNumber.equals(1))) { + throw new Error(`Failed to decode boolean. Expected 0x0 or 0x1, got ${valueHex}`); + } + /* tslint:disable boolean-naming */ + const value: boolean = !valueNumber.equals(0); + /* tslint:enable boolean-naming */ + return value; + } + + public getSignature(): string { + return SolidityTypes.Bool; + } + /* tslint:enable prefer-function-over-method */ +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/dynamic_bytes.ts b/packages/utils/src/abi_encoder/evm_data_types/dynamic_bytes.ts new file mode 100644 index 0000000000..5277efd6cb --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/dynamic_bytes.ts @@ -0,0 +1,72 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class DynamicBytesDataType extends AbstractBlobDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; + + public static matchType(type: string): boolean { + return type === SolidityTypes.Bytes; + } + + private static _sanityCheckValue(value: string | Buffer): void { + if (typeof value !== 'string') { + return; + } + if (!_.startsWith(value, '0x')) { + throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix.`); + } else if (value.length % 2 !== 0) { + throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); + } + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, DynamicBytesDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!DynamicBytesDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Dynamic Bytes with bad input: ${dataItem}`); + } + } + + // Disable prefer-function-over-method for inherited abstract methods. + /* tslint:disable prefer-function-over-method */ + public encodeValue(value: string | Buffer): Buffer { + // Encoded value is of the form: , with each field padded to be word-aligned. + // 1/3 Construct the length + const valueBuf = ethUtil.toBuffer(value); + const wordsToStoreValuePadded = Math.ceil(valueBuf.byteLength / constants.EVM_WORD_WIDTH_IN_BYTES); + const bytesToStoreValuePadded = wordsToStoreValuePadded * constants.EVM_WORD_WIDTH_IN_BYTES; + const lengthBuf = ethUtil.toBuffer(valueBuf.byteLength); + const lengthBufPadded = ethUtil.setLengthLeft(lengthBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + // 2/3 Construct the value + DynamicBytesDataType._sanityCheckValue(value); + const valueBufPadded = ethUtil.setLengthRight(valueBuf, bytesToStoreValuePadded); + // 3/3 Combine length and value + const encodedValue = Buffer.concat([lengthBufPadded, valueBufPadded]); + return encodedValue; + } + + public decodeValue(calldata: RawCalldata): string { + // Encoded value is of the form: , with each field padded to be word-aligned. + // 1/2 Decode length + const lengthBuf = calldata.popWord(); + const lengthHex = ethUtil.bufferToHex(lengthBuf); + const length = parseInt(lengthHex, constants.HEX_BASE); + // 2/2 Decode value + const wordsToStoreValuePadded = Math.ceil(length / constants.EVM_WORD_WIDTH_IN_BYTES); + const valueBufPadded = calldata.popWords(wordsToStoreValuePadded); + const valueBuf = valueBufPadded.slice(0, length); + const value = ethUtil.bufferToHex(valueBuf); + DynamicBytesDataType._sanityCheckValue(value); + return value; + } + + public getSignature(): string { + return SolidityTypes.Bytes; + } + /* tslint:enable prefer-function-over-method */ +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/int.ts b/packages/utils/src/abi_encoder/evm_data_types/int.ts new file mode 100644 index 0000000000..f1dcf5ea15 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/int.ts @@ -0,0 +1,59 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../configured_bignumber'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; +import * as EncoderMath from '../utils/math'; + +export class IntDataType extends AbstractBlobDataType { + private static readonly _MATCHER = RegExp( + '^int(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$', + ); + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + private static readonly _MAX_WIDTH: number = 256; + private static readonly _DEFAULT_WIDTH: number = IntDataType._MAX_WIDTH; + private readonly _width: number; + private readonly _minValue: BigNumber; + private readonly _maxValue: BigNumber; + + public static matchType(type: string): boolean { + return IntDataType._MATCHER.test(type); + } + + private static _decodeWidthFromType(type: string): number { + const matches = IntDataType._MATCHER.exec(type); + const width = + !_.isNull(matches) && matches.length === 2 && !_.isUndefined(matches[1]) + ? parseInt(matches[1], constants.DEC_BASE) + : IntDataType._DEFAULT_WIDTH; + return width; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, IntDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!IntDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Int with bad input: ${dataItem}`); + } + this._width = IntDataType._decodeWidthFromType(dataItem.type); + this._minValue = new BigNumber(2).toPower(this._width - 1).times(-1); + this._maxValue = new BigNumber(2).toPower(this._width - 1).sub(1); + } + + public encodeValue(value: BigNumber | string | number): Buffer { + const encodedValue = EncoderMath.safeEncodeNumericValue(value, this._minValue, this._maxValue); + return encodedValue; + } + + public decodeValue(calldata: RawCalldata): BigNumber { + const valueBuf = calldata.popWord(); + const value = EncoderMath.safeDecodeNumericValue(valueBuf, this._minValue, this._maxValue); + return value; + } + + public getSignature(): string { + return `${SolidityTypes.Int}${this._width}`; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/method.ts b/packages/utils/src/abi_encoder/evm_data_types/method.ts new file mode 100644 index 0000000000..b1cd1377f4 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/method.ts @@ -0,0 +1,72 @@ +import { DataItem, MethodAbi } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataType } from '../abstract_data_types/data_type'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractSetDataType } from '../abstract_data_types/types/set'; +import { constants } from '../utils/constants'; +import { DecodingRules, EncodingRules } from '../utils/rules'; + +import { TupleDataType } from './tuple'; + +export class MethodDataType extends AbstractSetDataType { + private readonly _methodSignature: string; + private readonly _methodSelector: string; + private readonly _returnDataType: DataType; + + public constructor(abi: MethodAbi, dataTypeFactory: DataTypeFactory) { + const methodDataItem = { type: 'method', name: abi.name, components: abi.inputs }; + super(methodDataItem, dataTypeFactory); + this._methodSignature = this._computeSignature(); + this._methodSelector = this._computeSelector(); + const returnDataItem: DataItem = { type: 'tuple', name: abi.name, components: abi.outputs }; + this._returnDataType = new TupleDataType(returnDataItem, this.getFactory()); + } + + public encode(value: any, rules?: EncodingRules): string { + const calldata = super.encode(value, rules, this._methodSelector); + return calldata; + } + + public decode(calldata: string, rules?: DecodingRules): any[] | object { + const value = super.decode(calldata, rules, this._methodSelector); + return value; + } + + public encodeReturnValues(value: any, rules?: EncodingRules): string { + const returnData = this._returnDataType.encode(value, rules); + return returnData; + } + + public decodeReturnValues(returndata: string, rules?: DecodingRules): any { + const returnValues = this._returnDataType.decode(returndata, rules); + return returnValues; + } + + public getSignature(): string { + return this._methodSignature; + } + + public getSelector(): string { + return this._methodSelector; + } + + private _computeSignature(): string { + const memberSignature = this._computeSignatureOfMembers(); + const methodSignature = `${this.getDataItem().name}${memberSignature}`; + return methodSignature; + } + + private _computeSelector(): string { + const signature = this._computeSignature(); + const selector = ethUtil.bufferToHex( + ethUtil.toBuffer( + ethUtil + .sha3(signature) + .slice(constants.HEX_SELECTOR_BYTE_OFFSET_IN_CALLDATA, constants.HEX_SELECTOR_LENGTH_IN_BYTES), + ), + ); + return selector; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/pointer.ts b/packages/utils/src/abi_encoder/evm_data_types/pointer.ts new file mode 100644 index 0000000000..389e759271 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/pointer.ts @@ -0,0 +1,17 @@ +import { DataItem } from 'ethereum-types'; + +import { DataType } from '../abstract_data_types/data_type'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractPointerDataType } from '../abstract_data_types/types/pointer'; + +export class PointerDataType extends AbstractPointerDataType { + constructor(destDataType: DataType, parentDataType: DataType, dataTypeFactory: DataTypeFactory) { + const destDataItem = destDataType.getDataItem(); + const dataItem: DataItem = { name: `ptr<${destDataItem.name}>`, type: `ptr<${destDataItem.type}>` }; + super(dataItem, dataTypeFactory, destDataType, parentDataType); + } + + public getSignature(): string { + return this._destination.getSignature(); + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts b/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts new file mode 100644 index 0000000000..2e371c5058 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/static_bytes.ts @@ -0,0 +1,78 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class StaticBytesDataType extends AbstractBlobDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + private static readonly _MATCHER = RegExp( + '^(byte|bytes(1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32))$', + ); + private static readonly _DEFAULT_WIDTH = 1; + private readonly _width: number; + + public static matchType(type: string): boolean { + return StaticBytesDataType._MATCHER.test(type); + } + + private static _decodeWidthFromType(type: string): number { + const matches = StaticBytesDataType._MATCHER.exec(type); + const width = + !_.isNull(matches) && matches.length === 3 && !_.isUndefined(matches[2]) + ? parseInt(matches[2], constants.DEC_BASE) + : StaticBytesDataType._DEFAULT_WIDTH; + return width; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, StaticBytesDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!StaticBytesDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Static Bytes with bad input: ${dataItem}`); + } + this._width = StaticBytesDataType._decodeWidthFromType(dataItem.type); + } + + public getSignature(): string { + // Note that `byte` reduces to `bytes1` + return `${SolidityTypes.Bytes}${this._width}`; + } + + public encodeValue(value: string | Buffer): Buffer { + // 1/2 Convert value into a buffer and do bounds checking + this._sanityCheckValue(value); + const valueBuf = ethUtil.toBuffer(value); + // 2/2 Store value as hex + const valuePadded = ethUtil.setLengthRight(valueBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + return valuePadded; + } + + public decodeValue(calldata: RawCalldata): string { + const valueBufPadded = calldata.popWord(); + const valueBuf = valueBufPadded.slice(0, this._width); + const value = ethUtil.bufferToHex(valueBuf); + this._sanityCheckValue(value); + return value; + } + + private _sanityCheckValue(value: string | Buffer): void { + if (typeof value === 'string') { + if (!_.startsWith(value, '0x')) { + throw new Error(`Tried to encode non-hex value. Value must inlcude '0x' prefix.`); + } else if (value.length % 2 !== 0) { + throw new Error(`Tried to assign ${value}, which is contains a half-byte. Use full bytes only.`); + } + } + const valueBuf = ethUtil.toBuffer(value); + if (valueBuf.byteLength > this._width) { + throw new Error( + `Tried to assign ${value} (${ + valueBuf.byteLength + } bytes), which exceeds max bytes that can be stored in a ${this.getSignature()}`, + ); + } + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/string.ts b/packages/utils/src/abi_encoder/evm_data_types/string.ts new file mode 100644 index 0000000000..91a72ad3fa --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/string.ts @@ -0,0 +1,59 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; + +export class StringDataType extends AbstractBlobDataType { + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = false; + + public static matchType(type: string): boolean { + return type === SolidityTypes.String; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, StringDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!StringDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate String with bad input: ${dataItem}`); + } + } + + // Disable prefer-function-over-method for inherited abstract methods. + /* tslint:disable prefer-function-over-method */ + public encodeValue(value: string): Buffer { + // Encoded value is of the form: , with each field padded to be word-aligned. + // 1/3 Construct the length + const wordsToStoreValuePadded = Math.ceil(value.length / constants.EVM_WORD_WIDTH_IN_BYTES); + const bytesToStoreValuePadded = wordsToStoreValuePadded * constants.EVM_WORD_WIDTH_IN_BYTES; + const lengthBuf = ethUtil.toBuffer(value.length); + const lengthBufPadded = ethUtil.setLengthLeft(lengthBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + // 2/3 Construct the value + const valueBuf = new Buffer(value); + const valueBufPadded = ethUtil.setLengthRight(valueBuf, bytesToStoreValuePadded); + // 3/3 Combine length and value + const encodedValue = Buffer.concat([lengthBufPadded, valueBufPadded]); + return encodedValue; + } + + public decodeValue(calldata: RawCalldata): string { + // Encoded value is of the form: , with each field padded to be word-aligned. + // 1/2 Decode length + const lengthBufPadded = calldata.popWord(); + const lengthHexPadded = ethUtil.bufferToHex(lengthBufPadded); + const length = parseInt(lengthHexPadded, constants.HEX_BASE); + // 2/2 Decode value + const wordsToStoreValuePadded = Math.ceil(length / constants.EVM_WORD_WIDTH_IN_BYTES); + const valueBufPadded = calldata.popWords(wordsToStoreValuePadded); + const valueBuf = valueBufPadded.slice(0, length); + const value = valueBuf.toString('ascii'); + return value; + } + + public getSignature(): string { + return SolidityTypes.String; + } + /* tslint:enable prefer-function-over-method */ +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/tuple.ts b/packages/utils/src/abi_encoder/evm_data_types/tuple.ts new file mode 100644 index 0000000000..31593c882a --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/tuple.ts @@ -0,0 +1,24 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; + +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractSetDataType } from '../abstract_data_types/types/set'; + +export class TupleDataType extends AbstractSetDataType { + private readonly _signature: string; + + public static matchType(type: string): boolean { + return type === SolidityTypes.Tuple; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory); + if (!TupleDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate Tuple with bad input: ${dataItem}`); + } + this._signature = this._computeSignatureOfMembers(); + } + + public getSignature(): string { + return this._signature; + } +} diff --git a/packages/utils/src/abi_encoder/evm_data_types/uint.ts b/packages/utils/src/abi_encoder/evm_data_types/uint.ts new file mode 100644 index 0000000000..5180f0cf31 --- /dev/null +++ b/packages/utils/src/abi_encoder/evm_data_types/uint.ts @@ -0,0 +1,58 @@ +import { DataItem, SolidityTypes } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BigNumber } from '../../configured_bignumber'; +import { DataTypeFactory } from '../abstract_data_types/interfaces'; +import { AbstractBlobDataType } from '../abstract_data_types/types/blob'; +import { RawCalldata } from '../calldata/raw_calldata'; +import { constants } from '../utils/constants'; +import * as EncoderMath from '../utils/math'; + +export class UIntDataType extends AbstractBlobDataType { + private static readonly _MATCHER = RegExp( + '^uint(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$', + ); + private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; + private static readonly _MAX_WIDTH: number = 256; + private static readonly _DEFAULT_WIDTH: number = UIntDataType._MAX_WIDTH; + private static readonly _MIN_VALUE = new BigNumber(0); + private readonly _width: number; + private readonly _maxValue: BigNumber; + + public static matchType(type: string): boolean { + return UIntDataType._MATCHER.test(type); + } + + private static _decodeWidthFromType(type: string): number { + const matches = UIntDataType._MATCHER.exec(type); + const width = + !_.isNull(matches) && matches.length === 2 && !_.isUndefined(matches[1]) + ? parseInt(matches[1], constants.DEC_BASE) + : UIntDataType._DEFAULT_WIDTH; + return width; + } + + public constructor(dataItem: DataItem, dataTypeFactory: DataTypeFactory) { + super(dataItem, dataTypeFactory, UIntDataType._SIZE_KNOWN_AT_COMPILE_TIME); + if (!UIntDataType.matchType(dataItem.type)) { + throw new Error(`Tried to instantiate UInt with bad input: ${dataItem}`); + } + this._width = UIntDataType._decodeWidthFromType(dataItem.type); + this._maxValue = new BigNumber(2).toPower(this._width).sub(1); + } + + public encodeValue(value: BigNumber | string | number): Buffer { + const encodedValue = EncoderMath.safeEncodeNumericValue(value, UIntDataType._MIN_VALUE, this._maxValue); + return encodedValue; + } + + public decodeValue(calldata: RawCalldata): BigNumber { + const valueBuf = calldata.popWord(); + const value = EncoderMath.safeDecodeNumericValue(valueBuf, UIntDataType._MIN_VALUE, this._maxValue); + return value; + } + + public getSignature(): string { + return `${SolidityTypes.Uint}${this._width}`; + } +} diff --git a/packages/utils/src/abi_encoder/index.ts b/packages/utils/src/abi_encoder/index.ts new file mode 100644 index 0000000000..baf844ac65 --- /dev/null +++ b/packages/utils/src/abi_encoder/index.ts @@ -0,0 +1,14 @@ +export { EncodingRules, DecodingRules } from './utils/rules'; +export { + Address, + Array, + Bool, + DynamicBytes, + Int, + Method, + Pointer, + StaticBytes, + String, + Tuple, + UInt, +} from './evm_data_type_factory'; diff --git a/packages/utils/src/abi_encoder/utils/constants.ts b/packages/utils/src/abi_encoder/utils/constants.ts new file mode 100644 index 0000000000..2f43ba04d9 --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/constants.ts @@ -0,0 +1,17 @@ +import { DecodingRules, EncodingRules } from './rules'; + +export const constants = { + EVM_WORD_WIDTH_IN_BYTES: 32, + EVM_WORD_WIDTH_IN_BITS: 256, + HEX_BASE: 16, + DEC_BASE: 10, + BIN_BASE: 2, + HEX_SELECTOR_LENGTH_IN_CHARS: 10, + HEX_SELECTOR_LENGTH_IN_BYTES: 4, + HEX_SELECTOR_BYTE_OFFSET_IN_CALLDATA: 0, + // Disable no-object-literal-type-assertion so we can enforce cast + /* tslint:disable no-object-literal-type-assertion */ + DEFAULT_DECODING_RULES: { structsAsObjects: false } as DecodingRules, + DEFAULT_ENCODING_RULES: { optimize: true, annotate: false } as EncodingRules, + /* tslint:enable no-object-literal-type-assertion */ +}; diff --git a/packages/utils/src/abi_encoder/utils/math.ts b/packages/utils/src/abi_encoder/utils/math.ts new file mode 100644 index 0000000000..d84983c5b9 --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/math.ts @@ -0,0 +1,111 @@ +import BigNumber from 'bignumber.js'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { constants } from '../utils/constants'; + +function sanityCheckBigNumberRange( + value_: BigNumber | string | number, + minValue: BigNumber, + maxValue: BigNumber, +): void { + const value = new BigNumber(value_, 10); + if (value.greaterThan(maxValue)) { + throw new Error(`Tried to assign value of ${value}, which exceeds max value of ${maxValue}`); + } else if (value.lessThan(minValue)) { + throw new Error(`Tried to assign value of ${value}, which exceeds min value of ${minValue}`); + } +} +function bigNumberToPaddedBuffer(value: BigNumber): Buffer { + const valueHex = `0x${value.toString(constants.HEX_BASE)}`; + const valueBuf = ethUtil.toBuffer(valueHex); + const valueBufPadded = ethUtil.setLengthLeft(valueBuf, constants.EVM_WORD_WIDTH_IN_BYTES); + return valueBufPadded; +} +/** + * Takes a numeric value and returns its ABI-encoded value + * @param value_ The value to encode. + * @return ABI Encoded value + */ +export function encodeNumericValue(value_: BigNumber | string | number): Buffer { + const value = new BigNumber(value_, 10); + // Case 1/2: value is non-negative + if (value.greaterThanOrEqualTo(0)) { + const encodedPositiveValue = bigNumberToPaddedBuffer(value); + return encodedPositiveValue; + } + // Case 2/2: Value is negative + // Use two's-complement to encode the value + // Step 1/3: Convert negative value to positive binary string + const valueBin = value.times(-1).toString(constants.BIN_BASE); + // Step 2/3: Invert binary value + let invertedValueBin = '1'.repeat(constants.EVM_WORD_WIDTH_IN_BITS - valueBin.length); + _.each(valueBin, (bit: string) => { + invertedValueBin += bit === '1' ? '0' : '1'; + }); + const invertedValue = new BigNumber(invertedValueBin, constants.BIN_BASE); + // Step 3/3: Add 1 to inverted value + const negativeValue = invertedValue.plus(1); + const encodedValue = bigNumberToPaddedBuffer(negativeValue); + return encodedValue; +} +/** + * Takes a numeric value and returns its ABI-encoded value. + * Performs an additional sanity check, given the min/max allowed value. + * @param value_ The value to encode. + * @return ABI Encoded value + */ +export function safeEncodeNumericValue( + value: BigNumber | string | number, + minValue: BigNumber, + maxValue: BigNumber, +): Buffer { + sanityCheckBigNumberRange(value, minValue, maxValue); + const encodedValue = encodeNumericValue(value); + return encodedValue; +} +/** + * Takes an ABI-encoded numeric value and returns its decoded value as a BigNumber. + * @param encodedValue The encoded numeric value. + * @param minValue The minimum possible decoded value. + * @return ABI Decoded value + */ +export function decodeNumericValue(encodedValue: Buffer, minValue: BigNumber): BigNumber { + const valueHex = ethUtil.bufferToHex(encodedValue); + // Case 1/3: value is definitely non-negative because of numeric boundaries + const value = new BigNumber(valueHex, constants.HEX_BASE); + if (!minValue.lessThan(0)) { + return value; + } + // Case 2/3: value is non-negative because there is no leading 1 (encoded as two's-complement) + const valueBin = value.toString(constants.BIN_BASE); + const isValueNegative = valueBin.length === constants.EVM_WORD_WIDTH_IN_BITS && _.startsWith(valueBin[0], '1'); + if (!isValueNegative) { + return value; + } + // Case 3/3: value is negative + // Step 1/3: Invert b inary value + let invertedValueBin = ''; + _.each(valueBin, (bit: string) => { + invertedValueBin += bit === '1' ? '0' : '1'; + }); + const invertedValue = new BigNumber(invertedValueBin, constants.BIN_BASE); + // Step 2/3: Add 1 to inverted value + // The result is the two's-complement representation of the input value. + const positiveValue = invertedValue.plus(1); + // Step 3/3: Invert positive value to get the negative value + const negativeValue = positiveValue.times(-1); + return negativeValue; +} +/** + * Takes an ABI-encoded numeric value and returns its decoded value as a BigNumber. + * Performs an additional sanity check, given the min/max allowed value. + * @param encodedValue The encoded numeric value. + * @param minValue The minimum possible decoded value. + * @return ABI Decoded value + */ +export function safeDecodeNumericValue(encodedValue: Buffer, minValue: BigNumber, maxValue: BigNumber): BigNumber { + const value = decodeNumericValue(encodedValue, minValue); + sanityCheckBigNumberRange(value, minValue, maxValue); + return value; +} diff --git a/packages/utils/src/abi_encoder/utils/queue.ts b/packages/utils/src/abi_encoder/utils/queue.ts new file mode 100644 index 0000000000..53afb7e112 --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/queue.ts @@ -0,0 +1,39 @@ +export class Queue { + private _store: T[] = []; + + public pushBack(val: T): void { + this._store.push(val); + } + + public pushFront(val: T): void { + this._store.unshift(val); + } + + public popFront(): T | undefined { + return this._store.shift(); + } + + public popBack(): T | undefined { + if (this._store.length === 0) { + return undefined; + } + const backElement = this._store.splice(-1, 1)[0]; + return backElement; + } + + public mergeBack(q: Queue): void { + this._store = this._store.concat(q._store); + } + + public mergeFront(q: Queue): void { + this._store = q._store.concat(this._store); + } + + public getStore(): T[] { + return this._store; + } + + public peekFront(): T | undefined { + return this._store.length >= 0 ? this._store[0] : undefined; + } +} diff --git a/packages/utils/src/abi_encoder/utils/rules.ts b/packages/utils/src/abi_encoder/utils/rules.ts new file mode 100644 index 0000000000..31471e97ae --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/rules.ts @@ -0,0 +1,8 @@ +export interface DecodingRules { + structsAsObjects: boolean; +} + +export interface EncodingRules { + optimize?: boolean; + annotate?: boolean; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 0723e57886..082aff6bbe 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -10,3 +10,4 @@ export { NULL_BYTES } from './constants'; export { errorUtils } from './error_utils'; export { fetchAsync } from './fetch_async'; export { signTypedDataUtils } from './sign_typed_data_utils'; +export import AbiEncoder = require('./abi_encoder'); diff --git a/packages/utils/test/abi_encoder/abi_samples/method_abis.ts b/packages/utils/test/abi_encoder/abi_samples/method_abis.ts new file mode 100644 index 0000000000..fc552c1275 --- /dev/null +++ b/packages/utils/test/abi_encoder/abi_samples/method_abis.ts @@ -0,0 +1,780 @@ +/* tslint:disable max-file-line-count */ +import { MethodAbi } from 'ethereum-types'; + +export const simpleAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'greg', + type: 'uint256', + }, + { + name: 'gregStr', + type: 'string', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const stringAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'greg', + type: 'string[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const GAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'a', + type: 'uint256', + }, + { + name: 'b', + type: 'string', + }, + { + name: 'e', + type: 'bytes', + }, + { + name: 'f', + type: 'address', + }, + ], + + name: 'f', + type: 'tuple', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const typesWithDefaultWidthsAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'someUint', + type: 'uint', + }, + { + name: 'someInt', + type: 'int', + }, + { + name: 'someByte', + type: 'byte', + }, + { + name: 'someUint', + type: 'uint[]', + }, + { + name: 'someInt', + type: 'int[]', + }, + { + name: 'someByte', + type: 'byte[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const multiDimensionalArraysStaticTypeAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'a', + type: 'uint8[][][]', + }, + { + name: 'b', + type: 'uint8[][][2]', + }, + { + name: 'c', + type: 'uint8[][2][]', + }, + { + name: 'd', + type: 'uint8[2][][]', + }, + { + name: 'e', + type: 'uint8[][2][2]', + }, + { + name: 'f', + type: 'uint8[2][2][]', + }, + { + name: 'g', + type: 'uint8[2][][2]', + }, + { + name: 'h', + type: 'uint8[2][2][2]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const multiDimensionalArraysDynamicTypeAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'a', + type: 'string[][][]', + }, + { + name: 'b', + type: 'string[][][2]', + }, + { + name: 'c', + type: 'string[][2][]', + }, + { + name: 'h', + type: 'string[2][2][2]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const dynamicTupleAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someStr', + type: 'string', + }, + ], + name: 'order', + type: 'tuple', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const arrayOfStaticTuplesWithDefinedLengthAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someUint2', + type: 'uint256', + }, + ], + name: 'order', + type: 'tuple[8]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const arrayOfStaticTuplesWithDynamicLengthAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someUint2', + type: 'uint256', + }, + ], + name: 'order', + type: 'tuple[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const arrayOfDynamicTuplesWithDefinedLengthAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someString', + type: 'string', + }, + ], + name: 'order', + type: 'tuple[8]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const arrayOfDynamicTuplesWithUndefinedLengthAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someString', + type: 'string', + }, + ], + name: 'order', + type: 'tuple[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const arrayOfDynamicTuplesAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someString', + type: 'string', + }, + ], + name: 'order', + type: 'tuple[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const multidimensionalArrayOfDynamicTuplesAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someString', + type: 'string', + }, + ], + name: 'order', + type: 'tuple[][2][]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const staticTupleAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'someUint1', + type: 'uint256', + }, + { + name: 'someUint2', + type: 'uint256', + }, + { + name: 'someUint3', + type: 'uint256', + }, + { + name: 'someBool', + type: 'bool', + }, + ], + name: 'order', + type: 'tuple', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const staticArrayAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'someStaticArray', + type: 'uint8[3]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const staticArrayDynamicMembersAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'someStaticArray', + type: 'string[3]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const dynamicArrayDynamicMembersAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'someStaticArray', + type: 'string[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const dynamicArrayStaticMembersAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'someStaticArray', + type: 'uint8[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const largeFlatAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'someUInt256', + type: 'uint256', + }, + { + name: 'someInt256', + type: 'int256', + }, + { + name: 'someInt32', + type: 'int32', + }, + { + name: 'someByte', + type: 'byte', + }, + { + name: 'someBytes32', + type: 'bytes32', + }, + { + name: 'someBytes', + type: 'bytes', + }, + { + name: 'someString', + type: 'string', + }, + { + name: 'someAddress', + type: 'address', + }, + { + name: 'someBool', + type: 'bool', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const largeNestedAbi: MethodAbi = { + constant: false, + inputs: [ + { + name: 'someStaticArray', + type: 'uint8[3]', + }, + { + name: 'someStaticArrayWithDynamicMembers', + type: 'string[2]', + }, + { + name: 'someDynamicArrayWithDynamicMembers', + type: 'bytes[]', + }, + { + name: 'some2DArray', + type: 'string[][]', + }, + { + name: 'someTuple', + type: 'tuple', + components: [ + { + name: 'someUint32', + type: 'uint32', + }, + { + name: 'someStr', + type: 'string', + }, + ], + }, + { + name: 'someTupleWithDynamicTypes', + type: 'tuple', + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someStr', + type: 'string', + }, + /*{ + name: 'someStrArray', + type: 'string[]', + },*/ + { + name: 'someBytes', + type: 'bytes', + }, + { + name: 'someAddress', + type: 'address', + }, + ], + }, + { + name: 'someArrayOfTuplesWithDynamicTypes', + type: 'tuple[]', + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someStr', + type: 'string', + }, + /*{ + name: 'someStrArray', + type: 'string[]', + },*/ + { + name: 'someBytes', + type: 'bytes', + }, + { + name: 'someAddress', + type: 'address', + }, + ], + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const nestedTuples: MethodAbi = { + constant: false, + inputs: [ + { + name: 'firstTuple', + type: 'tuple[1]', + components: [ + { + name: 'someUint32', + type: 'uint32', + }, + { + name: 'nestedTuple', + type: 'tuple', + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someAddress', + type: 'address', + }, + ], + }, + ], + }, + { + name: 'secondTuple', + type: 'tuple[]', + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someStr', + type: 'string', + }, + { + name: 'nestedTuple', + type: 'tuple', + components: [ + { + name: 'someUint32', + type: 'uint32', + }, + { + name: 'secondNestedTuple', + type: 'tuple', + components: [ + { + name: 'someUint', + type: 'uint256', + }, + { + name: 'someStr', + type: 'string', + }, + { + name: 'someBytes', + type: 'bytes', + }, + { + name: 'someAddress', + type: 'address', + }, + ], + }, + ], + }, + { + name: 'someBytes', + type: 'bytes', + }, + { + name: 'someAddress', + type: 'address', + }, + ], + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const simpleAbi2: MethodAbi = { + constant: false, + inputs: [ + { + name: 'someByte', + type: 'byte', + }, + { + name: 'someBytes32', + type: 'bytes32', + }, + { + name: 'someBytes', + type: 'bytes', + }, + { + name: 'someString', + type: 'string', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const fillOrderAbi: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'makerAddress', + type: 'address', + }, + { + name: 'takerAddress', + type: 'address', + }, + { + name: 'feeRecipientAddress', + type: 'address', + }, + { + name: 'senderAddress', + type: 'address', + }, + { + name: 'makerAssetAmount', + type: 'uint256', + }, + { + name: 'takerAssetAmount', + type: 'uint256', + }, + { + name: 'makerFee', + type: 'uint256', + }, + { + name: 'takerFee', + type: 'uint256', + }, + { + name: 'expirationTimeSeconds', + type: 'uint256', + }, + { + name: 'salt', + type: 'uint256', + }, + { + name: 'makerAssetData', + type: 'bytes', + }, + { + name: 'takerAssetData', + type: 'bytes', + }, + ], + name: 'order', + type: 'tuple', + }, + { + name: 'takerAssetFillAmount', + type: 'uint256', + }, + { + name: 'salt', + type: 'uint256', + }, + { + name: 'orderSignature', + type: 'bytes', + }, + { + name: 'takerSignature', + type: 'bytes', + }, + ], + name: 'fillOrder', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; diff --git a/packages/utils/test/abi_encoder/abi_samples/optimizer_abis.ts b/packages/utils/test/abi_encoder/abi_samples/optimizer_abis.ts new file mode 100644 index 0000000000..7cfd7a1183 --- /dev/null +++ b/packages/utils/test/abi_encoder/abi_samples/optimizer_abis.ts @@ -0,0 +1,340 @@ +/* tslint:disable max-file-line-count */ +import { MethodAbi } from 'ethereum-types'; + +export const duplicateDynamicArraysWithStaticElements: MethodAbi = { + constant: false, + inputs: [ + { + name: 'array1', + type: 'uint[]', + }, + { + name: 'array2', + type: 'uint[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateDynamicArraysWithDynamicElements: MethodAbi = { + constant: false, + inputs: [ + { + name: 'array1', + type: 'string[]', + }, + { + name: 'array2', + type: 'string[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateStaticArraysWithStaticElements: MethodAbi = { + constant: false, + inputs: [ + { + name: 'array1', + type: 'uint[2]', + }, + { + name: 'array2', + type: 'uint[2]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateStaticArraysWithDynamicElements: MethodAbi = { + constant: false, + inputs: [ + { + name: 'array1', + type: 'string[2]', + }, + { + name: 'array2', + type: 'string[2]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateArrayElements: MethodAbi = { + constant: false, + inputs: [ + { + name: 'array', + type: 'string[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateTupleFields: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'field1', + type: 'string', + }, + { + name: 'field2', + type: 'string', + }, + ], + name: 'Tuple', + type: 'tuple', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateStrings: MethodAbi = { + constant: false, + inputs: [ + { + name: 'string1', + type: 'string', + }, + { + name: 'string2', + type: 'string', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateBytes: MethodAbi = { + constant: false, + inputs: [ + { + name: 'bytes1', + type: 'bytes', + }, + { + name: 'bytes2', + type: 'bytes', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateTuples: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'field1', + type: 'string', + }, + { + name: 'field2', + type: 'uint', + }, + ], + name: 'Tuple', + type: 'tuple', + }, + { + components: [ + { + name: 'field1', + type: 'string', + }, + { + name: 'field2', + type: 'uint', + }, + ], + name: 'Tuple', + type: 'tuple', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateArraysNestedInTuples: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + name: 'field', + type: 'uint[]', + }, + ], + name: 'Tuple1', + type: 'tuple', + }, + { + components: [ + { + name: 'field', + type: 'uint[]', + }, + { + name: 'extraField', + type: 'string', + }, + ], + name: 'Tuple2', + type: 'tuple', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateTuplesNestedInTuples: MethodAbi = { + constant: false, + inputs: [ + { + components: [ + { + components: [ + { + name: 'nestedField', + type: 'string', + }, + ], + name: 'field', + type: 'tuple', + }, + ], + name: 'Tuple1', + type: 'tuple', + }, + { + components: [ + { + components: [ + { + name: 'nestedField', + type: 'string', + }, + ], + name: 'field', + type: 'tuple', + }, + { + name: 'extraField', + type: 'string', + }, + ], + name: 'Tuple1', + type: 'tuple', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const duplicateTwoDimensionalArrays: MethodAbi = { + constant: false, + inputs: [ + { + name: 'array1', + type: 'string[][]', + }, + { + name: 'array2', + type: 'string[][]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const arrayElementsDuplicatedAsSeparateParameter: MethodAbi = { + constant: false, + inputs: [ + { + name: 'stringArray', + type: 'string[]', + }, + { + name: 'string', + type: 'string', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const arrayElementsDuplicatedAsTupleFields: MethodAbi = { + constant: false, + inputs: [ + { + name: 'uint8Array', + type: 'uint8[]', + }, + { + components: [ + { + name: 'uint', + type: 'uint', + }, + ], + name: 'uintTuple', + type: 'tuple[]', + }, + ], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; diff --git a/packages/utils/test/abi_encoder/abi_samples/return_value_abis.ts b/packages/utils/test/abi_encoder/abi_samples/return_value_abis.ts new file mode 100644 index 0000000000..ac21240112 --- /dev/null +++ b/packages/utils/test/abi_encoder/abi_samples/return_value_abis.ts @@ -0,0 +1,99 @@ +/* tslint:disable max-file-line-count */ +import { MethodAbi } from 'ethereum-types'; + +export const noReturnValues: MethodAbi = { + constant: false, + inputs: [], + name: 'simpleFunction', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const singleStaticReturnValue: MethodAbi = { + constant: false, + inputs: [], + name: 'simpleFunction', + outputs: [ + { + name: 'Bytes4', + type: 'bytes4', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const multipleStaticReturnValues: MethodAbi = { + constant: false, + inputs: [], + name: 'simpleFunction', + outputs: [ + { + name: 'val1', + type: 'bytes4', + }, + { + name: 'val2', + type: 'bytes4', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const singleDynamicReturnValue: MethodAbi = { + constant: false, + inputs: [], + name: 'simpleFunction', + outputs: [ + { + name: 'val', + type: 'bytes', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const multipleDynamicReturnValues: MethodAbi = { + constant: false, + inputs: [], + name: 'simpleFunction', + outputs: [ + { + name: 'val1', + type: 'bytes', + }, + { + name: 'val2', + type: 'bytes', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; + +export const mixedStaticAndDynamicReturnValues: MethodAbi = { + constant: false, + inputs: [], + name: 'simpleFunction', + outputs: [ + { + name: 'val1', + type: 'bytes4', + }, + { + name: 'val2', + type: 'bytes', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', +}; diff --git a/packages/utils/test/abi_encoder/evm_data_types_test.ts b/packages/utils/test/abi_encoder/evm_data_types_test.ts new file mode 100644 index 0000000000..9ef80a5602 --- /dev/null +++ b/packages/utils/test/abi_encoder/evm_data_types_test.ts @@ -0,0 +1,1007 @@ +/* tslint:disable max-file-line-count */ +import * as chai from 'chai'; +import * as ethUtil from 'ethereumjs-util'; +import 'mocha'; + +import { AbiEncoder, BigNumber } from '../../src/'; +import { chaiSetup } from '../utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('ABI Encoder: EVM Data Type Encoding/Decoding', () => { + const encodingRules: AbiEncoder.EncodingRules = { optimize: false }; // optimizer is tested separately. + describe('Array', () => { + it('Fixed size; Static elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'int[2]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const args = [new BigNumber(5), new BigNumber(6)]; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Dynamic size; Static elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'int[]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const args = [new BigNumber(5), new BigNumber(6)]; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Fixed size; Dynamic elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'string[2]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const args = ['Hello', 'world']; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Dynamic size; Dynamic elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'string[]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const args = ['Hello', 'world']; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005776f726c64000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Dynamic Size; Multidimensional; Dynamic Elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'bytes[][]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const array1 = ['0x01020304', '0x05060708', '0x09101112']; + const array2 = ['0x10111213', '0x14151617']; + const array3 = ['0x18192021']; + const args = [array1, array2, array3]; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000040102030400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000405060708000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004091011120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000041011121300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414151617000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000041819202100000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Dynamic Size; Multidimensional; Static Elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'bytes4[][]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const array1 = ['0x01020304', '0x05060708', '0x09101112']; + const array2 = ['0x10111213', '0x14151617']; + const array3 = ['0x18192021']; + const args = [array1, array2, array3]; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000301020304000000000000000000000000000000000000000000000000000000000506070800000000000000000000000000000000000000000000000000000000091011120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021011121300000000000000000000000000000000000000000000000000000000141516170000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011819202100000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Static Size; Multidimensional; Static Elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'bytes4[3][2]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const array1 = ['0x01020304', '0x05060708', '0x09101112']; + const array2 = ['0x10111213', '0x14151617', '0x18192021']; + const args = [array1, array2]; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x010203040000000000000000000000000000000000000000000000000000000005060708000000000000000000000000000000000000000000000000000000000910111200000000000000000000000000000000000000000000000000000000101112130000000000000000000000000000000000000000000000000000000014151617000000000000000000000000000000000000000000000000000000001819202100000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Static Size; Multidimensional; Dynamic Elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'bytes[3][2]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const array1 = ['0x01020304', '0x05060708', '0x09101112']; + const array2 = ['0x10111213', '0x14151617', '0x18192021']; + const args = [array1, array2]; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000401020304000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004050607080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040910111200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000410111213000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004141516170000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041819202100000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Static size; Too Few Elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'string[3]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const args = ['Hello', 'world']; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw('Expected array of 3 elements, but got array of length 2'); + }); + it('Static size; Too Many Elements', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'string[1]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const args = ['Hello', 'world']; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw('Expected array of 1 elements, but got array of length 2'); + }); + it('Element Type Mismatch', async () => { + // Create DataType object + const testDataItem = { name: 'testArray', type: 'uint[]' }; + const dataType = new AbiEncoder.Array(testDataItem); + // Construct args to be encoded + const args = [new BigNumber(1), 'Bad Argument']; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + }); + + describe('Tuple', () => { + it('Static elements only', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [{ name: 'field_1', type: 'int32' }, { name: 'field_2', type: 'bool' }], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const args = { field_1: new BigNumber(-5), field_2: true }; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb0000000000000000000000000000000000000000000000000000000000000001'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodingRules: AbiEncoder.DecodingRules = { structsAsObjects: true }; + const decodedArgs = dataType.decode(encodedArgs, decodingRules); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Dynamic elements only', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [{ name: 'field_1', type: 'string' }, { name: 'field_2', type: 'bytes' }], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const args = { field_1: 'Hello, World!', field_2: '0xabcdef0123456789' }; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20576f726c6421000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008abcdef0123456789000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodingRules: AbiEncoder.DecodingRules = { structsAsObjects: true }; + const decodedArgs = dataType.decode(encodedArgs, decodingRules); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Nested Static Array', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [{ name: 'field', type: 'uint[2]' }], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const args = { field: [new BigNumber(1), new BigNumber(2)] }; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodingRules: AbiEncoder.DecodingRules = { structsAsObjects: true }; + const decodedArgs = dataType.decode(encodedArgs, decodingRules); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Nested Dynamic Array', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [{ name: 'field', type: 'uint[]' }], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const args = { field: [new BigNumber(1), new BigNumber(2)] }; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodingRules: AbiEncoder.DecodingRules = { structsAsObjects: true }; + const decodedArgs = dataType.decode(encodedArgs, decodingRules); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Nested Static Multidimensional Array', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [{ name: 'field', type: 'bytes4[2][2]' }], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const array1 = ['0x01020304', '0x05060708']; + const array2 = ['0x09101112', '0x13141516']; + const args = { field: [array1, array2] }; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x0102030400000000000000000000000000000000000000000000000000000000050607080000000000000000000000000000000000000000000000000000000009101112000000000000000000000000000000000000000000000000000000001314151600000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodingRules: AbiEncoder.DecodingRules = { structsAsObjects: true }; + const decodedArgs = dataType.decode(encodedArgs, decodingRules); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Nested Dynamic Multidimensional Array', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [{ name: 'field', type: 'bytes[2][2]' }], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const array1 = ['0x01020304', '0x05060708']; + const array2 = ['0x09101112', '0x13141516']; + const args = { field: [array1, array2] }; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004010203040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040506070800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004091011120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041314151600000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodingRules: AbiEncoder.DecodingRules = { structsAsObjects: true }; + const decodedArgs = dataType.decode(encodedArgs, decodingRules); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Static and dynamic elements mixed', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [ + { name: 'field_1', type: 'int32' }, + { name: 'field_2', type: 'string' }, + { name: 'field_3', type: 'bool' }, + { name: 'field_4', type: 'bytes' }, + ], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const args = { + field_1: new BigNumber(-5), + field_2: 'Hello, World!', + field_3: true, + field_4: '0xabcdef0123456789', + }; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20576f726c6421000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008abcdef0123456789000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodingRules: AbiEncoder.DecodingRules = { structsAsObjects: true }; + const decodedArgs = dataType.decode(encodedArgs, decodingRules); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Missing Key', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [{ name: 'field_1', type: 'int32' }, { name: 'field_2', type: 'bool' }], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const args = { field_1: new BigNumber(-5) }; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw('Could not assign tuple to object: missing keys field_2'); + }); + it('Bad Key', async () => { + // Create DataType object + const testDataItem = { + name: 'Tuple', + type: 'tuple', + components: [{ name: 'field_1', type: 'int32' }, { name: 'field_2', type: 'bool' }], + }; + const dataType = new AbiEncoder.Tuple(testDataItem); + // Construct args to be encoded + const args = { unknown_field: new BigNumber(-5) }; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw("Could not assign tuple to object: unrecognized key 'unknown_field' in object Tuple"); + }); + }); + + describe('Address', () => { + it('Valid Address', async () => { + // Create DataType object + const testDataItem = { name: 'Address', type: 'address' }; + const dataType = new AbiEncoder.Address(testDataItem); + // Construct args to be encoded + const args = '0xe41d2489571d322189246dafa5ebde1f4699f498'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Invalid Address - input is not valid hex', async () => { + // Create DataType object + const testDataItem = { name: 'Address', type: 'address' }; + const dataType = new AbiEncoder.Address(testDataItem); + // Construct args to be encoded + const args = 'e4'; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(`Invalid address: '${args}'`); + }); + it('Invalid Address - input is not 20 bytes', async () => { + // Create DataType object + const testDataItem = { name: 'Address', type: 'address' }; + const dataType = new AbiEncoder.Address(testDataItem); + // Construct args to be encoded + const args = '0xe4'; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(`Invalid address: '${args}'`); + }); + }); + + describe('Bool', () => { + it('True', async () => { + // Create DataType object + const testDataItem = { name: 'Boolean', type: 'bool' }; + const dataType = new AbiEncoder.Bool(testDataItem); + // Construct args to be encoded + const args = true; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0000000000000000000000000000000000000000000000000000000000000001'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('False', async () => { + // Create DataType object + const testDataItem = { name: 'Boolean', type: 'bool' }; + const dataType = new AbiEncoder.Bool(testDataItem); + // Construct args to be encoded + const args = false; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0000000000000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + }); + + describe('Integer', () => { + /* tslint:disable custom-no-magic-numbers */ + const max256BitInteger = new BigNumber(2).pow(255).minus(1); + const min256BitInteger = new BigNumber(2).pow(255).times(-1); + const max32BitInteger = new BigNumber(2).pow(31).minus(1); + const min32BitInteger = new BigNumber(2).pow(31).times(-1); + /* tslint:enable custom-no-magic-numbers */ + + it('Int256 - Positive Base Case', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (256)', type: 'int' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = new BigNumber(1); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0000000000000000000000000000000000000000000000000000000000000001'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Int256 - Negative Base Case', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (256)', type: 'int' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = new BigNumber(-1); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Int256 - Positive Value', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (256)', type: 'int' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = max256BitInteger; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Int256 - Negative Value', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (256)', type: 'int' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = min256BitInteger; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = `0x8000000000000000000000000000000000000000000000000000000000000000`; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Int256 - Value too large', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (256)', type: 'int' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = max256BitInteger.plus(1); + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + it('Int256 - Value too small', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (256)', type: 'int' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = min256BitInteger.minus(1); + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + it('Int32 - Positive Base Case', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (32)', type: 'int32' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = new BigNumber(1); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0000000000000000000000000000000000000000000000000000000000000001'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Int32 - Negative Base Case', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (32)', type: 'int32' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = new BigNumber(-1); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Int32 - Positive Value', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (32)', type: 'int32' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = max32BitInteger; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x000000000000000000000000000000000000000000000000000000007fffffff'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Int32 - Negative Value', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (32)', type: 'int32' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = min32BitInteger; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = `0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff80000000`; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Int32 - Value too large', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (32)', type: 'int32' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = max32BitInteger.plus(1); + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + it('Int32 - Value too small', async () => { + // Create DataType object + const testDataItem = { name: 'Integer (32)', type: 'int32' }; + const dataType = new AbiEncoder.Int(testDataItem); + // Construct args to be encoded + const args = min32BitInteger.minus(1); + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + }); + + describe('Unsigned Integer', () => { + /* tslint:disable custom-no-magic-numbers */ + const max256BitUnsignedInteger = new BigNumber(2).pow(256).minus(1); + const min256BitUnsignedInteger = new BigNumber(0); + const max32BitUnsignedInteger = new BigNumber(2).pow(32).minus(1); + const min32BitUnsignedInteger = new BigNumber(0); + /* tslint:enable custom-no-magic-numbers */ + + it('UInt256 - Positive Base Case', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (256)', type: 'uint' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = new BigNumber(1); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0000000000000000000000000000000000000000000000000000000000000001'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('UInt256 - Positive Value', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (256)', type: 'uint' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = max256BitUnsignedInteger; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('UInt256 - Zero Value', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (256)', type: 'uint' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = min256BitUnsignedInteger; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = `0x0000000000000000000000000000000000000000000000000000000000000000`; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('UInt256 - Value too large', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (256)', type: 'uint' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = max256BitUnsignedInteger.plus(1); + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + it('UInt256 - Value too small', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (256)', type: 'uint' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = min256BitUnsignedInteger.minus(1); + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + it('UInt32 - Positive Base Case', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (32)', type: 'uint32' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = new BigNumber(1); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0000000000000000000000000000000000000000000000000000000000000001'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('UInt32 - Positive Value', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (32)', type: 'uint32' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = max32BitUnsignedInteger; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x00000000000000000000000000000000000000000000000000000000ffffffff'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('UInt32 - Zero Value', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (32)', type: 'uint32' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = min32BitUnsignedInteger; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = `0x0000000000000000000000000000000000000000000000000000000000000000`; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('UInt32 - Value too large', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (32)', type: 'uint32' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = max32BitUnsignedInteger.plus(1); + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + it('UInt32 - Value too small', async () => { + // Create DataType object + const testDataItem = { name: 'Unsigned Integer (32)', type: 'uint32' }; + const dataType = new AbiEncoder.UInt(testDataItem); + // Construct args to be encoded + const args = min32BitUnsignedInteger.minus(1); + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw(); + }); + }); + + describe('Static Bytes', () => { + it('Single Byte (byte)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Byte', type: 'byte' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + const args = '0x05'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0500000000000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Single Byte (bytes1)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes1', type: 'bytes1' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + const args = '0x05'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0500000000000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('4 Bytes (bytes4)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes4', type: 'bytes4' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + const args = '0x00010203'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0001020300000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('4 Bytes (bytes4); Encoder must pad input', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes4', type: 'bytes4' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + // Note: There will be padding because this is a bytes32 but we are only passing in 4 bytes. + const args = '0x1a18'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x1a18000000000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + const paddedArgs = '0x1a180000'; + expect(decodedArgs).to.be.deep.equal(paddedArgs); + }); + it('32 Bytes (bytes32)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes32', type: 'bytes32' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + const args = '0x0001020304050607080911121314151617181920212223242526272829303132'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x0001020304050607080911121314151617181920212223242526272829303132'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('32 Bytes (bytes32); Encoder must pad input', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes32', type: 'bytes32' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + // Note: There will be padding because this is a bytes32 but we are only passing in 4 bytes. + const args = '0x1a18bf61'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = '0x1a18bf6100000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + const paddedArgs = '0x1a18bf6100000000000000000000000000000000000000000000000000000000'; + expect(decodedArgs).to.be.deep.equal(paddedArgs); + }); + it('Should throw when pass in too many bytes (bytes4)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes4', type: 'bytes4' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + const args = '0x0102030405'; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw( + 'Tried to assign 0x0102030405 (5 bytes), which exceeds max bytes that can be stored in a bytes4', + ); + }); + it('Should throw when pass in too many bytes (bytes32)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes32', type: 'bytes32' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + const args = '0x010203040506070809101112131415161718192021222324252627282930313233'; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw( + 'Tried to assign 0x010203040506070809101112131415161718192021222324252627282930313233 (33 bytes), which exceeds max bytes that can be stored in a bytes32', + ); + }); + it('Should throw when pass in bad hex (no 0x prefix)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes32', type: 'bytes32' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + const args = '0102030405060708091011121314151617181920212223242526272829303132'; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw("Tried to encode non-hex value. Value must inlcude '0x' prefix."); + }); + it('Should throw when pass in bad hex (include a half-byte)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes32', type: 'bytes32' }; + const dataType = new AbiEncoder.StaticBytes(testDataItem); + // Construct args to be encoded + const args = '0x010'; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw('Tried to assign 0x010, which is contains a half-byte. Use full bytes only.'); + }); + }); + + describe('Dynamic Bytes', () => { + it('Fits into one EVM word', async () => { + // Create DataType object + const testDataItem = { name: 'Dynamic Bytes', type: 'bytes' }; + const dataType = new AbiEncoder.DynamicBytes(testDataItem); + // Construct args to be encoded + // Note: There will be padding because this is a bytes32 but we are only passing in 4 bytes. + const args = '0x1a18bf61'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x00000000000000000000000000000000000000000000000000000000000000041a18bf6100000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Spans multiple EVM words', async () => { + // Create DataType object + const testDataItem = { name: 'Dynamic Bytes', type: 'bytes' }; + const dataType = new AbiEncoder.DynamicBytes(testDataItem); + // Construct args to be encoded + // Note: There will be padding because this is a bytes32 but we are only passing in 4 bytes. + const bytesLength = 40; + const args = '0x' + '61'.repeat(bytesLength); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x000000000000000000000000000000000000000000000000000000000000002861616161616161616161616161616161616161616161616161616161616161616161616161616161000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Input as Buffer', async () => { + // Create DataType object + const testDataItem = { name: 'Dynamic Bytes', type: 'bytes' }; + const dataType = new AbiEncoder.DynamicBytes(testDataItem); + // Construct args to be encoded + // Note: There will be padding because this is a bytes32 but we are only passing in 4 bytes. + const args = '0x1a18bf61'; + const argsAsBuffer = ethUtil.toBuffer(args); + // Encode Args and validate result + const encodedArgs = dataType.encode(argsAsBuffer); + const expectedEncodedArgs = + '0x00000000000000000000000000000000000000000000000000000000000000041a18bf6100000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Should throw when pass in bad hex (no 0x prefix)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes', type: 'bytes' }; + const dataType = new AbiEncoder.DynamicBytes(testDataItem); + // Construct args to be encoded + const args = '01'; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw("Tried to encode non-hex value. Value must inlcude '0x' prefix."); + }); + it('Should throw when pass in bad hex (include a half-byte)', async () => { + // Create DataType object + const testDataItem = { name: 'Static Bytes', type: 'bytes' }; + const dataType = new AbiEncoder.DynamicBytes(testDataItem); + // Construct args to be encoded + const args = '0x010'; + // Encode Args and validate result + expect(() => { + dataType.encode(args, encodingRules); + }).to.throw('Tried to assign 0x010, which is contains a half-byte. Use full bytes only.'); + }); + }); + + describe('String', () => { + it('Fits into one EVM word', async () => { + // Create DataType object + const testDataItem = { name: 'String', type: 'string' }; + const dataType = new AbiEncoder.String(testDataItem); + // Construct args to be encoded + // Note: There will be padding because this is a bytes32 but we are only passing in 4 bytes. + const args = 'five'; + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x00000000000000000000000000000000000000000000000000000000000000046669766500000000000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Spans multiple EVM words', async () => { + // Create DataType object + const testDataItem = { name: 'String', type: 'string' }; + const dataType = new AbiEncoder.String(testDataItem); + // Construct args to be encoded + // Note: There will be padding because this is a bytes32 but we are only passing in 4 bytes. + const bytesLength = 40; + const args = 'a'.repeat(bytesLength); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x000000000000000000000000000000000000000000000000000000000000002861616161616161616161616161616161616161616161616161616161616161616161616161616161000000000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('String that begins with 0x prefix', async () => { + // Create DataType object + const testDataItem = { name: 'String', type: 'string' }; + const dataType = new AbiEncoder.String(testDataItem); + // Construct args to be encoded + // Note: There will be padding because this is a bytes32 but we are only passing in 4 bytes. + const strLength = 40; + const args = '0x' + 'a'.repeat(strLength); + // Encode Args and validate result + const encodedArgs = dataType.encode(args, encodingRules); + const expectedEncodedArgs = + '0x000000000000000000000000000000000000000000000000000000000000002a30786161616161616161616161616161616161616161616161616161616161616161616161616161616100000000000000000000000000000000000000000000'; + expect(encodedArgs).to.be.equal(expectedEncodedArgs); + // Decode Encoded Args and validate result + const decodedArgs = dataType.decode(encodedArgs); + expect(decodedArgs).to.be.deep.equal(args); + }); + }); +}); diff --git a/packages/utils/test/abi_encoder/methods_test.ts b/packages/utils/test/abi_encoder/methods_test.ts new file mode 100644 index 0000000000..8370208836 --- /dev/null +++ b/packages/utils/test/abi_encoder/methods_test.ts @@ -0,0 +1,366 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { AbiEncoder, BigNumber } from '../../src/'; +import { chaiSetup } from '../utils/chai_setup'; + +import * as AbiSamples from './abi_samples/method_abis'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('ABI Encoder: Method Encoding / Decoding', () => { + const encodingRules: AbiEncoder.EncodingRules = { optimize: false }; // optimizer is tested separately. + it('Types with default widths', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.typesWithDefaultWidthsAbi); + const args = [new BigNumber(1), new BigNumber(-1), '0x56', [new BigNumber(1)], [new BigNumber(-1)], ['0x56']]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x09f2b0c30000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff560000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000015600000000000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Array of Static Tuples (Array has defined length)', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.arrayOfStaticTuplesWithDefinedLengthAbi); + let value = 0; + const arrayOfTuples = []; + const arrayOfTuplesLength = 8; + for (let i = 0; i < arrayOfTuplesLength; ++i) { + arrayOfTuples.push([new BigNumber(++value), new BigNumber(++value)]); + } + const args = [arrayOfTuples]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x9eb20969000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000010'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Array of Static Tuples (Array has dynamic length)', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.arrayOfStaticTuplesWithDynamicLengthAbi); + let value = 0; + const arrayOfTuples = []; + const arrayOfTuplesLength = 8; + for (let i = 0; i < arrayOfTuplesLength; ++i) { + arrayOfTuples.push([new BigNumber(++value), new BigNumber(++value)]); + } + const args = [arrayOfTuples]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x63275d6e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000010'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Array of Dynamic Tuples (Array has defined length)', async () => { + // Generate Calldata + const method = new AbiEncoder.Method(AbiSamples.arrayOfDynamicTuplesWithDefinedLengthAbi); + let value = 0; + const arrayOfTuples = []; + const arrayOfTuplesLength = 8; + for (let i = 0; i < arrayOfTuplesLength; ++i) { + arrayOfTuples.push([new BigNumber(++value), new BigNumber(++value).toString()]); + } + const args = [arrayOfTuples]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0xdeedb00f00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000132000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000013400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000138000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000023130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000023132000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000023134000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000023136000000000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Array of Dynamic Tuples (Array has dynamic length)', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.arrayOfDynamicTuplesWithUndefinedLengthAbi); + let value = 0; + const arrayOfTuples = []; + const arrayOfTuplesLength = 8; + for (let i = 0; i < arrayOfTuplesLength; ++i) { + arrayOfTuples.push([new BigNumber(++value), new BigNumber(++value).toString()]); + } + const args = [arrayOfTuples]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x60c847fb000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000132000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000013400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000138000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000023130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000023132000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000023134000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000023136000000000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Multidimensional Arrays / Static Members', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.multiDimensionalArraysStaticTypeAbi); + // Eight 3-dimensional arrays of uint8[2][2][2] + let value = 0; + const args = []; + const argsLength = 8; + for (let i = 0; i < argsLength; ++i) { + args.push([ + [[new BigNumber(++value), new BigNumber(++value)], [new BigNumber(++value), new BigNumber(++value)]], + [[new BigNumber(++value), new BigNumber(++value)], [new BigNumber(++value), new BigNumber(++value)]], + ]); + } + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0xc2f47d6f00000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000480000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000d400000000000000000000000000000000000000000000000000000000000000e600000000000000000000000000000000000000000000000000000000000000039000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000003b000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000003d000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000003f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000130000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001500000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001700000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000019000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001d000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000001f000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000230000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000027000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000029000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000002b000000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000002d000000000000000000000000000000000000000000000000000000000000002e000000000000000000000000000000000000000000000000000000000000002f0000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003100000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000033000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000035000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000370000000000000000000000000000000000000000000000000000000000000038'; + expect(calldata).to.be.equal(expectedCalldata); + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Multidimensional Arrays / Dynamic Members', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.multiDimensionalArraysDynamicTypeAbi); + // Eight 3-dimensional arrays of string[2][2][2] + let value = 0; + const args = []; + const argsLength = 4; + for (let i = 0; i < argsLength; ++i) { + args.push([ + [ + [new BigNumber(++value).toString(), new BigNumber(++value).toString()], + [new BigNumber(++value).toString(), new BigNumber(++value).toString()], + ], + [ + [new BigNumber(++value).toString(), new BigNumber(++value).toString()], + [new BigNumber(++value).toString(), new BigNumber(++value).toString()], + ], + ]); + } + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x81534ebd0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000000000000000000000000000009a00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000134000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000137000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000139000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002313000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023131000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000231320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002313300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023134000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000231350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002313600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000231370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002313800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023139000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000232300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023231000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000232320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002323300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000232350000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002323600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000232370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002323800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002323900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002333100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023332000000000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Fixed Length Array / Dynamic Members', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.staticArrayDynamicMembersAbi); + const args = [['Brave', 'New', 'World']]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x243a6e6e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000005427261766500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034e657700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c64000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Fixed Length Array / Dynamic Members', async () => { + // Generaet calldata + const method = new AbiEncoder.Method(AbiSamples.staticArrayDynamicMembersAbi); + const args = [['Brave', 'New', 'World']]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x243a6e6e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000005427261766500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034e657700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c64000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Unfixed Length Array / Dynamic Members ABI', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.dynamicArrayDynamicMembersAbi); + const args = [['Brave', 'New', 'World']]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x13e751a900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000005427261766500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034e657700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c64000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Unfixed Length Array / Static Members ABI', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.dynamicArrayStaticMembersAbi); + const args = [[new BigNumber(127), new BigNumber(14), new BigNumber(54)]]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x4fc8a83300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000036'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Fixed Length Array / Static Members ABI', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.staticArrayAbi); + const args = [[new BigNumber(127), new BigNumber(14), new BigNumber(54)]]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0xf68ade72000000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000036'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Array ABI', async () => { + // Generate calldata + const method = new AbiEncoder.Method(AbiSamples.stringAbi); + const args = [['five', 'six', 'seven']]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x13e751a900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000046669766500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000373697800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005736576656e000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Static Tuple', async () => { + // Generate calldata + // This is dynamic because it has dynamic members + const method = new AbiEncoder.Method(AbiSamples.staticTupleAbi); + const args = [[new BigNumber(5), new BigNumber(10), new BigNumber(15), false]]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0xa9125e150000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Dynamic Tuple (Array input)', async () => { + // Generate calldata + // This is dynamic because it has dynamic members + const method = new AbiEncoder.Method(AbiSamples.dynamicTupleAbi); + const args = [[new BigNumber(5), 'five']]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x5b998f3500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000046669766500000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Dynamic Tuple (Object input)', async () => { + // Generate Calldata + // This is dynamic because it has dynamic members + const method = new AbiEncoder.Method(AbiSamples.dynamicTupleAbi); + const args = [[new BigNumber(5), 'five']]; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x5b998f3500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000046669766500000000000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Large, Flat ABI', async () => { + // Construct calldata + const method = new AbiEncoder.Method(AbiSamples.largeFlatAbi); + const args = [ + new BigNumber(256745454), + new BigNumber(-256745454), + new BigNumber(434244), + '0x43', + '0x0001020304050607080911121314151617181920212223242526272829303132', + '0x0001020304050607080911121314151617181920212223242526272829303132080911121314151617181920212223242526272829303132', + 'Little peter piper piped a piping pepper pot', + '0xe41d2489571d322189246dafa5ebde1f4699f498', + true, + ]; + // Validate calldata + const calldata = method.encode(args, encodingRules); + const expectedCalldata = + '0x312d4d42000000000000000000000000000000000000000000000000000000000f4d9feefffffffffffffffffffffffffffffffffffffffffffffffffffffffff0b26012000000000000000000000000000000000000000000000000000000000006a0444300000000000000000000000000000000000000000000000000000000000000000102030405060708091112131415161718192021222324252627282930313200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f4980000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000003800010203040506070809111213141516171819202122232425262728293031320809111213141516171819202122232425262728293031320000000000000000000000000000000000000000000000000000000000000000000000000000002c4c6974746c65207065746572207069706572207069706564206120706970696e672070657070657220706f740000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata); + expect(decodedValue).to.be.deep.equal(args); + }); + it('Large, Nested ABI', async () => { + // Construct Calldata + const method = new AbiEncoder.Method(AbiSamples.largeNestedAbi); + const someStaticArray = [new BigNumber(127), new BigNumber(14), new BigNumber(54)]; + const someStaticArrayWithDynamicMembers = [ + 'the little piping piper piped a piping pipper papper', + 'the kid knows how to write poems, what can I say -- I guess theres a lot I could say to try to fill this line with a lot of text.', + ]; + const someDynamicArrayWithDynamicMembers = [ + '0x38745637834987324827439287423897238947239847', + '0x7283472398237423984723984729847248927498748974284728947239487498749847874329423743492347329847239842374892374892374892347238947289478947489374289472894738942749823743298742389472389473289472389437249823749823742893472398', + '0x283473298473248923749238742398742398472894729843278942374982374892374892743982', + ]; + const some2DArray = [ + [ + 'some string', + 'some another string', + 'there are just too many stringsup in', + 'here', + 'yall ghonna make me lose my mind', + ], + [ + 'the little piping piper piped a piping pipper papper', + 'the kid knows how to write poems, what can I say -- I guess theres a lot I could say to try to fill this line with a lot of text.', + ], + [], + ]; + const someTuple = { + someUint32: new BigNumber(4037824789), + someStr: + 'the kid knows how to write poems, what can I say -- I guess theres a lot I could say to try to fill this line with a lot of text.', + }; + const someTupleWithDynamicTypes = { + someUint: new BigNumber(4024789), + someStr: 'akdhjasjkdhasjkldshdjahdkjsahdajksdhsajkdhsajkdhadjkashdjksadhajkdhsajkdhsadjk', + someBytes: '0x29384723894723843743289742389472398473289472348927489274894738427428947389facdea', + someAddress: '0xe41d2489571d322189246dafa5ebde1f4699f498', + }; + const someTupleWithDynamicTypes2 = { + someUint: new BigNumber(9024789), + someStr: 'ksdhsajkdhsajkdhadjkashdjksadhajkdhsajkdhsadjkakdhjasjkdhasjkldshdjahdkjsahdaj', + someBytes: '0x29384723894398473289472348927489272384374328974238947274894738427428947389facde1', + someAddress: '0x746dafa5ebde1f4699f4981d3221892e41d24895', + }; + const someTupleWithDynamicTypes3 = { + someUint: new BigNumber(1024789), + someStr: 'sdhsajkdhsajkdhadjkashdjakdhjasjkdhasjkldshdjahdkjsahdajkksadhajkdhsajkdhsadjk', + someBytes: '0x38947238437432829384729742389472398473289472348927489274894738427428947389facdef', + someAddress: '0x89571d322189e415ebde1f4699f498d24246dafa', + }; + const someArrayOfTuplesWithDynamicTypes = [someTupleWithDynamicTypes2, someTupleWithDynamicTypes3]; + const args = { + someStaticArray, + someStaticArrayWithDynamicMembers, + someDynamicArrayWithDynamicMembers, + some2DArray, + someTuple, + someTupleWithDynamicTypes, + someArrayOfTuplesWithDynamicTypes, + }; + const calldata = method.encode(args, encodingRules); + // Validate calldata + const expectedCalldata = + '0x4b49031c000000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000440000000000000000000000000000000000000000000000000000000000000088000000000000000000000000000000000000000000000000000000000000009800000000000000000000000000000000000000000000000000000000000000ae0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000034746865206c6974746c6520706970696e67207069706572207069706564206120706970696e6720706970706572207061707065720000000000000000000000000000000000000000000000000000000000000000000000000000000000000081746865206b6964206b6e6f777320686f7720746f20777269746520706f656d732c20776861742063616e204920736179202d2d2049206775657373207468657265732061206c6f74204920636f756c642073617920746f2074727920746f2066696c6c2074686973206c696e6520776974682061206c6f74206f6620746578742e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000163874563783498732482743928742389723894723984700000000000000000000000000000000000000000000000000000000000000000000000000000000006e72834723982374239847239847298472489274987489742847289472394874987498478743294237434923473298472398423748923748923748923472389472894789474893742894728947389427498237432987423894723894732894723894372498237498237428934723980000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027283473298473248923749238742398742398472894729843278942374982374892374892743982000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000000b736f6d6520737472696e670000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013736f6d6520616e6f7468657220737472696e67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024746865726520617265206a75737420746f6f206d616e7920737472696e6773757020696e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046865726500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002079616c6c2067686f6e6e61206d616b65206d65206c6f7365206d79206d696e640000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000034746865206c6974746c6520706970696e67207069706572207069706564206120706970696e6720706970706572207061707065720000000000000000000000000000000000000000000000000000000000000000000000000000000000000081746865206b6964206b6e6f777320686f7720746f20777269746520706f656d732c20776861742063616e204920736179202d2d2049206775657373207468657265732061206c6f74204920636f756c642073617920746f2074727920746f2066696c6c2074686973206c696e6520776974682061206c6f74206f6620746578742e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f0ac511500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000081746865206b6964206b6e6f777320686f7720746f20777269746520706f656d732c20776861742063616e204920736179202d2d2049206775657373207468657265732061206c6f74204920636f756c642073617920746f2074727920746f2066696c6c2074686973206c696e6520776974682061206c6f74206f6620746578742e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d69d500000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498000000000000000000000000000000000000000000000000000000000000004e616b64686a61736a6b646861736a6b6c647368646a6168646b6a73616864616a6b73646873616a6b646873616a6b646861646a6b617368646a6b73616468616a6b646873616a6b64687361646a6b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002829384723894723843743289742389472398473289472348927489274894738427428947389facdea0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000089b51500000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000746dafa5ebde1f4699f4981d3221892e41d24895000000000000000000000000000000000000000000000000000000000000004e6b73646873616a6b646873616a6b646861646a6b617368646a6b73616468616a6b646873616a6b64687361646a6b616b64686a61736a6b646861736a6b6c647368646a6168646b6a73616864616a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002829384723894398473289472348927489272384374328974238947274894738427428947389facde100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa3150000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000089571d322189e415ebde1f4699f498d24246dafa000000000000000000000000000000000000000000000000000000000000004e73646873616a6b646873616a6b646861646a6b617368646a616b64686a61736a6b646861736a6b6c647368646a6168646b6a73616864616a6b6b73616468616a6b646873616a6b64687361646a6b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002838947238437432829384729742389472398473289472348927489274894738427428947389facdef000000000000000000000000000000000000000000000000'; + expect(calldata).to.be.equal(expectedCalldata); + // Validate decoding + const decodedValue = method.decode(calldata, { structsAsObjects: true }); + expect(decodedValue).to.be.deep.equal(args); + }); +}); diff --git a/packages/utils/test/abi_encoder/optimizer_test.ts b/packages/utils/test/abi_encoder/optimizer_test.ts new file mode 100644 index 0000000000..18aa6549ae --- /dev/null +++ b/packages/utils/test/abi_encoder/optimizer_test.ts @@ -0,0 +1,262 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { AbiEncoder, BigNumber } from '../../src/'; +import { chaiSetup } from '../utils/chai_setup'; + +import * as OptimizedAbis from './abi_samples/optimizer_abis'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('ABI Encoder: Optimized Method Encoding/Decoding', () => { + const encodingRules: AbiEncoder.EncodingRules = { optimize: true }; + it('Duplicate Dynamic Arrays with Static Elements', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateDynamicArraysWithStaticElements); + const array1 = [new BigNumber(100), new BigNumber(150)]; + const array2 = array1; + const args = [array1, array2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x7221063300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000096'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Dynamic Arrays with Dynamic Elements', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateDynamicArraysWithDynamicElements); + const array1 = ['Hello', 'World']; + const array2 = array1; + const args = [array1, array2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0xbb4f12e300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c64000000000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Static Arrays with Static Elements (should not optimize)', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateStaticArraysWithStaticElements); + const array1 = [new BigNumber(100), new BigNumber(150)]; + const array2 = array1; + const args = [array1, array2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x7f8130430000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000096'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + const unoptimizedCalldata = method.encode(args); + expect(optimizedCalldata).to.be.equal(unoptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Static Arrays with Dynamic Elements', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateStaticArraysWithDynamicElements); + const array1 = ['Hello', 'World']; + const array2 = array1; + const args = [array1, array2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x9fe31f8e0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c64000000000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Array Elements (should optimize)', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateArrayElements); + const strings = ['Hello', 'World', 'Hello', 'World']; + const args = [strings]; + // Validate calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x13e751a900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c64000000000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Tuple Fields', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateTupleFields); + const tuple = ['Hello', 'Hello']; + const args = [tuple]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x16780a5e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Strings', async () => { + // Description: + // Two dynamic arrays with the same values. + // In the optimized calldata, only one set of elements should be included. + // Both arrays should point to this set. + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateStrings); + const args = ['Hello', 'Hello']; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x07370bfa00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Bytes', async () => { + // Description: + // Two dynamic arrays with the same values. + // In the optimized calldata, only one set of elements should be included. + // Both arrays should point to this set. + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateBytes); + const value = '0x01020304050607080910111213141516171819202122232425262728293031323334353637383940'; + const args = [value, value]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x6045e42900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002801020304050607080910111213141516171819202122232425262728293031323334353637383940000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Tuples', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateTuples); + const tuple1 = ['Hello, World!', new BigNumber(424234)]; + const tuple2 = tuple1; + const args = [tuple1, tuple2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x564f826d000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000006792a000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20576f726c642100000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Fields Across Two Tuples', async () => { + // Description: + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateTuples); + const tuple1 = ['Hello, World!', new BigNumber(1)]; + const tuple2 = [tuple1[0], new BigNumber(2)]; + const args = [tuple1, tuple2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x564f826d000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20576f726c642100000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Arrays, Nested in Separate Tuples', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateArraysNestedInTuples); + const array = [new BigNumber(100), new BigNumber(150), new BigNumber(200)]; + const tuple1 = [array]; + const tuple2 = [array, 'extra argument to prevent exactly matching the tuples']; + const args = [tuple1, tuple2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x18970a9e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000c80000000000000000000000000000000000000000000000000000000000000035657874726120617267756d656e7420746f2070726576656e742065786163746c79206d61746368696e6720746865207475706c65730000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Tuples, Nested in Separate Tuples', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateTuplesNestedInTuples); + const nestedTuple = ['Hello, World!']; + const tuple1 = [nestedTuple]; + const tuple2 = [nestedTuple, 'extra argument to prevent exactly matching the tuples']; + const args = [tuple1, tuple2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x0b4d2e6a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20576f726c6421000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035657874726120617267756d656e7420746f2070726576656e742065786163746c79206d61746368696e6720746865207475706c65730000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Two-Dimensional Arrays', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateTwoDimensionalArrays); + const twoDimArray1 = [['Hello', 'World'], ['Foo', 'Bar', 'Zaa']]; + const twoDimArray2 = twoDimArray1; + const args = [twoDimArray1, twoDimArray2]; + // Validata calldata + const optimizedCalldata = method.encode(args, { optimize: false }); + const expectedOptimizedCalldata = + '0x0d28c4f9000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000003466f6f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003426172000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a61610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000003466f6f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003426172000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035a61610000000000000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Duplicate Array, Nested within Separate Two-Dimensional Arrays', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.duplicateTwoDimensionalArrays); + const twoDimArray1 = [['Hello', 'World'], ['Foo']]; + const twoDimArray2 = [['Hello', 'World'], ['Bar']]; + const args = [twoDimArray1, twoDimArray2]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x0d28c4f900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003466f6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000548656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005576f726c640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000034261720000000000000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Array Elements Duplicated as Tuple Fields', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.arrayElementsDuplicatedAsTupleFields); + const array = [new BigNumber(100), new BigNumber(150), new BigNumber(200), new BigNumber(225)]; + const tuple = [[array[0]], [array[1]], [array[2]], [array[3]]]; + const args = [array, tuple]; + // Validata calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0x5b5c78fd0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000e1'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); + it('Array Elements Duplicated as Separate Parameter', async () => { + // Generate calldata + const method = new AbiEncoder.Method(OptimizedAbis.arrayElementsDuplicatedAsSeparateParameter); + const array = ['Hello', 'Hello', 'Hello', 'World']; + const str = 'Hello'; + const args = [array, str]; + // Validate calldata + const optimizedCalldata = method.encode(args, encodingRules); + const expectedOptimizedCalldata = + '0xe0e0d34900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000005576f726c64000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000'; + expect(optimizedCalldata).to.be.equal(expectedOptimizedCalldata); + // Validate decoding + const decodedArgs = method.decode(optimizedCalldata); + expect(decodedArgs).to.be.deep.equal(args); + }); +}); diff --git a/packages/utils/test/abi_encoder/return_values_test.ts b/packages/utils/test/abi_encoder/return_values_test.ts new file mode 100644 index 0000000000..a8cdd6ca35 --- /dev/null +++ b/packages/utils/test/abi_encoder/return_values_test.ts @@ -0,0 +1,67 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { AbiEncoder } from '../../src/'; +import { chaiSetup } from '../utils/chai_setup'; + +import * as ReturnValueAbis from './abi_samples/return_value_abis'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('ABI Encoder: Return Value Encoding/Decoding', () => { + const encodingRules: AbiEncoder.EncodingRules = { optimize: false }; // optimizer is tested separately. + it('No Return Value', async () => { + // Decode return value + const method = new AbiEncoder.Method(ReturnValueAbis.noReturnValues); + const returnValue = '0x'; + const decodedReturnValue = method.decodeReturnValues(returnValue); + const expectedDecodedReturnValue: any[] = []; + expect(decodedReturnValue).to.be.deep.equal(expectedDecodedReturnValue); + }); + it('Single static return value', async () => { + // Generate Return Value + const method = new AbiEncoder.Method(ReturnValueAbis.singleStaticReturnValue); + const returnValue = ['0x01020304']; + const encodedReturnValue = method.encodeReturnValues(returnValue, encodingRules); + const decodedReturnValue = method.decodeReturnValues(encodedReturnValue); + // Validate decoded return value + expect(decodedReturnValue).to.be.deep.equal(returnValue); + }); + it('Multiple static return values', async () => { + // Generate Return Value + const method = new AbiEncoder.Method(ReturnValueAbis.multipleStaticReturnValues); + const returnValue = ['0x01020304', '0x05060708']; + const encodedReturnValue = method.encodeReturnValues(returnValue, encodingRules); + const decodedReturnValue = method.decodeReturnValues(encodedReturnValue); + // Validate decoded return value + expect(decodedReturnValue).to.be.deep.equal(returnValue); + }); + it('Single dynamic return value', async () => { + // Generate Return Value + const method = new AbiEncoder.Method(ReturnValueAbis.singleDynamicReturnValue); + const returnValue = ['0x01020304']; + const encodedReturnValue = method.encodeReturnValues(returnValue, encodingRules); + const decodedReturnValue = method.decodeReturnValues(encodedReturnValue); + // Validate decoded return value + expect(decodedReturnValue).to.be.deep.equal(returnValue); + }); + it('Multiple dynamic return values', async () => { + // Generate Return Value + const method = new AbiEncoder.Method(ReturnValueAbis.multipleDynamicReturnValues); + const returnValue = ['0x01020304', '0x05060708']; + const encodedReturnValue = method.encodeReturnValues(returnValue, encodingRules); + const decodedReturnValue = method.decodeReturnValues(encodedReturnValue); + // Validate decoded return value + expect(decodedReturnValue).to.be.deep.equal(returnValue); + }); + it('Mixed static/dynamic return values', async () => { + // Generate Return Value + const method = new AbiEncoder.Method(ReturnValueAbis.mixedStaticAndDynamicReturnValues); + const returnValue = ['0x01020304', '0x05060708']; + const encodedReturnValue = method.encodeReturnValues(returnValue, encodingRules); + const decodedReturnValue = method.decodeReturnValues(encodedReturnValue); + // Validate decoded return value + expect(decodedReturnValue).to.be.deep.equal(returnValue); + }); +}); diff --git a/packages/utils/test/utils/chai_setup.ts b/packages/utils/test/utils/chai_setup.ts new file mode 100644 index 0000000000..1a87330932 --- /dev/null +++ b/packages/utils/test/utils/chai_setup.ts @@ -0,0 +1,13 @@ +import * as chai from 'chai'; +import chaiAsPromised = require('chai-as-promised'); +import ChaiBigNumber = require('chai-bignumber'); +import * as dirtyChai from 'dirty-chai'; + +export const chaiSetup = { + configure(): void { + chai.config.includeStack = true; + chai.use(ChaiBigNumber()); + chai.use(dirtyChai); + chai.use(chaiAsPromised); + }, +}; diff --git a/packages/website/md/docs/smart_contracts/1/introduction.md b/packages/website/md/docs/smart_contracts/1/introduction.md index 79a8f00fd9..81715a3d14 100644 --- a/packages/website/md/docs/smart_contracts/1/introduction.md +++ b/packages/website/md/docs/smart_contracts/1/introduction.md @@ -1 +1 @@ -Welcome to the [0x smart contracts](https://github.com/0xProject/0x-monorepo/tree/development/packages/contracts) documentation! This documentation is intended for dApp developers who want to integrate 0x exchange functionality directly into their own smart contracts. +Welcome to the [0x smart contracts](https://github.com/0xProject/0x-monorepo/tree/development/contracts/core) documentation! This documentation is intended for dApp developers who want to integrate 0x exchange functionality directly into their own smart contracts. diff --git a/packages/website/md/docs/smart_contracts/2/introduction.md b/packages/website/md/docs/smart_contracts/2/introduction.md index 4aa31db3db..0b4e2aac62 100644 --- a/packages/website/md/docs/smart_contracts/2/introduction.md +++ b/packages/website/md/docs/smart_contracts/2/introduction.md @@ -1,4 +1,4 @@ -Welcome to the [0x smart contracts](https://github.com/0xProject/0x-monorepo/tree/development/packages/contracts) documentation! This documentation is intended for dApp developers who want to integrate 0x exchange functionality directly into their own smart contracts. +Welcome to the [0x smart contracts](https://github.com/0xProject/0x-monorepo/tree/development/contracts/core) documentation! This documentation is intended for dApp developers who want to integrate 0x exchange functionality directly into their own smart contracts. ### Helpful wiki articles: diff --git a/python-packages/order_utils/src/zero_ex/json_schemas/__init__.py b/python-packages/order_utils/src/zero_ex/json_schemas/__init__.py index 2a1728b8af..a76a2fa3b2 100644 --- a/python-packages/order_utils/src/zero_ex/json_schemas/__init__.py +++ b/python-packages/order_utils/src/zero_ex/json_schemas/__init__.py @@ -8,6 +8,51 @@ from pkg_resources import resource_string import jsonschema +class _LocalRefResolver(jsonschema.RefResolver): + """Resolve package-local JSON schema id's.""" + + def __init__(self): + """Initialize a new instance.""" + self.ref_to_file = { + "/addressSchema": "address_schema.json", + "/hexSchema": "hex_schema.json", + "/orderSchema": "order_schema.json", + "/wholeNumberSchema": "whole_number_schema.json", + "/ECSignature": "ec_signature_schema.json", + "/signedOrderSchema": "signed_order_schema.json", + "/ecSignatureParameterSchema": ( + "ec_signature_parameter_schema.json" + "" + ), + } + jsonschema.RefResolver.__init__(self, "", "") + + def resolve_from_url(self, url: str) -> str: + """Resolve the given URL. + + :param url: a string representing the URL of the JSON schema to fetch. + :returns: a string representing the deserialized JSON schema + :raises jsonschema.ValidationError: when the resource associated with + `url` does not exist. + """ + ref = url.replace("file://", "") + if ref in self.ref_to_file: + return json.loads( + resource_string( + "zero_ex.json_schemas", f"schemas/{self.ref_to_file[ref]}" + ) + ) + raise jsonschema.ValidationError( + f"Unknown ref '{ref}'. " + + f"Known refs: {list(self.ref_to_file.keys())}." + ) + + +# Instantiate the `_LocalRefResolver()` only once so that `assert_valid()` can +# perform multiple schema validations without reading from disk the schema +# every time. +_LOCAL_RESOLVER = _LocalRefResolver() + + def assert_valid(data: Mapping, schema_id: str) -> None: """Validate the given `data` against the specified `schema`. @@ -24,38 +69,6 @@ def assert_valid(data: Mapping, schema_id: str) -> None: ... ) """ # noqa - class LocalRefResolver(jsonschema.RefResolver): - """Resolve package-local JSON schema id's.""" - def __init__(self): - self.ref_to_file = { - "/addressSchema": "address_schema.json", - "/hexSchema": "hex_schema.json", - "/orderSchema": "order_schema.json", - "/wholeNumberSchema": "whole_number_schema.json", - "/ECSignature": "ec_signature_schema.json", - "/ecSignatureParameterSchema": ( - "ec_signature_parameter_schema.json" + "" - ), - } - jsonschema.RefResolver.__init__(self, "", "") - - def resolve_from_url(self, url): - """Resolve the given URL.""" - ref = url.replace("file://", "") - if ref in self.ref_to_file: - return json.loads( - resource_string( - "zero_ex.json_schemas", - f"schemas/{self.ref_to_file[ref]}", - ) - ) - raise jsonschema.ValidationError( - f"Unknown ref '{ref}'. " - + f"Known refs: {list(self.ref_to_file.keys())}." - ) - - resolver = LocalRefResolver() - jsonschema.validate( - data, resolver.resolve_from_url(schema_id), resolver=resolver - ) + _, schema = _LOCAL_RESOLVER.resolve(schema_id) + jsonschema.validate(data, schema, resolver=_LOCAL_RESOLVER) diff --git a/python-packages/order_utils/stubs/jsonschema/__init__.pyi b/python-packages/order_utils/stubs/jsonschema/__init__.pyi index 762b58b224..442e2f65e5 100644 --- a/python-packages/order_utils/stubs/jsonschema/__init__.pyi +++ b/python-packages/order_utils/stubs/jsonschema/__init__.pyi @@ -1,5 +1,11 @@ -from typing import Any, Dict +from typing import Any, Dict, Tuple -class RefResolver: pass + +class RefResolver: + def resolve(self, url: str) -> Tuple[str, Dict]: + ... + + +class ValidationError(Exception): pass def validate(instance: Any, schema: Dict, cls=None, *args, **kwargs) -> None: pass diff --git a/python-packages/order_utils/test/test_doctest.py b/python-packages/order_utils/test/test_doctest.py index f692b3b6cf..297f75e756 100644 --- a/python-packages/order_utils/test/test_doctest.py +++ b/python-packages/order_utils/test/test_doctest.py @@ -2,16 +2,17 @@ from doctest import testmod import pkgutil +import importlib import zero_ex def test_all_doctests(): """Gather zero_ex.* modules and doctest them.""" - for (importer, modname, _) in pkgutil.walk_packages( + for (_, modname, _) in pkgutil.walk_packages( path=zero_ex.__path__, prefix="zero_ex." ): - module = importer.find_module(modname).load_module(modname) + module = importlib.import_module(modname) print(module) (failure_count, _) = testmod(module) assert failure_count == 0 diff --git a/python-packages/order_utils/test/test_json_schemas.py b/python-packages/order_utils/test/test_json_schemas.py new file mode 100644 index 0000000000..51cecbd4f5 --- /dev/null +++ b/python-packages/order_utils/test/test_json_schemas.py @@ -0,0 +1,23 @@ +"""Tests of zero_ex.json_schemas""" + + +from zero_ex.order_utils import make_empty_order +from zero_ex.json_schemas import _LOCAL_RESOLVER, assert_valid + + +def test_assert_valid_caches_resources(): + """Test that the JSON ref resolver in `assert_valid()` caches resources + + In order to test the cache we much access the private class of + `json_schemas` and reset the LRU cache on `_LocalRefResolver`. + For this to happen, we need to disable errror `W0212` + on _LOCAL_RESOLVER + """ + _LOCAL_RESOLVER._remote_cache.cache_clear() # pylint: disable=W0212 + + assert_valid(make_empty_order(), "/orderSchema") + cache_info = ( + _LOCAL_RESOLVER._remote_cache.cache_info() # pylint: disable=W0212 + ) + assert cache_info.currsize == 4 + assert cache_info.hits == 10 diff --git a/tsconfig.json b/tsconfig.json index eaaca8e4ec..3d2dc6da76 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,11 +30,10 @@ { "path": "./packages/contract-addresses" }, { "path": "./packages/contract-artifacts" }, { "path": "./packages/contract-wrappers" }, - { "path": "./packages/contracts" }, + { "path": "./contracts/core" }, { "path": "./packages/dev-utils" }, { "path": "./packages/ethereum-types" }, { "path": "./packages/fill-scenarios" }, - { "path": "./packages/instant" }, { "path": "./packages/json-schemas" }, { "path": "./packages/metacoin" }, { "path": "./packages/migrations" }, @@ -57,5 +56,8 @@ // Skipping website because it requires allowJs: false and this is // incompatible with project references. // { "path": "./packages/website" } + // Skipping instant because it only produces a UMD bundle + // which it uses webpack to create + // { "path": "./packages/instant" }, ] } diff --git a/yarn.lock b/yarn.lock index e07aa724d8..ed01009aa0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -478,7 +478,7 @@ dependencies: npm-registry-client "7.0.9" -"@babel/code-frame@^7.0.0-beta.35": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35": version "7.0.0" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" dependencies: @@ -1248,8 +1248,8 @@ bignumber.js "7.2.1" "@types/ethereumjs-abi@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@types/ethereumjs-abi/-/ethereumjs-abi-0.6.0.tgz#72d21083a36d9288821b62905e04b15e0c12175d" + version "0.6.1" + resolved "https://registry.yarnpkg.com/@types/ethereumjs-abi/-/ethereumjs-abi-0.6.1.tgz#68dad80888a6e9f12b04f85adcb9cad2cb825e5c" dependencies: "@types/node" "*" @@ -1342,7 +1342,7 @@ "@types/js-combinatorics@^0.5.29": version "0.5.29" - resolved "https://registry.yarnpkg.com/@types/js-combinatorics/-/js-combinatorics-0.5.29.tgz#47a7819a0b6925b6dc4bd2c2278a7e6329b29387" + resolved "http://registry.npmjs.org/@types/js-combinatorics/-/js-combinatorics-0.5.29.tgz#47a7819a0b6925b6dc4bd2c2278a7e6329b29387" "@types/jsonschema@^1.1.1": version "1.1.1" @@ -1874,28 +1874,18 @@ acorn-globals@^4.1.0: acorn "^6.0.1" acorn-walk "^6.0.1" -acorn-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" - dependencies: - acorn "^3.0.4" +acorn-jsx@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e" acorn-walk@^6.0.1: version "6.1.0" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz#c957f4a1460da46af4a0388ce28b4c99355b0cbc" -acorn@^3.0.4: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" - acorn@^5.0.0: version "5.5.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" -acorn@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" - acorn@^5.5.3, acorn@^5.6.2: version "5.7.3" resolved "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" @@ -1904,6 +1894,10 @@ acorn@^6.0.1: version "6.0.2" resolved "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz#6a459041c320ab17592c6317abbfdf4bbaa98ca4" +acorn@^6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754" + aes-js@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" @@ -1914,7 +1908,7 @@ aes-js@^0.2.3: aes-js@^3.1.1: version "3.1.2" - resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: version "4.2.1" @@ -1932,10 +1926,6 @@ ajv-errors@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" -ajv-keywords@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" - ajv-keywords@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" @@ -1947,7 +1937,7 @@ ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0: +ajv@^5.1.0, ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: @@ -1965,6 +1955,15 @@ ajv@^6.1.0: json-schema-traverse "^0.3.0" uri-js "^3.0.2" +ajv@^6.5.3: + version "6.5.5" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1" + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -2039,9 +2038,9 @@ ansi@^0.3.0, ansi@~0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" -antlr4@4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.7.0.tgz#297f956ddc06f83397fc0990ecf2e0cf20bfbbee" +antlr4@4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.7.1.tgz#69984014f096e9e775f53dd9744bf994d8959773" any-observable@^0.3.0: version "0.3.0" @@ -3386,7 +3385,7 @@ bs58check@^1.0.8: bs58check@^2.1.2: version "2.1.2" - resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" dependencies: bs58 "^4.0.0" create-hash "^1.1.0" @@ -3593,7 +3592,7 @@ caller-path@^0.1.0: callsites@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + resolved "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" callsites@^2.0.0: version "2.0.0" @@ -4111,6 +4110,10 @@ commander@2.15.1, commander@^2.12.1, commander@^2.8.1: version "2.15.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" +commander@2.18.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" + commander@^2.15.1: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -4529,14 +4532,14 @@ cross-fetch@^2.1.0: node-fetch "2.1.1" whatwg-fetch "2.0.3" -cross-spawn@^4: +cross-spawn@^4, cross-spawn@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" dependencies: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^5.0.1, cross-spawn@^5.1.0: +cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: @@ -4836,6 +4839,12 @@ debug@3.1.0, debug@=3.1.0, debug@^3.1.0: dependencies: ms "2.0.0" +debug@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" + dependencies: + ms "^2.1.1" + debuglog@^1.0.1: version "1.0.1" resolved "http://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5026,18 +5035,6 @@ defined@^1.0.0, defined@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" -del@^2.0.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" - dependencies: - globby "^5.0.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - rimraf "^2.2.8" - del@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" @@ -5294,6 +5291,14 @@ dot-prop@^4.1.0, dot-prop@^4.2.0: dependencies: is-obj "^1.0.0" +dotenv-cli@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-1.4.0.tgz#e8e80830ed88b48a03b5eb7ec26147ca717f7409" + dependencies: + cross-spawn "^4.0.0" + dotenv "^4.0.0" + minimist "^1.1.3" + dotenv@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" @@ -5597,13 +5602,6 @@ escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" -eslint-scope@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - eslint-scope@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" @@ -5611,59 +5609,70 @@ eslint-scope@^4.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-utils@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512" + eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@^4.19.1: - version "4.19.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" +eslint@^5.6.0: + version "5.9.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.9.0.tgz#b234b6d15ef84b5849c6de2af43195a2d59d408e" dependencies: - ajv "^5.3.0" - babel-code-frame "^6.22.0" + "@babel/code-frame" "^7.0.0" + ajv "^6.5.3" chalk "^2.1.0" - concat-stream "^1.6.0" - cross-spawn "^5.1.0" - debug "^3.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" doctrine "^2.1.0" - eslint-scope "^3.7.1" + eslint-scope "^4.0.0" + eslint-utils "^1.3.1" eslint-visitor-keys "^1.0.0" - espree "^3.5.4" - esquery "^1.0.0" + espree "^4.0.0" + esquery "^1.0.1" esutils "^2.0.2" file-entry-cache "^2.0.0" functional-red-black-tree "^1.0.1" glob "^7.1.2" - globals "^11.0.1" - ignore "^3.3.3" + globals "^11.7.0" + ignore "^4.0.6" imurmurhash "^0.1.4" - inquirer "^3.0.6" - is-resolvable "^1.0.0" - js-yaml "^3.9.1" + inquirer "^6.1.0" + is-resolvable "^1.1.0" + js-yaml "^3.12.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.3.0" - lodash "^4.17.4" - minimatch "^3.0.2" + lodash "^4.17.5" + minimatch "^3.0.4" mkdirp "^0.5.1" natural-compare "^1.4.0" optionator "^0.8.2" path-is-inside "^1.0.2" pluralize "^7.0.0" progress "^2.0.0" - regexpp "^1.0.1" + regexpp "^2.0.1" require-uncached "^1.0.3" - semver "^5.3.0" + semver "^5.5.1" strip-ansi "^4.0.0" - strip-json-comments "~2.0.1" - table "4.0.2" - text-table "~0.2.0" + strip-json-comments "^2.0.1" + table "^5.0.2" + text-table "^0.2.0" -espree@^3.5.4: - version "3.5.4" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" +espree@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-4.1.0.tgz#728d5451e0fd156c04384a7ad89ed51ff54eb25f" dependencies: - acorn "^5.5.0" - acorn-jsx "^3.0.0" + acorn "^6.0.2" + acorn-jsx "^5.0.0" + eslint-visitor-keys "^1.0.0" + +esprima-extract-comments@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/esprima-extract-comments/-/esprima-extract-comments-1.1.0.tgz#0dacab567a5900240de6d344cf18c33617becbc9" + dependencies: + esprima "^4.0.0" esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" @@ -5677,7 +5686,7 @@ esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" -esquery@^1.0.0: +esquery@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" dependencies: @@ -5996,7 +6005,7 @@ ethereumjs-wallet@0.6.0: ethereumjs-wallet@~0.6.0: version "0.6.2" - resolved "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda" + resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda" dependencies: aes-js "^3.1.1" bs58check "^2.1.2" @@ -6303,6 +6312,13 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +extract-comments@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/extract-comments/-/extract-comments-1.0.0.tgz#ad4e640704d8a9a124faf8776b47735ff092a593" + dependencies: + esprima-extract-comments "^1.0.1" + parse-code-context "^0.2.2" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -6333,6 +6349,14 @@ fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + fast-glob@^2.0.2: version "2.2.1" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.1.tgz#686c2345be88f3741e174add0be6f2e5b6078889" @@ -6577,12 +6601,12 @@ flagged-respawn@^1.0.0: resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.0.tgz#4e79ae9b2eb38bf86b3bb56bf3e0a56aa5fcabd7" flat-cache@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" + version "1.3.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" dependencies: circular-json "^0.3.1" - del "^2.0.2" graceful-fs "^4.1.2" + rimraf "~2.6.2" write "^0.2.1" flatten@^1.0.2: @@ -7120,6 +7144,17 @@ glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glo once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.1.3, glob@^7.1.3: + version "7.1.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^4.3.1: version "4.5.3" resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" @@ -7139,17 +7174,6 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3: - version "7.1.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@~3.1.21: version "3.1.21" resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" @@ -7193,25 +7217,14 @@ global@^4.3.0, global@~4.3.0: min-document "^2.19.0" process "~0.5.1" -globals@^11.0.1: - version "11.7.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" +globals@^11.7.0: + version "11.9.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.9.0.tgz#bde236808e987f290768a93d065060d78e6ab249" globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" -globby@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" - dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -7565,7 +7578,7 @@ hdkey@^0.7.0, hdkey@^0.7.1: hdkey@^1.0.0: version "1.1.0" - resolved "https://registry.npmjs.org/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29" + resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29" dependencies: coinstring "^2.0.0" safe-buffer "^5.1.1" @@ -7843,14 +7856,14 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" -ignore@^3.3.3, ignore@^3.3.7: - version "3.3.10" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" - ignore@^3.3.5: version "3.3.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" @@ -7965,7 +7978,7 @@ inquirer@^0.8.2: rx "^2.4.3" through "^2.3.6" -inquirer@^3.0.6, inquirer@^3.3.0: +inquirer@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" dependencies: @@ -8002,7 +8015,7 @@ inquirer@^5.1.0: strip-ansi "^4.0.0" through "^2.3.6" -inquirer@^6.2.0: +inquirer@^6.1.0, inquirer@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" dependencies: @@ -8382,7 +8395,7 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" -is-resolvable@^1.0.0: +is-resolvable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" @@ -8938,8 +8951,8 @@ js-base64@^2.1.9: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" js-combinatorics@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/js-combinatorics/-/js-combinatorics-0.5.3.tgz#5da5a1c4632ec59fdf8d49dccfe59ef088122b15" + version "0.5.4" + resolved "https://registry.yarnpkg.com/js-combinatorics/-/js-combinatorics-0.5.4.tgz#c92916b8f8171b64ecd7c4435b72cfabc803c756" js-scrypt@^0.2.0: version "0.2.0" @@ -8978,7 +8991,7 @@ js-yaml@3.x, js-yaml@^3.4.2, js-yaml@^3.6.1, js-yaml@^3.7.0: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^3.12.0, js-yaml@^3.9.0, js-yaml@^3.9.1: +js-yaml@^3.12.0, js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: @@ -9078,6 +9091,10 @@ json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -9850,7 +9867,7 @@ lodash@^3.3.1, lodash@^3.6.0, lodash@^3.7.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0: +lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.11: version "4.17.11" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -10535,7 +10552,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" @@ -11507,6 +11524,10 @@ parse-asn1@^5.0.0: evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" +parse-code-context@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/parse-code-context/-/parse-code-context-0.2.2.tgz#144b8afb7219482d7e88c1eb6a765596f3a6ac0d" + parse-entities@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.1.1.tgz#8112d88471319f27abae4d64964b122fe4e1b890" @@ -12085,10 +12106,28 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + dependencies: + fast-diff "^1.1.2" + +prettier-plugin-solidity@^1.0.0-alpha.4: + version "1.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-alpha.11.tgz#ef078a80dd471437693a0508de7691dd3ba814af" + dependencies: + extract-comments "^1.0.0" + prettier "^1.14.2" + solidity-parser-antlr "^0.3.1" + prettier@^1.11.1: version "1.12.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" +prettier@^1.14.2, prettier@^1.14.3: + version "1.15.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.2.tgz#d31abe22afa4351efa14c7f8b94b58bb7452205e" + pretty-bytes@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" @@ -13052,9 +13091,9 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexpp@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" regexpu-core@^1.0.0: version "1.0.0" @@ -13306,7 +13345,7 @@ require-package-name@^2.0.1: require-uncached@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + resolved "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" dependencies: caller-path "^0.1.0" resolve-from "^1.0.0" @@ -13416,7 +13455,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2: +rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -13477,6 +13516,21 @@ rollbar@^2.4.7: optionalDependencies: decache "^3.0.5" +rollbar@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/rollbar/-/rollbar-2.5.0.tgz#dbb513eea34f1e3bae57611810c3e6df0ef50a12" + dependencies: + async "~1.2.1" + console-polyfill "0.3.0" + debug "2.6.9" + error-stack-parser "1.3.3" + json-stringify-safe "~5.0.0" + lru-cache "~2.2.1" + request-ip "~2.0.1" + uuid "3.0.x" + optionalDependencies: + decache "^3.0.5" + rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" @@ -13722,6 +13776,10 @@ semver@^4.1.0: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" +semver@^5.5.1: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + semver@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" @@ -14061,7 +14119,7 @@ socks@~2.2.0: ip "^1.1.5" smart-buffer "^4.0.1" -solc@0.4.24, solc@^0.4.24: +solc@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/solc/-/solc-0.4.24.tgz#354f14b269b38cbaa82a47d1ff151723502b954e" dependencies: @@ -14091,21 +14149,40 @@ solc@^0.4.23: semver "^5.3.0" yargs "^4.7.1" -solhint@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/solhint/-/solhint-1.2.1.tgz#59a1416cef94da38d587f768a73536d6e3403dd3" +solc@^0.4.24: + version "0.4.25" + resolved "https://registry.yarnpkg.com/solc/-/solc-0.4.25.tgz#06b8321f7112d95b4b903639b1138a4d292f5faa" dependencies: - antlr4 "4.7.0" - commander "2.11.0" - eslint "^4.19.1" - glob "7.1.2" - ignore "^3.3.7" - lodash "^4.17.10" + fs-extra "^0.30.0" + memorystream "^0.3.1" + require-from-string "^1.1.0" + semver "^5.3.0" + yargs "^4.7.1" + +solhint@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-1.4.0.tgz#59018cfc86e2fc268c8b520322ab1e0db1fdb94b" + dependencies: + antlr4 "4.7.1" + commander "2.18.0" + eslint "^5.6.0" + fast-diff "^1.1.2" + glob "7.1.3" + ignore "^4.0.6" + lodash "^4.17.11" + prettier-linter-helpers "^1.0.0" + optionalDependencies: + prettier "^1.14.3" + prettier-plugin-solidity "^1.0.0-alpha.4" solidity-parser-antlr@^0.2.12: version "0.2.12" resolved "https://registry.yarnpkg.com/solidity-parser-antlr/-/solidity-parser-antlr-0.2.12.tgz#1154f183d5cdda2c7677549ee584dbdb7fb2269c" +solidity-parser-antlr@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/solidity-parser-antlr/-/solidity-parser-antlr-0.3.2.tgz#1cf9d019280550a31299dc380e87a310dc4ca154" + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -14542,7 +14619,7 @@ strip-indent@^2.0.0: version "2.0.0" resolved "http://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" -strip-json-comments@~2.0.1: +strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -14745,14 +14822,12 @@ symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" -table@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" +table@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/table/-/table-5.1.0.tgz#69a54644f6f01ad1628f8178715b408dc6bf11f7" dependencies: - ajv "^5.2.3" - ajv-keywords "^2.1.0" - chalk "^2.1.0" - lodash "^4.17.4" + ajv "^6.5.3" + lodash "^4.17.10" slice-ansi "1.0.0" string-width "^2.1.1" @@ -14931,7 +15006,7 @@ text-extensions@^1.0.0: version "1.7.0" resolved "http://registry.yarnpkg.com/text-extensions/-/text-extensions-1.7.0.tgz#faaaba2625ed746d568a23e4d0aacd9bf08a8b39" -text-table@~0.2.0: +text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -15674,6 +15749,12 @@ uri-js@^3.0.2: dependencies: punycode "^2.1.0" +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + dependencies: + punycode "^2.1.0" + urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" @@ -15740,7 +15821,7 @@ utf8@^2.1.1: utf8@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" util-deprecate@~1.0.1: version "1.0.2"