diff --git a/src/actions/SendFormActions.js b/src/actions/SendFormActions.js index 456458de..135f97e1 100644 --- a/src/actions/SendFormActions.js +++ b/src/actions/SendFormActions.js @@ -13,7 +13,13 @@ import * as RippleSendFormActions from './ripple/SendFormActions'; export type SendFormAction = | { - type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR, + type: + | typeof SEND.INIT + | typeof SEND.VALIDATION + | typeof SEND.CHANGE + | typeof SEND.CLEAR + | typeof SEND.DOMAIN_RESOLVING + | typeof SEND.DOMAIN_COMPLETE, networkType: 'ethereum', state: EthereumState, } diff --git a/src/actions/constants/dotCrypto.js b/src/actions/constants/dotCrypto.js new file mode 100644 index 00000000..9ee56125 --- /dev/null +++ b/src/actions/constants/dotCrypto.js @@ -0,0 +1,29 @@ +export const ProxyReader = { + address: '0x7ea9Ee21077F84339eDa9C80048ec6db678642B1', + abi: [ + { + constant: true, + inputs: [ + { internalType: 'string', name: 'key', type: 'string' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'get', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [ + { internalType: 'string[]', name: 'keys', type: 'string[]' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'getMany', + outputs: [{ internalType: 'string[]', name: '', type: 'string[]' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + ], +}; diff --git a/src/actions/constants/send.js b/src/actions/constants/send.js index 37549cf9..5dcc857b 100644 --- a/src/actions/constants/send.js +++ b/src/actions/constants/send.js @@ -8,3 +8,5 @@ export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete'; export const TX_ERROR: 'send__tx_error' = 'send__tx_error'; export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced'; export const CLEAR: 'send__clear' = 'send__clear'; +export const DOMAIN_RESOLVING: 'send__domain_resolving' = 'send__domain_resolving'; +export const DOMAIN_COMPLETE: 'send__domain_complete' = 'send__domain_complete'; diff --git a/src/actions/ethereum/BlockchainActions.js b/src/actions/ethereum/BlockchainActions.js index ac299de1..b6bce24a 100644 --- a/src/actions/ethereum/BlockchainActions.js +++ b/src/actions/ethereum/BlockchainActions.js @@ -6,7 +6,8 @@ import * as PENDING from 'actions/constants/pendingTx'; import * as AccountsActions from 'actions/AccountsActions'; import * as Web3Actions from 'actions/Web3Actions'; import { mergeAccount, enhanceTransaction } from 'utils/accountUtils'; - +import { namehash } from 'utils/ethUtils'; +import * as dotCrypto from 'actions/constants/dotCrypto'; import type { Dispatch, GetState, PromiseAction, Network } from 'flowtype'; import type { BlockchainNotification } from 'trezor-connect'; import type { Token } from 'reducers/TokensReducer'; @@ -143,3 +144,18 @@ export const onError = (network: string): PromiseAction => async ( ): Promise => { dispatch(Web3Actions.disconnect(network)); }; + +export const resolveDomain = (domain: string, ticker: string): PromiseAction => async ( + dispatch: Dispatch +): Promise => { + const instance = await dispatch(Web3Actions.initWeb3('eth')); + const ProxyReader = new instance.web3.eth.Contract( + dotCrypto.ProxyReader.abi, + dotCrypto.ProxyReader.address + ); + const node = namehash(domain); + return ProxyReader.methods + .get(`crypto.${ticker}.address`, node) + .call() + .catch(() => ''); +}; diff --git a/src/actions/ethereum/SendFormActions.js b/src/actions/ethereum/SendFormActions.js index f4b9c07d..a1b53b62 100644 --- a/src/actions/ethereum/SendFormActions.js +++ b/src/actions/ethereum/SendFormActions.js @@ -144,6 +144,9 @@ export const init = (): AsyncAction => async ( networkType: 'ethereum', state: stateFromStorage, }); + if (stateFromStorage.domainResolving) { + dispatch(onDomainChange(stateFromStorage.address)); + } return; } @@ -233,26 +236,67 @@ export const onClear = (): AsyncAction => async ( }); }; -/* - * Called from UI on "address" field change - */ -export const onAddressChange = (address: string): ThunkAction => ( +export const onDomainChange = (domain: string): AsyncAction => async ( dispatch: Dispatch, getState: GetState -): void => { - const state: State = getState().sendFormEthereum; +): Promise => { + const state = getState().sendFormEthereum; + dispatch({ - type: SEND.CHANGE, + type: SEND.DOMAIN_RESOLVING, + networkType: 'ethereum', + state: { + ...state, + address: domain, + resolvedDomain: '', + domainResolving: true, + }, + }); + + const address = await dispatch(BlockchainActions.resolveDomain(domain, 'ETH')); + dispatch({ + type: SEND.DOMAIN_COMPLETE, networkType: 'ethereum', state: { ...state, untouched: false, touched: { ...state.touched, address: true }, - address, + address: address || domain, + resolvedDomain: address ? domain : '', + domainResolving: false, }, }); }; +/* + * Called from UI on "address" field change + */ +export const onAddressChange = (address: string): ThunkAction => ( + dispatch: Dispatch, + getState: GetState +): void => { + const state: State = getState().sendFormEthereum; + + if (state.domainResolving) { + return; + } + if (address.endsWith('.crypto')) { + dispatch(onDomainChange(address)); + } else { + dispatch({ + type: SEND.CHANGE, + networkType: 'ethereum', + state: { + ...state, + untouched: false, + touched: { ...state.touched, address: true }, + address, + domainResolving: false, + }, + }); + } +}; + /* * Called from UI on "amount" field change */ diff --git a/src/actions/ethereum/SendFormValidationActions.js b/src/actions/ethereum/SendFormValidationActions.js index 875a23a0..799fc5ee 100644 --- a/src/actions/ethereum/SendFormValidationActions.js +++ b/src/actions/ethereum/SendFormValidationActions.js @@ -81,8 +81,10 @@ export const validation = (): PayloadAction => ( state.errors = {}; state.warnings = {}; state.infos = {}; + state = dispatch(recalculateTotalAmount(state)); state = dispatch(updateCustomFeeLabel(state)); + state = dispatch(domainResolution(state)); state = dispatch(addressValidation(state)); state = dispatch(addressLabel(state)); state = dispatch(amountValidation(state)); @@ -164,6 +166,32 @@ export const addressValidation = ($state: State): PayloadAction => (): St return state; }; +/* + * Domain resolving + */ +export const domainResolution = ($state: State): PayloadAction => (): State => { + const state = { ...$state }; + + if (state.domainResolving) { + state.warnings.address = { + ...l10nCommonMessages.TR_DOMAIN_RESOLVING, + values: { + domain: state.address, + }, + }; + } + if (state.resolvedDomain) { + state.infos.address = { + ...l10nCommonMessages.TR_DOMAIN_COMPLETE, + values: { + domain: state.resolvedDomain, + address: state.address, + }, + }; + } + return state; +}; + /* * Address label assignation */ diff --git a/src/reducers/SendFormEthereumReducer.js b/src/reducers/SendFormEthereumReducer.js index 78bdb322..fa01d9c7 100644 --- a/src/reducers/SendFormEthereumReducer.js +++ b/src/reducers/SendFormEthereumReducer.js @@ -42,6 +42,8 @@ export type State = { infos: { [k: string]: MessageDescriptor }, sending: boolean, + domainResolving: boolean, + resolvedDomain: string, }; export const initialState: State = { @@ -74,6 +76,8 @@ export const initialState: State = { nonce: '0', total: '0', sending: false, + domainResolving: false, + resolvedDomain: '', errors: {}, warnings: {}, infos: {}, @@ -87,6 +91,8 @@ export default (state: State = initialState, action: Action): State => { case SEND.INIT: case SEND.CHANGE: case SEND.VALIDATION: + case SEND.DOMAIN_RESOLVING: + case SEND.DOMAIN_COMPLETE: case SEND.CLEAR: return action.state; diff --git a/src/utils/ethUtils.js b/src/utils/ethUtils.js index 0f8ba01a..66b40d3e 100644 --- a/src/utils/ethUtils.js +++ b/src/utils/ethUtils.js @@ -3,6 +3,16 @@ import BigNumber from 'bignumber.js'; import EthereumjsUtil from 'ethereumjs-util'; +export const namehash = (domain: string): string => { + if (!domain) { + return '0x0000000000000000000000000000000000000000000000000000000000000000'; + } + const [label, ...remainder] = domain.split('.'); + return `0x${EthereumjsUtil.keccak256( + namehash(remainder.join('.')) + EthereumjsUtil.keccak256(label).toString('hex') + ).toString('hex')}`; +}; + export const decimalToHex = (dec: number): string => new BigNumber(dec).toString(16); export const padLeftEven = (hex: string): string => (hex.length % 2 !== 0 ? `0${hex}` : hex); diff --git a/src/views/Wallet/views/Account/Send/ethereum/index.js b/src/views/Wallet/views/Account/Send/ethereum/index.js index 7d0ff924..e59b71c2 100644 --- a/src/views/Wallet/views/Account/Send/ethereum/index.js +++ b/src/views/Wallet/views/Account/Send/ethereum/index.js @@ -206,13 +206,14 @@ const StyledIcon = styled(Icon)` const getAddressInputState = ( address: string, addressErrors: boolean, - addressWarnings: boolean + addressWarnings: boolean, + domainResolving: boolean ): ?string => { let state = null; if (address && !addressErrors) { state = 'success'; } - if (addressWarnings && !addressErrors) { + if ((addressWarnings && !addressErrors) || domainResolving) { state = 'warning'; } if (addressErrors) { @@ -270,6 +271,7 @@ const AccountSend = (props: Props) => { infos, sending, advanced, + domainResolving, } = props.sendForm; const { @@ -314,6 +316,7 @@ const AccountSend = (props: Props) => { amount.length === 0 || address.length === 0 || sending || + domainResolving || account.imported; let amountText = ''; @@ -348,7 +351,12 @@ const AccountSend = (props: Props) => {