From 11006ea576cdaebe50f4ca4e26edc78f7e1df777 Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Wed, 7 Aug 2024 16:13:34 +0200 Subject: [PATCH 01/12] feat: env.example --- .env => .env.example | 0 README.md | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename .env => .env.example (100%) diff --git a/.env b/.env.example similarity index 100% rename from .env rename to .env.example diff --git a/README.md b/README.md index 57c4c34..710519c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Perform token transfers using Snowbridge. ### Configure -On the `.env`-file, configure of your project by assigning the values that fit your needs. +Copy the `.env.example`-file into an `.env`-file on the same scope. +Then, on the `.env`-file, configure of your project by assigning the values that fit your needs. **NEXT_PUBLIC_SNOWBRIDGE_ENV** accepts: From c74e67d79ef335d9b7be286d520a72e124cb9650 Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Tue, 20 Aug 2024 19:14:10 +0200 Subject: [PATCH 02/12] fix: use a single .env file --- .env.example | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index a055d4d..85348bb 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,8 @@ # - `kusama_mainnet` for Kusama <=> Ethereum bridge. (TBD) # - `polkadot_mainnet` for Polkadot <=> Ethereum bridge. NEXT_PUBLIC_SNOWBRIDGE_ENV=local_e2e + + +# Indexers A.P.I. Keys: +NEXT_PUBLIC_ALCHEMY_KEY= +NEXT_PUBLIC_SUBSCAN_KEY= \ No newline at end of file diff --git a/README.md b/README.md index 710519c..49ab347 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Then, on the `.env`-file, configure of your project by assigning the values that NEXT_PUBLIC_SNOWBRIDGE_ENV=rococo_sepolia ``` -If you are not using local chains, create an `.env.local` to set the required A.P.I. keys. +If you are not using local chains, set the required A.P.I. keys. ```env NEXT_PUBLIC_ALCHEMY_KEY=... From 71e349f61c1db38ddb6bb1f8fba09764ad8ab310 Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Fri, 16 Aug 2024 13:39:52 +0200 Subject: [PATCH 03/12] feat: command to fix all eslint problems --- __tests__/countryBlock.test.ts | 8 ++++++-- app/history/page.tsx | 12 ++++++++---- app/status/api/route.ts | 4 +++- app/status/page.tsx | 8 ++++++-- components/BridgeStatus.tsx | 4 +++- components/BusyDialog.tsx | 4 +++- components/ErrorDialog.tsx | 4 +++- components/Transfer.tsx | 17 ++++++++++++----- hooks/useBridgeStatus.ts | 4 +++- hooks/useConnectEthereumWallet.ts | 4 +++- hooks/useConnectPolkadotWallet.ts | 8 ++++++-- hooks/useEthereumProvider.ts | 8 ++++++-- hooks/useSnowbridgeContext.ts | 4 +++- hooks/useSwitchEthereumNetwork.ts | 4 +++- lib/snowbridge.ts | 15 +++++++++------ package.json | 1 + store/polkadot.ts | 4 +++- utils/doApproveSpend.ts | 4 +++- utils/doDepositAndApproveWeth.ts | 4 +++- utils/formatting.ts | 12 +++++++++--- 20 files changed, 96 insertions(+), 37 deletions(-) diff --git a/__tests__/countryBlock.test.ts b/__tests__/countryBlock.test.ts index 28add09..44f2194 100644 --- a/__tests__/countryBlock.test.ts +++ b/__tests__/countryBlock.test.ts @@ -27,8 +27,12 @@ function countryBlockCase( when(mockedRequest.nextUrl).thenReturn(nextUrl); const headers = new Headers(); - if (country) headers.set(COUNTRY_HEADER_NAME, country); - if (region) headers.set(REGION_HEADER_NAME, region); + if (country) { + headers.set(COUNTRY_HEADER_NAME, country); + } + if (region) { + headers.set(REGION_HEADER_NAME, region); + } when(mockedRequest.headers).thenReturn(headers); const result = middleware(instance(mockedRequest)); diff --git a/app/history/page.tsx b/app/history/page.tsx index 5acddec..4751994 100644 --- a/app/history/page.tsx +++ b/app/history/page.tsx @@ -287,8 +287,8 @@ const transferTitle = ( history.TransferStatus.Failed == transfer.status ? " bg-destructive" : history.TransferStatus.Pending == transfer.status - ? "" - : "bg-secondary"; + ? "" + : "bg-secondary"; const { tokenName, amount } = formatTokenData( transfer, @@ -464,7 +464,9 @@ export default function History() { const hashItem = useWindowHash(); useEffect(() => { - if (transfers === null) return; + if (transfers === null) { + return; + } setTransferHistoryCache(transfers); }, [transfers, setTransferHistoryCache]); @@ -505,7 +507,9 @@ export default function History() { transfer.info.sourceAddress, transfer.info.beneficiaryAddress, ); - if (!showGlobal && !transfer.isWalletTransaction) continue; + if (!showGlobal && !transfer.isWalletTransaction) { + continue; + } allTransfers.push(transfer); } diff --git a/app/status/api/route.ts b/app/status/api/route.ts index c8301c2..65314a7 100644 --- a/app/status/api/route.ts +++ b/app/status/api/route.ts @@ -19,7 +19,9 @@ const CACHE_REVALIDATE_IN_SECONDS = 60; // 1 minutes let context: Context | null = null; async function getContext() { - if (context) return context; + if (context) { + return context; + } const env = getEnvironment(); const alchemyKey = process.env.NEXT_PUBLIC_ALCHEMY_KEY; diff --git a/app/status/page.tsx b/app/status/page.tsx index 3a4803f..b75b5da 100644 --- a/app/status/page.tsx +++ b/app/status/page.tsx @@ -91,7 +91,9 @@ const StatusCard = () => { const hash = useWindowHash(); const diagnostic = hash === "diagnostic"; - if (status == null) return ; + if (status == null) { + return ; + } const toPolkadotStyle = status.summary.toPolkadotOperatingMode === "Normal" @@ -105,7 +107,9 @@ const StatusCard = () => { status.summary.overallStatus === "Normal" ? "text-green-700 font-semibold" : "text-red-700 font-semibold"; - if (status == null) return ; + if (status == null) { + return ; + } return ( <> diff --git a/components/BridgeStatus.tsx b/components/BridgeStatus.tsx index 12611aa..795647f 100644 --- a/components/BridgeStatus.tsx +++ b/components/BridgeStatus.tsx @@ -15,7 +15,9 @@ const StatusCard = () => { const { data: bridgeStatus } = useBridgeStatus(); const pathname = usePathname(); - if (bridgeStatus == null) return ; + if (bridgeStatus == null) { + return ; + } const toPolkadotStyle = bridgeStatus.summary.toPolkadotOperatingMode === "Normal" diff --git a/components/BusyDialog.tsx b/components/BusyDialog.tsx index 1bffe2c..b749a52 100644 --- a/components/BusyDialog.tsx +++ b/components/BusyDialog.tsx @@ -27,7 +27,9 @@ export const BusyDialog: FC = ({ { - if (!a && dismiss) dismiss(); + if (!a && dismiss) { + dismiss(); + } }} > diff --git a/components/ErrorDialog.tsx b/components/ErrorDialog.tsx index c233e0d..8b6c27d 100644 --- a/components/ErrorDialog.tsx +++ b/components/ErrorDialog.tsx @@ -27,7 +27,9 @@ export const ErrorDialog: FC = ({ { - if (!a && dismiss) dismiss(); + if (!a && dismiss) { + dismiss(); + } }} > diff --git a/components/Transfer.tsx b/components/Transfer.tsx index f3e6da8..6e4fd80 100644 --- a/components/Transfer.tsx +++ b/components/Transfer.tsx @@ -106,7 +106,7 @@ export const TransferComponent: FC = () => { .toLowerCase() .trim() === "true"; - if (maintenance) + if (maintenance) { return (
@@ -115,6 +115,7 @@ export const TransferComponent: FC = () => {

Under Maintenance: Check back soon!

); + } return ; }; @@ -167,7 +168,9 @@ export const TransferForm: FC = () => { }); useEffect(() => { - if (context == null) return; + if (context == null) { + return; + } switch (source.type) { case "substrate": { toEthereum @@ -290,7 +293,9 @@ export const TransferForm: FC = () => { ]); useEffect(() => { - if (context == null) return; + if (context == null) { + return; + } if (assetErc20MetaData !== null && assetErc20MetaData[token]) { setTokenMetadata(assetErc20MetaData[token]); return; @@ -328,8 +333,9 @@ export const TransferForm: FC = () => { ethereumChainId == null || token === "" || tokenMetadata == null - ) + ) { return; + } updateBalance( context, ethereumChainId, @@ -431,8 +437,9 @@ export const TransferForm: FC = () => { context == null || ethereumChainId == null || sourceAccount == undefined - ) + ) { return; + } const toastTitle = "Approve Token Spend"; setBusyMessage("Approving spend..."); try { diff --git a/hooks/useBridgeStatus.ts b/hooks/useBridgeStatus.ts index 7afca2f..36f9d01 100644 --- a/hooks/useBridgeStatus.ts +++ b/hooks/useBridgeStatus.ts @@ -20,7 +20,9 @@ const fetchStatus = async ([env, context]: [ ]): Promise => { if (process.env.NEXT_PUBLIC_USE_CLIENT_SIDE_HISTORY_FETCH === "true") { try { - if (context === null) return null; + if (context === null) { + return null; + } return await getBridgeStatus(context, env); } catch (err) { console.error(err); diff --git a/hooks/useConnectEthereumWallet.ts b/hooks/useConnectEthereumWallet.ts index 059a74d..465582c 100644 --- a/hooks/useConnectEthereumWallet.ts +++ b/hooks/useConnectEthereumWallet.ts @@ -39,7 +39,9 @@ export const useConnectEthereumWallet = (): [ } } catch (err) { let message = "Unknown Error"; - if (err instanceof Error) message = err.message; + if (err instanceof Error) { + message = err.message; + } setError(message); } setLoading(false); diff --git a/hooks/useConnectPolkadotWallet.ts b/hooks/useConnectPolkadotWallet.ts index e5db0a9..76baa51 100644 --- a/hooks/useConnectPolkadotWallet.ts +++ b/hooks/useConnectPolkadotWallet.ts @@ -16,7 +16,9 @@ export const useConnectPolkadotWallet = (ss58Format?: number): void => { const [walletName] = useAtom(walletNameAtom); useEffect(() => { - if (wallet != null || walletName == null) return; + if (wallet != null || walletName == null) { + return; + } let unmounted = false; const connect = async (): Promise => { const { getWalletBySource } = await import("@talismn/connect-wallets"); @@ -43,7 +45,9 @@ export const useConnectPolkadotWallet = (ss58Format?: number): void => { let unsub: () => void; let unmounted = false; const saveAccounts = (accounts?: WalletAccount[]): void => { - if (accounts == null || unmounted) return; + if (accounts == null || unmounted) { + return; + } if (ss58Format === undefined) { setAccounts(accounts); } else { diff --git a/hooks/useEthereumProvider.ts b/hooks/useEthereumProvider.ts index 2b84f89..9f6cc83 100644 --- a/hooks/useEthereumProvider.ts +++ b/hooks/useEthereumProvider.ts @@ -30,10 +30,14 @@ export const useEthereumProvider = () => { const setEthereumChainId = useSetAtom(ethereumChainIdAtom); useEffect(() => { - if (ethereumProvider != null) return; + if (ethereumProvider != null) { + return; + } const init = async (): Promise => { const provider = await getEthereumProvider(); - if (provider == null) return; + if (provider == null) { + return; + } const updateAccounts = (accounts: string[]): void => { setEthereumAccount(accounts[0] ?? null); setEthereumAccounts(accounts); diff --git a/hooks/useSnowbridgeContext.ts b/hooks/useSnowbridgeContext.ts index 9985ad7..a68a05b 100644 --- a/hooks/useSnowbridgeContext.ts +++ b/hooks/useSnowbridgeContext.ts @@ -87,7 +87,9 @@ export const useSnowbridgeContext = (): [ }) .catch((error) => { let message = "Unknown Error"; - if (error instanceof Error) message = error.message; + if (error instanceof Error) { + message = error.message; + } setLoading(false); setError(message); }); diff --git a/hooks/useSwitchEthereumNetwork.ts b/hooks/useSwitchEthereumNetwork.ts index 53ac192..032b650 100644 --- a/hooks/useSwitchEthereumNetwork.ts +++ b/hooks/useSwitchEthereumNetwork.ts @@ -14,7 +14,9 @@ export const useSwitchEthereumNetwork = (): { const shouldSwitchNetwork = providerChainID !== envChainId; const switchNetwork = useCallback(async () => { - if (!shouldSwitchNetwork || ethereum === null) return; + if (!shouldSwitchNetwork || ethereum === null) { + return; + } const chainIdHex = `0x${envChainId.toString(16)}`; try { await ethereum.request({ diff --git a/lib/snowbridge.ts b/lib/snowbridge.ts index 81f68b8..10cbcc9 100644 --- a/lib/snowbridge.ts +++ b/lib/snowbridge.ts @@ -23,17 +23,20 @@ export const ACCEPTABLE_BRIDGE_LATENCY = 28800; // 8 hours export function getEnvironmentName() { const name = process.env.NEXT_PUBLIC_SNOWBRIDGE_ENV; - if (!name) throw new Error("NEXT_PUBLIC_SNOWBRIDGE_ENV var not configured."); + if (!name) { + throw new Error("NEXT_PUBLIC_SNOWBRIDGE_ENV var not configured."); + } return name; } export function getEnvironment() { const envName = getEnvironmentName(); const env = environment.SNOWBRIDGE_ENV[envName]; - if (env === undefined) + if (env === undefined) { throw new Error( `NEXT_PUBLIC_SNOWBRIDGE_ENV configured for unknown environment '${envName}'`, ); + } return env; } @@ -325,8 +328,8 @@ export async function getBridgeStatus( !toPolkadot.bridgeOperational || !toPolkadot.channelOperational ? "Halted" : !toPolkadot.lightClientLatencyIsAcceptable - ? "Delayed" - : "Normal"; + ? "Delayed" + : "Normal"; const toEthereum = { bridgeOperational: @@ -337,8 +340,8 @@ export async function getBridgeStatus( const toEthereumOperatingMode = !toEthereum.bridgeOperational ? "Halted" : !toEthereum.lightClientLatencyIsAcceptable - ? "Delayed" - : "Normal"; + ? "Delayed" + : "Normal"; let overallStatus: StatusValue = toEthereumOperatingMode; if (toEthereumOperatingMode === "Normal") { diff --git a/package.json b/package.json index c1b3133..40ca27f 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "jest && next build", "start": "next start", "lint": "eslint .", + "lint:fix": "eslint --fix .", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json}\"", "test": "jest" }, diff --git a/store/polkadot.ts b/store/polkadot.ts index 935a764..6ffa51b 100644 --- a/store/polkadot.ts +++ b/store/polkadot.ts @@ -10,7 +10,9 @@ const polkadotAccountAddressAtom = atomWithStorage( export const polkadotAccountAtom = atom( (get) => { const polkadotAccountAddress = get(polkadotAccountAddressAtom); - if (polkadotAccountAddress == null) return null; + if (polkadotAccountAddress == null) { + return null; + } return ( get(polkadotAccountsAtom)?.find( (account) => account.address === get(polkadotAccountAddressAtom), diff --git a/utils/doApproveSpend.ts b/utils/doApproveSpend.ts index 6444be7..85f5068 100644 --- a/utils/doApproveSpend.ts +++ b/utils/doApproveSpend.ts @@ -8,7 +8,9 @@ export async function doApproveSpend( token: string, amount: bigint, ): Promise { - if (context == null || ethereumProvider == null) return; + if (context == null || ethereumProvider == null) { + return; + } const signer = await ethereumProvider.getSigner(); const response = await toPolkadot.approveTokenSpend( diff --git a/utils/doDepositAndApproveWeth.ts b/utils/doDepositAndApproveWeth.ts index f363db1..4144f97 100644 --- a/utils/doDepositAndApproveWeth.ts +++ b/utils/doDepositAndApproveWeth.ts @@ -9,7 +9,9 @@ export async function doDepositAndApproveWeth( token: string, amount: bigint, ): Promise { - if (context == null || ethereumProvider == null) return; + if (context == null || ethereumProvider == null) { + return; + } const signer = await ethereumProvider.getSigner(); const response = await toPolkadot.depositWeth(context, signer, token, amount); diff --git a/utils/formatting.ts b/utils/formatting.ts index 3126594..c4a52ae 100644 --- a/utils/formatting.ts +++ b/utils/formatting.ts @@ -24,7 +24,9 @@ export function formatBalance({ }): string { const replaceZeros = (str: string): string => { const newStr = str.replace(/(\.0+)$/, "").replace(/(0+)$/, ""); - if (newStr !== "") return newStr; + if (newStr !== "") { + return newStr; + } return "0"; }; @@ -42,8 +44,12 @@ export function formatTime(time: number): string { let minutes = Math.floor((time % 3600) / 60); let seconds = Math.floor(time % 60); let fmt = ""; - if (hours > 0) fmt += `${hours}h `; - if (minutes > 0) fmt += `${minutes}m `; + if (hours > 0) { + fmt += `${hours}h `; + } + if (minutes > 0) { + fmt += `${minutes}m `; + } fmt += `${seconds}s`; return fmt; } From 2136630e9e1e83932835772cba8bd79e8c0f7f52 Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Fri, 30 Aug 2024 18:45:40 +0200 Subject: [PATCH 04/12] feat: api getters --- utils/parachainConfigs/getApi.ts | 79 ++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 utils/parachainConfigs/getApi.ts diff --git a/utils/parachainConfigs/getApi.ts b/utils/parachainConfigs/getApi.ts new file mode 100644 index 0000000..8c1aa2f --- /dev/null +++ b/utils/parachainConfigs/getApi.ts @@ -0,0 +1,79 @@ +import { ApiPromise, HttpProvider, WsProvider } from "@polkadot/api"; + +/** Attempts to establish an API connection with a _Substrate_ blockchain node using the provided URL `wsUrl`. + * + * If the initial connection is successful, returns a ready to use `ApiPromise` instance; otherwise returns `undefined`. + * + * If the WebSocket turns unavailable after the initial connection, it will persistently retry to connect. + * + * @param wsUrl HTTP or WS endpoint of a _Substrate_ blockchain node. + * @returns the api `ApiPromise` if connection was established or `undefined` otherwise. + */ +export async function getSubstApi( + wsUrl: string, +): Promise { + const provider = wsUrl.startsWith("http") + ? new HttpProvider(wsUrl) + : new WsProvider(wsUrl); + try { + // // #1 Variant + // const api = await ApiPromise.create({ + // provider, + // throwOnConnect: true, + // }); + + // return api; + + // #2 Variant + const api = new ApiPromise({ + provider, + }); + + return await api.isReadyOrError; + } catch (error) { + console.error( + `Could not connect to API under ${wsUrl}. Because: ${error instanceof Error ? error.message : JSON.stringify(error)}`, + ); + + // stop from trying to reconnect to the webSocket + provider.disconnect(); + return undefined; + } +} + +import { AbstractProvider, JsonRpcProvider, WebSocketProvider } from "ethers"; + +/** + * Attempts to establish a connection with an _Ethereum_ node using the provided URL `nodeUrl`. + * + * If the initial connection is successful, it returns a ready to use "provider" API instance. + * + * When passing a HTTP endpoint: + * If the connection attempt fails, the function returns `undefined`. + * + * Sadly, when passing a WebSocket endpoint: + * If the connection attempt fails, an error will be thrown asynchronously, crashing the app. + * + * @param nodeUrl - The HTTP or WebSocket endpoint of an Ethereum node. + * @returns A promise that resolves to a provider instance if the connection is established, or `undefined` if the connection attempt fails. + */ +export async function getEtherApi( + nodeUrl: string, +): Promise { + const provider = nodeUrl.startsWith("http") + ? new JsonRpcProvider(nodeUrl) + : new WebSocketProvider(nodeUrl, undefined, { polling: true }); + + try { + // Verify the connection is successful by making a basic request + await provider.getBlockNumber(); + + return provider; + } catch (err) { + console.error( + `Could not connect to Ethereum node at ${nodeUrl}. Reason: ${err instanceof Error ? err.message : JSON.stringify(err)}`, + ); + provider.destroy(); + return undefined; + } +} From 1395958ce09d22d4a6ded4224fa81d7387d4f33c Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Fri, 30 Aug 2024 18:48:46 +0200 Subject: [PATCH 05/12] feat: utils for the parachain configs --- utils/parachainConfigs/paraUtils.ts | 115 ++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 utils/parachainConfigs/paraUtils.ts diff --git a/utils/parachainConfigs/paraUtils.ts b/utils/parachainConfigs/paraUtils.ts new file mode 100644 index 0000000..81e7a6e --- /dev/null +++ b/utils/parachainConfigs/paraUtils.ts @@ -0,0 +1,115 @@ +import { ApiPromise } from "@polkadot/api"; +import { Bytes, Option, Struct, TypeDefInfo, u32 } from "@polkadot/types"; +import { H256 } from "@polkadot/types/interfaces"; + +import { + AddressType, + SnowbridgeEnvironment, +} from "@snowbridge/api/dist/environment"; + +import { getSubstApi } from "./getApi"; + +// explicit Definition: +interface PolkadotPrimitivesV5PersistedValidationData extends Struct { + readonly parentHead: Bytes; + readonly relayParentNumber: u32; + readonly relayParentStorageRoot: H256; + readonly maxPovSize: u32; +} + +async function getRelaysChainLastParentBlockInfo(api: ApiPromise) { + const validationData = + await api.query.parachainSystem.validationData< + Option + >(); + + if (validationData.isNone) { + throw new Error( + "This is not a parachain or validation data is unavailable", + ); + } + const { relayParentNumber, relayParentStorageRoot } = validationData.unwrap(); + + const lastRelayParentBlock = relayParentNumber.toNumber(); + const lastRelayParentBlockStorageRoot = relayParentStorageRoot.toHex(); + + // console.log("lastRelayParentBlock: ", lastRelayParentBlock); + // console.log( + // "lastRelayParentBlockStorageRoot: ", + // lastRelayParentBlockStorageRoot, + // ); + + return { + lastRelayParentBlock, + lastRelayParentBlockStorageRoot, + }; +} + +/** Returns to which `SnowbridgeEnvironment` the parachain under the give `paraApi` corresponds to. */ +export async function getSnowEnvBasedOnRelayChain( + paraApi: ApiPromise, + snowEnvironments: { [id: string]: SnowbridgeEnvironment }, +) { + const { lastRelayParentBlock, lastRelayParentBlockStorageRoot } = + await getRelaysChainLastParentBlockInfo(paraApi); + + const parachainName = await paraApi.rpc.system.chain(); + + const coldEnvironments = Object.values(snowEnvironments); + + for await (const env of coldEnvironments) { + const relayApi = await getSubstApi(env.config.RELAY_CHAIN_URL); + + if (!relayApi) { + continue; + } + + const examinedBlockHash = + await relayApi.rpc.chain.getBlockHash(lastRelayParentBlock); + + const examinedBlock = await relayApi.rpc.chain.getBlock(examinedBlockHash); + const relaychainName = await relayApi.rpc.system.chain(); + + await relayApi.disconnect(); + + if ( + examinedBlock.block.header.stateRoot.toHex() === + lastRelayParentBlockStorageRoot + ) { + console.log(`"${parachainName}" relays on chain: ${relaychainName}`); + return env.name; + } + console.log( + `"${parachainName}" does not relay on chain: ${relaychainName}`, + ); + } + + console.log( + `"${parachainName}" relays on a blockchain that is not part of the Snowbridge API.`, + ); + + return "unsupported_relaychain"; +} + +export async function getAddressType(api: ApiPromise): Promise { + // Assume that the first type defined in the runtime is the AccountId + const lookedUpType = api.registry.lookup.getTypeDef(0); + if (lookedUpType.type === "AccountId32") { + return "32byte"; + } + + if (lookedUpType.type === "AccountId20") { + return "20byte"; + } + + if (lookedUpType.info === TypeDefInfo.VecFixed) { + const length = lookedUpType.length; + if (length === 20) { + return "20byte"; + } + if (length === 32) { + return "32byte"; + } + } + return "both"; +} From 6628eeb9a33218a6a908238a837c74fbb5aae19c Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Fri, 30 Aug 2024 18:51:51 +0200 Subject: [PATCH 06/12] feat: function to build parachain configurations --- .../parachainConfigs/buildParachainConfig.ts | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 utils/parachainConfigs/buildParachainConfig.ts diff --git a/utils/parachainConfigs/buildParachainConfig.ts b/utils/parachainConfigs/buildParachainConfig.ts new file mode 100644 index 0000000..e82569c --- /dev/null +++ b/utils/parachainConfigs/buildParachainConfig.ts @@ -0,0 +1,149 @@ +import { parachainNativeAsset } from "@snowbridge/api/dist/assets"; +import { + SNOWBRIDGE_ENV, + TransferLocation, +} from "@snowbridge/api/dist/environment"; +// eslint-disable-next-line import/no-extraneous-dependencies +import { IERC20Metadata__factory } from "@snowbridge/contract-types"; + +import { getEtherApi, getSubstApi } from "./getApi"; +import { getAddressType, getSnowEnvBasedOnRelayChain } from "./paraUtils"; + +/** Mock up from: + * + * const snowbridgeEnvironmentNames = Object.keys(SNOWBRIDGE_ENV) as Array; + * + * type SnowbridgeEnvironmentNames = (typeof snowbridgeEnvironmentNames)[number]; */ +type SnowbridgeEnvironmentNames = + | "local_e2e" + | "rococo_sepolia" + | "polkadot_mainnet" + | "unsupported_relaychain"; + +interface ParaConfig { + name: string; + snowEnv: SnowbridgeEnvironmentNames; + endpoint: string; + pallet: string; + parachainId: number; + location: TransferLocation; +} + +export interface RegisterOfParaConfigs { + [name: string]: ParaConfig; +} +/** + * Gets all necessary Information about a _Substrate_ parachain to extend the Snowbridge Environment with it. + * + * It gathers information from a node of the parachain under `paraEndpoint` and the contract on the ethereum side through and _Alchemy_ node. + * + * If the name of the Switch Pallet is not passed, it assumes that first switch pool is between native token and its erc20 wrapped counterpart. + * + * @param paraEndpoint Endpoint of a Substrate Parachain Node. + * @param etherApiKey API Key to use connect to ether-node through Alchemy. + * @param switchPalletName Name of wished switch pallet on the parachain itself. + * @returns necessary data to extend the _Snowbridge Environment_ or `void` on failure. + */ +export async function buildParachainConfig( + paraEndpoint: string, + etherApiKey: string, + switchPalletName: string = "assetSwitchPool1", +): Promise { + const paraApi = await getSubstApi(paraEndpoint); + + if (!paraApi) { + console.log(`Could not connect to parachain API under "${paraEndpoint}"`); + return; + } + + const paraId = ( + await paraApi.query.parachainInfo.parachainId() + ).toPrimitive() as number; + + // Get information about the token on it's native parachain + const chainName = (await paraApi.rpc.system.chain()).toString(); + const snowBridgeEnvName = (await getSnowEnvBasedOnRelayChain( + paraApi, + SNOWBRIDGE_ENV, + )) as SnowbridgeEnvironmentNames; + + if (snowBridgeEnvName === "unsupported_relaychain") { + // error message already logged from getSnowEnvBasedOnRelayChain() + return; + } + + // debugger + console.log("snowBridgeEnvName: ", snowBridgeEnvName); + + /** The Snowbridge team decided to set the amount of the existential deposit as the minimal transfer amount. */ + const minimumTransferAmount = BigInt( + paraApi.consts.balances.existentialDeposit.toString(), + ); + + const { tokenDecimal } = await parachainNativeAsset(paraApi); + + const addressType = await getAddressType(paraApi); + + // debugger + console.log(`The address type used is: ${addressType}`); + + // Get information about the wrapped erc20 token from parachain + const switchPair = await paraApi.query[switchPalletName].switchPair(); + const contractAddress = (switchPair as any).unwrap().remoteAssetId.toJSON().v4 + .interior.x2[1].accountKey20.key; + + const xcmFee = (switchPair as any).unwrap().remoteXcmFee.toJSON().v4.fun + .fungible as number; + + // debuggers + console.log("contractAddress: ", contractAddress); + console.log("xcmFee: ", xcmFee); + + // Get information about the wrapped erc20 token from ethereum + const etherEndpoint = + SNOWBRIDGE_ENV[snowBridgeEnvName].config.ETHEREUM_API(etherApiKey); + const etherApi = await getEtherApi(etherEndpoint); + + if (!etherApi) { + console.log(`Could not connect to ethereum API under "${etherEndpoint}"`); + return; + } + + const ercTokenMetadata = IERC20Metadata__factory.connect( + contractAddress, + etherApi, + ); + const ercSymbol = await ercTokenMetadata.symbol(); + + paraApi.disconnect(); + etherApi.destroy(); + + return { + name: chainName, + snowEnv: snowBridgeEnvName, + endpoint: paraEndpoint, + pallet: switchPalletName, + parachainId: paraId, + location: { + id: chainName.toLowerCase().replaceAll(/\s/g, ""), + name: chainName, + type: "substrate", + destinationIds: ["assethub"], + paraInfo: { + paraId: paraId, + destinationFeeDOT: BigInt(xcmFee), + skipExistentialDepositCheck: false, + addressType: addressType, + decimals: tokenDecimal, + maxConsumers: 16, + }, + erc20tokensReceivable: [ + { + id: ercSymbol, + address: contractAddress, + minimumTransferAmount, + }, + ], + }, + }; +} From bac65d5001abb358aff1e85fdb05d1a398f32404 Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Fri, 30 Aug 2024 18:59:41 +0200 Subject: [PATCH 07/12] feat: populate parachain configs --- .env.example | 5 ++++- lib/snowbridge.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 85348bb..94da5d5 100644 --- a/.env.example +++ b/.env.example @@ -9,4 +9,7 @@ NEXT_PUBLIC_SNOWBRIDGE_ENV=local_e2e # Indexers A.P.I. Keys: NEXT_PUBLIC_ALCHEMY_KEY= -NEXT_PUBLIC_SUBSCAN_KEY= \ No newline at end of file +NEXT_PUBLIC_SUBSCAN_KEY= + +# parachain endpoints to build their configuration from node info. ";" separated +PARACHAIN_ENDPOINTS=wss://rilt.kilt.io \ No newline at end of file diff --git a/lib/snowbridge.ts b/lib/snowbridge.ts index 10cbcc9..af6083e 100644 --- a/lib/snowbridge.ts +++ b/lib/snowbridge.ts @@ -1,3 +1,7 @@ +import { + buildParachainConfig, + RegisterOfParaConfigs, +} from "@/utils/parachainConfigs/buildParachainConfig"; import { u8aToHex } from "@polkadot/util"; import { blake2AsU8a, encodeAddress } from "@polkadot/util-crypto"; import { @@ -21,6 +25,40 @@ export const HISTORY_IN_SECONDS = 60 * 60 * 24 * 7 * 2; // 2 Weeks export const ETHEREUM_BLOCK_TIME_SECONDS = 12; export const ACCEPTABLE_BRIDGE_LATENCY = 28800; // 8 hours +export const parachainConfigs: RegisterOfParaConfigs = {}; + +export async function populateParachainConfigs() { + const paraNodes = process.env.PARACHAIN_ENDPOINTS?.split(";"); + const etherApiKey = process.env.NEXT_PUBLIC_ALCHEMY_KEY; + + if (!paraNodes || !etherApiKey) { + return; + } + + for await (const endpoint of paraNodes) { + const newConfig = await buildParachainConfig(endpoint, etherApiKey); + + // debugger: + console.log( + "newConfig: ", + JSON.stringify( + newConfig, + (_, v) => (typeof v === "bigint" ? v.toString() : v), // replacer of bigInts + 2, + ), + ); + + if (!newConfig) { + return; + } + if (newConfig.name in parachainConfigs) { + // don't overwrite + } else { + parachainConfigs[newConfig.name] = newConfig; + } + } +} + export function getEnvironmentName() { const name = process.env.NEXT_PUBLIC_SNOWBRIDGE_ENV; if (!name) { From 0aac2626e05f28d2b15f963003c29b47e1360c3d Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Fri, 30 Aug 2024 19:07:57 +0200 Subject: [PATCH 08/12] feat: add parachains to snow env --- lib/snowbridge.ts | 61 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/lib/snowbridge.ts b/lib/snowbridge.ts index af6083e..77f19ee 100644 --- a/lib/snowbridge.ts +++ b/lib/snowbridge.ts @@ -38,16 +38,6 @@ export async function populateParachainConfigs() { for await (const endpoint of paraNodes) { const newConfig = await buildParachainConfig(endpoint, etherApiKey); - // debugger: - console.log( - "newConfig: ", - JSON.stringify( - newConfig, - (_, v) => (typeof v === "bigint" ? v.toString() : v), // replacer of bigInts - 2, - ), - ); - if (!newConfig) { return; } @@ -59,6 +49,56 @@ export async function populateParachainConfigs() { } } +function addParachains( + env: environment.SnowbridgeEnvironment, + parachainConfigs: RegisterOfParaConfigs, +) { + const assetHubLocation = env.locations.find(({ id }) => id === "assethub"); + if (!assetHubLocation) { + throw new Error( + `Could not find the asset hub configuration object inside of the chosen environment "${env.name}."`, + ); + } + const pertinentParaConfigs = Object.values(parachainConfigs).filter( + ({ snowEnv, location }) => + snowEnv === env.name && + !assetHubLocation.destinationIds.includes(location.id), + ); + + if (pertinentParaConfigs.length == 0) { + console.log( + `No suitable parachains to add to the given snowbridge environment "${env.name}".`, + ); + return; + } + + // add the parachains as destinations on the assetHub location + // and the corresponding tokens as receivable + + pertinentParaConfigs.forEach((paraConfig) => { + assetHubLocation.destinationIds.push(paraConfig.location.id); + assetHubLocation.erc20tokensReceivable.push( + ...paraConfig.location.erc20tokensReceivable, + ); + }); + + env.locations.push(...pertinentParaConfigs.map((para) => para.location)); + env.config.PARACHAINS.push( + ...pertinentParaConfigs.map((para) => para.endpoint), + ); + + // TODO: delete this log later + // during developing only: + console.log( + "SnowbridgeEnvironment after adding parachains: ", + JSON.stringify( + env, + (_, v) => (typeof v === "bigint" ? v.toString() : v), // replacer of bigInts + 2, + ), + ); +} + export function getEnvironmentName() { const name = process.env.NEXT_PUBLIC_SNOWBRIDGE_ENV; if (!name) { @@ -75,6 +115,7 @@ export function getEnvironment() { `NEXT_PUBLIC_SNOWBRIDGE_ENV configured for unknown environment '${envName}'`, ); } + addParachains(env, parachainConfigs); return env; } From 228b58812d8a7ecc04f55a3b9f02efdf7ea69621 Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Fri, 30 Aug 2024 19:08:57 +0200 Subject: [PATCH 09/12] chore: development comment --- store/snowbridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/snowbridge.ts b/store/snowbridge.ts index 627bd51..d129369 100644 --- a/store/snowbridge.ts +++ b/store/snowbridge.ts @@ -8,5 +8,5 @@ export const assetErc20MetaDataAtom = atom<{ } | null>(null); export const snowbridgeContextAtom = atom(null); -export const snowbridgeEnvNameAtom = atom((_) => getEnvironmentName()); +export const snowbridgeEnvNameAtom = atom((_) => getEnvironmentName()); // this one is unnecessary. snowbridgeEnvironmentAtom.name can be used instead export const snowbridgeEnvironmentAtom = atom((_) => getEnvironment()); From 26e6000a6c84587c960c9b2ff9ad0c959b83011f Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Mon, 2 Sep 2024 11:08:32 +0200 Subject: [PATCH 10/12] feat: log on positive case --- lib/snowbridge.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/snowbridge.ts b/lib/snowbridge.ts index 77f19ee..61dbe3a 100644 --- a/lib/snowbridge.ts +++ b/lib/snowbridge.ts @@ -97,6 +97,9 @@ function addParachains( 2, ), ); + console.log( + `Added this parachains to the "${env.name}" snowbridge environment: ${pertinentParaConfigs.map(({ name }) => name).join(";")}.`, + ); } export function getEnvironmentName() { From 71e4c949e783a4a6f10c8843000179ecbac07aee Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Mon, 2 Sep 2024 11:32:08 +0200 Subject: [PATCH 11/12] feat: function to print objects with BigInts --- lib/snowbridge.ts | 10 ++-------- utils/printify.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 utils/printify.ts diff --git a/lib/snowbridge.ts b/lib/snowbridge.ts index 61dbe3a..6ee54be 100644 --- a/lib/snowbridge.ts +++ b/lib/snowbridge.ts @@ -2,6 +2,7 @@ import { buildParachainConfig, RegisterOfParaConfigs, } from "@/utils/parachainConfigs/buildParachainConfig"; +import { printify } from "@/utils/printify"; import { u8aToHex } from "@polkadot/util"; import { blake2AsU8a, encodeAddress } from "@polkadot/util-crypto"; import { @@ -89,14 +90,7 @@ function addParachains( // TODO: delete this log later // during developing only: - console.log( - "SnowbridgeEnvironment after adding parachains: ", - JSON.stringify( - env, - (_, v) => (typeof v === "bigint" ? v.toString() : v), // replacer of bigInts - 2, - ), - ); + console.log("SnowbridgeEnvironment after adding parachains: ", printify(env)); console.log( `Added this parachains to the "${env.name}" snowbridge environment: ${pertinentParaConfigs.map(({ name }) => name).join(";")}.`, ); diff --git a/utils/printify.ts b/utils/printify.ts new file mode 100644 index 0000000..81cfecb --- /dev/null +++ b/utils/printify.ts @@ -0,0 +1,15 @@ +/** + * Turns `object` into a printable and human readable sting. + * + * It handles all JSON compatible types and BigInts. + * + * @param object + * @returns string representation of the object. + */ +export function printify(object: object) { + return JSON.stringify( + object, + (_, v) => (typeof v === "bigint" ? v.toString() : v), // replacer of bigInts + 2, + ); +} From 9cf8d636602f20459f9de6fd9f523c589f48acf0 Mon Sep 17 00:00:00 2001 From: kilted-andres Date: Mon, 2 Sep 2024 11:50:22 +0200 Subject: [PATCH 12/12] feat: failed placement of parachain configs populater --- store/snowbridge.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/store/snowbridge.ts b/store/snowbridge.ts index d129369..0e58e74 100644 --- a/store/snowbridge.ts +++ b/store/snowbridge.ts @@ -1,4 +1,10 @@ -import { getEnvironment, getEnvironmentName } from "@/lib/snowbridge"; +import { + getEnvironment, + getEnvironmentName, + parachainConfigs, + populateParachainConfigs, +} from "@/lib/snowbridge"; +import { printify } from "@/utils/printify"; import { Context, assets } from "@snowbridge/api"; import { atom } from "jotai"; @@ -9,4 +15,12 @@ export const assetErc20MetaDataAtom = atom<{ export const snowbridgeContextAtom = atom(null); export const snowbridgeEnvNameAtom = atom((_) => getEnvironmentName()); // this one is unnecessary. snowbridgeEnvironmentAtom.name can be used instead -export const snowbridgeEnvironmentAtom = atom((_) => getEnvironment()); +export const snowbridgeEnvironmentAtom = atom(async () => { + await populateParachainConfigs(); + console.log( + "Getting environment after adding this parachain configs: ", + printify(parachainConfigs), + ); + + return getEnvironment(); +});