Merge branch 'v2-prototype' into refactor/check-revert-reasons

* v2-prototype: (48 commits)
  Fix typos in comments
  Add modifier and tests for removeAuthorizedAddressAtIndex
  Update and add tests
  Change removeAuthorizedAddress => removeAuthorizedAddressAtIndex
  Move isFunctionRemoveAuthorizedAddress to test
  Fix usage of `popLastByte`
  Fix LibBytes is a library
  Remove `areBytesEqual`
  Fix usage of `contentAddress()`
  Clean low bits in bytes4
  Clean high bits in address
  Refactor LibBytes.readBytes4 for consistency
  Fix LibBytes.equals
  Add trailing garbage testcase for LibBytes.equals
  Rename bytes.equals
  Add slice and sliceDestructive
  Rename bytes.rawAddress and add bytes.contentAddress
  Rename read/writeBytesWithLength
  Using LibBytes for bytes
  Make LibBytes a library
  ...

# Conflicts:
#	packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol
#	packages/contracts/test/libraries/lib_bytes.ts
This commit is contained in:
Fabio Berger 2018-06-25 11:45:17 +02:00
commit df79fb19af
46 changed files with 1162 additions and 892 deletions

View File

@ -30,10 +30,10 @@
"MixinAuthorizable", "MixinAuthorizable",
"MultiSigWallet", "MultiSigWallet",
"MultiSigWalletWithTimeLock", "MultiSigWalletWithTimeLock",
"TestAssetProxyOwner",
"TestAssetDataDecoders", "TestAssetDataDecoders",
"TestAssetProxyDispatcher", "TestAssetProxyDispatcher",
"TestLibBytes", "TestLibBytes",
"TestLibMem",
"TestLibs", "TestLibs",
"TestSignatureValidator", "TestSignatureValidator",
"TestValidator", "TestValidator",

View File

@ -34,7 +34,7 @@
}, },
"config": { "config": {
"abis": "abis":
"../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Exchange|ExchangeWrapper|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetDataDecoders|TestAssetProxyDispatcher|TestLibBytes|TestLibMem|TestLibs|TestSignatureValidator|TestValidator|TestWallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Exchange|ExchangeWrapper|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetDataDecoders|TestAssetProxyDispatcher|TestLibBytes|TestLibs|TestSignatureValidator|TestValidator|TestWallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -69,7 +69,7 @@ contract MixinAuthorizable is
); );
delete authorized[target]; delete authorized[target];
for (uint i = 0; i < authorities.length; i++) { for (uint256 i = 0; i < authorities.length; i++) {
if (authorities[i] == target) { if (authorities[i] == target) {
authorities[i] = authorities[authorities.length - 1]; authorities[i] = authorities[authorities.length - 1];
authorities.length -= 1; authorities.length -= 1;
@ -87,7 +87,12 @@ contract MixinAuthorizable is
uint256 index uint256 index
) )
external external
onlyOwner
{ {
require(
authorized[target],
TARGET_NOT_AUTHORIZED
);
require( require(
index < authorities.length, index < authorities.length,
INDEX_OUT_OF_BOUNDS INDEX_OUT_OF_BOUNDS

View File

@ -24,9 +24,10 @@ import "../../tokens/ERC20Token/IERC20Token.sol";
import "./libs/LibTransferErrors.sol"; import "./libs/LibTransferErrors.sol";
contract MixinERC20Transfer is contract MixinERC20Transfer is
LibBytes,
LibTransferErrors LibTransferErrors
{ {
using LibBytes for bytes;
/// @dev Internal version of `transferFrom`. /// @dev Internal version of `transferFrom`.
/// @param assetData Encoded byte array. /// @param assetData Encoded byte array.
/// @param from Address to transfer asset from. /// @param from Address to transfer asset from.
@ -41,7 +42,7 @@ contract MixinERC20Transfer is
internal internal
{ {
// Decode asset data. // Decode asset data.
address token = readAddress(assetData, 0); address token = assetData.readAddress(0);
// Transfer tokens. // Transfer tokens.
// We do a raw call so we can check the success separate // We do a raw call so we can check the success separate

View File

@ -24,9 +24,10 @@ import "../../tokens/ERC721Token/ERC721Token.sol";
import "./libs/LibTransferErrors.sol"; import "./libs/LibTransferErrors.sol";
contract MixinERC721Transfer is contract MixinERC721Transfer is
LibBytes,
LibTransferErrors LibTransferErrors
{ {
using LibBytes for bytes;
/// @dev Internal version of `transferFrom`. /// @dev Internal version of `transferFrom`.
/// @param assetData Encoded byte array. /// @param assetData Encoded byte array.
/// @param from Address to transfer asset from. /// @param from Address to transfer asset from.
@ -78,10 +79,10 @@ contract MixinERC721Transfer is
) )
{ {
// Decode asset data. // Decode asset data.
token = readAddress(assetData, 0); token = assetData.readAddress(0);
tokenId = readUint256(assetData, 20); tokenId = assetData.readUint256(20);
if (assetData.length > 52) { if (assetData.length > 52) {
receiverData = readBytes(assetData, 52); receiverData = assetData.readBytesWithLength(52);
} }
return ( return (

View File

@ -22,24 +22,24 @@ import "../../multisig/MultiSigWalletWithTimeLock.sol";
import "../../utils/LibBytes/LibBytes.sol"; import "../../utils/LibBytes/LibBytes.sol";
contract AssetProxyOwner is contract AssetProxyOwner is
LibBytes,
MultiSigWalletWithTimeLock MultiSigWalletWithTimeLock
{ {
using LibBytes for bytes;
event AssetProxyRegistration(address assetProxyContract, bool isRegistered); event AssetProxyRegistration(address assetProxyContract, bool isRegistered);
// Mapping of AssetProxy contract address => // Mapping of AssetProxy contract address =>
// if this contract is allowed to call the AssetProxy's removeAuthorizedAddress method without a time lock. // if this contract is allowed to call the AssetProxy's `removeAuthorizedAddressAtIndex` method without a time lock.
mapping (address => bool) public isAssetProxyRegistered; mapping (address => bool) public isAssetProxyRegistered;
bytes4 constant REMOVE_AUTHORIZED_ADDRESS_SELECTOR = bytes4(keccak256("removeAuthorizedAddress(address)")); bytes4 constant REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR = bytes4(keccak256("removeAuthorizedAddressAtIndex(address,uint256)"));
/// @dev Function will revert if the transaction does not call `removeAuthorizedAddress` /// @dev Function will revert if the transaction does not call `removeAuthorizedAddressAtIndex`
/// on an approved AssetProxy contract. /// on an approved AssetProxy contract.
modifier validRemoveAuthorizedAddressTx(uint256 transactionId) { modifier validRemoveAuthorizedAddressAtIndexTx(uint256 transactionId) {
Transaction storage tx = transactions[transactionId]; Transaction storage tx = transactions[transactionId];
require(isAssetProxyRegistered[tx.destination]); require(isAssetProxyRegistered[tx.destination]);
require(isFunctionRemoveAuthorizedAddress(tx.data)); require(tx.data.readBytes4(0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR);
_; _;
} }
@ -66,7 +66,7 @@ contract AssetProxyOwner is
} }
/// @dev Registers or deregisters an AssetProxy to be able to execute /// @dev Registers or deregisters an AssetProxy to be able to execute
/// removeAuthorizedAddress without a timelock. /// `removeAuthorizedAddressAtIndex` without a timelock.
/// @param assetProxyContract Address of AssetProxy contract. /// @param assetProxyContract Address of AssetProxy contract.
/// @param isRegistered Status of approval for AssetProxy contract. /// @param isRegistered Status of approval for AssetProxy contract.
function registerAssetProxy(address assetProxyContract, bool isRegistered) function registerAssetProxy(address assetProxyContract, bool isRegistered)
@ -78,13 +78,13 @@ contract AssetProxyOwner is
AssetProxyRegistration(assetProxyContract, isRegistered); AssetProxyRegistration(assetProxyContract, isRegistered);
} }
/// @dev Allows execution of removeAuthorizedAddress without time lock. /// @dev Allows execution of `removeAuthorizedAddressAtIndex` without time lock.
/// @param transactionId Transaction ID. /// @param transactionId Transaction ID.
function executeRemoveAuthorizedAddress(uint256 transactionId) function executeRemoveAuthorizedAddressAtIndex(uint256 transactionId)
public public
notExecuted(transactionId) notExecuted(transactionId)
fullyConfirmed(transactionId) fullyConfirmed(transactionId)
validRemoveAuthorizedAddressTx(transactionId) validRemoveAuthorizedAddressAtIndexTx(transactionId)
{ {
Transaction storage tx = transactions[transactionId]; Transaction storage tx = transactions[transactionId];
tx.executed = true; tx.executed = true;
@ -95,17 +95,4 @@ contract AssetProxyOwner is
tx.executed = false; tx.executed = false;
} }
} }
/// @dev Compares first 4 bytes of byte array to removeAuthorizedAddress function selector.
/// @param data Transaction data.
/// @return Successful if data is a call to removeAuthorizedAddress.
function isFunctionRemoveAuthorizedAddress(bytes memory data)
public
pure
returns (bool)
{
bytes4 first4Bytes = readFirst4(data);
require(REMOVE_AUTHORIZED_ADDRESS_SELECTOR == first4Bytes);
return true;
}
} }

View File

@ -19,14 +19,12 @@
pragma solidity ^0.4.24; pragma solidity ^0.4.24;
import "../../utils/Ownable/Ownable.sol"; import "../../utils/Ownable/Ownable.sol";
import "../../utils/LibBytes/LibBytes.sol";
import "./libs/LibExchangeErrors.sol"; import "./libs/LibExchangeErrors.sol";
import "./mixins/MAssetProxyDispatcher.sol"; import "./mixins/MAssetProxyDispatcher.sol";
import "../AssetProxy/interfaces/IAssetProxy.sol"; import "../AssetProxy/interfaces/IAssetProxy.sol";
contract MixinAssetProxyDispatcher is contract MixinAssetProxyDispatcher is
Ownable, Ownable,
LibBytes,
LibExchangeErrors, LibExchangeErrors,
MAssetProxyDispatcher MAssetProxyDispatcher
{ {

View File

@ -32,7 +32,6 @@ import "./mixins/MAssetProxyDispatcher.sol";
contract MixinExchangeCore is contract MixinExchangeCore is
LibConstants, LibConstants,
LibBytes,
LibMath, LibMath,
LibOrder, LibOrder,
LibFillResults, LibFillResults,
@ -42,6 +41,8 @@ contract MixinExchangeCore is
MSignatureValidator, MSignatureValidator,
MTransactions MTransactions
{ {
using LibBytes for bytes;
// Mapping of orderHash => amount of takerAsset already bought by maker // Mapping of orderHash => amount of takerAsset already bought by maker
mapping (bytes32 => uint256) public filled; mapping (bytes32 => uint256) public filled;
@ -411,8 +412,8 @@ contract MixinExchangeCore is
) )
private private
{ {
uint8 makerAssetProxyId = uint8(popLastByte(order.makerAssetData)); uint8 makerAssetProxyId = uint8(order.makerAssetData.popLastByte());
uint8 takerAssetProxyId = uint8(popLastByte(order.takerAssetData)); uint8 takerAssetProxyId = uint8(order.takerAssetData.popLastByte());
bytes memory zrxAssetData = ZRX_ASSET_DATA; bytes memory zrxAssetData = ZRX_ASSET_DATA;
dispatchTransferFrom( dispatchTransferFrom(
order.makerAssetData, order.makerAssetData,

View File

@ -27,7 +27,6 @@ import "./mixins/MAssetProxyDispatcher.sol";
contract MixinMatchOrders is contract MixinMatchOrders is
LibConstants, LibConstants,
LibBytes,
LibMath, LibMath,
LibExchangeErrors, LibExchangeErrors,
MAssetProxyDispatcher, MAssetProxyDispatcher,
@ -35,6 +34,7 @@ contract MixinMatchOrders is
MMatchOrders, MMatchOrders,
MTransactions MTransactions
{ {
using LibBytes for bytes;
/// @dev Match two complementary orders that have a profitable spread. /// @dev Match two complementary orders that have a profitable spread.
/// Each order is filled at their respective price point. However, the calculations are /// Each order is filled at their respective price point. However, the calculations are
@ -242,8 +242,8 @@ contract MixinMatchOrders is
) )
private private
{ {
uint8 leftMakerAssetProxyId = uint8(popLastByte(leftOrder.makerAssetData)); uint8 leftMakerAssetProxyId = uint8(leftOrder.makerAssetData.popLastByte());
uint8 rightMakerAssetProxyId = uint8(popLastByte(rightOrder.makerAssetData)); uint8 rightMakerAssetProxyId = uint8(rightOrder.makerAssetData.popLastByte());
bytes memory zrxAssetData = ZRX_ASSET_DATA; bytes memory zrxAssetData = ZRX_ASSET_DATA;
// Order makers and taker // Order makers and taker
dispatchTransferFrom( dispatchTransferFrom(

View File

@ -26,11 +26,12 @@ import "./interfaces/IWallet.sol";
import "./interfaces/IValidator.sol"; import "./interfaces/IValidator.sol";
contract MixinSignatureValidator is contract MixinSignatureValidator is
LibBytes,
LibExchangeErrors, LibExchangeErrors,
MSignatureValidator, MSignatureValidator,
MTransactions MTransactions
{ {
using LibBytes for bytes;
// Personal message headers // Personal message headers
string constant ETH_PERSONAL_MESSAGE = "\x19Ethereum Signed Message:\n32"; string constant ETH_PERSONAL_MESSAGE = "\x19Ethereum Signed Message:\n32";
string constant TREZOR_PERSONAL_MESSAGE = "\x19Ethereum Signed Message:\n\x20"; string constant TREZOR_PERSONAL_MESSAGE = "\x19Ethereum Signed Message:\n\x20";
@ -102,7 +103,7 @@ contract MixinSignatureValidator is
); );
// Ensure signature is supported // Ensure signature is supported
uint8 signatureTypeRaw = uint8(popLastByte(signature)); uint8 signatureTypeRaw = uint8(signature.popLastByte());
require( require(
signatureTypeRaw < uint8(SignatureType.NSignatureTypes), signatureTypeRaw < uint8(SignatureType.NSignatureTypes),
SIGNATURE_UNSUPPORTED SIGNATURE_UNSUPPORTED
@ -144,8 +145,8 @@ contract MixinSignatureValidator is
LENGTH_65_REQUIRED LENGTH_65_REQUIRED
); );
v = uint8(signature[0]); v = uint8(signature[0]);
r = readBytes32(signature, 1); r = signature.readBytes32(1);
s = readBytes32(signature, 33); s = signature.readBytes32(33);
recovered = ecrecover(hash, v, r, s); recovered = ecrecover(hash, v, r, s);
isValid = signerAddress == recovered; isValid = signerAddress == recovered;
return isValid; return isValid;
@ -157,8 +158,8 @@ contract MixinSignatureValidator is
LENGTH_65_REQUIRED LENGTH_65_REQUIRED
); );
v = uint8(signature[0]); v = uint8(signature[0]);
r = readBytes32(signature, 1); r = signature.readBytes32(1);
s = readBytes32(signature, 33); s = signature.readBytes32(33);
recovered = ecrecover( recovered = ecrecover(
keccak256(abi.encodePacked(ETH_PERSONAL_MESSAGE, hash)), keccak256(abi.encodePacked(ETH_PERSONAL_MESSAGE, hash)),
v, v,
@ -199,7 +200,9 @@ contract MixinSignatureValidator is
// | 0x14 + x | 1 | Signature type is always "\x06" | // | 0x14 + x | 1 | Signature type is always "\x06" |
} else if (signatureType == SignatureType.Validator) { } else if (signatureType == SignatureType.Validator) {
// Pop last 20 bytes off of signature byte array. // Pop last 20 bytes off of signature byte array.
address validatorAddress = popLast20Bytes(signature);
address validatorAddress = signature.popLast20Bytes();
// Ensure signer has approved validator. // Ensure signer has approved validator.
if (!allowedValidators[signerAddress][validatorAddress]) { if (!allowedValidators[signerAddress][validatorAddress]) {
return false; return false;
@ -230,8 +233,8 @@ contract MixinSignatureValidator is
LENGTH_65_REQUIRED LENGTH_65_REQUIRED
); );
v = uint8(signature[0]); v = uint8(signature[0]);
r = readBytes32(signature, 1); r = signature.readBytes32(1);
s = readBytes32(signature, 33); s = signature.readBytes32(33);
recovered = ecrecover( recovered = ecrecover(
keccak256(abi.encodePacked(TREZOR_PERSONAL_MESSAGE, hash)), keccak256(abi.encodePacked(TREZOR_PERSONAL_MESSAGE, hash)),
v, v,

View File

@ -51,17 +51,27 @@ contract MixinTransactions is
/// @param signerAddress Address of transaction signer. /// @param signerAddress Address of transaction signer.
/// @param data AbiV2 encoded calldata. /// @param data AbiV2 encoded calldata.
/// @return EIP712 hash of the Transaction. /// @return EIP712 hash of the Transaction.
function hashZeroExTransaction(uint256 salt, address signerAddress, bytes data) function hashZeroExTransaction(
uint256 salt,
address signerAddress,
bytes memory data
)
internal internal
pure pure
returns (bytes32) returns (bytes32 result)
{ {
return keccak256(abi.encode( bytes32 schemaHash = EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH;
EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH, bytes32 dataHash = keccak256(data);
salt, assembly {
signerAddress, let memPtr := mload(64)
keccak256(data) mstore(memPtr, schemaHash)
)); mstore(add(memPtr, 32), salt)
mstore(add(memPtr, 64), and(signerAddress, 0xffffffffffffffffffffffffffffffffffffffff))
mstore(add(memPtr, 96), dataHash)
result := keccak256(memPtr, 128)
}
return result;
} }
/// @dev Executes an exchange method call in the context of signer. /// @dev Executes an exchange method call in the context of signer.

View File

@ -0,0 +1,56 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/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 "../../protocol/AssetProxyOwner/AssetProxyOwner.sol";
contract TestAssetProxyOwner is
AssetProxyOwner
{
constructor(
address[] memory _owners,
address[] memory _assetProxyContracts,
uint256 _required,
uint256 _secondsTimeLocked
)
public
AssetProxyOwner(_owners, _assetProxyContracts, _required, _secondsTimeLocked)
{
}
function testValidRemoveAuthorizedAddressAtIndexTx(uint256 id)
public
validRemoveAuthorizedAddressAtIndexTx(id)
returns (bool)
{
// Do nothing. We expect reverts through the modifier
return true;
}
/// @dev Compares first 4 bytes of byte array to `removeAuthorizedAddressAtIndex` function selector.
/// @param data Transaction data.
/// @return Successful if data is a call to `removeAuthorizedAddressAtIndex`.
function isFunctionRemoveAuthorizedAddressAtIndex(bytes memory data)
public
pure
returns (bool)
{
return data.readBytes4(0) == REMOVE_AUTHORIZED_ADDRESS_AT_INDEX_SELECTOR;
}
}

View File

@ -21,9 +21,9 @@ pragma experimental ABIEncoderV2;
import "../../utils/LibBytes/LibBytes.sol"; import "../../utils/LibBytes/LibBytes.sol";
contract TestLibBytes is contract TestLibBytes {
LibBytes
{ using LibBytes for bytes;
/// @dev Pops the last byte off of a byte array by modifying its length. /// @dev Pops the last byte off of a byte array by modifying its length.
/// @param b Byte array that will be modified. /// @param b Byte array that will be modified.
@ -33,7 +33,7 @@ contract TestLibBytes is
pure pure
returns (bytes memory, bytes1 result) returns (bytes memory, bytes1 result)
{ {
result = popLastByte(b); result = b.popLastByte();
return (b, result); return (b, result);
} }
@ -45,7 +45,7 @@ contract TestLibBytes is
pure pure
returns (bytes memory, address result) returns (bytes memory, address result)
{ {
result = popLast20Bytes(b); result = b.popLast20Bytes();
return (b, result); return (b, result);
} }
@ -53,12 +53,23 @@ contract TestLibBytes is
/// @param lhs First byte array to compare. /// @param lhs First byte array to compare.
/// @param rhs Second byte array to compare. /// @param rhs Second byte array to compare.
/// @return True if arrays are the same. False otherwise. /// @return True if arrays are the same. False otherwise.
function publicAreBytesEqual(bytes memory lhs, bytes memory rhs) function publicEquals(bytes memory lhs, bytes memory rhs)
public public
pure pure
returns (bool equal) returns (bool equal)
{ {
equal = areBytesEqual(lhs, rhs); equal = lhs.equals(rhs);
return equal;
}
function publicEqualsPop1(bytes memory lhs, bytes memory rhs)
public
pure
returns (bool equal)
{
lhs.popLastByte();
rhs.popLastByte();
equal = lhs.equals(rhs);
return equal; return equal;
} }
@ -73,7 +84,7 @@ contract TestLibBytes is
pure pure
returns (bytes memory) returns (bytes memory)
{ {
deepCopyBytes(dest, source); LibBytes.deepCopyBytes(dest, source);
return dest; return dest;
} }
@ -89,7 +100,7 @@ contract TestLibBytes is
pure pure
returns (address result) returns (address result)
{ {
result = readAddress(b, index); result = b.readAddress(index);
return result; return result;
} }
@ -106,7 +117,7 @@ contract TestLibBytes is
pure pure
returns (bytes memory) returns (bytes memory)
{ {
writeAddress(b, index, input); b.writeAddress(index, input);
return b; return b;
} }
@ -122,7 +133,7 @@ contract TestLibBytes is
pure pure
returns (bytes32 result) returns (bytes32 result)
{ {
result = readBytes32(b, index); result = b.readBytes32(index);
return result; return result;
} }
@ -139,7 +150,7 @@ contract TestLibBytes is
pure pure
returns (bytes memory) returns (bytes memory)
{ {
writeBytes32(b, index, input); b.writeBytes32(index, input);
return b; return b;
} }
@ -155,7 +166,7 @@ contract TestLibBytes is
pure pure
returns (uint256 result) returns (uint256 result)
{ {
result = readUint256(b, index); result = b.readUint256(index);
return result; return result;
} }
@ -172,19 +183,23 @@ contract TestLibBytes is
pure pure
returns (bytes memory) returns (bytes memory)
{ {
writeUint256(b, index, input); b.writeUint256(index, input);
return b; return b;
} }
/// @dev Reads the first 4 bytes from a byte array of arbitrary length. /// @dev Reads an unpadded bytes4 value from a position in a byte array.
/// @param b Byte array to read first 4 bytes from. /// @param b Byte array containing a bytes4 value.
/// @return First 4 bytes of data. /// @param index Index in byte array of bytes4 value.
function publicReadFirst4(bytes memory b) /// @return bytes4 value from byte array.
function publicReadBytes4(
bytes memory b,
uint256 index
)
public public
pure pure
returns (bytes4 result) returns (bytes4 result)
{ {
result = readFirst4(b); result = b.readBytes4(index);
return result; return result;
} }
@ -192,7 +207,7 @@ contract TestLibBytes is
/// @param b Byte array containing nested bytes. /// @param b Byte array containing nested bytes.
/// @param index Index of nested bytes. /// @param index Index of nested bytes.
/// @return result Nested bytes. /// @return result Nested bytes.
function publicReadBytes( function publicReadBytesWithLength(
bytes memory b, bytes memory b,
uint256 index uint256 index
) )
@ -200,7 +215,7 @@ contract TestLibBytes is
pure pure
returns (bytes memory result) returns (bytes memory result)
{ {
result = readBytes(b, index); result = b.readBytesWithLength(index);
return result; return result;
} }
@ -209,7 +224,7 @@ contract TestLibBytes is
/// @param index Index in byte array of <input>. /// @param index Index in byte array of <input>.
/// @param input bytes to insert. /// @param input bytes to insert.
/// @return b Updated input byte array /// @return b Updated input byte array
function publicWriteBytes( function publicWriteBytesWithLength(
bytes memory b, bytes memory b,
uint256 index, uint256 index,
bytes memory input bytes memory input
@ -218,7 +233,37 @@ contract TestLibBytes is
pure pure
returns (bytes memory) returns (bytes memory)
{ {
writeBytes(b, index, input); b.writeBytesWithLength(index, input);
return b; return b;
} }
/// @dev Copies a block of memory from one location to another.
/// @param mem Memory contents we want to apply memCopy to
/// @param dest Destination offset into <mem>.
/// @param source Source offset into <mem>.
/// @param length Length of bytes to copy from <source> to <dest>
/// @return mem Memory contents after calling memCopy.
function testMemcpy(
bytes mem,
uint256 dest,
uint256 source,
uint256 length
)
public // not external, we need input in memory
pure
returns (bytes)
{
// Sanity check. Overflows are not checked.
require(source + length <= mem.length);
require(dest + length <= mem.length);
// Get pointer to memory contents
uint256 offset = mem.contentAddress();
// Execute memCopy adjusted for memory array location
LibBytes.memCopy(offset + dest, offset + source, length);
// Return modified memory contents
return mem;
}
} }

View File

@ -1,56 +0,0 @@
/*
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 "../../utils/LibMem/LibMem.sol";
contract TestLibMem is
LibMem
{
/// @dev Copies a block of memory from one location to another.
/// @param mem Memory contents we want to apply memCopy to
/// @param dest Destination offset into <mem>.
/// @param source Source offset into <mem>.
/// @param length Length of bytes to copy from <source> to <dest>
/// @return mem Memory contents after calling memCopy.
function testMemcpy(
bytes mem,
uint256 dest,
uint256 source,
uint256 length
)
public // not external, we need input in memory
pure
returns (bytes)
{
// Sanity check. Overflows are not checked.
require(source + length <= mem.length);
require(dest + length <= mem.length);
// Get pointer to memory contents
uint256 offset = getMemAddress(mem) + 32;
// Execute memCopy adjusted for memory array location
memCopy(offset + dest, offset + source, length);
// Return modified memory contents
return mem;
}
}

View File

@ -22,9 +22,9 @@ import "../../protocol/Exchange/interfaces/IWallet.sol";
import "../../utils/LibBytes/LibBytes.sol"; import "../../utils/LibBytes/LibBytes.sol";
contract TestWallet is contract TestWallet is
IWallet, IWallet
LibBytes
{ {
using LibBytes for bytes;
string constant LENGTH_65_REQUIRED = "LENGTH_65_REQUIRED"; string constant LENGTH_65_REQUIRED = "LENGTH_65_REQUIRED";
@ -56,8 +56,8 @@ contract TestWallet is
); );
uint8 v = uint8(eip712Signature[0]); uint8 v = uint8(eip712Signature[0]);
bytes32 r = readBytes32(eip712Signature, 1); bytes32 r = eip712Signature.readBytes32(1);
bytes32 s = readBytes32(eip712Signature, 33); bytes32 s = eip712Signature.readBytes32(33);
address recoveredAddress = ecrecover(hash, v, r, s); address recoveredAddress = ecrecover(hash, v, r, s);
isValid = WALLET_OWNER == recoveredAddress; isValid = WALLET_OWNER == recoveredAddress;
return isValid; return isValid;

View File

@ -18,11 +18,8 @@
pragma solidity ^0.4.24; pragma solidity ^0.4.24;
import "../LibMem/LibMem.sol"; library LibBytes {
using LibBytes for bytes;
contract LibBytes is
LibMem
{
// Revert reasons // Revert reasons
string constant GREATER_THAN_ZERO_LENGTH_REQUIRED = "GREATER_THAN_ZERO_LENGTH_REQUIRED"; string constant GREATER_THAN_ZERO_LENGTH_REQUIRED = "GREATER_THAN_ZERO_LENGTH_REQUIRED";
@ -31,6 +28,187 @@ contract LibBytes is
string constant GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED"; string constant GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_32_LENGTH_REQUIRED";
string constant GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED"; string constant GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_NESTED_BYTES_LENGTH_REQUIRED";
string constant GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED"; string constant GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED = "GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED";
string constant FROM_LESS_THAN_TO_REQUIRED = "FROM_LESS_THAN_TO_REQUIRED";
string constant TO_LESS_THAN_LENGTH_REQUIRED = "TO_LESS_THAN_LENGTH_REQUIRED";
/// @dev Gets the memory address for a byte array.
/// @param input Byte array to lookup.
/// @return memoryAddress Memory address of byte array. This
/// points to the header of the byte array which contains
/// the length.
function rawAddress(bytes memory input)
internal
pure
returns (uint256 memoryAddress)
{
assembly {
memoryAddress := input
}
return memoryAddress;
}
/// @dev Gets the memory address for the contents of a byte array.
/// @param input Byte array to lookup.
/// @return memoryAddress Memory address of the contents of the byte array.
function contentAddress(bytes memory input)
internal
pure
returns (uint256 memoryAddress)
{
assembly {
memoryAddress := add(input, 32)
}
return memoryAddress;
}
/// @dev Copies `length` bytes from memory location `source` to `dest`.
/// @param dest memory address to copy bytes to.
/// @param source memory address to copy bytes from.
/// @param length number of bytes to copy.
function memCopy(
uint256 dest,
uint256 source,
uint256 length
)
internal
pure
{
if (length < 32) {
// Handle a partial word by reading destination and masking
// off the bits we are interested in.
// This correctly handles overlap, zero lengths and source == dest
assembly {
let mask := sub(exp(256, sub(32, length)), 1)
let s := and(mload(source), not(mask))
let d := and(mload(dest), mask)
mstore(dest, or(s, d))
}
} else {
// Skip the O(length) loop when source == dest.
if (source == dest) {
return;
}
// For large copies we copy whole words at a time. The final
// word is aligned to the end of the range (instead of after the
// previous) to handle partial words. So a copy will look like this:
//
// ####
// ####
// ####
// ####
//
// We handle overlap in the source and destination range by
// changing the copying direction. This prevents us from
// overwriting parts of source that we still need to copy.
//
// This correctly handles source == dest
//
if (source > dest) {
assembly {
// We subtract 32 from `sEnd` and `dEnd` because it
// is easier to compare with in the loop, and these
// are also the addresses we need for copying the
// last bytes.
length := sub(length, 32)
let sEnd := add(source, length)
let dEnd := add(dest, length)
// Remember the last 32 bytes of source
// This needs to be done here and not after the loop
// because we may have overwritten the last bytes in
// source already due to overlap.
let last := mload(sEnd)
// Copy whole words front to back
// Note: the first check is always true,
// this could have been a do-while loop.
for {} lt(source, sEnd) {} {
mstore(dest, mload(source))
source := add(source, 32)
dest := add(dest, 32)
}
// Write the last 32 bytes
mstore(dEnd, last)
}
} else {
assembly {
// We subtract 32 from `sEnd` and `dEnd` because those
// are the starting points when copying a word at the end.
length := sub(length, 32)
let sEnd := add(source, length)
let dEnd := add(dest, length)
// Remember the first 32 bytes of source
// This needs to be done here and not after the loop
// because we may have overwritten the first bytes in
// source already due to overlap.
let first := mload(source)
// Copy whole words back to front
// We use a signed comparisson here to allow dEnd to become
// negative (happens when source and dest < 32). Valid
// addresses in local memory will never be larger than
// 2**255, so they can be safely re-interpreted as signed.
// Note: the first check is always true,
// this could have been a do-while loop.
for {} slt(dest, dEnd) {} {
mstore(dEnd, mload(sEnd))
sEnd := sub(sEnd, 32)
dEnd := sub(dEnd, 32)
}
// Write the first 32 bytes
mstore(dest, first)
}
}
}
}
/// @dev Returns a slices from a byte array.
/// @param b The byte array to take a slice from.
/// @param from The starting index for the slice (inclusive).
/// @param to The final index for the slice (exclusive).
/// @return result The slice containing bytes at indices [from, to)
function slice(bytes memory b, uint256 from, uint256 to)
internal
pure
returns (bytes memory result)
{
require(from <= to, FROM_LESS_THAN_TO_REQUIRED);
require(to < b.length, TO_LESS_THAN_LENGTH_REQUIRED);
// Create a new bytes structure and copy contents
result = new bytes(to - from);
memCopy(
result.contentAddress(),
b.contentAddress() + from,
result.length);
return result;
}
/// @dev Returns a slice from a byte array without preserving the input.
/// @param b The byte array to take a slice from. Will be destroyed in the process.
/// @param from The starting index for the slice (inclusive).
/// @param to The final index for the slice (exclusive).
/// @return result The slice containing bytes at indices [from, to)
/// @dev When `from == 0`, the original array will match the slice. In other cases its state will be corrupted.
function sliceDestructive(bytes memory b, uint256 from, uint256 to)
internal
pure
returns (bytes memory result)
{
require(from <= to, FROM_LESS_THAN_TO_REQUIRED);
require(to < b.length, TO_LESS_THAN_LENGTH_REQUIRED);
// Create a new bytes structure around [from, to) in-place.
assembly {
result := add(b, from)
mstore(result, sub(to, from))
}
return result;
}
/// @dev Pops the last byte off of a byte array by modifying its length. /// @dev Pops the last byte off of a byte array by modifying its length.
/// @param b Byte array that will be modified. /// @param b Byte array that will be modified.
@ -80,6 +258,24 @@ contract LibBytes is
return result; return result;
} }
/// @dev Tests equality of two byte arrays.
/// @param lhs First byte array to compare.
/// @param rhs Second byte array to compare.
/// @return True if arrays are the same. False otherwise.
function equals(
bytes memory lhs,
bytes memory rhs
)
internal
pure
returns (bool equal)
{
// Keccak gas cost is 30 + numWords * 6. This is a cheap way to compare.
// We early exit on unequal lengths, but keccak would also correctly
// handle this.
return lhs.length == rhs.length && keccak256(lhs) == keccak256(rhs);
}
/// @dev Reads an address from a position in a byte array. /// @dev Reads an address from a position in a byte array.
/// @param b Byte array containing an address. /// @param b Byte array containing an address.
/// @param index Index in byte array of address. /// @param index Index in byte array of address.
@ -145,6 +341,10 @@ contract LibBytes is
// 2. Load 32-byte word from memory // 2. Load 32-byte word from memory
// 3. Apply 12-byte mask to obtain extra bytes occupying word of memory where we'll store the address // 3. Apply 12-byte mask to obtain extra bytes occupying word of memory where we'll store the address
let neighbors := and(mload(add(b, index)), 0xffffffffffffffffffffffff0000000000000000000000000000000000000000) let neighbors := and(mload(add(b, index)), 0xffffffffffffffffffffffff0000000000000000000000000000000000000000)
// Make sure input address is clean.
// (Solidity does not guarantee this)
input := and(input, 0xffffffffffffffffffffffffffffffffffffffff)
// Store the neighbors and address into memory // Store the neighbors and address into memory
mstore(add(b, index), xor(input, neighbors)) mstore(add(b, index), xor(input, neighbors))
@ -234,20 +434,26 @@ contract LibBytes is
writeBytes32(b, index, bytes32(input)); writeBytes32(b, index, bytes32(input));
} }
/// @dev Reads the first 4 bytes from a byte array of arbitrary length. /// @dev Reads an unpadded bytes4 value from a position in a byte array.
/// @param b Byte array to read first 4 bytes from. /// @param b Byte array containing a bytes4 value.
/// @return First 4 bytes of data. /// @param index Index in byte array of bytes4 value.
function readFirst4(bytes memory b) /// @return bytes4 value from byte array.
function readBytes4(
bytes memory b,
uint256 index)
internal internal
pure pure
returns (bytes4 result) returns (bytes4 result)
{ {
require( require(
b.length >= 4, b.length >= index + 4,
GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED GREATER_OR_EQUAL_TO_4_LENGTH_REQUIRED
); );
assembly { assembly {
result := mload(add(b, 32)) result := mload(add(b, 32))
// Solidity does not require us to clean the trailing bytes.
// We do it anyway
result := and(result, 0xFFFFFFFF00000000000000000000000000000000000000000000000000000000)
} }
return result; return result;
} }
@ -256,7 +462,7 @@ contract LibBytes is
/// @param b Byte array containing nested bytes. /// @param b Byte array containing nested bytes.
/// @param index Index of nested bytes. /// @param index Index of nested bytes.
/// @return result Nested bytes. /// @return result Nested bytes.
function readBytes( function readBytesWithLength(
bytes memory b, bytes memory b,
uint256 index uint256 index
) )
@ -278,8 +484,8 @@ contract LibBytes is
// Allocate memory and copy value to result // Allocate memory and copy value to result
result = new bytes(nestedBytesLength); result = new bytes(nestedBytesLength);
memCopy( memCopy(
getMemAddress(result) + 32, // +32 skips array length result.contentAddress(),
getMemAddress(b) + index + 32, b.contentAddress() + index,
nestedBytesLength nestedBytesLength
); );
@ -290,7 +496,7 @@ contract LibBytes is
/// @param b Byte array to insert <input> into. /// @param b Byte array to insert <input> into.
/// @param index Index in byte array of <input>. /// @param index Index in byte array of <input>.
/// @param input bytes to insert. /// @param input bytes to insert.
function writeBytes( function writeBytesWithLength(
bytes memory b, bytes memory b,
uint256 index, uint256 index,
bytes memory input bytes memory input
@ -307,47 +513,12 @@ contract LibBytes is
// Copy <input> into <b> // Copy <input> into <b>
memCopy( memCopy(
getMemAddress(b) + 32 + index, // +32 to skip length of <b> b.contentAddress() + index,
getMemAddress(input), // includes length of <input> input.rawAddress(), // includes length of <input>
input.length + 32 // +32 bytes to store <input> length input.length + 32 // +32 bytes to store <input> length
); );
} }
/// @dev Tests equality of two byte arrays.
/// @param lhs First byte array to compare.
/// @param rhs Second byte array to compare.
/// @return True if arrays are the same. False otherwise.
function areBytesEqual(
bytes memory lhs,
bytes memory rhs
)
internal
pure
returns (bool equal)
{
assembly {
// Get the number of words occupied by <lhs>
let lenFullWords := div(add(mload(lhs), 0x1F), 0x20)
// Add 1 to the number of words, to account for the length field
lenFullWords := add(lenFullWords, 0x1)
// Test equality word-by-word.
// Terminates early if there is a mismatch.
for {let i := 0} lt(i, lenFullWords) {i := add(i, 1)} {
let lhsWord := mload(add(lhs, mul(i, 0x20)))
let rhsWord := mload(add(rhs, mul(i, 0x20)))
equal := eq(lhsWord, rhsWord)
if eq(equal, 0) {
// Break
i := lenFullWords
}
}
}
return equal;
}
/// @dev Performs a deep copy of a byte array onto another byte array of greater than or equal length. /// @dev Performs a deep copy of a byte array onto another byte array of greater than or equal length.
/// @param dest Byte array that will be overwritten with source bytes. /// @param dest Byte array that will be overwritten with source bytes.
/// @param source Byte array to copy onto dest bytes. /// @param source Byte array to copy onto dest bytes.
@ -365,8 +536,8 @@ contract LibBytes is
GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED GREATER_OR_EQUAL_TO_SOURCE_BYTES_LENGTH_REQUIRED
); );
memCopy( memCopy(
getMemAddress(dest) + 32, // +32 to skip length of <dest> dest.contentAddress(),
getMemAddress(source) + 32, // +32 to skip length of <source> source.contentAddress(),
sourceLen sourceLen
); );
} }

View File

@ -1,142 +0,0 @@
/*
Copyright 2018 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.4.24;
contract LibMem
{
/// @dev Gets the memory address for a byte array.
/// @param input Byte array to lookup.
/// @return memoryAddress Memory address of byte array.
function getMemAddress(bytes memory input)
internal
pure
returns (uint256 memoryAddress)
{
assembly {
memoryAddress := input
}
return memoryAddress;
}
/// @dev Copies `length` bytes from memory location `source` to `dest`.
/// @param dest memory address to copy bytes to.
/// @param source memory address to copy bytes from.
/// @param length number of bytes to copy.
function memCopy(
uint256 dest,
uint256 source,
uint256 length
)
internal
pure
{
if (length < 32) {
// Handle a partial word by reading destination and masking
// off the bits we are interested in.
// This correctly handles overlap, zero lengths and source == dest
assembly {
let mask := sub(exp(256, sub(32, length)), 1)
let s := and(mload(source), not(mask))
let d := and(mload(dest), mask)
mstore(dest, or(s, d))
}
} else {
// Skip the O(length) loop when source == dest.
if (source == dest) {
return;
}
// For large copies we copy whole words at a time. The final
// word is aligned to the end of the range (instead of after the
// previous) to handle partial words. So a copy will look like this:
//
// ####
// ####
// ####
// ####
//
// We handle overlap in the source and destination range by
// changing the copying direction. This prevents us from
// overwriting parts of source that we still need to copy.
//
// This correctly handles source == dest
//
if (source > dest) {
assembly {
// We subtract 32 from `sEnd` and `dEnd` because it
// is easier to compare with in the loop, and these
// are also the addresses we need for copying the
// last bytes.
length := sub(length, 32)
let sEnd := add(source, length)
let dEnd := add(dest, length)
// Remember the last 32 bytes of source
// This needs to be done here and not after the loop
// because we may have overwritten the last bytes in
// source already due to overlap.
let last := mload(sEnd)
// Copy whole words front to back
// Note: the first check is always true,
// this could have been a do-while loop.
for {} lt(source, sEnd) {} {
mstore(dest, mload(source))
source := add(source, 32)
dest := add(dest, 32)
}
// Write the last 32 bytes
mstore(dEnd, last)
}
} else {
assembly {
// We subtract 32 from `sEnd` and `dEnd` because those
// are the starting points when copying a word at the end.
length := sub(length, 32)
let sEnd := add(source, length)
let dEnd := add(dest, length)
// Remember the first 32 bytes of source
// This needs to be done here and not after the loop
// because we may have overwritten the first bytes in
// source already due to overlap.
let first := mload(source)
// Copy whole words back to front
// We use a signed comparisson here to allow dEnd to become
// negative (happens when source and dest < 32). Valid
// addresses in local memory will never be larger than
// 2**255, so they can be safely re-interpreted as signed.
// Note: the first check is always true,
// this could have been a do-while loop.
for {} slt(dest, dEnd) {} {
mstore(dEnd, mload(sEnd))
sEnd := sub(sEnd, 32)
dEnd := sub(dEnd, 32)
}
// Write the first 32 bytes
mstore(dest, first)
}
}
}
}
}

View File

@ -13,6 +13,9 @@ import "./IOwnable.sol";
contract Ownable is IOwnable { contract Ownable is IOwnable {
address public owner; address public owner;
// Revert reasons
string constant ONLY_CONTRACT_OWNER = "ONLY_CONTRACT_OWNER";
constructor () constructor ()
public public
{ {
@ -22,7 +25,7 @@ contract Ownable is IOwnable {
modifier onlyOwner() { modifier onlyOwner() {
require( require(
msg.sender == owner, msg.sender == owner,
'ONLY_CONTRACT_OWNER' ONLY_CONTRACT_OWNER
); );
_; _;
} }

View File

@ -13,8 +13,8 @@ import * as MultiSigWallet from '../artifacts/MultiSigWallet.json';
import * as MultiSigWalletWithTimeLock from '../artifacts/MultiSigWalletWithTimeLock.json'; import * as MultiSigWalletWithTimeLock from '../artifacts/MultiSigWalletWithTimeLock.json';
import * as TestAssetDataDecoders from '../artifacts/TestAssetDataDecoders.json'; import * as TestAssetDataDecoders from '../artifacts/TestAssetDataDecoders.json';
import * as TestAssetProxyDispatcher from '../artifacts/TestAssetProxyDispatcher.json'; import * as TestAssetProxyDispatcher from '../artifacts/TestAssetProxyDispatcher.json';
import * as TestAssetProxyOwner from '../artifacts/TestAssetProxyOwner.json';
import * as TestLibBytes from '../artifacts/TestLibBytes.json'; import * as TestLibBytes from '../artifacts/TestLibBytes.json';
import * as TestLibMem from '../artifacts/TestLibMem.json';
import * as TestLibs from '../artifacts/TestLibs.json'; import * as TestLibs from '../artifacts/TestLibs.json';
import * as TestSignatureValidator from '../artifacts/TestSignatureValidator.json'; import * as TestSignatureValidator from '../artifacts/TestSignatureValidator.json';
import * as TestValidator from '../artifacts/TestValidator.json'; import * as TestValidator from '../artifacts/TestValidator.json';
@ -37,10 +37,10 @@ export const artifacts = {
MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact, MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact,
MultiSigWallet: (MultiSigWallet as any) as ContractArtifact, MultiSigWallet: (MultiSigWallet as any) as ContractArtifact,
MultiSigWalletWithTimeLock: (MultiSigWalletWithTimeLock as any) as ContractArtifact, MultiSigWalletWithTimeLock: (MultiSigWalletWithTimeLock as any) as ContractArtifact,
TestAssetProxyOwner: (TestAssetProxyOwner as any) as ContractArtifact,
TestAssetProxyDispatcher: (TestAssetProxyDispatcher as any) as ContractArtifact, TestAssetProxyDispatcher: (TestAssetProxyDispatcher as any) as ContractArtifact,
TestAssetDataDecoders: (TestAssetDataDecoders as any) as ContractArtifact, TestAssetDataDecoders: (TestAssetDataDecoders as any) as ContractArtifact,
TestLibBytes: (TestLibBytes as any) as ContractArtifact, TestLibBytes: (TestLibBytes as any) as ContractArtifact,
TestLibMem: (TestLibMem as any) as ContractArtifact,
TestLibs: (TestLibs as any) as ContractArtifact, TestLibs: (TestLibs as any) as ContractArtifact,
TestSignatureValidator: (TestSignatureValidator as any) as ContractArtifact, TestSignatureValidator: (TestSignatureValidator as any) as ContractArtifact,
TestValidator: (TestValidator as any) as ContractArtifact, TestValidator: (TestValidator as any) as ContractArtifact,

View File

@ -40,13 +40,15 @@ export class MultiSigWrapper {
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx; return tx;
} }
public async executeRemoveAuthorizedAddressAsync( public async executeRemoveAuthorizedAddressAtIndexAsync(
txId: BigNumber, txId: BigNumber,
from: string, from: string,
): Promise<TransactionReceiptWithDecodedLogs> { ): Promise<TransactionReceiptWithDecodedLogs> {
// tslint:disable-next-line:no-unnecessary-type-assertion // tslint:disable-next-line:no-unnecessary-type-assertion
const txHash = await (this const txHash = await (this
._multiSig as AssetProxyOwnerContract).executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from }); ._multiSig as AssetProxyOwnerContract).executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
from,
});
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx; return tx;
} }

View File

@ -92,7 +92,6 @@ export enum ContractName {
Arbitrage = 'Arbitrage', Arbitrage = 'Arbitrage',
TestAssetDataDecoders = 'TestAssetDataDecoders', TestAssetDataDecoders = 'TestAssetDataDecoders',
TestAssetProxyDispatcher = 'TestAssetProxyDispatcher', TestAssetProxyDispatcher = 'TestAssetProxyDispatcher',
TestLibMem = 'TestLibMem',
TestLibs = 'TestLibs', TestLibs = 'TestLibs',
TestSignatureValidator = 'TestSignatureValidator', TestSignatureValidator = 'TestSignatureValidator',
ERC20Proxy = 'ERC20Proxy', ERC20Proxy = 'ERC20Proxy',

View File

@ -1,4 +1,5 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils'; import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai'; import * as chai from 'chai';
import { MixinAuthorizableContract } from '../../src/generated_contract_wrappers/mixin_authorizable'; import { MixinAuthorizableContract } from '../../src/generated_contract_wrappers/mixin_authorizable';
@ -107,6 +108,74 @@ describe('Authorizable', () => {
}); });
}); });
describe('removeAuthorizedAddressAtIndex', () => {
it('should throw if not called by owner', async () => {
await web3Wrapper.awaitTransactionSuccessAsync(
await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
constants.AWAIT_TRANSACTION_MINED_MS,
);
const index = new BigNumber(0);
return expectRevertOrAlwaysFailingTransactionAsync(
authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
from: notOwner,
}),
);
});
it('should throw if index is >= authorities.length', async () => {
await web3Wrapper.awaitTransactionSuccessAsync(
await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
constants.AWAIT_TRANSACTION_MINED_MS,
);
const index = new BigNumber(1);
return expectRevertOrAlwaysFailingTransactionAsync(
authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
from: owner,
}),
);
});
it('should throw if owner attempts to remove an address that is not authorized', async () => {
const index = new BigNumber(0);
return expectRevertOrAlwaysFailingTransactionAsync(
authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
from: owner,
}),
);
});
it('should throw if address at index does not match target', async () => {
const address1 = address;
const address2 = notOwner;
await web3Wrapper.awaitTransactionSuccessAsync(
await authorizable.addAuthorizedAddress.sendTransactionAsync(address1, { from: owner }),
constants.AWAIT_TRANSACTION_MINED_MS,
);
await web3Wrapper.awaitTransactionSuccessAsync(
await authorizable.addAuthorizedAddress.sendTransactionAsync(address2, { from: owner }),
constants.AWAIT_TRANSACTION_MINED_MS,
);
const address1Index = new BigNumber(0);
return expectRevertOrAlwaysFailingTransactionAsync(
authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address2, address1Index, {
from: owner,
}),
);
});
it('should allow owner to remove an authorized address', async () => {
await web3Wrapper.awaitTransactionSuccessAsync(
await authorizable.addAuthorizedAddress.sendTransactionAsync(address, { from: owner }),
constants.AWAIT_TRANSACTION_MINED_MS,
);
const index = new BigNumber(0);
await web3Wrapper.awaitTransactionSuccessAsync(
await authorizable.removeAuthorizedAddressAtIndex.sendTransactionAsync(address, index, {
from: owner,
}),
constants.AWAIT_TRANSACTION_MINED_MS,
);
const isAuthorized = await authorizable.authorized.callAsync(address);
expect(isAuthorized).to.be.false();
});
});
describe('getAuthorizedAddresses', () => { describe('getAuthorizedAddresses', () => {
it('should return all authorized addresses', async () => { it('should return all authorized addresses', async () => {
const initial = await authorizable.getAuthorizedAddresses.callAsync(); const initial = await authorizable.getAuthorizedAddresses.callAsync();

View File

@ -27,7 +27,6 @@ describe('TestAssetDataDecoders', () => {
// Setup accounts & addresses // Setup accounts & addresses
const accounts = await web3Wrapper.getAvailableAddressesAsync(); const accounts = await web3Wrapper.getAvailableAddressesAsync();
testAddress = accounts[0]; testAddress = accounts[0];
// Deploy TestLibMem
testAssetProxyDecoder = await TestAssetDataDecodersContract.deployFrom0xArtifactAsync( testAssetProxyDecoder = await TestAssetDataDecodersContract.deployFrom0xArtifactAsync(
artifacts.TestAssetDataDecoders, artifacts.TestAssetDataDecoders,
provider, provider,

View File

@ -11,6 +11,7 @@ import {
SubmissionContractEventArgs, SubmissionContractEventArgs,
} from '../src/generated_contract_wrappers/asset_proxy_owner'; } from '../src/generated_contract_wrappers/asset_proxy_owner';
import { MixinAuthorizableContract } from '../src/generated_contract_wrappers/mixin_authorizable'; import { MixinAuthorizableContract } from '../src/generated_contract_wrappers/mixin_authorizable';
import { TestAssetProxyOwnerContract } from '../src/generated_contract_wrappers/test_asset_proxy_owner';
import { artifacts } from '../src/utils/artifacts'; import { artifacts } from '../src/utils/artifacts';
import { import {
expectRevertOrAlwaysFailingTransactionAsync, expectRevertOrAlwaysFailingTransactionAsync,
@ -34,7 +35,7 @@ describe('AssetProxyOwner', () => {
let erc20Proxy: MixinAuthorizableContract; let erc20Proxy: MixinAuthorizableContract;
let erc721Proxy: MixinAuthorizableContract; let erc721Proxy: MixinAuthorizableContract;
let multiSig: AssetProxyOwnerContract; let testAssetProxyOwner: TestAssetProxyOwnerContract;
let multiSigWrapper: MultiSigWrapper; let multiSigWrapper: MultiSigWrapper;
before(async () => { before(async () => {
@ -58,8 +59,8 @@ describe('AssetProxyOwner', () => {
txDefaults, txDefaults,
); );
const defaultAssetProxyContractAddresses: string[] = []; const defaultAssetProxyContractAddresses: string[] = [];
multiSig = await AssetProxyOwnerContract.deployFrom0xArtifactAsync( testAssetProxyOwner = await TestAssetProxyOwnerContract.deployFrom0xArtifactAsync(
artifacts.AssetProxyOwner, artifacts.TestAssetProxyOwner,
provider, provider,
txDefaults, txDefaults,
owners, owners,
@ -67,13 +68,17 @@ describe('AssetProxyOwner', () => {
REQUIRED_APPROVALS, REQUIRED_APPROVALS,
SECONDS_TIME_LOCKED, SECONDS_TIME_LOCKED,
); );
multiSigWrapper = new MultiSigWrapper(multiSig, provider); multiSigWrapper = new MultiSigWrapper(testAssetProxyOwner, provider);
await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.awaitTransactionSuccessAsync(
await erc20Proxy.transferOwnership.sendTransactionAsync(multiSig.address, { from: initialOwner }), await erc20Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, {
from: initialOwner,
}),
constants.AWAIT_TRANSACTION_MINED_MS, constants.AWAIT_TRANSACTION_MINED_MS,
); );
await web3Wrapper.awaitTransactionSuccessAsync( await web3Wrapper.awaitTransactionSuccessAsync(
await erc721Proxy.transferOwnership.sendTransactionAsync(multiSig.address, { from: initialOwner }), await erc721Proxy.transferOwnership.sendTransactionAsync(testAssetProxyOwner.address, {
from: initialOwner,
}),
constants.AWAIT_TRANSACTION_MINED_MS, constants.AWAIT_TRANSACTION_MINED_MS,
); );
}); });
@ -117,24 +122,28 @@ describe('AssetProxyOwner', () => {
}); });
}); });
describe('isFunctionRemoveAuthorizedAddress', () => { describe('isFunctionRemoveAuthorizedAddressAtIndex', () => {
it('should throw if data is not for removeAuthorizedAddress', async () => { it('should return false if data is not for removeAuthorizedAddressAtIndex', async () => {
const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData( const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
owners[0], owners[0],
); );
return expectRevertOrContractCallFailedAsync(
multiSig.isFunctionRemoveAuthorizedAddress.callAsync(notRemoveAuthorizedAddressData), const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync(
notRemoveAuthorizedAddressData,
); );
expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.false();
}); });
it('should return true if data is for removeAuthorizedAddress', async () => { it('should return true if data is for removeAuthorizedAddressAtIndex', async () => {
const removeAuthorizedAddressData = erc20Proxy.removeAuthorizedAddress.getABIEncodedTransactionData( const index = new BigNumber(0);
const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
owners[0], owners[0],
index,
); );
const isFunctionRemoveAuthorizedAddress = await multiSig.isFunctionRemoveAuthorizedAddress.callAsync( const isFunctionRemoveAuthorizedAddressAtIndex = await testAssetProxyOwner.isFunctionRemoveAuthorizedAddressAtIndex.callAsync(
removeAuthorizedAddressData, removeAuthorizedAddressAtIndexData,
); );
expect(isFunctionRemoveAuthorizedAddress).to.be.true(); expect(isFunctionRemoveAuthorizedAddressAtIndex).to.be.true();
}); });
}); });
@ -142,19 +151,21 @@ describe('AssetProxyOwner', () => {
it('should throw if not called by multisig', async () => { it('should throw if not called by multisig', async () => {
const isRegistered = true; const isRegistered = true;
return expectRevertOrAlwaysFailingTransactionAsync( return expectRevertOrAlwaysFailingTransactionAsync(
multiSig.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, isRegistered, { from: owners[0] }), testAssetProxyOwner.registerAssetProxy.sendTransactionAsync(erc20Proxy.address, isRegistered, {
from: owners[0],
}),
); );
}); });
it('should register an address if called by multisig after timelock', async () => { it('should register an address if called by multisig after timelock', async () => {
const addressToRegister = erc20Proxy.address; const addressToRegister = erc20Proxy.address;
const isRegistered = true; const isRegistered = true;
const registerAssetProxyData = multiSig.registerAssetProxy.getABIEncodedTransactionData( const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
addressToRegister, addressToRegister,
isRegistered, isRegistered,
); );
const submitTxRes = await multiSigWrapper.submitTransactionAsync( const submitTxRes = await multiSigWrapper.submitTransactionAsync(
multiSig.address, testAssetProxyOwner.address,
registerAssetProxyData, registerAssetProxyData,
owners[0], owners[0],
); );
@ -170,19 +181,21 @@ describe('AssetProxyOwner', () => {
expect(registerLog.args.assetProxyContract).to.equal(addressToRegister); expect(registerLog.args.assetProxyContract).to.equal(addressToRegister);
expect(registerLog.args.isRegistered).to.equal(isRegistered); expect(registerLog.args.isRegistered).to.equal(isRegistered);
const isAssetProxyRegistered = await multiSig.isAssetProxyRegistered.callAsync(addressToRegister); const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync(
addressToRegister,
);
expect(isAssetProxyRegistered).to.equal(isRegistered); expect(isAssetProxyRegistered).to.equal(isRegistered);
}); });
it('should fail if registering a null address', async () => { it('should fail if registering a null address', async () => {
const addressToRegister = constants.NULL_ADDRESS; const addressToRegister = constants.NULL_ADDRESS;
const isRegistered = true; const isRegistered = true;
const registerAssetProxyData = multiSig.registerAssetProxy.getABIEncodedTransactionData( const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
addressToRegister, addressToRegister,
isRegistered, isRegistered,
); );
const submitTxRes = await multiSigWrapper.submitTransactionAsync( const submitTxRes = await multiSigWrapper.submitTransactionAsync(
multiSig.address, testAssetProxyOwner.address,
registerAssetProxyData, registerAssetProxyData,
owners[0], owners[0],
); );
@ -196,22 +209,26 @@ describe('AssetProxyOwner', () => {
const failureLog = executeTxRes.logs[0] as LogWithDecodedArgs<ExecutionFailureContractEventArgs>; const failureLog = executeTxRes.logs[0] as LogWithDecodedArgs<ExecutionFailureContractEventArgs>;
expect(failureLog.args.transactionId).to.be.bignumber.equal(txId); expect(failureLog.args.transactionId).to.be.bignumber.equal(txId);
const isAssetProxyRegistered = await multiSig.isAssetProxyRegistered.callAsync(addressToRegister); const isAssetProxyRegistered = await testAssetProxyOwner.isAssetProxyRegistered.callAsync(
addressToRegister,
);
expect(isAssetProxyRegistered).to.equal(false); expect(isAssetProxyRegistered).to.equal(false);
}); });
}); });
describe('executeRemoveAuthorizedAddress', () => { describe('Calling removeAuthorizedAddressAtIndex', () => {
const erc20Index = new BigNumber(0);
const erc721Index = new BigNumber(1);
before('authorize both proxies and register erc20 proxy', async () => { before('authorize both proxies and register erc20 proxy', async () => {
// Only register ERC20 proxy // Only register ERC20 proxy
const addressToRegister = erc20Proxy.address; const addressToRegister = erc20Proxy.address;
const isRegistered = true; const isRegistered = true;
const registerAssetProxyData = multiSig.registerAssetProxy.getABIEncodedTransactionData( const registerAssetProxyData = testAssetProxyOwner.registerAssetProxy.getABIEncodedTransactionData(
addressToRegister, addressToRegister,
isRegistered, isRegistered,
); );
const registerAssetProxySubmitRes = await multiSigWrapper.submitTransactionAsync( const registerAssetProxySubmitRes = await multiSigWrapper.submitTransactionAsync(
multiSig.address, testAssetProxyOwner.address,
registerAssetProxyData, registerAssetProxyData,
owners[0], owners[0],
); );
@ -248,113 +265,180 @@ describe('AssetProxyOwner', () => {
await multiSigWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0]); await multiSigWrapper.executeTransactionAsync(erc721AddAuthorizedAddressTxId, owners[0]);
}); });
it('should throw without the required confirmations', async () => { describe('validRemoveAuthorizedAddressAtIndexTx', () => {
const removeAuthorizedAddressData = erc20Proxy.removeAuthorizedAddress.getABIEncodedTransactionData( it('should revert if data is not for removeAuthorizedAddressAtIndex and proxy is registered', async () => {
authorized, const notRemoveAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
); authorized,
const res = await multiSigWrapper.submitTransactionAsync( );
erc20Proxy.address, const submitTxRes = await multiSigWrapper.submitTransactionAsync(
removeAuthorizedAddressData, erc20Proxy.address,
owners[0], notRemoveAuthorizedAddressData,
); owners[0],
const log = res.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>; );
const txId = log.args.transactionId; const log = submitTxRes.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = log.args.transactionId;
return expectRevertOrContractCallFailedAsync(
testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId),
);
});
return expectRevertOrAlwaysFailingTransactionAsync( it('should return true if data is for removeAuthorizedAddressAtIndex and proxy is registered', async () => {
multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
); authorized,
erc20Index,
);
const submitTxRes = await multiSigWrapper.submitTransactionAsync(
erc20Proxy.address,
removeAuthorizedAddressAtIndexData,
owners[0],
);
const log = submitTxRes.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = log.args.transactionId;
const isValidRemoveAuthorizedAddressAtIndexTx = await testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(
txId,
);
expect(isValidRemoveAuthorizedAddressAtIndexTx).to.be.true();
});
it('should revert if data is for removeAuthorizedAddressAtIndex and proxy is not registered', async () => {
const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
authorized,
erc721Index,
);
const submitTxRes = await multiSigWrapper.submitTransactionAsync(
erc721Proxy.address,
removeAuthorizedAddressAtIndexData,
owners[0],
);
const log = submitTxRes.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = log.args.transactionId;
return expectRevertOrContractCallFailedAsync(
testAssetProxyOwner.testValidRemoveAuthorizedAddressAtIndexTx.callAsync(txId),
);
});
}); });
it('should throw if tx destination is not registered', async () => { describe('executeRemoveAuthorizedAddressAtIndex', () => {
const removeAuthorizedAddressData = erc721Proxy.removeAuthorizedAddress.getABIEncodedTransactionData( it('should throw without the required confirmations', async () => {
authorized, const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
); authorized,
const res = await multiSigWrapper.submitTransactionAsync( erc20Index,
erc721Proxy.address, );
removeAuthorizedAddressData, const res = await multiSigWrapper.submitTransactionAsync(
owners[0], erc20Proxy.address,
); removeAuthorizedAddressAtIndexData,
const log = res.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>; owners[0],
const txId = log.args.transactionId; );
const log = res.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = log.args.transactionId;
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); return expectRevertOrAlwaysFailingTransactionAsync(
testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
from: owners[1],
}),
);
});
return expectRevertOrAlwaysFailingTransactionAsync( it('should throw if tx destination is not registered', async () => {
multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), const removeAuthorizedAddressAtIndexData = erc721Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
); authorized,
}); erc721Index,
);
const res = await multiSigWrapper.submitTransactionAsync(
erc721Proxy.address,
removeAuthorizedAddressAtIndexData,
owners[0],
);
const log = res.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = log.args.transactionId;
it('should throw if tx data is not for removeAuthorizedAddress', async () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
const newAuthorized = owners[1];
const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
newAuthorized,
);
const res = await multiSigWrapper.submitTransactionAsync(
erc20Proxy.address,
addAuthorizedAddressData,
owners[0],
);
const log = res.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = log.args.transactionId;
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); return expectRevertOrAlwaysFailingTransactionAsync(
testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
from: owners[1],
}),
);
});
return expectRevertOrAlwaysFailingTransactionAsync( it('should throw if tx data is not for removeAuthorizedAddressAtIndex', async () => {
multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), const newAuthorized = owners[1];
); const addAuthorizedAddressData = erc20Proxy.addAuthorizedAddress.getABIEncodedTransactionData(
}); newAuthorized,
);
const res = await multiSigWrapper.submitTransactionAsync(
erc20Proxy.address,
addAuthorizedAddressData,
owners[0],
);
const log = res.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = log.args.transactionId;
it('should execute removeAuthorizedAddress for registered address if fully confirmed', async () => { await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
const removeAuthorizedAddressData = erc20Proxy.removeAuthorizedAddress.getABIEncodedTransactionData(
authorized,
);
const submitRes = await multiSigWrapper.submitTransactionAsync(
erc20Proxy.address,
removeAuthorizedAddressData,
owners[0],
);
const submitLog = submitRes.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = submitLog.args.transactionId;
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); return expectRevertOrAlwaysFailingTransactionAsync(
testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
from: owners[1],
}),
);
});
const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAsync(txId, owners[0]); it('should execute removeAuthorizedAddressAtIndex for registered address if fully confirmed', async () => {
const execLog = execRes.logs[0] as LogWithDecodedArgs<ExecutionContractEventArgs>; const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
expect(execLog.args.transactionId).to.be.bignumber.equal(txId); authorized,
erc20Index,
);
const submitRes = await multiSigWrapper.submitTransactionAsync(
erc20Proxy.address,
removeAuthorizedAddressAtIndexData,
owners[0],
);
const submitLog = submitRes.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = submitLog.args.transactionId;
const tx = await multiSig.transactions.callAsync(txId); await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
const isExecuted = tx[3];
expect(isExecuted).to.equal(true);
const isAuthorized = await erc20Proxy.authorized.callAsync(authorized); const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, owners[0]);
expect(isAuthorized).to.equal(false); const execLog = execRes.logs[0] as LogWithDecodedArgs<ExecutionContractEventArgs>;
}); expect(execLog.args.transactionId).to.be.bignumber.equal(txId);
it('should throw if already executed', async () => { const tx = await testAssetProxyOwner.transactions.callAsync(txId);
const removeAuthorizedAddressData = erc20Proxy.removeAuthorizedAddress.getABIEncodedTransactionData( const isExecuted = tx[3];
authorized, expect(isExecuted).to.equal(true);
);
const submitRes = await multiSigWrapper.submitTransactionAsync(
erc20Proxy.address,
removeAuthorizedAddressData,
owners[0],
);
const submitLog = submitRes.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = submitLog.args.transactionId;
await multiSigWrapper.confirmTransactionAsync(txId, owners[1]); const isAuthorized = await erc20Proxy.authorized.callAsync(authorized);
expect(isAuthorized).to.equal(false);
});
const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAsync(txId, owners[0]); it('should throw if already executed', async () => {
const execLog = execRes.logs[0] as LogWithDecodedArgs<ExecutionContractEventArgs>; const removeAuthorizedAddressAtIndexData = erc20Proxy.removeAuthorizedAddressAtIndex.getABIEncodedTransactionData(
expect(execLog.args.transactionId).to.be.bignumber.equal(txId); authorized,
erc20Index,
);
const submitRes = await multiSigWrapper.submitTransactionAsync(
erc20Proxy.address,
removeAuthorizedAddressAtIndexData,
owners[0],
);
const submitLog = submitRes.logs[0] as LogWithDecodedArgs<SubmissionContractEventArgs>;
const txId = submitLog.args.transactionId;
const tx = await multiSig.transactions.callAsync(txId); await multiSigWrapper.confirmTransactionAsync(txId, owners[1]);
const isExecuted = tx[3];
expect(isExecuted).to.equal(true);
return expectRevertOrAlwaysFailingTransactionAsync( const execRes = await multiSigWrapper.executeRemoveAuthorizedAddressAtIndexAsync(txId, owners[0]);
multiSig.executeRemoveAuthorizedAddress.sendTransactionAsync(txId, { from: owners[1] }), const execLog = execRes.logs[0] as LogWithDecodedArgs<ExecutionContractEventArgs>;
); expect(execLog.args.transactionId).to.be.bignumber.equal(txId);
const tx = await testAssetProxyOwner.transactions.callAsync(txId);
const isExecuted = tx[3];
expect(isExecuted).to.equal(true);
return expectRevertOrAlwaysFailingTransactionAsync(
testAssetProxyOwner.executeRemoveAuthorizedAddressAtIndex.sendTransactionAsync(txId, {
from: owners[1],
}),
);
});
}); });
}); });
}); });

View File

@ -18,6 +18,12 @@ chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
// BUG: Ideally we would use Buffer.from(memory).toString('hex')
// https://github.com/Microsoft/TypeScript/issues/23155
const toHex = (buf: Uint8Array): string => buf.reduce((a, v) => a + ('00' + v.toString(16)).slice(-2), '0x');
const fromHex = (str: string): Uint8Array => Uint8Array.from(Buffer.from(str.slice(2), 'hex'));
describe('LibBytes', () => { describe('LibBytes', () => {
let libBytes: TestLibBytesContract; let libBytes: TestLibBytesContract;
const byteArrayShorterThan32Bytes = '0x012345'; const byteArrayShorterThan32Bytes = '0x012345';
@ -124,48 +130,55 @@ describe('LibBytes', () => {
}); });
}); });
describe('areBytesEqual', () => { describe('equals', () => {
it('should return true if byte arrays are equal (both arrays < 32 bytes)', async () => { it('should return true if byte arrays are equal (both arrays < 32 bytes)', async () => {
const areBytesEqual = await libBytes.publicAreBytesEqual.callAsync( const isEqual = await libBytes.publicEquals.callAsync(
byteArrayShorterThan32Bytes, byteArrayShorterThan32Bytes,
byteArrayShorterThan32Bytes, byteArrayShorterThan32Bytes,
); );
return expect(areBytesEqual).to.be.true(); return expect(isEqual).to.be.true();
}); });
it('should return true if byte arrays are equal (both arrays > 32 bytes)', async () => { it('should return true if byte arrays are equal (both arrays > 32 bytes)', async () => {
const areBytesEqual = await libBytes.publicAreBytesEqual.callAsync( const isEqual = await libBytes.publicEquals.callAsync(
byteArrayLongerThan32Bytes, byteArrayLongerThan32Bytes,
byteArrayLongerThan32Bytes, byteArrayLongerThan32Bytes,
); );
return expect(areBytesEqual).to.be.true(); return expect(isEqual).to.be.true();
}); });
it('should return false if byte arrays are not equal (first array < 32 bytes, second array > 32 bytes)', async () => { it('should return false if byte arrays are not equal (first array < 32 bytes, second array > 32 bytes)', async () => {
const areBytesEqual = await libBytes.publicAreBytesEqual.callAsync( const isEqual = await libBytes.publicEquals.callAsync(
byteArrayShorterThan32Bytes, byteArrayShorterThan32Bytes,
byteArrayLongerThan32Bytes, byteArrayLongerThan32Bytes,
); );
return expect(areBytesEqual).to.be.false(); return expect(isEqual).to.be.false();
}); });
it('should return false if byte arrays are not equal (first array > 32 bytes, second array < 32 bytes)', async () => { it('should return false if byte arrays are not equal (first array > 32 bytes, second array < 32 bytes)', async () => {
const areBytesEqual = await libBytes.publicAreBytesEqual.callAsync( const isEqual = await libBytes.publicEquals.callAsync(
byteArrayLongerThan32Bytes, byteArrayLongerThan32Bytes,
byteArrayShorterThan32Bytes, byteArrayShorterThan32Bytes,
); );
return expect(areBytesEqual).to.be.false(); return expect(isEqual).to.be.false();
}); });
it('should return false if byte arrays are not equal (same length, but a byte in first word differs)', async () => { it('should return false if byte arrays are not equal (same length, but a byte in first word differs)', async () => {
const areBytesEqual = await libBytes.publicAreBytesEqual.callAsync( const isEqual = await libBytes.publicEquals.callAsync(
byteArrayLongerThan32BytesFirstBytesSwapped, byteArrayLongerThan32BytesFirstBytesSwapped,
byteArrayLongerThan32Bytes, byteArrayLongerThan32Bytes,
); );
return expect(areBytesEqual).to.be.false(); return expect(isEqual).to.be.false();
}); });
it('should return false if byte arrays are not equal (same length, but a byte in last word differs)', async () => { it('should return false if byte arrays are not equal (same length, but a byte in last word differs)', async () => {
const areBytesEqual = await libBytes.publicAreBytesEqual.callAsync( const isEqual = await libBytes.publicEquals.callAsync(
byteArrayLongerThan32BytesLastBytesSwapped, byteArrayLongerThan32BytesLastBytesSwapped,
byteArrayLongerThan32Bytes, byteArrayLongerThan32Bytes,
); );
return expect(areBytesEqual).to.be.false(); return expect(isEqual).to.be.false();
});
describe('should ignore trailing data', () => {
it('should return true when both < 32 bytes', async () => {
const isEqual = await libBytes.publicEqualsPop1.callAsync('0x0102', '0x0103');
return expect(isEqual).to.be.true();
});
}); });
}); });
@ -444,26 +457,26 @@ describe('LibBytes', () => {
}); });
}); });
describe('readFirst4', () => { describe('readBytes4', () => {
// AssertionError: expected promise to be rejected with an error including 'revert' but it was fulfilled with '0x08c379a0' // AssertionError: expected promise to be rejected with an error including 'revert' but it was fulfilled with '0x08c379a0'
it('should revert if byte array has a length < 4', async () => { it('should revert if byte array has a length < 4', async () => {
const byteArrayLessThan4Bytes = '0x010101'; const byteArrayLessThan4Bytes = '0x010101';
return expectRevertOrOtherErrorAsync( return expectRevertOrOtherErrorAsync(
libBytes.publicReadFirst4.callAsync(byteArrayLessThan4Bytes), libBytes.publicReadBytes4.callAsync(byteArrayLessThan4Bytes, new BigNumber(0)),
RevertReasons.LibBytesGreaterOrEqualTo4LengthRequired, RevertReasons.LibBytesGreaterOrEqualTo4LengthRequired,
); );
}); });
it('should return the first 4 bytes of a byte array of arbitrary length', async () => { it('should return the first 4 bytes of a byte array of arbitrary length', async () => {
const first4Bytes = await libBytes.publicReadFirst4.callAsync(byteArrayLongerThan32Bytes); const first4Bytes = await libBytes.publicReadBytes4.callAsync(byteArrayLongerThan32Bytes, new BigNumber(0));
const expectedFirst4Bytes = byteArrayLongerThan32Bytes.slice(0, 10); const expectedFirst4Bytes = byteArrayLongerThan32Bytes.slice(0, 10);
expect(first4Bytes).to.equal(expectedFirst4Bytes); expect(first4Bytes).to.equal(expectedFirst4Bytes);
}); });
}); });
describe('readBytes', () => { describe('readBytesWithLength', () => {
it('should successfully read short, nested array of bytes when it takes up the whole array', async () => { it('should successfully read short, nested array of bytes when it takes up the whole array', async () => {
const testBytesOffset = new BigNumber(0); const testBytesOffset = new BigNumber(0);
const bytes = await libBytes.publicReadBytes.callAsync(shortTestBytes, testBytesOffset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(shortTestBytes, testBytesOffset);
return expect(bytes).to.be.equal(shortData); return expect(bytes).to.be.equal(shortData);
}); });
it('should successfully read short, nested array of bytes when it is offset in the array', async () => { it('should successfully read short, nested array of bytes when it is offset in the array', async () => {
@ -471,12 +484,12 @@ describe('LibBytes', () => {
const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, shortTestBytesAsBuffer]); const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, shortTestBytesAsBuffer]);
const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer);
const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength);
const bytes = await libBytes.publicReadBytes.callAsync(combinedByteArray, testUint256Offset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset);
return expect(bytes).to.be.equal(shortData); return expect(bytes).to.be.equal(shortData);
}); });
it('should successfully read a nested array of bytes - one word in length - when it takes up the whole array', async () => { it('should successfully read a nested array of bytes - one word in length - when it takes up the whole array', async () => {
const testBytesOffset = new BigNumber(0); const testBytesOffset = new BigNumber(0);
const bytes = await libBytes.publicReadBytes.callAsync(wordOfTestBytes, testBytesOffset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(wordOfTestBytes, testBytesOffset);
return expect(bytes).to.be.equal(wordOfData); return expect(bytes).to.be.equal(wordOfData);
}); });
it('should successfully read a nested array of bytes - one word in length - when it is offset in the array', async () => { it('should successfully read a nested array of bytes - one word in length - when it is offset in the array', async () => {
@ -484,12 +497,12 @@ describe('LibBytes', () => {
const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, wordOfTestBytesAsBuffer]); const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, wordOfTestBytesAsBuffer]);
const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer);
const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength);
const bytes = await libBytes.publicReadBytes.callAsync(combinedByteArray, testUint256Offset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset);
return expect(bytes).to.be.equal(wordOfData); return expect(bytes).to.be.equal(wordOfData);
}); });
it('should successfully read long, nested array of bytes when it takes up the whole array', async () => { it('should successfully read long, nested array of bytes when it takes up the whole array', async () => {
const testBytesOffset = new BigNumber(0); const testBytesOffset = new BigNumber(0);
const bytes = await libBytes.publicReadBytes.callAsync(longTestBytes, testBytesOffset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(longTestBytes, testBytesOffset);
return expect(bytes).to.be.equal(longData); return expect(bytes).to.be.equal(longData);
}); });
it('should successfully read long, nested array of bytes when it is offset in the array', async () => { it('should successfully read long, nested array of bytes when it is offset in the array', async () => {
@ -497,46 +510,50 @@ describe('LibBytes', () => {
const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, longTestBytesAsBuffer]); const combinedByteArrayBuffer = Buffer.concat([prefixByteArrayBuffer, longTestBytesAsBuffer]);
const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer); const combinedByteArray = ethUtil.bufferToHex(combinedByteArrayBuffer);
const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength); const testUint256Offset = new BigNumber(prefixByteArrayBuffer.byteLength);
const bytes = await libBytes.publicReadBytes.callAsync(combinedByteArray, testUint256Offset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(combinedByteArray, testUint256Offset);
return expect(bytes).to.be.equal(longData); return expect(bytes).to.be.equal(longData);
}); });
it('should fail if the byte array is too short to hold the length of a nested byte array', async () => { it('should fail if the byte array is too short to hold the length of a nested byte array', async () => {
// The length of the nested array is 32 bytes. By storing less than 32 bytes, a length cannot be read. // The length of the nested array is 32 bytes. By storing less than 32 bytes, a length cannot be read.
const offset = new BigNumber(0); const offset = new BigNumber(0);
return expectRevertOrOtherErrorAsync( return expectRevertOrOtherErrorAsync(
libBytes.publicReadBytes.callAsync(byteArrayShorterThan32Bytes, offset), libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, offset),
RevertReasons.LibBytesGreaterOrEqualTo32LengthRequired, RevertReasons.LibBytesGreaterOrEqualTo32LengthRequired,
); );
}); });
it('should fail if we store a nested byte array length, without a nested byte array', async () => { it('should fail if we store a nested byte array length, without a nested byte array', async () => {
const offset = new BigNumber(0); const offset = new BigNumber(0);
return expectRevertOrOtherErrorAsync( return expectRevertOrOtherErrorAsync(
libBytes.publicReadBytes.callAsync(testBytes32, offset), libBytes.publicReadBytesWithLength.callAsync(testBytes32, offset),
RevertReasons.LibBytesGreaterOrEqualToNestedBytesLengthRequired, RevertReasons.LibBytesGreaterOrEqualToNestedBytesLengthRequired,
); );
}); });
it('should fail if the length between the offset and end of the byte array is too short to hold the length of a nested byte array', async () => { it('should fail if the length between the offset and end of the byte array is too short to hold the length of a nested byte array', async () => {
const badOffset = new BigNumber(ethUtil.toBuffer(byteArrayShorterThan32Bytes).byteLength); const badOffset = new BigNumber(ethUtil.toBuffer(byteArrayShorterThan32Bytes).byteLength);
return expectRevertOrOtherErrorAsync( return expectRevertOrOtherErrorAsync(
libBytes.publicReadBytes.callAsync(byteArrayShorterThan32Bytes, badOffset), libBytes.publicReadBytesWithLength.callAsync(byteArrayShorterThan32Bytes, badOffset),
RevertReasons.LibBytesGreaterOrEqualTo32LengthRequired, RevertReasons.LibBytesGreaterOrEqualTo32LengthRequired,
); );
}); });
it('should fail if the length between the offset and end of the byte array is too short to hold the nested byte array', async () => { it('should fail if the length between the offset and end of the byte array is too short to hold the nested byte array', async () => {
const badOffset = new BigNumber(ethUtil.toBuffer(testBytes32).byteLength); const badOffset = new BigNumber(ethUtil.toBuffer(testBytes32).byteLength);
return expectRevertOrOtherErrorAsync( return expectRevertOrOtherErrorAsync(
libBytes.publicReadBytes.callAsync(testBytes32, badOffset), libBytes.publicReadBytesWithLength.callAsync(testBytes32, badOffset),
RevertReasons.LibBytesGreaterOrEqualTo32LengthRequired, RevertReasons.LibBytesGreaterOrEqualTo32LengthRequired,
); );
}); });
}); });
describe('writeBytes', () => { describe('writeBytesWithLength', () => {
it('should successfully write short, nested array of bytes when it takes up the whole array)', async () => { it('should successfully write short, nested array of bytes when it takes up the whole array)', async () => {
const testBytesOffset = new BigNumber(0); const testBytesOffset = new BigNumber(0);
const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength)); const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength));
const bytesWritten = await libBytes.publicWriteBytes.callAsync(emptyByteArray, testBytesOffset, shortData); const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(
const bytesRead = await libBytes.publicReadBytes.callAsync(bytesWritten, testBytesOffset); emptyByteArray,
testBytesOffset,
shortData,
);
const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset);
return expect(bytesRead).to.be.equal(shortData); return expect(bytesRead).to.be.equal(shortData);
}); });
it('should successfully write short, nested array of bytes when it is offset in the array', async () => { it('should successfully write short, nested array of bytes when it is offset in the array', async () => {
@ -547,19 +564,31 @@ describe('LibBytes', () => {
const emptyByteArray = ethUtil.bufferToHex( const emptyByteArray = ethUtil.bufferToHex(
new Buffer(prefixDataAsBuffer.byteLength + shortTestBytesAsBuffer.byteLength), new Buffer(prefixDataAsBuffer.byteLength + shortTestBytesAsBuffer.byteLength),
); );
let bytesWritten = await libBytes.publicWriteBytes.callAsync(emptyByteArray, prefixOffset, prefixData); let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(
emptyByteArray,
prefixOffset,
prefixData,
);
// Write data after prefix // Write data after prefix
const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength);
bytesWritten = await libBytes.publicWriteBytes.callAsync(bytesWritten, testBytesOffset, shortData); bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(
bytesWritten,
testBytesOffset,
shortData,
);
// Read data after prefix and validate // Read data after prefix and validate
const bytes = await libBytes.publicReadBytes.callAsync(bytesWritten, testBytesOffset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset);
return expect(bytes).to.be.equal(shortData); return expect(bytes).to.be.equal(shortData);
}); });
it('should successfully write a nested array of bytes - one word in length - when it takes up the whole array', async () => { it('should successfully write a nested array of bytes - one word in length - when it takes up the whole array', async () => {
const testBytesOffset = new BigNumber(0); const testBytesOffset = new BigNumber(0);
const emptyByteArray = ethUtil.bufferToHex(new Buffer(wordOfTestBytesAsBuffer.byteLength)); const emptyByteArray = ethUtil.bufferToHex(new Buffer(wordOfTestBytesAsBuffer.byteLength));
const bytesWritten = await libBytes.publicWriteBytes.callAsync(emptyByteArray, testBytesOffset, wordOfData); const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(
const bytesRead = await libBytes.publicReadBytes.callAsync(bytesWritten, testBytesOffset); emptyByteArray,
testBytesOffset,
wordOfData,
);
const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset);
return expect(bytesRead).to.be.equal(wordOfData); return expect(bytesRead).to.be.equal(wordOfData);
}); });
it('should successfully write a nested array of bytes - one word in length - when it is offset in the array', async () => { it('should successfully write a nested array of bytes - one word in length - when it is offset in the array', async () => {
@ -570,19 +599,31 @@ describe('LibBytes', () => {
const emptyByteArray = ethUtil.bufferToHex( const emptyByteArray = ethUtil.bufferToHex(
new Buffer(prefixDataAsBuffer.byteLength + wordOfTestBytesAsBuffer.byteLength), new Buffer(prefixDataAsBuffer.byteLength + wordOfTestBytesAsBuffer.byteLength),
); );
let bytesWritten = await libBytes.publicWriteBytes.callAsync(emptyByteArray, prefixOffset, prefixData); let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(
emptyByteArray,
prefixOffset,
prefixData,
);
// Write data after prefix // Write data after prefix
const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength);
bytesWritten = await libBytes.publicWriteBytes.callAsync(bytesWritten, testBytesOffset, wordOfData); bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(
bytesWritten,
testBytesOffset,
wordOfData,
);
// Read data after prefix and validate // Read data after prefix and validate
const bytes = await libBytes.publicReadBytes.callAsync(bytesWritten, testBytesOffset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset);
return expect(bytes).to.be.equal(wordOfData); return expect(bytes).to.be.equal(wordOfData);
}); });
it('should successfully write a long, nested bytes when it takes up the whole array', async () => { it('should successfully write a long, nested bytes when it takes up the whole array', async () => {
const testBytesOffset = new BigNumber(0); const testBytesOffset = new BigNumber(0);
const emptyByteArray = ethUtil.bufferToHex(new Buffer(longTestBytesAsBuffer.byteLength)); const emptyByteArray = ethUtil.bufferToHex(new Buffer(longTestBytesAsBuffer.byteLength));
const bytesWritten = await libBytes.publicWriteBytes.callAsync(emptyByteArray, testBytesOffset, longData); const bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(
const bytesRead = await libBytes.publicReadBytes.callAsync(bytesWritten, testBytesOffset); emptyByteArray,
testBytesOffset,
longData,
);
const bytesRead = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset);
return expect(bytesRead).to.be.equal(longData); return expect(bytesRead).to.be.equal(longData);
}); });
it('should successfully write long, nested array of bytes when it is offset in the array', async () => { it('should successfully write long, nested array of bytes when it is offset in the array', async () => {
@ -593,19 +634,23 @@ describe('LibBytes', () => {
const emptyByteArray = ethUtil.bufferToHex( const emptyByteArray = ethUtil.bufferToHex(
new Buffer(prefixDataAsBuffer.byteLength + longTestBytesAsBuffer.byteLength), new Buffer(prefixDataAsBuffer.byteLength + longTestBytesAsBuffer.byteLength),
); );
let bytesWritten = await libBytes.publicWriteBytes.callAsync(emptyByteArray, prefixOffset, prefixData); let bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(
emptyByteArray,
prefixOffset,
prefixData,
);
// Write data after prefix // Write data after prefix
const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength); const testBytesOffset = new BigNumber(prefixDataAsBuffer.byteLength);
bytesWritten = await libBytes.publicWriteBytes.callAsync(bytesWritten, testBytesOffset, longData); bytesWritten = await libBytes.publicWriteBytesWithLength.callAsync(bytesWritten, testBytesOffset, longData);
// Read data after prefix and validate // Read data after prefix and validate
const bytes = await libBytes.publicReadBytes.callAsync(bytesWritten, testBytesOffset); const bytes = await libBytes.publicReadBytesWithLength.callAsync(bytesWritten, testBytesOffset);
return expect(bytes).to.be.equal(longData); return expect(bytes).to.be.equal(longData);
}); });
it('should fail if the byte array is too short to hold the length of a nested byte array', async () => { it('should fail if the byte array is too short to hold the length of a nested byte array', async () => {
const offset = new BigNumber(0); const offset = new BigNumber(0);
const emptyByteArray = ethUtil.bufferToHex(new Buffer(1)); const emptyByteArray = ethUtil.bufferToHex(new Buffer(1));
return expectRevertOrOtherErrorAsync( return expectRevertOrOtherErrorAsync(
libBytes.publicWriteBytes.callAsync(emptyByteArray, offset, longData), libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, offset, longData),
RevertReasons.LibBytesGreaterOrEqualToNestedBytesLengthRequired, RevertReasons.LibBytesGreaterOrEqualToNestedBytesLengthRequired,
); );
}); });
@ -613,10 +658,176 @@ describe('LibBytes', () => {
const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength)); const emptyByteArray = ethUtil.bufferToHex(new Buffer(shortTestBytesAsBuffer.byteLength));
const badOffset = new BigNumber(ethUtil.toBuffer(shortTestBytesAsBuffer).byteLength); const badOffset = new BigNumber(ethUtil.toBuffer(shortTestBytesAsBuffer).byteLength);
return expectRevertOrOtherErrorAsync( return expectRevertOrOtherErrorAsync(
libBytes.publicWriteBytes.callAsync(emptyByteArray, badOffset, shortData), libBytes.publicWriteBytesWithLength.callAsync(emptyByteArray, badOffset, shortData),
RevertReasons.LibBytesGreaterOrEqualToNestedBytesLengthRequired, RevertReasons.LibBytesGreaterOrEqualToNestedBytesLengthRequired,
); );
}); });
}); });
describe('memCopy', () => {
// Create memory 0x000102...FF
const memSize = 256;
// tslint:disable:no-shadowed-variable
const memory = new Uint8Array(memSize).map((_, i) => i);
const memHex = toHex(memory);
// Reference implementation to test against
const refMemcpy = (mem: Uint8Array, dest: number, source: number, length: number): Uint8Array =>
Uint8Array.from(mem).copyWithin(dest, source, source + length);
// Test vectors: destination, source, length, job description
type Tests = Array<[number, number, number, string]>;
const test = (tests: Tests) =>
tests.forEach(([dest, source, length, job]) =>
it(job, async () => {
const expected = refMemcpy(memory, dest, source, length);
const resultStr = await libBytes.testMemcpy.callAsync(
memHex,
new BigNumber(dest),
new BigNumber(source),
new BigNumber(length),
);
const result = fromHex(resultStr);
expect(result).to.deep.equal(expected);
}),
);
test([[0, 0, 0, 'copies zero bytes with overlap']]);
describe('copies forward', () =>
test([
[128, 0, 0, 'zero bytes'],
[128, 0, 1, 'one byte'],
[128, 0, 11, 'eleven bytes'],
[128, 0, 31, 'thirty-one bytes'],
[128, 0, 32, 'one word'],
[128, 0, 64, 'two words'],
[128, 0, 96, 'three words'],
[128, 0, 33, 'one word and one byte'],
[128, 0, 72, 'two words and eight bytes'],
[128, 0, 100, 'three words and four bytes'],
]));
describe('copies forward within one word', () =>
test([
[16, 0, 0, 'zero bytes'],
[16, 0, 1, 'one byte'],
[16, 0, 11, 'eleven bytes'],
[16, 0, 16, 'sixteen bytes'],
]));
describe('copies forward with one byte overlap', () =>
test([
[0, 0, 1, 'one byte'],
[10, 0, 11, 'eleven bytes'],
[30, 0, 31, 'thirty-one bytes'],
[31, 0, 32, 'one word'],
[32, 0, 33, 'one word and one byte'],
[71, 0, 72, 'two words and eight bytes'],
[99, 0, 100, 'three words and four bytes'],
]));
describe('copies forward with thirty-one bytes overlap', () =>
test([
[0, 0, 31, 'thirty-one bytes'],
[1, 0, 32, 'one word'],
[2, 0, 33, 'one word and one byte'],
[41, 0, 72, 'two words and eight bytes'],
[69, 0, 100, 'three words and four bytes'],
]));
describe('copies forward with one word overlap', () =>
test([
[0, 0, 32, 'one word'],
[1, 0, 33, 'one word and one byte'],
[41, 0, 72, 'two words and eight bytes'],
[69, 0, 100, 'three words and four bytes'],
]));
describe('copies forward with one word and one byte overlap', () =>
test([
[0, 0, 33, 'one word and one byte'],
[40, 0, 72, 'two words and eight bytes'],
[68, 0, 100, 'three words and four bytes'],
]));
describe('copies forward with two words overlap', () =>
test([
[0, 0, 64, 'two words'],
[8, 0, 72, 'two words and eight bytes'],
[36, 0, 100, 'three words and four bytes'],
]));
describe('copies forward within one word and one byte overlap', () =>
test([[0, 0, 1, 'one byte'], [10, 0, 11, 'eleven bytes'], [15, 0, 16, 'sixteen bytes']]));
describe('copies backward', () =>
test([
[0, 128, 0, 'zero bytes'],
[0, 128, 1, 'one byte'],
[0, 128, 11, 'eleven bytes'],
[0, 128, 31, 'thirty-one bytes'],
[0, 128, 32, 'one word'],
[0, 128, 64, 'two words'],
[0, 128, 96, 'three words'],
[0, 128, 33, 'one word and one byte'],
[0, 128, 72, 'two words and eight bytes'],
[0, 128, 100, 'three words and four bytes'],
]));
describe('copies backward within one word', () =>
test([
[0, 16, 0, 'zero bytes'],
[0, 16, 1, 'one byte'],
[0, 16, 11, 'eleven bytes'],
[0, 16, 16, 'sixteen bytes'],
]));
describe('copies backward with one byte overlap', () =>
test([
[0, 0, 1, 'one byte'],
[0, 10, 11, 'eleven bytes'],
[0, 30, 31, 'thirty-one bytes'],
[0, 31, 32, 'one word'],
[0, 32, 33, 'one word and one byte'],
[0, 71, 72, 'two words and eight bytes'],
[0, 99, 100, 'three words and four bytes'],
]));
describe('copies backward with thirty-one bytes overlap', () =>
test([
[0, 0, 31, 'thirty-one bytes'],
[0, 1, 32, 'one word'],
[0, 2, 33, 'one word and one byte'],
[0, 41, 72, 'two words and eight bytes'],
[0, 69, 100, 'three words and four bytes'],
]));
describe('copies backward with one word overlap', () =>
test([
[0, 0, 32, 'one word'],
[0, 1, 33, 'one word and one byte'],
[0, 41, 72, 'two words and eight bytes'],
[0, 69, 100, 'three words and four bytes'],
]));
describe('copies backward with one word and one byte overlap', () =>
test([
[0, 0, 33, 'one word and one byte'],
[0, 40, 72, 'two words and eight bytes'],
[0, 68, 100, 'three words and four bytes'],
]));
describe('copies backward with two words overlap', () =>
test([
[0, 0, 64, 'two words'],
[0, 8, 72, 'two words and eight bytes'],
[0, 36, 100, 'three words and four bytes'],
]));
describe('copies forward within one word and one byte overlap', () =>
test([[0, 0, 1, 'one byte'], [0, 10, 11, 'eleven bytes'], [0, 15, 16, 'sixteen bytes']]));
});
}); });
// tslint:disable:max-file-line-count // tslint:disable:max-file-line-count

View File

@ -1,190 +0,0 @@
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import { TestLibMemContract } from '../../src/generated_contract_wrappers/test_lib_mem';
import { artifacts } from '../../src/utils/artifacts';
import { chaiSetup } from '../../src/utils/chai_setup';
import { provider, txDefaults } from '../../src/utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
// BUG: Ideally we would use Buffer.from(memory).toString('hex')
// https://github.com/Microsoft/TypeScript/issues/23155
const toHex = (buf: Uint8Array): string => buf.reduce((a, v) => a + ('00' + v.toString(16)).slice(-2), '0x');
const fromHex = (str: string): Uint8Array => Uint8Array.from(Buffer.from(str.slice(2), 'hex'));
describe('LibMem', () => {
let testLibMem: TestLibMemContract;
before(async () => {
// Deploy TestLibMem
testLibMem = await TestLibMemContract.deployFrom0xArtifactAsync(artifacts.TestLibMem, provider, txDefaults);
});
describe('memCopy', () => {
// Create memory 0x000102...FF
const memSize = 256;
const memory = new Uint8Array(memSize).map((_, i) => i);
const memHex = toHex(memory);
// Reference implementation to test against
const refMemcpy = (_mem: Uint8Array, dest: number, source: number, length: number): Uint8Array =>
Uint8Array.from(memory).copyWithin(dest, source, source + length);
// Test vectors: destination, source, length, job description
type Tests = Array<[number, number, number, string]>;
const test = (tests: Tests) =>
tests.forEach(([dest, source, length, job]) =>
it(job, async () => {
const expected = refMemcpy(memory, dest, source, length);
const resultStr = await testLibMem.testMemcpy.callAsync(
memHex,
new BigNumber(dest),
new BigNumber(source),
new BigNumber(length),
);
const result = fromHex(resultStr);
expect(result).to.deep.equal(expected);
}),
);
test([[0, 0, 0, 'copies zero bytes with overlap']]);
describe('copies forward', () =>
test([
[128, 0, 0, 'zero bytes'],
[128, 0, 1, 'one byte'],
[128, 0, 11, 'eleven bytes'],
[128, 0, 31, 'thirty-one bytes'],
[128, 0, 32, 'one word'],
[128, 0, 64, 'two words'],
[128, 0, 96, 'three words'],
[128, 0, 33, 'one word and one byte'],
[128, 0, 72, 'two words and eight bytes'],
[128, 0, 100, 'three words and four bytes'],
]));
describe('copies forward within one word', () =>
test([
[16, 0, 0, 'zero bytes'],
[16, 0, 1, 'one byte'],
[16, 0, 11, 'eleven bytes'],
[16, 0, 16, 'sixteen bytes'],
]));
describe('copies forward with one byte overlap', () =>
test([
[0, 0, 1, 'one byte'],
[10, 0, 11, 'eleven bytes'],
[30, 0, 31, 'thirty-one bytes'],
[31, 0, 32, 'one word'],
[32, 0, 33, 'one word and one byte'],
[71, 0, 72, 'two words and eight bytes'],
[99, 0, 100, 'three words and four bytes'],
]));
describe('copies forward with thirty-one bytes overlap', () =>
test([
[0, 0, 31, 'thirty-one bytes'],
[1, 0, 32, 'one word'],
[2, 0, 33, 'one word and one byte'],
[41, 0, 72, 'two words and eight bytes'],
[69, 0, 100, 'three words and four bytes'],
]));
describe('copies forward with one word overlap', () =>
test([
[0, 0, 32, 'one word'],
[1, 0, 33, 'one word and one byte'],
[41, 0, 72, 'two words and eight bytes'],
[69, 0, 100, 'three words and four bytes'],
]));
describe('copies forward with one word and one byte overlap', () =>
test([
[0, 0, 33, 'one word and one byte'],
[40, 0, 72, 'two words and eight bytes'],
[68, 0, 100, 'three words and four bytes'],
]));
describe('copies forward with two words overlap', () =>
test([
[0, 0, 64, 'two words'],
[8, 0, 72, 'two words and eight bytes'],
[36, 0, 100, 'three words and four bytes'],
]));
describe('copies forward within one word and one byte overlap', () =>
test([[0, 0, 1, 'one byte'], [10, 0, 11, 'eleven bytes'], [15, 0, 16, 'sixteen bytes']]));
describe('copies backward', () =>
test([
[0, 128, 0, 'zero bytes'],
[0, 128, 1, 'one byte'],
[0, 128, 11, 'eleven bytes'],
[0, 128, 31, 'thirty-one bytes'],
[0, 128, 32, 'one word'],
[0, 128, 64, 'two words'],
[0, 128, 96, 'three words'],
[0, 128, 33, 'one word and one byte'],
[0, 128, 72, 'two words and eight bytes'],
[0, 128, 100, 'three words and four bytes'],
]));
describe('copies backward within one word', () =>
test([
[0, 16, 0, 'zero bytes'],
[0, 16, 1, 'one byte'],
[0, 16, 11, 'eleven bytes'],
[0, 16, 16, 'sixteen bytes'],
]));
describe('copies backward with one byte overlap', () =>
test([
[0, 0, 1, 'one byte'],
[0, 10, 11, 'eleven bytes'],
[0, 30, 31, 'thirty-one bytes'],
[0, 31, 32, 'one word'],
[0, 32, 33, 'one word and one byte'],
[0, 71, 72, 'two words and eight bytes'],
[0, 99, 100, 'three words and four bytes'],
]));
describe('copies backward with thirty-one bytes overlap', () =>
test([
[0, 0, 31, 'thirty-one bytes'],
[0, 1, 32, 'one word'],
[0, 2, 33, 'one word and one byte'],
[0, 41, 72, 'two words and eight bytes'],
[0, 69, 100, 'three words and four bytes'],
]));
describe('copies backward with one word overlap', () =>
test([
[0, 0, 32, 'one word'],
[0, 1, 33, 'one word and one byte'],
[0, 41, 72, 'two words and eight bytes'],
[0, 69, 100, 'three words and four bytes'],
]));
describe('copies backward with one word and one byte overlap', () =>
test([
[0, 0, 33, 'one word and one byte'],
[0, 40, 72, 'two words and eight bytes'],
[0, 68, 100, 'three words and four bytes'],
]));
describe('copies backward with two words overlap', () =>
test([
[0, 0, 64, 'two words'],
[0, 8, 72, 'two words and eight bytes'],
[0, 36, 100, 'three words and four bytes'],
]));
describe('copies forward within one word and one byte overlap', () =>
test([[0, 0, 1, 'one byte'], [0, 10, 11, 'eleven bytes'], [0, 15, 16, 'sixteen bytes']]));
});
});

View File

@ -36,11 +36,15 @@
"@0xproject/subproviders": "^0.10.4", "@0xproject/subproviders": "^0.10.4",
"@0xproject/tslint-config": "^0.4.20", "@0xproject/tslint-config": "^0.4.20",
"@0xproject/types": "^0.8.1", "@0xproject/types": "^0.8.1",
"@0xproject/typescript-typings": "^0.4.1",
"@0xproject/utils": "^0.7.1", "@0xproject/utils": "^0.7.1",
"@0xproject/web3-wrapper": "^0.7.1", "@0xproject/web3-wrapper": "^0.7.1",
"ethereum-types": "^0.0.1", "@types/mocha": "^5.2.2",
"copyfiles": "^2.0.0",
"ethereum-types": "^0.0.2",
"ethers": "3.0.22", "ethers": "3.0.22",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"run-s": "^0.0.0",
"web3-provider-engine": "^14.0.4" "web3-provider-engine": "^14.0.4"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,7 +1,16 @@
{ {
"extends": "../../tsconfig",
"compilerOptions": { "compilerOptions": {
"outDir": "lib" "outDir": "lib",
"lib": ["es2017"],
"skipLibCheck": true,
"typeRoots": [
"comment: for building within 0x-monorepo:",
"../../node_modules/@0xproject/typescript-typings/types",
"../../node_modules/@types",
"comment: for building in an isolated environment:",
"node_modules/@0xproject/typescript-typings/types",
"node_modules/@types"
]
}, },
"include": ["src/**/*", "test/**/*"] "include": ["src/**/*", "test/**/*"]
} }

View File

@ -12,8 +12,8 @@ const expect = chai.expect;
describe('Order hashing', () => { describe('Order hashing', () => {
describe('#getOrderHashHex', () => { describe('#getOrderHashHex', () => {
const expectedOrderHash = '0x367ad7730eb8b5feab8a9c9f47c6fcba77a2d4df125ee6a59cc26ac955710f7e'; const expectedOrderHash = '0x434c6b41e2fb6dfcfe1b45c4492fb03700798e9c1afc6f801ba6203f948c1fa7';
const fakeExchangeContractAddress = '0xb69e673309512a9d726f87304c6984054f87a93b'; const fakeExchangeContractAddress = '0x1dc4c1cefef38a777b15aa20260a54e584b16c48';
const order: Order = { const order: Order = {
makerAddress: constants.NULL_ADDRESS, makerAddress: constants.NULL_ADDRESS,
takerAddress: constants.NULL_ADDRESS, takerAddress: constants.NULL_ADDRESS,
@ -29,15 +29,11 @@ describe('Order hashing', () => {
takerAssetAmount: new BigNumber(0), takerAssetAmount: new BigNumber(0),
expirationTimeSeconds: new BigNumber(0), expirationTimeSeconds: new BigNumber(0),
}; };
// HACK: Temporarily disable these tests until @dekz has time to fix. it('calculates the order hash', async () => {
// This allows us to get all tests running on CI immediately
it.skip('calculates the order hash', async () => {
const orderHash = orderHashUtils.getOrderHashHex(order); const orderHash = orderHashUtils.getOrderHashHex(order);
expect(orderHash).to.be.equal(expectedOrderHash); expect(orderHash).to.be.equal(expectedOrderHash);
}); });
// HACK: Temporarily disable these tests until @dekz has time to fix. it('throws a readable error message if taker format is invalid', async () => {
// This allows us to get all tests running on CI immediately
it.skip('throws a readable error message if taker format is invalid', async () => {
const orderWithInvalidtakerFormat = { const orderWithInvalidtakerFormat = {
...order, ...order,
takerAddress: (null as any) as string, takerAddress: (null as any) as string,

View File

@ -9,6 +9,7 @@ import { TokenIcon } from 'ts/components/ui/token_icon';
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
import { Dispatcher } from 'ts/redux/dispatcher'; import { Dispatcher } from 'ts/redux/dispatcher';
import { DialogConfigs, Token, TokenByAddress, TokenVisibility } from 'ts/types'; import { DialogConfigs, Token, TokenByAddress, TokenVisibility } from 'ts/types';
import { constants } from 'ts/utils/constants';
const TOKEN_ICON_DIMENSION = 100; const TOKEN_ICON_DIMENSION = 100;
const TILE_DIMENSION = 146; const TILE_DIMENSION = 146;
@ -134,7 +135,9 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
const gridTiles = _.map(this.props.tokenByAddress, (token: Token, address: string) => { const gridTiles = _.map(this.props.tokenByAddress, (token: Token, address: string) => {
if ( if (
(this.props.tokenVisibility === TokenVisibility.TRACKED && !token.isTracked) || (this.props.tokenVisibility === TokenVisibility.TRACKED && !token.isTracked) ||
(this.props.tokenVisibility === TokenVisibility.UNTRACKED && token.isTracked) (this.props.tokenVisibility === TokenVisibility.UNTRACKED && token.isTracked) ||
token.symbol === constants.ZRX_TOKEN_SYMBOL ||
token.symbol === constants.ETHER_TOKEN_SYMBOL
) { ) {
return null; // Skip return null; // Skip
} }
@ -232,12 +235,14 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
this.props.onTokenChosen(newToken.address); this.props.onTokenChosen(newToken.address);
} }
private async _onTrackConfirmationRespondedAsync(didUserAcceptTracking: boolean): Promise<void> { private async _onTrackConfirmationRespondedAsync(didUserAcceptTracking: boolean): Promise<void> {
const resetState: AssetPickerState = {
...this.state,
isAddingTokenToTracked: false,
assetView: AssetViews.ASSET_PICKER,
chosenTrackTokenAddress: undefined,
};
if (!didUserAcceptTracking) { if (!didUserAcceptTracking) {
this.setState({ this.setState(resetState);
isAddingTokenToTracked: false,
assetView: AssetViews.ASSET_PICKER,
chosenTrackTokenAddress: undefined,
});
this._onCloseDialog(); this._onCloseDialog();
return; return;
} }
@ -246,6 +251,10 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
}); });
const tokenAddress = this.state.chosenTrackTokenAddress; const tokenAddress = this.state.chosenTrackTokenAddress;
const token = this.props.tokenByAddress[tokenAddress]; const token = this.props.tokenByAddress[tokenAddress];
if (_.isUndefined(tokenAddress)) {
this.setState(resetState);
return;
}
const newTokenEntry = { const newTokenEntry = {
...token, ...token,
}; };
@ -254,11 +263,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry); trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
this.props.dispatcher.updateTokenByAddress([newTokenEntry]); this.props.dispatcher.updateTokenByAddress([newTokenEntry]);
this.setState({ this.setState(resetState);
isAddingTokenToTracked: false,
assetView: AssetViews.ASSET_PICKER,
chosenTrackTokenAddress: undefined,
});
this.props.onTokenChosen(tokenAddress); this.props.onTokenChosen(tokenAddress);
} }
} }

View File

@ -42,7 +42,11 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> {
onboardingElement = <Animation type="easeUpFromBottom">{this._renderOnboardignCard()}</Animation>; onboardingElement = <Animation type="easeUpFromBottom">{this._renderOnboardignCard()}</Animation>;
} else { } else {
onboardingElement = ( onboardingElement = (
<Popper referenceElement={this._getElementForStep()} placement={this._getCurrentStep().placement}> <Popper
referenceElement={this._getElementForStep()}
placement={this._getCurrentStep().placement}
positionFixed={true}
>
{this._renderPopperChildren.bind(this)} {this._renderPopperChildren.bind(this)}
</Popper> </Popper>
); );

View File

@ -65,7 +65,7 @@ interface HeaderProps {
const Header = (props: HeaderProps) => { const Header = (props: HeaderProps) => {
return ( return (
<div className="flex flex-center py4"> <div className="flex flex-center py4">
<div className="flex flex-column mx-auto"> <div className="flex flex-column mx-auto items-center">
<Identicon address={props.userAddress} diameter={IDENTICON_DIAMETER} style={styles.identicon} /> <Identicon address={props.userAddress} diameter={IDENTICON_DIAMETER} style={styles.identicon} />
<Text className="pt2" fontColor={colors.white}> <Text className="pt2" fontColor={colors.white}>
{props.displayMessage} {props.displayMessage}

View File

@ -1,4 +1,4 @@
import { colors, constants as sharedConstants, Styles } from '@0xproject/react-shared'; import { colors, constants as sharedConstants } from '@0xproject/react-shared';
import { BigNumber } from '@0xproject/utils'; import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
@ -107,26 +107,7 @@ const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded);
const LEFT_COLUMN_WIDTH = 346; const LEFT_COLUMN_WIDTH = 346;
const MENU_PADDING_LEFT = 185; const MENU_PADDING_LEFT = 185;
const LARGE_LAYOUT_MAX_WIDTH = 1200; const LARGE_LAYOUT_MAX_WIDTH = 1200;
const LARGE_LAYOUT_MARGIN = 30;
const styles: Styles = {
root: {
width: '100%',
height: '100%',
backgroundColor: colors.lightestGrey,
},
body: {
height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
},
leftColumn: {
width: LEFT_COLUMN_WIDTH,
height: '100%',
},
scrollContainer: {
height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
WebkitOverflowScrolling: 'touch',
overflow: 'auto',
},
};
export class Portal extends React.Component<PortalProps, PortalState> { export class Portal extends React.Component<PortalProps, PortalState> {
private _blockchain: Blockchain; private _blockchain: Blockchain;
@ -245,7 +226,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
? TokenVisibility.UNTRACKED ? TokenVisibility.UNTRACKED
: TokenVisibility.TRACKED; : TokenVisibility.TRACKED;
return ( return (
<div style={styles.root}> <Container>
<DocumentTitle title="0x Portal DApp" /> <DocumentTitle title="0x Portal DApp" />
<TopBar <TopBar
userAddress={this.props.userAddress} userAddress={this.props.userAddress}
@ -259,10 +240,14 @@ export class Portal extends React.Component<PortalProps, PortalState> {
blockchain={this._blockchain} blockchain={this._blockchain}
translate={this.props.translate} translate={this.props.translate}
displayType={TopBarDisplayType.Expanded} displayType={TopBarDisplayType.Expanded}
style={{ backgroundColor: colors.lightestGrey }} style={{
backgroundColor: colors.lightestGrey,
position: 'fixed',
zIndex: zIndex.topBar,
}}
maxWidth={LARGE_LAYOUT_MAX_WIDTH} maxWidth={LARGE_LAYOUT_MAX_WIDTH}
/> />
<div id="portal" style={styles.body}> <Container marginTop={TOP_BAR_HEIGHT} minHeight="100vh" backgroundColor={colors.lightestGrey}>
<Switch> <Switch>
<Route path={`${WebsitePaths.Portal}/:route`} render={this._renderOtherRoutes.bind(this)} /> <Route path={`${WebsitePaths.Portal}/:route`} render={this._renderOtherRoutes.bind(this)} />
<Route <Route
@ -301,13 +286,8 @@ export class Portal extends React.Component<PortalProps, PortalState> {
tokenByAddress={this.props.tokenByAddress} tokenByAddress={this.props.tokenByAddress}
tokenVisibility={tokenVisibility} tokenVisibility={tokenVisibility}
/> />
</div> </Container>
<PortalOnboardingFlow </Container>
blockchain={this._blockchain}
trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
/>
</div>
); );
} }
private _renderMainRoute(): React.ReactNode { private _renderMainRoute(): React.ReactNode {
@ -341,41 +321,48 @@ export class Portal extends React.Component<PortalProps, PortalState> {
} }
private _renderWallet(): React.ReactNode { private _renderWallet(): React.ReactNode {
const startOnboarding = this._renderStartOnboarding(); const startOnboarding = this._renderStartOnboarding();
const isMobile = this.props.screenWidth === ScreenWidths.Sm; const isMobile = utils.isMobile(this.props.screenWidth);
// We need room to scroll down for mobile onboarding // We need room to scroll down for mobile onboarding
const marginBottom = isMobile ? '200px' : '15px'; const marginBottom = isMobile ? '200px' : '15px';
return ( return (
<div> <div>
{isMobile && <Container marginBottom="15px">{startOnboarding}</Container>} <Container>
<Container marginBottom={marginBottom}> {isMobile && <Container marginBottom="15px">{startOnboarding}</Container>}
<Wallet <Container marginBottom={marginBottom}>
style={ <Wallet
!isMobile && this.props.isPortalOnboardingShowing style={
? { zIndex: zIndex.aboveOverlay, position: 'relative' } !isMobile && this.props.isPortalOnboardingShowing
: undefined ? { zIndex: zIndex.aboveOverlay, position: 'relative' }
} : undefined
userAddress={this.props.userAddress} }
networkId={this.props.networkId} userAddress={this.props.userAddress}
blockchain={this._blockchain} networkId={this.props.networkId}
blockchainIsLoaded={this.props.blockchainIsLoaded} blockchain={this._blockchain}
blockchainErr={this.props.blockchainErr} blockchainIsLoaded={this.props.blockchainIsLoaded}
dispatcher={this.props.dispatcher} blockchainErr={this.props.blockchainErr}
tokenByAddress={this.props.tokenByAddress} dispatcher={this.props.dispatcher}
trackedTokens={this._getCurrentTrackedTokens()} tokenByAddress={this.props.tokenByAddress}
userEtherBalanceInWei={this.props.userEtherBalanceInWei} trackedTokens={this._getCurrentTrackedTokens()}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} userEtherBalanceInWei={this.props.userEtherBalanceInWei}
injectedProviderName={this.props.injectedProviderName} lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
providerType={this.props.providerType} injectedProviderName={this.props.injectedProviderName}
screenWidth={this.props.screenWidth} providerType={this.props.providerType}
location={this.props.location} screenWidth={this.props.screenWidth}
trackedTokenStateByAddress={this.state.trackedTokenStateByAddress} location={this.props.location}
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)} trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
onAddToken={this._onAddToken.bind(this)} onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
onRemoveToken={this._onRemoveToken.bind(this)} onAddToken={this._onAddToken.bind(this)}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)} onRemoveToken={this._onRemoveToken.bind(this)}
/> refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
/>
</Container>
{!isMobile && <Container marginTop="15px">{startOnboarding}</Container>}
</Container> </Container>
{!isMobile && <Container marginTop="15px">{startOnboarding}</Container>} <PortalOnboardingFlow
blockchain={this._blockchain}
trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
/>
</div> </div>
); );
} }
@ -551,7 +538,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
private _renderRelayerIndexSection(): React.ReactNode { private _renderRelayerIndexSection(): React.ReactNode {
return ( return (
<Section <Section
header={<TextHeader labelText="Explore 0x Relayers" />} header={<TextHeader labelText="0x Relayers" />}
body={<RelayerIndex networkId={this.props.networkId} screenWidth={this.props.screenWidth} />} body={<RelayerIndex networkId={this.props.networkId} screenWidth={this.props.screenWidth} />}
/> />
); );
@ -708,14 +695,23 @@ interface LargeLayoutProps {
} }
const LargeLayout = (props: LargeLayoutProps) => { const LargeLayout = (props: LargeLayoutProps) => {
return ( return (
<div className="mx-auto flex flex-center" style={{ maxWidth: LARGE_LAYOUT_MAX_WIDTH }}> <Container className="mx-auto flex flex-center" maxWidth={LARGE_LAYOUT_MAX_WIDTH}>
<div className="flex-last px2"> <div className="flex-last">
<div style={styles.leftColumn}>{props.left}</div> <Container
width={LEFT_COLUMN_WIDTH}
position="fixed"
zIndex={zIndex.aboveTopBar}
marginLeft={LARGE_LAYOUT_MARGIN}
>
{props.left}
</Container>
</div> </div>
<div className="flex-auto px2" style={styles.scrollContainer}> <Container className="flex-auto" marginLeft={LEFT_COLUMN_WIDTH + LARGE_LAYOUT_MARGIN}>
{props.right} <Container className="flex-auto" marginLeft={LARGE_LAYOUT_MARGIN} marginRight={LARGE_LAYOUT_MARGIN}>
</div> {props.right}
</div> </Container>
</Container>
</Container>
); );
}; };
@ -725,9 +721,7 @@ interface SmallLayoutProps {
const SmallLayout = (props: SmallLayoutProps) => { const SmallLayout = (props: SmallLayoutProps) => {
return ( return (
<div className="flex flex-center"> <div className="flex flex-center">
<div className="flex-auto px3" style={styles.scrollContainer}> <div className="flex-auto px3">{props.content}</div>
{props.content}
</div>
</div> </div>
); );
}; // tslint:disable:max-file-line-count }; // tslint:disable:max-file-line-count

View File

@ -1,21 +1,16 @@
import { Styles } from '@0xproject/react-shared'; import { colors } from '@0xproject/react-shared';
import * as React from 'react'; import * as React from 'react';
import { Text } from 'ts/components/ui/text';
export interface TextHeaderProps { export interface TextHeaderProps {
labelText: string; labelText: string;
} }
const styles: Styles = {
title: {
fontWeight: 'bold',
fontSize: 20,
},
};
export const TextHeader = (props: TextHeaderProps) => { export const TextHeader = (props: TextHeaderProps) => {
return ( return (
<div className="py3" style={styles.title}> <Text className="pt3 pb2" fontWeight="bold" fontSize="16px" fontColor={colors.darkestGrey}>
{props.labelText} {props.labelText}
</div> </Text>
); );
}; };

View File

@ -1,6 +1,6 @@
import { constants as sharedConstants, Styles } from '@0xproject/react-shared'; import { constants as sharedConstants, Styles } from '@0xproject/react-shared';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { GridTile } from 'material-ui/GridList'; import { GridTile as PlainGridTile } from 'material-ui/GridList';
import * as React from 'react'; import * as React from 'react';
import { analytics } from 'ts/utils/analytics'; import { analytics } from 'ts/utils/analytics';
@ -9,7 +9,9 @@ import { Container } from 'ts/components/ui/container';
import { Image } from 'ts/components/ui/image'; import { Image } from 'ts/components/ui/image';
import { Island } from 'ts/components/ui/island'; import { Island } from 'ts/components/ui/island';
import { colors } from 'ts/style/colors'; import { colors } from 'ts/style/colors';
import { styled } from 'ts/style/theme';
import { WebsiteBackendRelayerInfo } from 'ts/types'; import { WebsiteBackendRelayerInfo } from 'ts/types';
import { utils } from 'ts/utils/utils';
export interface RelayerGridTileProps { export interface RelayerGridTileProps {
relayerInfo: WebsiteBackendRelayerInfo; relayerInfo: WebsiteBackendRelayerInfo;
@ -19,29 +21,23 @@ export interface RelayerGridTileProps {
const styles: Styles = { const styles: Styles = {
root: { root: {
boxSizing: 'border-box', boxSizing: 'border-box',
// All material UI components have position: relative
// which creates a new stacking context and makes z-index stuff impossible. So reset.
position: 'static',
}, },
innerDiv: { innerDiv: {
padding: 6,
height: '100%', height: '100%',
boxSizing: 'border-box', boxSizing: 'border-box',
}, },
header: { header: {
height: '50%', height: '50%',
width: '100%', width: '100%',
borderBottomRightRadius: 4,
borderBottomLeftRadius: 4,
borderTopRightRadius: 4,
borderTopLeftRadius: 4,
borderWidth: 1,
borderStyle: 'solid',
borderColor: colors.walletBorder,
}, },
body: { body: {
paddingLeft: 6,
paddingRight: 6,
height: '50%', height: '50%',
width: '100%', width: '100%',
boxSizing: 'border-box', boxSizing: 'border-box',
padding: 12,
}, },
weeklyTradeVolumeLabel: { weeklyTradeVolumeLabel: {
fontSize: 14, fontSize: 14,
@ -69,7 +65,10 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (
const weeklyTxnVolume = props.relayerInfo.weeklyTxnVolume; const weeklyTxnVolume = props.relayerInfo.weeklyTxnVolume;
const networkName = sharedConstants.NETWORK_NAME_BY_ID[props.networkId]; const networkName = sharedConstants.NETWORK_NAME_BY_ID[props.networkId];
const eventLabel = `${props.relayerInfo.name}-${networkName}`; const eventLabel = `${props.relayerInfo.name}-${networkName}`;
const trackRelayerClick = () => analytics.logEvent('Portal', 'Relayer Click', eventLabel); const onClick = () => {
analytics.logEvent('Portal', 'Relayer Click', eventLabel);
utils.openUrl(link);
};
const headerImageUrl = props.relayerInfo.logoImgUrl; const headerImageUrl = props.relayerInfo.logoImgUrl;
const headerBackgroundColor = const headerBackgroundColor =
!_.isUndefined(headerImageUrl) && !_.isUndefined(props.relayerInfo.primaryColor) !_.isUndefined(headerImageUrl) && !_.isUndefined(props.relayerInfo.primaryColor)
@ -77,22 +76,17 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (
: FALLBACK_PRIMARY_COLOR; : FALLBACK_PRIMARY_COLOR;
return ( return (
<Island style={styles.root} Component={GridTile}> <Island style={styles.root} Component={GridTile}>
<div style={styles.innerDiv}> <div style={styles.innerDiv} onClick={onClick}>
<a href={link} target="_blank" style={{ textDecoration: 'none' }} onClick={trackRelayerClick}> <div className="flex items-center" style={{ ...styles.header, backgroundColor: headerBackgroundColor }}>
<div <Image
className="flex items-center" className="mx-auto"
style={{ ...styles.header, backgroundColor: headerBackgroundColor }} src={props.relayerInfo.logoImgUrl}
> fallbackSrc={FALLBACK_IMG_SRC}
<Image height={RELAYER_ICON_HEIGHT}
className="mx-auto" />
src={props.relayerInfo.logoImgUrl} </div>
fallbackSrc={FALLBACK_IMG_SRC}
height={RELAYER_ICON_HEIGHT}
/>
</div>
</a>
<div style={styles.body}> <div style={styles.body}>
<div className="py1" style={styles.relayerNameLabel}> <div className="pb1" style={styles.relayerNameLabel}>
{props.relayerInfo.name} {props.relayerInfo.name}
</div> </div>
<Section titleText="Weekly Trade Volume"> <Section titleText="Weekly Trade Volume">
@ -111,6 +105,14 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (
); );
}; };
const GridTile = styled(PlainGridTile)`
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: translate(0px, -3px);
}
`;
interface SectionProps { interface SectionProps {
titleText: string; titleText: string;
children?: React.ReactNode; children?: React.ReactNode;

View File

@ -1,4 +1,3 @@
import { Styles } from '@0xproject/react-shared';
import * as _ from 'lodash'; import * as _ from 'lodash';
import CircularProgress from 'material-ui/CircularProgress'; import CircularProgress from 'material-ui/CircularProgress';
import { GridList } from 'material-ui/GridList'; import { GridList } from 'material-ui/GridList';
@ -6,7 +5,6 @@ import * as React from 'react';
import { RelayerGridTile } from 'ts/components/relayer_index/relayer_grid_tile'; import { RelayerGridTile } from 'ts/components/relayer_index/relayer_grid_tile';
import { Retry } from 'ts/components/ui/retry'; import { Retry } from 'ts/components/ui/retry';
import { colors } from 'ts/style/colors';
import { ScreenWidths, WebsiteBackendRelayerInfo } from 'ts/types'; import { ScreenWidths, WebsiteBackendRelayerInfo } from 'ts/types';
import { backendClient } from 'ts/utils/backend_client'; import { backendClient } from 'ts/utils/backend_client';
@ -20,22 +18,6 @@ interface RelayerIndexState {
error?: Error; error?: Error;
} }
const styles: Styles = {
root: {
width: '100%',
},
item: {
backgroundColor: colors.white,
borderBottomRightRadius: 10,
borderBottomLeftRadius: 10,
borderTopRightRadius: 10,
borderTopLeftRadius: 10,
boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`,
overflow: 'hidden',
padding: 4,
},
};
const CELL_HEIGHT = 290; const CELL_HEIGHT = 290;
const NUMBER_OF_COLUMNS_LARGE = 3; const NUMBER_OF_COLUMNS_LARGE = 3;
const NUMBER_OF_COLUMNS_MEDIUM = 2; const NUMBER_OF_COLUMNS_MEDIUM = 2;
@ -76,18 +58,16 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde
} else { } else {
const numberOfColumns = this._numberOfColumnsForScreenWidth(this.props.screenWidth); const numberOfColumns = this._numberOfColumnsForScreenWidth(this.props.screenWidth);
return ( return (
<div style={styles.root}> <GridList
<GridList cellHeight={CELL_HEIGHT}
cellHeight={CELL_HEIGHT} cols={numberOfColumns}
cols={numberOfColumns} padding={GRID_PADDING}
padding={GRID_PADDING} style={{ marginTop: -10, marginBottom: 0 }}
style={styles.gridList} >
> {this.state.relayerInfos.map((relayerInfo: WebsiteBackendRelayerInfo, index) => (
{this.state.relayerInfos.map((relayerInfo: WebsiteBackendRelayerInfo, index) => ( <RelayerGridTile key={index} relayerInfo={relayerInfo} networkId={this.props.networkId} />
<RelayerGridTile key={index} relayerInfo={relayerInfo} networkId={this.props.networkId} /> ))}
))} </GridList>
</GridList>
</div>
); );
} }
} }

View File

@ -70,7 +70,10 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> {
}; };
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
const eventLabel = `${this.props.tokenInfo.symbol}-${networkName}`; const eventLabel = `${this.props.tokenInfo.symbol}-${networkName}`;
const trackTokenClick = () => analytics.logEvent('Portal', 'Token Click', eventLabel); const onClick = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();
analytics.logEvent('Portal', 'Token Click', eventLabel);
};
return ( return (
<a <a
href={tokenLinkFromToken(this.props.tokenInfo, this.props.networkId)} href={tokenLinkFromToken(this.props.tokenInfo, this.props.networkId)}
@ -78,7 +81,7 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> {
style={style} style={style}
onMouseEnter={this._onToggleHover.bind(this, true)} onMouseEnter={this._onToggleHover.bind(this, true)}
onMouseLeave={this._onToggleHover.bind(this, false)} onMouseLeave={this._onToggleHover.bind(this, false)}
onClick={trackTokenClick} onClick={onClick}
> >
{this.props.tokenInfo.symbol} {this.props.tokenInfo.symbol}
</a> </a>

View File

@ -11,13 +11,15 @@ const PlainAnimation: React.StatelessComponent<AnimationProps> = props => <div {
const appearFromBottomFrames = keyframes` const appearFromBottomFrames = keyframes`
from { from {
position: absolute; position: fixed;
bottom: -500px; bottom: -500px;
left: 0px;
} }
to { to {
position: absolute; position: fixed;
bottom: 0px; bottom: 0px;
left: 0px;
} }
`; `;

View File

@ -15,6 +15,7 @@ export interface ContainerProps {
borderRadius?: StringOrNum; borderRadius?: StringOrNum;
maxWidth?: StringOrNum; maxWidth?: StringOrNum;
width?: StringOrNum; width?: StringOrNum;
minHeight?: StringOrNum;
isHidden?: boolean; isHidden?: boolean;
className?: string; className?: string;
position?: 'absolute' | 'fixed' | 'relative' | 'unset'; position?: 'absolute' | 'fixed' | 'relative' | 'unset';

View File

@ -29,6 +29,7 @@ import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
import { Dispatcher } from 'ts/redux/dispatcher'; import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors'; import { colors } from 'ts/style/colors';
import { styled } from 'ts/style/theme';
import { import {
BlockchainErrs, BlockchainErrs,
ProviderType, ProviderType,
@ -138,6 +139,12 @@ const USD_DECIMAL_PLACES = 2;
const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56; const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56;
const ACCOUNT_PATH = `${WebsitePaths.Portal}/account`; const ACCOUNT_PATH = `${WebsitePaths.Portal}/account`;
const ActionButton = styled(FloatingActionButton)`
button {
position: static !important;
}
`;
export class Wallet extends React.Component<WalletProps, WalletState> { export class Wallet extends React.Component<WalletProps, WalletState> {
public static defaultProps = { public static defaultProps = {
style: {}, style: {},
@ -244,17 +251,12 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
<ListItem <ListItem
primaryText={ primaryText={
<div className="flex"> <div className="flex">
<FloatingActionButton mini={true} zDepth={0} onClick={this.props.onAddToken}> <ActionButton mini={true} zDepth={0} onClick={this.props.onAddToken}>
<ContentAdd /> <ContentAdd />
</FloatingActionButton> </ActionButton>
<FloatingActionButton <ActionButton mini={true} zDepth={0} className="px1" onClick={this.props.onRemoveToken}>
mini={true}
zDepth={0}
className="px1"
onClick={this.props.onRemoveToken}
>
<ContentRemove /> <ContentRemove />
</FloatingActionButton> </ActionButton>
<div <div
style={{ style={{
paddingLeft: 10, paddingLeft: 10,
@ -309,7 +311,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
wrappedEtherDirection: Side.Deposit, wrappedEtherDirection: Side.Deposit,
}; };
const key = ETHER_ITEM_KEY; const key = ETHER_ITEM_KEY;
return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig, 'eth-row'); return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig, false, 'eth-row');
} }
private _renderTokenRows(): React.ReactNode { private _renderTokenRows(): React.ReactNode {
const trackedTokens = this.props.trackedTokens; const trackedTokens = this.props.trackedTokens;
@ -320,7 +322,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
); );
return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
} }
private _renderTokenRow(token: Token, _index: number): React.ReactNode { private _renderTokenRow(token: Token, index: number): React.ReactNode {
const tokenState = this.props.trackedTokenStateByAddress[token.address]; const tokenState = this.props.trackedTokenStateByAddress[token.address];
if (_.isUndefined(tokenState)) { if (_.isUndefined(tokenState)) {
return null; return null;
@ -348,12 +350,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
}, },
}; };
const key = token.address; const key = token.address;
const isLastRow = index === this.props.trackedTokens.length - 1;
return this._renderBalanceRow( return this._renderBalanceRow(
key, key,
icon, icon,
primaryText, primaryText,
secondaryText, secondaryText,
accessoryItemConfig, accessoryItemConfig,
isLastRow,
isWeth ? 'weth-row' : undefined, isWeth ? 'weth-row' : undefined,
); );
} }
@ -363,13 +367,19 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
primaryText: React.ReactNode, primaryText: React.ReactNode,
secondaryText: React.ReactNode, secondaryText: React.ReactNode,
accessoryItemConfig: AccessoryItemConfig, accessoryItemConfig: AccessoryItemConfig,
isLastRow: boolean,
className?: string, className?: string,
): React.ReactNode { ): React.ReactNode {
const shouldShowWrapEtherItem = const shouldShowWrapEtherItem =
!_.isUndefined(this.state.wrappedEtherDirection) && !_.isUndefined(this.state.wrappedEtherDirection) &&
this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection && this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection &&
!_.isUndefined(this.props.userEtherBalanceInWei); !_.isUndefined(this.props.userEtherBalanceInWei);
const additionalStyle = shouldShowWrapEtherItem ? walletItemStyles.focusedItem : styles.borderedItem; let additionalStyle;
if (shouldShowWrapEtherItem) {
additionalStyle = walletItemStyles.focusedItem;
} else if (!isLastRow) {
additionalStyle = styles.borderedItem;
}
const style = { ...styles.tokenItem, ...additionalStyle }; const style = { ...styles.tokenItem, ...additionalStyle };
const etherToken = this._getEthToken(); const etherToken = this._getEthToken();
return ( return (

View File

@ -156,26 +156,26 @@ const teamRow5: ProfileInfo[] = [
linkedIn: 'https://www.linkedin.com/in/fragosti/', linkedIn: 'https://www.linkedin.com/in/fragosti/',
github: 'http://github.com/fragosti', github: 'http://github.com/fragosti',
}, },
{
name: 'Chris Kalani',
title: 'Director of Design',
description: `Previously founded Wake (acquired by InVision). Early Facebook product designer.`,
image: 'images/team/chris.png',
linkedIn: 'https://www.linkedin.com/in/chriskalani/',
github: 'https://github.com/chriskalani',
},
];
const teamRow6: ProfileInfo[] = [
{ {
name: 'Mel Oberto', name: 'Mel Oberto',
title: 'Office Operations / Executive Assistant', title: 'Office Ops / Executive Assistant',
description: `Daily Operations. Previously People Operations Associate at Heap. Marketing and MBA at Sacred Heart University.`, description: `Daily Operations. Previously People Operations Associate at Heap. Marketing and MBA at Sacred Heart University.`,
image: 'images/team/mel.png', image: 'images/team/mel.png',
linkedIn: 'https://www.linkedin.com/in/melanieoberto', linkedIn: 'https://www.linkedin.com/in/melanieoberto',
}, },
]; ];
// const teamRow6: ProfileInfo[] = [
// {
// name: 'Chris Kalani',
// title: 'Director of Design',
// description: `Previously founded Wake (acquired by InVision). Early Facebook product designer.`,
// image: 'images/team/chris.png',
// linkedIn: 'https://www.linkedin.com/in/chriskalani/',
// github: 'https://github.com/chriskalani',
// },
// ];
const advisors: ProfileInfo[] = [ const advisors: ProfileInfo[] = [
{ {
name: 'Fred Ehrsam', name: 'Fred Ehrsam',
@ -259,9 +259,9 @@ export class About extends React.Component<AboutProps, AboutState> {
lineHeight: 1.5, lineHeight: 1.5,
}} }}
> >
Our team is a diverse and globally distributed group with backgrounds in engineering, Our team is a globally distributed group with backgrounds in engineering, research, business
research, business and design. We are passionate about decentralized technology and its and design. We are passionate about decentralized technology and its potential to act as an
potential to act as an equalizing force in the world. equalizing force in the world.
</div> </div>
</div> </div>
<div className="pt3 md-px4 lg-px0"> <div className="pt3 md-px4 lg-px0">
@ -270,7 +270,6 @@ export class About extends React.Component<AboutProps, AboutState> {
<div className="clearfix">{this._renderProfiles(teamRow3)}</div> <div className="clearfix">{this._renderProfiles(teamRow3)}</div>
<div className="clearfix">{this._renderProfiles(teamRow4)}</div> <div className="clearfix">{this._renderProfiles(teamRow4)}</div>
<div className="clearfix">{this._renderProfiles(teamRow5)}</div> <div className="clearfix">{this._renderProfiles(teamRow5)}</div>
<div className="clearfix">{this._renderProfiles(teamRow6)}</div>
</div> </div>
<div className="pt3 pb2"> <div className="pt3 pb2">
<div <div

View File

@ -39,6 +39,7 @@ export const Profile = (props: ProfileProps) => {
fontSize: 14, fontSize: 14,
fontFamily: 'Roboto Mono', fontFamily: 'Roboto Mono',
color: colors.darkGrey, color: colors.darkGrey,
whiteSpace: 'nowrap',
}} }}
> >
{props.profileInfo.title.toUpperCase()} {props.profileInfo.title.toUpperCase()}

View File

@ -11,6 +11,7 @@ import { colors } from 'ts/style/colors';
import { styled } from 'ts/style/theme'; import { styled } from 'ts/style/theme';
import { ScreenWidths, WebsiteBackendJobInfo } from 'ts/types'; import { ScreenWidths, WebsiteBackendJobInfo } from 'ts/types';
import { backendClient } from 'ts/utils/backend_client'; import { backendClient } from 'ts/utils/backend_client';
import { utils } from 'ts/utils/utils';
const labelStyle = { fontFamily: 'Roboto Mono', fontSize: 18 }; const labelStyle = { fontFamily: 'Roboto Mono', fontSize: 18 };
const HEADER_TEXT = 'Open Positions'; const HEADER_TEXT = 'Open Positions';
@ -161,7 +162,7 @@ export class OpenPositions extends React.Component<OpenPositionsProps, OpenPosit
private _openJobInfoUrl(jobInfo: WebsiteBackendJobInfo): void { private _openJobInfoUrl(jobInfo: WebsiteBackendJobInfo): void {
const url = jobInfo.url; const url = jobInfo.url;
window.open(url, '_blank'); utils.openUrl(url);
} }
} }

View File

@ -156,7 +156,7 @@ export function reducer(state: State = INITIAL_STATE, action: Action): State {
} }
case ActionTypes.AddTokenToTokenByAddress: { case ActionTypes.AddTokenToTokenByAddress: {
const newTokenByAddress = state.tokenByAddress; const newTokenByAddress = { ...state.tokenByAddress };
newTokenByAddress[action.data.address] = action.data; newTokenByAddress[action.data.address] = action.data;
return { return {
...state, ...state,
@ -165,7 +165,7 @@ export function reducer(state: State = INITIAL_STATE, action: Action): State {
} }
case ActionTypes.RemoveTokenFromTokenByAddress: { case ActionTypes.RemoveTokenFromTokenByAddress: {
const newTokenByAddress = state.tokenByAddress; const newTokenByAddress = { ...state.tokenByAddress };
delete newTokenByAddress[action.data.address]; delete newTokenByAddress[action.data.address];
return { return {
...state, ...state,
@ -174,7 +174,7 @@ export function reducer(state: State = INITIAL_STATE, action: Action): State {
} }
case ActionTypes.UpdateTokenByAddress: { case ActionTypes.UpdateTokenByAddress: {
const tokenByAddress = state.tokenByAddress; const tokenByAddress = { ...state.tokenByAddress };
const tokens = action.data; const tokens = action.data;
_.each(tokens, token => { _.each(tokens, token => {
const updatedToken = { const updatedToken = {
@ -253,7 +253,7 @@ export function reducer(state: State = INITIAL_STATE, action: Action): State {
} }
case ActionTypes.UpdateChosenAssetTokenAddress: { case ActionTypes.UpdateChosenAssetTokenAddress: {
const newAssetToken = state.sideToAssetToken[action.data.side]; const newAssetToken = { ...state.sideToAssetToken[action.data.side] };
newAssetToken.address = action.data.address; newAssetToken.address = action.data.address;
const newSideToAssetToken = { const newSideToAssetToken = {
...state.sideToAssetToken, ...state.sideToAssetToken,

View File

@ -1,5 +1,6 @@
export const zIndex = { export const zIndex = {
topBar: 1100, topBar: 1100,
aboveTopBar: 1101,
overlay: 1105, overlay: 1105,
aboveOverlay: 1106, aboveOverlay: 1106,
}; };

View File

@ -362,4 +362,10 @@ export const utils = {
const formattedAmount = unitAmount.toFixed(precision); const formattedAmount = unitAmount.toFixed(precision);
return `${formattedAmount} ${symbol}`; return `${formattedAmount} ${symbol}`;
}, },
openUrl(url: string): void {
window.open(url, '_blank');
},
isMobile(screenWidth: ScreenWidths): boolean {
return screenWidth === ScreenWidths.Sm;
},
}; };