From 42d7e0476fb95f3ece4cd4d135c002f5c04828d1 Mon Sep 17 00:00:00 2001 From: Jack Hamer Date: Wed, 6 Dec 2023 12:50:51 +0200 Subject: [PATCH 01/77] feat: add zksync sepolia testnet --- data/networks.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/data/networks.ts b/data/networks.ts index 6618e1f15..e63a14d24 100644 --- a/data/networks.ts +++ b/data/networks.ts @@ -131,6 +131,30 @@ export const eraNetworks: EraNetwork[] = [ getTokens: () => getTokensByNetworkId(280), l1Network: l1Networks.goerli, }, + { + id: 300, + key: "era-sepolia", + name: "zkSync Sepolia Testnet", + shortName: "zkSync Sepolia", + rpcUrl: "https://sepolia.era.zksync.dev", + blockExplorerUrl: "https://sepolia.explorer.zksync.io", + blockExplorerApi: "https://block-explorer-api.sepolia.zksync.dev", + displaySettings: { + showPartnerLinks: true, + showZkSyncLiteNetworks: true, + }, + getTokens: () => [ + { + address: "0x000000000000000000000000000000000000800A", + l1Address: "0x0000000000000000000000000000000000000000", + symbol: "ETH", + decimals: 18, + iconUrl: "/img/eth.svg", + enabledForFees: true, + }, + ], + l1Network: l1Networks.sepolia, + }, { id: 270, key: "era-stage", From 015c7bbbb1a40b6b150305664f145053cbee9cca Mon Sep 17 00:00:00 2001 From: Jack Hamer Date: Thu, 7 Dec 2023 18:37:57 +0200 Subject: [PATCH 02/77] feat: new token features --- .../era/deposit/ConfirmTransactionModal.vue | 4 +- composables/zksync/era/deposit/useFee.ts | 10 +- .../zksync/era/deposit/useTransaction.ts | 4 +- data/networks.ts | 89 +++++------------- package-lock.json | 23 +---- package.json | 2 +- store/ethereumBalance.ts | 43 ++++----- store/zksync/era/ethereumBalance.ts | 93 +++++++++---------- store/zksync/era/tokens.ts | 73 +++++++++------ store/zksync/era/transfersHistory.ts | 10 -- store/zksync/era/wallet.ts | 16 ---- types/index.d.ts | 3 + utils/constants.ts | 11 +++ views/zksync/era/Assets.vue | 4 +- views/zksync/era/Balances.vue | 6 +- views/zksync/era/transactions/Deposit.vue | 67 +++++-------- views/zksync/era/transactions/Transfer.vue | 29 +----- 17 files changed, 188 insertions(+), 299 deletions(-) diff --git a/components/transaction/zksync/era/deposit/ConfirmTransactionModal.vue b/components/transaction/zksync/era/deposit/ConfirmTransactionModal.vue index edf6b1538..ce0acd6d1 100644 --- a/components/transaction/zksync/era/deposit/ConfirmTransactionModal.vue +++ b/components/transaction/zksync/era/deposit/ConfirmTransactionModal.vue @@ -96,7 +96,6 @@ :token="transaction.token" :amount="transaction.amount" :direction="transactionReceiptDirection" - :loading="transaction.token.price === 'loading'" /> @@ -318,8 +317,7 @@ const makeTransaction = async () => { if (tx) { for (const tokenAddress in totalOfEachToken.value) { const token = totalOfEachToken.value[tokenAddress]; - if (!token?.token.l1Address) continue; - eraEthereumBalanceStore.deductBalance(token.token.l1Address, token.amount.toString()); + eraEthereumBalanceStore.deductBalance(token.token.address, token.amount.toString()); } tx.waitL1Commit() // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/composables/zksync/era/deposit/useFee.ts b/composables/zksync/era/deposit/useFee.ts index db6f989ba..32fbfa71b 100644 --- a/composables/zksync/era/deposit/useFee.ts +++ b/composables/zksync/era/deposit/useFee.ts @@ -10,7 +10,7 @@ import type { PublicClient } from "@wagmi/core"; import type { Ref } from "vue"; import type { L1Signer } from "zksync-web3"; -import { ETH_L2_ADDRESS } from "@/utils/constants"; +import { ETH_L1_ADDRESS } from "@/utils/constants"; import { retry } from "@/utils/helpers"; import { calculateFee } from "@/utils/helpers"; @@ -24,8 +24,8 @@ export type DepositFeeValues = { }; export default ( - tokens: Ref<{ [tokenSymbol: string]: Token } | undefined>, - balances: Ref, + tokens: Ref, + balances: Ref, getL1VoidSigner: () => L1Signer, getPublicClient: () => PublicClient ) => { @@ -52,10 +52,10 @@ export default ( }); const feeToken = computed(() => { - return tokens.value?.[ETH_L2_ADDRESS]; + return tokens.value.find((e) => e.address === ETH_L1_ADDRESS); }); const enoughBalanceToCoverFee = computed(() => { - if (!feeToken.value || inProgress.value) { + if (!feeToken.value || !balances.value || inProgress.value) { return true; } const feeTokenBalance = balances.value.find((e) => e.address === feeToken.value!.address); diff --git a/composables/zksync/era/deposit/useTransaction.ts b/composables/zksync/era/deposit/useTransaction.ts index d56d6722f..cd1c98682 100644 --- a/composables/zksync/era/deposit/useTransaction.ts +++ b/composables/zksync/era/deposit/useTransaction.ts @@ -13,8 +13,6 @@ export default (getL1Signer: () => Promise) => { try { error.value = undefined; - if (!transaction.token.l1Address) throw new Error("Token L1 address is not available"); - status.value = "processing"; const wallet = await getL1Signer(); if (!wallet) throw new Error("Wallet is not available"); @@ -31,7 +29,7 @@ export default (getL1Signer: () => Promise) => { } const depositResponse = await wallet.deposit({ to: transaction.to, - token: transaction.token.l1Address, + token: transaction.token.address, amount: transaction.amount, l2GasLimit: fee.l2GasLimit, overrides, diff --git a/data/networks.ts b/data/networks.ts index e63a14d24..fed8403fe 100644 --- a/data/networks.ts +++ b/data/networks.ts @@ -4,9 +4,6 @@ import type { Token } from "@/types"; import type { Chain } from "@wagmi/core/chains"; import type { Network } from "zksync/build/types"; -import { ETH_L1_ADDRESS, ETH_L2_ADDRESS } from "@/utils/constants"; -import { getTokensByNetworkId } from "@/utils/zksync/era/token-library"; - export type L2Network = { key: string; name: string; @@ -30,8 +27,8 @@ export const l1Networks = { ...sepolia, name: "Sepolia Testnet", rpcUrls: { - public: { http: ["https://ethereum-sepolia.publicnode.com"] }, - default: { http: ["https://ethereum-sepolia.publicnode.com"] }, + public: { http: ["https://rpc.ankr.com/eth_sepolia"] }, + default: { http: ["https://rpc.ankr.com/eth_sepolia"] }, }, }, } as const; @@ -46,7 +43,7 @@ export type EraNetwork = L2Network & { showPartnerLinks?: boolean; showZkSyncLiteNetworks?: boolean; }; - getTokens: () => Token[] | Promise; + getTokens?: () => Token[] | Promise; // If blockExplorerApi is specified, tokens will be fetched from there. Otherwise, this function will be used. }; // See the official documentation on running a local zkSync node: https://era.zksync.io/docs/tools/testing/ @@ -59,15 +56,6 @@ export const eraInMemoryNode: EraNetwork = { name: "In-memory node", shortName: "In-memory local node", rpcUrl: "http://localhost:8011", - getTokens: () => [ - { - address: ETH_L2_ADDRESS, - symbol: "ETH", - decimals: 18, - iconUrl: "/img/eth.svg", // optional - enabledForFees: true, // optional - }, - ], }; // Dockerized local setup default config. Docs: https://era.zksync.io/docs/tools/testing/dockerized-testing.html @@ -77,16 +65,6 @@ export const eraDockerizedNode: EraNetwork = { name: "Dockerized local node", shortName: "Dockerized node", rpcUrl: "http://localhost:3050", - getTokens: () => [ - { - address: ETH_L2_ADDRESS, - l1Address: ETH_L1_ADDRESS, // optional - symbol: "ETH", - decimals: 18, - iconUrl: "/img/eth.svg", // optional - enabledForFees: true, // optional - }, - ], l1Network: { id: 9, name: "L1 Local", @@ -103,8 +81,8 @@ export const eraNetworks: EraNetwork[] = [ { id: 324, key: "era-mainnet", - name: "zkSync Era Mainnet", - shortName: "Era Mainnet", + name: "zkSync Mainnet", + shortName: "zkSync", rpcUrl: "https://mainnet.era.zksync.io", blockExplorerUrl: "https://explorer.zksync.io", blockExplorerApi: "https://block-explorer-api.mainnet.zksync.io", @@ -112,25 +90,8 @@ export const eraNetworks: EraNetwork[] = [ showPartnerLinks: true, showZkSyncLiteNetworks: true, }, - getTokens: () => getTokensByNetworkId(324), l1Network: l1Networks.mainnet, }, - { - id: 280, - key: "era-goerli", - name: "zkSync Era Testnet", - shortName: "Era Testnet", - rpcUrl: "https://testnet.era.zksync.dev", - blockExplorerUrl: "https://goerli.explorer.zksync.io", - blockExplorerApi: "https://block-explorer-api.testnets.zksync.dev", - faucetUrl: "https://testnet2-faucet.zksync.dev/ask_money", - displaySettings: { - showPartnerLinks: true, - showZkSyncLiteNetworks: true, - }, - getTokens: () => getTokensByNetworkId(280), - l1Network: l1Networks.goerli, - }, { id: 300, key: "era-sepolia", @@ -143,38 +104,32 @@ export const eraNetworks: EraNetwork[] = [ showPartnerLinks: true, showZkSyncLiteNetworks: true, }, - getTokens: () => [ - { - address: "0x000000000000000000000000000000000000800A", - l1Address: "0x0000000000000000000000000000000000000000", - symbol: "ETH", - decimals: 18, - iconUrl: "/img/eth.svg", - enabledForFees: true, - }, - ], l1Network: l1Networks.sepolia, }, + { + id: 280, + key: "era-goerli", + name: "zkSync Goerli Testnet", + shortName: "zkSync Goerli", + rpcUrl: "https://testnet.era.zksync.dev", + blockExplorerUrl: "https://goerli.explorer.zksync.io", + blockExplorerApi: "https://block-explorer-api.testnets.zksync.dev", + faucetUrl: "https://testnet2-faucet.zksync.dev/ask_money", + displaySettings: { + showPartnerLinks: true, + showZkSyncLiteNetworks: true, + }, + l1Network: l1Networks.goerli, + }, { id: 270, key: "era-stage", - name: "zkSync Era Stage", - shortName: "Era Stage", + name: "zkSync Stage", + shortName: "zkSync Stage", rpcUrl: "https://z2-dev-api.zksync.dev", blockExplorerUrl: "https://goerli-beta.staging-scan-v2.zksync.dev", blockExplorerApi: "https://block-explorer-api.stage.zksync.dev", faucetUrl: "https://stage2-faucet.zksync.dev/ask_money", - getTokens: () => getTokensByNetworkId(270), - l1Network: l1Networks.sepolia, - hidden: true, - }, - { - id: 300, - key: "era-boojnet", - name: "zkSync Era Boojnet", - shortName: "Era Boojnet", - rpcUrl: "https://sepolia.era.zksync.dev", - getTokens: () => getTokensByNetworkId(300), l1Network: l1Networks.sepolia, hidden: true, }, diff --git a/package-lock.json b/package-lock.json index 9baeab2b5..be8325341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "name": "zksync-dapp-portal", "hasInstallScript": true, "dependencies": { - "@ankr.com/ankr.js": "^0.4.0", + "@ankr.com/ankr.js": "^0.5.0", "@chenfengyuan/vue-qrcode": "^2.0.0", "@headlessui/vue": "^1.7.7", "@heroicons/vue": "^2.0.14", @@ -126,24 +126,11 @@ } }, "node_modules/@ankr.com/ankr.js": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@ankr.com/ankr.js/-/ankr.js-0.4.0.tgz", - "integrity": "sha512-7yA/NM/XSzx7vXartN6PDGN6toS/dueFt7Yp/+LiRDa6dB7FfGa380YHNWipf81CqSkwQEeHVWvS+fts+TWXeg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@ankr.com/ankr.js/-/ankr.js-0.5.0.tgz", + "integrity": "sha512-NZSYcMQNA+XoE1AjTaZJitzMCc9KOhqtAw+HKRwREix9EYSygYV5tWvqjuCBg/GiNJUYKNC5e0xOj8vNJE6YHQ==", "dependencies": { - "axios": "^0.26.1", - "typescript": "^4.6.2" - } - }, - "node_modules/@ankr.com/ankr.js/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" + "axios": "^0.26.1" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index d4d74a3fe..d501a2a8e 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "*.{vue,js,ts,html,json}": "npm run lint" }, "dependencies": { - "@ankr.com/ankr.js": "^0.4.0", + "@ankr.com/ankr.js": "^0.5.0", "@chenfengyuan/vue-qrcode": "^2.0.0", "@headlessui/vue": "^1.7.7", "@heroicons/vue": "^2.0.14", diff --git a/store/ethereumBalance.ts b/store/ethereumBalance.ts index 4c9964e34..f81894a61 100644 --- a/store/ethereumBalance.ts +++ b/store/ethereumBalance.ts @@ -5,6 +5,7 @@ import { defineStore, storeToRefs } from "pinia"; import type { TokenAmount } from "@/types"; import type { Blockchain as AnkrSupportedChains } from "@ankr.com/ankr.js"; +import { l1Networks } from "@/data/networks"; import { useOnboardStore } from "@/store/onboard"; import { useEraProviderStore } from "@/store/zksync/era/provider"; import { ETH_L1_ADDRESS } from "@/utils/constants"; @@ -29,31 +30,31 @@ export const useEthereumBalanceStore = defineStore("ethereumBalance", () => { if (!runtimeConfig.public.ankrToken) throw new Error("Ankr token is not available"); const ankrProvider = new AnkrProvider(`https://rpc.ankr.com/multichain/${runtimeConfig.public.ankrToken}`); + const networkIdToAnkr = new Map([ + [l1Networks.mainnet.id, "eth"], + [l1Networks.goerli.id, "eth_goerli"], + ]); + if (!networkIdToAnkr.has(eraNetwork.value.l1Network.id)) { + throw new Error(`Ankr does not support ${eraNetwork.value.l1Network.name}`); + } const balances = await ankrProvider.getAccountBalance({ - blockchain: [ - eraNetwork.value.l1Network.network === "mainnet" - ? "eth" - : (`eth_${eraNetwork.value.l1Network.network}` as AnkrSupportedChains), - ], + blockchain: [networkIdToAnkr.get(eraNetwork.value.l1Network.id)!], walletAddress: account.value.address, onlyWhitelisted: false, }); - return [ - ...balances.assets - .filter((e) => e.contractAddress || e.tokenType === "NATIVE") - .map( - (e) => - ({ - address: e.tokenType === "NATIVE" ? ETH_L1_ADDRESS : checksumAddress(e.contractAddress!), - symbol: e.tokenSymbol, - name: e.tokenName, - decimals: e.tokenDecimals, - iconUrl: e.thumbnail, - price: e.tokenPrice, - amount: e.balanceRawInteger, - } as TokenAmount) - ), - ]; + return balances.assets + .filter((e) => e.contractAddress || e.tokenType === "NATIVE") + .map((e) => { + return { + address: e.tokenType === "NATIVE" ? ETH_L1_ADDRESS : checksumAddress(e.contractAddress!), + symbol: e.tokenSymbol, + name: e.tokenName, + decimals: e.tokenDecimals, + iconUrl: e.thumbnail, + price: e.tokenPrice === "0" ? undefined : parseFloat(e.tokenPrice), + amount: e.balanceRawInteger, + } as TokenAmount; + }); }, { cache: false } ); diff --git a/store/zksync/era/ethereumBalance.ts b/store/zksync/era/ethereumBalance.ts index adbcfa4a9..8c0e1ad73 100644 --- a/store/zksync/era/ethereumBalance.ts +++ b/store/zksync/era/ethereumBalance.ts @@ -1,9 +1,9 @@ import { fetchBalance } from "@wagmi/core"; -import { BigNumber } from "ethers"; import { defineStore, storeToRefs } from "pinia"; import type { Hash, TokenAmount } from "@/types"; +import { l1Networks } from "@/data/networks"; import { useEthereumBalanceStore } from "@/store/ethereumBalance"; import { useNetworkStore } from "@/store/network"; import { useOnboardStore } from "@/store/onboard"; @@ -17,56 +17,66 @@ export const useEraEthereumBalanceStore = defineStore("eraEthereumBalances", () const { l1Network, selectedNetwork } = storeToRefs(useNetworkStore()); const { account } = storeToRefs(onboardStore); const { balance: ethereumBalance } = storeToRefs(ethereumBalancesStore); - const { tokens } = storeToRefs(eraTokensStore); + const { l1Tokens } = storeToRefs(eraTokensStore); - const getBalancesFromApi = async () => { + const getBalancesFromApi = async (): Promise => { await Promise.all([ethereumBalancesStore.requestBalance(), eraTokensStore.requestTokens()]); - if (!tokens.value) throw new Error("Tokens are not available"); if (!ethereumBalance.value) throw new Error("Ethereum balances are not available"); - return Object.fromEntries(ethereumBalance.value.map((token) => [token.address, token.amount])); + // Get balances from Ankr API and merge them with tokens data from explorer + return [ + ...ethereumBalance.value.map((e) => { + const tokenFromExplorer = l1Tokens.value?.[e.address]; + return { + ...e, + symbol: tokenFromExplorer?.symbol ?? e.symbol, + name: tokenFromExplorer?.name ?? e.name, + iconUrl: tokenFromExplorer?.iconUrl ?? e.iconUrl, + price: tokenFromExplorer?.price ?? e.price, + }; + }), + ...Object.values(l1Tokens.value ?? []) // Add tokens that are not in Ankr API + .filter((token) => !ethereumBalance.value?.find((e) => e.address === token.address)) + .map((e) => ({ + ...e, + amount: "0", + })), + ]; }; - const getBalancesFromRPC = async () => { + const getBalancesFromRPC = async (): Promise => { await eraTokensStore.requestTokens(); - if (!tokens.value) throw new Error("Tokens are not available"); + if (!l1Tokens.value) throw new Error("Tokens are not available"); if (!account.value.address) throw new Error("Account is not available"); - const balances = await Promise.all( - Object.entries(tokens.value) - .filter(([, token]) => token.l1Address) - .map(async ([, token]) => { - const amount = await fetchBalance({ - address: account.value.address!, - chainId: l1Network.value!.id, - token: token.l1Address === ETH_L1_ADDRESS ? undefined : (token.l1Address! as Hash), - }); - if (amount.value) { - eraTokensStore.requestTokenPrice(token.address); - } - return { - address: token.l1Address!, - amount: amount.value.toString(), - }; - }) + return await Promise.all( + Object.values(l1Tokens.value ?? []).map(async (token) => { + const amount = await fetchBalance({ + address: account.value.address!, + chainId: l1Network.value!.id, + token: token.address === ETH_L1_ADDRESS ? undefined : (token.address! as Hash), + }); + return { + ...token, + amount: amount.value.toString(), + }; + }) ); - - return balances.reduce((accumulator: { [tokenL1Address: string]: string }, { address, amount }) => { - accumulator[address] = amount; - return accumulator; - }, {}); }; const { - result: balancesResult, + result: balance, inProgress: balanceInProgress, error: balanceError, execute: requestBalance, reset: resetBalance, - } = usePromise<{ [tokenL1Address: string]: string }>( + } = usePromise( async () => { if (!l1Network.value) throw new Error(`L1 network is not available on ${selectedNetwork.value.name}`); - if (["mainnet", "goerli"].includes(l1Network.value?.network) && runtimeConfig.public.ankrToken) { + if ( + ([l1Networks.mainnet.id, l1Networks.goerli.id] as number[]).includes(l1Network.value?.id) && + runtimeConfig.public.ankrToken + ) { return getBalancesFromApi(); } else { return getBalancesFromRPC(); @@ -74,24 +84,6 @@ export const useEraEthereumBalanceStore = defineStore("eraEthereumBalances", () }, { cache: 30000 } ); - const balance = computed(() => { - if (!balancesResult.value) return []; - return Object.entries(tokens.value ?? {}).map(([, token]) => { - const amount = (token.l1Address && balancesResult.value![token.l1Address]) ?? "0"; - return { ...token, amount }; - }); - }); - watch( - balance, - (balances) => { - balances.map(({ address, amount }) => { - if (BigNumber.from(amount).isZero()) return; - eraTokensStore.requestTokenPrice(address); - }); - }, - { immediate: true } - ); - const allBalancePricesLoaded = computed(() => !balance.value.some((e) => e.price === "loading")); onboardStore.subscribeOnAccountChange(() => { resetBalance(); @@ -101,7 +93,6 @@ export const useEraEthereumBalanceStore = defineStore("eraEthereumBalances", () balance, balanceInProgress, balanceError, - allBalancePricesLoaded, requestBalance, deductBalance: ethereumBalancesStore.deductBalance, diff --git a/store/zksync/era/tokens.ts b/store/zksync/era/tokens.ts index f0b462830..fe400694c 100644 --- a/store/zksync/era/tokens.ts +++ b/store/zksync/era/tokens.ts @@ -1,9 +1,11 @@ +import { $fetch } from "ofetch"; import { defineStore, storeToRefs } from "pinia"; -import type { Token, TokenPrice } from "@/types"; +import type { Api } from "@/types"; +import type { Token } from "@/types"; import { useEraProviderStore } from "@/store/zksync/era/provider"; -import { ETH_L1_ADDRESS, ETH_L2_ADDRESS } from "@/utils/constants"; +import { ETH_L2_ADDRESS, ETH_TOKEN } from "@/utils/constants"; export const useEraTokensStore = defineStore("eraTokens", () => { const eraProviderStore = useEraProviderStore(); @@ -16,44 +18,59 @@ export const useEraTokensStore = defineStore("eraTokens", () => { execute: requestTokens, reset: resetTokens, } = usePromise(async () => { - return await eraNetwork.value.getTokens(); - }); - - const tokenPrices = ref<{ [tokenAddress: string]: TokenPrice }>({}); - const requestTokenPrice = async (tokenAddress: string, { force } = { force: false }) => { - if (!force && typeof tokenPrices.value[tokenAddress] === "number") return; - if (tokenPrices.value[tokenAddress] === "loading") return; - tokenPrices.value[tokenAddress] = "loading"; - try { - const provider = eraProviderStore.requestProvider(); - - const price = await provider - .getTokenPrice(tokenAddress === ETH_L2_ADDRESS ? ETH_L1_ADDRESS : tokenAddress) - .catch(() => 0); - tokenPrices.value[tokenAddress] = - typeof price === "number" || typeof price === "string" ? parseFloat(price.toString()) : 0; - } catch (error) { - console.warn(`Failed to get price for Era token ${tokenAddress}`, error); - tokenPrices.value[tokenAddress] = undefined; + if (eraNetwork.value.blockExplorerApi) { + const response: Api.Response.Collection = await $fetch( + `${eraNetwork.value.blockExplorerApi}/tokens?limit=100` + ); + const explorerTokens = response.items.map((token) => ({ + address: token.l2Address, + l1Address: token.l1Address || undefined, + name: token.name || "Unknown token", + symbol: token.symbol || "unknown", + decimals: token.decimals, + iconUrl: token.iconURL || undefined, + price: token.usdPrice || undefined, + })); + const etherExplorerToken = explorerTokens.find((token) => token.address === ETH_L2_ADDRESS); + const tokensWithoutEther = explorerTokens.filter((token) => token.address !== ETH_L2_ADDRESS); + if (!etherExplorerToken) { + return [ETH_TOKEN, ...tokensWithoutEther]; + } else { + return [ + { + ...etherExplorerToken, + iconUrl: ETH_TOKEN.iconUrl, + }, + ...tokensWithoutEther, + ] as Token[]; + } } - }; + if (eraNetwork.value.getTokens) { + return await eraNetwork.value.getTokens(); + } else { + return [ETH_TOKEN]; + } + }); const tokens = computed<{ [tokenAddress: string]: Token } | undefined>(() => { + if (!tokensRaw.value) return undefined; + return Object.fromEntries(tokensRaw.value.map((token) => [token.address, token])); + }); + const l1Tokens = computed<{ [tokenAddress: string]: Token } | undefined>(() => { if (!tokensRaw.value) return undefined; return Object.fromEntries( - tokensRaw.value.map((token) => { - return [token.address, { ...token, price: tokenPrices.value[token.address] }]; - }) + tokensRaw.value + .filter((e) => e.l1Address) + .map((token) => [token.l1Address!, { ...token, l1Address: undefined, address: token.l1Address! }]) ); }); return { - tokens: computed(() => tokens.value), + l1Tokens, + tokens, tokensRequestInProgress: computed(() => tokensRequestInProgress.value), tokensRequestError: computed(() => tokensRequestError.value), requestTokens, resetTokens, - - requestTokenPrice, }; }); diff --git a/store/zksync/era/transfersHistory.ts b/store/zksync/era/transfersHistory.ts index 14a45b90a..1c18233e1 100644 --- a/store/zksync/era/transfersHistory.ts +++ b/store/zksync/era/transfersHistory.ts @@ -70,14 +70,6 @@ export const useEraTransfersHistoryStore = defineStore("eraTransfersHistory", () }); const transfers = ref([]); - const getTransferTokenPrices = (transfers: EraTransfer[]) => { - transfers.forEach((transfer) => { - if (transfer.token && tokens.value?.[transfer.token.address]) { - eraTokensStore.requestTokenPrice(transfer.token.address); - } - }); - }; - const { inProgress: recentTransfersRequestInProgress, error: recentTransfersRequestError, @@ -92,7 +84,6 @@ export const useEraTransfersHistoryStore = defineStore("eraTransfersHistory", () const [response] = await Promise.all([loadNext(), eraTokensStore.requestTokens()]); const mappedTransfers = response.items.map((e) => mapApiTransfer(e)); transfers.value = filterOutDuplicateTransfers(mappedTransfers); - getTransferTokenPrices(mappedTransfers); }, { cache: 30000 } ); @@ -111,7 +102,6 @@ export const useEraTransfersHistoryStore = defineStore("eraTransfersHistory", () const [response] = await Promise.all([loadNext(), eraTokensStore.requestTokens()]); const mappedTransfers = response.items.map((e) => mapApiTransfer(e)); transfers.value = filterOutDuplicateTransfers([...transfers.value, ...mappedTransfers]); - getTransferTokenPrices(mappedTransfers); }, { cache: false } ); diff --git a/store/zksync/era/wallet.ts b/store/zksync/era/wallet.ts index a0fea8f7a..2ad2fcfea 100644 --- a/store/zksync/era/wallet.ts +++ b/store/zksync/era/wallet.ts @@ -1,5 +1,3 @@ -import { watch } from "vue"; - import { BigNumber, ethers, VoidSigner } from "ethers"; import { $fetch } from "ofetch"; import { defineStore, storeToRefs } from "pinia"; @@ -137,19 +135,6 @@ export const useEraWalletStore = defineStore("eraWallet", () => { return [...knownTokens, ...otherTokens]; }); - watch( - balance, - (balances) => { - balances.map((e) => { - if (BigNumber.from(e.amount).isZero()) return; - eraTokensStore.requestTokenPrice(e.address); - }); - }, - { immediate: true } - ); - const allBalancePricesLoaded = computed( - () => !balance.value.some((e) => e.price === "loading") && !balanceInProgress.value - ); const deductBalance = (tokenAddress: string, amount: string) => { if (!balance.value) return; const tokenBalance = balance.value.find((balance) => balance.address === tokenAddress); @@ -191,7 +176,6 @@ export const useEraWalletStore = defineStore("eraWallet", () => { balance, balanceInProgress: computed(() => balanceInProgress.value), balanceError: computed(() => balanceError.value), - allBalancePricesLoaded, requestBalance, deductBalance, diff --git a/types/index.d.ts b/types/index.d.ts index a1b22fe78..d53ac91d6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -41,6 +41,9 @@ export declare namespace Api { name: string | null; symbol: string | null; decimals: number; + usdPrice: number | null; + liquidity: number | null; + iconURL: string | null; }; type Transfer = { diff --git a/utils/constants.ts b/utils/constants.ts index 5d95954cf..c4e9c765c 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -2,5 +2,16 @@ import { L2_ETH_TOKEN_ADDRESS } from "zksync-web3/build/src/utils"; import { checksumAddress } from "./formatters"; +import type { Token } from "@/types"; + export const ETH_L1_ADDRESS = "0x0000000000000000000000000000000000000000"; export const ETH_L2_ADDRESS = checksumAddress(L2_ETH_TOKEN_ADDRESS); + +export const ETH_TOKEN: Token = { + address: ETH_L2_ADDRESS, + l1Address: ETH_L1_ADDRESS, + symbol: "ETH", + decimals: 18, + iconUrl: "/img/eth.svg", + enabledForFees: true, +}; diff --git a/views/zksync/era/Assets.vue b/views/zksync/era/Assets.vue index 18eb6addf..e5788b588 100644 --- a/views/zksync/era/Assets.vue +++ b/views/zksync/era/Assets.vue @@ -122,7 +122,7 @@ import { TransitionAlertScaleInOutTransition } from "@/utils/transitions"; const onboardStore = useOnboardStore(); const walletEraStore = useEraWalletStore(); -const { balance, balanceInProgress, balanceError, allBalancePricesLoaded } = storeToRefs(walletEraStore); +const { balance, balanceInProgress, balanceError } = storeToRefs(walletEraStore); const { destinations } = storeToRefs(useDestinationsStore()); const { eraNetwork } = storeToRefs(useEraProviderStore()); @@ -137,7 +137,7 @@ const displayedBalances = computed(() => { }); }); -const loading = computed(() => balanceInProgress.value || !allBalancePricesLoaded.value); +const loading = computed(() => balanceInProgress.value); const isFaucetDisplayed = computed(() => { if (loading.value) return false; if (eraNetwork.value.faucetUrl) { diff --git a/views/zksync/era/Balances.vue b/views/zksync/era/Balances.vue index 18c6f3eee..72b2afc36 100644 --- a/views/zksync/era/Balances.vue +++ b/views/zksync/era/Balances.vue @@ -42,12 +42,10 @@ import { groupBalancesByAmount } from "@/utils/mappers"; const onboardStore = useOnboardStore(); const walletEraStore = useEraWalletStore(); -const { balance, balanceInProgress, balanceError, allBalancePricesLoaded } = storeToRefs(walletEraStore); +const { balance, balanceInProgress, balanceError } = storeToRefs(walletEraStore); const { eraNetwork } = storeToRefs(useEraProviderStore()); -const { loading, reset: resetSingleLoading } = useSingleLoading( - computed(() => balanceInProgress.value || !allBalancePricesLoaded.value) -); +const { loading, reset: resetSingleLoading } = useSingleLoading(computed(() => balanceInProgress.value)); const balanceGroups = groupBalancesByAmount(balance); diff --git a/views/zksync/era/transactions/Deposit.vue b/views/zksync/era/transactions/Deposit.vue index e68b12e6e..d38e269e1 100644 --- a/views/zksync/era/transactions/Deposit.vue +++ b/views/zksync/era/transactions/Deposit.vue @@ -68,7 +68,7 @@ :tokens="availableTokens" :balances="availableBalances" :max-amount="maxAmount" - :loading="tokensRequestInProgress || balancesLoading" + :loading="tokensRequestInProgress || balanceInProgress" autofocus /> @@ -166,7 +166,7 @@ import useNetworks from "@/composables/useNetworks"; import useFee from "@/composables/zksync/era/deposit/useFee"; import type { ConfirmationModalTransaction } from "@/components/transaction/zksync/era/deposit/ConfirmTransactionModal.vue"; -import type { Token } from "@/types"; +import type { Token, TokenAmount } from "@/types"; import type { BigNumberish } from "ethers"; import type { PropType } from "vue"; @@ -201,20 +201,18 @@ const eraWalletStore = useEraWalletStore(); const { account } = storeToRefs(onboardStore); const { eraNetwork } = storeToRefs(eraProviderStore); const { destinations } = storeToRefs(useDestinationsStore()); -const { tokens, tokensRequestInProgress, tokensRequestError } = storeToRefs(eraTokensStore); -const { balance, balanceInProgress, allBalancePricesLoaded, balanceError } = storeToRefs(eraEthereumBalance); +const { l1Tokens, tokensRequestInProgress, tokensRequestError } = storeToRefs(eraTokensStore); +const { balance, balanceInProgress, balanceError } = storeToRefs(eraEthereumBalance); const { isCustomNode } = useNetworks(); const destination = computed(() => destinations.value.era); -const availableTokens = computed(() => { - if (!tokens.value) return []; - return Object.values(tokens.value).filter((e) => e.l1Address); +const availableTokens = computed(() => { + if (balance.value) return balance.value; + return Object.values(l1Tokens.value ?? []); }); -const availableBalances = computed(() => { - if (!tokens.value) return []; - // return balance.value.filter((e) => e.l1Address); <-- Uncomment once Era Withdrawal Finalizer is live on mainnet - return balance.value.filter((e) => e.l1Address && tokens.value![e.address]); +const availableBalances = computed(() => { + return balance.value ?? []; }); const routeTokenAddress = computed(() => { if (!route.query.token || Array.isArray(route.query.token) || !isAddress(route.query.token)) { @@ -230,19 +228,19 @@ const tokenWithHighestBalancePrice = computed(() => { }); return tokenWithHighestBalancePrice[0] ? tokenWithHighestBalancePrice[0] : undefined; }); -const defaultToken = computed(() => availableTokens.value?.[0] ?? undefined); +const defaultToken = computed(() => availableTokens.value[0] ?? undefined); const selectedTokenAddress = ref( routeTokenAddress.value ?? tokenWithHighestBalancePrice.value?.address ?? defaultToken.value?.address ); const selectedToken = computed(() => { - if (!tokens.value) { - return undefined; + if (!selectedTokenAddress.value) { + return defaultToken.value; } - return selectedTokenAddress.value - ? availableTokens.value.find((e) => e.address === selectedTokenAddress.value) || - availableBalances.value.find((e) => e.address === selectedTokenAddress.value) || - defaultToken.value - : defaultToken.value; + return ( + availableTokens.value.find((e) => e.address === selectedTokenAddress.value) || + availableBalances.value.find((e) => e.address === selectedTokenAddress.value) || + defaultToken.value + ); }); const amountInputTokenAddress = computed({ get: () => selectedToken.value?.address, @@ -251,24 +249,7 @@ const amountInputTokenAddress = computed({ }, }); const tokenBalance = computed(() => { - return balance.value.find((e) => e.address === selectedToken.value?.address)?.amount; -}); -watch( - () => selectedToken?.value?.address, - (address) => { - if (!address) return; - eraTokensStore.requestTokenPrice(address); - }, - { immediate: true } -); -watch(allBalancePricesLoaded, (loaded) => { - if (loaded && !selectedTokenAddress.value) { - if (totalComputeAmount.value.isZero()) { - selectedTokenAddress.value = tokenWithHighestBalancePrice.value?.address; - } else { - selectedTokenAddress.value = selectedToken.value?.address; - } - } + return balance.value?.find((e) => e.address === selectedToken.value?.address)?.amount; }); const transactionKey = ref(0); @@ -282,7 +263,7 @@ const { setAllowance, } = useAllowance( computed(() => account.value.address), - computed(() => selectedToken.value?.l1Address), + computed(() => selectedToken.value?.address), async () => (await eraProviderStore.requestProvider().getDefaultBridgeAddresses()).erc20L1, onboardStore.getWallet, onboardStore.getPublicClient @@ -332,7 +313,7 @@ const { enoughBalanceToCoverFee, estimateFee, resetFee, -} = useFee(tokens, balance, eraWalletStore.getL1VoidSigner, onboardStore.getPublicClient); +} = useFee(availableTokens, balance, eraWalletStore.getL1VoidSigner, onboardStore.getPublicClient); watch(enoughBalanceToCoverFee, (isEnough) => { if (!isEnough && transactionConfirmModalOpened.value) { transactionConfirmModalOpened.value = false; @@ -404,11 +385,7 @@ watch( { immediate: true } ); -const feeLoading = computed(() => feeInProgress.value || (!fee.value && balancesLoading.value)); - -const balancesLoading = computed(() => { - return balanceInProgress.value || (!selectedTokenAddress.value && !allBalancePricesLoaded.value); -}); +const feeLoading = computed(() => feeInProgress.value || (!fee.value && balanceInProgress.value)); const continueButtonDisabled = computed(() => { if ( @@ -435,7 +412,7 @@ const fetchBalances = async (force = false) => { if (!account.value.address) return; await eraEthereumBalance.requestBalance({ force }).then(() => { - if (allBalancePricesLoaded.value && !selectedToken.value) { + if (!selectedToken.value) { selectedTokenAddress.value = tokenWithHighestBalancePrice.value?.address; } }); diff --git a/views/zksync/era/transactions/Transfer.vue b/views/zksync/era/transactions/Transfer.vue index cadde54b0..c41bd5a30 100644 --- a/views/zksync/era/transactions/Transfer.vue +++ b/views/zksync/era/transactions/Transfer.vue @@ -51,7 +51,7 @@ :tokens="availableTokens" :balances="availableBalances" :max-amount="maxAmount" - :loading="tokensRequestInProgress || balancesLoading" + :loading="tokensRequestInProgress || balanceInProgress" autofocus >