diff --git a/apps/bridge/package.json b/apps/bridge/package.json index 0e03c7dc..c45d4980 100644 --- a/apps/bridge/package.json +++ b/apps/bridge/package.json @@ -18,11 +18,11 @@ "@mantle/constants": "workspace:*", "@mantle/supagraph": "workspace:*", "@mantle/ui": "workspace:*", + "@mantle/utils": "workspace:*", "@mantleio/contracts": "^0.2.0", "@mantleio/core-utils": "^0.1.0", "@mantleio/sdk": "^0.2.1", "@tanstack/react-query": "^4.14.5", - "decimal.js": "^10.4.3", "ethers": "^5.7.2", "framer-motion": "^10.12.10", "lodash": "^4.17.21", @@ -32,7 +32,7 @@ "react-dom": "18.2.0", "react-error-boundary": "^3.1.4", "react-icons": "^4.8.0", - "wagmi": "^0.12.13", + "wagmi": "^1.3.9", "zustand": "^4.0.0-rc.1" }, "devDependencies": { @@ -49,6 +49,6 @@ "eslint": "^8.30.0", "postcss": "^8.4.18", "tailwindcss": "^3.2.1", - "typescript": "4.9.4" + "typescript": "5.0.4" } } diff --git a/apps/bridge/src/app/graphql/sync/gas/route.ts b/apps/bridge/src/app/graphql/sync/gas/route.ts new file mode 100644 index 00000000..d6abdb6d --- /dev/null +++ b/apps/bridge/src/app/graphql/sync/gas/route.ts @@ -0,0 +1,242 @@ +/* eslint-disable no-await-in-loop, no-restricted-syntax, @typescript-eslint/no-loop-func, no-param-reassign */ +import { NextResponse } from "next/server"; + +// Import mongodb client +import { DB, Mongo, Store, getEngine } from "@mantle/supagraph"; +import { getMongodb } from "@providers/mongoClient"; +import { MongoClient } from "mongodb"; + +// Import the current configuration +import config from "@supagraph/config"; + +// Supagraph components for claims +import { claim } from "@supagraph/handlers/claim"; +import { L1ToL2MessageEntity } from "@supagraph/types"; + +// forces the route handler to be dynamic +export const dynamic = "force-dynamic"; + +// Switch out the engine for development to avoid the mongo requirment locally +Store.setEngine({ + // name the connection + name: config.name, + // db is dependent on env + db: + // in production/production like environments we want to store mutations to mongo otherwise we can store them locally + process.env.NODE_ENV === "development" && config.dev + ? // connect store to in-memory/node-persist store + DB.create({ kv: {}, name: config.name }) + : // connect store to MongoDB + Mongo.create({ + kv: {}, + client: getMongodb(process.env.MONGODB_URI!), + name: config.name, + mutable: config.mutable, + }), +}); + +// Pull any drops that have been missed (we know these are missed because the first bridge should always has gasDropped: true) +const getMissedDrops = async (db: ReturnType) => { + // query for missed gas drops (groupby $from taking the first deposit only then check gasDropped for that "from" on subsequent deposits) + return db.collection("l1ToL2Message").aggregate<{ + _id: string; + id: string; + from: string; + gasDropped: boolean; + }>( + [ + { + $sort: { + l1BlockNumber: 1, + }, + }, + { + $group: { + _id: "$from", + id: { $first: "$id" }, + from: { $first: "$from" }, + gasDropped: { $first: "$gasDropped" }, + }, + }, + { + $lookup: { + from: "l1ToL2Message", + let: { + joinOn: "$from", + }, + pipeline: [ + { + $match: { + $expr: { + $eq: ["$from", "$$joinOn"], + }, + gasDropped: true, + }, + }, + ], + as: "alreadyDropped", + }, + }, + { + $project: { + id: 1, + from: 1, + gasDropped: 1, + count: { + $size: "$alreadyDropped", + }, + }, + }, + { + $match: { + gasDropped: null, + count: { + $eq: 0, + }, + }, + }, + ], + { maxTimeMS: 60000, allowDiskUse: true } + ); +}; + +// mark as dropped +const markDrop = async (tx: { id: string }) => { + // clear chainId and block to skip ts updates + Store.setChainId(0); + Store.clearBlock(); + // issue the gasDrop + const message = await Store.get( + "L1ToL2Message", + tx.id, + // we don't care what state it holds... + true + ); + // mark that gas was dropped + message.set("gasDropped", true); + // save the changes + await message.save(); + + return true; +}; + +// Process any drops that have been missed between operations (send them over one at a time, one after the other) +const processMissedDrops = async ( + missedIn: { + from: string; + id: string; + }[] +) => { + // set how many claims to process at once + const perBlock = 25; + // split the input into blocks to process n [perBlock] at a time + const missed = missedIn.reduce( + (missedOut, item, index) => { + // get index for block + const blockIndex = Math.floor(index / perBlock); + // set new block + missedOut[blockIndex] = missedOut[blockIndex] || []; + // push item to block + missedOut[blockIndex].push(item); + + return missedOut; + }, + [] as { + from: string; + id: string; + }[][] + ); + // push true/false for all claims here + const processed: boolean[] = []; + + // process in blocks to avoid fetch timeouts + for (const block of missed) { + await Promise.all( + block.map(async (tx) => { + // add a gas-drop claim for the sender + return ( + claim(tx.from) + .then(async (result: any) => { + // so long as the claim hasn't errored (alredy claimed etc...) + if ( + !result?.error || + result.error?.meta?.target === "ClaimCode_code_key" + ) { + // eslint-disable-next-line no-console + console.log( + `Gas-drop created for ${result?.data?.reservedFor || tx.from}` + ); + // issue the gasDrop + processed.push(await markDrop(tx)); + } + // mark as failed + processed.push(false); + }) + // noop any errors + .catch(() => { + processed.push(false); + }) + ); + }) + ); + } + + return processed; +}; + +// Expose the sync command on a route so that we can call it with a cron job +export async function GET() { + // open a checkpoint on the db... + const engine = await getEngine(); + + // select the named db + const db = engine?.db as Mongo; + + // if we have a db + if (db.db) { + // create a checkpoint + engine?.stage?.checkpoint(); + + // pull the missed drops + const result = await getMissedDrops(await Promise.resolve(db.db)); + const missed = await result.toArray(); + + // eslint-disable-next-line no-console + console.log("Missed", missed.length); + + // process the missed claims + const claimed = (await processMissedDrops(missed)).filter((v) => v); + + // write all updates to db + await engine?.stage?.commit(); + + // eslint-disable-next-line no-console + console.log("Claimed", claimed.length); + + // we don't need to sync more often than once per block - and if we're using vercel.json crons we can only sync 1/min - 1/10mins seems reasonable for this + return NextResponse.json( + { + missed: missed.length, + claimed: claimed.length, + }, + { + headers: { + // allow to be cached for revalidate seconds and allow caching in shared public cache (upto revalidate seconds) + "Cache-Control": `max-age=${config.revalidate}, public, s-maxage=${config.revalidate}`, + }, + } + ); + } + + return NextResponse.json( + { + error: "no db", + }, + { + headers: { + // allow to be cached for revalidate seconds and allow caching in shared public cache (upto revalidate seconds) + "Cache-Control": `max-age=${config.revalidate}, public, s-maxage=${config.revalidate}`, + }, + } + ); +} diff --git a/apps/bridge/src/app/graphql/sync/route.ts b/apps/bridge/src/app/graphql/sync/route.ts index 8e0e7c22..8f387aba 100644 --- a/apps/bridge/src/app/graphql/sync/route.ts +++ b/apps/bridge/src/app/graphql/sync/route.ts @@ -24,6 +24,9 @@ import { handlers } from "@supagraph/handlers"; // Import revalidation timings from config import config from "@supagraph/config"; +// forces the route handler to be dynamic +export const dynamic = "force-dynamic"; + // Object of providers by rpcUrl const providerCache: { [rpcUrl: string]: providers.JsonRpcProvider } = {}; diff --git a/apps/bridge/src/app/head.tsx b/apps/bridge/src/app/head.tsx index b0395a69..2b7fe297 100644 --- a/apps/bridge/src/app/head.tsx +++ b/apps/bridge/src/app/head.tsx @@ -1,6 +1,8 @@ import { ABSOLUTE_PATH, APP_NAME, + L1_CHAIN_ID, + L2_CHAIN_ID, META, OG_TITLE, TWITTER_DESC, @@ -9,6 +11,8 @@ import { import { Cookies } from "@mantle/ui"; export default function Head() { + const isTestnet = L1_CHAIN_ID === 5 || L2_CHAIN_ID === 5001; + return ( <> {APP_NAME} @@ -27,7 +31,7 @@ export default function Head() { - + ); } diff --git a/apps/bridge/src/app/layout.tsx b/apps/bridge/src/app/layout.tsx index 12202105..73f0c727 100644 --- a/apps/bridge/src/app/layout.tsx +++ b/apps/bridge/src/app/layout.tsx @@ -2,13 +2,12 @@ import "./styles/globals.css"; // Dummy components import { - GTWalsheimRegular, - GTWalsheimMedium, Header, SlimFooter, PageWrapper, PageBackroundImage, PageContainer, + GTWalsheim, } from "@mantle/ui"; import LegalDisclaimer from "@components/LegalDisclaimer"; @@ -25,12 +24,9 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + - + {/* @ts-expect-error Server Component */} (); + const [address, setAddress] = useState(); // chain is valid if it matches any of these states... const isChainID = useMemo(() => { @@ -51,6 +52,22 @@ function ConnectWallet() { address, ]); + // pick up connection details from wagmi + const { address: wagmiAddress } = useAccount({ + onConnect: async () => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + await checkConnection(); + + // auto-switch - ask the wallet to attempt to switch to chosen chain on first-connect + if (!isChainID) { + // await changeNetwork(); + } + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + await changeAccount(); + }, + }); + // when disconnecting we want to retain control over whether or not to attempt a reconnect const reconnect = useRef(false); @@ -58,7 +75,7 @@ function ConnectWallet() { const { switchToNetwork } = useSwitchToNetwork(); // control wagmi connector - const { connect, connectors } = useConnect(); + const { connect, connectors, pendingConnector } = useConnect(); const { disconnect, disconnectAsync } = useDisconnect({ onMutate: () => { @@ -81,63 +98,35 @@ function ConnectWallet() { // record change of account const changeAccount = async () => { - const accounts = await window.ethereum?.request({ - method: "eth_requestAccounts", + setClient({ + chainId, + isConnected: true, + address: wagmiAddress, + connector: client?.connector || pendingConnector?.id, }); - - if (accounts) { - setClient({ - chainId: parseInt( - (await window.ethereum?.request({ - method: "eth_chainId", - })) || "-1", - 16 - ), - isConnected: true, - address: accounts[0], - }); - } }; // trigger change of network const changeNetwork = async () => { - if (!window.ethereum) throw new Error("No crypto wallet found"); // trigger a change of network await switchToNetwork(chainId); }; // check the connection is valid const checkConnection = async () => { - const { ethereum } = window; - if (ethereum) { - const accounts = await ethereum.request({ method: "eth_accounts" }); - if (accounts.length > 0) { - setClient({ - isConnected: true, - address: accounts[0], - }); - } else { - setClient({ - isConnected: false, - }); - } + if (wagmiAddress) { + setClient({ + isConnected: true, + address: wagmiAddress, + connector: client?.connector, + }); + } else { + setClient({ + isConnected: false, + }); } }; - // pick up connection details from wagmi - const { address: wagmiAddress } = useAccount({ - onConnect: async () => { - await checkConnection(); - - // auto-switch - ask the wallet to attempt to switch to chosen chain on first-connect - if (!isChainID) { - // await changeNetwork(); - } - - await changeAccount(); - }, - }); - // set wagmi address to address for ssr useEffect(() => { if (!reconnect.current || wagmiAddress) { @@ -165,14 +154,14 @@ function ConnectWallet() { // return connect/disconnect component return ( -
+
{isChainID && client.isConnected && client.address ? ( - + +
+ +
) : (
diff --git a/apps/bridge/src/components/account/Tabs.tsx b/apps/bridge/src/components/account/Tabs.tsx index 168e09f9..d0847a91 100644 --- a/apps/bridge/src/components/account/Tabs.tsx +++ b/apps/bridge/src/components/account/Tabs.tsx @@ -78,7 +78,7 @@ export default function Tabs() { return ( (view === Views.Account && ( - + - connectors.find((conn) => conn.id === "metamask") || - // fallback to injected provider - new InjectedConnector(), - [connectors] - ); - // check that we're connected to the appropriate chain const isLayer1ChainID = useIsChainID(L1_CHAIN_ID); const isMantleChainID = useIsChainID(L2_CHAIN_ID); // set address with useState to avoid hydration errors - const [address, setAddress] = useState<`0x${string}`>(client?.address!); + const [address, setAddress] = useState(client?.address!); // switch to the correct network if missing const { switchToNetwork } = useSwitchToNetwork(); @@ -183,47 +173,73 @@ export default function CTA({ return (
- +
+ + ) : ( + + > + {CTAButtonText} + + )} {/*
*/} {isChainID && diff --git a/apps/bridge/src/components/bridge/TokenSelect.tsx b/apps/bridge/src/components/bridge/TokenSelect.tsx index 8d36e512..847fa6cd 100644 --- a/apps/bridge/src/components/bridge/TokenSelect.tsx +++ b/apps/bridge/src/components/bridge/TokenSelect.tsx @@ -16,9 +16,8 @@ import { L1_CHAIN_ID, L2_CHAIN_ID, } from "@config/constants"; -import { localeZero, formatBigNumberString } from "@utils/formatStrings"; import { formatUnits, parseUnits } from "ethers/lib/utils.js"; - +import { formatBigNumberString, localeZero } from "@mantle/utils"; import DirectionLabel from "@components/bridge/utils/DirectionLabel"; import { MantleLogo } from "@components/bridge/utils/MantleLogo"; import KindReminder from "@components/bridge/utils/KindReminder"; diff --git a/apps/bridge/src/components/bridge/TransactionPanel.tsx b/apps/bridge/src/components/bridge/TransactionPanel.tsx index fe051787..4d1f9acd 100644 --- a/apps/bridge/src/components/bridge/TransactionPanel.tsx +++ b/apps/bridge/src/components/bridge/TransactionPanel.tsx @@ -15,11 +15,7 @@ import { import { formatEther, formatUnits, parseUnits } from "ethers/lib/utils.js"; import { BigNumber, constants } from "ethers"; -import { - localeZero, - formatBigNumberString, - formatTime, -} from "@utils/formatStrings"; +import { formatBigNumberString, formatTime, localeZero } from "@mantle/utils"; import { useIsChainID } from "@hooks/web3/read/useIsChainID"; import { useMantleSDK } from "@providers/mantleSDKContext"; import { useQuery } from "wagmi"; @@ -57,7 +53,10 @@ export default function TransactionPanel({ }, ], async () => { - return crossChainMessenger?.getChallengePeriodSeconds(); + if (crossChainMessenger?.l1ChainId) { + return crossChainMessenger?.getChallengePeriodSeconds(); + } + return false; } ); @@ -118,7 +117,7 @@ export default function TransactionPanel({ const isMantleChainID = useIsChainID(L2_CHAIN_ID); // set address with useState to avoid hydration errors - const [address, setAddress] = useState<`0x${string}`>(client?.address!); + const [address, setAddress] = useState(client?.address!); // check that the chainId is valid for the selected use-case const isChainID = useMemo(() => { diff --git a/apps/bridge/src/components/bridge/dialogue/Default.tsx b/apps/bridge/src/components/bridge/dialogue/Default.tsx index 10821f9a..fc749a8a 100644 --- a/apps/bridge/src/components/bridge/dialogue/Default.tsx +++ b/apps/bridge/src/components/bridge/dialogue/Default.tsx @@ -18,7 +18,7 @@ import { Button, Typography } from "@mantle/ui"; import { MdClear } from "react-icons/md"; import Values from "@components/bridge/utils/Values"; import { useMantleSDK } from "@providers/mantleSDKContext"; -import { formatTime } from "@utils/formatStrings"; +import { formatTime } from "@mantle/utils"; import { useQuery } from "wagmi"; export default function Default({ diff --git a/apps/bridge/src/components/bridge/utils/TxLink.tsx b/apps/bridge/src/components/bridge/utils/TxLink.tsx index bb53525e..d35c372f 100644 --- a/apps/bridge/src/components/bridge/utils/TxLink.tsx +++ b/apps/bridge/src/components/bridge/utils/TxLink.tsx @@ -1,5 +1,5 @@ import { CHAINS } from "@config/constants"; -import { truncateAddress } from "@utils/formatStrings"; +import { truncateAddress } from "@mantle/utils"; // a link to the networks block explorer export default function TxLink({ @@ -55,7 +55,7 @@ export default function TxLink({ {asHash - ? truncateAddress(txHash as `0x${string}`) + ? truncateAddress(txHash as string) : `${CHAINS[chainId].chainName} Explorer`} diff --git a/apps/bridge/src/config/constants.ts b/apps/bridge/src/config/constants.ts index 0321349b..7c518f74 100644 --- a/apps/bridge/src/config/constants.ts +++ b/apps/bridge/src/config/constants.ts @@ -39,16 +39,16 @@ export const TWITTER_DESC = `Bridge your ${ // Get the current absolute path from the env export function getBaseUrl() { - const vercel = - // eslint-disable-next-line turbo/no-undeclared-env-vars - process.env.NEXT_PUBLIC_SITE_URL ?? - // eslint-disable-next-line turbo/no-undeclared-env-vars - process.env.NEXT_PUBLIC_VERCEL_URL; // return the fully resolved absolute url - return vercel - ? `https://${vercel}` - : // this should match the port used by the current app - "http://localhost:3003"; + return ( + process.env.NEXT_PUBLIC_VERCEL_URL || + (process.env.NEXT_PUBLIC_SITE_URL + ? `https://${ + L1_CHAIN_ID === 1 ? "bridge.mantle.xyz" : "bridge.testnet.mantle.xyz" + }` + : // this should match the port used by the current app + "http://localhost:3003") + ); } // export the absolute path @@ -116,7 +116,7 @@ export const CHAINS: Record< }, rpcUrls: [ // infura backed redirect gateway - `/rpc`, + `${ABSOLUTE_PATH}/rpc`, // public gateway `https://rpc.ankr.com/eth`, ], @@ -144,7 +144,7 @@ export const CHAINS: Record< }, rpcUrls: [ // infura backed redirect gateway - `/rpc`, + `${ABSOLUTE_PATH}/rpc`, // public gateway `https://rpc.ankr.com/eth_goerli`, ], @@ -187,7 +187,7 @@ export const CHAINS_FORMATTED: Record = { http: [CHAINS[5000].rpcUrls[0]], }, public: { - http: [CHAINS[5000].rpcUrls[1]], + http: [CHAINS[5000].rpcUrls[0]], }, }, id: 5000, @@ -258,7 +258,7 @@ export const MANTLE_TOKEN_LIST_URL = "https://token-list.mantle.xyz/mantle.tokenlist.json"; // Address for multicall3 contract on each network - Multicall3: https://github.com/mds1/multicall -export const MULTICALL_CONTRACTS: Record = { +export const MULTICALL_CONTRACTS: Record = { 1: "0xcA11bde05977b3631167028862bE2a173976CA11", 5000: "0x9155FcC40E05616EBFf068446136308e757e43dA", 5: "0xcA11bde05977b3631167028862bE2a173976CA11", @@ -267,10 +267,45 @@ export const MULTICALL_CONTRACTS: Record = { // ERC-20 abi for balanceOf && allowanceOf export const TOKEN_ABI = [ - "function balanceOf(address account) external view returns (uint256)", - "function allowance(address owner, address spender) external view returns (uint256)", - "function approve(address spender, uint256 amount) returns (bool)", -]; + { + constant: true, + inputs: [ + { + name: "_owner", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + name: "balance", + type: "uint256", + }, + ], + payable: false, + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + ], + name: "allowance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, +] as const; export const MANTLE_MIGRATOR_URL = "https://migratebit.mantle.xyz"; export const MANTLE_MIGRATOR_HISTORY_PATH = "/account/migrate"; diff --git a/apps/bridge/src/hooks/web3/bridge/read/useAllowanceCheck.ts b/apps/bridge/src/hooks/web3/bridge/read/useAllowanceCheck.ts index b1e79655..3197cce9 100644 --- a/apps/bridge/src/hooks/web3/bridge/read/useAllowanceCheck.ts +++ b/apps/bridge/src/hooks/web3/bridge/read/useAllowanceCheck.ts @@ -15,7 +15,7 @@ import { useQuery } from "wagmi"; function useAllowanceCheck( chainId: number, - client: { address?: `0x${string}` | undefined }, + client: { address?: string | undefined }, bridgeAddress: string | false | undefined, selectedToken: { [x: string]: string }, tokens: Token[], diff --git a/apps/bridge/src/hooks/web3/bridge/read/useGasEstimate.ts b/apps/bridge/src/hooks/web3/bridge/read/useGasEstimate.ts index 49c38278..9c7a75c1 100644 --- a/apps/bridge/src/hooks/web3/bridge/read/useGasEstimate.ts +++ b/apps/bridge/src/hooks/web3/bridge/read/useGasEstimate.ts @@ -15,7 +15,7 @@ import useTokenList from "./useTokenList"; function useGasEstimate( chainId: number, - client: { address?: `0x${string}` | undefined }, + client: { address?: string | undefined }, selectedToken: { [x: string]: string }, destinationToken: { [x: string]: string }, bridgeAddress: string | false | undefined, diff --git a/apps/bridge/src/hooks/web3/bridge/read/useHistoryDeposits.ts b/apps/bridge/src/hooks/web3/bridge/read/useHistoryDeposits.ts index 2ad0b156..afa8c0b0 100644 --- a/apps/bridge/src/hooks/web3/bridge/read/useHistoryDeposits.ts +++ b/apps/bridge/src/hooks/web3/bridge/read/useHistoryDeposits.ts @@ -12,7 +12,7 @@ export type Deposit = { }; function useHistoryDeposits( - client: { address?: `0x${string}` | undefined }, + client: { address?: string | undefined }, depositsUrl: string, deposits: Deposit[] | undefined, setDeposits: (val: Deposit[]) => void diff --git a/apps/bridge/src/hooks/web3/bridge/read/useHistoryWithdrawals.ts b/apps/bridge/src/hooks/web3/bridge/read/useHistoryWithdrawals.ts index 36e2f76b..a3be728b 100644 --- a/apps/bridge/src/hooks/web3/bridge/read/useHistoryWithdrawals.ts +++ b/apps/bridge/src/hooks/web3/bridge/read/useHistoryWithdrawals.ts @@ -12,7 +12,7 @@ export type Withdrawal = { }; function useHistoryWithdrawals( - client: { address?: `0x${string}` | undefined }, + client: { address?: string | undefined }, withdrawalsUrl: string, withdrawals: Withdrawal[] | undefined, setWithdrawals: (val: Withdrawal[]) => void diff --git a/apps/bridge/src/hooks/web3/bridge/write/useCallApprove.tsx b/apps/bridge/src/hooks/web3/bridge/write/useCallApprove.tsx index ccf6048b..3b51b081 100644 --- a/apps/bridge/src/hooks/web3/bridge/write/useCallApprove.tsx +++ b/apps/bridge/src/hooks/web3/bridge/write/useCallApprove.tsx @@ -6,7 +6,7 @@ import { useContractWrite } from "wagmi"; export function useCallApprove(selected: Token) { // hydrate context into state - const { bridgeAddress, destinationTokenAmount, resetAllowance } = + const { provider, bridgeAddress, destinationTokenAmount, resetAllowance } = useContext(StateContext); // if we're running an approve tx, we'll track the state on approvalStatus @@ -14,7 +14,6 @@ export function useCallApprove(selected: Token) { // setup a call to approve an allowance on the selected token const { writeAsync: writeApprove } = useContractWrite({ - mode: "recklesslyUnprepared", address: selected.address, abi: TOKEN_ABI, functionName: "approve", @@ -26,10 +25,13 @@ export function useCallApprove(selected: Token) { // mark as waiting... setApprovalStatus("Waiting for tx approval..."); // perform the tx call - const txRes = await writeApprove({ - recklesslySetUnpreparedArgs: [ + const txRes = await writeApprove?.({ + args: [ bridgeAddress, - parseUnits(destinationTokenAmount || "0", selected.decimals), + parseUnits( + destinationTokenAmount || "1", + selected.decimals + ).toString(), ], }).catch((e) => { throw e; @@ -37,9 +39,11 @@ export function useCallApprove(selected: Token) { // mark approval... setApprovalStatus("Tx approved, waiting for confirmation..."); // wait for one confirmation - await txRes.wait(1).catch((e) => { - throw e; - }); + await provider + .waitForTransaction(txRes?.hash || "", 1) + .catch((e: any) => { + throw e; + }); // final update setApprovalStatus("Tx settled"); } catch { @@ -57,8 +61,9 @@ export function useCallApprove(selected: Token) { }, [ bridgeAddress, destinationTokenAmount, - selected.decimals, + provider, resetAllowance, + selected.decimals, writeApprove, ]); diff --git a/apps/bridge/src/hooks/web3/bridge/write/useCallBridge.tsx b/apps/bridge/src/hooks/web3/bridge/write/useCallBridge.tsx index a03f1dbe..a3a9d3e0 100644 --- a/apps/bridge/src/hooks/web3/bridge/write/useCallBridge.tsx +++ b/apps/bridge/src/hooks/web3/bridge/write/useCallBridge.tsx @@ -14,7 +14,7 @@ import { parseUnits } from "ethers/lib/utils.js"; import { ToastProps, useToast } from "@hooks/useToast"; import { useMutation, useQuery } from "wagmi"; -import { formatTime } from "@utils/formatStrings"; +import { formatTime } from "@mantle/utils"; import { useWaitForRelay } from "./useWaitForRelay"; class TxError extends Error { diff --git a/apps/bridge/src/hooks/web3/bridge/write/useWaitForRelay.tsx b/apps/bridge/src/hooks/web3/bridge/write/useWaitForRelay.tsx index 348343e6..3faead17 100644 --- a/apps/bridge/src/hooks/web3/bridge/write/useWaitForRelay.tsx +++ b/apps/bridge/src/hooks/web3/bridge/write/useWaitForRelay.tsx @@ -19,7 +19,7 @@ import { } from "@config/constants"; import MantleToL1SVG from "@components/bridge/utils/MantleToL1SVG"; import { useMantleSDK } from "@providers/mantleSDKContext"; -import { formatTime } from "@utils/formatStrings"; +import { formatTime } from "@mantle/utils"; import { useQuery } from "wagmi"; // How long to stay inside the waitForMessageStatus while loop for diff --git a/apps/bridge/src/hooks/web3/read/useAccountBalances.ts b/apps/bridge/src/hooks/web3/read/useAccountBalances.ts index 36670652..4e59717b 100644 --- a/apps/bridge/src/hooks/web3/read/useAccountBalances.ts +++ b/apps/bridge/src/hooks/web3/read/useAccountBalances.ts @@ -1,25 +1,25 @@ import { CHAINS_FORMATTED, L1_CHAIN_ID, + L2_CHAIN_ID, + MULTICALL_CONTRACTS, TOKEN_ABI, Token, } from "@config/constants"; -import { Network } from "@ethersproject/providers"; import { BigNumberish, Contract, providers } from "ethers"; -import { MutableRefObject } from "react"; -import { callMulticallContract } from "@utils/multicallContract"; +import { + callMulticallContract, + getMulticallContract, +} from "@utils/multicallContract"; import { formatUnits } from "ethers/lib/utils.js"; import { useQuery } from "wagmi"; function useAccountBalances( chainId: number, - client: { address?: `0x${string}` | undefined }, + client: { address?: string | undefined }, tokens: Token[], - multicall: MutableRefObject< - { network: Network; multicallContract: Contract } | undefined - >, setIsLoadingBalances: (arg: boolean) => void ) { // perform a multicall on the given network to get all token balances for user @@ -36,24 +36,23 @@ function useAccountBalances( { address: client?.address, chainId, - multicall: multicall.current?.network.name, }, ], async () => { // connect to L1 on public gateway but use default rpc for L2 - const provider = - chainId === L1_CHAIN_ID - ? new providers.JsonRpcProvider( - CHAINS_FORMATTED[L1_CHAIN_ID].rpcUrls.public.http[0] - ) - : multicall.current?.multicallContract.provider; - // only run the multicall if we're connected to the correct network - if ( - client?.address && - client?.address !== "0x" && - multicall.current?.network.chainId === chainId && + const provider = new providers.JsonRpcProvider( + CHAINS_FORMATTED[ + chainId === L1_CHAIN_ID ? L1_CHAIN_ID : L2_CHAIN_ID + ].rpcUrls.public.http[0] + ); + // get the current multicall contract + const multicall = await getMulticallContract( + MULTICALL_CONTRACTS[chainId], provider - ) { + ); + + // only run the multicall if we're connected to the correct network + if (client?.address && client?.address !== "0x" && multicall) { // filter any native tokens from the selection const filteredTokens = tokens.filter( (v: { address: string }) => @@ -66,7 +65,7 @@ function useAccountBalances( // produce a set of balanceOf calls to check users balance against every token const calls = filteredTokens.map((token: { address: string }) => { return { - target: token.address as `0x${string}`, + target: token.address as string, contract: new Contract(token.address, TOKEN_ABI, provider), fns: [ { @@ -79,7 +78,7 @@ function useAccountBalances( // run all calls... const responses = await callMulticallContract( // connect to provider if different multicallContract default - multicall.current.multicallContract.connect(provider), + multicall, calls ); const newBalances = responses.reduce((fillBalances, value, key) => { diff --git a/apps/bridge/src/hooks/web3/read/useL2FeeData.ts b/apps/bridge/src/hooks/web3/read/useL2FeeData.ts index 3629fda7..9821db63 100644 --- a/apps/bridge/src/hooks/web3/read/useL2FeeData.ts +++ b/apps/bridge/src/hooks/web3/read/useL2FeeData.ts @@ -1,9 +1,28 @@ import { L2_CHAIN_ID } from "@config/constants"; -import { useProvider } from "wagmi"; +import { usePublicClient } from "wagmi"; +import { providers } from "ethers"; +import { useMemo } from "react"; import useFeeData from "./useFeeData"; function useL2FeeData() { - const mantleProvider = useProvider({ chainId: L2_CHAIN_ID }); + const publicClient = usePublicClient({ chainId: L2_CHAIN_ID }); + + // create an ethers provider from the publicClient + const mantleProvider = useMemo(() => { + const { chain, transport } = publicClient; + const network = { + chainId: chain.id, + name: chain.name, + ensAddress: chain.contracts?.ensRegistry?.address, + }; + if (transport.type === "fallback") + return new providers.FallbackProvider( + (transport.transports as { value: { url: string } }[]).map( + ({ value }) => new providers.JsonRpcProvider(value?.url, network) + ) + ); + return new providers.JsonRpcProvider(transport.url, network); + }, [publicClient]); const { feeData, refetchFeeData } = useFeeData(mantleProvider); diff --git a/apps/bridge/src/hooks/web3/write/useSwitchToNetwork.ts b/apps/bridge/src/hooks/web3/write/useSwitchToNetwork.ts index c7ae7abc..28a75365 100644 --- a/apps/bridge/src/hooks/web3/write/useSwitchToNetwork.ts +++ b/apps/bridge/src/hooks/web3/write/useSwitchToNetwork.ts @@ -1,13 +1,32 @@ "use client"; import { CHAINS } from "@config/constants"; +import { useToast } from "@hooks/useToast"; +import { useEffect, useState } from "react"; + +import { useSwitchNetwork } from "wagmi"; + +declare global { + interface Window { + ethereum: import("ethers").providers.ExternalProvider; + } +} export function useSwitchToNetwork() { + // allow wagmi to attempt to switch networks + const { switchNetwork, error } = useSwitchNetwork(); + + // set the given error into context to control dismissals + const [displayError, setDisplayError] = useState(""); + + // create an error toast if required + const { updateToast } = useToast(); + // trigger addNetwork const addNetwork = async (chainId: number) => { if (!window.ethereum) throw new Error("No crypto wallet found"); // add the woollyhat network to users wallet - await window.ethereum.request({ + await window.ethereum.request?.({ method: "wallet_addEthereumChain", params: [CHAINS[chainId]], }); @@ -15,24 +34,56 @@ export function useSwitchToNetwork() { // trigger change of network const switchToNetwork = async (chainId: number): Promise => { - if (!window.ethereum) throw new Error("No crypto wallet found"); + // perform switch with wagmi try { - await window.ethereum.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: `0x${Number(chainId).toString(16)}` }], - }); + // reset prev error + setDisplayError(""); + // attempt to switch the network + switchNetwork?.(chainId); + // on success return the chainID we moved to return chainId; } catch (switchError: any) { // This error code indicates that the chain has not been added to MetaMask. if (switchError && switchError.code === 4902) { await addNetwork(chainId); } else if (switchError.code !== -32000) { - throw switchError; + updateToast({ + id: "switch-error", + content: "Error: Unable to switch chains", + type: "error", + borderLeft: "red-600", + }); } } return Promise.resolve(); }; + // hydrate error + useEffect(() => { + setDisplayError(error?.toString() || ""); + }, [error]); + + // place toast on error + useEffect( + () => { + if (displayError) { + updateToast({ + id: "switch-error", + content: "Error: Unable to switch chains", + type: "error", + borderLeft: "red-600", + buttonText: "Close", + onButtonClick: () => { + setDisplayError(""); + return true; + }, + }); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [displayError] + ); + return { addNetwork, switchToNetwork, diff --git a/apps/bridge/src/providers/mantleSDKContext.tsx b/apps/bridge/src/providers/mantleSDKContext.tsx index 80e55fd7..389f2a74 100644 --- a/apps/bridge/src/providers/mantleSDKContext.tsx +++ b/apps/bridge/src/providers/mantleSDKContext.tsx @@ -24,21 +24,18 @@ import { hashCrossDomainMessage } from "@mantleio/core-utils"; import { CHAINS_FORMATTED, L1_CHAIN_ID, L2_CHAIN_ID } from "@config/constants"; -import { useSigner, useProvider, useNetwork } from "wagmi"; +import { useNetwork, usePublicClient, useWalletClient } from "wagmi"; import type { - FallbackProvider, Provider, TransactionReceipt, TransactionResponse, } from "@ethersproject/providers"; import { BytesLike, Signer, ethers, providers } from "ethers"; -import ErrorFallback from "@components/ErrorFallback"; -import { withErrorBoundary, useErrorHandler } from "react-error-boundary"; - import { timeout } from "@utils/toolSet"; import { gql, useApolloClient } from "@apollo/client"; +import { useMemo } from "react"; type WaitForMessageStatus = ( message: MessageLike, @@ -127,10 +124,22 @@ function MantleSDKProvider({ children }: MantleSDKProviderProps) { const gqclient = useApolloClient(); // pull all the signers/privders and set handlers and associate boundaries as we go - const { data: layer1Signer, error: layer1SignerError } = useSigner({ - chainId: L1_CHAIN_ID, - }); - useErrorHandler(layer1SignerError); + const walletClient = useWalletClient({ chainId: L1_CHAIN_ID }); + const layer1Signer = useMemo(() => { + if (walletClient.data) { + const { account, chain: l1Chain, transport } = walletClient.data!; + const network = { + chainId: l1Chain?.id, + name: l1Chain?.name, + ensAddress: l1Chain?.contracts?.ensRegistry?.address, + }; + const provider = new providers.Web3Provider(transport, network); + const signer = provider.getSigner(account.address); + return signer; + } + return undefined; + }, [walletClient]); + // get an infura backed provider so we can search through more blocks - this enables the full sdk to work // eslint-disable-next-line react-hooks/exhaustive-deps const layer1Infura = React.useMemo( @@ -150,24 +159,54 @@ function MantleSDKProvider({ children }: MantleSDKProviderProps) { ), [] ); - const { data: mantleTestnetSigner, error: mantleTestnetSignerError } = - useSigner({ - chainId: L2_CHAIN_ID, - }); - useErrorHandler(mantleTestnetSignerError); - const mantleTestnetProvider = useProvider({ - chainId: L2_CHAIN_ID, - }); + + // pull all the signers/privders and set handlers and associate boundaries as we go + const mantleWalletClient = useWalletClient({ chainId: L2_CHAIN_ID }); + const mantleSigner = useMemo(() => { + if (mantleWalletClient?.data) { + const { account, chain: l2Chain, transport } = mantleWalletClient.data!; + const network = { + chainId: l2Chain?.id, + name: l2Chain?.name, + ensAddress: l2Chain?.contracts?.ensRegistry?.address, + }; + const provider = new providers.Web3Provider(transport, network); + const signer = provider.getSigner(account.address); + return signer; + } + return undefined; + }, [mantleWalletClient]); + + // get the provider for the chosen chain + const publicClient = usePublicClient({ chainId: L2_CHAIN_ID }); + + // create an ethers provider from the publicClient + const mantleProvider = useMemo(() => { + const { chain: mantleChain, transport } = publicClient; + const network = { + chainId: mantleChain.id, + name: mantleChain.name, + ensAddress: mantleChain.contracts?.ensRegistry?.address, + }; + if (transport.type === "fallback") + return new providers.FallbackProvider( + (transport.transports as { value: { url: string } }[]).map( + ({ value }) => new providers.JsonRpcProvider(value?.url, network) + ) + ); + return new providers.JsonRpcProvider(transport.url, network); + }, [publicClient]); // construct a crossChainMessenger - this is responsible for nearly all of our web3 interactions const crossChainMessenger = React.useMemo(() => { + // avoid building the manager if we don't have an approproate signer for the currently selected chain (this avoids errors on hw wallets) if ( - layer1Signer === undefined || - layer1Signer === null || - mantleTestnetSigner === undefined || - mantleTestnetSigner === null || + (chain?.id === L1_CHAIN_ID && + (layer1Signer === undefined || layer1Signer === null)) || + (chain?.id === L2_CHAIN_ID && + (mantleSigner === undefined || mantleSigner === null)) || !layer1Provider || - !mantleTestnetProvider + !mantleProvider ) return { crossChainMessenger: undefined }; @@ -177,13 +216,9 @@ function MantleSDKProvider({ children }: MantleSDKProviderProps) { l1ChainId: L1_CHAIN_ID, l2ChainId: L2_CHAIN_ID, l1SignerOrProvider: - chain?.id === L1_CHAIN_ID ? layer1Signer : layer1Provider, + chain?.id === L1_CHAIN_ID ? layer1Signer! : layer1Provider!, l2SignerOrProvider: - chain?.id === L2_CHAIN_ID - ? mantleTestnetSigner - : // RE: https://github.com/ethers-io/ethers.js/discussions/2703 - (mantleTestnetProvider as FallbackProvider).providerConfigs[0] - .provider, + chain?.id === L2_CHAIN_ID ? mantleSigner! : mantleProvider!, }), } as MantleSDK; @@ -794,8 +829,8 @@ function MantleSDKProvider({ children }: MantleSDKProviderProps) { layer1Provider, layer1InfuraRef, layer1ProviderRef, - mantleTestnetSigner, - mantleTestnetProvider, + mantleSigner, + mantleProvider, gqclient, chain, challengePeriod, @@ -812,8 +847,8 @@ function MantleSDKProvider({ children }: MantleSDKProviderProps) { }, [layer1Provider]); React.useEffect(() => { - mantleTestnetRef.current = mantleTestnetProvider; - }, [mantleTestnetProvider]); + mantleTestnetRef.current = mantleProvider; + }, [mantleProvider]); React.useEffect(() => { layer1SignerRef.current = layer1Signer as Signer; @@ -835,17 +870,4 @@ const useMantleSDK = () => { return context; }; -const MantleSDKProviderWithErrorBoundary = withErrorBoundary( - MantleSDKProvider, - { - FallbackComponent: ErrorFallback, - onReset: () => { - window.location.reload(); - }, - } -); - -export { - MantleSDKProviderWithErrorBoundary as MantleSDKProvider, - useMantleSDK, -}; +export { useMantleSDK, MantleSDKProvider }; diff --git a/apps/bridge/src/providers/stateContext.tsx b/apps/bridge/src/providers/stateContext.tsx index 5aa74a65..65e2346b 100644 --- a/apps/bridge/src/providers/stateContext.tsx +++ b/apps/bridge/src/providers/stateContext.tsx @@ -14,9 +14,9 @@ import { Views, } from "@config/constants"; -import { Contract } from "ethers"; +import { Contract, providers } from "ethers"; import { MessageLike } from "@mantleio/sdk"; -import { Network } from "@ethersproject/providers"; +import { BaseProvider, Network } from "@ethersproject/providers"; import { createContext, @@ -26,7 +26,7 @@ import { useRef, useState, } from "react"; -import { useProvider } from "wagmi"; +import { usePublicClient } from "wagmi"; import { usePathname } from "next/navigation"; import { useAccountBalances, @@ -53,11 +53,13 @@ export type StateProps = { view: Views; client: { isConnected: boolean; + connector?: string; chainId?: number; - address?: `0x${string}`; + address?: string; }; safeChains: number[]; chainId: number; + provider: BaseProvider; multicall: MutableRefObject<{ network: Network; multicallContract: Contract; @@ -113,8 +115,9 @@ export type StateProps = { setChainId: (v: number) => void; setClient: (client: { isConnected: boolean; + connector?: string; chainId?: number; - address?: `0x${string}`; + address?: string; }) => void; setSafeChains: (chains: number[]) => void; resetBalances: () => void; @@ -156,19 +159,40 @@ export function StateProvider({ children }: { children: React.ReactNode }) { const [safeChains, setSafeChains] = useState([5]); // get the provider for the chosen chain - const provider = useProvider({ chainId }); + const publicClient = usePublicClient({ chainId }); + + // create an ethers provider from the publicClient + const provider = useMemo(() => { + const { chain, transport } = publicClient; + const network = { + chainId: chain.id, + name: chain.name, + ensAddress: chain.contracts?.ensRegistry?.address, + }; + if (transport.type === "fallback") + return new providers.FallbackProvider( + (transport.transports as { value: { url: string } }[]).map( + ({ value }) => new providers.JsonRpcProvider(value?.url, network) + ) + ); + return new providers.JsonRpcProvider(transport.url, network); + }, [publicClient]); // keep hold of all wallet connection details const [client, setClient] = useState<{ isConnected: boolean; chainId?: number; - address?: `0x${string}`; + address?: string; }>({ isConnected: false, }); // page toggled chainId (set according to Deposit/Withdraw) - const multicall = useRef<{ network: Network; multicallContract: Contract }>(); + const multicall = useRef<{ + network: Network; + multicallContract: Contract; + chainId: number; + }>(); // the selected page within CTAPage to open const [ctaPage, setCTAPage] = useState(CTAPages.Default); @@ -343,13 +367,7 @@ export function StateProvider({ children }: { children: React.ReactNode }) { // perform a multicall on the given network to get all token balances for user const { balances, resetBalances, isFetchingBalances, isRefetchingBalances } = - useAccountBalances( - chainId, - client, - tokens, - multicall, - setIsLoadingBalances - ); + useAccountBalances(chainId, client, tokens, setIsLoadingBalances); // corrent view on page turn useEffect( @@ -369,15 +387,28 @@ export function StateProvider({ children }: { children: React.ReactNode }) { // make sure the multicall contract in the current context is assigned to the current network useEffect(() => { + // save the chainId to indicate we're moving chains + multicall.current = { + chainId, + } as { + network: Network; + multicallContract: Contract; + chainId: number; + }; + // construct a new muticall contract for this chain getMulticallContract(MULTICALL_CONTRACTS[chainId], provider).then( async (multicallContract) => { // check that we're using the corrent network before proceeding const network = await multicallContract.provider.getNetwork(); - // setMulticall(multicallContract); - multicall.current = { - network, - multicallContract, - }; + // make sure we're still on the same chainId + if (multicall.current?.chainId === chainId) { + // setMulticall(multicallContract); + multicall.current = { + chainId, + network, + multicallContract, + }; + } } ); }, [chainId, provider, client]); @@ -550,6 +581,7 @@ export function StateProvider({ children }: { children: React.ReactNode }) { client, chainId, safeChains, + provider, multicall, bridgeAddress, @@ -627,6 +659,7 @@ export function StateProvider({ children }: { children: React.ReactNode }) { client, chainId, safeChains, + provider, multicall, bridgeAddress, diff --git a/apps/bridge/src/providers/wagmiContext.tsx b/apps/bridge/src/providers/wagmiContext.tsx index 2f09fda8..5ee321aa 100644 --- a/apps/bridge/src/providers/wagmiContext.tsx +++ b/apps/bridge/src/providers/wagmiContext.tsx @@ -4,15 +4,16 @@ import { CHAINS_FORMATTED, L1_CHAIN_ID, L2_CHAIN_ID } from "@config/constants"; // Required components for wagmi... -import { WagmiConfig, configureChains, createClient } from "wagmi"; +import { WagmiConfig, configureChains, createConfig } from "wagmi"; import { InjectedConnector } from "wagmi/connectors/injected"; import { MetaMaskConnector } from "wagmi/connectors/metaMask"; +import { WalletConnectConnector } from "wagmi/connectors/walletConnect"; import { jsonRpcProvider } from "wagmi/providers/jsonRpc"; import { publicProvider } from "wagmi/providers/public"; -const { chains, provider, webSocketProvider } = configureChains( +const { chains, publicClient, webSocketPublicClient } = configureChains( // We support L1 and Mantle (depending on state of ui) [CHAINS_FORMATTED[L1_CHAIN_ID], CHAINS_FORMATTED[L2_CHAIN_ID]], [ @@ -22,11 +23,10 @@ const { chains, provider, webSocketProvider } = configureChains( }), }), publicProvider(), - ], - { targetQuorum: 1 } + ] ); -const client = createClient({ +const config = createConfig({ autoConnect: true, connectors: [ new MetaMaskConnector({ @@ -47,9 +47,15 @@ const client = createClient({ shimDisconnect: true, }, }), + new WalletConnectConnector({ + chains, + options: { + projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_ID || "", + }, + }), ], - provider, - webSocketProvider, + publicClient, + webSocketPublicClient, }); interface WagmiProviderProps { @@ -57,7 +63,7 @@ interface WagmiProviderProps { } function WagmiProvider({ children }: WagmiProviderProps) { - return {children}; + return {children}; } export { WagmiProvider }; diff --git a/apps/bridge/src/utils/formatStrings.ts b/apps/bridge/src/utils/formatStrings.ts index cec8c36c..317945fe 100644 --- a/apps/bridge/src/utils/formatStrings.ts +++ b/apps/bridge/src/utils/formatStrings.ts @@ -1,7 +1,5 @@ "use client"; -import Decimal from "decimal.js"; - export const localeZero = new Intl.NumberFormat( globalThis.navigator?.language || "en", { @@ -9,52 +7,6 @@ export const localeZero = new Intl.NumberFormat( } ).format(Number("0.0")); -export const formatBigNumberString = ( - str: string, - precision = 6, - groupingInt = true, - groupingFrac = true, - locale = globalThis.navigator?.language || "en" -) => { - const integerPart = BigInt(str.split(".")[0] || 0); - const fractionPart = new Decimal( - `0.${str?.split(".")?.[1]?.slice(0, precision) || 0}` - ) - .toFixed(precision) - .toString() - .replace(/^0|0+$/g, "") - .replace(/^([^0-9])$/, "$10"); - - // format according to the given locale - const mainFormatInt = new Intl.NumberFormat(locale, { - minimumFractionDigits: 0, - useGrouping: groupingInt, - }); - // format according to the given locale - const mainFormatFrac = new Intl.NumberFormat(locale, { - minimumFractionDigits: 0, - useGrouping: groupingFrac, - }); - - // construct a template to pull locales decimal seperator - const template = mainFormatInt.format(1.1); - - // reconnect the number parts and place templates seperator - return `${mainFormatInt.format(integerPart)}${template[1]}${mainFormatFrac - .format( - (fractionPart.length === 1 ? `${fractionPart}0` : fractionPart) - .split("") - .reverse() - .join("") as unknown as number - ) - .split("") - .reverse() - .join("")}`; -}; - -export const truncateAddress = (address: `0x${string}`) => - `${address.slice(0, 6)}...${address.slice(-4)}`; - // format a given time into a nice representation export const formatTime = ( seconds: number, diff --git a/apps/bridge/src/utils/multicallContract.ts b/apps/bridge/src/utils/multicallContract.ts index 19e4e6c2..68ae92a4 100644 --- a/apps/bridge/src/utils/multicallContract.ts +++ b/apps/bridge/src/utils/multicallContract.ts @@ -30,7 +30,7 @@ export const MULTICALL_ABI = [ * @returns ethers.Contract multicall contract */ export async function getMulticallContract( - address: `0x${string}`, + address: string, provider: Provider ) { const multicall = new Contract(address, MULTICALL_ABI, provider); diff --git a/apps/bridge/vercel.json b/apps/bridge/vercel.json index 568430dd..032ed7e1 100644 --- a/apps/bridge/vercel.json +++ b/apps/bridge/vercel.json @@ -3,6 +3,10 @@ { "path": "/graphql/sync", "schedule": "* * * * *" + }, + { + "path": "/graphql/sync/gas", + "schedule": "*/10 * * * *" } ] } diff --git a/apps/converter/package.json b/apps/converter/package.json index 473e2467..aaeefb6c 100644 --- a/apps/converter/package.json +++ b/apps/converter/package.json @@ -19,8 +19,8 @@ "@mantle/constants": "workspace:*", "@mantle/supagraph": "workspace:*", "@mantle/ui": "workspace:*", + "@mantle/utils": "workspace:*", "@tanstack/react-query": "^4.14.5", - "decimal.js": "^10.4.3", "ethers": "^5.7.2", "framer-motion": "^10.12.10", "mongodb": "^5.6.0", @@ -28,7 +28,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.8.0", - "wagmi": "^0.11.6", + "wagmi": "^1.3.9", "zustand": "^4.0.0-rc.1" }, "devDependencies": { @@ -43,6 +43,6 @@ "eslint": "^8.30.0", "postcss": "^8.4.18", "tailwindcss": "^3.2.1", - "typescript": "4.9.4" + "typescript": "5.0.4" } } diff --git a/apps/converter/src/app/graphql/sync/route.ts b/apps/converter/src/app/graphql/sync/route.ts index 48b35e3f..cd4c9edc 100644 --- a/apps/converter/src/app/graphql/sync/route.ts +++ b/apps/converter/src/app/graphql/sync/route.ts @@ -24,6 +24,9 @@ import { handlers } from "@supagraph/handlers"; // Import revalidation timings from config import config from "@supagraph/config"; +// forces the route handler to be dynamic +export const dynamic = "force-dynamic"; + // Object of providers by rpcUrl const providerCache: { [rpcUrl: string]: providers.JsonRpcProvider } = {}; diff --git a/apps/converter/src/app/head.tsx b/apps/converter/src/app/head.tsx index b0395a69..1ffdc01b 100644 --- a/apps/converter/src/app/head.tsx +++ b/apps/converter/src/app/head.tsx @@ -27,7 +27,7 @@ export default function Head() { - + ); } diff --git a/apps/converter/src/app/layout.tsx b/apps/converter/src/app/layout.tsx index 62b34784..95c5a6ad 100644 --- a/apps/converter/src/app/layout.tsx +++ b/apps/converter/src/app/layout.tsx @@ -2,8 +2,7 @@ import "./styles/globals.css"; // Dummy components import { - GTWalsheimRegular, - GTWalsheimMedium, + GTWalsheim, PageWrapper, PageBackroundImage, PageContainer, @@ -25,12 +24,9 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + - + {/* @ts-expect-error Server Component */} } - header={