|
|
|
import { createSlice } from '@reduxjs/toolkit';
|
|
|
|
import ENS from 'ethjs-ens';
|
|
|
|
import log from 'loglevel';
|
|
|
|
import networkMap from 'ethereum-ens-network-map';
|
|
|
|
import { isConfusing } from 'unicode-confusables';
|
|
|
|
import { isHexString } from 'ethereumjs-util';
|
|
|
|
|
|
|
|
import { getCurrentChainId } from '../selectors';
|
|
|
|
import {
|
|
|
|
CHAIN_ID_TO_NETWORK_ID_MAP,
|
|
|
|
MAINNET_NETWORK_ID,
|
|
|
|
} from '../../shared/constants/network';
|
|
|
|
import {
|
|
|
|
CONFUSING_ENS_ERROR,
|
|
|
|
ENS_ILLEGAL_CHARACTER,
|
|
|
|
ENS_NOT_FOUND_ON_NETWORK,
|
|
|
|
ENS_NOT_SUPPORTED_ON_NETWORK,
|
|
|
|
ENS_NO_ADDRESS_FOR_NAME,
|
|
|
|
ENS_REGISTRATION_ERROR,
|
|
|
|
ENS_UNKNOWN_ERROR,
|
|
|
|
} from '../pages/send/send.constants';
|
|
|
|
import { isValidDomainName } from '../helpers/utils/util';
|
|
|
|
import { CHAIN_CHANGED } from '../store/actionConstants';
|
|
|
|
import {
|
|
|
|
BURN_ADDRESS,
|
|
|
|
isBurnAddress,
|
|
|
|
isValidHexAddress,
|
|
|
|
} from '../../shared/modules/hexstring-utils';
|
|
|
|
|
|
|
|
// Local Constants
|
|
|
|
const ZERO_X_ERROR_ADDRESS = '0x';
|
|
|
|
|
|
|
|
const initialState = {
|
|
|
|
stage: 'UNINITIALIZED',
|
|
|
|
resolution: null,
|
|
|
|
error: null,
|
|
|
|
warning: null,
|
|
|
|
network: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
export const ensInitialState = initialState;
|
|
|
|
|
|
|
|
const name = 'ENS';
|
|
|
|
|
|
|
|
let ens = null;
|
|
|
|
|
|
|
|
const slice = createSlice({
|
|
|
|
name,
|
|
|
|
initialState,
|
|
|
|
reducers: {
|
|
|
|
ensLookup: (state, action) => {
|
|
|
|
// first clear out the previous state
|
|
|
|
state.resolution = null;
|
|
|
|
state.error = null;
|
|
|
|
state.warning = null;
|
|
|
|
const { address, ensName, error, network } = action.payload;
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
if (
|
|
|
|
isValidDomainName(ensName) &&
|
|
|
|
error.message === 'ENS name not defined.'
|
|
|
|
) {
|
|
|
|
state.error =
|
|
|
|
network === MAINNET_NETWORK_ID
|
|
|
|
? ENS_NO_ADDRESS_FOR_NAME
|
|
|
|
: ENS_NOT_FOUND_ON_NETWORK;
|
|
|
|
} else if (error.message === 'Illegal Character for ENS.') {
|
|
|
|
state.error = ENS_ILLEGAL_CHARACTER;
|
|
|
|
} else {
|
|
|
|
log.error(error);
|
|
|
|
state.error = ENS_UNKNOWN_ERROR;
|
|
|
|
}
|
|
|
|
} else if (address) {
|
|
|
|
if (address === BURN_ADDRESS) {
|
|
|
|
state.error = ENS_NO_ADDRESS_FOR_NAME;
|
|
|
|
} else if (address === ZERO_X_ERROR_ADDRESS) {
|
|
|
|
state.error = ENS_REGISTRATION_ERROR;
|
|
|
|
} else {
|
|
|
|
state.resolution = address;
|
|
|
|
}
|
|
|
|
if (isValidDomainName(address) && isConfusing(address)) {
|
|
|
|
state.warning = CONFUSING_ENS_ERROR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
enableEnsLookup: (state, action) => {
|
|
|
|
state.stage = 'INITIALIZED';
|
|
|
|
state.error = null;
|
|
|
|
state.resolution = null;
|
|
|
|
state.warning = null;
|
|
|
|
state.network = action.payload;
|
|
|
|
},
|
|
|
|
disableEnsLookup: (state) => {
|
|
|
|
state.stage = 'NO_NETWORK_SUPPORT';
|
|
|
|
state.error = null;
|
|
|
|
state.warning = null;
|
|
|
|
state.resolution = null;
|
|
|
|
state.network = null;
|
|
|
|
},
|
|
|
|
ensNotSupported: (state) => {
|
|
|
|
state.resolution = null;
|
|
|
|
state.warning = null;
|
|
|
|
state.error = ENS_NOT_SUPPORTED_ON_NETWORK;
|
|
|
|
},
|
|
|
|
resetEnsResolution: (state) => {
|
|
|
|
state.resolution = null;
|
|
|
|
state.warning = null;
|
|
|
|
state.error = null;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
extraReducers: (builder) => {
|
|
|
|
builder.addCase(CHAIN_CHANGED, (state, action) => {
|
|
|
|
if (action.payload !== state.currentChainId) {
|
|
|
|
state.stage = 'UNINITIALIZED';
|
|
|
|
ens = null;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const { reducer, actions } = slice;
|
|
|
|
export default reducer;
|
|
|
|
|
|
|
|
const {
|
|
|
|
disableEnsLookup,
|
|
|
|
ensLookup,
|
|
|
|
enableEnsLookup,
|
|
|
|
ensNotSupported,
|
|
|
|
resetEnsResolution,
|
|
|
|
} = actions;
|
|
|
|
export { resetEnsResolution };
|
|
|
|
|
|
|
|
export function initializeEnsSlice() {
|
|
|
|
return (dispatch, getState) => {
|
|
|
|
const state = getState();
|
|
|
|
const chainId = getCurrentChainId(state);
|
|
|
|
const network = CHAIN_ID_TO_NETWORK_ID_MAP[chainId];
|
|
|
|
const networkIsSupported = Boolean(networkMap[network]);
|
|
|
|
if (networkIsSupported) {
|
|
|
|
ens = new ENS({ provider: global.ethereumProvider, network });
|
|
|
|
dispatch(enableEnsLookup(network));
|
|
|
|
} else {
|
|
|
|
ens = null;
|
|
|
|
dispatch(disableEnsLookup());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function lookupEnsName(ensName) {
|
|
|
|
return async (dispatch, getState) => {
|
|
|
|
const trimmedEnsName = ensName.trim();
|
|
|
|
let state = getState();
|
|
|
|
if (state[name].stage === 'UNINITIALIZED') {
|
|
|
|
await dispatch(initializeEnsSlice());
|
|
|
|
}
|
|
|
|
state = getState();
|
|
|
|
if (
|
|
|
|
state[name].stage === 'NO_NETWORK_SUPPORT' &&
|
|
|
|
!(
|
|
|
|
isBurnAddress(trimmedEnsName) === false &&
|
|
|
|
isValidHexAddress(trimmedEnsName, { mixedCaseUseChecksum: true })
|
|
|
|
) &&
|
|
|
|
!isHexString(trimmedEnsName)
|
|
|
|
) {
|
|
|
|
await dispatch(ensNotSupported());
|
|
|
|
} else {
|
|
|
|
log.info(`ENS attempting to resolve name: ${trimmedEnsName}`);
|
|
|
|
let address;
|
|
|
|
let error;
|
|
|
|
try {
|
|
|
|
address = await ens.lookup(trimmedEnsName);
|
|
|
|
} catch (err) {
|
|
|
|
error = err;
|
|
|
|
}
|
|
|
|
const chainId = getCurrentChainId(state);
|
|
|
|
const network = CHAIN_ID_TO_NETWORK_ID_MAP[chainId];
|
|
|
|
await dispatch(
|
|
|
|
ensLookup({
|
|
|
|
ensName: trimmedEnsName,
|
|
|
|
address,
|
|
|
|
error,
|
|
|
|
chainId,
|
|
|
|
network,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getEnsResolution(state) {
|
|
|
|
return state[name].resolution;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getEnsError(state) {
|
|
|
|
return state[name].error;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getEnsWarning(state) {
|
|
|
|
return state[name].warning;
|
|
|
|
}
|