From 8208fc5a4a67a12d03ad4f59675c5e08627df617 Mon Sep 17 00:00:00 2001 From: Qiwei Yang Date: Tue, 27 Feb 2024 20:42:45 +0800 Subject: [PATCH 01/34] Integrate chopsticks (#9965) * fork locally feature * use chopsticks provider * update * test locally * update provider usage * upgrade chospticks * upgrade chopsticks * fix lint * remove extra code * add setStorage back with alice * update chopsticks * update chopsticks * updade chospticks version * fix: chopsticks version * fix: update and testing * fix: remove isLocalFork from global var --- packages/apps/package.json | 1 + packages/apps/public/locales/en/apps.json | 2 + .../apps/public/locales/en/translation.json | 2 + .../apps/public/locales/zh/translation.json | 1 + packages/apps/src/Apps.tsx | 4 +- packages/apps/src/Endpoints/index.tsx | 60 +++- packages/apps/src/overlays/Base.tsx | 4 +- packages/apps/src/overlays/Bottom.tsx | 40 +++ packages/apps/src/overlays/LocalFork.tsx | 44 +++ packages/react-api/src/Api.tsx | 30 +- packages/react-components/src/Sidebar.tsx | 4 +- yarn.lock | 271 +++++++++++++++++- 12 files changed, 448 insertions(+), 15 deletions(-) create mode 100644 packages/apps/src/overlays/Bottom.tsx create mode 100644 packages/apps/src/overlays/LocalFork.tsx diff --git a/packages/apps/package.json b/packages/apps/package.json index 55ea76f99843..e68d22475814 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -16,6 +16,7 @@ "type": "module", "version": "0.133.2-96-x", "dependencies": { + "@acala-network/chopsticks-core": "^0.9.8", "@polkadot/apps-config": "^0.133.2-96-x", "@polkadot/apps-routing": "^0.133.2-96-x", "@polkadot/dev": "^0.78.4", diff --git a/packages/apps/public/locales/en/apps.json b/packages/apps/public/locales/en/apps.json index 543fecf64ac3..fd72d6ed2ae4 100644 --- a/packages/apps/public/locales/en/apps.json +++ b/packages/apps/public/locales/en/apps.json @@ -2,8 +2,10 @@ "Accounts": "Accounts", "Developer": "Developer", "Files": "Files", + "Fork Locally": "Fork Locally", "Governance": "Governance", "Initializing connection": "Initializing connection", + "Local fork powered by ": "Local fork powered by ", "Network": "Network", "Relay chain": "Relay chain", "Settings": "Settings", diff --git a/packages/apps/public/locales/en/translation.json b/packages/apps/public/locales/en/translation.json index 91715a213e40..24d448cb3126 100644 --- a/packages/apps/public/locales/en/translation.json +++ b/packages/apps/public/locales/en/translation.json @@ -277,6 +277,7 @@ "Forget": "", "Forget this account": "", "Forget this address": "", + "Fork Locally": "", "Forks": "", "Free balance": "", "From JSON": "", @@ -338,6 +339,7 @@ "Lifetime (# of blocks)": "", "Light theme": "", "Loading": "", + "Local fork powered by ": "", "Locked funds (e.g. for staking) are counted.": "", "Locked1x": "", "Locked2x": "", diff --git a/packages/apps/public/locales/zh/translation.json b/packages/apps/public/locales/zh/translation.json index 5cd5f22c7acd..1d64b7acc6ef 100644 --- a/packages/apps/public/locales/zh/translation.json +++ b/packages/apps/public/locales/zh/translation.json @@ -215,6 +215,7 @@ "Forget this asset": "忘记这个资产", "Forget this code hash": "忘记这个代码哈希", "Forget this contract": "忘记该合约", + "Fork Locally": "本地分叉", "Forks": "(Forks)分叉", "Full Legal Name": "法定全称", "Future versions of the web-only interface will drop support for non-external accounts, much like the IPFS version.": "未来版本的纯 web 界面将不再支持非外部帐户,这与 IPFS 版本非常相似。", diff --git a/packages/apps/src/Apps.tsx b/packages/apps/src/Apps.tsx index 2a52f0de41ba..0236ddd51e1a 100644 --- a/packages/apps/src/Apps.tsx +++ b/packages/apps/src/Apps.tsx @@ -12,8 +12,8 @@ import Signer from '@polkadot/react-signer'; import Content from './Content/index.js'; import Menu from './Menu/index.js'; +import BottomOverlay from './overlays/Bottom.js'; import ConnectingOverlay from './overlays/Connecting.js'; -import DotAppsOverlay from './overlays/DotApps.js'; import WarmUp from './WarmUp.js'; export const PORTAL_ID = 'portals'; @@ -39,7 +39,7 @@ function Apps ({ className = '' }: Props): React.ReactElement { - +
diff --git a/packages/apps/src/Endpoints/index.tsx b/packages/apps/src/Endpoints/index.tsx index a5efd98a69d3..9eb8d2d68499 100644 --- a/packages/apps/src/Endpoints/index.tsx +++ b/packages/apps/src/Endpoints/index.tsx @@ -120,7 +120,11 @@ function loadAffinities (groups: Group[]): Record { function isSwitchDisabled (hasUrlChanged: boolean, apiUrl: string, isUrlValid: boolean): boolean { if (!hasUrlChanged) { - return true; + if (store.get('isLocalFork')) { + return false; + } else { + return true; + } } else if (apiUrl.startsWith('light://')) { return false; } else if (isUrlValid) { @@ -130,6 +134,22 @@ function isSwitchDisabled (hasUrlChanged: boolean, apiUrl: string, isUrlValid: b return true; } +function isLocalForkDisabled (hasUrlChanged: boolean, apiUrl: string, isUrlValid: boolean): boolean { + if (!hasUrlChanged) { + if (store.get('isLocalFork')) { + return true; + } else { + return false; + } + } else if (apiUrl.startsWith('light://')) { + return true; + } else if (isUrlValid) { + return false; + } + + return true; +} + function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElement { const { t } = useTranslation(); const linkOptions = createWsEndpoints(t); @@ -223,12 +243,32 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem const _onApply = useCallback( (): void => { + store.set('isLocalFork', false); + settings.set({ ...(settings.get()), apiUrl }); + window.location.assign(`${window.location.origin}${window.location.pathname}?rpc=${encodeURIComponent(apiUrl)}${window.location.hash}`); + + if (!hasUrlChanged) { + window.location.reload(); + } + + onClose(); + }, + [apiUrl, onClose, hasUrlChanged] + ); + + const _onLocalFork = useCallback( + (): void => { + store.set('isLocalFork', true); settings.set({ ...(settings.get()), apiUrl }); window.location.assign(`${window.location.origin}${window.location.pathname}?rpc=${encodeURIComponent(apiUrl)}${window.location.hash}`); - // window.location.reload(); + + if (!hasUrlChanged) { + window.location.reload(); + } + onClose(); }, - [apiUrl, onClose] + [apiUrl, onClose, hasUrlChanged] ); const _saveApiEndpoint = useCallback( @@ -249,6 +289,11 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem [hasUrlChanged, apiUrl, isUrlValid] ); + const canLocalFork = useMemo( + () => isLocalForkDisabled(hasUrlChanged, apiUrl, isUrlValid), + [hasUrlChanged, apiUrl, isUrlValid] + ); + return ( + } sidebarRef={sidebarRef} > {groups.map((group, index): React.ReactNode => ( diff --git a/packages/apps/src/overlays/Base.tsx b/packages/apps/src/overlays/Base.tsx index 3eb125d7ba29..269190ffdba6 100644 --- a/packages/apps/src/overlays/Base.tsx +++ b/packages/apps/src/overlays/Base.tsx @@ -60,8 +60,8 @@ const StyledDiv = styled.div` z-index: 500; &.isBottom { - bottom: 0.75rem; - top: auto; + position: static; + z-index: 0; } &.isFull { diff --git a/packages/apps/src/overlays/Bottom.tsx b/packages/apps/src/overlays/Bottom.tsx new file mode 100644 index 000000000000..f83239b346c6 --- /dev/null +++ b/packages/apps/src/overlays/Bottom.tsx @@ -0,0 +1,40 @@ +// Copyright 2017-2024 @polkadot/apps authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { styled } from '@polkadot/react-components'; + +import DotApps from './DotApps.js'; +import LocalFork from './LocalFork.js'; + +interface Props { + className?: string; +} + +function Bottom ({ className }: Props): React.ReactElement | null { + return ( + + + + + ); +} + +const StyledDiv = styled.div` + position: fixed; + bottom: 0.75rem; + right: 0.75rem; + left: 0.75rem; + top: auto; + padding: 1rem; + z-index: 500; + display: flex; + flex-direction: column; + row-gap: 0.75rem;; + div.isInfo:before { + content: none; + } +`; + +export default React.memo(Bottom); diff --git a/packages/apps/src/overlays/LocalFork.tsx b/packages/apps/src/overlays/LocalFork.tsx new file mode 100644 index 000000000000..f7756115fd29 --- /dev/null +++ b/packages/apps/src/overlays/LocalFork.tsx @@ -0,0 +1,44 @@ +// Copyright 2017-2024 @polkadot/apps authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import store from 'store'; + +import { useTranslation } from '../translate.js'; +import BaseOverlay from './Base.js'; + +interface Props { + className?: string; +} + +function LocalFork ({ className }: Props): React.ReactElement | null { + const { t } = useTranslation(); + + if (store.get('isLocalFork')) { + return ( + +
+ {t('Local fork powered by ')} + + Chopsticks + + . +
+
+ ); + } + + return null; +} + +export default React.memo(LocalFork); diff --git a/packages/react-api/src/Api.tsx b/packages/react-api/src/Api.tsx index 3ba9e4bd1e66..28865917c0e0 100644 --- a/packages/react-api/src/Api.tsx +++ b/packages/react-api/src/Api.tsx @@ -7,6 +7,7 @@ import type { ChainProperties, ChainType } from '@polkadot/types/interfaces'; import type { KeyringStore } from '@polkadot/ui-keyring/types'; import type { ApiProps, ApiState } from './types.js'; +import { ChopsticksProvider, setStorage } from '@acala-network/chopsticks-core'; import * as Sc from '@substrate/connect'; import React, { useEffect, useMemo, useState } from 'react'; import store from 'store'; @@ -232,11 +233,23 @@ async function getLightProvider (chain: string): Promise { async function createApi (apiUrl: string, signer: ApiSigner, onError: (error: unknown) => void): Promise>> { const types = getDevTypes(); const isLight = apiUrl.startsWith('light://'); + let provider; try { - const provider = isLight - ? await getLightProvider(apiUrl.replace('light://', '')) - : new WsProvider(apiUrl); + if (isLight) { + provider = await getLightProvider(apiUrl.replace('light://', '')); + } else if (store.get('isLocalFork')) { + provider = await ChopsticksProvider.fromEndpoint(apiUrl); + await setStorage(provider.chain, { + System: { + Account: [ + [['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], { data: { free: 1000 * 1e12 }, providers: 1 }] + ] + } + }); + } else { + provider = new WsProvider(apiUrl); + } statics.api = new ApiPromise({ provider, @@ -257,7 +270,7 @@ async function createApi (apiUrl: string, signer: ApiSigner, onError: (error: un return types; } -export function ApiCtxRoot ({ apiUrl, children, isElectron, store }: Props): React.ReactElement | null { +export function ApiCtxRoot ({ apiUrl, children, isElectron, store: keyringStore }: Props): React.ReactElement | null { const { queuePayload, queueSetTxStatus } = useQueue(); const [state, setState] = useState(EMPTY_STATE); const [isApiConnected, setIsApiConnected] = useState(false); @@ -303,15 +316,20 @@ export function ApiCtxRoot ({ apiUrl, children, isElectron, store }: Props): Rea const urlIsEthereum = !!location.href.includes('keyring-type=ethereum'); - loadOnReady(statics.api, apiEndpoint, injectedPromise, store, types, urlIsEthereum) + loadOnReady(statics.api, apiEndpoint, injectedPromise, keyringStore, types, urlIsEthereum) .then(setState) .catch(onError); }); + if (store.get('isLocalFork')) { + statics.api.connect() + .catch(onError); + } + setIsApiInitialized(true); }) .catch(onError); - }, [apiEndpoint, apiUrl, queuePayload, queueSetTxStatus, store]); + }, [apiEndpoint, apiUrl, queuePayload, queueSetTxStatus, keyringStore]); if (!value.isApiInitialized) { return null; diff --git a/packages/react-components/src/Sidebar.tsx b/packages/react-components/src/Sidebar.tsx index 472a8151a882..da24229376de 100644 --- a/packages/react-components/src/Sidebar.tsx +++ b/packages/react-components/src/Sidebar.tsx @@ -8,6 +8,7 @@ import { styled } from './styled.js'; interface Props { button?: React.ReactNode; + secondaryButton?: React.ReactNode; children: React.ReactNode; className?: string; dataTestId?: string; @@ -17,7 +18,7 @@ interface Props { sidebarRef: React.RefObject; } -function Sidebar ({ button, children, className = '', dataTestId = '', onClose, position, sidebarRef }: Props): React.ReactElement { +function Sidebar ({ button, children, className = '', dataTestId = '', onClose, position, secondaryButton, sidebarRef }: Props): React.ReactElement { return ( {button} + {secondaryButton}