MyCrypto/common/features/selectors.ts

665 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import erc20 from 'libs/erc20';
import BN from 'bn.js';
import EthTx from 'ethereumjs-tx';
import { bufferToHex } from 'ethereumjs-util';
import { Address, Wei, TokenValue, Nonce, getDecimalFromEtherUnit } from 'libs/units';
import {
EAC_SCHEDULING_CONFIG,
EAC_ADDRESSES,
calcEACEndowment,
getSchedulerAddress,
getScheduleData,
getValidateRequestParamsData
} from 'libs/scheduling';
import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/transaction';
import { stripHexPrefixAndLower } from 'libs/formatters';
import { SecureWalletName, WalletName, getAddressMessage, AddressMessage } from 'config';
import { Token } from 'types/network';
import { ICurrentTo, ICurrentValue, IGetTransaction } from './types';
import { AppState } from './reducers';
import { addressBookSelectors } from './addressBook';
import { walletTypes, walletSelectors } from './wallet';
import { ratesSelectors } from './rates';
import { customTokensSelectors } from './customTokens';
import { scheduleSelectors, scheduleHelpers } from './schedule';
import { transactionsSelectors } from './transactions';
import { SavedTransaction } from 'types/transactions';
import * as configMetaSelectors from './config/meta/selectors';
import * as configSelectors from './config/selectors';
import * as transactionSelectors from './transaction/selectors';
import * as transactionFieldsSelectors from './transaction/fields/selectors';
import * as transactionMetaSelectors from './transaction/meta/selectors';
import * as transactionSignTypes from './transaction/sign/types';
import * as transactionSignSelectors from './transaction/sign/selectors';
import { reduceToValues, isFullTx } from './helpers';
export const isAnyOfflineWithWeb3 = (state: AppState): boolean => {
const { isWeb3Wallet } = walletSelectors.getWalletType(state);
const offline = configMetaSelectors.getOffline(state);
return offline && isWeb3Wallet;
};
// TODO: Convert to reselect selector (Issue #884)
export function getDisabledWallets(state: AppState): any {
const network = configSelectors.getNetworkConfig(state);
const isOffline = configMetaSelectors.getOffline(state);
const disabledWallets: any = {
wallets: [],
reasons: {}
};
const addReason = (wallets: WalletName[], reason: string) => {
if (!wallets.length) {
return;
}
disabledWallets.wallets = disabledWallets.wallets.concat(wallets);
wallets.forEach(wallet => {
disabledWallets.reasons[wallet] = reason;
});
};
// Some wallets don't support some networks
addReason(
configSelectors.unSupportedWalletFormatsOnNetwork(state),
`This wallet doesnt support the ${network.name} network`
);
// Some wallets are unavailable offline
if (isOffline) {
addReason(
[SecureWalletName.WEB3, SecureWalletName.TREZOR, SecureWalletName.SAFE_T],
'This wallet cannot be accessed offline'
);
}
// Some wallets are disabled on certain platforms
if (process.env.BUILD_ELECTRON) {
addReason([SecureWalletName.WEB3], 'This wallet is not supported in the MyCrypto app');
addReason(
[SecureWalletName.SAFE_T],
'Coming soon. Please use the MyCrypto.com website in the meantime'
);
}
// Dedupe and sort for consistency
disabledWallets.wallets = disabledWallets.wallets
.filter((name: string, idx: number) => disabledWallets.wallets.indexOf(name) === idx)
.sort();
return disabledWallets;
}
export function getTokens(state: AppState): walletTypes.MergedToken[] {
const network = configSelectors.getStaticNetworkConfig(state);
const tokens: Token[] = network ? network.tokens : [];
return tokens.concat(
state.customTokens.map((token: Token) => {
const mergedToken = { ...token, custom: true };
return mergedToken;
})
) as walletTypes.MergedToken[];
}
export function getWalletConfigTokens(state: AppState): walletTypes.MergedToken[] {
const tokens = getTokens(state);
const config = walletSelectors.getWalletConfig(state);
if (!config || !config.tokens) {
return [];
}
return config.tokens
.map(symbol => tokens.find(t => t.symbol === symbol))
.filter(token => token) as walletTypes.MergedToken[];
}
export const getToken = (state: AppState, unit: string): walletTypes.MergedToken | undefined => {
const tokens = getTokens(state);
const token = tokens.find(t => t.symbol === unit);
return token;
};
export function getTokenBalances(
state: AppState,
nonZeroOnly: boolean = false
): walletTypes.TokenBalance[] {
const tokens = getTokens(state);
if (!tokens) {
return [];
}
const ret = tokens.map(t => ({
symbol: t.symbol,
balance: state.wallet.tokens[t.symbol]
? state.wallet.tokens[t.symbol].balance
: TokenValue('0'),
error: state.wallet.tokens[t.symbol] ? state.wallet.tokens[t.symbol].error : null,
custom: t.custom,
decimal: t.decimal
}));
return nonZeroOnly ? ret.filter(t => !t.balance.isZero()) : ret;
}
export const getTokenWithBalance = (state: AppState, unit: string): walletTypes.TokenBalance => {
const tokens = getTokenBalances(state, false);
const currentToken = tokens.filter(t => t.symbol === unit);
//TODO: getting the first index is kinda hacky
return currentToken[0];
};
export const getTokenBalance = (state: AppState, unit: string): TokenValue | null => {
const token = getTokenWithBalance(state, unit);
if (!token) {
return token;
}
return token.balance;
};
export function getShownTokenBalances(
state: AppState,
nonZeroOnly: boolean = false
): walletTypes.TokenBalance[] {
const tokenBalances = getTokenBalances(state, nonZeroOnly);
const walletConfig = walletSelectors.getWalletConfig(state);
let walletTokens: string[] = [];
if (walletConfig) {
if (walletConfig.tokens) {
walletTokens = walletConfig.tokens;
}
}
return tokenBalances.filter(t => walletTokens.includes(t.symbol));
}
const getUSDConversionRate = (state: AppState, unit: string) => {
const { isTestnet } = configSelectors.getNetworkConfig(state);
const { rates } = ratesSelectors.getRates(state);
if (isTestnet) {
return null;
}
const conversionRate = rates[unit];
if (!conversionRate) {
return null;
}
return conversionRate.USD;
};
export const getValueInUSD = (state: AppState, value: TokenValue | Wei) => {
const unit = getUnit(state);
const conversionRate = getUSDConversionRate(state, unit);
if (!conversionRate) {
return null;
}
const sendValueUSD = value.muln(conversionRate);
return sendValueUSD;
};
export const getTransactionFeeInUSD = (state: AppState, fee: Wei) => {
const { unit } = configSelectors.getNetworkConfig(state);
const conversionRate = getUSDConversionRate(state, unit);
if (!conversionRate) {
return null;
}
const feeValueUSD = fee.muln(conversionRate);
return feeValueUSD;
};
export interface AllUSDValues {
valueUSD: BN | null;
feeUSD: BN | null;
totalUSD: BN | null;
}
export const getAllUSDValuesFromSerializedTx = (state: AppState): AllUSDValues => {
const fields = getParamsFromSerializedTx(state);
if (!fields) {
return {
feeUSD: null,
valueUSD: null,
totalUSD: null
};
}
const { currentValue, fee } = fields;
const valueUSD = getValueInUSD(state, currentValue);
const feeUSD = getTransactionFeeInUSD(state, fee);
return {
feeUSD,
valueUSD,
totalUSD: feeUSD && valueUSD ? valueUSD.add(feeUSD) : null
};
};
export function getRecentNetworkTransactions(state: AppState): SavedTransaction[] {
const txs = transactionsSelectors.getRecentTransactions(state);
const network = configSelectors.getNetworkConfig(state);
return txs.filter(tx => tx.chainId === network.chainId);
}
export function getRecentWalletTransactions(state: AppState): SavedTransaction[] {
const networkTxs = getRecentNetworkTransactions(state);
const wallet = walletSelectors.getWalletInst(state);
if (wallet) {
const addr = wallet.getAddressString().toLowerCase();
return networkTxs.filter(tx => tx.from.toLowerCase() === addr);
} else {
return [];
}
}
export const getSchedulingTransaction = (state: AppState): IGetTransaction => {
const { isFullTransaction } = getTransaction(state);
const currentTo = getCurrentTo(state);
const currentValue = getCurrentValue(state);
const nonce = transactionFieldsSelectors.getNonce(state);
const gasPrice = transactionFieldsSelectors.getGasPrice(state);
const timeBounty = scheduleSelectors.getTimeBounty(state);
const scheduleGasPrice = scheduleSelectors.getScheduleGasPrice(state);
const scheduleGasLimit = scheduleSelectors.getScheduleGasLimit(state);
const scheduleType = scheduleSelectors.getScheduleType(state);
const endowment = calcEACEndowment(
scheduleGasLimit.value,
currentValue.value,
scheduleGasPrice.value,
timeBounty.value
);
let transactionData = null;
const transactionFullAndValid = isFullTransaction && isSchedulingTransactionValid(state);
if (transactionFullAndValid) {
const deposit = scheduleSelectors.getScheduleDeposit(state);
const scheduleTimestamp = scheduleSelectors.getScheduleTimestamp(state);
const windowSize = scheduleSelectors.getWindowSize(state);
const callData = transactionFieldsSelectors.getData(state);
const scheduleTimezone = scheduleSelectors.getScheduleTimezone(state);
const windowStart = scheduleSelectors.getWindowStart(state);
transactionData = getScheduleData(
currentTo.raw,
callData.raw,
scheduleGasLimit.value,
currentValue.value,
scheduleHelpers.windowSizeBlockToMin(windowSize.value, scheduleType.value),
scheduleHelpers.calculateWindowStart(
scheduleType.value,
scheduleTimestamp,
scheduleTimezone.value,
windowStart.value
),
scheduleGasPrice.value,
timeBounty.value,
deposit.value
);
}
const transactionOptions = {
to: getSchedulerAddress(scheduleType.value),
data: transactionData,
gasLimit: EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT,
gasPrice: gasPrice.value,
nonce: Nonce('0'),
value: endowment
};
if (nonce) {
transactionOptions.nonce = Nonce(nonce.raw);
}
const schedulingTransaction: EthTx = makeTransaction(transactionOptions);
return {
transaction: schedulingTransaction,
isFullTransaction: transactionFullAndValid
};
};
const isSchedulingTransactionValid = (state: AppState): boolean => {
const schedulingState = scheduleSelectors.getScheduleState(state);
const windowSizeValid = scheduleSelectors.isWindowSizeValid(state);
const windowStartValid = scheduleHelpers.isWindowStartValid(
schedulingState,
configMetaSelectors.getLatestBlock(state)
);
const scheduleTimestampValid = scheduleHelpers.isScheduleTimestampValid(schedulingState);
const scheduleGasPriceValid = scheduleSelectors.isValidScheduleGasPrice(state);
const scheduleGasLimitValid = scheduleSelectors.isValidScheduleGasLimit(state);
const depositValid = scheduleSelectors.isValidScheduleDeposit(state);
const timeBountyValid = scheduleSelectors.isValidCurrentTimeBounty(state);
// return true if all fields are valid
return (
// either windowStart or scheduleTimestamp is used for scheduling
(windowStartValid || scheduleTimestampValid) &&
windowSizeValid &&
scheduleGasPriceValid &&
scheduleGasLimitValid &&
depositValid &&
timeBountyValid
);
};
export interface IGetValidateScheduleParamsCallPayload {
to: string;
data: string;
}
export const getValidateScheduleParamsCallPayload = (
state: AppState
): IGetValidateScheduleParamsCallPayload | undefined => {
const wallet = walletSelectors.getWalletInst(state);
const currentTo = getCurrentTo(state);
const currentValue = getCurrentValue(state);
const timeBounty = scheduleSelectors.getTimeBounty(state);
const scheduleGasPrice = scheduleSelectors.getScheduleGasPrice(state);
const scheduleGasLimit = scheduleSelectors.getScheduleGasLimit(state);
const scheduleType = scheduleSelectors.getScheduleType(state);
const deposit = scheduleSelectors.getScheduleDeposit(state);
const scheduleTimestamp = scheduleSelectors.getScheduleTimestamp(state);
const windowSize = scheduleSelectors.getWindowSize(state);
const scheduleTimezone = scheduleSelectors.getScheduleTimezone(state);
const windowStart = scheduleSelectors.getWindowStart(state);
/*
* Checks if any of these values are null or invalid
* due to an user input.
*/
if (
!currentValue.value ||
!currentTo.value ||
!scheduleGasPrice.value ||
!wallet ||
!windowSize.value ||
// we need either windowStart or scheduleTimestamp for scheduling
!(windowStart.value || scheduleTimestamp.value)
) {
return;
}
const callGasLimit = scheduleGasLimit.value || EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_LIMIT_FALLBACK;
const endowment = calcEACEndowment(
callGasLimit,
currentValue.value,
scheduleGasPrice.value,
timeBounty.value
);
const fromAddress = wallet.getAddressString();
const data = getValidateRequestParamsData(
bufferToHex(currentTo.value),
callGasLimit,
currentValue.value,
scheduleHelpers.windowSizeBlockToMin(windowSize.value, scheduleType.value),
scheduleHelpers.calculateWindowStart(
scheduleType.value,
scheduleTimestamp,
scheduleTimezone.value,
windowStart.value
),
scheduleGasPrice.value,
timeBounty.value,
deposit.value || Wei('0'),
scheduleType.value === 'time',
endowment,
fromAddress
);
return {
to: EAC_ADDRESSES.KOVAN.requestFactory,
data
};
};
export const isValidCurrentWindowStart = (state: AppState) => {
const currentWindowStart = scheduleSelectors.getWindowStart(state);
if (!currentWindowStart.value) {
return false;
}
return currentWindowStart.value > parseInt(configMetaSelectors.getLatestBlock(state), 10);
};
export const isEtherTransaction = (state: AppState) => {
const unit = getUnit(state);
const etherUnit = configSelectors.isNetworkUnit(state, unit);
return etherUnit;
};
export const getValidGasCost = (state: AppState) => {
const gasCost = transactionSelectors.getGasCost(state);
const etherBalance = walletSelectors.getEtherBalance(state);
const isOffline = configMetaSelectors.getOffline(state);
if (isOffline || !etherBalance) {
return true;
}
return gasCost.lte(etherBalance);
};
export const getDecimalFromUnit = (state: AppState, unit: string) => {
if (configSelectors.isNetworkUnit(state, unit)) {
return getDecimalFromEtherUnit('ether');
} else {
const token = getToken(state, unit);
if (!token) {
throw Error(`Token ${unit} not found`);
}
return token.decimal;
}
};
export const getCurrentTo = (state: AppState): ICurrentTo =>
isEtherTransaction(state)
? transactionFieldsSelectors.getTo(state)
: transactionMetaSelectors.getTokenTo(state);
export const getCurrentValue = (state: AppState): ICurrentValue =>
isEtherTransaction(state)
? transactionFieldsSelectors.getValue(state)
: transactionMetaSelectors.getTokenValue(state);
export const isValidCurrentTo = (state: AppState) => {
const currentTo = getCurrentTo(state);
const dataExists = transactionSelectors.getDataExists(state);
if (isEtherTransaction(state)) {
// if data exists the address can be 0x
return !!currentTo.value || dataExists;
} else {
return !!currentTo.value;
}
};
export const isCurrentToLabelEntry = (state: AppState): boolean => {
const currentTo = getCurrentTo(state);
return !currentTo.raw.startsWith('0x');
};
export function getCurrentToAddressMessage(state: AppState): AddressMessage | undefined {
const to = getCurrentTo(state);
return getAddressMessage(to.raw);
}
export const getUnit = (state: AppState) => {
const serializedTransaction = getSerializedTransaction(state);
const contractInteraction = transactionMetaSelectors.isContractInteraction(state);
// attempt to get the to address from the transaction
if (serializedTransaction && !contractInteraction) {
const transactionInstance = new EthTx(serializedTransaction);
const { to } = transactionInstance;
if (to) {
// see if any tokens match
let networkTokens: null | Token[] = null;
const customTokens = customTokensSelectors.getCustomTokens(state);
const networkConfig = configSelectors.getNetworkConfig(state);
if (!networkConfig.isCustom) {
networkTokens = networkConfig.tokens;
}
const mergedTokens = networkTokens ? [...networkTokens, ...customTokens] : customTokens;
const toChecksumAddress = configSelectors.getChecksumAddressFn(state);
const stringTo = toChecksumAddress(stripHexPrefixAndLower(to.toString('hex')));
const result = mergedTokens.find(t => t.address === stringTo);
if (result) {
return result.symbol;
}
}
}
return transactionMetaSelectors.getMetaState(state).unit;
};
export const signaturePending = (state: AppState) => {
const { isHardwareWallet } = walletSelectors.getWalletType(state);
const { pending } = state.transaction.sign;
return { isHardwareWallet, isSignaturePending: pending };
};
export const getSerializedTransaction = (state: AppState) =>
walletSelectors.getWalletType(state).isWeb3Wallet
? transactionSignSelectors.getWeb3Tx(state)
: transactionSignSelectors.getSignedTx(state);
export const getParamsFromSerializedTx = (
state: AppState
): transactionSignTypes.SerializedTxParams => {
const tx = getSerializedTransaction(state);
const isEther = isEtherTransaction(state);
const decimal = transactionMetaSelectors.getDecimal(state);
if (!tx) {
throw Error('Serialized transaction not found');
}
const fields = getTransactionFields(makeTransaction(tx));
const { value, data, gasLimit, gasPrice, to } = fields;
const currentValue = isEther ? Wei(value) : TokenValue(erc20.transfer.decodeInput(data)._value);
const currentTo = isEther ? Address(to) : Address(erc20.transfer.decodeInput(data)._to);
const unit = getUnit(state);
const fee = Wei(gasLimit).mul(Wei(gasPrice));
const total = fee.add(Wei(value));
return { ...fields, currentValue, currentTo, fee, total, unit, decimal, isToken: !isEther };
};
export const getTransaction = (state: AppState): IGetTransaction => {
const currentTo = getCurrentTo(state);
const currentValue = getCurrentValue(state);
const transactionFields = transactionFieldsSelectors.getFields(state);
const unit = getUnit(state);
const reducedValues = reduceToValues(transactionFields);
const transaction: EthTx = makeTransaction(reducedValues);
const dataExists = transactionSelectors.getDataExists(state);
const validGasCost = getValidGasCost(state);
const isFullTransaction = isFullTx(
state,
transactionFields,
currentTo,
currentValue,
dataExists,
validGasCost,
unit
);
return { transaction, isFullTransaction };
};
export const nonStandardTransaction = (state: AppState): boolean => {
const etherTransaction = isEtherTransaction(state);
const { isFullTransaction } = getTransaction(state);
const dataExists = transactionSelectors.getDataExists(state);
return isFullTransaction && dataExists && etherTransaction;
};
export const serializedAndTransactionFieldsMatch = (state: AppState, isLocallySigned: boolean) => {
const serialzedTransaction = getSerializedTransaction(state);
const { transaction, isFullTransaction } = getTransaction(state);
if (!isFullTransaction || !serialzedTransaction) {
return false;
}
const t1 = getTransactionFields(transaction);
// inject chainId into t1 as it wont have it from the fields
const networkConfig = configSelectors.getNetworkConfig(state);
if (!networkConfig) {
return false;
}
const { chainId } = networkConfig;
t1.chainId = chainId;
const t2 = getTransactionFields(makeTransaction(serialzedTransaction));
const checkValidity = (tx: IHexStrTransaction) =>
Object.keys(tx).reduce(
(match, currField: keyof IHexStrTransaction) => match && t1[currField] === t2[currField],
true
);
//reduce both ways to make sure both are exact same
const transactionsMatch = checkValidity(t1) && checkValidity(t2);
// if its signed then verify the signature too
return transactionsMatch && isLocallySigned
? makeTransaction(serialzedTransaction).verifySignature()
: true;
};
export const getFrom = (state: AppState) => {
const serializedTransaction = getSerializedTransaction(state);
// attempt to get the from address from the transaction
if (serializedTransaction) {
const transactionInstance = new EthTx(serializedTransaction);
try {
const from = transactionInstance.from;
if (from) {
const toChecksumAddress = configSelectors.getChecksumAddressFn(state);
return toChecksumAddress(from.toString('hex'));
}
} catch (e) {
console.warn(e);
}
}
return transactionMetaSelectors.getMetaState(state).from;
};
export const getCurrentBalance = (state: AppState): Wei | TokenValue | null => {
const etherTransaction = isEtherTransaction(state);
if (etherTransaction) {
return walletSelectors.getEtherBalance(state);
} else {
const unit = getUnit(state);
return getTokenBalance(state, unit);
}
};
export function getSelectedTokenContractAddress(state: AppState): string {
const allTokens = configSelectors.getAllTokens(state);
const currentUnit = getUnit(state);
if (configSelectors.isNetworkUnit(state, currentUnit)) {
return '';
}
return allTokens.reduce((tokenAddr, tokenInfo) => {
if (tokenAddr && tokenAddr.length) {
return tokenAddr;
}
if (tokenInfo.symbol === currentUnit) {
return tokenInfo.address;
}
return tokenAddr;
}, '');
}
export function getCurrentToLabel(state: AppState) {
const addresses = addressBookSelectors.getAddressLabels(state);
const currentTo = getCurrentTo(state);
return addresses[currentTo.raw.toLowerCase()] || null;
}