Auto token add (#1808)
* Add support for decimal and symbol getters * Make custom token form interactive via address lookup * Add balance field, improve error handling * Fix lint errors * Fix erc20 interface * Expand method name * Normalize parameter name * Remove extra variable * Stricten typing for decimals * Use common input field between decimal and symbol fields * use mycrypto-nano-result
This commit is contained in:
parent
cd26970d7f
commit
41f8ab8966
|
@ -1,166 +0,0 @@
|
|||
import React from 'react';
|
||||
import { HELP_ARTICLE } from 'config';
|
||||
import { isPositiveIntegerOrZero, isValidETHAddress } from 'libs/validators';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { HelpLink, Input } from 'components/ui';
|
||||
import './AddCustomTokenForm.scss';
|
||||
import { Token } from 'types/network';
|
||||
|
||||
interface Props {
|
||||
allTokens: Token[];
|
||||
onSave(params: Token): void;
|
||||
toggleForm(): void;
|
||||
}
|
||||
|
||||
interface IGenerateSymbolLookup {
|
||||
[tokenSymbol: string]: boolean;
|
||||
}
|
||||
|
||||
interface IGenerateAddressLookup {
|
||||
[address: string]: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
tokenSymbolLookup: IGenerateSymbolLookup;
|
||||
tokenAddressLookup: IGenerateAddressLookup;
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimal: string;
|
||||
}
|
||||
|
||||
export default class AddCustomTokenForm extends React.PureComponent<Props, State> {
|
||||
public state: State = {
|
||||
tokenSymbolLookup: this.generateSymbolLookup(),
|
||||
tokenAddressLookup: this.generateAddressMap(),
|
||||
address: '',
|
||||
symbol: '',
|
||||
decimal: ''
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { address, symbol, decimal } = this.state;
|
||||
const errors = this.getErrors();
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'symbol',
|
||||
value: symbol,
|
||||
label: translateRaw('TOKEN_SYMBOL')
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
value: address,
|
||||
label: translateRaw('TOKEN_ADDR')
|
||||
},
|
||||
{
|
||||
name: 'decimal',
|
||||
value: decimal,
|
||||
label: translateRaw('TOKEN_DEC')
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<form className="AddCustom" onSubmit={this.onSave}>
|
||||
{fields.map(field => {
|
||||
return (
|
||||
<label className="AddCustom-field form-group" key={field.name}>
|
||||
<div className="input-group-header">{field.label}</div>
|
||||
<Input
|
||||
isValid={!errors[field.name]}
|
||||
className="input-group-input-small"
|
||||
type="text"
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onChange={this.onFieldChange}
|
||||
/>
|
||||
{errors[field.name] && (
|
||||
<div className="AddCustom-field-error">{errors[field.name]}</div>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
|
||||
<HelpLink article={HELP_ARTICLE.ADDING_NEW_TOKENS} className="AddCustom-buttons-help">
|
||||
{translate('ADD_CUSTOM_TKN_HELP')}
|
||||
</HelpLink>
|
||||
<div className="AddCustom-buttons">
|
||||
<button
|
||||
className="AddCustom-buttons-btn btn btn-sm btn-default"
|
||||
onClick={this.props.toggleForm}
|
||||
>
|
||||
{translate('ACTION_2')}
|
||||
</button>
|
||||
<button
|
||||
className="AddCustom-buttons-btn btn btn-primary btn-sm"
|
||||
disabled={!this.isValid()}
|
||||
>
|
||||
{translate('X_SAVE')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
public getErrors() {
|
||||
const { address, symbol, decimal } = this.state;
|
||||
const errors: { [key: string]: string } = {};
|
||||
|
||||
// Formatting errors
|
||||
if (decimal && !isPositiveIntegerOrZero(Number(decimal))) {
|
||||
errors.decimal = 'Invalid decimal';
|
||||
}
|
||||
if (address) {
|
||||
if (!isValidETHAddress(address)) {
|
||||
errors.address = 'Not a valid address';
|
||||
}
|
||||
if (this.state.tokenAddressLookup[address]) {
|
||||
errors.address = 'A token with this address already exists';
|
||||
}
|
||||
}
|
||||
|
||||
// Message errors
|
||||
if (symbol && this.state.tokenSymbolLookup[symbol]) {
|
||||
errors.symbol = 'A token with this symbol already exists';
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public isValid() {
|
||||
const { address, symbol, decimal } = this.state;
|
||||
return !Object.keys(this.getErrors()).length && address && symbol && decimal;
|
||||
}
|
||||
|
||||
public onFieldChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
// TODO: typescript bug: https://github.com/Microsoft/TypeScript/issues/13948
|
||||
const name: any = e.currentTarget.name;
|
||||
const value = e.currentTarget.value;
|
||||
this.setState({ [name]: value });
|
||||
};
|
||||
|
||||
public onSave = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
if (!this.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { address, symbol, decimal } = this.state;
|
||||
this.props.onSave({ address, symbol, decimal: parseInt(decimal, 10) });
|
||||
};
|
||||
|
||||
private generateSymbolLookup() {
|
||||
return this.tknArrToMap('symbol');
|
||||
}
|
||||
|
||||
private generateAddressMap() {
|
||||
return this.tknArrToMap('address');
|
||||
}
|
||||
|
||||
private tknArrToMap(key: Exclude<keyof Token, 'error'>) {
|
||||
const tokens = this.props.allTokens;
|
||||
return tokens.reduce<{ [k: string]: boolean }>((prev, tk) => {
|
||||
prev[tk[key]] = true;
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import React from 'react';
|
||||
import { HELP_ARTICLE } from 'config';
|
||||
import { AddressField } from './AddressField';
|
||||
import { DecimalField } from './DecimalField';
|
||||
import { SymbolField } from './SymbolField';
|
||||
import { BalanceField } from './BalanceField';
|
||||
import translate from 'translations';
|
||||
import { HelpLink } from 'components/ui';
|
||||
import './AddCustomTokenForm.scss';
|
||||
import { Token } from 'types/network';
|
||||
import { Result } from 'mycrypto-nano-result';
|
||||
|
||||
interface Props {
|
||||
allTokens: Token[];
|
||||
onSave(params: Token): void;
|
||||
toggleForm(): void;
|
||||
}
|
||||
|
||||
export interface IGenerateSymbolLookup {
|
||||
[tokenSymbol: string]: boolean;
|
||||
}
|
||||
|
||||
export interface IGenerateAddressLookup {
|
||||
[address: string]: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
address: Result<string>;
|
||||
symbol: Result<string>;
|
||||
decimal: Result<string>;
|
||||
}
|
||||
|
||||
export class AddCustomTokenForm extends React.PureComponent<Props, State> {
|
||||
public state: State = {
|
||||
address: Result.from({ err: 'This field is empty' }),
|
||||
symbol: Result.from({ err: 'This field is empty' }),
|
||||
decimal: Result.from({ err: 'This field is empty' })
|
||||
};
|
||||
|
||||
private tokenSymbolLookup = this.generateSymbolLookup();
|
||||
private tokenAddressLookup = this.generateAddressMap();
|
||||
|
||||
public render() {
|
||||
const address = this.state.address.toVal().res;
|
||||
|
||||
return (
|
||||
<form className="AddCustom" onSubmit={this.onSave}>
|
||||
<AddressField
|
||||
addressLookup={this.tokenAddressLookup}
|
||||
onChange={this.handleFieldChange('address')}
|
||||
/>
|
||||
<DecimalField address={address} onChange={this.handleFieldChange('decimal')} />
|
||||
<SymbolField
|
||||
address={address}
|
||||
symbolLookup={this.tokenSymbolLookup}
|
||||
onChange={this.handleFieldChange('symbol')}
|
||||
/>
|
||||
<BalanceField address={address} />
|
||||
<HelpLink article={HELP_ARTICLE.ADDING_NEW_TOKENS} className="AddCustom-buttons-help">
|
||||
{translate('ADD_CUSTOM_TKN_HELP')}
|
||||
</HelpLink>
|
||||
<div className="AddCustom-buttons">
|
||||
<button
|
||||
className="AddCustom-buttons-btn btn btn-sm btn-default"
|
||||
onClick={this.props.toggleForm}
|
||||
>
|
||||
{translate('ACTION_2')}
|
||||
</button>
|
||||
<button
|
||||
className="AddCustom-buttons-btn btn btn-primary btn-sm"
|
||||
disabled={!this.isValid()}
|
||||
>
|
||||
{translate('X_SAVE')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
public onSave = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
if (!this.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { address, symbol, decimal } = this.state;
|
||||
this.props.onSave({
|
||||
address: address.unwrap(),
|
||||
symbol: symbol.unwrap(),
|
||||
decimal: parseInt(decimal.unwrap(), 10)
|
||||
});
|
||||
};
|
||||
|
||||
private handleFieldChange = (fieldName: keyof State) => (res: Result<string>) => {
|
||||
this.setState({ [fieldName as any]: res });
|
||||
};
|
||||
|
||||
private isValid() {
|
||||
const { address, decimal, symbol } = this.state;
|
||||
const valid = address.ok() && decimal.ok() && symbol.ok();
|
||||
return valid;
|
||||
}
|
||||
|
||||
private generateSymbolLookup() {
|
||||
return this.tokenArrayToMap('symbol');
|
||||
}
|
||||
|
||||
private generateAddressMap() {
|
||||
return this.tokenArrayToMap('address');
|
||||
}
|
||||
|
||||
private tokenArrayToMap(key: Exclude<keyof Token, 'error'>) {
|
||||
const tokens = this.props.allTokens;
|
||||
return tokens.reduce<{ [k: string]: boolean }>((prev, tk) => {
|
||||
prev[tk[key]] = true;
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { Input } from 'components/ui';
|
||||
import { translateRaw } from 'translations';
|
||||
import { IGenerateAddressLookup } from './AddCustomTokenForm';
|
||||
import { isValidETHAddress } from 'libs/validators';
|
||||
import { Result } from 'mycrypto-nano-result';
|
||||
|
||||
interface OwnProps {
|
||||
addressLookup: IGenerateAddressLookup;
|
||||
onChange(address: Result<string>): void;
|
||||
}
|
||||
|
||||
enum ErrType {
|
||||
INVALIDADDR = 'Not a valid address',
|
||||
ADDRTAKEN = 'A token with this address already exists'
|
||||
}
|
||||
|
||||
interface State {
|
||||
address: Result<string>;
|
||||
userInput: string;
|
||||
}
|
||||
|
||||
export class AddressField extends React.Component<OwnProps, State> {
|
||||
public state: State = {
|
||||
address: Result.from({ res: '' }),
|
||||
userInput: ''
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { userInput, address } = this.state;
|
||||
|
||||
return (
|
||||
<label className="AddCustom-field form-group">
|
||||
<div className="input-group-header">{translateRaw('TOKEN_ADDR')}</div>
|
||||
<Input
|
||||
isValid={address.ok()}
|
||||
className="input-group-input-small"
|
||||
type="text"
|
||||
name="Address"
|
||||
value={address.ok() ? address.unwrap() : userInput}
|
||||
onChange={this.handleFieldChange}
|
||||
/>
|
||||
{address.err() && <div className="AddCustom-field-error">{address.err()}</div>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
private handleFieldChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const userInput = e.currentTarget.value;
|
||||
const addrTaken = this.props.addressLookup[userInput];
|
||||
const validAddr = isValidETHAddress(userInput);
|
||||
const err = addrTaken ? ErrType.ADDRTAKEN : !validAddr ? ErrType.INVALIDADDR : undefined;
|
||||
const address: Result<string> = err ? Result.from({ err }) : Result.from({ res: userInput });
|
||||
|
||||
this.setState({ userInput, address });
|
||||
this.props.onChange(address);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react';
|
||||
import { Input } from 'components/ui';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import ERC20 from 'libs/erc20';
|
||||
import { shepherdProvider } from 'libs/nodes';
|
||||
import { Result } from 'mycrypto-nano-result';
|
||||
import { getWalletInst } from 'selectors/wallet';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
interface OwnProps {
|
||||
address?: string;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
walletInst: ReturnType<typeof getWalletInst>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
balance: Result<string>;
|
||||
addressToLoad?: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps;
|
||||
|
||||
class BalanceFieldClass extends React.Component<Props, State> {
|
||||
public static getDerivedStateFromProps(
|
||||
nextProps: OwnProps,
|
||||
prevState: State
|
||||
): Partial<State> | null {
|
||||
if (nextProps.address && nextProps.address !== prevState.addressToLoad) {
|
||||
return { loading: true, addressToLoad: nextProps.address };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public state: State = {
|
||||
balance: Result.from({ res: '' }),
|
||||
loading: false
|
||||
};
|
||||
|
||||
private currentRequest: Promise<any> | null;
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.state.addressToLoad && this.state.loading) {
|
||||
this.attemptToLoadBalance(this.state.addressToLoad);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.currentRequest) {
|
||||
this.currentRequest = null;
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
const { balance, loading } = this.state;
|
||||
|
||||
return (
|
||||
<label className="AddCustom-field form-group">
|
||||
<div className="input-group-header">Balance</div>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Input
|
||||
isValid={balance.ok()}
|
||||
className="input-group-input-small"
|
||||
type="text"
|
||||
name="Balance"
|
||||
readOnly={true}
|
||||
value={balance.ok() ? balance.unwrap() : '0'}
|
||||
/>
|
||||
)}
|
||||
{balance.err() && <div className="AddCustom-field-error">{balance.err()}</div>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
private attemptToLoadBalance(address: string) {
|
||||
// process request
|
||||
this.currentRequest = this.loadBalance(address)
|
||||
// set state on successful request e.g it was not cancelled
|
||||
// and then also set our current request to null
|
||||
.then(({ balance }) =>
|
||||
this.setState({
|
||||
balance,
|
||||
loading: false
|
||||
})
|
||||
)
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
// if the component is unmounted, then dont call set state
|
||||
if (!this.currentRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise it was a failed fetch call
|
||||
this.setState({ loading: false });
|
||||
})
|
||||
.then(() => (this.currentRequest = null));
|
||||
}
|
||||
|
||||
private loadBalance(address: string) {
|
||||
if (!this.props.walletInst) {
|
||||
return Promise.reject('No wallet found');
|
||||
}
|
||||
|
||||
const owner = this.props.walletInst.getAddressString();
|
||||
return shepherdProvider
|
||||
.sendCallRequest({ data: ERC20.balanceOf.encodeInput({ _owner: owner }), to: address })
|
||||
.then(ERC20.balanceOf.decodeOutput)
|
||||
.then(({ balance }) => {
|
||||
const result = Result.from({ res: balance });
|
||||
return { balance: result };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: AppState): StateProps {
|
||||
return { walletInst: getWalletInst(state) };
|
||||
}
|
||||
|
||||
export const BalanceField = connect(mapStateToProps)(BalanceFieldClass);
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import { isPositiveIntegerOrZero } from 'libs/validators';
|
||||
import { translateRaw } from 'translations';
|
||||
import { Result } from 'mycrypto-nano-result';
|
||||
|
||||
import { FieldInput } from './FieldInput';
|
||||
|
||||
interface OwnProps {
|
||||
address?: string;
|
||||
onChange(decimals: Result<string>): void;
|
||||
}
|
||||
|
||||
export class DecimalField extends React.Component<OwnProps> {
|
||||
public render() {
|
||||
return (
|
||||
<FieldInput
|
||||
fieldName={translateRaw('TOKEN_DEC')}
|
||||
fieldToFetch={'decimals'}
|
||||
shouldEnableAutoField={req => !(req.toVal().res === '0')}
|
||||
address={this.props.address}
|
||||
userInputValidator={this.isValidUserInput}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private isValidUserInput = (userInput: string) => {
|
||||
const validDecimals = isPositiveIntegerOrZero(Number(userInput));
|
||||
const decimals: Result<string> = validDecimals
|
||||
? Result.from({ res: userInput })
|
||||
: Result.from({ err: 'Invalid decimal' });
|
||||
return decimals;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import React from 'react';
|
||||
import { Result } from 'mycrypto-nano-result';
|
||||
import { shepherdProvider } from 'libs/nodes';
|
||||
import { Input } from 'components/ui';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import ERC20 from 'libs/erc20';
|
||||
|
||||
interface OwnProps {
|
||||
fieldToFetch: keyof Pick<typeof ERC20, 'symbol' | 'decimals'>;
|
||||
fieldName: string;
|
||||
address?: string;
|
||||
userInputValidator(input: string): Result<string>;
|
||||
fetchedFieldValidator?(input: any): Result<string>;
|
||||
shouldEnableAutoField(input: Result<string>): boolean;
|
||||
|
||||
onChange(symbol: Result<string>): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
field: Result<string>;
|
||||
autoField: boolean;
|
||||
userInput: string;
|
||||
addressToLoad?: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class FieldInput extends React.Component<OwnProps, State> {
|
||||
public static getDerivedStateFromProps(
|
||||
nextProps: OwnProps,
|
||||
prevState: State
|
||||
): Partial<State> | null {
|
||||
if (nextProps.address && nextProps.address !== prevState.addressToLoad) {
|
||||
return { loading: true, autoField: true, addressToLoad: nextProps.address };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public state: State = {
|
||||
userInput: '',
|
||||
autoField: true,
|
||||
field: Result.from({ res: '' }),
|
||||
loading: false
|
||||
};
|
||||
|
||||
private currentRequest: Promise<any> | null;
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.state.addressToLoad && this.state.loading) {
|
||||
this.attemptToLoadField(this.state.addressToLoad);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.currentRequest) {
|
||||
this.currentRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { userInput, field, autoField, loading } = this.state;
|
||||
|
||||
return (
|
||||
<label className="AddCustom-field form-group">
|
||||
<div className="input-group-header">{this.props.fieldName}</div>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Input
|
||||
isValid={field.ok()}
|
||||
className="input-group-input-small"
|
||||
type="text"
|
||||
name={this.props.fieldName}
|
||||
readOnly={autoField}
|
||||
value={field.ok() ? field.unwrap() : userInput}
|
||||
onChange={this.handleFieldChange}
|
||||
/>
|
||||
)}
|
||||
{field.err() && <div className="AddCustom-field-error">{field.err()}</div>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
private handleFieldChange = (args: React.FormEvent<HTMLInputElement>) => {
|
||||
const userInput = args.currentTarget.value;
|
||||
const field = this.props.userInputValidator(userInput);
|
||||
this.setState({ userInput, field });
|
||||
this.props.onChange(field);
|
||||
};
|
||||
|
||||
private attemptToLoadField(address: string) {
|
||||
// process request
|
||||
this.currentRequest = this.loadField(address)
|
||||
// set state on successful request e.g it was not cancelled
|
||||
// and then also set our current request to null
|
||||
.then(({ [this.props.fieldToFetch]: field }) =>
|
||||
this.setState({
|
||||
field,
|
||||
loading: false,
|
||||
autoField: this.props.shouldEnableAutoField(field)
|
||||
})
|
||||
)
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
// if the component is unmounted, then dont call set state
|
||||
if (!this.currentRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise it was a failed fetch call
|
||||
this.setState({ autoField: false, loading: false });
|
||||
})
|
||||
.then(() => (this.currentRequest = null));
|
||||
}
|
||||
|
||||
private loadField(address: string) {
|
||||
const { fieldToFetch } = this.props;
|
||||
return shepherdProvider
|
||||
.sendCallRequest({ data: ERC20[fieldToFetch].encodeInput(), to: address })
|
||||
.then(ERC20[fieldToFetch].decodeOutput as any)
|
||||
.then(({ [fieldToFetch]: field }) => {
|
||||
let result: Result<string>;
|
||||
if (this.props.fetchedFieldValidator) {
|
||||
result = this.props.fetchedFieldValidator(field);
|
||||
} else {
|
||||
result = Result.from({ res: field });
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
this.props.onChange(result);
|
||||
return { [fieldToFetch]: result };
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { translateRaw } from 'translations';
|
||||
import { IGenerateSymbolLookup } from './AddCustomTokenForm';
|
||||
import { Result } from 'mycrypto-nano-result';
|
||||
import { FieldInput } from './FieldInput';
|
||||
|
||||
interface OwnProps {
|
||||
address?: string;
|
||||
symbolLookup: IGenerateSymbolLookup;
|
||||
onChange(symbol: Result<string>): void;
|
||||
}
|
||||
|
||||
export class SymbolField extends React.Component<OwnProps> {
|
||||
public render() {
|
||||
return (
|
||||
<FieldInput
|
||||
fieldName={translateRaw('TOKEN_SYMBOL')}
|
||||
fieldToFetch={'symbol'}
|
||||
shouldEnableAutoField={req => !req.err()}
|
||||
address={this.props.address}
|
||||
userInputValidator={this.isValidUserInput}
|
||||
fetchedFieldValidator={field =>
|
||||
field
|
||||
? Result.from({ res: field })
|
||||
: Result.from({ err: 'No Symbol found, please input the token symbol manually' })
|
||||
}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private isValidUserInput = (userInput: string) => {
|
||||
const validSymbol = !this.props.symbolLookup[userInput];
|
||||
const symbol: Result<string> = validSymbol
|
||||
? Result.from({ res: userInput })
|
||||
: Result.from({ err: 'A token with this symbol already exists' });
|
||||
return symbol;
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './AddCustomTokenForm';
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { TokenBalance } from 'selectors/wallet';
|
||||
import AddCustomTokenForm from './AddCustomTokenForm';
|
||||
import { AddCustomTokenForm } from './AddCustomTokenForm';
|
||||
import TokenRow from './TokenRow';
|
||||
import { Token } from 'types/network';
|
||||
|
||||
|
|
|
@ -1,20 +1,62 @@
|
|||
import Contract from 'libs/contracts';
|
||||
|
||||
interface ABIFunc<T, K = void> {
|
||||
encodeInput(x: T): string;
|
||||
type uint256 = any;
|
||||
|
||||
type address = any;
|
||||
|
||||
export interface ABIFunc<T, K = void> {
|
||||
outputType: K;
|
||||
decodeInput(argStr: string): T;
|
||||
encodeInput(x: T): string;
|
||||
decodeOutput(argStr: string): K;
|
||||
}
|
||||
|
||||
type address = any;
|
||||
type uint256 = any;
|
||||
export interface ABIFuncParamless<T = void> {
|
||||
outputType: T;
|
||||
encodeInput(): string;
|
||||
decodeOutput(argStr: string): T;
|
||||
}
|
||||
|
||||
interface IErc20 {
|
||||
decimals: ABIFuncParamless<{ decimals: string }>;
|
||||
symbol: ABIFuncParamless<{ symbol: string }>;
|
||||
|
||||
balanceOf: ABIFunc<{ _owner: address }, { balance: uint256 }>;
|
||||
transfer: ABIFunc<{ _to: address; _value: uint256 }>;
|
||||
}
|
||||
|
||||
const erc20Abi = [
|
||||
{
|
||||
name: 'decimals',
|
||||
type: 'function',
|
||||
|
||||
constant: true,
|
||||
payable: false,
|
||||
inputs: [],
|
||||
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint8'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'symbol',
|
||||
type: 'function',
|
||||
constant: true,
|
||||
payable: false,
|
||||
|
||||
inputs: [],
|
||||
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'balanceOf',
|
||||
type: 'function',
|
||||
|
@ -35,11 +77,13 @@ const erc20Abi = [
|
|||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'transfer',
|
||||
type: 'function',
|
||||
constant: false,
|
||||
payable: false,
|
||||
|
||||
inputs: [
|
||||
{
|
||||
name: '_to',
|
||||
|
@ -57,7 +101,32 @@ const erc20Abi = [
|
|||
type: 'bool'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Transfer',
|
||||
type: 'event',
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: '_from',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: '_to',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: '_value',
|
||||
type: 'uint256'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default (new Contract(erc20Abi) as any) as IErc20;
|
||||
export default (new Contract(erc20Abi, {
|
||||
decimals: ['decimals'],
|
||||
symbol: ['symbol']
|
||||
}) as any) as IErc20;
|
||||
|
|
|
@ -112,6 +112,7 @@
|
|||
"lint-staged": "7.0.4",
|
||||
"mini-css-extract-plugin": "0.4.0",
|
||||
"minimist": "1.2.0",
|
||||
"mycrypto-nano-result": "0.0.1",
|
||||
"node-sass": "4.8.3",
|
||||
"nodemon": "1.17.3",
|
||||
"null-loader": "0.1.1",
|
||||
|
|
|
@ -7432,6 +7432,10 @@ mycrypto-eth-exists@1.0.0:
|
|||
isomorphic-ws "^4.0.1"
|
||||
toml "^2.3.3"
|
||||
|
||||
mycrypto-nano-result@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mycrypto-nano-result/-/mycrypto-nano-result-0.0.1.tgz#c1d3208458dc485441b0230c69948f62344f9d5a"
|
||||
|
||||
mycrypto-shepherd@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/mycrypto-shepherd/-/mycrypto-shepherd-1.4.0.tgz#ad86e0f18040da524631bf471bce03ba65c165a3"
|
||||
|
@ -9953,8 +9957,8 @@ sdp@^2.6.0:
|
|||
resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.7.0.tgz#02b64ea0c29d73179afa19794e466b123b1b29f3"
|
||||
|
||||
sdp@^2.7.0:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.7.4.tgz#cac76b0e2f16f55243d25bc0432f6bbb5488bfc1"
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.7.3.tgz#ed177eb4074aa3213e150e74a9ab2d06ae6e5dbf"
|
||||
|
||||
secp256k1@^3.0.1:
|
||||
version "3.5.0"
|
||||
|
|
Loading…
Reference in New Issue