diff options
Diffstat (limited to 'packages')
71 files changed, 6604 insertions, 268 deletions
diff --git a/packages/contracts/CHANGELOG.json b/packages/contracts/CHANGELOG.json index 00f94c83b..7dfa06990 100644 --- a/packages/contracts/CHANGELOG.json +++ b/packages/contracts/CHANGELOG.json @@ -1,5 +1,15 @@ [ { + "name": "MultiAssetProxy", + "version": "1.0.0", + "changes": [ + { + "note": "Add MultiAssetProxy implementation", + "pr": 1224 + } + ] + }, + { "name": "OrderValidator", "version": "1.0.1", "changes": [ diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index af3980b4e..c824e4645 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -38,6 +38,7 @@ "IValidator", "IWallet", "MixinAuthorizable", + "MultiAssetProxy", "MultiSigWallet", "MultiSigWalletWithTimeLock", "OrderValidator", diff --git a/packages/contracts/contracts/protocol/AssetProxy/MultiAssetProxy.sol b/packages/contracts/contracts/protocol/AssetProxy/MultiAssetProxy.sol new file mode 100644 index 000000000..42231e73b --- /dev/null +++ b/packages/contracts/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/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol index 3e76e38dd..e2da68919 100644 --- a/packages/contracts/contracts/protocol/AssetProxy/interfaces/IAssetData.sol +++ b/packages/contracts/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/package.json b/packages/contracts/package.json index 6f031fcee..16dd3c9c1 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/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", diff --git a/packages/contracts/src/artifacts/index.ts b/packages/contracts/src/artifacts/index.ts index c30972a91..97c1b6209 100644 --- a/packages/contracts/src/artifacts/index.ts +++ b/packages/contracts/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/test/asset_proxy/proxies.ts b/packages/contracts/test/asset_proxy/proxies.ts index b8305993e..8fa1e602a 100644 --- a/packages/contracts/test/asset_proxy/proxies.ts +++ b/packages/contracts/test/asset_proxy/proxies.ts @@ -12,7 +12,9 @@ import { DummyMultipleReturnERC20TokenContract } from '../../generated-wrappers/ 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'; @@ -30,26 +32,35 @@ const assetProxyInterface = new IAssetProxyContract( 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 exchangeAddress: string; - let makerAddress: string; - let takerAddress: string; + let authorized: string; + let fromAddress: string; + let toAddress: string; - let zrxToken: DummyERC20TokenContract; - let erc721Token: DummyERC721TokenContract; + 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 erc721MakerTokenId: BigNumber; + let erc721AFromTokenId: BigNumber; + let erc721BFromTokenId: BigNumber; before(async () => { await blockchainLifecycle.startAsync(); @@ -59,41 +70,73 @@ describe('Asset Transfer Proxies', () => { }); before(async () => { const accounts = await web3Wrapper.getAvailableAddressesAsync(); - const usedAddresses = ([owner, notAuthorized, exchangeAddress, makerAddress, takerAddress] = _.slice( - accounts, - 0, - 5, - )); + const usedAddresses = ([owner, notAuthorized, authorized, fromAddress, toAddress] = _.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); + // Deploy AssetProxies erc20Proxy = await erc20Wrapper.deployProxyAsync(); - await erc20Wrapper.setBalancesAndAllowancesAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync( + artifacts.MultiAssetProxy, + provider, + txDefaults, + ); + + // Configure ERC20Proxy await web3Wrapper.awaitTransactionSuccessAsync( - await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { + 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, ); - [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); - erc721Proxy = await erc721Wrapper.deployProxyAsync(); - await erc721Wrapper.setBalancesAndAllowancesAsync(); - const erc721Balances = await erc721Wrapper.getBalancesAsync(); - erc721MakerTokenId = erc721Balances[makerAddress][erc721Token.address][0]; + // Configure ERC721Proxy await web3Wrapper.awaitTransactionSuccessAsync( - await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeAddress, { + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(authorized, { from: owner, }), constants.AWAIT_TRANSACTION_MINED_MS, ); - erc721Receiver = await DummyERC721ReceiverContract.deployFrom0xArtifactAsync( - artifacts.DummyERC721Receiver, - provider, - txDefaults, + 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, @@ -104,30 +147,32 @@ describe('Asset Transfer Proxies', () => { 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(makerAddress, constants.INITIAL_ERC20_BALANCE), + 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: makerAddress }, + { from: fromAddress }, ), 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, + fromAddress, constants.INITIAL_ERC20_BALANCE, ), constants.AWAIT_TRANSACTION_MINED_MS, @@ -136,10 +181,23 @@ describe('Asset Transfer Proxies', () => { await multipleReturnErc20Token.approve.sendTransactionAsync( erc20Proxy.address, constants.INITIAL_ERC20_ALLOWANCE, - { from: makerAddress }, + { 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(); @@ -147,7 +205,8 @@ describe('Asset Transfer Proxies', () => { afterEach(async () => { await blockchainLifecycle.revertAsync(); }); - describe('Transfer Proxy - ERC20', () => { + + describe('ERC20Proxy', () => { it('should revert if undefined function is called', async () => { const undefinedSelector = '0x01020304'; await expectTransactionFailedWithoutReasonAsync( @@ -159,141 +218,146 @@ describe('Asset Transfer Proxies', () => { }), ); }); + 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(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), 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[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(amount), ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address].add(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 makerAddress to takerAddress - const initialMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + // 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), 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)); + 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(zrxToken.address)}${extraData}`; - // Perform a transfer from makerAddress to takerAddress + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), 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[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address].minus(amount), ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address].add(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(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), 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[fromAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[fromAddress][erc20TokenA.address], ); - expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][zrxToken.address], + expect(newBalances[toAddress][erc20TokenA.address]).to.be.bignumber.equal( + erc20Balances[toAddress][erc20TokenA.address], ); }); - it('should throw if allowances are too low', async () => { + it('should revert if allowances are too low', async () => { // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( - await zrxToken.approve.sendTransactionAsync(erc20Proxy.address, allowance, { - from: makerAddress, + await erc20TokenA.approve.sendTransactionAsync(erc20Proxy.address, allowance, { + from: fromAddress, }), constants.AWAIT_TRANSACTION_MINED_MS, ); @@ -303,7 +367,7 @@ describe('Asset Transfer Proxies', () => { web3Wrapper.sendTransactionAsync({ to: erc20Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.TransferFailed, ); @@ -311,7 +375,7 @@ describe('Asset Transfer Proxies', () => { expect(newBalances).to.deep.equal(erc20Balances); }); - it('should throw if allowances are too low and token does not return a value', async () => { + 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. @@ -319,42 +383,42 @@ describe('Asset Transfer Proxies', () => { const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await noReturnErc20Token.approve.sendTransactionAsync(erc20Proxy.address, allowance, { - from: makerAddress, + from: fromAddress, }), constants.AWAIT_TRANSACTION_MINED_MS, ); - const initialMakerBalance = await noReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await noReturnErc20Token.balanceOf.callAsync(takerAddress); + 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: exchangeAddress, + from: authorized, }), 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); + 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 throw if requesting address is not authorized', async () => { + it('should revert if caller is not authorized', async () => { // Construct ERC20 asset data - const encodedAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - // Perform a transfer from makerAddress to takerAddress + const encodedAssetData = assetDataUtils.encodeERC20AssetData(erc20TokenA.address); + // Perform a transfer from fromAddress to toAddress const amount = new BigNumber(10); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); const erc20Balances = await erc20Wrapper.getBalancesAsync(); @@ -370,42 +434,36 @@ describe('Asset Transfer Proxies', () => { expect(newBalances).to.deep.equal(erc20Balances); }); - it('should throw if token returns more than 32 bytes', async () => { + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); - const initialMakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(makerAddress); - const initialTakerBalance = await multipleReturnErc20Token.balanceOf.callAsync(takerAddress); + 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: exchangeAddress, + from: authorized, }), 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); + 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); }); }); - - 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', () => { + describe('ERC721Proxy', () => { it('should revert if undefined function is called', async () => { const undefinedSelector = '0x01020304'; await expectTransactionFailedWithoutReasonAsync( @@ -417,76 +475,81 @@ describe('Asset Transfer Proxies', () => { }), ); }); + 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(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), constants.AWAIT_TRANSACTION_MINED_MS, ); // Verify transfer was successful - const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); + 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( - erc721Token.address, - erc721MakerTokenId, + erc721TokenA.address, + erc721AFromTokenId, )}${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 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), constants.AWAIT_TRANSACTION_MINED_MS, ); // Verify transfer was successful - const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress); + 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(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + 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, - makerAddress, + fromAddress, erc721Receiver.address, amount, ); @@ -495,79 +558,79 @@ describe('Asset Transfer Proxies', () => { await web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + 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 newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwnerMakerAsset).to.be.bignumber.equal(erc721Receiver.address); + const newOwnerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwnerFromAsset).to.be.bignumber.equal(erc721Receiver.address); }); - it('should throw if transferring 0 amount of a token', async () => { + it('should revert if transferring 0 amount of a token', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await expectTransactionFailedAsync( web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.InvalidAmount, ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); }); - it('should throw if transferring > 1 amount of a token', async () => { + it('should revert if transferring > 1 amount of a token', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await expectTransactionFailedAsync( web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.InvalidAmount, ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); }); - it('should throw if allowances are too low', async () => { + it('should revert if allowances are too low', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Remove transfer approval for makerAddress. + const ownerFromAsset = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(ownerFromAsset).to.be.equal(fromAddress); + // Remove transfer approval for fromAddress. await web3Wrapper.awaitTransactionSuccessAsync( - await erc721Token.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721MakerTokenId, { - from: makerAddress, + await erc721TokenA.approve.sendTransactionAsync(constants.NULL_ADDRESS, erc721AFromTokenId, { + from: fromAddress, }), constants.AWAIT_TRANSACTION_MINED_MS, ); @@ -575,34 +638,34 @@ describe('Asset Transfer Proxies', () => { const amount = new BigNumber(1); const data = assetProxyInterface.transferFrom.getABIEncodedTransactionData( encodedAssetData, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await expectTransactionFailedAsync( web3Wrapper.sendTransactionAsync({ to: erc721Proxy.address, data, - from: exchangeAddress, + from: authorized, }), RevertReason.TransferFailed, ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); }); - it('should throw if requesting address is not authorized', async () => { + it('should revert if caller is not authorized', async () => { // Construct ERC721 asset data - const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId); + const encodedAssetData = assetDataUtils.encodeERC721AssetData(erc721TokenA.address, erc721AFromTokenId); // Verify pre-condition - const ownerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(ownerMakerAsset).to.be.equal(makerAddress); - // Perform a transfer from makerAddress to takerAddress + 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, - makerAddress, - takerAddress, + fromAddress, + toAddress, amount, ); await expectTransactionFailedAsync( @@ -613,16 +676,570 @@ describe('Asset Transfer Proxies', () => { }), RevertReason.SenderNotAuthorized, ); - const newOwner = await erc721Token.ownerOf.callAsync(erc721MakerTokenId); - expect(newOwner).to.be.equal(ownerMakerAsset); + const newOwner = await erc721TokenA.ownerOf.callAsync(erc721AFromTokenId); + expect(newOwner).to.be.equal(ownerFromAsset); }); }); - - it('should have an id of 0x02571792', async () => { - const proxyId = await erc721Proxy.getProxyId.callAsync(); - const expectedProxyId = '0x02571792'; + }); + 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 diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index fc8dc5346..9159b0d8f 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/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,31 +84,39 @@ 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); + maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync( + artifacts.TestStaticCallReceiver, + provider, + txDefaults, + ); + reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.ReentrantERC20Token, + provider, + txDefaults, + exchange.address, + ); + // Configure ERC20Proxy await web3Wrapper.awaitTransactionSuccessAsync( await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner, @@ -108,27 +124,64 @@ describe('Exchange core', () => { 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, + ); - maliciousWallet = maliciousValidator = await TestStaticCallReceiverContract.deployFrom0xArtifactAsync( - artifacts.TestStaticCallReceiver, - provider, - txDefaults, + // Configure MultiAssetProxy + await web3Wrapper.awaitTransactionSuccessAsync( + await multiAssetProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }), + constants.AWAIT_TRANSACTION_MINED_MS, ); - reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync( - artifacts.ReentrantERC20Token, - provider, - txDefaults, - exchange.address, + 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/utils/constants.ts b/packages/contracts/test/utils/constants.ts index cd21330e9..d2c3ab512 100644 --- a/packages/contracts/test/utils/constants.ts +++ b/packages/contracts/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/erc721_wrapper.ts b/packages/contracts/test/utils/erc721_wrapper.ts index 3ef4e701d..e9da553d0 100644 --- a/packages/contracts/test/utils/erc721_wrapper.ts +++ b/packages/contracts/test/utils/erc721_wrapper.ts @@ -29,7 +29,8 @@ export class ERC721Wrapper { this._contractOwnerAddress = contractOwnerAddress; } public async deployDummyTokensAsync(): Promise<DummyERC721TokenContract[]> { - 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/tsconfig.json b/packages/contracts/tsconfig.json index 8b29365cc..e0f85079a 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/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/ethereum-types/src/index.ts b/packages/ethereum-types/src/index.ts index eff38711a..9430fdc98 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/src/components/erc20_token_selector.tsx b/packages/instant/src/components/erc20_token_selector.tsx index 0a3d4427a..f7d5a4fe4 100644 --- a/packages/instant/src/components/erc20_token_selector.tsx +++ b/packages/instant/src/components/erc20_token_selector.tsx @@ -19,12 +19,12 @@ export interface ERC20TokenSelectorProps { } export interface ERC20TokenSelectorState { - searchQuery?: string; + searchQuery: string; } export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps> { 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<ERC20TokenSelectorProps> }; 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 88c26f59c..481d82da0 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<InstallWalletPane default: break; } + const onActionClick = () => { + analytics.trackInstallWalletModalClickedGet(); + util.createOpenUrlInNewWindow(actionUrl)(); + }; return { image: <MetaMaskLogo width={85} height={80} />, title: 'Install MetaMask', @@ -52,10 +58,11 @@ export class InstallWalletPanelContent extends React.Component<InstallWalletPane moreInfoSettings: { href: META_MASK_SITE_URL, text: 'What is MetaMask?', + onClick: analytics.trackInstallWalletModalClickedExplanation, }, action: ( <Button - href={actionUrl} + onClick={onActionClick} width="100%" fontColor={ColorOption.white} backgroundColor={ColorOption.darkOrange} diff --git a/packages/instant/src/components/scaling_input.tsx b/packages/instant/src/components/scaling_input.tsx index 129162a74..791692257 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<ScalingInputProps, ScalingInpu inputWidthPx: this._getInputWidthInPx(), }; } + public componentDidMount(): void { + // Trigger an initial notification of the calculated fontSize. + const currentPhase = ScalingInput.getPhaseFromProps(this.props); + const currentFontSize = ScalingInput.calculateFontSizeFromProps(this.props, currentPhase); + this.props.onFontSizeChange(currentFontSize); + } public componentDidUpdate( prevProps: ScalingInputProps, prevState: ScalingInputState, diff --git a/packages/instant/src/components/standard_panel_content.tsx b/packages/instant/src/components/standard_panel_content.tsx index 582b3318e..79b7bff24 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<StandardPanelContentProps> = ({ image, title, @@ -50,7 +61,7 @@ export const StandardPanelContent: React.StatelessComponent<StandardPanelContent fontSize="13px" textDecorationLine="underline" fontColor={ColorOption.lightGrey} - href={moreInfoSettings.href} + onClick={onMoreInfoClick(moreInfoSettings.href, moreInfoSettings.onClick)} > {moreInfoSettings.text} </Text> diff --git a/packages/instant/src/components/timed_progress_bar.tsx b/packages/instant/src/components/timed_progress_bar.tsx index 8465b9cd0..fb3927088 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<ProgressBarProps & React.ClassAttributes<{}>> = React.forwardRef( (props, ref) => ( - <Container width="100%" backgroundColor={ColorOption.lightGrey} borderRadius="6px"> - <Progress {...props} ref={ref as any} /> - </Container> + <ThemeConsumer> + {theme => ( + <Container + width="100%" + borderRadius="6px" + rawBackgroundColor={transparentize(0.5, theme[ColorOption.primaryColor])} + > + <Progress {...props} ref={ref as any} /> + </Container> + )} + </ThemeConsumer> ), ); diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index 2143b0503..e7d909d92 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -27,6 +27,7 @@ export interface ContainerProps { borderBottom?: string; className?: string; backgroundColor?: ColorOption; + rawBackgroundColor?: string; hasBoxShadow?: boolean; isHidden?: boolean; zIndex?: number; @@ -39,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 > @@ -72,7 +83,7 @@ export const Container = ${props => props.height && stylesForMedia<string>('height', props.height)} ${props => props.borderRadius && stylesForMedia<string>('border-radius', props.borderRadius)} ${props => (props.isHidden ? 'visibility: hidden;' : '')} - background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; + 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/text.tsx b/packages/instant/src/components/ui/text.tsx index 8e573d7b9..282477758 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<TextProps> = ({ href, onClick, ...re return <StyledText {...rest} onClick={computedOnClick} />; }; -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 e006a5a5f..a4a03bbf4 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -11,7 +11,7 @@ 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, OrderSource } from '../types'; +import { AccountState, AffiliateInfo, AssetMetaData, Network, OrderSource, QuoteFetchOrigin } from '../types'; import { analytics, disableAnalytics } from '../util/analytics'; import { assetUtils } from '../util/asset'; import { errorFlasher } from '../util/error_flasher'; @@ -115,7 +115,9 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider this._buyQuoteHeartbeat.start(BUY_QUOTE_UPDATE_INTERVAL_TIME_MS); // Trigger first buyquote fetch // tslint:disable-next-line:no-floating-promises - asyncData.fetchCurrentBuyQuoteAndDispatchToStore(state, dispatch, { updateSilently: false }); + asyncData.fetchCurrentBuyQuoteAndDispatchToStore(state, dispatch, QuoteFetchOrigin.Manual, { + updateSilently: false, + }); // warm up the gas price estimator cache just in case we can't // grab the gas price estimate when submitting the transaction // tslint:disable-next-line:no-floating-promises diff --git a/packages/instant/src/containers/connected_account_payment_method.ts b/packages/instant/src/containers/connected_account_payment_method.ts index cdeb49a25..e9327a288 100644 --- a/packages/instant/src/containers/connected_account_payment_method.ts +++ b/packages/instant/src/containers/connected_account_payment_method.ts @@ -11,7 +11,7 @@ import { import { Action, actions } from '../redux/actions'; import { asyncData } from '../redux/async_data'; import { State } from '../redux/reducer'; -import { Network, Omit, OperatingSystem, ProviderState, StandardSlidingPanelContent } from '../types'; +import { Network, Omit, OperatingSystem, ProviderState, StandardSlidingPanelContent, WalletSuggestion } from '../types'; import { analytics } from '../util/analytics'; import { envUtil } from '../util/env'; @@ -60,23 +60,28 @@ const mergeProps = ( 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; - } - 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; + } 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'); } - window.open(url, '_blank'); }, }); 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 a39bc46a2..cb9df527e 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/redux/analytics_middleware.ts b/packages/instant/src/redux/analytics_middleware.ts index 8aa76eb77..3dc5fe924 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 { Account, 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 6feb760e7..9fdcea3ca 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -4,7 +4,7 @@ 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'; @@ -84,6 +84,7 @@ export const asyncData = { fetchCurrentBuyQuoteAndDispatchToStore: async ( state: State, dispatch: Dispatch, + fetchOrigin: QuoteFetchOrigin, options: { updateSilently: boolean }, ) => { const { buyOrderState, providerState, selectedAsset, selectedAssetUnitAmount, affiliateInfo } = state; @@ -99,7 +100,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/style/theme.ts b/packages/instant/src/style/theme.ts index a0751286b..71e4b7052 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 999d50fed..2d73ba29e 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', diff --git a/packages/instant/src/util/analytics.ts b/packages/instant/src/util/analytics.ts index 5bc9bb385..4b8aff4c9 100644 --- a/packages/instant/src/util/analytics.ts +++ b/packages/instant/src/util/analytics.ts @@ -1,7 +1,16 @@ import { BuyQuote } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { AffiliateInfo, Asset, Network, OrderSource, ProviderState } from '../types'; +import { + AffiliateInfo, + Asset, + Network, + OrderSource, + ProviderState, + QuoteFetchOrigin, + WalletSuggestion, +} from '../types'; import { EventProperties, heapUtil } from './heap'; @@ -33,10 +42,17 @@ 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', + QUOTE_FETCHED = 'Quote - Fetched', + QUOTE_ERROR = 'Quote - Error', } const track = (eventName: EventNames, eventProperties: EventProperties = {}): void => { @@ -170,6 +186,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 +201,16 @@ export const analytics = { trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_CHOSE)(payload), trackTokenSelectorSearched: (searchText: string) => trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_SEARCHED)({ searchText }), + 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/buy_quote_updater.ts b/packages/instant/src/util/buy_quote_updater.ts index 2fd16d781..c1899f8c1 100644 --- a/packages/instant/src/util/buy_quote_updater.ts +++ b/packages/instant/src/util/buy_quote_updater.ts @@ -6,7 +6,8 @@ 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'; @@ -16,7 +17,12 @@ export const buyQuoteUpdater = { dispatch: Dispatch<Action>, asset: ERC20Asset, assetUnitAmount: BigNumber, - options: { setPending: boolean; dispatchErrors: boolean; affiliateInfo?: AffiliateInfo }, + fetchOrigin: QuoteFetchOrigin, + options: { + setPending: boolean; + dispatchErrors: boolean; + affiliateInfo?: AffiliateInfo; + }, ): Promise<void> => { // get a new buy quote. const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetUnitAmount, asset.metaData.decimals); @@ -31,6 +37,7 @@ export const buyQuoteUpdater = { } catch (error) { if (options.dispatchErrors) { dispatch(actions.setQuoteRequestStateFailure()); + analytics.trackQuoteError(error.message ? error.message : 'other', baseUnitValue, fetchOrigin); let errorMessage; if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); @@ -58,5 +65,6 @@ export const buyQuoteUpdater = { errorFlasher.clearError(dispatch); // invalidate the last buy quote. dispatch(actions.updateLatestBuyQuote(newBuyQuote)); + analytics.trackQuoteFetched(newBuyQuote, fetchOrigin); }, }; diff --git a/packages/instant/src/util/heartbeater_factory.ts b/packages/instant/src/util/heartbeater_factory.ts index 2b852fb0d..cf29bf3ea 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/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 0b32b60f0..53b24aff0 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "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 1a86f45e6..26d8f8e22 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 8c6fb124f..08801a891 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -1,5 +1,15 @@ [ { + "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", "changes": [ diff --git a/packages/utils/package.json b/packages/utils/package.json index 8dc1f0739..1f4d85843 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 000000000..13cc87e2a --- /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 000000000..2f2f60871 --- /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 000000000..a091e55b9 --- /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 000000000..0f3c55280 --- /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 000000000..089d04659 --- /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 000000000..219ea6c61 --- /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 000000000..72d6a3173 --- /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 000000000..d1abc4986 --- /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 000000000..5f3eee94a --- /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<array1> (alias for array2) + * 0x20 0000000000000000000000000000000000000000000000000000000000000040 ptr<array2> + * + * 0x40 0000000000000000000000000000000000000000000000000000000000000002 ### array2 + * 0x60 0000000000000000000000000000000000000000000000000000000000000040 ptr<array2[0]> + * 0x80 0000000000000000000000000000000000000000000000000000000000000080 ptr<array2[1]> + * 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 <offsetStr><valueStr><nameStr> + 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 000000000..35bd994e5 --- /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 000000000..333b32b4f --- /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<CalldataBlock> { + protected readonly _root: CalldataBlock; + protected readonly _queue: Queue<CalldataBlock>; + + private static _createQueue(block: CalldataBlock): Queue<CalldataBlock> { + const queue = new Queue<CalldataBlock>(); + // 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<CalldataBlock> } { + 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 000000000..189841989 --- /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<number>; + 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<number>(); + 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 000000000..4cc124e0a --- /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 000000000..88846b1fa --- /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 000000000..7595cb667 --- /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 000000000..d713d5a94 --- /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 000000000..5277efd6c --- /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: <length><value>, 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: <length><value>, 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 000000000..f1dcf5ea1 --- /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 000000000..b1cd1377f --- /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 000000000..389e75927 --- /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 000000000..2e371c505 --- /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 000000000..91a72ad3f --- /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: <length><value>, 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: <length><value>, 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 000000000..31593c882 --- /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 000000000..5180f0cf3 --- /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 000000000..baf844ac6 --- /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 000000000..2f43ba04d --- /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 000000000..d84983c5b --- /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 000000000..53afb7e11 --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/queue.ts @@ -0,0 +1,39 @@ +export class Queue<T> { + 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<T>): void { + this._store = this._store.concat(q._store); + } + + public mergeFront(q: Queue<T>): 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 000000000..31471e97a --- /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 0723e5788..082aff6bb 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 000000000..fc552c127 --- /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 000000000..7cfd7a118 --- /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 000000000..ac2124011 --- /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 000000000..9ef80a560 --- /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 000000000..837020883 --- /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 000000000..18aa6549a --- /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 000000000..a8cdd6ca3 --- /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 000000000..1a8733093 --- /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); + }, +}; |