Skip to content

Commit

Permalink
Feature: token deeplinks support (#588)
Browse files Browse the repository at this point in the history
* handling data through deeplink

* handle chainId

* handle token value correctly

* use contract metadata to get token info is in there

* showing alert while changing network

* don't validate toen that user doesn't have

* validate amount token to send if user doesn't have it in state

* move messages to locales

* fix getbalanceof

* handle state token

* fix typo

* update locale

* locales
  • Loading branch information
estebanmino authored Apr 9, 2019
1 parent a0350d9 commit bf53730
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 19 deletions.
23 changes: 19 additions & 4 deletions app/components/UI/TransactionEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ class TransactionEditor extends Component {
} = this.props;
const validations = {
ETH: () => this.validateEtherAmount(allowEmpty),
ERC20: () => this.validateTokenAmount(allowEmpty),
ERC20: async () => await this.validateTokenAmount(allowEmpty),
ERC721: async () => await this.validateCollectibleOwnership()
};
return await validations[assetType]();
Expand Down Expand Up @@ -358,7 +358,7 @@ class TransactionEditor extends Component {
* @param {bool} allowEmpty - Whether the validation allows empty amount or not
* @returns {string} - String containing error message whether the Ether transaction amount is valid or not
*/
validateTokenAmount = (allowEmpty = true) => {
validateTokenAmount = async (allowEmpty = true) => {
let error;
if (!allowEmpty) {
const {
Expand All @@ -370,9 +370,24 @@ class TransactionEditor extends Component {
if (!value || !gas || !gasPrice || !from) {
return strings('transaction.invalid_amount');
}
const contractBalanceForAddress = hexToBN(contractBalances[selectedAsset.address].toString(16));
// If user trying to send a token that doesn't own, validate balance querying contract
// If it fails, skip validation
let contractBalanceForAddress;
if (contractBalances[selectedAsset.address]) {
contractBalanceForAddress = hexToBN(contractBalances[selectedAsset.address].toString(16));
} else {
const { AssetsContractController } = Engine.context;
try {
contractBalanceForAddress = await AssetsContractController.getBalanceOf(
selectedAsset.address,
checksummedFrom
);
} catch (e) {
// Don't validate balance if error
}
}
if (value && !isBN(value)) return strings('transaction.invalid_amount');
const validateAssetAmount = contractBalanceForAddress.lt(value);
const validateAssetAmount = contractBalanceForAddress && contractBalanceForAddress.lt(value);
const ethTotalAmount = gas.mul(gasPrice);
if (
value &&
Expand Down
136 changes: 124 additions & 12 deletions app/components/Views/Send/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { InteractionManager, SafeAreaView, ActivityIndicator, Alert, StyleSheet,
import { colors } from '../../../styles/common';
import Engine from '../../../core/Engine';
import TransactionEditor from '../../UI/TransactionEditor';
import { toBN, BNToHex, hexToBN, fromWei } from '../../../util/number';
import { toBN, BNToHex, hexToBN, fromWei, toTokenMinimalUnit, renderFromTokenMinimalUnit } from '../../../util/number';
import { toChecksumAddress } from 'ethereumjs-util';
import { strings } from '../../../../locales/i18n';
import { getTransactionOptionsTitle } from '../../UI/Navbar';
import { connect } from 'react-redux';
import { newTransaction, setTransactionObject } from '../../../actions/transaction';
import TransactionsNotificationManager from '../../../core/TransactionsNotificationManager';
import NetworkList, { getNetworkTypeById } from '../../../util/networks';
import contractMap from 'eth-contract-metadata';
import { showAlert } from '../../../actions/alert';

const styles = StyleSheet.create({
wrapper: {
Expand Down Expand Up @@ -41,14 +44,26 @@ class Send extends Component {
* Action that cleans transaction state
*/
newTransaction: PropTypes.func.isRequired,
/**
* A string representing the network name
*/
networkType: PropTypes.string,
/**
* Action that sets transaction attributes from object to a transaction
*/
setTransactionObject: PropTypes.func.isRequired,
/**
* Array of ERC20 assets
*/
tokens: PropTypes.array,
/**
* Transaction state
*/
transaction: PropTypes.object.isRequired
transaction: PropTypes.object.isRequired,
/**
/* Triggers global alert
*/
showAlert: PropTypes.func
};

state = {
Expand Down Expand Up @@ -93,6 +108,9 @@ class Send extends Component {
this.props.newTransaction();
};

/**
* Check if view is called with txMeta object for a deeplink
*/
checkForDeeplinks() {
const { navigation } = this.props;
const txMeta = navigation && navigation.getParam('txMeta', null);
Expand Down Expand Up @@ -141,21 +159,49 @@ class Send extends Component {
}
}

/**
* Handle txMeta object, setting neccesary state to make a transaction
*/
handleNewTxMeta = async ({
target_address,
chain_id = null, // eslint-disable-line no-unused-vars
action,
chain_id = null,
function_name = null, // eslint-disable-line no-unused-vars
parameters = null
}) => {
const newTxMeta = { symbol: 'ETH', assetType: 'ETH', type: 'ETHER_TRANSACTION' };
newTxMeta.to = toChecksumAddress(target_address);
if (chain_id) {
this.handleNetworkSwitch(chain_id);
}
let newTxMeta;
switch (action) {
case 'send-eth':
newTxMeta = { symbol: 'ETH', assetType: 'ETH', type: 'ETHER_TRANSACTION' };
newTxMeta.to = toChecksumAddress(target_address);
if (parameters && parameters.value) {
newTxMeta.value = toBN(parameters.value);
newTxMeta.readableValue = fromWei(newTxMeta.value);
}
break;
case 'send-token': {
const selectedAsset = await this.handleTokenDeeplink(target_address);
newTxMeta = {
assetType: 'ERC20',
type: 'INDIVIDUAL_TOKEN_TRANSACTION',
selectedAsset
};
newTxMeta.to = toChecksumAddress(parameters.address);
if (parameters && parameters.uint256) {
newTxMeta.value = toTokenMinimalUnit(parameters.uint256, selectedAsset.decimals);
newTxMeta.readableValue = String(
renderFromTokenMinimalUnit(newTxMeta.value, selectedAsset.decimals)
);
}
break;
}
}

if (parameters) {
const { value, gas, gasPrice } = parameters;
if (value) {
newTxMeta.value = toBN(value);
newTxMeta.readableValue = fromWei(newTxMeta.value);
}
const { gas, gasPrice } = parameters;
if (gas) {
newTxMeta.gas = toBN(gas);
}
Expand All @@ -180,6 +226,69 @@ class Send extends Component {
this.mounted && this.setState({ ready: true, mode: 'edit', transactionKey: Date.now() });
};

/**
* Method in charge of changing network if is needed
*
* @param chainId - Corresponding id for network
*/
handleNetworkSwitch = chainId => {
const { networkType } = this.props;
const newNetworkType = getNetworkTypeById(chainId);
if (newNetworkType && networkType !== newNetworkType) {
const { NetworkController } = Engine.context;
NetworkController.setProviderType(newNetworkType);
this.props.showAlert({
isVisible: true,
autodismiss: 5000,
content: 'clipboard-alert',
data: { msg: strings('send.warn_network_change') + NetworkList[newNetworkType].name }
});
}
};

/**
* Retrieves ERC20 asset information (symbol and decimals) to be used with deeplinks
*
* @param address - Corresponding ERC20 asset address
*
* @returns ERC20 asset, containing address, symbol and decimals
*/
handleTokenDeeplink = async address => {
const { tokens } = this.props;
address = toChecksumAddress(address);
// First check if we have token information in contractMap
if (address in contractMap) {
return contractMap[address];
}
// Then check if the token is already in state
const stateToken = tokens.find(token => token.address === address);
if (stateToken) {
return stateToken;
}
// Finally try to query the contract
const { AssetsContractController } = Engine.context;
const token = { address };
try {
const decimals = await AssetsContractController.getTokenDecimals(address);
token.decimals = parseInt(String(decimals));
} catch (e) {
// Drop tx since we don't have any form to get decimals and send the correct tx
this.props.showAlert({
isVisible: true,
autodismiss: 2000,
content: 'clipboard-alert',
data: { msg: strings(`send.deeplink_failure`) }
});
this.onCancel();
}
try {
token.symbol = await AssetsContractController.getAssetSymbol(address);
} catch (e) {
token.symbol = 'ERC20';
}
return token;
};

/**
* Returns transaction object with gas, gasPrice and value in hex format
*
Expand Down Expand Up @@ -303,12 +412,15 @@ class Send extends Component {
const mapStateToProps = state => ({
accounts: state.engine.backgroundState.AccountTrackerController.accounts,
contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances,
transaction: state.transaction
transaction: state.transaction,
networkType: state.engine.backgroundState.NetworkController.provider.type,
tokens: state.engine.backgroundState.AssetsController.tokens
});

const mapDispatchToProps = dispatch => ({
newTransaction: () => dispatch(newTransaction()),
setTransactionObject: transaction => dispatch(setTransactionObject(transaction))
setTransactionObject: transaction => dispatch(setTransactionObject(transaction)),
showAlert: config => dispatch(showAlert(config))
});

export default connect(
Expand Down
2 changes: 1 addition & 1 deletion app/util/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function toTokenMinimalUnit(tokenValue, decimals) {
* @param {Number|String|BN} tokenValue - Token value to convert
* @param {Number} decimals - Token decimals to convert
* @param {Number} decimalsToShow - Decimals to 5
* @returns {Number} - String of token minimal unit, in render format
* @returns {Number} - Number of token minimal unit, in render format
*/
export function renderFromTokenMinimalUnit(tokenValue, decimals, decimalsToShow = 5) {
const minimalUnit = fromTokenMinimalUnit(tokenValue, decimals);
Expand Down
4 changes: 3 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@
"public_address": "Public Address"
},
"send": {
"title": "Send"
"title": "Send",
"deeplink_failure": "Ooops! Something went wrong! Please try again",
"warn_network_change": "Network changed to "
},
"receive": {
"title": "Receive"
Expand Down
4 changes: 3 additions & 1 deletion locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@
"public_address": "Public Address"
},
"send": {
"title": "Enviar"
"title": "Enviar",
"deeplink_failure": "Ooops! Algo ha salido mal, por favor intėntalo de nuevo",
"warn_network_change": "La red ha sido cambiada a "
},
"receive": {
"title": "Recibir"
Expand Down

0 comments on commit bf53730

Please sign in to comment.