From ca344eb64eac543a9f5a5455b12d311d04e9a94b Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:21:01 +0100 Subject: [PATCH] fix: optimal rpc for writes, no more infinite loading --- .knip.jsonc | 2 +- package.json | 2 +- .../read-claim-data-from-url.ts | 35 +++++----- static/scripts/rewards/toaster.ts | 10 +-- static/scripts/rewards/web3/connect-wallet.ts | 68 +++++++++++++++---- static/scripts/rewards/web3/erc20-permit.ts | 37 ++++++---- static/scripts/rewards/web3/erc721-permit.ts | 2 +- yarn.lock | 7 +- 8 files changed, 112 insertions(+), 51 deletions(-) diff --git a/.knip.jsonc b/.knip.jsonc index 3b28c2af..27f07c24 100644 --- a/.knip.jsonc +++ b/.knip.jsonc @@ -1,5 +1,5 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "entry": ["static/scripts/rewards/init.ts"], - "ignore": ["lib/**"] + "ignore": ["lib/**"], } diff --git a/package.json b/package.json index c1981063..42d8a412 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@uniswap/permit2-sdk": "^1.2.0", "axios": "^1.6.7", "dotenv": "^16.4.4", - "ethers": "^5.7.2", + "ethers": "5.7.2", "npm-run-all": "^4.1.5" }, "devDependencies": { diff --git a/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts index 95927e08..cd22a4eb 100644 --- a/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts +++ b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts @@ -3,10 +3,9 @@ import { Value } from "@sinclair/typebox/value"; import { createClient } from "@supabase/supabase-js"; import { AppState, app } from "../app-state"; import { useFastestRpc } from "../rpc-optimization/get-optimal-provider"; -import { buttonController, toaster } from "../toaster"; +import { toaster } from "../toaster"; import { connectWallet } from "../web3/connect-wallet"; import { checkRenderInvalidatePermitAdminControl, checkRenderMakeClaimControl } from "../web3/erc20-permit"; -import { verifyCurrentNetwork } from "../web3/verify-current-network"; import { claimRewardsPagination } from "./claim-rewards-pagination"; import { renderTransaction } from "./render-transaction"; import { setClaimMessage } from "./set-claim-message"; @@ -31,34 +30,36 @@ export async function readClaimDataFromUrl(app: AppState) { app.claims = decodeClaimData(base64encodedTxData).flat(); app.claimTxs = await getClaimedTxs(app); + try { app.provider = await useFastestRpc(app); } catch (e) { toaster.create("error", `${e}`); } - if (window.ethereum) { - try { - app.signer = await connectWallet(); - } catch (error) { - /* empty */ - } - window.ethereum.on("accountsChanged", () => { + + try { + app.signer = await connectWallet(app); + } catch (error) { + /* empty */ + } + + try { + // this would throw on mobile browsers & non-web3 browsers + window?.ethereum.on("accountsChanged", () => { checkRenderMakeClaimControl(app).catch(console.error); checkRenderInvalidatePermitAdminControl(app).catch(console.error); }); - } else { - buttonController.hideAll(); - toaster.create("info", "Please use a web3 enabled browser to collect this reward."); + } catch (err) { + /* + * handled feedback upstream already + * buttons are hidden and non-web3 infinite toast exists + */ } + displayRewardDetails(); displayRewardPagination(); await renderTransaction(); - if (app.networkId !== null) { - await verifyCurrentNetwork(app.networkId); - } else { - throw new Error("Network ID is null"); - } } async function getClaimedTxs(app: AppState): Promise> { diff --git a/static/scripts/rewards/toaster.ts b/static/scripts/rewards/toaster.ts index f5ed7b60..3e455593 100644 --- a/static/scripts/rewards/toaster.ts +++ b/static/scripts/rewards/toaster.ts @@ -19,10 +19,10 @@ export const viewClaimButton = document.getElementById("view-claim") as HTMLButt const notifications = document.querySelector(".notifications") as HTMLUListElement; export const buttonController = new ButtonController(controls); -function createToast(meaning: keyof typeof toaster.icons, text: string) { +function createToast(meaning: keyof typeof toaster.icons, text: string, timeout: number = 5000) { if (meaning != "info") buttonController.hideLoader(); const toastDetails = { - timer: 5000, + timer: timeout, } as { timer: number; timeoutId?: NodeJS.Timeout; @@ -43,8 +43,10 @@ function createToast(meaning: keyof typeof toaster.icons, text: string) { notifications.appendChild(toastContent); // Append the toast to the notification ul - // Setting a timeout to remove the toast after the specified duration - toastDetails.timeoutId = setTimeout(() => removeToast(toastContent, toastDetails.timeoutId), toastDetails.timer); + if (timeout !== Infinity) { + // Setting a timeout to remove the toast after the specified duration + toastDetails.timeoutId = setTimeout(() => removeToast(toastContent, toastDetails.timeoutId), toastDetails.timer); + } } function removeToast(toast: HTMLElement, timeoutId?: NodeJS.Timeout) { diff --git a/static/scripts/rewards/web3/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts index 81a29cb7..d94ccdf8 100644 --- a/static/scripts/rewards/web3/connect-wallet.ts +++ b/static/scripts/rewards/web3/connect-wallet.ts @@ -1,33 +1,77 @@ import { JsonRpcSigner } from "@ethersproject/providers"; import { ethers } from "ethers"; import { buttonController, toaster } from "../toaster"; +import { useFastestRpc } from "../rpc-optimization/get-optimal-provider"; +import { AppState } from "../app-state"; +import { verifyCurrentNetwork } from "./verify-current-network"; -export async function connectWallet(): Promise { +export async function connectWallet(app: AppState): Promise { try { + // take the wallet provider from the window const wallet = new ethers.providers.Web3Provider(window.ethereum); + // the rewards are populated before the provider is so this is safe + const rewardNetworkId = app.reward.networkId; + + // verify we are on the correct network immediately + await verifyCurrentNetwork(rewardNetworkId); + + // get account access from the wallet provider i.e metamask await wallet.send("eth_requestAccounts", []); - const signer = wallet.getSigner(); + // get our optimal rpc provider + const rpc = await useFastestRpc(app); + + // take the signer from the wallet + const walletSigner = wallet.getSigner(); + + // take the address of the signer + const walletAddress = await walletSigner.getAddress(); + + // use the rpc and connect the signer to an UncheckedRpcSigner instance + const rpcSigner = rpc.getUncheckedSigner(walletAddress) as JsonRpcSigner; - const address = await signer.getAddress(); + // get the address of the signer + const newJsonSignerAddress = await rpcSigner.getAddress(); - if (!address) { + // if we have window.eth then we should have an address + if (walletAddress == "" || newJsonSignerAddress == "" || !walletAddress || !newJsonSignerAddress) { + toaster.create("info", "Please connect your wallet to collect this reward."); buttonController.hideAll(); console.error("Wallet not connected"); return null; } - return signer; + if (walletAddress !== newJsonSignerAddress) { + // although this should never happen just use the wallet provider if it does + return walletSigner; + } else { + return rpcSigner; + } } catch (error: unknown) { - if (error instanceof Error) { - console.error(error); - if (error?.message?.includes("missing provider")) { - toaster.create("info", "Please use a web3 enabled browser to collect this reward."); - } else { - toaster.create("info", "Please connect your wallet to collect this reward."); + connectErrorHandler(error); + } + return null; +} + +function connectErrorHandler(error: unknown) { + if (error instanceof Error) { + console.error(error); + if (error?.message?.includes("missing provider")) { + // mobile browsers don't really support window.ethereum + const mediaQuery = window.matchMedia("(max-width: 768px)"); + + if (mediaQuery.matches) { + // "Please use a mobile-friendly Web3 or desktop browser to collect this reward." push to desktop if possible? + toaster.create("warning", "Please use a mobile-friendly Web3 browser such as MetaMask to collect this reward", Infinity); + } else if (!window.ethereum) { + toaster.create("warning", "Please use a web3 enabled browser to collect this reward.", Infinity); + buttonController.hideAll(); } + } else { + toaster.create("error", error.message); } - return null; + } else { + toaster.create("error", "An unknown error occurred."); } } diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index 858694ac..495156d3 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -1,11 +1,12 @@ -import { JsonRpcSigner, TransactionResponse } from "@ethersproject/providers"; -import { BigNumber, BigNumberish, Contract, ethers } from "ethers"; +import { TransactionResponse } from "@ethersproject/providers"; +import { BigNumber, BigNumberish, Contract, Signer, ethers } from "ethers"; import { erc20Abi, permit2Abi } from "../abis"; import { AppState, app } from "../app-state"; import { permit2Address } from "../constants"; import { supabase } from "../render-transaction/read-claim-data-from-url"; import { Erc20Permit, Erc721Permit } from "../render-transaction/tx-type"; import { MetaMaskError, buttonController, errorToast, getMakeClaimButton, toaster } from "../toaster"; +import { connectWallet } from "./connect-wallet"; export async function fetchTreasury( permit: Erc20Permit | Erc721Permit @@ -60,6 +61,9 @@ async function checkPermitClaimability(app: AppState): Promise { async function transferFromPermit(permit2Contract: Contract, app: AppState) { const reward = app.reward; + const signer = app.signer; + if (!signer) return null; + try { const tx = await permit2Contract.permitTransferFrom(reward.permit, reward.transferDetails, reward.owner, reward.signature); toaster.create("info", `Transaction sent`); @@ -91,6 +95,7 @@ async function waitForTransaction(tx: TransactionResponse) { buttonController.hideLoader(); buttonController.hideMakeClaim(); console.log(receipt.transactionHash); + return receipt; } catch (error: unknown) { if (error instanceof Error) { @@ -103,21 +108,23 @@ async function waitForTransaction(tx: TransactionResponse) { export function claimErc20PermitHandlerWrapper(app: AppState) { return async function claimErc20PermitHandler() { + const signer = await connectWallet(app); + if (!signer) { + return; + } + buttonController.hideMakeClaim(); buttonController.showLoader(); const isPermitClaimable = await checkPermitClaimability(app); if (!isPermitClaimable) return; - const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, app.signer); + const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); if (!permit2Contract) return; const tx = await transferFromPermit(permit2Contract, app); if (!tx) return; - // buttonController.showLoader(); - // buttonController.hideMakeClaim(); - const receipt = await waitForTransaction(tx); if (!receipt) return; @@ -168,9 +175,10 @@ async function checkPermitClaimable(app: AppState): Promise { return false; } - let user: string; + let user: string | undefined; try { - user = (await app.signer.getAddress()).toLowerCase(); + const address = await app.signer?.getAddress(); + user = address?.toLowerCase(); } catch (error: unknown) { console.error("Error in signer.getAddress: ", error); return false; @@ -188,8 +196,8 @@ async function checkPermitClaimable(app: AppState): Promise { export async function checkRenderMakeClaimControl(app: AppState) { try { - const address = await app.signer.getAddress(); - const user = address.toLowerCase(); + const address = await app.signer?.getAddress(); + const user = address?.toLowerCase(); if (app.reward) { const beneficiary = app.reward.transferDetails.to.toLowerCase(); @@ -207,8 +215,8 @@ export async function checkRenderMakeClaimControl(app: AppState) { export async function checkRenderInvalidatePermitAdminControl(app: AppState) { try { - const address = await app.signer.getAddress(); - const user = address.toLowerCase(); + const address = await app.signer?.getAddress(); + const user = address?.toLowerCase(); if (app.reward) { const owner = app.reward.owner.toLowerCase(); @@ -266,12 +274,13 @@ async function isNonceClaimed(app: AppState): Promise { return bit.and(flipped).eq(0); } -async function invalidateNonce(signer: JsonRpcSigner, nonce: BigNumberish): Promise { +async function invalidateNonce(signer: Signer | null, nonce: BigNumberish): Promise { + if (!signer) return; const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); const { wordPos, bitPos } = nonceBitmap(nonce); // mimics https://github.com/ubiquity/pay.ubq.fi/blob/c9e7ed90718fe977fd9f348db27adf31d91d07fb/scripts/solidity/test/Permit2.t.sol#L428 const bit = BigNumber.from(1).shl(bitPos); - const sourceBitmap = await permit2Contract.nonceBitmap(await signer.getAddress(), wordPos.toString()); + const sourceBitmap = await permit2Contract.nonceBitmap(await signer?.getAddress(), wordPos.toString()); const mask = sourceBitmap.or(bit); await permit2Contract.invalidateUnorderedNonces(wordPos, mask); } diff --git a/static/scripts/rewards/web3/erc721-permit.ts b/static/scripts/rewards/web3/erc721-permit.ts index a6bf6e49..c94d646e 100644 --- a/static/scripts/rewards/web3/erc721-permit.ts +++ b/static/scripts/rewards/web3/erc721-permit.ts @@ -8,7 +8,7 @@ import { connectWallet } from "./connect-wallet"; export function claimErc721PermitHandler(reward: Erc721Permit) { return async function claimHandler() { - const signer = await connectWallet(); + const signer = await connectWallet(app); if (!signer) { return; } diff --git a/yarn.lock b/yarn.lock index d94f49e1..6ce10e36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2700,7 +2700,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -ethers@^5.3.1, ethers@^5.7.2: +ethers@5.7.2, ethers@^5.3.1: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -3974,6 +3974,11 @@ minimist@1.2.8, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +mipd@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mipd/-/mipd-0.0.7.tgz#bb5559e21fa18dc3d9fe1c08902ef14b7ce32fd9" + integrity sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"