aboutsummaryrefslogblamecommitdiffstats
path: root/packages/website/ts/pages/instant/config_generator.tsx
blob: fbeeeaeaf0b3900057bbff73f0e3af3af403fa69 (plain) (tree)
1
2
3
4
5
6
7
8
9



                                                                               


                               
                                                        
                                                       
                                                            
                                                                   
                                                   
                                             
                                                                                              
                                                                             
                                         
                                        
                                               
 

                                                                                            





                                                              


                                       
                                                    

 
                                                                                                  
 
                                                                                                  

                                          

                                      
                                                        
                                                    


                                                                           
                                                            
                                                        




                                                        

         

                                      


                                                                                                        
                
                                        
                                                                              

                                                                                      
                                                                           
                                                             
                                         
                                                                                                                  




                                                                                           




                                                                                     
                                        

                                                                        




                                                                              

                                         


                        
                                                                                  




                                                    












                                                                  





                                                                   
                                                                             
                                                                                 
      
                                                                     
                                                    




                                             
                                                                                             









                                                                     
                                                                            









                                                                    













                                                      

                                                                 
                                                  
                                                                      


                                                              

                                                                           
                     
                                                                         



                                                                              

                        
                                                                            
         
                                                    




                                                        
                                                                                        







                                                                                                            









                                                                                               



                                                                                
                                                                                

                                                                         
                                          

                                       










                                                                                   


                            













                                                                                           

                                                                   
                    
                                 









                                                                                          
                                                                                      

                                



                                                                      
                                                                                                                   
      
 




                                              

                          

 



                                                                                               

                 

             
                                           
                                                                                     
                       
                                                                                                  


                                

                                                                                                     



                                  




                                                                                          
                    
                  

                



                                       
import { StandardRelayerAPIOrderProvider } from '@0x/asset-buyer';
import { getContractAddressesForNetworkOrThrow } from '@0x/contract-addresses';
import { assetDataUtils } from '@0x/order-utils';
import { ObjectMap } from '@0x/types';
import * as _ from 'lodash';
import * as React from 'react';

import { CheckMark } from 'ts/components/ui/check_mark';
import { Container } from 'ts/components/ui/container';
import { MultiSelect } from 'ts/components/ui/multi_select';
import { Select, SelectItemConfig } from 'ts/components/ui/select';
import { Spinner } from 'ts/components/ui/spinner';
import { Text } from 'ts/components/ui/text';
import { ConfigGeneratorAddressInput } from 'ts/pages/instant/config_generator_address_input';
import { FeePercentageSlider } from 'ts/pages/instant/fee_percentage_slider';
import { colors } from 'ts/style/colors';
import { WebsitePaths } from 'ts/types';
import { constants } from 'ts/utils/constants';

import { assetMetaDataMap } from '../../../../instant/src/data/asset_meta_data_map';
import { ERC20AssetMetaData, ZeroExInstantBaseConfig } from '../../../../instant/src/types';

export interface ConfigGeneratorProps {
    value: ZeroExInstantBaseConfig;
    onConfigChange: (config: ZeroExInstantBaseConfig) => void;
}

export interface ConfigGeneratorState {
    isLoadingAvailableTokens: boolean;
    // Address to token info
    availableTokens?: ObjectMap<ERC20AssetMetaData>;
}

const SRA_ENDPOINTS = ['https://api.radarrelay.com/0x/v2/', 'https://sra.bamboorelay.com/0x/v2/'];

export class ConfigGenerator extends React.Component<ConfigGeneratorProps, ConfigGeneratorState> {
    public state: ConfigGeneratorState = {
        isLoadingAvailableTokens: true,
    };
    public componentDidMount(): void {
        // tslint:disable-next-line:no-floating-promises
        this._setAvailableAssetsFromOrderProvider();
    }
    public componentDidUpdate(prevProps: ConfigGeneratorProps): void {
        if (prevProps.value.orderSource !== this.props.value.orderSource) {
            // tslint:disable-next-line:no-floating-promises
            this._setAvailableAssetsFromOrderProvider();
            const newConfig: ZeroExInstantBaseConfig = {
                ...this.props.value,
                availableAssetDatas: undefined,
            };
            this.props.onConfigChange(newConfig);
        }
    }
    public render(): React.ReactNode {
        const { value } = this.props;
        if (!_.isString(value.orderSource)) {
            throw new Error('ConfigGenerator component only supports string values as an orderSource.');
        }
        return (
            <Container minWidth="350px">
                <ConfigGeneratorSection title="Standard relayer API endpoint">
                    <Select value={value.orderSource} items={this._generateItems()} />
                </ConfigGeneratorSection>
                <ConfigGeneratorSection {...this._getTokenSelectorProps()}>
                    {this._renderTokenMultiSelectOrSpinner()}
                </ConfigGeneratorSection>
                <ConfigGeneratorSection title="Transaction fee ETH address" marginBottom="10px" isOptional={true}>
                    <ConfigGeneratorAddressInput
                        value={value.affiliateInfo ? value.affiliateInfo.feeRecipient : ''}
                        onChange={this._handleAffiliateAddressChange}
                    />
                </ConfigGeneratorSection>
                <ConfigGeneratorSection
                    title="Fee percentage"
                    actionText="Learn more"
                    onActionTextClick={this._handleAffiliatePercentageLearnMoreClick}
                >
                    <FeePercentageSlider
                        value={value.affiliateInfo.feePercentage}
                        onChange={this._handleAffiliatePercentageChange}
                        isDisabled={
                            _.isUndefined(value.affiliateInfo) ||
                            _.isUndefined(value.affiliateInfo.feeRecipient) ||
                            _.isEmpty(value.affiliateInfo.feeRecipient)
                        }
                    />
                </ConfigGeneratorSection>
            </Container>
        );
    }
    private readonly _getTokenSelectorProps = (): ConfigGeneratorSectionProps => {
        if (_.isEmpty(this.state.availableTokens)) {
            return {
                title: 'What tokens can users buy?',
            };
        }
        if (_.isUndefined(this.props.value.availableAssetDatas)) {
            return {
                title: 'What tokens can users buy?',
                actionText: 'Unselect All',
                onActionTextClick: this._handleUnselectAllClick,
            };
        }
        return {
            title: 'What tokens can users buy?',
            actionText: 'Select All',
            onActionTextClick: this._handleSelectAllClick,
        };
    };
    private readonly _generateItems = (): SelectItemConfig[] => {
        return _.map(SRA_ENDPOINTS, endpoint => ({
            text: endpoint,
            onClick: this._handleSRASelection.bind(this, endpoint),
        }));
    };
    private readonly _handleAffiliatePercentageLearnMoreClick = (): void => {
        window.open(`${WebsitePaths.Wiki}#Learn-About-Affiliate-Fees`, '_blank');
    };
    private readonly _handleSRASelection = (sraEndpoint: string) => {
        const newConfig: ZeroExInstantBaseConfig = {
            ...this.props.value,
            orderSource: sraEndpoint,
        };
        this.props.onConfigChange(newConfig);
    };
    private readonly _handleAffiliateAddressChange = (address: string, isValid: boolean) => {
        const oldConfig: ZeroExInstantBaseConfig = this.props.value;
        const newConfig: ZeroExInstantBaseConfig = {
            ...oldConfig,
            affiliateInfo: {
                feeRecipient: address,
                feePercentage: oldConfig.affiliateInfo.feePercentage,
            },
        };
        this.props.onConfigChange(newConfig);
    };
    private readonly _handleAffiliatePercentageChange = (value: number) => {
        const oldConfig: ZeroExInstantBaseConfig = this.props.value;
        const newConfig: ZeroExInstantBaseConfig = {
            ...oldConfig,
            affiliateInfo: {
                feeRecipient: oldConfig.affiliateInfo.feeRecipient,
                feePercentage: value,
            },
        };
        this.props.onConfigChange(newConfig);
    };
    private readonly _handleSelectAllClick = () => {
        const newConfig: ZeroExInstantBaseConfig = {
            ...this.props.value,
            availableAssetDatas: undefined,
        };
        this.props.onConfigChange(newConfig);
    };
    private readonly _handleUnselectAllClick = () => {
        const newConfig: ZeroExInstantBaseConfig = {
            ...this.props.value,
            availableAssetDatas: [],
        };
        this.props.onConfigChange(newConfig);
    };
    private readonly _handleTokenClick = (assetData: string) => {
        const { value } = this.props;
        let newAvailableAssetDatas: string[] = [];
        const allKnownAssetDatas = _.keys(this.state.availableTokens);
        const availableAssetDatas = value.availableAssetDatas;
        if (_.isUndefined(availableAssetDatas)) {
            // It being undefined means it's all tokens.
            newAvailableAssetDatas = _.pull(allKnownAssetDatas, assetData);
        } else if (!_.includes(availableAssetDatas, assetData)) {
            // Add it
            newAvailableAssetDatas = [...availableAssetDatas, assetData];
            if (newAvailableAssetDatas.length === allKnownAssetDatas.length) {
                // If all tokens are manually selected, just show none.
                newAvailableAssetDatas = undefined;
            }
        } else {
            // Remove it
            newAvailableAssetDatas = _.pull(availableAssetDatas, assetData);
        }
        const newConfig: ZeroExInstantBaseConfig = {
            ...this.props.value,
            availableAssetDatas: newAvailableAssetDatas,
        };
        this.props.onConfigChange(newConfig);
    };
    private readonly _setAvailableAssetsFromOrderProvider = async (): Promise<void> => {
        const { value } = this.props;
        if (!_.isUndefined(value.orderSource) && _.isString(value.orderSource)) {
            this.setState({ isLoadingAvailableTokens: true });
            const networkId = constants.NETWORK_ID_MAINNET;
            const sraOrderProvider = new StandardRelayerAPIOrderProvider(value.orderSource, networkId);
            const etherTokenAddress = getContractAddressesForNetworkOrThrow(networkId).etherToken;
            const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddress);
            const assetDatas = await sraOrderProvider.getAvailableMakerAssetDatasAsync(etherTokenAssetData);
            const availableTokens = _.reduce(
                assetDatas,
                (acc, assetData) => {
                    const metaDataIfExists = assetMetaDataMap[assetData] as ERC20AssetMetaData;
                    if (metaDataIfExists) {
                        acc[assetData] = metaDataIfExists;
                    }
                    return acc;
                },
                {} as ObjectMap<ERC20AssetMetaData>,
            );
            this.setState({ availableTokens, isLoadingAvailableTokens: false });
        }
    };
    private readonly _renderTokenMultiSelectOrSpinner = (): React.ReactNode => {
        const { value } = this.props;
        const { availableTokens, isLoadingAvailableTokens } = this.state;
        const multiSelectHeight = '200px';
        if (isLoadingAvailableTokens) {
            return (
                <Container
                    className="flex flex-column items-center justify-center"
                    height={multiSelectHeight}
                    backgroundColor={colors.white}
                    borderRadius="4px"
                    width="100%"
                >
                    <Container position="relative" left="12px" marginBottom="20px">
                        <Spinner />
                    </Container>
                    <Text fontSize="16px">Loading...</Text>
                </Container>
            );
        }
        const availableAssetDatas = _.keys(availableTokens);
        if (availableAssetDatas.length === 0) {
            return (
                <Container
                    className="flex flex-column items-center justify-center"
                    height={multiSelectHeight}
                    backgroundColor={colors.white}
                    borderRadius="4px"
                    width="100%"
                >
                    <Text fontSize="16px">No tokens available. Try another endpoint?</Text>
                </Container>
            );
        }
        const items = _.map(_.keys(availableTokens), assetData => {
            const metaData = availableTokens[assetData];
            return {
                value: assetData,
                renderItemContent: (isSelected: boolean) => (
                    <Container className="flex items-center">
                        <Container marginRight="10px">
                            <CheckMark isChecked={isSelected} />
                        </Container>
                        <Text
                            fontSize="16px"
                            fontColor={isSelected ? colors.mediumBlue : colors.darkerGrey}
                            fontWeight={300}
                        >
                            <b>{metaData.symbol.toUpperCase()}</b> — {metaData.name}
                        </Text>
                    </Container>
                ),
                onClick: this._handleTokenClick.bind(this, assetData),
            };
        });
        return <MultiSelect items={items} selectedValues={value.availableAssetDatas} height={multiSelectHeight} />;
    };
}

export interface ConfigGeneratorSectionProps {
    title: string;
    actionText?: string;
    onActionTextClick?: () => void;
    isOptional?: boolean;
    marginBottom?: string;
}

export const ConfigGeneratorSection: React.StatelessComponent<ConfigGeneratorSectionProps> = ({
    title,
    actionText,
    onActionTextClick,
    isOptional,
    marginBottom,
    children,
}) => (
    <Container marginBottom={marginBottom}>
        <Container marginBottom="10px" className="flex justify-between items-center">
            <Container>
                <Text fontColor={colors.white} fontSize="16px" lineHeight="18px" display="inline">
                    {title}
                </Text>
                {isOptional && (
                    <Text fontColor={colors.grey} fontSize="16px" lineHeight="18px" display="inline">
                        {' '}
                        (optional)
                    </Text>
                )}
            </Container>
            {actionText && (
                <Text fontSize="12px" fontColor={colors.grey} onClick={onActionTextClick}>
                    {actionText}
                </Text>
            )}
        </Container>
        {children}
    </Container>
);

ConfigGeneratorSection.defaultProps = {
    marginBottom: '30px',
};