From fd29357b621b0413a0e778cd6c07236643c2a607 Mon Sep 17 00:00:00 2001 From: Bence Haromi <56651250+benceharomi@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:47:07 +0100 Subject: [PATCH] feat: shared bridge support (#178) Co-authored-by: Ramon "9Tails" Canales Co-authored-by: Jack Hamer --- composables/zksync/useTransaction.ts | 2 +- .../zksync/useWithdrawalFinalization.ts | 38 ++-- data/networks.ts | 25 ++- data/wagmi.ts | 1 + hyperchains/README.md | 19 +- package-lock.json | 11 +- package.json | 3 +- pages/send-methods.vue | 3 +- scripts/hyperchains/common.ts | 9 + scripts/hyperchains/configure.ts | 2 +- scripts/hyperchains/create.ts | 182 +++++++++++------- scripts/hyperchains/utils.ts | 11 +- store/zksync/transactionStatus.ts | 16 +- views/transactions/Deposit.vue | 2 +- views/transactions/Transfer.vue | 8 +- views/transactions/WithdrawalSubmitted.vue | 7 + 16 files changed, 186 insertions(+), 153 deletions(-) create mode 100644 scripts/hyperchains/common.ts diff --git a/composables/zksync/useTransaction.ts b/composables/zksync/useTransaction.ts index 19ca672aa..d8662c93b 100644 --- a/composables/zksync/useTransaction.ts +++ b/composables/zksync/useTransaction.ts @@ -40,7 +40,7 @@ export default (getSigner: () => Promise, getProvider: () => const getRequiredBridgeAddress = async () => { if (transaction.tokenAddress === ETH_TOKEN.address) return undefined; const bridgeAddresses = await retrieveBridgeAddresses(); - return bridgeAddresses.erc20L2; + return bridgeAddresses.sharedL2; }; const bridgeAddress = transaction.type === "withdrawal" ? await getRequiredBridgeAddress() : undefined; diff --git a/composables/zksync/useWithdrawalFinalization.ts b/composables/zksync/useWithdrawalFinalization.ts index 3d91bf7b1..06ea67a3f 100644 --- a/composables/zksync/useWithdrawalFinalization.ts +++ b/composables/zksync/useWithdrawalFinalization.ts @@ -1,8 +1,7 @@ import { useMemoize } from "@vueuse/core"; import { BigNumber, type BigNumberish } from "ethers"; import { Wallet } from "zksync-ethers"; -import ZkSyncL1BridgeAbi from "zksync-ethers/abi/IL1Bridge.json"; -import ZkSyncContractAbi from "zksync-ethers/abi/IZkSync.json"; +import IL1SharedBridge from "zksync-ethers/abi/IL1SharedBridge.json"; import type { Hash } from "@/types"; @@ -16,13 +15,14 @@ export default (transactionInfo: ComputedRef) => { const { isCorrectNetworkSet } = storeToRefs(onboardStore); const { tokens } = storeToRefs(tokensStore); - const retrieveBridgeAddress = useMemoize(() => + const retrieveBridgeAddresses = useMemoize(() => providerStore.requestProvider().getDefaultBridgeAddresses()); + + const retrieveChainId = useMemoize(() => providerStore .requestProvider() - .getDefaultBridgeAddresses() - .then((e) => e.erc20L1) + .getNetwork() + .then((network) => network.chainId) ); - const retrieveMainContractAddress = useMemoize(() => providerStore.requestProvider().getMainContractAddress()); const gasLimit = ref(); const gasPrice = ref(); @@ -44,7 +44,6 @@ export default (transactionInfo: ComputedRef) => { const feeToken = computed(() => { return tokens.value?.[ETH_TOKEN.address]; }); - const usingMainContract = computed(() => transactionInfo.value.token.address === ETH_TOKEN.address); const getFinalizationParams = async () => { const provider = providerStore.requestProvider(); @@ -58,6 +57,7 @@ export default (transactionInfo: ComputedRef) => { transactionInfo.value.transactionHash ); return { + chainId: await retrieveChainId(), l1BatchNumber, l2MessageIndex, l2TxNumberInBlock, @@ -68,23 +68,13 @@ export default (transactionInfo: ComputedRef) => { const getTransactionParams = async () => { finalizeWithdrawalParams.value = await getFinalizationParams(); - if (usingMainContract.value) { - return { - address: (await retrieveMainContractAddress()) as Hash, - abi: ZkSyncContractAbi, - account: onboardStore.account.address!, - functionName: "finalizeEthWithdrawal", - args: Object.values(finalizeWithdrawalParams.value!), - }; - } else { - return { - address: (await retrieveBridgeAddress()) as Hash, - abi: ZkSyncL1BridgeAbi, - account: onboardStore.account.address!, - functionName: "finalizeWithdrawal", - args: Object.values(finalizeWithdrawalParams.value!), - }; - } + return { + address: (await retrieveBridgeAddresses()).sharedL1 as Hash, + abi: IL1SharedBridge, + account: onboardStore.account.address!, + functionName: "finalizeWithdrawal", + args: Object.values(finalizeWithdrawalParams.value!), + }; }; const { diff --git a/data/networks.ts b/data/networks.ts index 567368e8c..cf0fdb991 100644 --- a/data/networks.ts +++ b/data/networks.ts @@ -1,6 +1,7 @@ import { mainnet, sepolia } from "@wagmi/core/chains"; import Hyperchains from "@/hyperchains/config.json"; +import { PUBLIC_L1_CHAINS, type Config } from "@/scripts/hyperchains/common"; import type { Token } from "@/types"; import type { Chain } from "@wagmi/core/chains"; @@ -101,6 +102,25 @@ const publicChains: ZkSyncNetwork[] = [ }, ]; +const getHyperchains = (): ZkSyncNetwork[] => { + const hyperchains = Hyperchains as Config; + return hyperchains.map((e) => { + const network: ZkSyncNetwork = { + ...e.network, + getTokens: () => e.tokens, + }; + if (e.network.publicL1NetworkId) { + network.l1Network = PUBLIC_L1_CHAINS.find((chain) => chain.id === e.network.publicL1NetworkId); + if (!network.l1Network) { + throw new Error( + `L1 network with ID ${e.network.publicL1NetworkId} from ${network.name} config wasn't found in the list of public L1 networks.` + ); + } + } + return network; + }); +}; + const nodeType = portalRuntimeConfig.nodeType; const determineChainList = (): ZkSyncNetwork[] => { switch (nodeType) { @@ -109,10 +129,7 @@ const determineChainList = (): ZkSyncNetwork[] => { case "dockerized": return [dockerizedNode]; case "hyperchain": - return (Hyperchains as unknown as Array<{ network: ZkSyncNetwork; tokens: Token[] }>).map((e) => ({ - ...e.network, - getTokens: () => e.tokens, - })); + return getHyperchains(); default: return [...publicChains]; } diff --git a/data/wagmi.ts b/data/wagmi.ts index f2470cf89..777aabfec 100644 --- a/data/wagmi.ts +++ b/data/wagmi.ts @@ -41,6 +41,7 @@ const formatZkSyncChain = (network: ZkSyncNetwork) => { : undefined, }; }; + const getAllChains = () => { const chains: Chain[] = []; const addUniqueChain = (chain: Chain) => { diff --git a/hyperchains/README.md b/hyperchains/README.md index 6fdaec904..803b8bdf4 100644 --- a/hyperchains/README.md +++ b/hyperchains/README.md @@ -8,23 +8,6 @@ Portal supports custom ZK Stack Hyperchain nodes. There are a few different ways to configure the application: -### 📁 Configure using ZK Stack configuration files -
-If you're using ZK Stack, just link your zksync-era repo directory to configure Portal. - -1. If you haven't already setup your hyperchain yet, follow the [instructions](https://zkstack.io/quickstart) -2. Make sure to install the dependencies: - ```bash - npm install - ``` -3. 🔄 Pull your hyperchain config files by running: - ```bash - npm run hyperchain:configure - ``` - This will regenerate `/hyperchains/config.json` file. You can edit this file manually if needed. -4. 🚀 Now you can start or build the application. See [Development](#development-server) or [Production](#production) section below for more details. -
- ### 🖊️ Configure automatically with form
Fill out a simple form to configure the application. @@ -61,7 +44,9 @@ Array<{ rpcUrl: string; // L2 RPC URL name: string; blockExplorerUrl?: string; // L2 Block Explorer URL + blockExplorerApi?: string; // L2 Block Explorer API hidden?: boolean; // Hidden in the network selector + publicL1NetworkId?: number; // If you wish to use Ethereum Mainnet or Ethereum Sepolia Testnet with default configuration. Can be provided instead of `l1Network` l1Network?: { // @wagmi `Chain` structure https://wagmi.sh/core/chains#build-your-own // minimal required fields shown id: number; diff --git a/package-lock.json b/package-lock.json index c4d5608cf..c0e426bbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "vite": "^3.0.0", "vue-tippy": "^6.0.0", "web3-avatar-vue": "^1.0.0", - "zksync-ethers": "^5.5.0" + "zksync-ethers": "^5.9.0" }, "devDependencies": { "@commitlint/cli": "^17.4.2", @@ -34517,12 +34517,15 @@ } }, "node_modules/zksync-ethers": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/zksync-ethers/-/zksync-ethers-5.5.0.tgz", - "integrity": "sha512-sH77qSRSa1iEzy9abK2d5yaKA30QIpztbLUtjRJ34kWujdlVre8Sd0uUzLu+zW56AksU3AHGDl/iMtr/eHhDwg==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/zksync-ethers/-/zksync-ethers-5.9.0.tgz", + "integrity": "sha512-VnRUesrBcPBmiTYTAp+WreIazK2qCIJEHE7j8BiK+cDApHzjAfIXX+x8SXXJpG1npGJANxiJKnPwA5wjGZtCRg==", "dependencies": { "ethers": "~5.7.0" }, + "engines": { + "node": ">=16.0.0" + }, "peerDependencies": { "ethers": "~5.7.0" } diff --git a/package.json b/package.json index bdad0dda3..44ce3963f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "generate:node:hyperchain": "ts-node --transpile-only scripts/hyperchains/empty-check.ts && cross-env NODE_TYPE=hyperchain npm run generate", "generate-meta": "ts-node --transpile-only scripts/updateBridgeMetaTags.ts", "hyperchain:create": "ts-node --transpile-only scripts/hyperchains/create.ts", - "hyperchain:configure": "ts-node --transpile-only scripts/hyperchains/configure.ts", "preview": "nuxt preview", "postinstall": "nuxt prepare", "prepare": "husky install", @@ -87,7 +86,7 @@ "vite": "^3.0.0", "vue-tippy": "^6.0.0", "web3-avatar-vue": "^1.0.0", - "zksync-ethers": "^5.5.0" + "zksync-ethers": "^5.9.0" }, "overrides": { "vue": "latest" diff --git a/pages/send-methods.vue b/pages/send-methods.vue index 934945229..043507e83 100644 --- a/pages/send-methods.vue +++ b/pages/send-methods.vue @@ -20,9 +20,8 @@ :to="{ name: 'bridge-withdraw', query: $route.query }" /> - + ; +export type Config = { network: Network; tokens: Token[] }[]; + +export const PUBLIC_L1_CHAINS = [mainnet, sepolia]; diff --git a/scripts/hyperchains/configure.ts b/scripts/hyperchains/configure.ts index 850d24c55..9678de096 100644 --- a/scripts/hyperchains/configure.ts +++ b/scripts/hyperchains/configure.ts @@ -10,7 +10,7 @@ import { join as pathJoin, parse as pathParse } from "path"; import { generateNetworkConfig, logUserInfo, promptNetworkReplacement } from "./utils"; -import type { Network } from "./utils"; +import type { Network } from "./common"; import type { Token } from "../../types"; const rootPath = process.env.ZKSYNC_HOME; diff --git a/scripts/hyperchains/create.ts b/scripts/hyperchains/create.ts index a84a7b0a7..c63ba8a9a 100644 --- a/scripts/hyperchains/create.ts +++ b/scripts/hyperchains/create.ts @@ -1,10 +1,10 @@ import { prompt } from "enquirer"; import slugify from "slugify"; +import { mainnet, Chain } from "viem/chains"; +import { PUBLIC_L1_CHAINS, type Network } from "./common"; import { generateNetworkConfig, logUserInfo, promptNetworkReplacement } from "./utils"; -import type { Network } from "./utils"; - const promptHyperchainInfo = async (): Promise => { const { id, name }: { id: number; name: string } = await prompt([ { @@ -25,95 +25,127 @@ const promptHyperchainInfo = async (): Promise => { key, rpcUrl, blockExplorerUrl, + blockExplorerApi, connectedToL1, - }: { key: string; rpcUrl: string; blockExplorerUrl: string; connectedToL1: boolean } = await prompt([ - { - message: "Hyperchain key", - name: "key", - type: "input", - required: true, - initial: slugify(name, { - lower: true, - replacement: "-", - strict: true, - }), - }, - { - message: "Hyperchain RPC URL", - name: "rpcUrl", - type: "input", - required: true, - }, - { - message: "Hyperchain Block Explorer URL (optional)", - name: "blockExplorerUrl", - type: "input", - }, - { - message: "Is hyperchain connected to L1 network?", - name: "connectedToL1", - type: "confirm", - required: true, - initial: true, - }, - ]); - - let l1Network: Network["l1Network"] | undefined; - if (connectedToL1) { - const { - l1NetworkId, - l1NetworkName, - l1NetworkRpcUrl, - l1NetworkBlockExplorerUrl, - }: { - l1NetworkId: number; - l1NetworkName: string; - l1NetworkRpcUrl: string; - l1NetworkBlockExplorerUrl: string; - } = await prompt([ + }: { key: string; rpcUrl: string; blockExplorerUrl: string; blockExplorerApi: string; connectedToL1: boolean } = + await prompt([ { - message: "L1 chain id", - name: "l1NetworkId", - type: "numeral", + message: "Hyperchain key", + name: "key", + type: "input", required: true, - float: false, + initial: slugify(name, { + lower: true, + replacement: "-", + strict: true, + }), }, { - message: "Displayed L1 chain name", - name: "l1NetworkName", + message: "Hyperchain RPC URL", + name: "rpcUrl", type: "input", required: true, }, { - message: "L1 chain RPC URL", - name: "l1NetworkRpcUrl", + message: "Hyperchain Block Explorer URL (optional)", + name: "blockExplorerUrl", type: "input", - required: true, }, { - message: "L1 chain Block Explorer URL (optional)", - name: "l1NetworkBlockExplorerUrl", + message: "Hyperchain Block Explorer API (optional)", + name: "blockExplorerApi", type: "input", }, + { + message: "Is hyperchain connected to L1 network?", + name: "connectedToL1", + type: "confirm", + required: true, + initial: true, + }, ]); - l1Network = { - id: l1NetworkId, - name: l1NetworkName, - nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, - rpcUrls: { - default: { http: [l1NetworkRpcUrl] }, - public: { http: [l1NetworkRpcUrl] }, + let l1Network: Network["l1Network"] | undefined; + let publicL1NetworkId: number | undefined; + + if (connectedToL1) { + // Ask select from public L1 chains (should list all options) or `+ Add custom L1 chain` + const { selectedOption }: { selectedOption: "add-custom-chain" | Chain } = await prompt({ + message: "Select L1 chain", + name: "selectedOption", + type: "select", + required: true, + choices: [ + ...PUBLIC_L1_CHAINS.map((chain) => ({ + name: `${chain.id === mainnet.id ? "Ethereum Mainnet" : "Ethereum " + chain.name}${ + chain.testnet ? " Testnet" : "" + }`, + value: chain, + })), + { name: "+ Add custom L1 chain", value: "add-custom-chain" }, + ], + result(resultName) { + return this.choices.find((choice: { name: string }) => choice.name === resultName).value; }, - blockExplorers: l1NetworkBlockExplorerUrl - ? { - default: { - name: l1NetworkName, - url: l1NetworkBlockExplorerUrl, - }, - } - : undefined, - }; + }); + if (selectedOption === "add-custom-chain") { + const { + l1NetworkId, + l1NetworkName, + l1NetworkRpcUrl, + l1NetworkBlockExplorerUrl, + }: { + l1NetworkId: number; + l1NetworkName: string; + l1NetworkRpcUrl: string; + l1NetworkBlockExplorerUrl: string; + } = await prompt([ + { + message: "L1 chain id", + name: "l1NetworkId", + type: "numeral", + required: true, + float: false, + }, + { + message: "Displayed L1 chain name", + name: "l1NetworkName", + type: "input", + required: true, + }, + { + message: "L1 chain RPC URL", + name: "l1NetworkRpcUrl", + type: "input", + required: true, + }, + { + message: "L1 chain Block Explorer URL (optional)", + name: "l1NetworkBlockExplorerUrl", + type: "input", + }, + ]); + + l1Network = { + id: l1NetworkId, + name: l1NetworkName, + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { + default: { http: [l1NetworkRpcUrl] }, + public: { http: [l1NetworkRpcUrl] }, + }, + blockExplorers: l1NetworkBlockExplorerUrl + ? { + default: { + name: l1NetworkName, + url: l1NetworkBlockExplorerUrl, + }, + } + : undefined, + }; + } else { + publicL1NetworkId = selectedOption.id; + } } return { @@ -122,7 +154,9 @@ const promptHyperchainInfo = async (): Promise => { key, rpcUrl, blockExplorerUrl, + blockExplorerApi, l1Network, + publicL1NetworkId, }; }; diff --git a/scripts/hyperchains/utils.ts b/scripts/hyperchains/utils.ts index c50614ba3..6a0b5e92c 100644 --- a/scripts/hyperchains/utils.ts +++ b/scripts/hyperchains/utils.ts @@ -3,14 +3,9 @@ import { prompt } from "enquirer"; import { readFileSync, writeFileSync } from "fs"; import { join as pathJoin } from "path"; -import { ETH_TOKEN } from "../../utils/constants"; - -import type { ZkSyncNetwork } from "../../data/networks"; +import type { Network, Config } from "./common"; import type { Token } from "../../types"; -export type Network = Omit; -export type Config = { network: Network; tokens: Token[] }[]; - export const configPath = pathJoin(__dirname, "../../hyperchains/config.json"); const getConfig = (): Config => { return JSON.parse(readFileSync(configPath).toString()); @@ -50,10 +45,10 @@ export const promptNetworkReplacement = async (network: Network) => { export const generateNetworkConfig = (network: Network, tokens: Token[]) => { const config = getConfig(); - // Add ETH token if it's not in the list + /* // Add ETH token if it's not in the list if (!tokens.some((token: Token) => token.address === ETH_TOKEN.address)) { tokens.unshift(ETH_TOKEN); - } + } */ config.unshift({ network, tokens }); saveConfig(config); diff --git a/store/zksync/transactionStatus.ts b/store/zksync/transactionStatus.ts index bb760b149..0d165e898 100644 --- a/store/zksync/transactionStatus.ts +++ b/store/zksync/transactionStatus.ts @@ -1,6 +1,6 @@ import { useStorage } from "@vueuse/core"; import { decodeEventLog } from "viem"; -import ZkSyncContractInterface from "zksync-ethers/abi/IZkSync.json"; +import IZkSyncHyperchain from "zksync-ethers/abi/IZkSyncHyperchain.json"; import type { FeeEstimationParams } from "@/composables/zksync/useFee"; import type { TokenAmount, Hash } from "@/types"; @@ -30,14 +30,6 @@ export const useZkSyncTransactionStatusStore = defineStore("zkSyncTransactionSta const { account } = storeToRefs(onboardStore); const { eraNetwork } = storeToRefs(providerStore); - const failedTransaction = useStorage("zksync-bridge-failed-transaction", []); - const addFailedTransaction = (transaction: TransactionInfo) => { - if (failedTransaction.value.some((tx) => tx.transactionHash === transaction.transactionHash)) { - return; - } - failedTransaction.value = [...failedTransaction.value, transaction]; - }; - const storageSavedTransactions = useStorage<{ [networkKey: string]: TransactionInfo[] }>( "zksync-bridge-transactions", {} @@ -66,7 +58,7 @@ export const useZkSyncTransactionStatusStore = defineStore("zkSyncTransactionSta for (const log of transaction.logs) { try { const { args, eventName } = decodeEventLog({ - abi: ZkSyncContractInterface, + abi: IZkSyncHyperchain, data: log.data, topics: log.topics, }); @@ -96,7 +88,6 @@ export const useZkSyncTransactionStatusStore = defineStore("zkSyncTransactionSta transaction.info.withdrawalFinalizationAvailable = false; transaction.info.failed = true; transaction.info.completed = true; - addFailedTransaction(transaction); return transaction; } if (transactionDetails.status !== "verified") { @@ -115,11 +106,10 @@ export const useZkSyncTransactionStatusStore = defineStore("zkSyncTransactionSta const transactionReceipt = await providerStore.requestProvider().getTransactionReceipt(transaction.transactionHash); if (!transactionReceipt) return transaction; const transactionDetails = await providerStore.requestProvider().getTransactionDetails(transaction.transactionHash); - transaction.info.completed = true; if (transactionDetails.status === "failed") { transaction.info.failed = true; - addFailedTransaction(transaction); } + transaction.info.completed = true; return transaction; }; const waitForCompletion = async (transaction: TransactionInfo) => { diff --git a/views/transactions/Deposit.vue b/views/transactions/Deposit.vue index b0ed89d80..819e3e5dd 100644 --- a/views/transactions/Deposit.vue +++ b/views/transactions/Deposit.vue @@ -479,7 +479,7 @@ const { } = useAllowance( computed(() => account.value.address), computed(() => selectedToken.value?.address), - async () => (await providerStore.requestProvider().getDefaultBridgeAddresses()).erc20L1 + async () => (await providerStore.requestProvider().getDefaultBridgeAddresses()).sharedL1 ); const enoughAllowance = computed(() => { if (!allowance.value || !selectedToken.value) { diff --git a/views/transactions/Transfer.vue b/views/transactions/Transfer.vue index c402ab67f..0f68a36c3 100644 --- a/views/transactions/Transfer.vue +++ b/views/transactions/Transfer.vue @@ -109,7 +109,7 @@