diff --git a/.env b/.env.example similarity index 61% rename from .env rename to .env.example index a055d4d..94da5d5 100644 --- a/.env +++ b/.env.example @@ -5,3 +5,11 @@ # - `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= + +# 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/README.md b/README.md index 57c4c34..49ab347 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: @@ -19,7 +20,7 @@ On the `.env`-file, configure of your project by assigning the values that fit y 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=... 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..6ee54be 100644 --- a/lib/snowbridge.ts +++ b/lib/snowbridge.ts @@ -1,3 +1,8 @@ +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 { @@ -21,19 +26,93 @@ 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); + + if (!newConfig) { + return; + } + if (newConfig.name in parachainConfigs) { + // don't overwrite + } else { + parachainConfigs[newConfig.name] = newConfig; + } + } +} + +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: ", printify(env)); + console.log( + `Added this parachains to the "${env.name}" snowbridge environment: ${pertinentParaConfigs.map(({ name }) => name).join(";")}.`, + ); +} + 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}'`, ); + } + addParachains(env, parachainConfigs); return env; } @@ -325,8 +404,8 @@ export async function getBridgeStatus( !toPolkadot.bridgeOperational || !toPolkadot.channelOperational ? "Halted" : !toPolkadot.lightClientLatencyIsAcceptable - ? "Delayed" - : "Normal"; + ? "Delayed" + : "Normal"; const toEthereum = { bridgeOperational: @@ -337,8 +416,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/store/snowbridge.ts b/store/snowbridge.ts index 627bd51..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"; @@ -8,5 +14,13 @@ export const assetErc20MetaDataAtom = atom<{ } | null>(null); export const snowbridgeContextAtom = atom(null); -export const snowbridgeEnvNameAtom = atom((_) => getEnvironmentName()); -export const snowbridgeEnvironmentAtom = atom((_) => getEnvironment()); +export const snowbridgeEnvNameAtom = atom((_) => getEnvironmentName()); // this one is unnecessary. snowbridgeEnvironmentAtom.name can be used instead +export const snowbridgeEnvironmentAtom = atom(async () => { + await populateParachainConfigs(); + console.log( + "Getting environment after adding this parachain configs: ", + printify(parachainConfigs), + ); + + return getEnvironment(); +}); 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; } 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, + }, + ], + }, + }; +} 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; + } +} 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"; +} 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, + ); +}