/* @flow */ import Web3 from 'web3'; import BigNumber from 'bignumber.js'; import EthereumjsUnits from 'ethereumjs-units'; // import InputDataDecoder from 'ethereum-input-data-decoder'; import type { EstimateGasOptions } from 'web3'; import * as WEB3 from 'actions/constants/web3'; import * as PENDING from 'actions/constants/pendingTx'; import * as ethUtils from 'utils/ethUtils'; import type { Dispatch, GetState, ThunkAction, PromiseAction } from 'flowtype'; import type { Account } from 'reducers/AccountsReducer'; import type { Web3Instance } from 'reducers/Web3Reducer'; import type { Token } from 'reducers/TokensReducer'; import type { NetworkToken } from 'reducers/LocalStorageReducer'; import * as TokenActions from './TokenActions'; export type Web3UpdateBlockAction = { type: typeof WEB3.BLOCK_UPDATED, network: string, blockHash: string, }; export type Web3UpdateGasPriceAction = { type: typeof WEB3.GAS_PRICE_UPDATED, network: string, gasPrice: string, }; export type Web3Action = | { type: typeof WEB3.READY, } | { type: typeof WEB3.START, } | { type: typeof WEB3.CREATE | typeof WEB3.DISCONNECT, instance: Web3Instance, } | Web3UpdateBlockAction | Web3UpdateGasPriceAction; export const initWeb3 = ( network: string, urlIndex: number = 0 ): PromiseAction => async ( dispatch: Dispatch, getState: GetState ): Promise => new Promise(async (resolve, reject) => { // check if requested web was initialized before const instance = getState().web3.find(w3 => w3.network === network); if (instance && instance.web3.currentProvider.connected) { resolve(instance); return; } // requested web3 wasn't initialized or is disconnected // initialize again const { config, ERC20Abi } = getState().localStorage; const networkData = config.networks.find(c => c.shortcut === network); if (!networkData) { // network not found reject(new Error(`Network ${network} not found in application config.`)); return; } // get first url const url = networkData.web3[urlIndex]; if (!url) { reject(new Error('Web3 backend is not responding')); return; } const web3 = new Web3(new Web3.providers.WebsocketProvider(url)); const onConnect = async () => { const latestBlock = await web3.eth.getBlockNumber(); const gasPrice = await web3.eth.getGasPrice(); const newInstance = { network, web3, chainId: networkData.chainId, erc20: new web3.eth.Contract(ERC20Abi), latestBlock, gasPrice, }; dispatch({ type: WEB3.CREATE, instance: newInstance, }); resolve(newInstance); }; const onEnd = async () => { web3.currentProvider.reset(); const oldInstance = getState().web3.find(w3 => w3.network === network); if (oldInstance && oldInstance.web3.currentProvider.connected) { // backend disconnects // dispatch({ // type: 'WEB3.DISCONNECT', // network // }); } else { // backend initialization error for given url, try next one try { const otherWeb3 = await dispatch(initWeb3(network, urlIndex + 1)); resolve(otherWeb3); } catch (error) { console.error(error); reject(error); } } }; web3.currentProvider.on('connect', onConnect); web3.currentProvider.on('end', onEnd); web3.currentProvider.on('error', onEnd); }); // not used since connect@8 // export const discoverAccount = (descriptor: string, network: string): PromiseAction => async ( // dispatch: Dispatch // ): Promise => { // const instance: Web3Instance = await dispatch(initWeb3(network)); // const balance = await instance.web3.eth.getBalance(descriptor); // const nonce = await instance.web3.eth.getTransactionCount(descriptor); // return { // descriptor, // transactions: 0, // block: 0, // balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), // availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), // nonce, // }; // }; export const resolvePendingTransactions = (network: string): PromiseAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { const instance: Web3Instance = await dispatch(initWeb3(network)); const pending = getState().pending.filter(p => p.network === network); pending.forEach(async tx => { const status = await instance.web3.eth.getTransaction(tx.txid); if (!status) { dispatch({ type: PENDING.TX_REJECTED, hash: tx.txid, }); } else { const receipt = await instance.web3.eth.getTransactionReceipt(tx.txid); if (receipt) { if (status.gas !== receipt.gasUsed) { dispatch({ type: PENDING.TX_TOKEN_ERROR, hash: tx.txid, }); } dispatch({ type: PENDING.TX_RESOLVED, hash: tx.txid, }); } } }); }; /* export const getPendingInfo = (network: string, txid: string): PromiseAction => async (dispatch: Dispatch): Promise => { const instance: Web3Instance = await dispatch(initWeb3(network)); const tx = await instance.web3.eth.getTransaction(txid); if (tx.input !== "0x") { // find token: // tx.to <= smart contract address // smart contract data const decoder = new InputDataDecoder(instance.erc20.options.jsonInterface); const data = decoder.decodeData(tx.input); if (data.name === 'transfer') { console.warn("DATA!", data.inputs[0], data.inputs[1].toString(10)); } } // return tx; }; export const getTxInput = (): PromiseAction => async (dispatch: Dispatch): Promise => { const instance: Web3Instance = await dispatch(initWeb3('ropsten')); // const inputData = instance.web3.utils.hexToAscii("0xa9059cbb00000000000000000000000073d0385f4d8e00c5e6504c6030f47bf6212736a80000000000000000000000000000000000000000000000000000000000000001"); // console.warn("input data!", inputData); }; */ // not used since connect@8 // export const updateAccount = ( // account: Account, // newAccount: any, // network: string // ): PromiseAction => async (dispatch: Dispatch): Promise => { // const instance: Web3Instance = await dispatch(initWeb3(network)); // const balance = await instance.web3.eth.getBalance(account.descriptor); // const nonce = await instance.web3.eth.getTransactionCount(account.descriptor); // const empty = nonce <= 0 && balance === '0'; // dispatch( // AccountsActions.update({ // networkType: 'ethereum', // ...account, // ...newAccount, // empty, // nonce, // balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), // availableBalance: EthereumjsUnits.convert(balance, 'wei', 'ether'), // }) // ); // // update tokens for this account // dispatch(updateAccountTokens(account)); // }; export const updateAccountTokens = (account: Account): PromiseAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { const tokens = getState().tokens.filter( t => t.network === account.network && t.ethAddress === account.descriptor ); tokens.forEach(async token => { const balance = await dispatch(getTokenBalance(token)); // const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10); if (balance !== token.balance) { dispatch(TokenActions.setBalance(token.address, token.ethAddress, balance)); } }); }; export const getTokenInfo = ( address: string, network: string ): PromiseAction => async (dispatch: Dispatch): Promise => { const instance: Web3Instance = await dispatch(initWeb3(network)); const contract = instance.erc20.clone(); contract.options.address = address; const name = await contract.methods.name().call(); const symbol = await contract.methods.symbol().call(); const decimals = await contract.methods.decimals().call(); return { address, name, symbol, decimals, }; }; export const getTokenBalance = (token: Token): PromiseAction => async ( dispatch: Dispatch ): Promise => { const instance = await dispatch(initWeb3(token.network)); const contract = instance.erc20.clone(); contract.options.address = token.address; const balance = await contract.methods.balanceOf(token.ethAddress).call(); return new BigNumber(balance).dividedBy(10 ** token.decimals).toString(10); }; export const getCurrentGasPrice = (network: string): PromiseAction => async ( dispatch: Dispatch, getState: GetState ): Promise => { const instance = getState().web3.find(w3 => w3.network === network); if (instance) { return EthereumjsUnits.convert(instance.gasPrice, 'wei', 'gwei'); } return '0'; }; export const updateGasPrice = (network: string): PromiseAction => async ( dispatch: Dispatch ): Promise => { try { const instance = await dispatch(initWeb3(network)); const gasPrice = await instance.web3.eth.getGasPrice(); if (instance.gasPrice !== gasPrice) { dispatch({ type: WEB3.GAS_PRICE_UPDATED, network, gasPrice, }); } } catch (e) { // silent action // nothing happens if this fails } }; export const estimateGasLimit = ( network: string, $options: EstimateGasOptions ): PromiseAction => async (dispatch: Dispatch): Promise => { const instance = await dispatch(initWeb3(network)); const data = ethUtils.sanitizeHex($options.data); const options = { ...$options, to: '0x0000000000000000000000000000000000000000', data, value: instance.web3.utils.toHex( EthereumjsUnits.convert($options.value || '0', 'ether', 'wei') ), gasPrice: instance.web3.utils.toHex( EthereumjsUnits.convert($options.gasPrice, 'gwei', 'wei') ), }; const limit = await instance.web3.eth.estimateGas(options); return limit.toString(); }; export const disconnect = (network: string): ThunkAction => ( dispatch: Dispatch, getState: GetState ): void => { // check if Web3 was already initialized const instance = getState().web3.find(w3 => w3.network === network); if (instance) { // reset current connection instance.web3.currentProvider.reset(); instance.web3.currentProvider.connection.close(); // remove instance from reducer dispatch({ type: WEB3.DISCONNECT, instance, }); } };