From 6b94ffad9047e70ddc86ff6f4a484c85e2125528 Mon Sep 17 00:00:00 2001 From: Satyam Bansal Date: Wed, 11 Sep 2024 14:56:13 +0530 Subject: [PATCH] Custom network setup (#173) * Custom network setup * Add Custom Network Integration * feat: add ability to select custom network * fix: change placeholder --------- Co-authored-by: Bohdan Ohorodnii --- plugin/src/atoms/environment.ts | 12 +- plugin/src/atoms/index.ts | 4 +- .../CustomNetwork/customNetwork.css | 174 ++++++++++++++++++ plugin/src/components/CustomNetwork/index.tsx | 158 ++++++++++++++++ .../DevnetAccountSelector/index.tsx | 61 +++--- plugin/src/components/DevnetStatus/index.tsx | 27 +-- .../components/EnvironmentSelector/index.tsx | 70 ++++--- plugin/src/components/Wallet/index.tsx | 6 +- plugin/src/components/Wallet/wallet.css | 3 +- plugin/src/components/index.tsx | 3 +- plugin/src/features/Environment/index.tsx | 9 +- plugin/src/features/Environment/styles.css | 2 +- plugin/src/types/transaction.ts | 8 +- plugin/src/utils/misc.ts | 2 + plugin/src/utils/network.ts | 3 + 15 files changed, 461 insertions(+), 81 deletions(-) create mode 100644 plugin/src/components/CustomNetwork/customNetwork.css create mode 100644 plugin/src/components/CustomNetwork/index.tsx diff --git a/plugin/src/atoms/environment.ts b/plugin/src/atoms/environment.ts index c8bcad8d..9c2b7b2c 100644 --- a/plugin/src/atoms/environment.ts +++ b/plugin/src/atoms/environment.ts @@ -11,5 +11,15 @@ const isDevnetAliveAtom = atom(true) const selectedDevnetAccountAtom = atom(null) const availableDevnetAccountsAtom = atom([]) +const customNetworkAtom = atom('') +const isCustomNetworkAliveAtom = atom(false) -export { devnetAtom, envAtom, isDevnetAliveAtom, selectedDevnetAccountAtom, availableDevnetAccountsAtom } +export { + devnetAtom, + envAtom, + isDevnetAliveAtom, + selectedDevnetAccountAtom, + availableDevnetAccountsAtom, + customNetworkAtom, + isCustomNetworkAliveAtom +} diff --git a/plugin/src/atoms/index.ts b/plugin/src/atoms/index.ts index 93fc53e9..5b301abd 100644 --- a/plugin/src/atoms/index.ts +++ b/plugin/src/atoms/index.ts @@ -18,7 +18,9 @@ export { devnetAtom, envAtom, isDevnetAliveAtom, - selectedDevnetAccountAtom + selectedDevnetAccountAtom, + customNetworkAtom, + isCustomNetworkAliveAtom } from './environment' export { accountsAtom, networkNameAtom, selectedAccountAtom } from './manualAccount' export { transactionsAtom } from './transaction' diff --git a/plugin/src/components/CustomNetwork/customNetwork.css b/plugin/src/components/CustomNetwork/customNetwork.css new file mode 100644 index 00000000..f58068d1 --- /dev/null +++ b/plugin/src/components/CustomNetwork/customNetwork.css @@ -0,0 +1,174 @@ +.custom-network-container { + display: flex; + flex-direction: column; +} + +.input-button-wrapper { + display: flex; + align-items: stretch; +} + +.custom-input { + flex-grow: 1; + padding: 5px 8px; + border: 1px solid var(--secondary); + border-right: none; + border-radius: 6px 0 0 6px; + font-size: 1rem; + background: transparent; + color: var(--text); + -webkit-border-radius: 6px 0 0 6px; + -moz-border-radius: 6px 0 0 6px; + -ms-border-radius: 6px 0 0 6px; + -o-border-radius: 6px 0 0 6px; +} + +.custom-input:disabled { + background-color: var(--body-bg); + opacity: 0.7; + cursor: not-allowed; +} + +.custom-input:disabled:hover { + cursor: not-allowed; +} + +.btn-connect { + all: unset; + cursor: pointer; + padding: 0 0.5rem; + border: 1px solid var(--secondary); + border-radius: 0 0.25rem 0.25rem 0; + display: flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + font-size: 1rem; + -webkit-border-radius: 0 0.25rem 0.25rem 0; + -moz-border-radius: 0 0.25rem 0.25rem 0; + -ms-border-radius: 0 0.25rem 0.25rem 0; + -o-border-radius: 0 0.25rem 0.25rem 0; + transition: all 0.3s ease; +} + +.btn-connect:hover { + animation: quick-zoom 0.3s ease; +} + +@keyframes quick-zoom { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} + +/* For better browser compatibility, include vendor prefixes */ +@-webkit-keyframes quick-zoom { + 0% { + -webkit-transform: scale(1); + } + 50% { + -webkit-transform: scale(1.05); + } + 100% { + -webkit-transform: scale(1); + } +} + +@-moz-keyframes quick-zoom { + 0% { + -moz-transform: scale(1); + } + 50% { + -moz-transform: scale(1.05); + } + 100% { + -moz-transform: scale(1); + } +} + +@-ms-keyframes quick-zoom { + 0% { + -ms-transform: scale(1); + } + 50% { + -ms-transform: scale(1.05); + } + 100% { + -ms-transform: scale(1); + } +} + +@-o-keyframes quick-zoom { + 0% { + -o-transform: scale(1); + } + 50% { + -o-transform: scale(1.05); + } + 100% { + -o-transform: scale(1); + } +} + +.btn-connect:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.btn-connect-secondary { + background-color: var(--secondary); + color: var(--text); +} + +.btn-connect-info { + background-color: var(--info); + color: var(--text); +} + +.btn-connect-success { + background-color: var(--primary); + color: var(--text); +} + +.btn-connect-danger { + background-color: var(--danger); + color: var(--text); +} + +.spinner-border { + width: 1rem; + height: 1rem; + border-width: 0.2em; +} + +.btn-connect .connect-icon, +.btn-connect .disconnect-icon { + transition: opacity 0.3s ease; +} + +.btn-connect .disconnect-icon { + position: absolute; + opacity: 0; +} + +.btn-connect:hover .connect-icon { + opacity: 1; +} + +.btn-connect:hover .disconnect-icon { + opacity: 0; +} + +.btn-connect-success:hover .connect-icon { + opacity: 0; +} + +.btn-connect-success:hover .disconnect-icon { + opacity: 1; +} diff --git a/plugin/src/components/CustomNetwork/index.tsx b/plugin/src/components/CustomNetwork/index.tsx new file mode 100644 index 00000000..7c46a944 --- /dev/null +++ b/plugin/src/components/CustomNetwork/index.tsx @@ -0,0 +1,158 @@ +import React, {useCallback, useState, useEffect} from 'react' +import {useAtom} from 'jotai' +import {customNetworkAtom, isCustomNetworkAliveAtom} from '@/atoms' +import useInterval from '@/hooks/useInterval' +import {remixClient} from '@/PluginClient' +import {FaCheck, FaPlug, FaTimes} from 'react-icons/fa' +import './customNetwork.css' +import {AccountSelector} from '../DevnetAccountSelector' +import {FaRotate} from "react-icons/fa6" + +const CUSTOM_NETWORK_POLL_INTERVAL = 10_000 + +export const CustomNetwork = () => { + const [customNetwork, setCustomNetwork] = useAtom(customNetworkAtom) + const [customNetworkInput, setCustomNetworkInput] = useState(customNetwork) + const [, setIsCustomNetworkAlive] = useAtom(isCustomNetworkAliveAtom) + const [connectionStatus, setConnectionStatus] = useState('idle') // 'idle', 'connecting', 'connected', 'error' + + const fetchCustomNetworkStatus = useCallback(() => { + if (customNetwork) { + setConnectionStatus('connecting') + fetch(`${customNetwork}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'zks_L1BatchNumber', + params: [], + id: 1 + }) + }) + .then(async (res) => await res.json()) + .then((res) => { + if (res.result) { + setIsCustomNetworkAlive(true) + setConnectionStatus('connected') + } else { + remixClient.terminal.log({ + type: 'error', + value: `Failed to connect with RPC Url ${customNetwork}` + }) + setIsCustomNetworkAlive(false) + setConnectionStatus('error') + } + }) + .catch((err) => { + console.error(err) + remixClient.terminal.log({ + type: 'error', + value: `Failed to connect with RPC Url ${customNetwork}` + }) + setIsCustomNetworkAlive(false) + setConnectionStatus('error') + }) + } + }, [setIsCustomNetworkAlive, customNetwork]) + + useInterval(fetchCustomNetworkStatus, customNetwork.length > 0 ? CUSTOM_NETWORK_POLL_INTERVAL : null) + + useEffect(() => { + if (customNetwork) { + fetchCustomNetworkStatus() + } else { + setConnectionStatus('idle') + } + }, [customNetwork, fetchCustomNetworkStatus]) + + const handleConnect = () => { + setCustomNetwork(customNetworkInput) + } + + const handleDisconnect = () => { + setCustomNetwork('') + setConnectionStatus('idle') + setIsCustomNetworkAlive(false) + } + + const getButtonClass = () => { + switch (connectionStatus) { + case 'not-connected': + return 'btn-connect-secondary' + case 'connecting': + return 'btn-connect-info' + case 'connected': + return 'btn-connect-success' + case 'error': + return 'btn-connect-danger' + default: + return 'btn-connect-secondary' + } + } + + const getButtonText = () => { + switch (connectionStatus) { + case 'connecting': + return 'Connecting...' + case 'connected': + return 'Disconnect' + case 'error': + return 'Retry' + default: + return 'Connect' + } + } + + return ( +
+
+ +
+ setCustomNetworkInput(e.target.value)} + disabled={connectionStatus === 'connected'} + /> + +
+
+ +
+ ) +} diff --git a/plugin/src/components/DevnetAccountSelector/index.tsx b/plugin/src/components/DevnetAccountSelector/index.tsx index 1208dfd0..f31f7613 100644 --- a/plugin/src/components/DevnetAccountSelector/index.tsx +++ b/plugin/src/components/DevnetAccountSelector/index.tsx @@ -10,8 +10,10 @@ import { getAccounts, updateBalances } from '@/utils/network' import { accountAtom, providerAtom } from '@/atoms/connection' import { availableDevnetAccountsAtom, + customNetworkAtom, devnetAtom, envAtom, + isCustomNetworkAliveAtom, isDevnetAliveAtom, selectedDevnetAccountAtom } from '@/atoms/environment' @@ -24,13 +26,15 @@ import useAsyncFn from '@/hooks/useAsyncFn' const DEVNET_POLL_INTERVAL = 10_000 -export const DevnetAccountSelector = () => { +export const AccountSelector = ({ accountsType }: { accountsType: 'devnet' | 'customNet' }) => { const remixClient = useAtomValue(remixClientAtom) const [account, setAccount] = useAtom(accountAtom) const [provider, setProvider] = useAtom(providerAtom) const devnet = useAtomValue(devnetAtom) const [isDevnetAlive, setIsDevnetAlive] = useAtom(isDevnetAliveAtom) + const customNetwork = useAtomValue(customNetworkAtom) + const isCustomNetworkAlive = useAtomValue(isCustomNetworkAliveAtom) const [selectedDevnetAccount, setSelectedDevnetAccount] = useAtom(selectedDevnetAccountAtom) const [availableDevnetAccounts, setAvailableDevnetAccounts] = useAtom(availableDevnetAccountsAtom) const [accountRefreshing, setAccountRefreshing] = useState(false) @@ -38,6 +42,9 @@ export const DevnetAccountSelector = () => { const [accountIdx, setAccountIdx] = useState(0) const env = useAtomValue(envAtom) + const isAlive = accountsType === 'devnet' ? isDevnetAlive : isCustomNetworkAlive + const networkUrl = accountsType === 'devnet' ? devnet.url : customNetwork + const fetchDevnetStatus = useCallback(() => { fetch(`${devnet.url}`, { method: 'POST', @@ -64,43 +71,45 @@ export const DevnetAccountSelector = () => { }) }, [setIsDevnetAlive, devnet.url]) - useInterval(fetchDevnetStatus, devnet.url.length > 0 ? DEVNET_POLL_INTERVAL : null) + useInterval(fetchDevnetStatus, accountsType === 'devnet' && devnet.url.length > 0 ? DEVNET_POLL_INTERVAL : null) useEffect(() => { - fetchDevnetStatus() - if (!isDevnetAlive) { - remixClient - .call( - 'notification' as any, - 'toast', - `❗️ Server ${devnet.name} - ${devnet.url} is not healthy or not reachable at the moment` - ) - .catch((e) => { - console.error(e) - }) + if (accountsType === 'devnet') { + fetchDevnetStatus() + if (!isDevnetAlive) { + remixClient + .call( + 'notification' as any, + 'toast', + `❗️ Server ${devnet.name} - ${devnet.url} is not healthy or not reachable at the moment` + ) + .catch((e) => { + console.error(e) + }) + } } - }, [isDevnetAlive, remixClient, devnet, fetchDevnetStatus]) + }, [isDevnetAlive, remixClient, devnet, fetchDevnetStatus, accountsType]) useAsync(async () => { - const updatedAccounts = await updateBalances(availableDevnetAccounts, devnet.url) + const updatedAccounts = await updateBalances(availableDevnetAccounts, networkUrl) setAvailableDevnetAccounts(updatedAccounts) - }, [devnet, env]) + }, [networkUrl, env]) const [, refreshDevnetAccounts] = useAsyncFn(async () => { try { setAccountRefreshing(true) - const accounts = await getAccounts(`${devnet.url}`) + const accounts = await getAccounts(networkUrl) if (JSON.stringify(accounts) !== JSON.stringify(availableDevnetAccounts)) { setAvailableDevnetAccounts(accounts) } } catch (error) { await remixClient.terminal.log({ type: 'error', - value: `Failed to get accounts information from ${devnet.url}` + value: `Failed to get accounts information from ${networkUrl}` }) } setAccountRefreshing(false) - }, [remixClient, devnet]) + }, [remixClient, networkUrl]) useEffect(() => { refreshDevnetAccounts() @@ -110,22 +119,22 @@ export const DevnetAccountSelector = () => { if ( !(selectedDevnetAccount !== null && availableDevnetAccounts.includes(selectedDevnetAccount)) && availableDevnetAccounts.length > 0 && - isDevnetAlive + isAlive ) { setSelectedDevnetAccount(availableDevnetAccounts[0]) } - if (!isDevnetAlive && selectedDevnetAccount !== null) { + if (!isAlive && selectedDevnetAccount !== null) { setSelectedDevnetAccount(null) } - }, [availableDevnetAccounts, devnet, selectedDevnetAccount, setSelectedDevnetAccount, env, isDevnetAlive]) + }, [availableDevnetAccounts, devnet, selectedDevnetAccount, setSelectedDevnetAccount, env, isAlive]) useEffect(() => { - const newProvider = new Provider(devnet.url) + const newProvider = new Provider(networkUrl) if (selectedDevnetAccount != null) { setAccount(new Wallet(selectedDevnetAccount.private_key, newProvider)) } setProvider(newProvider) - }, [devnet, selectedDevnetAccount, setAccount, setProvider]) + }, [networkUrl, selectedDevnetAccount, setAccount, setProvider]) function handleAccountChange(index: number): void { if (index === -1) { @@ -146,7 +155,7 @@ export const DevnetAccountSelector = () => { return (
- +
{
) } - -export default DevnetAccountSelector diff --git a/plugin/src/components/DevnetStatus/index.tsx b/plugin/src/components/DevnetStatus/index.tsx index 2f2bb195..257742bf 100644 --- a/plugin/src/components/DevnetStatus/index.tsx +++ b/plugin/src/components/DevnetStatus/index.tsx @@ -1,21 +1,26 @@ import React from 'react' import { RxDotFilled } from 'react-icons/rx' import { useAtomValue } from 'jotai' -import { envAtom, isDevnetAliveAtom } from '@/atoms' +import { envAtom, isCustomNetworkAliveAtom, isDevnetAliveAtom } from '@/atoms' export const DevnetStatus = () => { const env = useAtomValue(envAtom) const isDevnetAlive = useAtomValue(isDevnetAliveAtom) + const isCustomNetworkAlive = useAtomValue(isCustomNetworkAliveAtom) - return ( - <> - {env === 'wallet' ? ( - - ) : isDevnetAlive ? ( - - ) : ( - - )} - + if (env === 'wallet') { + return + } + if (env === 'customNetwork') { + return isCustomNetworkAlive ? ( + + ) : ( + + ) + } + return isDevnetAlive ? ( + + ) : ( + ) } diff --git a/plugin/src/components/EnvironmentSelector/index.tsx b/plugin/src/components/EnvironmentSelector/index.tsx index 844c4602..d2aa16cb 100644 --- a/plugin/src/components/EnvironmentSelector/index.tsx +++ b/plugin/src/components/EnvironmentSelector/index.tsx @@ -7,24 +7,31 @@ import * as Dropdown from '@/ui_components/Dropdown' import { envName } from '@/utils/misc' import './styles.css' +const [localDevnet, remoteDevnet] = devnets + export const EnvironmentSelector = () => { const [env, setEnv] = useAtom(envAtom) const setDevnet = useSetAtom(devnetAtom) const setProvider = useSetAtom(providerAtom) const [dropdownControl, setDropdownControl] = useState(false) - const handleEnvironmentChange = (ipValue: string): void => { - const value = parseInt(ipValue) - if (!isNaN(value) && value > 1) { - setDevnet(devnets[value - 2]) - if (value === 3) { - setEnv('remoteDevnet') - } else if (value === 2) { - setEnv('localDevnet') + const handleEnvironmentChange = (env: string): void => { + switch (env) { + case 'localDevnet': + case 'remoteDevnet': { + const devnet = env === 'localDevnet' ? localDevnet : remoteDevnet + setDevnet(devnet) + setEnv(env) + setProvider(null) + break } - setProvider(null) - } else if (value === 0) { - setEnv('wallet') + case 'wallet': + setEnv('wallet') + break + case 'customNetwork': + setEnv('customNetwork') + break + default: } } @@ -50,26 +57,37 @@ export const EnvironmentSelector = () => { { - handleEnvironmentChange('0') + handleEnvironmentChange('wallet') }} > Wallet - - {devnets.map((devnet, i) => { - return ( - { - handleEnvironmentChange((i + 2).toString()) - }} - > - {devnet?.name} - - ) - })} + { + handleEnvironmentChange(localDevnet.id) + }} + > + {localDevnet?.name} + + { + handleEnvironmentChange(remoteDevnet.id) + }} + > + {remoteDevnet?.name} + + { + handleEnvironmentChange('customNetwork') + }} + > + Custom Network + diff --git a/plugin/src/components/Wallet/index.tsx b/plugin/src/components/Wallet/index.tsx index e1bdbd57..69ac7f57 100644 --- a/plugin/src/components/Wallet/index.tsx +++ b/plugin/src/components/Wallet/index.tsx @@ -47,7 +47,7 @@ export const Wallet = () => { return (
{ padding: '1rem 0rem' }} > -
- -
+
) } diff --git a/plugin/src/components/Wallet/wallet.css b/plugin/src/components/Wallet/wallet.css index 229bbaa3..109d6aaa 100644 --- a/plugin/src/components/Wallet/wallet.css +++ b/plugin/src/components/Wallet/wallet.css @@ -32,6 +32,7 @@ .wallet-actions { align-items: center; display: flex; - flex-direction: row-reverse; + flex-direction: center; justify-content: space-between; + width: 100%; } diff --git a/plugin/src/components/index.tsx b/plugin/src/components/index.tsx index a64106de..7060cfb8 100644 --- a/plugin/src/components/index.tsx +++ b/plugin/src/components/index.tsx @@ -1,6 +1,7 @@ -export { DevnetAccountSelector } from './DevnetAccountSelector' +export { AccountSelector } from './DevnetAccountSelector' export { EnvironmentSelector } from './EnvironmentSelector' export { ManualAccount } from './ManualAccount' export { Wallet } from './Wallet' export { DevnetStatus } from './DevnetStatus' export { CurrentEnv } from './CurrentEnv' +export { CustomNetwork } from './CustomNetwork' diff --git a/plugin/src/features/Environment/index.tsx b/plugin/src/features/Environment/index.tsx index d5d6424a..4876bf66 100644 --- a/plugin/src/features/Environment/index.tsx +++ b/plugin/src/features/Environment/index.tsx @@ -2,12 +2,13 @@ import React from 'react' import { useAtom } from 'jotai' import * as Tabs from '@radix-ui/react-tabs' import { - DevnetAccountSelector, + AccountSelector, EnvironmentSelector, Wallet, ManualAccount, DevnetStatus, - CurrentEnv + CurrentEnv, + CustomNetwork } from '@/components' import Accordian, { AccordianItem, AccordionContent, AccordionTrigger } from '@/ui_components/Accordian' import { envAtom } from '@/atoms/environment' @@ -60,7 +61,9 @@ export const Environment = () => {
- {['localDevnet', 'remoteDevnet'].includes(env) ? : } + {['localDevnet', 'remoteDevnet'].includes(env) && } + {env === 'wallet' && } + {env === 'customNetwork' && }
) : ( diff --git a/plugin/src/features/Environment/styles.css b/plugin/src/features/Environment/styles.css index a3bf69a3..e043f037 100644 --- a/plugin/src/features/Environment/styles.css +++ b/plugin/src/features/Environment/styles.css @@ -4,7 +4,7 @@ } .tabs-content-env { - padding: 16px 16px 16px; + padding: 16px 8px; border-top: 1px solid var(--secondary); } diff --git a/plugin/src/types/transaction.ts b/plugin/src/types/transaction.ts index e71a886a..fb4b10c1 100644 --- a/plugin/src/types/transaction.ts +++ b/plugin/src/types/transaction.ts @@ -1,7 +1,7 @@ import { type Provider, type Signer, type Wallet } from 'zksync-ethers' import { type Chain, type ChainFormatters } from 'viem' -export type EnvType = 'localDevnet' | 'remoteDevnet' | 'wallet' | 'manual' +export type EnvType = 'localDevnet' | 'remoteDevnet' | 'wallet' | 'manual' | 'customNetwork' export interface Transaction { type: 'deploy' | 'invoke' @@ -21,10 +21,8 @@ export const mockManualChain: Chain = { decimals: 18 }, rpcUrls: { - default: - { http: [''], webSocket: [''] }, - public: - { http: [''], webSocket: [''] } + default: { http: [''], webSocket: [''] }, + public: { http: [''], webSocket: [''] } }, network: 'testnet', name: 'testnet', diff --git a/plugin/src/utils/misc.ts b/plugin/src/utils/misc.ts index 073321dd..04c3dee9 100644 --- a/plugin/src/utils/misc.ts +++ b/plugin/src/utils/misc.ts @@ -25,6 +25,8 @@ export const envName = (env: EnvType): string => { return 'Manual' case 'localDevnet': return 'Local Devnet' + case 'customNetwork': + return 'Custom Network' default: return 'Unknown' } diff --git a/plugin/src/utils/network.ts b/plugin/src/utils/network.ts index 9df7f5d1..3a365a5d 100644 --- a/plugin/src/utils/network.ts +++ b/plugin/src/utils/network.ts @@ -8,14 +8,17 @@ const remoteDevnetUrl = process.env.VITE_REMOTE_DEVNET_URL ?? 'https://zksync-de interface Devnet { name: string url: string + id: string } const devnets: Devnet[] = [ { + id: 'localDevnet', name: 'Local Devnet', url: devnetUrl }, { + id: 'remoteDevnet', name: 'Remote Devnet', url: remoteDevnetUrl }