From 28d0097fe19431d98be82ae0c692d0eae789bc03 Mon Sep 17 00:00:00 2001 From: Jan Kalina Date: Mon, 22 Feb 2021 11:54:27 +0100 Subject: [PATCH] Add Fantom Opera chain --- app/_locales/en/messages.json | 6 ++++++ app/scripts/controllers/network/network.js | 12 +++++++++--- shared/constants/network.js | 9 +++++++++ ui/app/components/app/dropdowns/network-dropdown.js | 3 +++ .../app/dropdowns/tests/network-dropdown.test.js | 12 +++++++++--- .../loading-network-screen.component.js | 2 ++ ui/app/css/design-system/colors.scss | 2 ++ ui/app/helpers/utils/transactions.util.js | 4 ++++ ui/app/pages/routes/routes.component.js | 2 ++ .../settings/networks-tab/networks-tab.constants.js | 11 +++++++++++ ui/lib/account-link.js | 2 ++ 11 files changed, 59 insertions(+), 6 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 5e6ef82f4c7d..d6776f3e4908 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -340,6 +340,9 @@ "connectingTo": { "message": "Connecting to $1" }, + "connectingToFantom": { + "message": "Connecting to Fantom Opera" + }, "connectingToGoerli": { "message": "Connecting to Goerli Test Network" }, @@ -666,6 +669,9 @@ "failureMessage": { "message": "Something went wrong, and we were unable to complete the action" }, + "fantom": { + "message": "Fantom Opera" + }, "fast": { "message": "Fast" }, diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 1c9c8060097d..46448e6cb190 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -12,11 +12,14 @@ import EthQuery from 'eth-query'; import { RINKEBY, MAINNET, + FANTOM, + FANTOM_RPC, INFURA_PROVIDER_TYPES, NETWORK_TYPE_RPC, NETWORK_TYPE_TO_ID_MAP, MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, + FANTOM_CHAIN_ID, } from '../../../../shared/constants/network'; import { isPrefixedFormattedHexString, @@ -189,16 +192,17 @@ export default class NetworkController extends EventEmitter { }); } - async setProviderType(type, rpcUrl = '', ticker = 'ETH', nickname = '') { + async setProviderType(type, rpcUrl = '', tickerParam = null, nickname = '') { assert.notStrictEqual( type, NETWORK_TYPE_RPC, `NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPE_RPC}". Use "setRpcTarget"`, ); assert.ok( - INFURA_PROVIDER_TYPES.includes(type), - `Unknown Infura provider type "${type}".`, + INFURA_PROVIDER_TYPES.includes(type) || type === FANTOM, + `Unknown built-in provider type "${type}".`, ); + const ticker = tickerParam || (type === FANTOM ? 'FTM' : 'ETH'); const { chainId } = NETWORK_TYPE_TO_ID_MAP[type]; this.setProviderConfig({ type, rpcUrl, chainId, ticker, nickname }); } @@ -247,6 +251,8 @@ export default class NetworkController extends EventEmitter { const isInfura = INFURA_PROVIDER_TYPES.includes(type); if (isInfura) { this._configureInfuraProvider(type, this._infuraProjectId); + } else if (type === FANTOM) { + this._configureStandardProvider(FANTOM_RPC, FANTOM_CHAIN_ID); // url-based rpc endpoints } else if (type === NETWORK_TYPE_RPC) { this._configureStandardProvider(rpcUrl, chainId); diff --git a/shared/constants/network.js b/shared/constants/network.js index a3cb2e7c926b..402fca21d724 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -3,6 +3,7 @@ export const RINKEBY = 'rinkeby'; export const KOVAN = 'kovan'; export const MAINNET = 'mainnet'; export const GOERLI = 'goerli'; +export const FANTOM = 'fantom'; export const NETWORK_TYPE_RPC = 'rpc'; export const MAINNET_NETWORK_ID = '1'; @@ -10,12 +11,14 @@ export const ROPSTEN_NETWORK_ID = '3'; export const RINKEBY_NETWORK_ID = '4'; export const GOERLI_NETWORK_ID = '5'; export const KOVAN_NETWORK_ID = '42'; +export const FANTOM_NETWORK_ID = '250'; export const MAINNET_CHAIN_ID = '0x1'; export const ROPSTEN_CHAIN_ID = '0x3'; export const RINKEBY_CHAIN_ID = '0x4'; export const GOERLI_CHAIN_ID = '0x5'; export const KOVAN_CHAIN_ID = '0x2a'; +export const FANTOM_CHAIN_ID = '0xfa'; /** * The largest possible chain ID we can handle. @@ -28,8 +31,10 @@ export const RINKEBY_DISPLAY_NAME = 'Rinkeby'; export const KOVAN_DISPLAY_NAME = 'Kovan'; export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'; export const GOERLI_DISPLAY_NAME = 'Goerli'; +export const FANTOM_DISPLAY_NAME = 'Fantom Opera'; export const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI]; +export const FANTOM_RPC = 'https://rpcapi.fantom.network'; export const TEST_CHAINS = [ ROPSTEN_CHAIN_ID, @@ -44,6 +49,7 @@ export const NETWORK_TYPE_TO_ID_MAP = { [KOVAN]: { networkId: KOVAN_NETWORK_ID, chainId: KOVAN_CHAIN_ID }, [GOERLI]: { networkId: GOERLI_NETWORK_ID, chainId: GOERLI_CHAIN_ID }, [MAINNET]: { networkId: MAINNET_NETWORK_ID, chainId: MAINNET_CHAIN_ID }, + [FANTOM]: { networkId: FANTOM_NETWORK_ID, chainId: FANTOM_CHAIN_ID }, }; export const NETWORK_TO_NAME_MAP = { @@ -52,18 +58,21 @@ export const NETWORK_TO_NAME_MAP = { [KOVAN]: KOVAN_DISPLAY_NAME, [MAINNET]: MAINNET_DISPLAY_NAME, [GOERLI]: GOERLI_DISPLAY_NAME, + [FANTOM]: FANTOM_DISPLAY_NAME, [ROPSTEN_NETWORK_ID]: ROPSTEN_DISPLAY_NAME, [RINKEBY_NETWORK_ID]: RINKEBY_DISPLAY_NAME, [KOVAN_NETWORK_ID]: KOVAN_DISPLAY_NAME, [GOERLI_NETWORK_ID]: GOERLI_DISPLAY_NAME, [MAINNET_NETWORK_ID]: MAINNET_DISPLAY_NAME, + [FANTOM_NETWORK_ID]: FANTOM_DISPLAY_NAME, [ROPSTEN_CHAIN_ID]: ROPSTEN_DISPLAY_NAME, [RINKEBY_CHAIN_ID]: RINKEBY_DISPLAY_NAME, [KOVAN_CHAIN_ID]: KOVAN_DISPLAY_NAME, [GOERLI_CHAIN_ID]: GOERLI_DISPLAY_NAME, [MAINNET_CHAIN_ID]: MAINNET_DISPLAY_NAME, + [FANTOM_CHAIN_ID]: FANTOM_DISPLAY_NAME, }; export const CHAIN_ID_TO_TYPE_MAP = Object.entries( diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index c0d0105170a5..78df979f2127 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -194,6 +194,8 @@ class NetworkDropdown extends Component { name = this.context.t('rinkeby'); } else if (providerName === 'goerli') { name = this.context.t('goerli'); + } else if (providerName === 'fantom') { + name = this.context.t('fantom'); } else { name = provider.nickname || this.context.t('unknownNetwork'); } @@ -285,6 +287,7 @@ class NetworkDropdown extends Component { {this.renderNetworkEntry('kovan')} {this.renderNetworkEntry('rinkeby')} {this.renderNetworkEntry('goerli')} + {this.renderNetworkEntry('fantom')} {this.renderCustomRpcList(rpcListDetail, this.props.provider)} ); }); - it('renders 8 DropDownMenuItems ', function () { - assert.strictEqual(wrapper.find(DropdownMenuItem).length, 8); + it('renders 9 DropDownMenuItems ', function () { + assert.strictEqual(wrapper.find(DropdownMenuItem).length, 9); }); it('checks background color for first ColorIndicator', function () { @@ -97,6 +97,12 @@ describe('Network Dropdown', function () { it('checks background color for sixth ColorIndicator', function () { const colorIndicator = wrapper.find(ColorIndicator).at(5); + assert.strictEqual(colorIndicator.prop('color'), 'fantom'); + assert.strictEqual(colorIndicator.prop('borderColor'), 'fantom'); + }); + + it('checks background color for seventh ColorIndicator', function () { + const colorIndicator = wrapper.find(ColorIndicator).at(6); const customNetworkGray = 'ui-2'; assert.strictEqual(colorIndicator.prop('color'), customNetworkGray); assert.strictEqual(colorIndicator.prop('borderColor'), customNetworkGray); @@ -104,7 +110,7 @@ describe('Network Dropdown', function () { it('checks dropdown for frequestRPCList from state', function () { assert.strictEqual( - wrapper.find(DropdownMenuItem).at(6).text(), + wrapper.find(DropdownMenuItem).at(7).text(), '✓http://localhost:7545', ); }); diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.component.js b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js index 0419aed42a78..31d29f991c74 100644 --- a/ui/app/components/app/loading-network-screen/loading-network-screen.component.js +++ b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js @@ -50,6 +50,8 @@ export default class LoadingNetworkScreen extends PureComponent { name = this.context.t('connectingToRinkeby'); } else if (providerName === 'goerli') { name = this.context.t('connectingToGoerli'); + } else if (providerName === 'fantom') { + name = this.context.t('connectingToFantom'); } else { name = this.context.t('connectingTo', [providerId]); } diff --git a/ui/app/css/design-system/colors.scss b/ui/app/css/design-system/colors.scss index b447315f8a99..28b6946ab4f8 100644 --- a/ui/app/css/design-system/colors.scss +++ b/ui/app/css/design-system/colors.scss @@ -108,6 +108,7 @@ $ropsten: #ff4a8d; $kovan: #9064ff; $rinkeby: #f6c343; $goerli: #3099f2; +$fantom: #9064ff; $color-map: ( 'ui-1': $ui-1, @@ -136,5 +137,6 @@ $color-map: ( 'kovan': $kovan, 'rinkeby': $rinkeby, 'goerli': $goerli, + 'fantom': $fantom, 'transparent': transparent, ); diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js index 463ddab5ea95..066516317c46 100644 --- a/ui/app/helpers/utils/transactions.util.js +++ b/ui/app/helpers/utils/transactions.util.js @@ -4,6 +4,7 @@ import { ethers } from 'ethers'; import log from 'loglevel'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; import { getEtherscanNetworkPrefix } from '../../../lib/etherscan-prefix-for-network'; +import { FANTOM_NETWORK_ID } from '../../../../shared/constants/network'; import { TRANSACTION_CATEGORIES, TRANSACTION_GROUP_STATUSES, @@ -202,6 +203,9 @@ export function getBlockExplorerUrlForTx(networkId, hash, rpcPrefs = {}) { if (rpcPrefs.blockExplorerUrl) { return `${rpcPrefs.blockExplorerUrl.replace(/\/+$/u, '')}/tx/${hash}`; } + if (networkId === FANTOM_NETWORK_ID) { + return `https://ftmscan.com/tx/${hash}`; + } const prefix = getEtherscanNetworkPrefix(networkId); return `https://${prefix}etherscan.io/tx/${hash}`; } diff --git a/ui/app/pages/routes/routes.component.js b/ui/app/pages/routes/routes.component.js index c563d2b03e53..9bc5351730df 100644 --- a/ui/app/pages/routes/routes.component.js +++ b/ui/app/pages/routes/routes.component.js @@ -366,6 +366,8 @@ export default class Routes extends Component { return this.context.t('connectingToRinkeby'); case 'goerli': return this.context.t('connectingToGoerli'); + case 'fantom': + return this.context.t('connectingToFantom'); default: return this.context.t('connectingTo', [providerId]); } diff --git a/ui/app/pages/settings/networks-tab/networks-tab.constants.js b/ui/app/pages/settings/networks-tab/networks-tab.constants.js index 52f6514d7cbc..723ce81a4c9d 100644 --- a/ui/app/pages/settings/networks-tab/networks-tab.constants.js +++ b/ui/app/pages/settings/networks-tab/networks-tab.constants.js @@ -9,6 +9,8 @@ import { RINKEBY_CHAIN_ID, ROPSTEN, ROPSTEN_CHAIN_ID, + FANTOM, + FANTOM_CHAIN_ID, } from '../../../../../shared/constants/network'; const defaultNetworksData = [ @@ -57,6 +59,15 @@ const defaultNetworksData = [ ticker: 'ETH', blockExplorerUrl: 'https://kovan.etherscan.io', }, + { + labelKey: FANTOM, + iconColor: '#9064FF', + providerType: FANTOM, + rpcUrl: `https://rpcapi.fantom.network`, + chainId: FANTOM_CHAIN_ID, + ticker: 'FTM', + blockExplorerUrl: 'https://ftmscan.com', + }, ]; export { defaultNetworksData }; diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js index 9d7014487c02..62aca57dfab8 100644 --- a/ui/lib/account-link.js +++ b/ui/lib/account-link.js @@ -21,6 +21,8 @@ export default function getAccountLink(address, network, rpcPrefs) { return `https://kovan.etherscan.io/address/${address}`; case 5: // goerli test net return `https://goerli.etherscan.io/address/${address}`; + case 250: // fantom net + return `https://ftmscan.com/address/${address}`; default: return ''; }