Merge branch 'v2-prototype' into feature/combinatorial-testing

* v2-prototype: (22 commits)
  Fix closing parens in liborder
  Update after rebase
  ERC721Proxy Always call safeTransferFrom
  Rename makerEpoch => orderEpoch
  Make cancelOrdersUpTo compatible with sender abstraction
  Update PR template
  Use Image component instead of img tag
  Assembler orderHash function
  Optimize and remove redundant encodePacked
  Fix linting issue
  Fix bug where we do fetch balances on wallet login
  Check network state immediately instead of waiting for delay
  Fix onboarding persisting when changing routes
  Consolidate account state messaging logic
  Only elevate wallet zIndex when onboarding is in progress
  Rebase and update feedback
  Run linter
  Add Portal v2 logging
  Simplified handling of source < 32 edge case
  Basic EIP712 encoder
  ...
This commit is contained in:
Fabio Berger
2018-06-20 13:25:29 +02:00
48 changed files with 1167 additions and 366 deletions

View File

@@ -1,24 +1,10 @@
<!--- Thank you for taking the time to submit a Pull Request -->
<!--- Provide a general summary of the issue in the Title above -->
## Description
<!--- Describe your changes in detail -->
## Motivation and Context
## Testing instructions
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
<!--- Please describe how reviewers can test your changes -->
## Types of changes

View File

@@ -26,6 +26,7 @@
"ERC20Proxy",
"ERC721Proxy",
"Exchange",
"ExchangeWrapper",
"MixinAuthorizable",
"MultiSigWallet",
"MultiSigWalletWithTimeLock",

View File

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

View File

@@ -53,13 +53,7 @@ contract MixinERC721Transfer is
bytes memory receiverData
) = decodeERC721AssetData(assetData);
// Transfer token. Saves gas by calling safeTransferFrom only
// when there is receiverData present. Either succeeds or throws.
if (receiverData.length > 0) {
ERC721Token(token).safeTransferFrom(from, to, tokenId, receiverData);
} else {
ERC721Token(token).transferFrom(from, to, tokenId);
}
ERC721Token(token).safeTransferFrom(from, to, tokenId, receiverData);
}
/// @dev Decodes ERC721 Asset data.

View File

@@ -44,32 +44,36 @@ contract MixinExchangeCore is
// Mapping of orderHash => cancelled
mapping (bytes32 => bool) public cancelled;
// Mapping of makerAddress => lowest salt an order can have in order to be fillable
// Orders with a salt less than their maker's epoch are considered cancelled
mapping (address => uint256) public makerEpoch;
// Mapping of makerAddress => senderAddress => lowest salt an order can have in order to be fillable
// Orders with specified senderAddress and with a salt less than their epoch to are considered cancelled
mapping (address => mapping (address => uint256)) public orderEpoch;
////// Core exchange functions //////
/// @dev Cancels all orders created by sender with a salt less than or equal to the specified salt value.
/// @param salt Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 salt)
/// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
/// and senderAddress equal to msg.sender (or null address if msg.sender == makerAddress).
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 targetOrderEpoch)
external
{
address makerAddress = getCurrentContextAddress();
// If this function is called via `executeTransaction`, we only update the orderEpoch for the makerAddress/msg.sender combination.
// This allows external filter contracts to add rules to how orders are cancelled via this function.
address senderAddress = makerAddress == msg.sender ? address(0) : msg.sender;
// makerEpoch is initialized to 0, so to cancelUpTo we need salt + 1
uint256 newMakerEpoch = salt + 1;
uint256 oldMakerEpoch = makerEpoch[makerAddress];
// orderEpoch is initialized to 0, so to cancelUpTo we need salt + 1
uint256 newOrderEpoch = targetOrderEpoch + 1;
uint256 oldOrderEpoch = orderEpoch[makerAddress][senderAddress];
// Ensure makerEpoch is monotonically increasing
// Ensure orderEpoch is monotonically increasing
require(
newMakerEpoch > oldMakerEpoch,
INVALID_NEW_MAKER_EPOCH
newOrderEpoch > oldOrderEpoch,
INVALID_NEW_ORDER_EPOCH
);
// Update makerEpoch
makerEpoch[makerAddress] = newMakerEpoch;
emit CancelUpTo(makerAddress, newMakerEpoch);
// Update orderEpoch
orderEpoch[makerAddress][senderAddress] = newOrderEpoch;
emit CancelUpTo(makerAddress, senderAddress, newOrderEpoch);
}
/// @dev Fills the input order.
@@ -180,7 +184,7 @@ contract MixinExchangeCore is
orderInfo.orderStatus = uint8(OrderStatus.CANCELLED);
return orderInfo;
}
if (makerEpoch[order.makerAddress] > order.salt) {
if (orderEpoch[order.makerAddress][order.senderAddress] > order.salt) {
orderInfo.orderStatus = uint8(OrderStatus.CANCELLED);
return orderInfo;
}

View File

@@ -20,8 +20,11 @@ pragma solidity ^0.4.24;
import "./libs/LibExchangeErrors.sol";
import "./mixins/MSignatureValidator.sol";
import "./mixins/MTransactions.sol";
import "./libs/LibExchangeErrors.sol";
import "./libs/LibEIP712.sol";
contract MixinTransactions is
LibEIP712,
LibExchangeErrors,
MSignatureValidator,
MTransactions
@@ -34,6 +37,33 @@ contract MixinTransactions is
// Address of current transaction signer
address public currentContextAddress;
// Hash for the EIP712 ZeroEx Transaction Schema
bytes32 constant EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH = keccak256(abi.encodePacked(
"ZeroExTransaction(",
"uint256 salt,",
"address signer,",
"bytes data",
")"
));
/// @dev Calculates EIP712 hash of the Transaction.
/// @param salt Arbitrary number to ensure uniqueness of transaction hash.
/// @param signer Address of transaction signer.
/// @param data AbiV2 encoded calldata.
/// @return EIP712 hash of the Transaction.
function hashZeroExTransaction(uint256 salt, address signer, bytes data)
internal
pure
returns (bytes32)
{
return keccak256(abi.encode(
EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH,
salt,
signer,
keccak256(data)
));
}
/// @dev Executes an exchange method call in the context of signer.
/// @param salt Arbitrary number to ensure uniqueness of transaction hash.
/// @param signer Address of transaction signer.
@@ -53,13 +83,7 @@ contract MixinTransactions is
REENTRANCY_ILLEGAL
);
// Calculate transaction hash
bytes32 transactionHash = keccak256(abi.encodePacked(
address(this),
signer,
salt,
data
));
bytes32 transactionHash = hashEIP712Message(hashZeroExTransaction(salt, signer, data));
// Validate transaction has not been executed
require(

View File

@@ -24,9 +24,10 @@ import "../libs/LibFillResults.sol";
contract IExchangeCore {
/// @dev Cancels all orders reated by sender with a salt less than or equal to the specified salt value.
/// @param salt Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 salt)
/// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
/// and senderAddress equal to msg.sender (or null address if msg.sender == makerAddress).
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 targetOrderEpoch)
external;
/// @dev Fills the input order.

View File

@@ -0,0 +1,64 @@
/*
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 LibEIP712 {
// EIP191 header for EIP712 prefix
string constant EIP191_HEADER = "\x19\x01";
// EIP712 Domain Name value
string constant EIP712_DOMAIN_NAME = "0x Protocol";
// EIP712 Domain Version value
string constant EIP712_DOMAIN_VERSION = "2";
// Hash of the EIP712 Domain Separator Schema
bytes32 public constant EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked(
"EIP712Domain(",
"string name,",
"string version,",
"address verifyingContract",
")"
));
// Hash of the EIP712 Domain Separator data
bytes32 public EIP712_DOMAIN_HASH;
constructor ()
public
{
EIP712_DOMAIN_HASH = keccak256(abi.encode(
EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH,
keccak256(bytes(EIP712_DOMAIN_NAME)),
keccak256(bytes(EIP712_DOMAIN_VERSION)),
address(this)
));
}
/// @dev Calculates EIP712 encoding for a hash struct in this EIP712 Domain.
/// @param hashStruct The EIP712 hash struct.
/// @return EIP712 hash applied to this EIP712 Domain.
function hashEIP712Message(bytes32 hashStruct)
internal
view
returns (bytes32)
{
return keccak256(abi.encodePacked(EIP191_HEADER, EIP712_DOMAIN_HASH, hashStruct));
}
}

View File

@@ -36,7 +36,7 @@ contract LibExchangeErrors {
string constant SIGNATURE_UNSUPPORTED = "SIGNATURE_UNSUPPORTED"; // Signature type unsupported.
/// cancelOrdersUptTo errors ///
string constant INVALID_NEW_MAKER_EPOCH = "INVALID_NEW_MAKER_EPOCH"; // Specified salt must be greater than or equal to existing makerEpoch.
string constant INVALID_NEW_ORDER_EPOCH = "INVALID_NEW_ORDER_EPOCH"; // Specified salt must be greater than or equal to existing orderEpoch.
/// fillOrKillOrder errors ///
string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired takerAssetFillAmount could not be completely filled.
@@ -55,7 +55,7 @@ contract LibExchangeErrors {
string constant ASSET_PROXY_ID_MISMATCH = "ASSET_PROXY_ID_MISMATCH"; // newAssetProxyId does not match given assetProxyId.
/// Length validation errors ///
string constant LENGTH_GREATER_THAN_0_REQUIRED = "LENGTH_GREATER_THAN_0_REQUIRED"; // Byte array must have a length greater than 0.
string constant LENGTH_GREATER_THAN_0_REQUIRED = "LENGTH_GREATER_THAN_0_REQUIRED"; // Byte array must have a length greater than 0.
string constant LENGTH_0_REQUIRED = "LENGTH_1_REQUIRED"; // Byte array must have a length of 1.
string constant LENGTH_65_REQUIRED = "LENGTH_66_REQUIRED"; // Byte array must have a length of 66.
}

View File

@@ -18,13 +18,14 @@
pragma solidity ^0.4.24;
contract LibOrder {
import "./LibEIP712.sol";
bytes32 constant DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked(
"DomainSeparator(address contract)"
));
contract LibOrder is
LibEIP712
{
bytes32 constant ORDER_SCHEMA_HASH = keccak256(abi.encodePacked(
// Hash for the EIP712 Order Schema
bytes32 constant EIP712_ORDER_SCHEMA_HASH = keccak256(abi.encodePacked(
"Order(",
"address makerAddress,",
"address takerAddress,",
@@ -37,7 +38,7 @@ contract LibOrder {
"uint256 expirationTimeSeconds,",
"uint256 salt,",
"bytes makerAssetData,",
"bytes takerAssetData,",
"bytes takerAssetData",
")"
));
@@ -85,27 +86,38 @@ contract LibOrder {
view
returns (bytes32 orderHash)
{
// TODO: EIP712 is not finalized yet
// Source: https://github.com/ethereum/EIPs/pull/712
orderHash = keccak256(abi.encodePacked(
DOMAIN_SEPARATOR_SCHEMA_HASH,
keccak256(abi.encodePacked(address(this))),
ORDER_SCHEMA_HASH,
keccak256(abi.encodePacked(
order.makerAddress,
order.takerAddress,
order.feeRecipientAddress,
order.senderAddress,
order.makerAssetAmount,
order.takerAssetAmount,
order.makerFee,
order.takerFee,
order.expirationTimeSeconds,
order.salt,
keccak256(abi.encodePacked(order.makerAssetData)),
keccak256(abi.encodePacked(order.takerAssetData))
))
));
orderHash = hashEIP712Message(hashOrder(order));
return orderHash;
}
/// @dev Calculates EIP712 hash of the order.
/// @param order The order structure.
/// @return EIP712 hash of the order.
function hashOrder(Order memory order)
internal
pure
returns (bytes32 result)
{
bytes32 schemaHash = EIP712_ORDER_SCHEMA_HASH;
bytes32 makerAssetDataHash = keccak256(order.makerAssetData);
bytes32 takerAssetDataHash = keccak256(order.takerAssetData);
assembly {
// Backup
let temp1 := mload(sub(order, 32))
let temp2 := mload(add(order, 320))
let temp3 := mload(add(order, 352))
// Hash in place
mstore(sub(order, 32), schemaHash)
mstore(add(order, 320), makerAssetDataHash)
mstore(add(order, 352), takerAssetDataHash)
result := keccak256(sub(order, 32), 416)
// Restore
mstore(sub(order, 32), temp1)
mstore(add(order, 320), temp2)
mstore(add(order, 352), temp3)
}
return result;
}
}

View File

@@ -52,7 +52,8 @@ contract MExchangeCore is
// CancelUpTo event is emitted whenever `cancelOrdersUpTo` is executed succesfully.
event CancelUpTo(
address indexed makerAddress,
uint256 makerEpoch
address indexed senderAddress,
uint256 orderEpoch
);
/// @dev Updates state with results of a fill order.

View File

@@ -0,0 +1,98 @@
/*
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;
pragma experimental ABIEncoderV2;
import "../../protocol/Exchange/interfaces/IExchange.sol";
import "../../protocol/Exchange/libs/LibOrder.sol";
contract ExchangeWrapper {
// Exchange contract.
IExchange EXCHANGE;
constructor (address _exchange)
public
{
EXCHANGE = IExchange(_exchange);
}
/// @dev Fills an order using `msg.sender` as the taker.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
/// @param orderSignature Proof that order has been created by maker.
/// @param takerSignature Proof that taker wishes to call this function with given params.
function fillOrder(
LibOrder.Order memory order,
uint256 takerAssetFillAmount,
uint256 salt,
bytes memory orderSignature,
bytes memory takerSignature
)
public
{
address takerAddress = msg.sender;
// Encode arguments into byte array.
bytes memory data = abi.encodeWithSelector(
EXCHANGE.fillOrder.selector,
order,
takerAssetFillAmount,
orderSignature
);
// Call `fillOrder` via `executeTransaction`.
EXCHANGE.executeTransaction(
salt,
takerAddress,
data,
takerSignature
);
}
/// @dev Cancels all orders created by sender with a salt less than or equal to the targetOrderEpoch
/// and senderAddress equal to this contract.
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
/// @param makerSignature Proof that maker wishes to call this function with given params.
function cancelOrdersUpTo(
uint256 targetOrderEpoch,
uint256 salt,
bytes makerSignature
)
external
{
address makerAddress = msg.sender;
// Encode arguments into byte array.
bytes memory data = abi.encodeWithSelector(
EXCHANGE.cancelOrdersUpTo.selector,
targetOrderEpoch
);
// Call `cancelOrdersUpTo` via `executeTransaction`.
EXCHANGE.executeTransaction(
salt,
makerAddress,
data,
makerSignature
);
}
}

View File

@@ -74,7 +74,7 @@ contract TestLibs is
pure
returns (bytes32)
{
return ORDER_SCHEMA_HASH;
return EIP712_ORDER_SCHEMA_HASH;
}
function getDomainSeparatorSchemaHash()
@@ -82,7 +82,7 @@ contract TestLibs is
pure
returns (bytes32)
{
return DOMAIN_SEPARATOR_SCHEMA_HASH;
return EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH;
}
function publicAddFillResults(FillResults memory totalFillResults, FillResults memory singleFillResults)

View File

@@ -16,7 +16,7 @@
*/
pragma solidity ^0.4.23;
pragma solidity ^0.4.24;
pragma experimental ABIEncoderV2;
import "../../protocol/Exchange/interfaces/IExchange.sol";

View File

@@ -80,9 +80,6 @@ contract LibMem
//
if (source > dest) {
assembly {
// Record the total number of full words to copy
let nWords := div(length, 32)
// 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
@@ -98,20 +95,19 @@ contract LibMem
let last := mload(sEnd)
// Copy whole words front to back
for {let i := 0} lt(i, nWords) {i := add(i, 1)} {
// 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 {
// Record the total number of full words to copy
let nWords := div(length, 32)
// We subtract 32 from `sEnd` and `dEnd` because those
// are the starting points when copying a word at the end.
length := sub(length, 32)
@@ -125,12 +121,18 @@ contract LibMem
let first := mload(source)
// Copy whole words back to front
for {let i := 0} lt(i, nWords) {i := add(i, 1)} {
// 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

@@ -7,6 +7,7 @@ import * as DummyERC721Token from '../artifacts/DummyERC721Token.json';
import * as ERC20Proxy from '../artifacts/ERC20Proxy.json';
import * as ERC721Proxy from '../artifacts/ERC721Proxy.json';
import * as Exchange from '../artifacts/Exchange.json';
import * as ExchangeWrapper from '../artifacts/ExchangeWrapper.json';
import * as MixinAuthorizable from '../artifacts/MixinAuthorizable.json';
import * as MultiSigWallet from '../artifacts/MultiSigWallet.json';
import * as MultiSigWalletWithTimeLock from '../artifacts/MultiSigWalletWithTimeLock.json';
@@ -29,6 +30,7 @@ export const artifacts = {
ERC20Proxy: (ERC20Proxy as any) as ContractArtifact,
ERC721Proxy: (ERC721Proxy as any) as ContractArtifact,
Exchange: (Exchange as any) as ContractArtifact,
ExchangeWrapper: (ExchangeWrapper as any) as ContractArtifact,
EtherToken: (EtherToken as any) as ContractArtifact,
MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact,
MultiSigWallet: (MultiSigWallet as any) as ContractArtifact,

View File

@@ -26,7 +26,7 @@ export class OrderFactory {
...this._defaultOrderParams,
...customOrderParams,
} as any) as Order;
const orderHashBuff = orderHashUtils.getOrderHashBuff(order);
const orderHashBuff = orderHashUtils.getOrderHashBuffer(order);
const signature = signingUtils.signMessage(orderHashBuff, this._privateKey, signatureType);
const signedOrder = {
...order,

View File

@@ -1,10 +1,19 @@
import { crypto, generatePseudoRandomSalt } from '@0xproject/order-utils';
import { EIP712Schema, EIP712Types, EIP712Utils, generatePseudoRandomSalt } from '@0xproject/order-utils';
import { SignatureType } from '@0xproject/types';
import * as ethUtil from 'ethereumjs-util';
import { signingUtils } from './signing_utils';
import { SignedTransaction } from './types';
const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = {
name: 'ZeroExTransaction',
parameters: [
{ name: 'salt', type: EIP712Types.Uint256 },
{ name: 'signer', type: EIP712Types.Address },
{ name: 'data', type: EIP712Types.Bytes },
],
};
export class TransactionFactory {
private _signerBuff: Buffer;
private _exchangeAddress: string;
@@ -16,14 +25,22 @@ export class TransactionFactory {
}
public newSignedTransaction(data: string, signatureType: SignatureType = SignatureType.EthSign): SignedTransaction {
const salt = generatePseudoRandomSalt();
const txHash = crypto.solSHA3([this._exchangeAddress, this._signerBuff, salt, ethUtil.toBuffer(data)]);
const signer = `0x${this._signerBuff.toString('hex')}`;
const executeTransactionData = {
salt,
signer,
data,
};
const executeTransactionHashBuff = EIP712Utils.structHash(
EIP712_ZEROEX_TRANSACTION_SCHEMA,
executeTransactionData,
);
const txHash = EIP712Utils.createEIP712Message(executeTransactionHashBuff, this._exchangeAddress);
const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType);
const signedTx = {
exchangeAddress: this._exchangeAddress,
salt,
signer: `0x${this._signerBuff.toString('hex')}`,
data,
signature: `0x${signature.toString('hex')}`,
...executeTransactionData,
};
return signedTx;
}

View File

@@ -279,7 +279,7 @@ describe('Asset Transfer Proxies', () => {
expect(newOwnerMakerAsset).to.be.bignumber.equal(takerAddress);
});
it('should not call onERC721Received when transferring to a smart contract without receiver data', async () => {
it('should call onERC721Received when transferring to a smart contract without receiver data', async () => {
// Construct ERC721 asset data
const encodedAssetData = assetProxyUtils.encodeERC721AssetData(erc721Token.address, erc721MakerTokenId);
const encodedAssetDataWithoutProxyId = encodedAssetData.slice(0, -2);
@@ -300,7 +300,11 @@ describe('Asset Transfer Proxies', () => {
const logDecoder = new LogDecoder(web3Wrapper, erc721Receiver.address);
const tx = await logDecoder.getTxWithDecodedLogsAsync(txHash);
// Verify that no log was emitted by erc721 receiver
expect(tx.logs.length).to.be.equal(0);
expect(tx.logs.length).to.be.equal(1);
const tokenReceivedLog = tx.logs[0] as LogWithDecodedArgs<TokenReceivedContractEventArgs>;
expect(tokenReceivedLog.args.from).to.be.equal(makerAddress);
expect(tokenReceivedLog.args.tokenId).to.be.bignumber.equal(erc721MakerTokenId);
expect(tokenReceivedLog.args.data).to.be.equal(constants.NULL_BYTES);
// Verify transfer was successful
const newOwnerMakerAsset = await erc721Token.ownerOf.callAsync(erc721MakerTokenId);
expect(newOwnerMakerAsset).to.be.bignumber.equal(erc721Receiver.address);

View File

@@ -252,30 +252,30 @@ describe('Exchange core', () => {
});
describe('cancelOrdersUpTo', () => {
it('should fail to set makerEpoch less than current makerEpoch', async () => {
const makerEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
const lesserMakerEpoch = new BigNumber(0);
it('should fail to set orderEpoch less than current orderEpoch', async () => {
const orderEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
const lesserOrderEpoch = new BigNumber(0);
return expectRevertOrAlwaysFailingTransactionAsync(
exchangeWrapper.cancelOrdersUpToAsync(lesserMakerEpoch, makerAddress),
exchangeWrapper.cancelOrdersUpToAsync(lesserOrderEpoch, makerAddress),
);
});
it('should fail to set makerEpoch equal to existing makerEpoch', async () => {
const makerEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
it('should fail to set orderEpoch equal to existing orderEpoch', async () => {
const orderEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
return expectRevertOrAlwaysFailingTransactionAsync(
exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress),
exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress),
);
});
it('should cancel only orders with a makerEpoch less than existing makerEpoch', async () => {
// Cancel all transactions with a makerEpoch less than 1
const makerEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
it('should cancel only orders with a orderEpoch less than existing orderEpoch', async () => {
// Cancel all transactions with a orderEpoch less than 1
const orderEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
// Create 3 orders with makerEpoch values: 0,1,2,3
// Since we cancelled with makerEpoch=1, orders with makerEpoch<=1 will not be processed
// Create 3 orders with orderEpoch values: 0,1,2,3
// Since we cancelled with orderEpoch=1, orders with orderEpoch<=1 will not be processed
erc20Balances = await erc20Wrapper.getBalancesAsync();
const signedOrders = [
orderFactory.newSignedOrder({

View File

@@ -1,5 +1,5 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { assetProxyUtils, orderHashUtils } from '@0xproject/order-utils';
import { assetProxyUtils, EIP712Utils, orderHashUtils } from '@0xproject/order-utils';
import { SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
@@ -56,13 +56,17 @@ describe('Exchange libs', () => {
describe('getOrderSchema', () => {
it('should output the correct order schema hash', async () => {
const orderSchema = await libs.getOrderSchemaHash.callAsync();
expect(orderHashUtils._getOrderSchemaHex()).to.be.equal(orderSchema);
const schemaHashBuffer = orderHashUtils._getOrderSchemaBuffer();
const schemaHashHex = `0x${schemaHashBuffer.toString('hex')}`;
expect(schemaHashHex).to.be.equal(orderSchema);
});
});
describe('getDomainSeparatorSchema', () => {
it('should output the correct domain separator schema hash', async () => {
const domainSeparatorSchema = await libs.getDomainSeparatorSchemaHash.callAsync();
expect(orderHashUtils._getDomainSeparatorSchemaHex()).to.be.equal(domainSeparatorSchema);
const domainSchemaBuffer = EIP712Utils._getDomainSeparatorSchemaBuffer();
const schemaHashHex = `0x${domainSchemaBuffer.toString('hex')}`;
expect(schemaHashHex).to.be.equal(domainSeparatorSchema);
});
});
describe('getOrderHash', () => {

View File

@@ -7,6 +7,7 @@ import * as chai from 'chai';
import { DummyERC20TokenContract } from '../../src/generated_contract_wrappers/dummy_e_r_c20_token';
import { ERC20ProxyContract } from '../../src/generated_contract_wrappers/e_r_c20_proxy';
import { ExchangeContract } from '../../src/generated_contract_wrappers/exchange';
import { ExchangeWrapperContract } from '../../src/generated_contract_wrappers/exchange_wrapper';
import { WhitelistContract } from '../../src/generated_contract_wrappers/whitelist';
import { artifacts } from '../../src/utils/artifacts';
import { expectRevertOrAlwaysFailingTransactionAsync } from '../../src/utils/assertions';
@@ -201,6 +202,117 @@ describe('Exchange transactions', () => {
);
});
});
describe('cancelOrdersUpTo', () => {
let exchangeWrapperContract: ExchangeWrapperContract;
before(async () => {
exchangeWrapperContract = await ExchangeWrapperContract.deployFrom0xArtifactAsync(
artifacts.ExchangeWrapper,
provider,
txDefaults,
exchange.address,
);
});
it("should cancel an order if called from the order's sender", async () => {
const orderSalt = new BigNumber(0);
signedOrder = orderFactory.newSignedOrder({
senderAddress: exchangeWrapperContract.address,
salt: orderSalt,
});
const targetOrderEpoch = orderSalt.add(1);
const cancelData = exchange.cancelOrdersUpTo.getABIEncodedTransactionData(targetOrderEpoch);
const signedCancelTx = makerTransactionFactory.newSignedTransaction(cancelData);
await exchangeWrapperContract.cancelOrdersUpTo.sendTransactionAsync(
targetOrderEpoch,
signedCancelTx.salt,
signedCancelTx.signature,
{
from: makerAddress,
},
);
const takerAssetFillAmount = signedOrder.takerAssetAmount;
orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
const fillData = exchange.fillOrder.getABIEncodedTransactionData(
orderWithoutExchangeAddress,
takerAssetFillAmount,
signedOrder.signature,
);
const signedFillTx = takerTransactionFactory.newSignedTransaction(fillData);
return expectRevertOrAlwaysFailingTransactionAsync(
exchangeWrapperContract.fillOrder.sendTransactionAsync(
orderWithoutExchangeAddress,
takerAssetFillAmount,
signedFillTx.salt,
signedOrder.signature,
signedFillTx.signature,
{ from: takerAddress },
),
);
});
it("should not cancel an order if not called from the order's sender", async () => {
const orderSalt = new BigNumber(0);
signedOrder = orderFactory.newSignedOrder({
senderAddress: exchangeWrapperContract.address,
salt: orderSalt,
});
const targetOrderEpoch = orderSalt.add(1);
await exchangeWrapper.cancelOrdersUpToAsync(targetOrderEpoch, makerAddress);
erc20Balances = await erc20Wrapper.getBalancesAsync();
const takerAssetFillAmount = signedOrder.takerAssetAmount;
orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
const data = exchange.fillOrder.getABIEncodedTransactionData(
orderWithoutExchangeAddress,
takerAssetFillAmount,
signedOrder.signature,
);
signedTx = takerTransactionFactory.newSignedTransaction(data);
await exchangeWrapperContract.fillOrder.sendTransactionAsync(
orderWithoutExchangeAddress,
takerAssetFillAmount,
signedTx.salt,
signedOrder.signature,
signedTx.signature,
{ from: takerAddress },
);
const newBalances = await erc20Wrapper.getBalancesAsync();
const makerAssetFillAmount = takerAssetFillAmount
.times(signedOrder.makerAssetAmount)
.dividedToIntegerBy(signedOrder.takerAssetAmount);
const makerFeePaid = signedOrder.makerFee
.times(makerAssetFillAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
const takerFeePaid = signedOrder.takerFee
.times(makerAssetFillAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
);
expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
);
expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
);
expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
);
});
});
});
describe('Whitelist', () => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,93 @@
import ethUtil = require('ethereumjs-util');
import * as _ from 'lodash';
import { crypto } from './crypto';
import { EIP712Schema, EIP712Types } from './types';
const EIP191_PREFIX = '\x19\x01';
const EIP712_DOMAIN_NAME = '0x Protocol';
const EIP712_DOMAIN_VERSION = '2';
const EIP712_VALUE_LENGTH = 32;
const EIP712_DOMAIN_SCHEMA: EIP712Schema = {
name: 'EIP712Domain',
parameters: [
{ name: 'name', type: EIP712Types.String },
{ name: 'version', type: EIP712Types.String },
{ name: 'verifyingContract', type: EIP712Types.Address },
],
};
export const EIP712Utils = {
/**
* Compiles the EIP712Schema and returns the hash of the schema.
* @param schema The EIP712 schema.
* @return The hash of the compiled schema
*/
compileSchema(schema: EIP712Schema): Buffer {
const eip712Schema = EIP712Utils._encodeType(schema);
const eip712SchemaHashBuffer = crypto.solSHA3([eip712Schema]);
return eip712SchemaHashBuffer;
},
/**
* Merges the EIP712 hash of a struct with the DomainSeparator for 0x v2.
* @param hashStruct the EIP712 hash of a struct
* @param contractAddress the exchange contract address
* @return The hash of an EIP712 message with domain separator prefixed
*/
createEIP712Message(hashStruct: Buffer, contractAddress: string): Buffer {
const domainSeparatorHashBuffer = EIP712Utils._getDomainSeparatorHashBuffer(contractAddress);
const messageBuff = crypto.solSHA3([EIP191_PREFIX, domainSeparatorHashBuffer, hashStruct]);
return messageBuff;
},
pad32Address(address: string): Buffer {
const addressBuffer = ethUtil.toBuffer(address);
const addressPadded = EIP712Utils.pad32Buffer(addressBuffer);
return addressPadded;
},
pad32Buffer(buffer: Buffer): Buffer {
const bufferPadded = ethUtil.setLengthLeft(buffer, EIP712_VALUE_LENGTH);
return bufferPadded;
},
_getDomainSeparatorSchemaBuffer(): Buffer {
return EIP712Utils.compileSchema(EIP712_DOMAIN_SCHEMA);
},
_getDomainSeparatorHashBuffer(exchangeAddress: string): Buffer {
const domainSeparatorSchemaBuffer = EIP712Utils._getDomainSeparatorSchemaBuffer();
const encodedData = EIP712Utils._encodeData(EIP712_DOMAIN_SCHEMA, {
name: EIP712_DOMAIN_NAME,
version: EIP712_DOMAIN_VERSION,
verifyingContract: exchangeAddress,
});
const domainSeparatorHashBuff2 = crypto.solSHA3([domainSeparatorSchemaBuffer, ...encodedData]);
return domainSeparatorHashBuff2;
},
_encodeType(schema: EIP712Schema): string {
const namedTypes = _.map(schema.parameters, ({ name, type }) => `${type} ${name}`);
const namedTypesJoined = namedTypes.join(',');
const encodedType = `${schema.name}(${namedTypesJoined})`;
return encodedType;
},
_encodeData(schema: EIP712Schema, data: { [key: string]: any }): any {
const encodedValues = [];
for (const parameter of schema.parameters) {
const value = data[parameter.name];
if (parameter.type === EIP712Types.String || parameter.type === EIP712Types.Bytes) {
encodedValues.push(crypto.solSHA3([ethUtil.toBuffer(value)]));
} else if (parameter.type === EIP712Types.Uint256) {
encodedValues.push(value);
} else if (parameter.type === EIP712Types.Address) {
encodedValues.push(EIP712Utils.pad32Address(value));
} else {
throw new Error(`Unable to encode ${parameter.type}`);
}
}
return encodedValues;
},
structHash(schema: EIP712Schema, data: { [key: string]: any }): Buffer {
const encodedData = EIP712Utils._encodeData(schema, data);
const schemaHash = EIP712Utils.compileSchema(schema);
const hashBuffer = crypto.solSHA3([schemaHash, ...encodedData]);
return hashBuffer;
},
};

View File

@@ -13,12 +13,13 @@ export { orderFactory } from './order_factory';
export { constants } from './constants';
export { crypto } from './crypto';
export { generatePseudoRandomSalt } from './salt';
export { OrderError, MessagePrefixType, MessagePrefixOpts } from './types';
export { OrderError, MessagePrefixType, MessagePrefixOpts, EIP712Parameter, EIP712Schema, EIP712Types } from './types';
export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
export { RemainingFillableCalculator } from './remaining_fillable_calculator';
export { OrderStateUtils } from './order_state_utils';
export { assetProxyUtils } from './asset_proxy_utils';
export { EIP712Utils } from './eip712_utils';
export { OrderValidationUtils } from './order_validation_utils';
export { ExchangeTransferSimulator } from './exchange_transfer_simulator';

View File

@@ -1,14 +1,31 @@
import { schemas, SchemaValidator } from '@0xproject/json-schemas';
import { Order, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash';
import { assert } from './assert';
import { crypto } from './crypto';
import { EIP712Utils } from './eip712_utils';
import { EIP712Schema, EIP712Types } from './types';
const INVALID_TAKER_FORMAT = 'instance.takerAddress is not of a type(s) string';
const EIP712_ORDER_SCHEMA: EIP712Schema = {
name: 'Order',
parameters: [
{ name: 'makerAddress', type: EIP712Types.Address },
{ name: 'takerAddress', type: EIP712Types.Address },
{ name: 'feeRecipientAddress', type: EIP712Types.Address },
{ name: 'senderAddress', type: EIP712Types.Address },
{ name: 'makerAssetAmount', type: EIP712Types.Uint256 },
{ name: 'takerAssetAmount', type: EIP712Types.Uint256 },
{ name: 'makerFee', type: EIP712Types.Uint256 },
{ name: 'takerFee', type: EIP712Types.Uint256 },
{ name: 'expirationTimeSeconds', type: EIP712Types.Uint256 },
{ name: 'salt', type: EIP712Types.Uint256 },
{ name: 'makerAssetData', type: EIP712Types.Bytes },
{ name: 'takerAssetData', type: EIP712Types.Bytes },
],
};
export const orderHashUtils = {
/**
* Checks if the supplied hex encoded order hash is valid.
@@ -42,7 +59,7 @@ export const orderHashUtils = {
throw error;
}
const orderHashBuff = this.getOrderHashBuff(order);
const orderHashBuff = orderHashUtils.getOrderHashBuffer(order);
const orderHashHex = `0x${orderHashBuff.toString('hex')}`;
return orderHashHex;
},
@@ -51,64 +68,12 @@ export const orderHashUtils = {
* @param order An object that conforms to the Order or SignedOrder interface definitions.
* @return The resulting orderHash from hashing the supplied order as a Buffer
*/
getOrderHashBuff(order: SignedOrder | Order): Buffer {
const makerAssetDataHash = crypto.solSHA3([ethUtil.toBuffer(order.makerAssetData)]);
const takerAssetDataHash = crypto.solSHA3([ethUtil.toBuffer(order.takerAssetData)]);
const orderParamsHashBuff = crypto.solSHA3([
order.makerAddress,
order.takerAddress,
order.feeRecipientAddress,
order.senderAddress,
order.makerAssetAmount,
order.takerAssetAmount,
order.makerFee,
order.takerFee,
order.expirationTimeSeconds,
order.salt,
makerAssetDataHash,
takerAssetDataHash,
]);
const orderParamsHashHex = `0x${orderParamsHashBuff.toString('hex')}`;
const orderSchemaHashHex = this._getOrderSchemaHex();
const domainSeparatorHashHex = this._getDomainSeparatorHashHex(order.exchangeAddress);
const domainSeparatorSchemaHex = this._getDomainSeparatorSchemaHex();
const orderHashBuff = crypto.solSHA3([
new BigNumber(domainSeparatorSchemaHex),
new BigNumber(domainSeparatorHashHex),
new BigNumber(orderSchemaHashHex),
new BigNumber(orderParamsHashHex),
]);
getOrderHashBuffer(order: SignedOrder | Order): Buffer {
const orderParamsHashBuff = EIP712Utils.structHash(EIP712_ORDER_SCHEMA, order);
const orderHashBuff = EIP712Utils.createEIP712Message(orderParamsHashBuff, order.exchangeAddress);
return orderHashBuff;
},
_getOrderSchemaHex(): string {
const orderSchemaHashBuff = crypto.solSHA3([
'Order(',
'address makerAddress,',
'address takerAddress,',
'address feeRecipientAddress,',
'address senderAddress,',
'uint256 makerAssetAmount,',
'uint256 takerAssetAmount,',
'uint256 makerFee,',
'uint256 takerFee,',
'uint256 expirationTimeSeconds,',
'uint256 salt,',
'bytes makerAssetData,',
'bytes takerAssetData,',
')',
]);
const schemaHashHex = `0x${orderSchemaHashBuff.toString('hex')}`;
return schemaHashHex;
},
_getDomainSeparatorSchemaHex(): string {
const domainSeparatorSchemaHashBuff = crypto.solSHA3(['DomainSeparator(address contract)']);
const schemaHashHex = `0x${domainSeparatorSchemaHashBuff.toString('hex')}`;
return schemaHashHex;
},
_getDomainSeparatorHashHex(exchangeAddress: string): string {
const domainSeparatorHashBuff = crypto.solSHA3([exchangeAddress]);
const domainSeparatorHashHex = `0x${domainSeparatorHashBuff.toString('hex')}`;
return domainSeparatorHashHex;
_getOrderSchemaBuffer(): Buffer {
return EIP712Utils.compileSchema(EIP712_ORDER_SCHEMA);
},
};

View File

@@ -33,3 +33,21 @@ export enum TransferType {
Trade = 'trade',
Fee = 'fee',
}
export interface EIP712Parameter {
name: string;
type: EIP712Types;
}
export interface EIP712Schema {
name: string;
parameters: EIP712Parameter[];
}
export enum EIP712Types {
Address = 'address',
Bytes = 'bytes',
Bytes32 = 'bytes32',
String = 'string',
Uint256 = 'uint256',
}

View File

@@ -229,7 +229,7 @@ export class Blockchain {
shouldPollUserAddress,
);
this._contractWrappers.setProvider(provider, this.networkId);
this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceState();
await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync();
this._dispatcher.updateProviderType(ProviderType.Ledger);
}
public async updateProviderToInjectedAsync(): Promise<void> {
@@ -259,7 +259,7 @@ export class Blockchain {
this._contractWrappers.setProvider(provider, this.networkId);
await this.fetchTokenInformationAsync();
this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceState();
await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync();
this._dispatcher.updateProviderType(ProviderType.Injected);
delete this._ledgerSubprovider;
delete this._cachedProvider;
@@ -816,7 +816,7 @@ export class Blockchain {
this._userAddressIfExists = userAddresses[0];
this._dispatcher.updateUserAddress(this._userAddressIfExists);
await this.fetchTokenInformationAsync();
this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceState();
await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync();
await this._rehydrateStoreWithContractEventsAsync();
}
private _updateProviderName(injectedWeb3: Web3): void {

View File

@@ -34,56 +34,15 @@ export class BlockchainWatcher {
public updatePrevUserAddress(userAddress: string): void {
this._prevUserAddressIfExists = userAddress;
}
public startEmittingNetworkConnectionAndUserBalanceState(): void {
public async startEmittingNetworkConnectionAndUserBalanceStateAsync(): Promise<void> {
if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) {
return; // we are already emitting the state
}
let prevNodeVersion: string;
this._prevUserEtherBalanceInWei = undefined;
this._dispatcher.updateNetworkId(this._prevNetworkId);
await this._updateNetworkAndBalanceAsync();
this._watchNetworkAndBalanceIntervalId = intervalUtils.setAsyncExcludingInterval(
async () => {
// Check for network state changes
let currentNetworkId;
try {
currentNetworkId = await this._web3Wrapper.getNetworkIdAsync();
} catch (err) {
// Noop
}
if (currentNetworkId !== this._prevNetworkId) {
this._prevNetworkId = currentNetworkId;
this._dispatcher.updateNetworkId(currentNetworkId);
}
// Check for node version changes
const currentNodeVersion = await this._web3Wrapper.getNodeVersionAsync();
if (currentNodeVersion !== prevNodeVersion) {
prevNodeVersion = currentNodeVersion;
this._dispatcher.updateNodeVersion(currentNodeVersion);
}
if (this._shouldPollUserAddress) {
const addresses = await this._web3Wrapper.getAvailableAddressesAsync();
const userAddressIfExists = addresses[0];
// Update makerAddress on network change
if (this._prevUserAddressIfExists !== userAddressIfExists) {
this._prevUserAddressIfExists = userAddressIfExists;
this._dispatcher.updateUserAddress(userAddressIfExists);
}
// Check for user ether balance changes
if (!_.isUndefined(userAddressIfExists)) {
await this._updateUserWeiBalanceAsync(userAddressIfExists);
}
} else {
// This logic is primarily for the Ledger, since we don't regularly poll for the address
// we simply update the balance for the last fetched address.
if (!_.isUndefined(this._prevUserAddressIfExists)) {
await this._updateUserWeiBalanceAsync(this._prevUserAddressIfExists);
}
}
},
this._updateNetworkAndBalanceAsync.bind(this),
5000,
(err: Error) => {
logUtils.log(`Watching network and balances failed: ${err.stack}`);
@@ -91,6 +50,48 @@ export class BlockchainWatcher {
},
);
}
private async _updateNetworkAndBalanceAsync(): Promise<void> {
// Check for network state changes
let prevNodeVersion: string;
let currentNetworkId;
try {
currentNetworkId = await this._web3Wrapper.getNetworkIdAsync();
} catch (err) {
// Noop
}
if (currentNetworkId !== this._prevNetworkId) {
this._prevNetworkId = currentNetworkId;
this._dispatcher.updateNetworkId(currentNetworkId);
}
// Check for node version changes
const currentNodeVersion = await this._web3Wrapper.getNodeVersionAsync();
if (currentNodeVersion !== prevNodeVersion) {
prevNodeVersion = currentNodeVersion;
this._dispatcher.updateNodeVersion(currentNodeVersion);
}
if (this._shouldPollUserAddress) {
const addresses = await this._web3Wrapper.getAvailableAddressesAsync();
const userAddressIfExists = addresses[0];
// Update makerAddress on network change
if (this._prevUserAddressIfExists !== userAddressIfExists) {
this._prevUserAddressIfExists = userAddressIfExists;
this._dispatcher.updateUserAddress(userAddressIfExists);
}
// Check for user ether balance changes
if (!_.isUndefined(userAddressIfExists)) {
await this._updateUserWeiBalanceAsync(userAddressIfExists);
}
} else {
// This logic is primarily for the Ledger, since we don't regularly poll for the address
// we simply update the balance for the last fetched address.
if (!_.isUndefined(this._prevUserAddressIfExists)) {
await this._updateUserWeiBalanceAsync(this._prevUserAddressIfExists);
}
}
}
private async _updateUserWeiBalanceAsync(userAddress: string): Promise<void> {
const balanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(userAddress);
if (_.isUndefined(this._prevUserEtherBalanceInWei) || !balanceInWei.eq(this._prevUserEtherBalanceInWei)) {

View File

@@ -1,5 +1,7 @@
import { constants as sharedConstants } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { BigNumber } from '@0xproject/utils';
import { Blockchain } from 'ts/blockchain';
@@ -13,9 +15,11 @@ import { UnlockWalletOnboardingStep } from 'ts/components/onboarding/unlock_wall
import { WrapEthOnboardingStep } from 'ts/components/onboarding/wrap_eth_onboarding_step';
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
import { ProviderType, Token, TokenByAddress, TokenStateByAddress } from 'ts/types';
import { analytics } from 'ts/utils/analytics';
import { utils } from 'ts/utils/utils';
export interface PortalOnboardingFlowProps {
export interface PortalOnboardingFlowProps extends RouteComponentProps<any> {
networkId: number;
blockchain: Blockchain;
stepIndex: number;
isRunning: boolean;
@@ -32,9 +36,15 @@ export interface PortalOnboardingFlowProps {
refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
}
export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowProps> {
class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProps> {
private _unlisten: () => void;
public componentDidMount(): void {
this._overrideOnboardingStateIfShould();
// If there is a route change, just close onboarding.
this._unlisten = this.props.history.listen(() => this.props.updateIsRunning(false));
}
public componentWillUnmount(): void {
this._unlisten();
}
public componentDidUpdate(): void {
this._overrideOnboardingStateIfShould();
@@ -45,8 +55,8 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
steps={this._getSteps()}
stepIndex={this.props.stepIndex}
isRunning={this.props.isRunning}
onClose={this.props.updateIsRunning.bind(this, false)}
updateOnboardingStep={this.props.updateOnboardingStep}
onClose={this._closeOnboarding.bind(this)}
updateOnboardingStep={this._updateOnboardingStep.bind(this)}
/>
);
}
@@ -181,9 +191,21 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
}
private _autoStartOnboardingIfShould(): void {
if (!this.props.isRunning && !this.props.hasBeenSeen && this.props.blockchainIsLoaded) {
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
analytics.logEvent('Portal', 'Onboarding Started - Automatic', networkName, this.props.stepIndex);
this.props.updateIsRunning(true);
}
}
private _updateOnboardingStep(stepIndex: number): void {
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
this.props.updateOnboardingStep(stepIndex);
analytics.logEvent('Portal', 'Update Onboarding Step', networkName, stepIndex);
}
private _closeOnboarding(): void {
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
this.props.updateIsRunning(false);
analytics.logEvent('Portal', 'Onboarding Closed', networkName, this.props.stepIndex);
}
private _renderZrxAllowanceToggle(): React.ReactNode {
const zrxToken = utils.getZrxToken(this.props.tokenByAddress);
return this._renderAllowanceToggle(zrxToken);
@@ -209,3 +231,5 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr
);
}
}
export const PortalOnboardingFlow = withRouter(PlainPortalOnboardingFlow);

View File

@@ -2,10 +2,12 @@ import { Styles } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { defaultMenuItemEntries, Menu } from 'ts/components/portal/menu';
import { Identicon } from 'ts/components/ui/identicon';
import { Text } from 'ts/components/ui/text';
import { colors } from 'ts/style/colors';
import { WebsitePaths } from 'ts/types';
import { ProviderType, WebsitePaths } from 'ts/types';
import { utils } from 'ts/utils/utils';
const IDENTICON_DIAMETER = 45;
@@ -25,14 +27,15 @@ const styles: Styles = {
MozBorderRadius: BORDER_RADIUS,
WebkitBorderRadius: BORDER_RADIUS,
},
userAddress: {
color: colors.white,
},
};
export interface DrawerMenuProps {
selectedPath?: string;
userAddress?: string;
injectedProviderName: string;
providerType: ProviderType;
blockchain?: Blockchain;
blockchainIsLoaded: boolean;
}
export const DrawerMenu = (props: DrawerMenuProps) => {
const relayerItemEntry = {
@@ -41,9 +44,15 @@ export const DrawerMenu = (props: DrawerMenuProps) => {
iconName: 'zmdi-portable-wifi',
};
const menuItemEntries = _.concat(relayerItemEntry, defaultMenuItemEntries);
const displayMessage = utils.getReadableAccountState(
props.blockchainIsLoaded && !_.isUndefined(props.blockchain),
props.providerType,
props.injectedProviderName,
props.userAddress,
);
return (
<div style={styles.root}>
<Header userAddress={props.userAddress} />
<Header userAddress={props.userAddress} displayMessage={displayMessage} />
<Menu selectedPath={props.selectedPath} menuItemEntries={menuItemEntries} />
</div>
);
@@ -51,17 +60,16 @@ export const DrawerMenu = (props: DrawerMenuProps) => {
interface HeaderProps {
userAddress?: string;
displayMessage: string;
}
const Header = (props: HeaderProps) => {
return (
<div className="flex flex-center py4">
<div className="flex flex-column mx-auto">
<Identicon address={props.userAddress} diameter={IDENTICON_DIAMETER} style={styles.identicon} />
{!_.isUndefined(props.userAddress) && (
<div className="pt2" style={styles.userAddress}>
{utils.getAddressBeginAndEnd(props.userAddress)}
</div>
)}
<Text className="pt2" fontColor={colors.white}>
{props.displayMessage}
</Text>
</div>
</div>
);

View File

@@ -1,4 +1,4 @@
import { colors, Styles } from '@0xproject/react-shared';
import { colors, constants as sharedConstants, Styles } from '@0xproject/react-shared';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
@@ -33,6 +33,7 @@ import { localStorage } from 'ts/local_storage/local_storage';
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
import { FullscreenMessage } from 'ts/pages/fullscreen_message';
import { Dispatcher } from 'ts/redux/dispatcher';
import { zIndex } from 'ts/style/z_index';
import {
BlockchainErrs,
HashData,
@@ -46,6 +47,7 @@ import {
TokenVisibility,
WebsitePaths,
} from 'ts/types';
import { analytics } from 'ts/utils/analytics';
import { backendClient } from 'ts/utils/backend_client';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
@@ -73,6 +75,8 @@ export interface PortalProps {
flashMessage?: string | React.ReactNode;
lastForceTokenStateRefetch: number;
translate: Translate;
isPortalOnboardingShowing: boolean;
portalOnboardingStep: number;
}
interface PortalState {
@@ -155,9 +159,6 @@ export class Portal extends React.Component<PortalProps, PortalState> {
}
public componentWillMount(): void {
this._blockchain = new Blockchain(this.props.dispatcher);
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
public componentWillUnmount(): void {
this._blockchain.destroy();
@@ -168,6 +169,13 @@ export class Portal extends React.Component<PortalProps, PortalState> {
// become disconnected from their backing Ethereum node, changed user accounts, etc...)
this.props.dispatcher.resetState();
}
public componentDidUpdate(prevProps: PortalProps): void {
if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
}
public componentWillReceiveProps(nextProps: PortalProps): void {
if (nextProps.networkId !== this.state.prevNetworkId) {
// tslint:disable-next-line:no-floating-promises
@@ -335,6 +343,7 @@ export class Portal extends React.Component<PortalProps, PortalState> {
return (
<div>
<Wallet
style={this.props.isPortalOnboardingShowing ? { zIndex: zIndex.aboveOverlay } : undefined}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
blockchain={this._blockchain}
@@ -384,6 +393,8 @@ export class Portal extends React.Component<PortalProps, PortalState> {
}
private _startOnboarding(): void {
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
analytics.logEvent('Portal', 'Onboarding Started - Manual', networkName, this.props.portalOnboardingStep);
this.props.dispatcher.updatePortalOnboardingShowing(true);
}
private _renderWalletSection(): React.ReactNode {

View File

@@ -1,7 +1,8 @@
import { Styles } from '@0xproject/react-shared';
import { constants as sharedConstants, Styles } from '@0xproject/react-shared';
import * as _ from 'lodash';
import { GridTile } from 'material-ui/GridList';
import * as React from 'react';
import { analytics } from 'ts/utils/analytics';
import { TopTokens } from 'ts/components/relayer_index/relayer_top_tokens';
import { Container } from 'ts/components/ui/container';
@@ -66,6 +67,9 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (
const link = props.relayerInfo.appUrl || props.relayerInfo.url;
const topTokens = props.relayerInfo.topTokens;
const weeklyTxnVolume = props.relayerInfo.weeklyTxnVolume;
const networkName = sharedConstants.NETWORK_NAME_BY_ID[props.networkId];
const eventLabel = `${props.relayerInfo.name}-${networkName}`;
const trackRelayerClick = () => analytics.logEvent('Portal', 'Relayer Click', eventLabel);
const headerImageUrl = props.relayerInfo.logoImgUrl;
const headerBackgroundColor =
!_.isUndefined(headerImageUrl) && !_.isUndefined(props.relayerInfo.primaryColor)
@@ -74,7 +78,7 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (
return (
<Island style={styles.root} Component={GridTile}>
<div style={styles.innerDiv}>
<a href={link} target="_blank" style={{ textDecoration: 'none' }}>
<a href={link} target="_blank" style={{ textDecoration: 'none' }} onClick={trackRelayerClick}>
<div
className="flex items-center"
style={{ ...styles.header, backgroundColor: headerBackgroundColor }}

View File

@@ -1,6 +1,13 @@
import { colors, EtherscanLinkSuffixes, Styles, utils as sharedUtils } from '@0xproject/react-shared';
import {
colors,
constants as sharedConstants,
EtherscanLinkSuffixes,
Styles,
utils as sharedUtils,
} from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
import { analytics } from 'ts/utils/analytics';
import { WebsiteBackendTokenInfo } from 'ts/types';
@@ -61,6 +68,9 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> {
cursor: 'pointer',
opacity: this.state.isHovering ? 0.5 : 1,
};
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
const eventLabel = `${this.props.tokenInfo.symbol}-${networkName}`;
const trackTokenClick = () => analytics.logEvent('Portal', 'Token Click', eventLabel);
return (
<a
href={tokenLinkFromToken(this.props.tokenInfo, this.props.networkId)}
@@ -68,6 +78,7 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> {
style={style}
onMouseEnter={this._onToggleHover.bind(this, true)}
onMouseLeave={this._onToggleHover.bind(this, false)}
onClick={trackTokenClick}
>
{this.props.tokenInfo.symbol}
</a>

View File

@@ -6,8 +6,11 @@ import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { ProviderPicker } from 'ts/components/top_bar/provider_picker';
import { Container } from 'ts/components/ui/container';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { Image } from 'ts/components/ui/image';
import { Text } from 'ts/components/ui/text';
import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors';
import { ProviderType } from 'ts/types';
@@ -40,23 +43,16 @@ const styles: Styles = {
export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> {
public render(): React.ReactNode {
const isAddressAvailable = !_.isEmpty(this.props.userAddress);
const isExternallyInjectedProvider = utils.isExternallyInjected(
this.props.providerType,
this.props.injectedProviderName,
);
let displayMessage;
if (!this._isBlockchainReady()) {
displayMessage = 'loading account';
} else if (isAddressAvailable) {
displayMessage = utils.getAddressBeginAndEnd(this.props.userAddress);
// tslint:disable-next-line: prefer-conditional-expression
} else if (isExternallyInjectedProvider) {
displayMessage = 'Account locked';
} else {
displayMessage = '0x0000...0000';
}
const displayMessage = utils.getReadableAccountState(
this._isBlockchainReady(),
this.props.providerType,
this.props.injectedProviderName,
this.props.userAddress,
);
// If the "injected" provider is our fallback public node, then we want to
// show the "connect a wallet" message instead of the providerName
const injectedProviderName = isExternallyInjectedProvider
@@ -66,7 +62,7 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi
this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S';
const isProviderMetamask = providerTitle === constants.PROVIDER_NAME_METAMASK;
const hoverActiveNode = (
<div className="flex right lg-pr0 md-pr2 sm-pr2 p1" style={styles.root}>
<div className="flex items-center p1" style={styles.root}>
<div>
{this._isBlockchainReady() ? (
<Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} />
@@ -74,13 +70,13 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi
<CircularProgress size={ROOT_HEIGHT} thickness={2} />
)}
</div>
<div style={{ marginLeft: 12, paddingTop: 3 }}>
<div style={{ fontSize: 16, color: colors.darkGrey }}>{displayMessage}</div>
</div>
<Container marginLeft="12px" marginRight="12px">
<Text fontSize="14px" fontColor={colors.darkGrey}>
{displayMessage}
</Text>
</Container>
{isProviderMetamask && (
<div style={{ marginLeft: 16 }}>
<img src="/images/metamask_icon.png" style={{ width: ROOT_HEIGHT, height: ROOT_HEIGHT }} />
</div>
<Image src="/images/metamask_icon.png" height={ROOT_HEIGHT} width={ROOT_HEIGHT} />
)}
</div>
);

View File

@@ -297,7 +297,14 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
openSecondary={true}
onRequestChange={this._onMenuButtonClick.bind(this)}
>
<DrawerMenu selectedPath={this.props.location.pathname} userAddress={this.props.userAddress} />
<DrawerMenu
selectedPath={this.props.location.pathname}
userAddress={this.props.userAddress}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
blockchainIsLoaded={this.props.blockchainIsLoaded}
blockchain={this.props.blockchain}
/>
</Drawer>
);
}

View File

@@ -1,7 +1,9 @@
import blockies = require('blockies');
import * as _ from 'lodash';
import * as React from 'react';
import { constants } from 'ts/utils/constants';
import { Image } from 'ts/components/ui/image';
import { colors } from 'ts/style/colors';
interface IdenticonProps {
address: string;
@@ -16,14 +18,9 @@ export class Identicon extends React.Component<IdenticonProps, IdenticonState> {
style: {},
};
public render(): React.ReactNode {
let address = this.props.address;
if (_.isEmpty(address)) {
address = constants.NULL_ADDRESS;
}
const address = this.props.address;
const diameter = this.props.diameter;
const icon = blockies({
seed: address.toLowerCase(),
});
const radius = diameter / 2;
return (
<div
className="circle mx-auto relative transitionFix"
@@ -34,14 +31,19 @@ export class Identicon extends React.Component<IdenticonProps, IdenticonState> {
...this.props.style,
}}
>
<img
src={icon.toDataURL()}
style={{
width: diameter,
height: diameter,
imageRendering: 'pixelated',
}}
/>
{!_.isEmpty(address) ? (
<Image
src={blockies({
seed: address.toLowerCase(),
}).toDataURL()}
height={diameter}
width={diameter}
/>
) : (
<svg height={diameter} width={diameter}>
<circle cx={radius} cy={radius} r={radius} fill={colors.grey200} />
</svg>
)}
</div>
);
}

View File

@@ -5,7 +5,8 @@ export interface ImageProps {
className?: string;
src?: string;
fallbackSrc?: string;
height?: string;
height?: string | number;
width?: string | number;
}
interface ImageState {
imageLoadFailed: boolean;
@@ -26,6 +27,7 @@ export class Image extends React.Component<ImageProps, ImageState> {
onError={this._onError.bind(this)}
src={src}
height={this.props.height}
width={this.props.width}
/>
);
}

View File

@@ -1,9 +1,15 @@
import { EtherscanLinkSuffixes, Styles, utils as sharedUtils } from '@0xproject/react-shared';
import {
constants as sharedConstants,
EtherscanLinkSuffixes,
Styles,
utils as sharedUtils,
} from '@0xproject/react-shared';
import { BigNumber, errorUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
import CircularProgress from 'material-ui/CircularProgress';
import FloatingActionButton from 'material-ui/FloatingActionButton';
import { ListItem } from 'material-ui/List';
import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
import ContentAdd from 'material-ui/svg-icons/content/add';
@@ -23,7 +29,6 @@ import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors';
import { zIndex } from 'ts/style/z_index';
import {
BlockchainErrs,
ProviderType,
@@ -35,6 +40,7 @@ import {
TokenStateByAddress,
WebsitePaths,
} from 'ts/types';
import { analytics } from 'ts/utils/analytics';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';
import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles';
@@ -59,6 +65,7 @@ export interface WalletProps {
onAddToken: () => void;
onRemoveToken: () => void;
refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
style: React.CSSProperties;
}
interface WalletState {
@@ -79,7 +86,6 @@ interface AccessoryItemConfig {
const styles: Styles = {
root: {
width: '100%',
zIndex: zIndex.aboveOverlay,
position: 'relative',
},
footerItemInnerDiv: {
@@ -134,6 +140,9 @@ const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56;
const ACCOUNT_PATH = `${WebsitePaths.Portal}/account`;
export class Wallet extends React.Component<WalletProps, WalletState> {
public static defaultProps = {
style: {},
};
constructor(props: WalletProps) {
super(props);
this.state = {
@@ -141,11 +150,10 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
isHoveringSidebar: false,
};
}
public render(): React.ReactNode {
const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError;
return (
<Island className="flex flex-column wallet" style={styles.root}>
<Island className="flex flex-column wallet" style={{ ...styles.root, ...this.props.style }}>
{isBlockchainLoaded ? this._renderLoadedRows() : this._renderLoadingRows()}
</Island>
);
@@ -269,7 +277,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
<ListItem
primaryText={
<div className="flex right" style={styles.manageYourWalletText}>
{'manage your wallet'}
manage your wallet
</div>
// https://github.com/palantir/tslint-react/issues/140
// tslint:disable-next-line:jsx-curly-spacing
@@ -488,18 +496,26 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
}
}
const onClick = isWrappedEtherDirectionOpen
? this._closeWrappedEtherActionRow.bind(this)
? this._closeWrappedEtherActionRow.bind(this, wrappedEtherDirection)
: this._openWrappedEtherActionRow.bind(this, wrappedEtherDirection);
return (
<IconButton iconName={buttonIconName} labelText={buttonLabel} onClick={onClick} color={colors.mediumBlue} />
);
}
private _openWrappedEtherActionRow(wrappedEtherDirection: Side): void {
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
const action =
wrappedEtherDirection === Side.Deposit ? 'Wallet - Wrap ETH Opened' : 'Wallet - Unwrap WETH Opened';
analytics.logEvent('Portal', action, networkName);
this.setState({
wrappedEtherDirection,
});
}
private _closeWrappedEtherActionRow(): void {
private _closeWrappedEtherActionRow(wrappedEtherDirection: Side): void {
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
const action =
wrappedEtherDirection === Side.Deposit ? 'Wallet - Wrap ETH Closed' : 'Wallet - Unwrap WETH Closed';
analytics.logEvent('Portal', action, networkName);
this.setState({
wrappedEtherDirection: undefined,
});

View File

@@ -1,4 +1,4 @@
import { Styles } from '@0xproject/react-shared';
import { constants as sharedConstants, Styles } from '@0xproject/react-shared';
import { BigNumber, logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
@@ -11,6 +11,7 @@ import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors';
import { BlockchainCallErrs, Side, Token } from 'ts/types';
import { analytics } from 'ts/utils/analytics';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';
@@ -186,6 +187,7 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther
this.setState({
isEthConversionHappening: true,
});
const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
try {
const etherToken = this.props.etherToken;
const amountToConvert = this.state.currentInputAmount;
@@ -193,10 +195,12 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther
await this.props.blockchain.convertEthToWrappedEthTokensAsync(etherToken.address, amountToConvert);
const ethAmount = Web3Wrapper.toUnitAmount(amountToConvert, constants.DECIMAL_PLACES_ETH);
this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`);
analytics.logEvent('Portal', 'Wrap ETH Successfully', networkName);
} else {
await this.props.blockchain.convertWrappedEthTokensToEthAsync(etherToken.address, amountToConvert);
const tokenAmount = Web3Wrapper.toUnitAmount(amountToConvert, etherToken.decimals);
this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`);
analytics.logEvent('Portal', 'Unwrap WETH Successfully', networkName);
}
await this.props.refetchEthTokenStateAsync();
this.props.onConversionSuccessful();
@@ -207,11 +211,13 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther
} else if (!utils.didUserDenyWeb3Request(errMsg)) {
logUtils.log(`Unexpected error encountered: ${err}`);
logUtils.log(err.stack);
const errorMsg =
this.props.direction === Side.Deposit
? 'Failed to wrap your ETH. Please try again.'
: 'Failed to unwrap your WETH. Please try again.';
this.props.dispatcher.showFlashMessage(errorMsg);
if (this.props.direction === Side.Deposit) {
this.props.dispatcher.showFlashMessage('Failed to wrap your ETH. Please try again.');
analytics.logEvent('Portal', 'Wrap ETH Failed', networkName);
} else {
this.props.dispatcher.showFlashMessage('Failed to unwrap your WETH. Please try again.');
analytics.logEvent('Portal', 'Unwrap WETH Failed', networkName);
}
await errorReporter.reportAsync(err);
}
}

View File

@@ -28,6 +28,8 @@ interface ConnectedState {
userSuppliedOrderCache: Order;
flashMessage?: string | React.ReactNode;
translate: Translate;
isPortalOnboardingShowing: boolean;
portalOnboardingStep: number;
}
interface ConnectedDispatch {
@@ -76,6 +78,8 @@ const mapStateToProps = (state: State, _ownProps: PortalComponentProps): Connect
userSuppliedOrderCache: state.userSuppliedOrderCache,
flashMessage: state.flashMessage,
translate: state.translate,
isPortalOnboardingShowing: state.isPortalOnboardingShowing,
portalOnboardingStep: state.portalOnboardingStep,
};
};

View File

@@ -15,6 +15,7 @@ interface PortalOnboardingFlowProps {
}
interface ConnectedState {
networkId: number;
stepIndex: number;
isRunning: boolean;
userAddress: string;
@@ -32,6 +33,7 @@ interface ConnectedDispatch {
}
const mapStateToProps = (state: State, _ownProps: PortalOnboardingFlowProps): ConnectedState => ({
networkId: state.networkId,
stepIndex: state.portalOnboardingStep,
isRunning: state.isPortalOnboardingShowing,
userAddress: state.userAddress,

View File

@@ -190,6 +190,25 @@ export const utils = {
const truncatedAddress = `${address.substring(0, 6)}...${address.substr(-4)}`; // 0x3d5a...b287
return truncatedAddress;
},
getReadableAccountState(
isBlockchainReady: boolean,
providerType: ProviderType,
injectedProviderName: string,
userAddress?: string,
): string {
const isAddressAvailable = !_.isUndefined(userAddress) && !_.isEmpty(userAddress);
const isExternallyInjectedProvider = utils.isExternallyInjected(providerType, injectedProviderName);
if (!isBlockchainReady) {
return 'Loading account';
} else if (isAddressAvailable) {
return utils.getAddressBeginAndEnd(userAddress);
// tslint:disable-next-line: prefer-conditional-expression
} else if (isExternallyInjectedProvider) {
return 'Account locked';
} else {
return 'No wallet detected';
}
},
hasUniqueNameAndSymbol(tokens: Token[], token: Token): boolean {
if (token.isRegistered) {
return true; // Since it's registered, it is the canonical token