From 8a09cec1d08a29e3f98449380cbe05d085094a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Wed, 21 Feb 2024 23:18:16 +0900 Subject: [PATCH 01/14] fix: merge bugs and working on stability --- .cspell.json | 1 + build/esbuild-build.ts | 7 +- package.json | 3 + static/scripts/rewards/constants.ts | 1 - static/scripts/rewards/helpers.ts | 59 +++++++++++--- yarn.lock | 115 ++++++++++++++++++++++++++-- 6 files changed, 164 insertions(+), 22 deletions(-) diff --git a/.cspell.json b/.cspell.json index 09825ad7..628308ff 100644 --- a/.cspell.json +++ b/.cspell.json @@ -27,6 +27,7 @@ "servedir", "solmate", "sonarjs", + "SUPABASE", "typebox", "TYPEHASH", "ubiquibot", diff --git a/build/esbuild-build.ts b/build/esbuild-build.ts index 30d024c6..27829338 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -1,6 +1,6 @@ -import extraRpcs from "../lib/chainlist/constants/extraRpcs"; -import esbuild from "esbuild"; import * as dotenv from "dotenv"; +import esbuild from "esbuild"; +import extraRpcs from "../lib/chainlist/constants/extraRpcs"; const typescriptEntries = [ "static/scripts/rewards/index.ts", "static/scripts/audit-report/audit.ts", @@ -33,7 +33,7 @@ export const esBuildContext: esbuild.BuildOptions = { ".svg": "dataurl", }, outdir: "static/out", - define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { allNetworkUrls }), + define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { extraRpcs: allNetworkUrls }), }; esbuild @@ -62,6 +62,5 @@ function createEnvDefines(envVarNames: string[], extras: Record defines[key] = JSON.stringify(extras[key]); } } - defines["extraRpcs"] = JSON.stringify(extraRpcs); return defines; } diff --git a/package.json b/package.json index fb45a918..9fa60af7 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ }, "scripts": { "start": "run-s utils:hash start:sign start:ui", + "watch": "nodemon -e ts,tsx --exec yarn start", + "watch:ui": "nodemon -e ts,tsx --exec yarn start:ui", "format": "run-s format:lint format:prettier format:cspell", "build": "run-s utils:hash utils:build", "start:ui": "tsx build/esbuild-server.ts", @@ -68,6 +70,7 @@ "husky": "^9.0.11", "knip": "^5.0.1", "lint-staged": "^15.2.2", + "nodemon": "^3.0.3", "npm-run-all": "^4.1.5", "prettier": "^3.2.5", "tsx": "^4.7.1", diff --git a/static/scripts/rewards/constants.ts b/static/scripts/rewards/constants.ts index 2794e84b..ec0bf5a7 100644 --- a/static/scripts/rewards/constants.ts +++ b/static/scripts/rewards/constants.ts @@ -9,7 +9,6 @@ export enum NetworkIds { Goerli = 5, Gnosis = 100, } -console.trace({ extraRpcs }); export enum Tokens { DAI = "0x6b175474e89094c44da98b954eedeac495271d0f", diff --git a/static/scripts/rewards/helpers.ts b/static/scripts/rewards/helpers.ts index a75d2fb8..78d732b9 100644 --- a/static/scripts/rewards/helpers.ts +++ b/static/scripts/rewards/helpers.ts @@ -1,7 +1,7 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; import axios from "axios"; import { Contract, ethers } from "ethers"; import { erc20Abi } from "./abis"; -import { JsonRpcProvider } from "@ethersproject/providers"; import { networkRpcs } from "./constants"; type DataType = { @@ -39,7 +39,9 @@ export async function getErc20Contract(contractAddress: string, provider: JsonRp return new ethers.Contract(contractAddress, erc20Abi, provider); } -export async function getOptimalProvider(networkId: number) { +export async function testRpcPerformance(networkId: number) { + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + const promises = networkRpcs[networkId].map(async (baseURL: string) => { try { const startTime = performance.now(); @@ -52,22 +54,61 @@ export async function getOptimalProvider(networkId: number) { const endTime = performance.now(); const latency = endTime - startTime; if (verifyBlock(data)) { - return Promise.resolve({ - latency, - baseURL, - }); + // Save the latency in localStorage + latencies[baseURL] = latency; } else { - return Promise.reject(); + // Save -1 in localStorage to indicate an error + latencies[baseURL] = -1; } } catch (error) { - return Promise.reject(); + // Save -1 in localStorage to indicate an error + latencies[baseURL] = -1; } }); - const { baseURL: optimalRPC } = await Promise.any(promises); + await Promise.all(promises); + localStorage.setItem("rpcLatencies", JSON.stringify(latencies)); +} + +export function getFastestRpcProvider(networkId: number) { + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + + // Get all latencies from localStorage and find the fastest RPC + const sortedLatencies = Object.entries(latencies).sort((a, b) => a[1] - b[1]); + const optimalRPC = sortedLatencies[0][0]; + return new ethers.providers.JsonRpcProvider(optimalRPC, { name: optimalRPC, chainId: networkId, ensAddress: "", }); } + +let isTestCompleted = false; + +export async function getOptimalProvider(networkId: number): Promise { + // If the test is already completed for this session, return the fastest RPC provider + if (isTestCompleted) { + return getFastestRpcProvider(networkId); + } + + // If the test is not completed yet, check if there are any latencies stored in the localStorage + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + if (Object.keys(latencies).length > 0) { + // If there are latencies stored in the localStorage, use the previous best RPC + const provider = getFastestRpcProvider(networkId); + // Start the test in the background + testRpcPerformance(networkId) + .then(() => { + isTestCompleted = true; + }) + .catch(console.error); + return provider; + } else { + // If it's the user's first time and there are no latencies stored in the localStorage, + // wait for the test to finish and then return the fastest RPC provider + await testRpcPerformance(networkId); + isTestCompleted = true; + return getFastestRpcProvider(networkId); + } +} diff --git a/yarn.lock b/yarn.lock index a1c70a2d..a00ad35c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1752,6 +1752,11 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1831,6 +1836,14 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1928,6 +1941,11 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -1966,7 +1984,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2042,6 +2060,21 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2395,7 +2428,7 @@ data-uri-to-buffer@^3.0.1: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== -debug@4.3.4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4.3.4, debug@^4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -3045,7 +3078,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.3: +fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3136,7 +3169,7 @@ git-raw-commits@^2.0.11: split2 "^3.0.0" through2 "^4.0.0" -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -3352,6 +3385,11 @@ identity-function@^1.0.0: resolved "https://registry.yarnpkg.com/identity-function/-/identity-function-1.0.0.tgz#bea1159f0985239be3ca348edf40ce2f0dd2c21d" integrity sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + ignore@^5.1.8, ignore@^5.2.0, ignore@^5.2.4: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -3438,6 +3476,13 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -3487,7 +3532,7 @@ is-fullwidth-code-point@^5.0.0: dependencies: get-east-asian-width "^1.0.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -4128,6 +4173,29 @@ node-fetch@3.0.0-beta.9: data-uri-to-buffer "^3.0.1" fetch-blob "^2.1.1" +nodemon@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.0.3.tgz#244a62d1c690eece3f6165c6cdb0db03ebd80b76" + integrity sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== + dependencies: + abbrev "1" + normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -4158,7 +4226,7 @@ normalize-package-data@^6.0.0: semver "^7.3.5" validate-npm-package-license "^3.0.4" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -4451,7 +4519,7 @@ picomatch@4.0.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.1.tgz#68c26c8837399e5819edce48590412ea07f17a07" integrity sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg== -picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -4518,6 +4586,11 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -4583,6 +4656,13 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -4826,6 +4906,13 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -5033,7 +5120,7 @@ summary@2.1.0: resolved "https://registry.yarnpkg.com/summary/-/summary-2.1.0.tgz#be8a49a0aa34eb6ceea56042cae88f8add4b0885" integrity sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw== -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -5113,6 +5200,13 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -5241,6 +5335,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" From faf721969e387f26b5cca3e39ce58e6afba107a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 00:22:41 +0900 Subject: [PATCH 02/14] chore: commit hash now in esbuild process --- build/esbuild-build.ts | 7 ++++++- package.json | 7 +++---- static/scripts/rewards/index.ts | 24 +++++++++--------------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/build/esbuild-build.ts b/build/esbuild-build.ts index 27829338..9b1665b0 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -1,6 +1,8 @@ import * as dotenv from "dotenv"; import esbuild from "esbuild"; import extraRpcs from "../lib/chainlist/constants/extraRpcs"; +import { execSync } from "child_process"; + const typescriptEntries = [ "static/scripts/rewards/index.ts", "static/scripts/audit-report/audit.ts", @@ -33,7 +35,10 @@ export const esBuildContext: esbuild.BuildOptions = { ".svg": "dataurl", }, outdir: "static/out", - define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { extraRpcs: allNetworkUrls }), + define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { + extraRpcs: allNetworkUrls, + commitHash: execSync(`git rev-parse --short HEAD`).toString().trim(), + }), }; esbuild diff --git a/package.json b/package.json index 9fa60af7..affebbce 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,14 @@ "node": ">=20.10.0" }, "scripts": { - "start": "run-s utils:hash start:sign start:ui", + "start": "run-s start:sign start:ui", "watch": "nodemon -e ts,tsx --exec yarn start", "watch:ui": "nodemon -e ts,tsx --exec yarn start:ui", "format": "run-s format:lint format:prettier format:cspell", - "build": "run-s utils:hash utils:build", + "build": "run-s utils:build", "start:ui": "tsx build/esbuild-server.ts", "start:sign": "tsx scripts/typescript/generate-permit2-url.ts", "utils:build": "tsx build/esbuild-build.ts", - "utils:hash": "git rev-parse --short HEAD > static/commit.txt", "utils:get-invalidate-params": "forge script --via-ir scripts/solidity/GetInvalidateNonceParams.s.sol", "format:lint": "eslint --fix .", "format:prettier": "prettier --write .", @@ -91,4 +90,4 @@ "@commitlint/config-conventional" ] } -} +} \ No newline at end of file diff --git a/static/scripts/rewards/index.ts b/static/scripts/rewards/index.ts index 9aa754c4..4f7731e7 100644 --- a/static/scripts/rewards/index.ts +++ b/static/scripts/rewards/index.ts @@ -1,20 +1,14 @@ import { init } from "./render-transaction/render-transaction"; import { grid } from "./the-grid"; +declare const commitHash: string; // @DEV: passed in at build time check build/esbuild-build.ts + (async function appAsyncWrapper() { - try { - // display commit hash - const commit = await fetch("commit.txt"); - if (commit.ok) { - const commitHash = await commit.text(); - const buildElement = document.querySelector(`#build a`) as HTMLAnchorElement; - buildElement.innerHTML = commitHash; - buildElement.href = `https://github.com/ubiquity/pay.ubq.fi/commit/${commitHash}`; - } - init().catch(console.error); - } catch (error) { - console.error(error); - } -})().catch(console.error); + // display commit hash + const buildElement = document.querySelector(`#build a`) as HTMLAnchorElement; + buildElement.innerHTML = commitHash; + buildElement.href = `https://github.com/ubiquity/pay.ubq.fi/commit/${commitHash}`; -grid(document.getElementById("grid") as HTMLElement); + init().catch(console.error); + grid(document.getElementById("grid") as HTMLElement); +})().catch(console.error); From 80ea9eaaed57de9c3fe0b44eb59a93b55e757a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 02:01:11 +0900 Subject: [PATCH 03/14] refactor: rpc only checks and selects once at session start --- .eslintrc | 42 ++--- build/esbuild-build.ts | 14 +- static/index.html | 2 +- static/scripts/rewards/app-state.ts | 62 +++++++ static/scripts/rewards/helpers.ts | 114 ------------ static/scripts/rewards/index.ts | 14 -- static/scripts/rewards/init.ts | 15 ++ .../claimRewardsPagination.ts | 34 ++++ .../rewards/render-transaction/index.ts | 42 ----- .../render-transaction/insert-table-data.ts | 2 +- .../read-claim-data-from-url.ts | 65 +++++++ .../render-transaction/render-ens-name.ts | 2 +- .../render-transaction/render-token-symbol.ts | 6 +- .../render-transaction/render-transaction.ts | 170 ------------------ .../render-transaction/renderTransaction.ts | 80 +++++++++ .../render-transaction/setPagination.ts | 12 ++ .../rpc-optimization/getErc20Contract.ts | 7 + .../rpc-optimization/getFastestRpcProvider.ts | 18 ++ .../rpc-optimization/getOptimalProvider.ts | 28 +++ .../rpc-optimization/testRpcPerformance.ts | 64 +++++++ static/scripts/rewards/web3/erc20-permit.ts | 29 +-- static/scripts/rewards/web3/erc721-permit.ts | 9 +- static/scripts/rewards/web3/wallet.ts | 7 +- tsconfig.json | 2 +- 24 files changed, 445 insertions(+), 395 deletions(-) create mode 100644 static/scripts/rewards/app-state.ts delete mode 100644 static/scripts/rewards/helpers.ts delete mode 100644 static/scripts/rewards/index.ts create mode 100644 static/scripts/rewards/init.ts create mode 100644 static/scripts/rewards/render-transaction/claimRewardsPagination.ts delete mode 100644 static/scripts/rewards/render-transaction/index.ts create mode 100644 static/scripts/rewards/render-transaction/read-claim-data-from-url.ts delete mode 100644 static/scripts/rewards/render-transaction/render-transaction.ts create mode 100644 static/scripts/rewards/render-transaction/renderTransaction.ts create mode 100644 static/scripts/rewards/render-transaction/setPagination.ts create mode 100644 static/scripts/rewards/rpc-optimization/getErc20Contract.ts create mode 100644 static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts create mode 100644 static/scripts/rewards/rpc-optimization/getOptimalProvider.ts create mode 100644 static/scripts/rewards/rpc-optimization/testRpcPerformance.ts diff --git a/.eslintrc b/.eslintrc index 22340c8a..5236ba3d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,7 @@ "root": true, "parser": "@typescript-eslint/parser", "parserOptions": { - "project": ["./tsconfig.json"] + "project": ["./tsconfig.json"], }, "plugins": ["@typescript-eslint", "sonarjs"], "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:sonarjs/recommended"], @@ -11,15 +11,15 @@ "prefer-arrow-callback": [ "warn", { - "allowNamedFunctions": true - } + "allowNamedFunctions": true, + }, ], "func-style": [ "warn", "declaration", { - "allowArrowFunctions": false - } + "allowArrowFunctions": false, + }, ], "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-non-null-assertion": "error", @@ -35,8 +35,8 @@ "ignoreRestSiblings": true, "vars": "all", "varsIgnorePattern": "^_", - "argsIgnorePattern": "^_" - } + "argsIgnorePattern": "^_", + }, ], "@typescript-eslint/await-thenable": "error", "@typescript-eslint/no-misused-new": "error", @@ -54,55 +54,55 @@ "format": ["PascalCase"], "custom": { "regex": "^I[A-Z]", - "match": false - } + "match": false, + }, }, { "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], - "leadingUnderscore": "require" + "leadingUnderscore": "require", }, { "selector": "typeLike", - "format": ["PascalCase"] + "format": ["PascalCase"], }, { "selector": "typeParameter", "format": ["PascalCase"], - "prefix": ["T"] + "prefix": ["T"], }, { "selector": "variable", "format": ["camelCase", "UPPER_CASE"], "leadingUnderscore": "allow", - "trailingUnderscore": "allow" + "trailingUnderscore": "allow", }, { "selector": "variable", "format": ["camelCase"], "leadingUnderscore": "allow", - "trailingUnderscore": "allow" + "trailingUnderscore": "allow", }, { "selector": "variable", "modifiers": ["destructured"], - "format": null + "format": null, }, { "selector": "variable", "types": ["boolean"], "format": ["PascalCase"], - "prefix": ["is", "should", "has", "can", "did", "will", "does"] + "prefix": ["is", "should", "has", "can", "did", "will", "does"], }, { "selector": "variableLike", - "format": ["camelCase"] + "format": ["camelCase"], }, { "selector": ["function", "variable"], - "format": ["camelCase"] - } - ] - } + "format": ["camelCase"], + }, + ], + }, } diff --git a/build/esbuild-build.ts b/build/esbuild-build.ts index 9b1665b0..ceba3423 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -1,10 +1,10 @@ +import { execSync } from "child_process"; import * as dotenv from "dotenv"; import esbuild from "esbuild"; import extraRpcs from "../lib/chainlist/constants/extraRpcs"; -import { execSync } from "child_process"; const typescriptEntries = [ - "static/scripts/rewards/index.ts", + "static/scripts/rewards/init.ts", "static/scripts/audit-report/audit.ts", "static/scripts/onboarding/onboarding.ts", "static/scripts/key-generator/keygen.ts", @@ -51,10 +51,10 @@ esbuild process.exit(1); }); -function createEnvDefines(envVarNames: string[], extras: Record): Record { +function createEnvDefines(environmentVariables: string[], generatedAtBuild: Record): Record { const defines: Record = {}; dotenv.config(); - for (const name of envVarNames) { + for (const name of environmentVariables) { const envVar = process.env[name]; if (envVar !== undefined) { defines[name] = JSON.stringify(envVar); @@ -62,9 +62,9 @@ function createEnvDefines(envVarNames: string[], extras: Record throw new Error(`Missing environment variable: ${name}`); } } - for (const key in extras) { - if (Object.prototype.hasOwnProperty.call(extras, key)) { - defines[key] = JSON.stringify(extras[key]); + for (const key in generatedAtBuild) { + if (Object.prototype.hasOwnProperty.call(generatedAtBuild, key)) { + defines[key] = JSON.stringify(generatedAtBuild[key]); } } return defines; diff --git a/static/index.html b/static/index.html index d839f159..4209cb33 100644 --- a/static/index.html +++ b/static/index.html @@ -178,6 +178,6 @@
    - + diff --git a/static/scripts/rewards/app-state.ts b/static/scripts/rewards/app-state.ts new file mode 100644 index 00000000..3519ae8e --- /dev/null +++ b/static/scripts/rewards/app-state.ts @@ -0,0 +1,62 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { networkExplorers } from "./constants"; +import { ClaimTx } from "./render-transaction/tx-type"; + +export class AppState { + public claims: ClaimTx[] = []; + private _provider!: JsonRpcProvider; + private _networkId: number | null = null; + private _currentIndex = 0; + // private _networkRpc: string; + + get networkId(): number | null { + return this.transaction.networkId; + } + + set networkId(value: number) { + this._networkId = value; + } + + get provider(): JsonRpcProvider { + return this._provider; + } + + set provider(value: JsonRpcProvider) { + this._provider = value; + } + + get transactionIndex(): number { + return this._currentIndex; + } + + get transaction(): ClaimTx | null { + return this.transactionIndex < this.claims.length ? this.claims[this.transactionIndex] : null; + } + + get transactionNetworkId() { + return this.transaction?.networkId; + } + + get networkRpc(): string { + return this._networkRpc; + } + + get currentExplorerUrl(): string { + if (!this.transaction) { + return "https://etherscan.io"; + } + return networkExplorers[this.transaction.networkId] || "https://etherscan.io"; + } + + nextTx(): ClaimTx | null { + this._currentIndex = Math.min(this.claims.length - 1, this._currentIndex + 1); + return this.transaction; + } + + previousTx(): ClaimTx | null { + this._currentIndex = Math.max(0, this._currentIndex - 1); + return this.transaction; + } +} + +export const app = new AppState(); diff --git a/static/scripts/rewards/helpers.ts b/static/scripts/rewards/helpers.ts deleted file mode 100644 index 78d732b9..00000000 --- a/static/scripts/rewards/helpers.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; -import axios from "axios"; -import { Contract, ethers } from "ethers"; -import { erc20Abi } from "./abis"; -import { networkRpcs } from "./constants"; - -type DataType = { - jsonrpc: string; - id: number; - result: { - number: string; - timestamp: string; - hash: string; - }; -}; - -function verifyBlock(data: DataType) { - try { - const { jsonrpc, id, result } = data; - const { number, timestamp, hash } = result; - return jsonrpc === "2.0" && id === 1 && parseInt(number, 16) > 0 && parseInt(timestamp, 16) > 0 && hash.match(/[0-9|a-f|A-F|x]/gm)?.join("").length === 66; - } catch (error) { - return false; - } -} - -const RPC_BODY = JSON.stringify({ - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: ["latest", false], - id: 1, -}); - -const RPC_HEADER = { - "Content-Type": "application/json", -}; - -export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { - return new ethers.Contract(contractAddress, erc20Abi, provider); -} - -export async function testRpcPerformance(networkId: number) { - const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); - - const promises = networkRpcs[networkId].map(async (baseURL: string) => { - try { - const startTime = performance.now(); - const API = axios.create({ - baseURL, - headers: RPC_HEADER, - }); - - const { data } = await API.post("", RPC_BODY); - const endTime = performance.now(); - const latency = endTime - startTime; - if (verifyBlock(data)) { - // Save the latency in localStorage - latencies[baseURL] = latency; - } else { - // Save -1 in localStorage to indicate an error - latencies[baseURL] = -1; - } - } catch (error) { - // Save -1 in localStorage to indicate an error - latencies[baseURL] = -1; - } - }); - - await Promise.all(promises); - localStorage.setItem("rpcLatencies", JSON.stringify(latencies)); -} - -export function getFastestRpcProvider(networkId: number) { - const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); - - // Get all latencies from localStorage and find the fastest RPC - const sortedLatencies = Object.entries(latencies).sort((a, b) => a[1] - b[1]); - const optimalRPC = sortedLatencies[0][0]; - - return new ethers.providers.JsonRpcProvider(optimalRPC, { - name: optimalRPC, - chainId: networkId, - ensAddress: "", - }); -} - -let isTestCompleted = false; - -export async function getOptimalProvider(networkId: number): Promise { - // If the test is already completed for this session, return the fastest RPC provider - if (isTestCompleted) { - return getFastestRpcProvider(networkId); - } - - // If the test is not completed yet, check if there are any latencies stored in the localStorage - const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); - if (Object.keys(latencies).length > 0) { - // If there are latencies stored in the localStorage, use the previous best RPC - const provider = getFastestRpcProvider(networkId); - // Start the test in the background - testRpcPerformance(networkId) - .then(() => { - isTestCompleted = true; - }) - .catch(console.error); - return provider; - } else { - // If it's the user's first time and there are no latencies stored in the localStorage, - // wait for the test to finish and then return the fastest RPC provider - await testRpcPerformance(networkId); - isTestCompleted = true; - return getFastestRpcProvider(networkId); - } -} diff --git a/static/scripts/rewards/index.ts b/static/scripts/rewards/index.ts deleted file mode 100644 index 4f7731e7..00000000 --- a/static/scripts/rewards/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { init } from "./render-transaction/render-transaction"; -import { grid } from "./the-grid"; - -declare const commitHash: string; // @DEV: passed in at build time check build/esbuild-build.ts - -(async function appAsyncWrapper() { - // display commit hash - const buildElement = document.querySelector(`#build a`) as HTMLAnchorElement; - buildElement.innerHTML = commitHash; - buildElement.href = `https://github.com/ubiquity/pay.ubq.fi/commit/${commitHash}`; - - init().catch(console.error); - grid(document.getElementById("grid") as HTMLElement); -})().catch(console.error); diff --git a/static/scripts/rewards/init.ts b/static/scripts/rewards/init.ts new file mode 100644 index 00000000..8579b83a --- /dev/null +++ b/static/scripts/rewards/init.ts @@ -0,0 +1,15 @@ +import { readClaimDataFromUrl } from "./render-transaction/read-claim-data-from-url"; +import { grid } from "./the-grid"; + +displayCommitHash(); // @DEV: display commit hash in footer +grid(document.getElementById("grid") as HTMLElement); // @DEV: display grid background + +readClaimDataFromUrl(); // @DEV: read claim data from URL + +declare const commitHash: string; // @DEV: passed in at build time check build/esbuild-build.ts +function displayCommitHash() { + // display commit hash in footer + const buildElement = document.querySelector(`#build a`) as HTMLAnchorElement; + buildElement.innerHTML = commitHash; + buildElement.href = `https://github.com/ubiquity/pay.ubq.fi/commit/${commitHash}`; +} diff --git a/static/scripts/rewards/render-transaction/claimRewardsPagination.ts b/static/scripts/rewards/render-transaction/claimRewardsPagination.ts new file mode 100644 index 00000000..30738c09 --- /dev/null +++ b/static/scripts/rewards/render-transaction/claimRewardsPagination.ts @@ -0,0 +1,34 @@ +import { app } from "../app-state"; +import { claimButton } from "../toaster"; +import { table } from "./read-claim-data-from-url"; +import { renderTransaction } from "./renderTransaction"; +import { setPagination } from "./setPagination"; +import { removeAllEventListeners } from "./utils"; + +export function claimRewardsPagination(rewardsCount: HTMLElement) { + rewardsCount.innerHTML = `${app.transactionIndex + 1}/${app.claims.length} reward`; + + const nextTxButton = document.getElementById("nextTx"); + if (nextTxButton) { + nextTxButton.addEventListener("click", () => { + claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; + app.nextTx(); + rewardsCount.innerHTML = `${app.transactionIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "none"); + renderTransaction(true).catch(console.error); + }); + } + + const prevTxButton = document.getElementById("previousTx"); + if (prevTxButton) { + prevTxButton.addEventListener("click", () => { + claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; + app.previousTx(); + rewardsCount.innerHTML = `${app.transactionIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "none"); + renderTransaction(true).catch(console.error); + }); + } + + setPagination(nextTxButton, prevTxButton); +} diff --git a/static/scripts/rewards/render-transaction/index.ts b/static/scripts/rewards/render-transaction/index.ts deleted file mode 100644 index ee56eba6..00000000 --- a/static/scripts/rewards/render-transaction/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { networkExplorers } from "../constants"; -import { getOptimalProvider } from "../helpers"; -import { ClaimTx } from "./tx-type"; - -class AppState { - public claimTxs: ClaimTx[] = []; - private _currentIndex = 0; - - get currentIndex(): number { - return this._currentIndex; - } - - get currentTx(): ClaimTx | null { - return this.currentIndex < this.claimTxs.length ? this.claimTxs[this.currentIndex] : null; - } - - async currentNetworkRpc(): Promise { - if (!this.currentTx) { - return (await getOptimalProvider(1)).connection.url; - } - return (await getOptimalProvider(this.currentTx.networkId)).connection.url; - } - - get currentExplorerUrl(): string { - if (!this.currentTx) { - return "https://etherscan.io"; - } - return networkExplorers[this.currentTx.networkId] || "https://etherscan.io"; - } - - nextTx(): ClaimTx | null { - this._currentIndex = Math.min(this.claimTxs.length - 1, this._currentIndex + 1); - return this.currentTx; - } - - previousTx(): ClaimTx | null { - this._currentIndex = Math.max(0, this._currentIndex - 1); - return this.currentTx; - } -} - -export const app = new AppState(); diff --git a/static/scripts/rewards/render-transaction/insert-table-data.ts b/static/scripts/rewards/render-transaction/insert-table-data.ts index 565bc1ce..0a487670 100644 --- a/static/scripts/rewards/render-transaction/insert-table-data.ts +++ b/static/scripts/rewards/render-transaction/insert-table-data.ts @@ -1,5 +1,5 @@ import { BigNumber, ethers } from "ethers"; -import { app } from "."; +import { app } from "../app-state"; import { Erc20Permit, Erc721Permit } from "./tx-type"; export function shortenAddress(address: string): string { 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 new file mode 100644 index 00000000..bde60ae3 --- /dev/null +++ b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts @@ -0,0 +1,65 @@ +import { Type } from "@sinclair/typebox"; +import { Value } from "@sinclair/typebox/value"; +import { app } from "../app-state"; +import { getOptimalProvider } from "../rpc-optimization/getOptimalProvider"; +import { claimRewardsPagination } from "./claimRewardsPagination"; +import { renderTransaction } from "./renderTransaction"; +import { setClaimMessage } from "./set-claim-message"; +import { claimTxT } from "./tx-type"; + +export const table = document.getElementsByTagName(`table`)[0]; +const urlParams = new URLSearchParams(window.location.search); +const base64encodedTxData = urlParams.get("claim"); + +export function readClaimDataFromUrl() { + if (!base64encodedTxData) { + // No claim data found + setClaimMessage({ type: "Notice", message: `No claim data found.` }); + table.setAttribute(`data-claim`, "none"); + return; + } + + decodeClaimData(base64encodedTxData); + getOptimalProvider(app).catch(console.error); + + displayRewardDetails(); + displayRewardPagination(); + + renderTransaction(true) + // .then(() => verifyCurrentNetwork(app.transaction?.networkId || app.networkId)) // @todo: verifyCurrentNetwork + .catch(console.error); +} + +function decodeClaimData(base64encodedTxData: string) { + try { + const claimTxs = Value.Decode(Type.Array(claimTxT), JSON.parse(atob(base64encodedTxData))); + app.claims = claimTxs; + app.networkId = app.claims[0].networkId; + } catch (error) { + console.error(error); + setClaimMessage({ type: "Error", message: `Invalid claim data passed in URL` }); + table.setAttribute(`data-claim`, "error"); + } +} + +function displayRewardPagination() { + const rewardsCount = document.getElementById("rewardsCount"); + if (rewardsCount) { + if (!app.claims || app.claims.length <= 1) { + // already hidden + } else { + claimRewardsPagination(rewardsCount); + } + } +} + +function displayRewardDetails() { + let isDetailsVisible = false; + table.setAttribute(`data-details-visible`, isDetailsVisible.toString()); + + const additionalDetails = document.getElementById(`additionalDetails`) as HTMLElement; + additionalDetails.addEventListener("click", () => { + isDetailsVisible = !isDetailsVisible; + table.setAttribute(`data-details-visible`, isDetailsVisible.toString()); + }); +} diff --git a/static/scripts/rewards/render-transaction/render-ens-name.ts b/static/scripts/rewards/render-transaction/render-ens-name.ts index 8be32621..df4043ed 100644 --- a/static/scripts/rewards/render-transaction/render-ens-name.ts +++ b/static/scripts/rewards/render-transaction/render-ens-name.ts @@ -1,5 +1,5 @@ +import { app } from "../app-state"; import { ensLookup } from "../cirip/ens-lookup"; -import { app } from "./index"; type EnsParams = | { diff --git a/static/scripts/rewards/render-transaction/render-token-symbol.ts b/static/scripts/rewards/render-transaction/render-token-symbol.ts index d7af472a..a8fe4227 100644 --- a/static/scripts/rewards/render-transaction/render-token-symbol.ts +++ b/static/scripts/rewards/render-transaction/render-token-symbol.ts @@ -1,7 +1,7 @@ -import { BigNumberish, Contract, utils } from "ethers"; -import { getErc20Contract } from "../helpers"; -import { MaxUint256 } from "@uniswap/permit2-sdk"; import { JsonRpcProvider } from "@ethersproject/providers"; +import { MaxUint256 } from "@uniswap/permit2-sdk"; +import { BigNumberish, Contract, utils } from "ethers"; +import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; export const tokens = [ { diff --git a/static/scripts/rewards/render-transaction/render-transaction.ts b/static/scripts/rewards/render-transaction/render-transaction.ts deleted file mode 100644 index 7d416e6f..00000000 --- a/static/scripts/rewards/render-transaction/render-transaction.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; -import { Type } from "@sinclair/typebox"; -import { Value } from "@sinclair/typebox/value"; -import { networkExplorers } from "../constants"; -import { getOptimalProvider } from "../helpers"; -import { claimButton, hideClaimButton, resetClaimButton } from "../toaster"; -import { claimErc20PermitHandler, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; -import { claimErc721PermitHandler } from "../web3/erc721-permit"; -import { handleNetwork } from "../web3/wallet"; -import { app } from "./index"; -import { insertErc20PermitTableData, insertErc721PermitTableData } from "./insert-table-data"; -import { renderEnsName } from "./render-ens-name"; -import { renderNftSymbol, renderTokenSymbol } from "./render-token-symbol"; -import { setClaimMessage } from "./set-claim-message"; -import { claimTxT } from "./tx-type"; -import { removeAllEventListeners } from "./utils"; - -let optimalRPC: JsonRpcProvider; - -export async function init() { - const table = document.getElementsByTagName(`table`)[0]; - - // decode base64 to get tx data - const urlParams = new URLSearchParams(window.location.search); - const base64encodedTxData = urlParams.get("claim"); - - if (!base64encodedTxData) { - setClaimMessage({ type: "Notice", message: `No claim data found.` }); - table.setAttribute(`data-claim`, "none"); - return false; - } - - try { - const claimTxs = Value.Decode(Type.Array(claimTxT), JSON.parse(atob(base64encodedTxData))); - app.claimTxs = claimTxs; - optimalRPC = await getOptimalProvider(app.currentTx?.networkId ?? app.claimTxs[0].networkId); - - handleNetwork(app.currentTx?.networkId ?? app.claimTxs[0].networkId).catch(console.error); - } catch (error) { - console.error(error); - setClaimMessage({ type: "Error", message: `Invalid claim data passed in URL` }); - table.setAttribute(`data-claim`, "error"); - return false; - } - - let isDetailsVisible = false; - - table.setAttribute(`data-details-visible`, isDetailsVisible.toString()); - - const additionalDetails = document.getElementById(`additionalDetails`) as Element; - additionalDetails.addEventListener("click", () => { - isDetailsVisible = !isDetailsVisible; - table.setAttribute(`data-details-visible`, isDetailsVisible.toString()); - }); - - const rewardsCount = document.getElementById("rewardsCount"); - if (rewardsCount) { - if (!app.claimTxs || app.claimTxs.length <= 1) { - // already hidden - } else { - rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; - - const nextTxButton = document.getElementById("nextTx"); - if (nextTxButton) { - nextTxButton.addEventListener("click", () => { - claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; - app.nextTx(); - rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; - table.setAttribute(`data-claim`, "none"); - renderTransaction(optimalRPC, true).catch(console.error); - }); - } - - const prevTxButton = document.getElementById("previousTx"); - if (prevTxButton) { - prevTxButton.addEventListener("click", () => { - claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; - app.previousTx(); - rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; - table.setAttribute(`data-claim`, "none"); - renderTransaction(optimalRPC, true).catch(console.error); - }); - } - - setPagination(nextTxButton, prevTxButton); - } - } - - renderTransaction(optimalRPC, true).catch(console.error); -} - -function setPagination(nextTxButton: Element | null, prevTxButton: Element | null) { - if (!nextTxButton || !prevTxButton) return; - if (app.claimTxs.length > 1) { - prevTxButton.classList.remove("hide-pagination"); - nextTxButton.classList.remove("hide-pagination"); - - prevTxButton.classList.add("show-pagination"); - nextTxButton.classList.add("show-pagination"); - } -} - -type Success = boolean; -export async function renderTransaction(provider: JsonRpcProvider, nextTx?: boolean): Promise { - const table = document.getElementsByTagName(`table`)[0]; - resetClaimButton(); - - if (nextTx) { - app.nextTx(); - if (!app.claimTxs || app.claimTxs.length <= 1) { - // already hidden - } else { - setPagination(document.getElementById("nextTx"), document.getElementById("previousTx")); - - const rewardsCount = document.getElementById("rewardsCount") as Element; - rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; - table.setAttribute(`data-claim`, "none"); - } - } - - if (!app.currentTx) { - hideClaimButton(); - return false; - } - - handleNetwork(app.currentTx.networkId).catch(console.error); - - if (app.currentTx.type === "erc20-permit") { - const treasury = await fetchTreasury(app.currentTx, provider); - - // insert tx data into table - const requestedAmountElement = insertErc20PermitTableData(app.currentTx, table, treasury); - table.setAttribute(`data-claim`, "ok"); - - renderTokenSymbol({ - tokenAddress: app.currentTx.permit.permitted.token, - ownerAddress: app.currentTx.owner, - amount: app.currentTx.transferDetails.requestedAmount, - explorerUrl: networkExplorers[app.currentTx.networkId], - table, - requestedAmountElement, - provider, - }).catch(console.error); - - const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.currentTx.transferDetails.to }).catch(console.error); - - generateInvalidatePermitAdminControl(app.currentTx).catch(console.error); - - claimButton.element.addEventListener("click", claimErc20PermitHandler(app.currentTx, optimalRPC)); - } else if (app.currentTx.type === "erc721-permit") { - const requestedAmountElement = insertErc721PermitTableData(app.currentTx, table); - table.setAttribute(`data-claim`, "ok"); - - renderNftSymbol({ - tokenAddress: app.currentTx.nftAddress, - explorerUrl: networkExplorers[app.currentTx.networkId], - table, - requestedAmountElement, - provider, - }).catch(console.error); - - const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.currentTx.request.beneficiary }).catch(console.error); - - claimButton.element.addEventListener("click", claimErc721PermitHandler(app.currentTx, provider)); - } - - return true; -} diff --git a/static/scripts/rewards/render-transaction/renderTransaction.ts b/static/scripts/rewards/render-transaction/renderTransaction.ts new file mode 100644 index 00000000..3a8d3974 --- /dev/null +++ b/static/scripts/rewards/render-transaction/renderTransaction.ts @@ -0,0 +1,80 @@ +import { app } from "../app-state"; +import { networkExplorers } from "../constants"; +import { claimButton, hideClaimButton, resetClaimButton } from "../toaster"; +import { claimErc20PermitHandler, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; +import { claimErc721PermitHandler } from "../web3/erc721-permit"; +import { verifyCurrentNetwork } from "../web3/wallet"; +import { insertErc20PermitTableData, insertErc721PermitTableData } from "./insert-table-data"; +import { renderEnsName } from "./render-ens-name"; +import { renderNftSymbol, renderTokenSymbol } from "./render-token-symbol"; +import { setPagination } from "./setPagination"; + +type Success = boolean; + +export async function renderTransaction(nextTx?: boolean): Promise { + const table = document.getElementsByTagName(`table`)[0]; + resetClaimButton(); + + if (nextTx) { + app.nextTx(); + if (!app.claims || app.claims.length <= 1) { + // already hidden + } else { + setPagination(document.getElementById("nextTx"), document.getElementById("previousTx")); + + const rewardsCount = document.getElementById("rewardsCount") as Element; + rewardsCount.innerHTML = `${app.transactionIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "none"); + } + } + + if (!app.transaction) { + hideClaimButton(); + return false; + } + + verifyCurrentNetwork(app.transaction.networkId).catch(console.error); + + if (app.transaction.type === "erc20-permit") { + const treasury = await fetchTreasury(app.transaction, app.provider); + + // insert tx data into table + const requestedAmountElement = insertErc20PermitTableData(app.transaction, table, treasury); + table.setAttribute(`data-claim`, "ok"); + + renderTokenSymbol({ + tokenAddress: app.transaction.permit.permitted.token, + ownerAddress: app.transaction.owner, + amount: app.transaction.transferDetails.requestedAmount, + explorerUrl: networkExplorers[app.transaction.networkId], + table, + requestedAmountElement, + provider: app.provider, + }).catch(console.error); + + const toElement = document.getElementById(`rewardRecipient`) as Element; + renderEnsName({ element: toElement, address: app.transaction.transferDetails.to }).catch(console.error); + + generateInvalidatePermitAdminControl(app.transaction).catch(console.error); + + claimButton.element.addEventListener("click", claimErc20PermitHandler(app.transaction)); + } else if (app.transaction.type === "erc721-permit") { + const requestedAmountElement = insertErc721PermitTableData(app.transaction, table); + table.setAttribute(`data-claim`, "ok"); + + renderNftSymbol({ + tokenAddress: app.transaction.nftAddress, + explorerUrl: networkExplorers[app.transaction.networkId], + table, + requestedAmountElement, + provider: app.provider, + }).catch(console.error); + + const toElement = document.getElementById(`rewardRecipient`) as Element; + renderEnsName({ element: toElement, address: app.transaction.request.beneficiary }).catch(console.error); + + claimButton.element.addEventListener("click", claimErc721PermitHandler(app.transaction, provider)); + } + + return true; +} diff --git a/static/scripts/rewards/render-transaction/setPagination.ts b/static/scripts/rewards/render-transaction/setPagination.ts new file mode 100644 index 00000000..d61977fb --- /dev/null +++ b/static/scripts/rewards/render-transaction/setPagination.ts @@ -0,0 +1,12 @@ +import { app } from "../app-state"; + +export function setPagination(nextTxButton: Element | null, prevTxButton: Element | null) { + if (!nextTxButton || !prevTxButton) return; + if (app.claims.length > 1) { + prevTxButton.classList.remove("hide-pagination"); + nextTxButton.classList.remove("hide-pagination"); + + prevTxButton.classList.add("show-pagination"); + nextTxButton.classList.add("show-pagination"); + } +} diff --git a/static/scripts/rewards/rpc-optimization/getErc20Contract.ts b/static/scripts/rewards/rpc-optimization/getErc20Contract.ts new file mode 100644 index 00000000..f9145644 --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/getErc20Contract.ts @@ -0,0 +1,7 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Contract, ethers } from "ethers"; +import { erc20Abi } from "../abis"; + +export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { + return new ethers.Contract(contractAddress, erc20Abi, provider); +} diff --git a/static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts b/static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts new file mode 100644 index 00000000..61c7f045 --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts @@ -0,0 +1,18 @@ +import { ethers } from "ethers"; + +export function getFastestRpcProvider(networkId: number) { + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + + // Filter out latencies with a value of less than 0 because -1 means it failed + // Also filter out latencies that do not belong to the desired network + const validLatencies = Object.entries(latencies).filter(([key, latency]) => latency >= 0 && key.startsWith(`${networkId}_`)); + + // Get all valid latencies from localStorage and find the fastest RPC + const sortedLatencies = validLatencies.sort((a, b) => a[1] - b[1]); + const optimalRPC = sortedLatencies[0][0].split("_")[0]; // Remove the network ID from the key + + return new ethers.providers.JsonRpcProvider(optimalRPC, { + name: optimalRPC, + chainId: networkId, + }); +} diff --git a/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts b/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts new file mode 100644 index 00000000..2a4eec76 --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts @@ -0,0 +1,28 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { AppState } from "../app-state"; +import { getFastestRpcProvider } from "./getFastestRpcProvider"; +import { testRpcPerformance } from "./testRpcPerformance"; + +let optimalProvider: JsonRpcProvider | null = null; +let isTestStarted = false; +let isTestCompleted = false; + +export async function getOptimalProvider(app: AppState): Promise { + const networkId = app.transactionNetworkId; + console.trace({ app }); + if (!networkId) throw new Error("Network ID not found"); + + if (!isTestCompleted && !isTestStarted) { + isTestStarted = true; + await testRpcPerformance(networkId); + isTestCompleted = true; + } + + if (!optimalProvider) { + optimalProvider = getFastestRpcProvider(networkId); + } + + console.trace({ optimalProvider }); + + return (app.provider = optimalProvider); +} diff --git a/static/scripts/rewards/rpc-optimization/testRpcPerformance.ts b/static/scripts/rewards/rpc-optimization/testRpcPerformance.ts new file mode 100644 index 00000000..9837a79f --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/testRpcPerformance.ts @@ -0,0 +1,64 @@ +import axios from "axios"; +import { networkRpcs } from "../constants"; + +type DataType = { + jsonrpc: string; + id: number; + result: { + number: string; + timestamp: string; + hash: string; + }; +}; + +function verifyBlock(data: DataType) { + try { + const { jsonrpc, id, result } = data; + const { number, timestamp, hash } = result; + return jsonrpc === "2.0" && id === 1 && parseInt(number, 16) > 0 && parseInt(timestamp, 16) > 0 && hash.match(/[0-9|a-f|A-F|x]/gm)?.join("").length === 66; + } catch (error) { + return false; + } +} + +const RPC_BODY = JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: ["latest", false], + id: 1, +}); + +const RPC_HEADER = { + "Content-Type": "application/json", +}; + +export async function testRpcPerformance(networkId: number) { + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + + const promises = networkRpcs[networkId].map(async (baseURL: string) => { + try { + const startTime = performance.now(); + const API = axios.create({ + baseURL, + headers: RPC_HEADER, + }); + + const { data } = await API.post("", RPC_BODY); + const endTime = performance.now(); + const latency = endTime - startTime; + if (verifyBlock(data)) { + // Save the latency in localStorage + latencies[`${networkId}_${baseURL}`] = latency; + } else { + // Save -1 in localStorage to indicate an error + latencies[`${networkId}_${baseURL}`] = -1; + } + } catch (error) { + // Save -1 in localStorage to indicate an error + latencies[`${networkId}_${baseURL}`] = -1; + } + }); + + await Promise.all(promises); + localStorage.setItem("rpcLatencies", JSON.stringify(latencies)); +} diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index 014f68fb..c9eda81a 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -1,14 +1,15 @@ +import { JsonRpcProvider, JsonRpcSigner } from "@ethersproject/providers"; import { BigNumber, BigNumberish, ethers } from "ethers"; import { permit2Abi } from "../abis"; +import { app } from "../app-state"; import { permit2Address } from "../constants"; -import { getErc20Contract, getOptimalProvider } from "../helpers"; -import { Erc20Permit } from "../render-transaction/tx-type"; -import { toaster, resetClaimButton, errorToast, loadingClaimButton, claimButton } from "../toaster"; -import { renderTransaction } from "../render-transaction/render-transaction"; -import { connectWallet } from "./wallet"; import invalidateButton from "../invalidate-component"; -import { JsonRpcProvider } from "@ethersproject/providers"; import { tokens } from "../render-transaction/render-token-symbol"; +import { renderTransaction } from "../render-transaction/renderTransaction"; +import { Erc20Permit } from "../render-transaction/tx-type"; +import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; +import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; +import { connectWallet } from "./wallet"; export async function fetchTreasury( permit: Erc20Permit, @@ -41,7 +42,7 @@ export async function fetchTreasury( } } -export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcProvider) { +export function claimErc20PermitHandler(permit: Erc20Permit) { return async function handler() { try { const signer = await connectWallet(); @@ -49,7 +50,7 @@ export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcPr return; } - if (!(await checkPermitClaimable(permit, signer, provider))) { + if (!(await checkPermitClaimable(permit, signer, app.provider))) { return; } @@ -62,10 +63,10 @@ export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcPr console.log(receipt.transactionHash); // @TODO: post to database claimButton.element.removeEventListener("click", handler); - renderTransaction(provider).catch(console.error); + renderTransaction().catch(console.error); } catch (error: unknown) { if (error instanceof Error) { - console.log(error); + console.error(error); errorToast(error, error.message); resetClaimButton(); } @@ -73,7 +74,7 @@ export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcPr }; } -export async function checkPermitClaimable(permit: Erc20Permit, signer: ethers.providers.JsonRpcSigner | null, provider: JsonRpcProvider) { +export async function checkPermitClaimable(permit: Erc20Permit, signer: JsonRpcSigner | null, provider: JsonRpcProvider) { const isClaimed = await isNonceClaimed(permit); if (isClaimed) { toaster.create("error", `Your reward for this task has already been claimed or invalidated.`); @@ -140,7 +141,7 @@ export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) await invalidateNonce(signer, permit.permit.nonce); } catch (error: unknown) { if (error instanceof Error) { - console.log(error); + console.error(error); errorToast(error, error.message); return; } @@ -151,7 +152,7 @@ export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) //mimics https://github.com/Uniswap/permit2/blob/a7cd186948b44f9096a35035226d7d70b9e24eaf/src/SignatureTransfer.sol#L150 export async function isNonceClaimed(permit: Erc20Permit): Promise { - const provider = await getOptimalProvider(permit.networkId); + const provider = app.provider; const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, provider); @@ -164,7 +165,7 @@ export async function isNonceClaimed(permit: Erc20Permit): Promise { return bit.and(flipped).eq(0); } -export async function invalidateNonce(signer: ethers.providers.JsonRpcSigner, nonce: BigNumberish): Promise { +export async function invalidateNonce(signer: JsonRpcSigner, nonce: BigNumberish): Promise { 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 diff --git a/static/scripts/rewards/web3/erc721-permit.ts b/static/scripts/rewards/web3/erc721-permit.ts index 0dafff68..febfc1e6 100644 --- a/static/scripts/rewards/web3/erc721-permit.ts +++ b/static/scripts/rewards/web3/erc721-permit.ts @@ -1,12 +1,13 @@ import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers"; import { ethers } from "ethers"; import { nftRewardAbi } from "../abis/nftRewardAbi"; -import { renderTransaction } from "../render-transaction/render-transaction"; +import { renderTransaction } from "../render-transaction/renderTransaction"; import { Erc721Permit } from "../render-transaction/tx-type"; import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; import { connectWallet } from "./wallet"; +import { app } from "../app-state"; -export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpcProvider) { +export function claimErc721PermitHandler(permit: Erc721Permit) { return async function claimButtonHandler() { const signer = await connectWallet(); if (!signer) { @@ -25,7 +26,7 @@ export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpc return; } - const isRedeemed = await isNonceRedeemed(permit, provider); + const isRedeemed = await isNonceRedeemed(permit, app.provider); if (isRedeemed) { toaster.create("error", `This NFT has already been redeemed.`); resetClaimButton(); @@ -44,7 +45,7 @@ export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpc claimButton.element.removeEventListener("click", claimButtonHandler); - renderTransaction(provider, true).catch((error) => { + renderTransaction(true).catch((error) => { console.error(error); toaster.create("error", `Error rendering transaction: ${error.message}`); }); diff --git a/static/scripts/rewards/web3/wallet.ts b/static/scripts/rewards/web3/wallet.ts index 39d67a39..8fcd5072 100644 --- a/static/scripts/rewards/web3/wallet.ts +++ b/static/scripts/rewards/web3/wallet.ts @@ -1,12 +1,14 @@ import { JsonRpcSigner } from "@ethersproject/providers"; import { ethers } from "ethers"; +import { app } from "../app-state"; import { getNetworkName, networkCurrencies, networkExplorers, networkRpcs } from "../constants"; import invalidateButton from "../invalidate-component"; import { claimButton, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; export async function connectWallet(): Promise { try { - const provider = new ethers.providers.Web3Provider(window.ethereum, "any"); + // const provider = new ethers.providers.Web3Provider(window.ethereum); + const provider = app.provider; await provider.send("eth_requestAccounts", []); const signer = provider.getSigner(); resetClaimButton(); @@ -25,7 +27,8 @@ export async function connectWallet(): Promise { } } -export async function handleNetwork(desiredNetworkId: number) { +// verifyCurrentNetwork checks if the user is on the correct network and displays an error if not +export async function verifyCurrentNetwork(desiredNetworkId: number) { const web3provider = new ethers.providers.Web3Provider(window.ethereum); if (!web3provider || !web3provider.provider.isMetaMask) { toaster.create("info", "Please connect to MetaMask."); diff --git a/tsconfig.json b/tsconfig.json index 45586ec1..f001a6da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,7 +42,7 @@ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ From 0fc24a2c9d455f7ea6fe13871e22255d608fd907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 03:23:58 +0900 Subject: [PATCH 04/14] fix: use fastest rpc endpoint for requests --- globals.d.ts | 14 +-- package.json | 3 +- static/scripts/rewards/helpers.ts | 111 ++++++++++++++++++ static/scripts/rewards/init.ts | 2 +- .../read-claim-data-from-url.ts | 5 +- .../render-transaction/renderTransaction.ts | 8 +- .../rpc-optimization/getFastestRpcProvider.ts | 4 +- .../rpc-optimization/getOptimalProvider.ts | 11 +- .../rpc-optimization/testRpcPerformance.ts | 8 +- static/scripts/rewards/web3/addNetwork.ts | 19 +++ static/scripts/rewards/web3/connectWallet.ts | 35 ++++++ static/scripts/rewards/web3/erc20-permit.ts | 33 ++++-- static/scripts/rewards/web3/erc721-permit.ts | 4 +- .../rewards/web3/handleIfOnCorrectNetwork.ts | 13 ++ .../rewards/web3/notOnCorrectNetwork.ts | 23 ++++ static/scripts/rewards/web3/switchNetwork.ts | 16 +++ .../rewards/web3/verifyCurrentNetwork.ts | 25 ++++ static/scripts/rewards/web3/wallet.ts | 107 ----------------- yarn.lock | 12 ++ 19 files changed, 299 insertions(+), 154 deletions(-) create mode 100644 static/scripts/rewards/helpers.ts create mode 100644 static/scripts/rewards/web3/addNetwork.ts create mode 100644 static/scripts/rewards/web3/connectWallet.ts create mode 100644 static/scripts/rewards/web3/handleIfOnCorrectNetwork.ts create mode 100644 static/scripts/rewards/web3/notOnCorrectNetwork.ts create mode 100644 static/scripts/rewards/web3/switchNetwork.ts create mode 100644 static/scripts/rewards/web3/verifyCurrentNetwork.ts delete mode 100644 static/scripts/rewards/web3/wallet.ts diff --git a/globals.d.ts b/globals.d.ts index 4ab599ac..1a9ff4d3 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -1,17 +1,7 @@ -export interface EthereumIsh { - autoRefreshOnNetworkChange: boolean; - chainId: string; - isMetaMask?: boolean; - isStatus?: boolean; - networkVersion: string; - selectedAddress: string; - - on(event: "close" | "accountsChanged" | "chainChanged" | "networkChanged", callback: (payload: unknown) => void): void; - once(event: "close" | "accountsChanged" | "chainChanged" | "networkChanged", callback: (payload: unknown) => void): void; -} +import { Ethereum } from "ethereum-protocol"; declare global { interface Window { - ethereum: EthereumIsh; + ethereum: Ethereum; } } diff --git a/package.json b/package.json index affebbce..8c4e495a 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@cspell/dict-node": "^4.0.3", "@cspell/dict-software-terms": "^3.3.18", "@cspell/dict-typescript": "^3.1.2", + "@types/ethereum-protocol": "^1.0.5", "@types/node": "^20.11.19", "@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/parser": "^7.0.1", @@ -90,4 +91,4 @@ "@commitlint/config-conventional" ] } -} \ No newline at end of file +} diff --git a/static/scripts/rewards/helpers.ts b/static/scripts/rewards/helpers.ts new file mode 100644 index 00000000..c1cb468a --- /dev/null +++ b/static/scripts/rewards/helpers.ts @@ -0,0 +1,111 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import axios from "axios"; +import { Contract, ethers } from "ethers"; +import { erc20Abi } from "./abis"; +import { AppState } from "./app-state"; +import { networkRpcs } from "./constants"; + +type DataType = { + jsonrpc: string; + id: number; + result: { + number: string; + timestamp: string; + hash: string; + }; +}; + +function verifyBlock(data: DataType) { + try { + const { jsonrpc, id, result } = data; + const { number, timestamp, hash } = result; + return jsonrpc === "2.0" && id === 1 && parseInt(number, 16) > 0 && parseInt(timestamp, 16) > 0 && hash.match(/[0-9|a-f|A-F|x]/gm)?.join("").length === 66; + } catch (error) { + return false; + } +} + +const RPC_BODY = JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: ["latest", false], + id: 1, +}); + +const RPC_HEADER = { + "Content-Type": "application/json", +}; + +export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { + return new ethers.Contract(contractAddress, erc20Abi, provider); +} + +export async function testRpcPerformance(networkId: number) { + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + + const promises = networkRpcs[networkId].map(async (baseURL: string) => { + try { + const startTime = performance.now(); + const API = axios.create({ + baseURL, + headers: RPC_HEADER, + }); + + const { data } = await API.post("", RPC_BODY); + const endTime = performance.now(); + const latency = endTime - startTime; + if (verifyBlock(data)) { + // Save the latency in localStorage + latencies[baseURL] = latency; + } else { + // Save -1 in localStorage to indicate an error + latencies[baseURL] = -1; + } + } catch (error) { + // Save -1 in localStorage to indicate an error + latencies[baseURL] = -1; + } + }); + + await Promise.all(promises); + localStorage.setItem("rpcLatencies", JSON.stringify(latencies)); +} + +export function getFastestRpcProvider(networkId: number) { + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + + // Filter out latencies with a value of less than 0 because -1 means it failed + // Also filter out latencies that do not belong to the desired network + const validLatencies = Object.entries(latencies).filter(([key, latency]) => latency >= 0 && key.startsWith(`${networkId}_`)); + + // Get all valid latencies from localStorage and find the fastest RPC + const sortedLatencies = validLatencies.sort((a, b) => a[1] - b[1]); + const optimalRPC = sortedLatencies[0][0].split("_").slice(1).join("_"); // Remove the network ID from the key + + return new ethers.providers.JsonRpcProvider(optimalRPC, { + name: optimalRPC, + chainId: networkId, + }); +} + +let optimalProvider: ethers.providers.JsonRpcProvider | null = null; + +let isTestStarted = false; +let isTestCompleted = false; + +export async function getOptimalProvider(app: AppState): Promise { + const networkId = app.transactionNetworkId; + if (!networkId) throw new Error("Network ID not found"); + + if (!isTestCompleted && !isTestStarted) { + isTestStarted = true; + await testRpcPerformance(networkId); + isTestCompleted = true; + } + + if (!optimalProvider) { + optimalProvider = getFastestRpcProvider(networkId); + } + console.trace({ optimalProvider }); + return optimalProvider; +} diff --git a/static/scripts/rewards/init.ts b/static/scripts/rewards/init.ts index 8579b83a..8fd646ea 100644 --- a/static/scripts/rewards/init.ts +++ b/static/scripts/rewards/init.ts @@ -4,7 +4,7 @@ import { grid } from "./the-grid"; displayCommitHash(); // @DEV: display commit hash in footer grid(document.getElementById("grid") as HTMLElement); // @DEV: display grid background -readClaimDataFromUrl(); // @DEV: read claim data from URL +readClaimDataFromUrl().catch(console.error); // @DEV: read claim data from URL declare const commitHash: string; // @DEV: passed in at build time check build/esbuild-build.ts function displayCommitHash() { 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 bde60ae3..b363652b 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 @@ -11,7 +11,7 @@ export const table = document.getElementsByTagName(`table`)[0]; const urlParams = new URLSearchParams(window.location.search); const base64encodedTxData = urlParams.get("claim"); -export function readClaimDataFromUrl() { +export async function readClaimDataFromUrl() { if (!base64encodedTxData) { // No claim data found setClaimMessage({ type: "Notice", message: `No claim data found.` }); @@ -20,7 +20,8 @@ export function readClaimDataFromUrl() { } decodeClaimData(base64encodedTxData); - getOptimalProvider(app).catch(console.error); + + await getOptimalProvider(app); displayRewardDetails(); displayRewardPagination(); diff --git a/static/scripts/rewards/render-transaction/renderTransaction.ts b/static/scripts/rewards/render-transaction/renderTransaction.ts index 3a8d3974..76ef798b 100644 --- a/static/scripts/rewards/render-transaction/renderTransaction.ts +++ b/static/scripts/rewards/render-transaction/renderTransaction.ts @@ -1,9 +1,9 @@ import { app } from "../app-state"; import { networkExplorers } from "../constants"; import { claimButton, hideClaimButton, resetClaimButton } from "../toaster"; -import { claimErc20PermitHandler, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; +import { claimErc20PermitHandlerWrapper, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; import { claimErc721PermitHandler } from "../web3/erc721-permit"; -import { verifyCurrentNetwork } from "../web3/wallet"; +import { verifyCurrentNetwork } from "../web3/verifyCurrentNetwork"; import { insertErc20PermitTableData, insertErc721PermitTableData } from "./insert-table-data"; import { renderEnsName } from "./render-ens-name"; import { renderNftSymbol, renderTokenSymbol } from "./render-token-symbol"; @@ -57,7 +57,7 @@ export async function renderTransaction(nextTx?: boolean): Promise { generateInvalidatePermitAdminControl(app.transaction).catch(console.error); - claimButton.element.addEventListener("click", claimErc20PermitHandler(app.transaction)); + claimButton.element.addEventListener("click", claimErc20PermitHandlerWrapper(app.transaction)); } else if (app.transaction.type === "erc721-permit") { const requestedAmountElement = insertErc721PermitTableData(app.transaction, table); table.setAttribute(`data-claim`, "ok"); @@ -73,7 +73,7 @@ export async function renderTransaction(nextTx?: boolean): Promise { const toElement = document.getElementById(`rewardRecipient`) as Element; renderEnsName({ element: toElement, address: app.transaction.request.beneficiary }).catch(console.error); - claimButton.element.addEventListener("click", claimErc721PermitHandler(app.transaction, provider)); + claimButton.element.addEventListener("click", claimErc721PermitHandler(app.transaction)); } return true; diff --git a/static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts b/static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts index 61c7f045..e0cf1e7e 100644 --- a/static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts +++ b/static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts @@ -5,11 +5,11 @@ export function getFastestRpcProvider(networkId: number) { // Filter out latencies with a value of less than 0 because -1 means it failed // Also filter out latencies that do not belong to the desired network - const validLatencies = Object.entries(latencies).filter(([key, latency]) => latency >= 0 && key.startsWith(`${networkId}_`)); + const validLatencies = Object.entries(latencies).filter(([key, latency]) => latency >= 0 && key.endsWith(`_${networkId}`)); // Get all valid latencies from localStorage and find the fastest RPC const sortedLatencies = validLatencies.sort((a, b) => a[1] - b[1]); - const optimalRPC = sortedLatencies[0][0].split("_")[0]; // Remove the network ID from the key + const optimalRPC = sortedLatencies[0][0].split("_").slice(0, -1).join("_"); // Remove the network ID from the key return new ethers.providers.JsonRpcProvider(optimalRPC, { name: optimalRPC, diff --git a/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts b/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts index 2a4eec76..a6906a95 100644 --- a/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts +++ b/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts @@ -9,20 +9,19 @@ let isTestCompleted = false; export async function getOptimalProvider(app: AppState): Promise { const networkId = app.transactionNetworkId; - console.trace({ app }); if (!networkId) throw new Error("Network ID not found"); if (!isTestCompleted && !isTestStarted) { isTestStarted = true; - await testRpcPerformance(networkId); - isTestCompleted = true; + testRpcPerformance(networkId) + .then(() => (isTestCompleted = true)) + .catch(console.error); } if (!optimalProvider) { optimalProvider = getFastestRpcProvider(networkId); } - console.trace({ optimalProvider }); - - return (app.provider = optimalProvider); + app.provider = optimalProvider; + return optimalProvider; } diff --git a/static/scripts/rewards/rpc-optimization/testRpcPerformance.ts b/static/scripts/rewards/rpc-optimization/testRpcPerformance.ts index 9837a79f..39f17a84 100644 --- a/static/scripts/rewards/rpc-optimization/testRpcPerformance.ts +++ b/static/scripts/rewards/rpc-optimization/testRpcPerformance.ts @@ -43,19 +43,19 @@ export async function testRpcPerformance(networkId: number) { headers: RPC_HEADER, }); - const { data } = await API.post("", RPC_BODY); + const { data } = await API.post("", RPC_BODY).catch(() => ({ data: null })); const endTime = performance.now(); const latency = endTime - startTime; if (verifyBlock(data)) { // Save the latency in localStorage - latencies[`${networkId}_${baseURL}`] = latency; + latencies[`${baseURL}_${networkId}`] = latency; } else { // Save -1 in localStorage to indicate an error - latencies[`${networkId}_${baseURL}`] = -1; + latencies[`${baseURL}_${networkId}`] = -1; } } catch (error) { // Save -1 in localStorage to indicate an error - latencies[`${networkId}_${baseURL}`] = -1; + latencies[`${baseURL}_${networkId}`] = -1; } }); diff --git a/static/scripts/rewards/web3/addNetwork.ts b/static/scripts/rewards/web3/addNetwork.ts new file mode 100644 index 00000000..1c9dd6f2 --- /dev/null +++ b/static/scripts/rewards/web3/addNetwork.ts @@ -0,0 +1,19 @@ +import { ethers } from "ethers"; +import { getNetworkName, networkCurrencies, networkExplorers, networkRpcs } from "../constants"; + +export async function addNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { + try { + await provider.send("wallet_addEthereumChain", [ + { + chainId: "0x" + networkId.toString(16), + chainName: getNetworkName(networkId), + rpcUrls: networkRpcs[networkId], + blockExplorerUrls: [networkExplorers[networkId]], + nativeCurrency: networkCurrencies[networkId], + }, + ]); + return true; + } catch (error: unknown) { + return false; + } +} diff --git a/static/scripts/rewards/web3/connectWallet.ts b/static/scripts/rewards/web3/connectWallet.ts new file mode 100644 index 00000000..d22162f8 --- /dev/null +++ b/static/scripts/rewards/web3/connectWallet.ts @@ -0,0 +1,35 @@ +import { claimButton, resetClaimButton, toaster } from "../toaster"; +import { JsonRpcSigner } from "@ethersproject/providers"; +import { ethers } from "ethers"; + +export async function connectWallet(): Promise { + try { + if (!window.ethereum) { + console.error("Ethereum provider not found"); + return null; + } + + const wallet = new ethers.providers.Web3Provider(window.ethereum); + const signer = wallet.getSigner(); + const address = await signer.getAddress(); + if (!address) { + console.error("Wallet not connected"); + return null; + } + + resetClaimButton(); + return signer; + } 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."); + claimButton.element.disabled = true; + } else { + toaster.create("info", "Please connect your wallet to collect this reward."); + claimButton.element.disabled = true; + } + } + return null; + } +} diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index c9eda81a..f295048a 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -9,7 +9,7 @@ import { renderTransaction } from "../render-transaction/renderTransaction"; import { Erc20Permit } from "../render-transaction/tx-type"; import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; -import { connectWallet } from "./wallet"; +import { connectWallet } from "./connectWallet"; export async function fetchTreasury( permit: Erc20Permit, @@ -42,14 +42,14 @@ export async function fetchTreasury( } } -export function claimErc20PermitHandler(permit: Erc20Permit) { - return async function handler() { - try { - const signer = await connectWallet(); - if (!signer) { - return; - } +export function claimErc20PermitHandlerWrapper(permit: Erc20Permit) { + return async function claimErc20PermitHandler() { + const signer = await connectWallet(); + if (!signer) { + return; + } + try { if (!(await checkPermitClaimable(permit, signer, app.provider))) { return; } @@ -62,7 +62,7 @@ export function claimErc20PermitHandler(permit: Erc20Permit) { toaster.create("success", `Claim Complete.`); console.log(receipt.transactionHash); // @TODO: post to database - claimButton.element.removeEventListener("click", handler); + claimButton.element.removeEventListener("click", claimErc20PermitHandler); renderTransaction().catch(console.error); } catch (error: unknown) { if (error instanceof Error) { @@ -115,13 +115,20 @@ export async function checkPermitClaimable(permit: Erc20Permit, signer: JsonRpcS export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) { const signer = await connectWallet(); if (!signer) { + console.log("Wallet not connected"); return; } - const user = (await signer.getAddress()).toLowerCase(); - const owner = permit.owner.toLowerCase(); - if (owner !== user) { - return; + try { + const address = await signer.getAddress(); + const user = address.toLowerCase(); + const owner = permit.owner.toLowerCase(); + if (owner !== user) { + return; + } + } catch (error) { + console.error("Error getting address from signer"); + console.error(error); } const controls = document.getElementById("controls") as HTMLDivElement; diff --git a/static/scripts/rewards/web3/erc721-permit.ts b/static/scripts/rewards/web3/erc721-permit.ts index febfc1e6..9272d3fc 100644 --- a/static/scripts/rewards/web3/erc721-permit.ts +++ b/static/scripts/rewards/web3/erc721-permit.ts @@ -1,11 +1,11 @@ import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers"; import { ethers } from "ethers"; import { nftRewardAbi } from "../abis/nftRewardAbi"; +import { app } from "../app-state"; import { renderTransaction } from "../render-transaction/renderTransaction"; import { Erc721Permit } from "../render-transaction/tx-type"; import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; -import { connectWallet } from "./wallet"; -import { app } from "../app-state"; +import { connectWallet } from "./connectWallet"; export function claimErc721PermitHandler(permit: Erc721Permit) { return async function claimButtonHandler() { diff --git a/static/scripts/rewards/web3/handleIfOnCorrectNetwork.ts b/static/scripts/rewards/web3/handleIfOnCorrectNetwork.ts new file mode 100644 index 00000000..27972424 --- /dev/null +++ b/static/scripts/rewards/web3/handleIfOnCorrectNetwork.ts @@ -0,0 +1,13 @@ +import invalidateButton from "../invalidate-component"; +import { loadingClaimButton, resetClaimButton } from "../toaster"; + +export function handleIfOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number) { + if (desiredNetworkId === currentNetworkId) { + // enable the button once on the correct network + resetClaimButton(); + invalidateButton.disabled = false; + } else { + loadingClaimButton(false); + invalidateButton.disabled = true; + } +} diff --git a/static/scripts/rewards/web3/notOnCorrectNetwork.ts b/static/scripts/rewards/web3/notOnCorrectNetwork.ts new file mode 100644 index 00000000..cdbd9478 --- /dev/null +++ b/static/scripts/rewards/web3/notOnCorrectNetwork.ts @@ -0,0 +1,23 @@ +import { ethers } from "ethers"; +import { getNetworkName } from "../constants"; +import invalidateButton from "../invalidate-component"; +import { loadingClaimButton, toaster } from "../toaster"; +import { switchNetwork } from "./switchNetwork"; + +export function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number, web3provider: ethers.providers.Web3Provider) { + if (currentNetworkId !== desiredNetworkId) { + if (desiredNetworkId == void 0) { + console.error(`You must pass in an EVM network ID in the URL query parameters using the key 'network' e.g. '?network=1'`); + } + const networkName = getNetworkName(desiredNetworkId); + if (!networkName) { + toaster.create("error", `This dApp currently does not support payouts for network ID ${desiredNetworkId}`); + } + loadingClaimButton(false); + invalidateButton.disabled = true; + switchNetwork(web3provider, desiredNetworkId).catch((error) => { + console.error(error); + toaster.create("error", `Please switch to the ${networkName} network to claim this reward.`); + }); + } +} diff --git a/static/scripts/rewards/web3/switchNetwork.ts b/static/scripts/rewards/web3/switchNetwork.ts new file mode 100644 index 00000000..9ab2cac5 --- /dev/null +++ b/static/scripts/rewards/web3/switchNetwork.ts @@ -0,0 +1,16 @@ +import { ethers } from "ethers"; +import { addNetwork } from "./addNetwork"; + +export async function switchNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { + try { + await provider.send("wallet_switchEthereumChain", [{ chainId: "0x" + networkId.toString(16) }]); + return true; + } catch (error: unknown) { + // Add network if it doesn't exist. + const code = (error as { code: number }).code; + if (code == 4902) { + return await addNetwork(provider, networkId); + } + return false; + } +} diff --git a/static/scripts/rewards/web3/verifyCurrentNetwork.ts b/static/scripts/rewards/web3/verifyCurrentNetwork.ts new file mode 100644 index 00000000..f50a8340 --- /dev/null +++ b/static/scripts/rewards/web3/verifyCurrentNetwork.ts @@ -0,0 +1,25 @@ +import { ethers } from "ethers"; +import invalidateButton from "../invalidate-component"; +import { loadingClaimButton, toaster } from "../toaster"; +import { handleIfOnCorrectNetwork } from "./handleIfOnCorrectNetwork"; +import { notOnCorrectNetwork } from "./notOnCorrectNetwork"; + +// verifyCurrentNetwork checks if the user is on the correct network and displays an error if not + +export async function verifyCurrentNetwork(desiredNetworkId: number) { + const web3provider = new ethers.providers.Web3Provider(window.ethereum); + if (!web3provider || !web3provider.provider.isMetaMask) { + toaster.create("info", "Please connect to MetaMask."); + loadingClaimButton(false); + invalidateButton.disabled = true; + } + + const network = await web3provider.getNetwork(); + const currentNetworkId = network.chainId; + + // watch for network changes + window.ethereum.on("chainChanged", (newNetworkId: T | string) => handleIfOnCorrectNetwork(parseInt(newNetworkId as string, 16), desiredNetworkId)); + + // if its not on ethereum mainnet, gnosis, or goerli, display error + notOnCorrectNetwork(currentNetworkId, desiredNetworkId, web3provider); +} diff --git a/static/scripts/rewards/web3/wallet.ts b/static/scripts/rewards/web3/wallet.ts deleted file mode 100644 index 8fcd5072..00000000 --- a/static/scripts/rewards/web3/wallet.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { JsonRpcSigner } from "@ethersproject/providers"; -import { ethers } from "ethers"; -import { app } from "../app-state"; -import { getNetworkName, networkCurrencies, networkExplorers, networkRpcs } from "../constants"; -import invalidateButton from "../invalidate-component"; -import { claimButton, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; - -export async function connectWallet(): Promise { - try { - // const provider = new ethers.providers.Web3Provider(window.ethereum); - const provider = app.provider; - await provider.send("eth_requestAccounts", []); - const signer = provider.getSigner(); - resetClaimButton(); - return signer; - } catch (error: unknown) { - if (error instanceof Error) { - if (error?.message?.includes("missing provider")) { - toaster.create("info", "Please use a web3 enabled browser to collect this reward."); - claimButton.element.disabled = true; - } else { - toaster.create("info", "Please connect your wallet to collect this reward."); - claimButton.element.disabled = true; - } - } - return null; - } -} - -// verifyCurrentNetwork checks if the user is on the correct network and displays an error if not -export async function verifyCurrentNetwork(desiredNetworkId: number) { - const web3provider = new ethers.providers.Web3Provider(window.ethereum); - if (!web3provider || !web3provider.provider.isMetaMask) { - toaster.create("info", "Please connect to MetaMask."); - loadingClaimButton(false); - invalidateButton.disabled = true; - } - - const network = await web3provider.getNetwork(); - const currentNetworkId = network.chainId; - - // watch for network changes - window.ethereum.on("chainChanged", (newNetworkId: T | string) => handleIfOnCorrectNetwork(parseInt(newNetworkId as string, 16), desiredNetworkId)); - - // if its not on ethereum mainnet, gnosis, or goerli, display error - notOnCorrectNetwork(currentNetworkId, desiredNetworkId, web3provider); -} - -function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number, web3provider: ethers.providers.Web3Provider) { - if (currentNetworkId !== desiredNetworkId) { - if (desiredNetworkId == void 0) { - console.error(`You must pass in an EVM network ID in the URL query parameters using the key 'network' e.g. '?network=1'`); - } - const networkName = getNetworkName(desiredNetworkId); - if (!networkName) { - toaster.create("error", `This dApp currently does not support payouts for network ID ${desiredNetworkId}`); - } - loadingClaimButton(false); - invalidateButton.disabled = true; - switchNetwork(web3provider, desiredNetworkId).catch((error) => { - console.error(error); - toaster.create("error", `Please switch to the ${networkName} network to claim this reward.`); - }); - } -} - -function handleIfOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number) { - if (desiredNetworkId === currentNetworkId) { - // enable the button once on the correct network - resetClaimButton(); - invalidateButton.disabled = false; - } else { - loadingClaimButton(false); - invalidateButton.disabled = true; - } -} - -export async function switchNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { - try { - await provider.send("wallet_switchEthereumChain", [{ chainId: "0x" + networkId.toString(16) }]); - return true; - } catch (error: unknown) { - // Add network if it doesn't exist. - const code = (error as { code: number }).code; - if (code == 4902) { - return await addNetwork(provider, networkId); - } - return false; - } -} - -export async function addNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { - try { - await provider.send("wallet_addEthereumChain", [ - { - chainId: "0x" + networkId.toString(16), - chainName: getNetworkName(networkId), - rpcUrls: networkRpcs[networkId], - blockExplorerUrls: [networkExplorers[networkId]], - nativeCurrency: networkCurrencies[networkId], - }, - ]); - return true; - } catch (error: unknown) { - return false; - } -} diff --git a/yarn.lock b/yarn.lock index a00ad35c..4de35c4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1584,6 +1584,13 @@ "@supabase/realtime-js" "2.9.3" "@supabase/storage-js" "2.5.5" +"@types/ethereum-protocol@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/ethereum-protocol/-/ethereum-protocol-1.0.5.tgz#6ad4c2c722d440d1f59e0d7e44a0fbb5fad2c41b" + integrity sha512-4wr+t2rYbwMmDrT447SGzE/43Z0EN++zyHCBoruIx32fzXQDxVa1rnQbYwPO8sLP2OugE/L8KaAIJC5kieUuBg== + dependencies: + bignumber.js "7.2.1" + "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1941,6 +1948,11 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== +bignumber.js@7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" + integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" From cef52807a331d0b629e55031e07279f8a7b28e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 04:08:25 +0900 Subject: [PATCH 05/14] chore: kebabalize --- .github/workflows/kebab-case.yml | 15 ++++++++++++ .github/workflows/scripts/kebab-case.sh | 29 ++++++++++++++++++++++ .github/workflows/scripts/kebabalize.sh | 32 +++++++++++++++++++++++++ package.json | 3 ++- 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/kebab-case.yml create mode 100755 .github/workflows/scripts/kebab-case.sh create mode 100755 .github/workflows/scripts/kebabalize.sh diff --git a/.github/workflows/kebab-case.yml b/.github/workflows/kebab-case.yml new file mode 100644 index 00000000..1adda494 --- /dev/null +++ b/.github/workflows/kebab-case.yml @@ -0,0 +1,15 @@ +name: Enforce kebab-case + +on: + push: + pull_request: + +jobs: + check-filenames: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Check For Non Kebab-Cased TypeScript Files + run: .github/workflows/scripts/kebab-case.sh diff --git a/.github/workflows/scripts/kebab-case.sh b/.github/workflows/scripts/kebab-case.sh new file mode 100755 index 00000000..7c4e1926 --- /dev/null +++ b/.github/workflows/scripts/kebab-case.sh @@ -0,0 +1,29 @@ +#!/bin/bash +non_compliant_files=() +ignoreList=("^\.\/.git" "^\.\/\..*" "^\.\/[^\/]*$") +while IFS= read -r line; do +ignoreList+=(".*$line") +done < .gitignore +while read -r file; do +basefile=$(basename "$file") +ignoreFile=false +for pattern in "${ignoreList[@]}"; do + if [[ "$file" =~ $pattern ]]; then + ignoreFile=true + break + fi +done +if $ignoreFile; then + continue +elif ! echo "$basefile" | grep -q -E "^([a-z0-9]+-)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$|^([a-z0-9]+_)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$"; then + non_compliant_files+=("$file") + echo "::warning file=$file::This file is not in kebab-case or snake_case" +fi +done < <(find . -type f -name '*.ts' -print | grep -E '/[a-z]+[a-zA-Z]*\.ts$') +if [ ${#non_compliant_files[@]} -ne 0 ]; then +echo "The following files are not in kebab-case or snake_case:" +for file in "${non_compliant_files[@]}"; do + echo " - $file" +done +exit 1 +fi \ No newline at end of file diff --git a/.github/workflows/scripts/kebabalize.sh b/.github/workflows/scripts/kebabalize.sh new file mode 100755 index 00000000..467c6e37 --- /dev/null +++ b/.github/workflows/scripts/kebabalize.sh @@ -0,0 +1,32 @@ +#!/bin/bash +non_compliant_files=() +ignoreList=("^\.\/.git" "^\.\/\..*" "^\.\/[^\/]*$") +while IFS= read -r line; do +ignoreList+=(".*$line") +done < .gitignore +while read -r file; do +basefile=$(basename "$file") +ignoreFile=false +for pattern in "${ignoreList[@]}"; do + if [[ "$file" =~ $pattern ]]; then + ignoreFile=true + break + fi +done +if $ignoreFile; then + continue +elif ! echo "$basefile" | grep -q -E "^([a-z0-9]+-)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$|^([a-z0-9]+_)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$"; then + non_compliant_files+=("$file") + echo "::warning file=$file::This file is not in kebab-case or snake_case" + # Dry run: print the rename command without executing it + newfile=$(dirname "$file")/$(echo "$basefile" | sed -r 's/([a-z0-9])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g') + mv "$file" "$newfile" +fi +done < <(find . -type f -name '*.ts' -print | grep -E '/[a-z]+[a-zA-Z]*\.ts$') +if [ ${#non_compliant_files[@]} -ne 0 ]; then +echo "The following files are not in kebab-case or snake_case:" +for file in "${non_compliant_files[@]}"; do + echo " - $file" +done +exit 1 +fi \ No newline at end of file diff --git a/package.json b/package.json index 8c4e495a..865c69d9 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "lint-staged": { "*.ts": [ "yarn prettier --write", - "eslint --fix" + "eslint --fix", + "bash .github/workflows/scripts/kebab-case.sh" ], "src/**.{ts,json}": [ "cspell" From dae1037bb394e2a36a5c1d2e6b1951688872fc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 04:10:07 +0900 Subject: [PATCH 06/14] feat: update imports in kebablize --- .github/workflows/scripts/kebabalize.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scripts/kebabalize.sh b/.github/workflows/scripts/kebabalize.sh index 467c6e37..2f86b603 100755 --- a/.github/workflows/scripts/kebabalize.sh +++ b/.github/workflows/scripts/kebabalize.sh @@ -18,9 +18,12 @@ if $ignoreFile; then elif ! echo "$basefile" | grep -q -E "^([a-z0-9]+-)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$|^([a-z0-9]+_)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$"; then non_compliant_files+=("$file") echo "::warning file=$file::This file is not in kebab-case or snake_case" - # Dry run: print the rename command without executing it - newfile=$(dirname "$file")/$(echo "$basefile" | sed -r 's/([a-z0-9])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g') + # Rename the file + newbasefile=$(echo "$basefile" | sed -r 's/([a-z0-9])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g') + newfile=$(dirname "$file")/$newbasefile mv "$file" "$newfile" + # Update import statements in all TypeScript files + find . -name '*.ts' -exec sed -i "s|$basefile|$newbasefile|g" {} \; fi done < <(find . -type f -name '*.ts' -print | grep -E '/[a-z]+[a-zA-Z]*\.ts$') if [ ${#non_compliant_files[@]} -ne 0 ]; then From 0ff7042df7d6c6a29676bbf1e1d61a1061bab6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 04:11:31 +0900 Subject: [PATCH 07/14] feat: mac support --- .github/workflows/scripts/kebabalize-mac.sh | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100755 .github/workflows/scripts/kebabalize-mac.sh diff --git a/.github/workflows/scripts/kebabalize-mac.sh b/.github/workflows/scripts/kebabalize-mac.sh new file mode 100755 index 00000000..ef7fc4d8 --- /dev/null +++ b/.github/workflows/scripts/kebabalize-mac.sh @@ -0,0 +1,35 @@ +#!/bin/bash +non_compliant_files=() +ignoreList=("^\.\/.git" "^\.\/\..*" "^\.\/[^\/]*$") +while IFS= read -r line; do +ignoreList+=(".*$line") +done < .gitignore +while read -r file; do +basefile=$(basename "$file") +ignoreFile=false +for pattern in "${ignoreList[@]}"; do + if [[ "$file" =~ $pattern ]]; then + ignoreFile=true + break + fi +done +if $ignoreFile; then + continue +elif ! echo "$basefile" | grep -q -E "^([a-z0-9]+-)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$|^([a-z0-9]+_)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$"; then + non_compliant_files+=("$file") + echo "::warning file=$file::This file is not in kebab-case or snake_case" + # Rename the file + newbasefile=$(echo "$basefile" | sed -r 's/([a-z0-9])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g') + newfile=$(dirname "$file")/$newbasefile + mv "$file" "$newfile" + # Update import statements in all TypeScript files + find . -name '*.ts' -exec sed -i '' "s|$basefile|$newbasefile|g" {} \; +fi +done < <(find . -type f -name '*.ts' -print | grep -E '/[a-z]+[a-zA-Z]*\.ts$') +if [ ${#non_compliant_files[@]} -ne 0 ]; then +echo "The following files are not in kebab-case or snake_case:" +for file in "${non_compliant_files[@]}"; do + echo " - $file" +done +exit 1 +fi \ No newline at end of file From f4c71ec4e20cba9acd9fa8333767f3e684e931d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 04:14:11 +0900 Subject: [PATCH 08/14] revert: kebabalize only files names --- .github/workflows/scripts/kebabalize-mac.sh | 35 --------------------- .github/workflows/scripts/kebabalize.sh | 6 +--- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100755 .github/workflows/scripts/kebabalize-mac.sh diff --git a/.github/workflows/scripts/kebabalize-mac.sh b/.github/workflows/scripts/kebabalize-mac.sh deleted file mode 100755 index ef7fc4d8..00000000 --- a/.github/workflows/scripts/kebabalize-mac.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -non_compliant_files=() -ignoreList=("^\.\/.git" "^\.\/\..*" "^\.\/[^\/]*$") -while IFS= read -r line; do -ignoreList+=(".*$line") -done < .gitignore -while read -r file; do -basefile=$(basename "$file") -ignoreFile=false -for pattern in "${ignoreList[@]}"; do - if [[ "$file" =~ $pattern ]]; then - ignoreFile=true - break - fi -done -if $ignoreFile; then - continue -elif ! echo "$basefile" | grep -q -E "^([a-z0-9]+-)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$|^([a-z0-9]+_)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$"; then - non_compliant_files+=("$file") - echo "::warning file=$file::This file is not in kebab-case or snake_case" - # Rename the file - newbasefile=$(echo "$basefile" | sed -r 's/([a-z0-9])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g') - newfile=$(dirname "$file")/$newbasefile - mv "$file" "$newfile" - # Update import statements in all TypeScript files - find . -name '*.ts' -exec sed -i '' "s|$basefile|$newbasefile|g" {} \; -fi -done < <(find . -type f -name '*.ts' -print | grep -E '/[a-z]+[a-zA-Z]*\.ts$') -if [ ${#non_compliant_files[@]} -ne 0 ]; then -echo "The following files are not in kebab-case or snake_case:" -for file in "${non_compliant_files[@]}"; do - echo " - $file" -done -exit 1 -fi \ No newline at end of file diff --git a/.github/workflows/scripts/kebabalize.sh b/.github/workflows/scripts/kebabalize.sh index 2f86b603..47892d48 100755 --- a/.github/workflows/scripts/kebabalize.sh +++ b/.github/workflows/scripts/kebabalize.sh @@ -18,12 +18,8 @@ if $ignoreFile; then elif ! echo "$basefile" | grep -q -E "^([a-z0-9]+-)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$|^([a-z0-9]+_)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$"; then non_compliant_files+=("$file") echo "::warning file=$file::This file is not in kebab-case or snake_case" - # Rename the file - newbasefile=$(echo "$basefile" | sed -r 's/([a-z0-9])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g') - newfile=$(dirname "$file")/$newbasefile + newfile=$(dirname "$file")/$(echo "$basefile" | sed -r 's/([a-z0-9])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g') mv "$file" "$newfile" - # Update import statements in all TypeScript files - find . -name '*.ts' -exec sed -i "s|$basefile|$newbasefile|g" {} \; fi done < <(find . -type f -name '*.ts' -print | grep -E '/[a-z]+[a-zA-Z]*\.ts$') if [ ${#non_compliant_files[@]} -ne 0 ]; then From ae73eee5299c2f3c4cbde9ef0b9046d4e9699b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 04:23:43 +0900 Subject: [PATCH 09/14] chore: kebabed --- static/scripts/audit-report/audit.ts | 4 +- .../utils/{blockInfo.ts => block-info.ts} | 0 .../{getTransaction.ts => get-transaction.ts} | 2 +- .../{nftRewardAbi.ts => nft-reward-abi.ts} | 0 static/scripts/rewards/helpers.ts | 83 +------------------ ...ination.ts => claim-rewards-pagination.ts} | 4 +- .../read-claim-data-from-url.ts | 6 +- ...erTransaction.ts => render-transaction.ts} | 4 +- .../{setPagination.ts => set-pagination.ts} | 0 ...rovider.ts => get-fastest-rpc-provider.ts} | 0 ...malProvider.ts => get-optimal-provider.ts} | 3 +- ...Performance.ts => test-rpc-performance.ts} | 0 .../web3/{addNetwork.ts => add-network.ts} | 0 .../{connectWallet.ts => connect-wallet.ts} | 0 static/scripts/rewards/web3/erc20-permit.ts | 4 +- static/scripts/rewards/web3/erc721-permit.ts | 6 +- ...ork.ts => handle-if-on-correct-network.ts} | 0 ...ctNetwork.ts => not-on-correct-network.ts} | 0 .../{switchNetwork.ts => switch-network.ts} | 0 ...ntNetwork.ts => verify-current-network.ts} | 2 - 20 files changed, 18 insertions(+), 100 deletions(-) rename static/scripts/audit-report/utils/{blockInfo.ts => block-info.ts} (100%) rename static/scripts/audit-report/utils/{getTransaction.ts => get-transaction.ts} (94%) rename static/scripts/rewards/abis/{nftRewardAbi.ts => nft-reward-abi.ts} (100%) rename static/scripts/rewards/render-transaction/{claimRewardsPagination.ts => claim-rewards-pagination.ts} (92%) rename static/scripts/rewards/render-transaction/{renderTransaction.ts => render-transaction.ts} (96%) rename static/scripts/rewards/render-transaction/{setPagination.ts => set-pagination.ts} (100%) rename static/scripts/rewards/rpc-optimization/{getFastestRpcProvider.ts => get-fastest-rpc-provider.ts} (100%) rename static/scripts/rewards/rpc-optimization/{getOptimalProvider.ts => get-optimal-provider.ts} (85%) rename static/scripts/rewards/rpc-optimization/{testRpcPerformance.ts => test-rpc-performance.ts} (100%) rename static/scripts/rewards/web3/{addNetwork.ts => add-network.ts} (100%) rename static/scripts/rewards/web3/{connectWallet.ts => connect-wallet.ts} (100%) rename static/scripts/rewards/web3/{handleIfOnCorrectNetwork.ts => handle-if-on-correct-network.ts} (100%) rename static/scripts/rewards/web3/{notOnCorrectNetwork.ts => not-on-correct-network.ts} (100%) rename static/scripts/rewards/web3/{switchNetwork.ts => switch-network.ts} (100%) rename static/scripts/rewards/web3/{verifyCurrentNetwork.ts => verify-current-network.ts} (88%) diff --git a/static/scripts/audit-report/audit.ts b/static/scripts/audit-report/audit.ts index ed9f862a..5a6a150b 100644 --- a/static/scripts/audit-report/audit.ts +++ b/static/scripts/audit-report/audit.ts @@ -7,13 +7,13 @@ import GoDB from "godb"; import { permit2Abi } from "../rewards/abis"; import { Chain, ChainScan, DATABASE_NAME, NULL_HASH, NULL_ID } from "./constants"; import { + RateLimitOptions, getCurrency, getGitHubUrlPartsArray, getOptimalRPC, getRandomAPIKey, populateTable, primaryRateLimitHandler, - RateLimitOptions, secondaryRateLimitHandler, } from "./helpers"; import { @@ -29,7 +29,7 @@ import { StandardInterface, TxData, } from "./types"; -import { getTxInfo } from "./utils/getTransaction"; +import { getTxInfo } from "./utils/get-transaction"; declare const SUPABASE_URL: string; declare const SUPABASE_ANON_KEY: string; diff --git a/static/scripts/audit-report/utils/blockInfo.ts b/static/scripts/audit-report/utils/block-info.ts similarity index 100% rename from static/scripts/audit-report/utils/blockInfo.ts rename to static/scripts/audit-report/utils/block-info.ts diff --git a/static/scripts/audit-report/utils/getTransaction.ts b/static/scripts/audit-report/utils/get-transaction.ts similarity index 94% rename from static/scripts/audit-report/utils/getTransaction.ts rename to static/scripts/audit-report/utils/get-transaction.ts index e165e21b..d230c710 100644 --- a/static/scripts/audit-report/utils/getTransaction.ts +++ b/static/scripts/audit-report/utils/get-transaction.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { Chain } from "../constants"; import { Transaction } from "../types/transaction"; -import { getBlockInfo, updateBlockInfo } from "./blockInfo"; +import { getBlockInfo, updateBlockInfo } from "./block-info"; export async function getTxInfo(hash: string, url: string, chain: Chain): Promise { try { diff --git a/static/scripts/rewards/abis/nftRewardAbi.ts b/static/scripts/rewards/abis/nft-reward-abi.ts similarity index 100% rename from static/scripts/rewards/abis/nftRewardAbi.ts rename to static/scripts/rewards/abis/nft-reward-abi.ts diff --git a/static/scripts/rewards/helpers.ts b/static/scripts/rewards/helpers.ts index c1cb468a..f2e9e0c4 100644 --- a/static/scripts/rewards/helpers.ts +++ b/static/scripts/rewards/helpers.ts @@ -1,93 +1,14 @@ import { JsonRpcProvider } from "@ethersproject/providers"; -import axios from "axios"; import { Contract, ethers } from "ethers"; import { erc20Abi } from "./abis"; import { AppState } from "./app-state"; -import { networkRpcs } from "./constants"; - -type DataType = { - jsonrpc: string; - id: number; - result: { - number: string; - timestamp: string; - hash: string; - }; -}; - -function verifyBlock(data: DataType) { - try { - const { jsonrpc, id, result } = data; - const { number, timestamp, hash } = result; - return jsonrpc === "2.0" && id === 1 && parseInt(number, 16) > 0 && parseInt(timestamp, 16) > 0 && hash.match(/[0-9|a-f|A-F|x]/gm)?.join("").length === 66; - } catch (error) { - return false; - } -} - -const RPC_BODY = JSON.stringify({ - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: ["latest", false], - id: 1, -}); - -const RPC_HEADER = { - "Content-Type": "application/json", -}; +import { getFastestRpcProvider } from "./rpc-optimization/get-fastest-rpc-provider"; +import { testRpcPerformance } from "./rpc-optimization/test-rpc-performance"; export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { return new ethers.Contract(contractAddress, erc20Abi, provider); } -export async function testRpcPerformance(networkId: number) { - const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); - - const promises = networkRpcs[networkId].map(async (baseURL: string) => { - try { - const startTime = performance.now(); - const API = axios.create({ - baseURL, - headers: RPC_HEADER, - }); - - const { data } = await API.post("", RPC_BODY); - const endTime = performance.now(); - const latency = endTime - startTime; - if (verifyBlock(data)) { - // Save the latency in localStorage - latencies[baseURL] = latency; - } else { - // Save -1 in localStorage to indicate an error - latencies[baseURL] = -1; - } - } catch (error) { - // Save -1 in localStorage to indicate an error - latencies[baseURL] = -1; - } - }); - - await Promise.all(promises); - localStorage.setItem("rpcLatencies", JSON.stringify(latencies)); -} - -export function getFastestRpcProvider(networkId: number) { - const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); - - // Filter out latencies with a value of less than 0 because -1 means it failed - // Also filter out latencies that do not belong to the desired network - const validLatencies = Object.entries(latencies).filter(([key, latency]) => latency >= 0 && key.startsWith(`${networkId}_`)); - - // Get all valid latencies from localStorage and find the fastest RPC - const sortedLatencies = validLatencies.sort((a, b) => a[1] - b[1]); - const optimalRPC = sortedLatencies[0][0].split("_").slice(1).join("_"); // Remove the network ID from the key - - return new ethers.providers.JsonRpcProvider(optimalRPC, { - name: optimalRPC, - chainId: networkId, - }); -} - let optimalProvider: ethers.providers.JsonRpcProvider | null = null; let isTestStarted = false; diff --git a/static/scripts/rewards/render-transaction/claimRewardsPagination.ts b/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts similarity index 92% rename from static/scripts/rewards/render-transaction/claimRewardsPagination.ts rename to static/scripts/rewards/render-transaction/claim-rewards-pagination.ts index 30738c09..60f6d9c1 100644 --- a/static/scripts/rewards/render-transaction/claimRewardsPagination.ts +++ b/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts @@ -1,8 +1,8 @@ import { app } from "../app-state"; import { claimButton } from "../toaster"; import { table } from "./read-claim-data-from-url"; -import { renderTransaction } from "./renderTransaction"; -import { setPagination } from "./setPagination"; +import { renderTransaction } from "./render-transaction"; +import { setPagination } from "./set-pagination"; import { removeAllEventListeners } from "./utils"; export function claimRewardsPagination(rewardsCount: HTMLElement) { 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 b363652b..e40775f1 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 @@ -1,11 +1,11 @@ import { Type } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; import { app } from "../app-state"; -import { getOptimalProvider } from "../rpc-optimization/getOptimalProvider"; -import { claimRewardsPagination } from "./claimRewardsPagination"; -import { renderTransaction } from "./renderTransaction"; import { setClaimMessage } from "./set-claim-message"; import { claimTxT } from "./tx-type"; +import { getOptimalProvider } from "../helpers"; +import { claimRewardsPagination } from "./claim-rewards-pagination"; +import { renderTransaction } from "./render-transaction"; export const table = document.getElementsByTagName(`table`)[0]; const urlParams = new URLSearchParams(window.location.search); diff --git a/static/scripts/rewards/render-transaction/renderTransaction.ts b/static/scripts/rewards/render-transaction/render-transaction.ts similarity index 96% rename from static/scripts/rewards/render-transaction/renderTransaction.ts rename to static/scripts/rewards/render-transaction/render-transaction.ts index 76ef798b..e09da2c3 100644 --- a/static/scripts/rewards/render-transaction/renderTransaction.ts +++ b/static/scripts/rewards/render-transaction/render-transaction.ts @@ -3,11 +3,11 @@ import { networkExplorers } from "../constants"; import { claimButton, hideClaimButton, resetClaimButton } from "../toaster"; import { claimErc20PermitHandlerWrapper, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; import { claimErc721PermitHandler } from "../web3/erc721-permit"; -import { verifyCurrentNetwork } from "../web3/verifyCurrentNetwork"; +import { verifyCurrentNetwork } from "../web3/verify-current-network"; import { insertErc20PermitTableData, insertErc721PermitTableData } from "./insert-table-data"; import { renderEnsName } from "./render-ens-name"; import { renderNftSymbol, renderTokenSymbol } from "./render-token-symbol"; -import { setPagination } from "./setPagination"; +import { setPagination } from "./set-pagination"; type Success = boolean; diff --git a/static/scripts/rewards/render-transaction/setPagination.ts b/static/scripts/rewards/render-transaction/set-pagination.ts similarity index 100% rename from static/scripts/rewards/render-transaction/setPagination.ts rename to static/scripts/rewards/render-transaction/set-pagination.ts diff --git a/static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts b/static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.ts similarity index 100% rename from static/scripts/rewards/rpc-optimization/getFastestRpcProvider.ts rename to static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.ts diff --git a/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts similarity index 85% rename from static/scripts/rewards/rpc-optimization/getOptimalProvider.ts rename to static/scripts/rewards/rpc-optimization/get-optimal-provider.ts index a6906a95..9818d91e 100644 --- a/static/scripts/rewards/rpc-optimization/getOptimalProvider.ts +++ b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts @@ -1,7 +1,6 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { AppState } from "../app-state"; -import { getFastestRpcProvider } from "./getFastestRpcProvider"; -import { testRpcPerformance } from "./testRpcPerformance"; +import { testRpcPerformance, getFastestRpcProvider } from "../helpers"; let optimalProvider: JsonRpcProvider | null = null; let isTestStarted = false; diff --git a/static/scripts/rewards/rpc-optimization/testRpcPerformance.ts b/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts similarity index 100% rename from static/scripts/rewards/rpc-optimization/testRpcPerformance.ts rename to static/scripts/rewards/rpc-optimization/test-rpc-performance.ts diff --git a/static/scripts/rewards/web3/addNetwork.ts b/static/scripts/rewards/web3/add-network.ts similarity index 100% rename from static/scripts/rewards/web3/addNetwork.ts rename to static/scripts/rewards/web3/add-network.ts diff --git a/static/scripts/rewards/web3/connectWallet.ts b/static/scripts/rewards/web3/connect-wallet.ts similarity index 100% rename from static/scripts/rewards/web3/connectWallet.ts rename to static/scripts/rewards/web3/connect-wallet.ts diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index f295048a..c925f2d9 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -5,11 +5,11 @@ import { app } from "../app-state"; import { permit2Address } from "../constants"; import invalidateButton from "../invalidate-component"; import { tokens } from "../render-transaction/render-token-symbol"; -import { renderTransaction } from "../render-transaction/renderTransaction"; import { Erc20Permit } from "../render-transaction/tx-type"; import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; -import { connectWallet } from "./connectWallet"; +import { renderTransaction } from "../render-transaction/render-transaction"; +import { connectWallet } from "./connect-wallet"; export async function fetchTreasury( permit: Erc20Permit, diff --git a/static/scripts/rewards/web3/erc721-permit.ts b/static/scripts/rewards/web3/erc721-permit.ts index 9272d3fc..ee01961b 100644 --- a/static/scripts/rewards/web3/erc721-permit.ts +++ b/static/scripts/rewards/web3/erc721-permit.ts @@ -1,11 +1,11 @@ import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers"; import { ethers } from "ethers"; -import { nftRewardAbi } from "../abis/nftRewardAbi"; import { app } from "../app-state"; -import { renderTransaction } from "../render-transaction/renderTransaction"; import { Erc721Permit } from "../render-transaction/tx-type"; import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; -import { connectWallet } from "./connectWallet"; +import { nftRewardAbi } from "../abis/nft-reward-abi"; +import { renderTransaction } from "../render-transaction/render-transaction"; +import { connectWallet } from "./connect-wallet"; export function claimErc721PermitHandler(permit: Erc721Permit) { return async function claimButtonHandler() { diff --git a/static/scripts/rewards/web3/handleIfOnCorrectNetwork.ts b/static/scripts/rewards/web3/handle-if-on-correct-network.ts similarity index 100% rename from static/scripts/rewards/web3/handleIfOnCorrectNetwork.ts rename to static/scripts/rewards/web3/handle-if-on-correct-network.ts diff --git a/static/scripts/rewards/web3/notOnCorrectNetwork.ts b/static/scripts/rewards/web3/not-on-correct-network.ts similarity index 100% rename from static/scripts/rewards/web3/notOnCorrectNetwork.ts rename to static/scripts/rewards/web3/not-on-correct-network.ts diff --git a/static/scripts/rewards/web3/switchNetwork.ts b/static/scripts/rewards/web3/switch-network.ts similarity index 100% rename from static/scripts/rewards/web3/switchNetwork.ts rename to static/scripts/rewards/web3/switch-network.ts diff --git a/static/scripts/rewards/web3/verifyCurrentNetwork.ts b/static/scripts/rewards/web3/verify-current-network.ts similarity index 88% rename from static/scripts/rewards/web3/verifyCurrentNetwork.ts rename to static/scripts/rewards/web3/verify-current-network.ts index f50a8340..e0328a63 100644 --- a/static/scripts/rewards/web3/verifyCurrentNetwork.ts +++ b/static/scripts/rewards/web3/verify-current-network.ts @@ -1,8 +1,6 @@ import { ethers } from "ethers"; import invalidateButton from "../invalidate-component"; import { loadingClaimButton, toaster } from "../toaster"; -import { handleIfOnCorrectNetwork } from "./handleIfOnCorrectNetwork"; -import { notOnCorrectNetwork } from "./notOnCorrectNetwork"; // verifyCurrentNetwork checks if the user is on the correct network and displays an error if not From c065b8c60a686f847959c596b3691b66724685fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 04:56:28 +0900 Subject: [PATCH 10/14] feat: fast claims --- .cspell.json | 26 ++- static/scripts/rewards/app-state.ts | 12 +- static/scripts/rewards/helpers.ts | 32 ---- .../read-claim-data-from-url.ts | 3 +- .../rpc-optimization/get-optimal-provider.ts | 6 +- .../rpc-optimization/test-rpc-performance.ts | 2 +- static/scripts/rewards/web3/erc20-permit.ts | 165 +++++++++++++++--- .../rewards/web3/get-erc20-contract.ts | 7 + .../rewards/web3/not-on-correct-network.ts | 2 +- static/scripts/rewards/web3/switch-network.ts | 2 +- .../rewards/web3/verify-current-network.ts | 2 + 11 files changed, 175 insertions(+), 84 deletions(-) delete mode 100644 static/scripts/rewards/helpers.ts create mode 100644 static/scripts/rewards/web3/get-erc20-contract.ts diff --git a/.cspell.json b/.cspell.json index 628308ff..9f1a72b2 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,7 +1,13 @@ { "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", "version": "0.2", - "ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log", "/lib"], + "ignorePaths": [ + "**/*.json", + "**/*.css", + "node_modules", + "**/*.log", + "/lib" + ], "useGitignore": true, "language": "en", "words": [ @@ -9,6 +15,7 @@ "binsec", "chainlist", "cirip", + "Claimability", "dataurl", "devpool", "ethersproject", @@ -37,7 +44,18 @@ "XDAI", "xmark" ], - "dictionaries": ["typescript", "node", "software-terms", "html"], - "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], - "ignoreRegExpList": ["[0-9a-fA-F]{6}"] + "dictionaries": [ + "typescript", + "node", + "software-terms", + "html" + ], + "import": [ + "@cspell/dict-typescript/cspell-ext.json", + "@cspell/dict-node/cspell-ext.json", + "@cspell/dict-software-terms" + ], + "ignoreRegExpList": [ + "[0-9a-fA-F]{6}" + ] } diff --git a/static/scripts/rewards/app-state.ts b/static/scripts/rewards/app-state.ts index 3519ae8e..002b0ee6 100644 --- a/static/scripts/rewards/app-state.ts +++ b/static/scripts/rewards/app-state.ts @@ -5,16 +5,10 @@ import { ClaimTx } from "./render-transaction/tx-type"; export class AppState { public claims: ClaimTx[] = []; private _provider!: JsonRpcProvider; - private _networkId: number | null = null; private _currentIndex = 0; - // private _networkRpc: string; get networkId(): number | null { - return this.transaction.networkId; - } - - set networkId(value: number) { - this._networkId = value; + return this.transaction?.networkId || null; } get provider(): JsonRpcProvider { @@ -37,10 +31,6 @@ export class AppState { return this.transaction?.networkId; } - get networkRpc(): string { - return this._networkRpc; - } - get currentExplorerUrl(): string { if (!this.transaction) { return "https://etherscan.io"; diff --git a/static/scripts/rewards/helpers.ts b/static/scripts/rewards/helpers.ts deleted file mode 100644 index f2e9e0c4..00000000 --- a/static/scripts/rewards/helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; -import { Contract, ethers } from "ethers"; -import { erc20Abi } from "./abis"; -import { AppState } from "./app-state"; -import { getFastestRpcProvider } from "./rpc-optimization/get-fastest-rpc-provider"; -import { testRpcPerformance } from "./rpc-optimization/test-rpc-performance"; - -export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { - return new ethers.Contract(contractAddress, erc20Abi, provider); -} - -let optimalProvider: ethers.providers.JsonRpcProvider | null = null; - -let isTestStarted = false; -let isTestCompleted = false; - -export async function getOptimalProvider(app: AppState): Promise { - const networkId = app.transactionNetworkId; - if (!networkId) throw new Error("Network ID not found"); - - if (!isTestCompleted && !isTestStarted) { - isTestStarted = true; - await testRpcPerformance(networkId); - isTestCompleted = true; - } - - if (!optimalProvider) { - optimalProvider = getFastestRpcProvider(networkId); - } - console.trace({ optimalProvider }); - return optimalProvider; -} 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 e40775f1..327e4a65 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,9 +3,9 @@ import { Value } from "@sinclair/typebox/value"; import { app } from "../app-state"; import { setClaimMessage } from "./set-claim-message"; import { claimTxT } from "./tx-type"; -import { getOptimalProvider } from "../helpers"; import { claimRewardsPagination } from "./claim-rewards-pagination"; import { renderTransaction } from "./render-transaction"; +import { getOptimalProvider } from "../rpc-optimization/get-optimal-provider"; export const table = document.getElementsByTagName(`table`)[0]; const urlParams = new URLSearchParams(window.location.search); @@ -35,7 +35,6 @@ function decodeClaimData(base64encodedTxData: string) { try { const claimTxs = Value.Decode(Type.Array(claimTxT), JSON.parse(atob(base64encodedTxData))); app.claims = claimTxs; - app.networkId = app.claims[0].networkId; } catch (error) { console.error(error); setClaimMessage({ type: "Error", message: `Invalid claim data passed in URL` }); diff --git a/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts index 9818d91e..76910d92 100644 --- a/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts +++ b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts @@ -1,8 +1,9 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { AppState } from "../app-state"; -import { testRpcPerformance, getFastestRpcProvider } from "../helpers"; +import { getFastestRpcProvider } from "./get-fastest-rpc-provider"; +import { testRpcPerformance } from "./test-rpc-performance"; -let optimalProvider: JsonRpcProvider | null = null; +let optimalProvider: JsonRpcProvider; let isTestStarted = false; let isTestCompleted = false; @@ -20,7 +21,6 @@ export async function getOptimalProvider(app: AppState): Promise { const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, provider); const { wordPos, bitPos } = nonceBitmap(BigNumber.from(permit.permit.nonce)); - const bitmap = await permit2Contract.nonceBitmap(permit.owner, wordPos); + + const bitmap = await permit2Contract.nonceBitmap(permit.owner, wordPos).catch((error) => { + console.error("Error in nonceBitmap method: ", error); + throw error; + }); const bit = BigNumber.from(1).shl(bitPos); const flipped = BigNumber.from(bitmap).xor(bit); diff --git a/static/scripts/rewards/web3/get-erc20-contract.ts b/static/scripts/rewards/web3/get-erc20-contract.ts new file mode 100644 index 00000000..f9145644 --- /dev/null +++ b/static/scripts/rewards/web3/get-erc20-contract.ts @@ -0,0 +1,7 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Contract, ethers } from "ethers"; +import { erc20Abi } from "../abis"; + +export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { + return new ethers.Contract(contractAddress, erc20Abi, provider); +} diff --git a/static/scripts/rewards/web3/not-on-correct-network.ts b/static/scripts/rewards/web3/not-on-correct-network.ts index cdbd9478..d1d26bbd 100644 --- a/static/scripts/rewards/web3/not-on-correct-network.ts +++ b/static/scripts/rewards/web3/not-on-correct-network.ts @@ -2,7 +2,7 @@ import { ethers } from "ethers"; import { getNetworkName } from "../constants"; import invalidateButton from "../invalidate-component"; import { loadingClaimButton, toaster } from "../toaster"; -import { switchNetwork } from "./switchNetwork"; +import { switchNetwork } from "./switch-network"; export function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number, web3provider: ethers.providers.Web3Provider) { if (currentNetworkId !== desiredNetworkId) { diff --git a/static/scripts/rewards/web3/switch-network.ts b/static/scripts/rewards/web3/switch-network.ts index 9ab2cac5..51ddae7c 100644 --- a/static/scripts/rewards/web3/switch-network.ts +++ b/static/scripts/rewards/web3/switch-network.ts @@ -1,5 +1,5 @@ import { ethers } from "ethers"; -import { addNetwork } from "./addNetwork"; +import { addNetwork } from "./add-network"; export async function switchNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { try { diff --git a/static/scripts/rewards/web3/verify-current-network.ts b/static/scripts/rewards/web3/verify-current-network.ts index e0328a63..3f723fad 100644 --- a/static/scripts/rewards/web3/verify-current-network.ts +++ b/static/scripts/rewards/web3/verify-current-network.ts @@ -1,6 +1,8 @@ import { ethers } from "ethers"; import invalidateButton from "../invalidate-component"; import { loadingClaimButton, toaster } from "../toaster"; +import { handleIfOnCorrectNetwork } from "./handle-if-on-correct-network"; +import { notOnCorrectNetwork } from "./not-on-correct-network"; // verifyCurrentNetwork checks if the user is on the correct network and displays an error if not From f9c2b87ddb864ec30fb2e1afd7b97054a127e25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 05:28:01 +0900 Subject: [PATCH 11/14] fix: post claim render bug --- .../scripts/rewards/render-transaction/insert-table-data.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/scripts/rewards/render-transaction/insert-table-data.ts b/static/scripts/rewards/render-transaction/insert-table-data.ts index 0a487670..e19a306f 100644 --- a/static/scripts/rewards/render-transaction/insert-table-data.ts +++ b/static/scripts/rewards/render-transaction/insert-table-data.ts @@ -88,8 +88,8 @@ function renderTokenFields(tokenAddress: string, explorerUrl: string) { } function renderToFields(receiverAddress: string, explorerUrl: string) { - const toFull = document.querySelector("#To .full") as Element; - const toShort = document.querySelector("#To .short") as Element; + const toFull = document.querySelector("#rewardRecipient .full") as Element; + const toShort = document.querySelector("#rewardRecipient .short") as Element; toFull.innerHTML = `
    ${receiverAddress}
    `; toShort.innerHTML = `
    ${shortenAddress(receiverAddress)}
    `; From 2c8f5ca1bad7215f0f0bc905e766d9ac3389bf72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 06:07:02 +0900 Subject: [PATCH 12/14] fix: toaster errors on claims for erc20 --- .cspell.json | 25 ++------- .../render-transaction/render-transaction.ts | 8 ++- static/scripts/rewards/toaster.ts | 30 +++++++---- static/scripts/rewards/web3/erc20-permit.ts | 52 ++++++++++++------- static/styles/rewards/claim-table.css | 1 + tsconfig.json | 2 +- 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/.cspell.json b/.cspell.json index 9f1a72b2..d54182ac 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,13 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", "version": "0.2", - "ignorePaths": [ - "**/*.json", - "**/*.css", - "node_modules", - "**/*.log", - "/lib" - ], + "ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log", "/lib"], "useGitignore": true, "language": "en", "words": [ @@ -44,18 +38,7 @@ "XDAI", "xmark" ], - "dictionaries": [ - "typescript", - "node", - "software-terms", - "html" - ], - "import": [ - "@cspell/dict-typescript/cspell-ext.json", - "@cspell/dict-node/cspell-ext.json", - "@cspell/dict-software-terms" - ], - "ignoreRegExpList": [ - "[0-9a-fA-F]{6}" - ] + "dictionaries": ["typescript", "node", "software-terms", "html"], + "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], + "ignoreRegExpList": ["[0-9a-fA-F]{6}"] } diff --git a/static/scripts/rewards/render-transaction/render-transaction.ts b/static/scripts/rewards/render-transaction/render-transaction.ts index e09da2c3..2cd78e68 100644 --- a/static/scripts/rewards/render-transaction/render-transaction.ts +++ b/static/scripts/rewards/render-transaction/render-transaction.ts @@ -57,7 +57,13 @@ export async function renderTransaction(nextTx?: boolean): Promise { generateInvalidatePermitAdminControl(app.transaction).catch(console.error); - claimButton.element.addEventListener("click", claimErc20PermitHandlerWrapper(app.transaction)); + const wrapped = claimErc20PermitHandlerWrapper(app.transaction); + + claimButton.element.addEventListener("click", function () { + this.classList.add("clicked"); + wrapped(); + this.classList.remove("clicked"); + }); } else if (app.transaction.type === "erc721-permit") { const requestedAmountElement = insertErc721PermitTableData(app.transaction, table); table.setAttribute(`data-claim`, "ok"); diff --git a/static/scripts/rewards/toaster.ts b/static/scripts/rewards/toaster.ts index 9294e720..341eb7a2 100644 --- a/static/scripts/rewards/toaster.ts +++ b/static/scripts/rewards/toaster.ts @@ -73,17 +73,27 @@ export function hideClaimButton() { claimButton.element.classList.remove("show-cl"); } -type Err = { stack?: unknown; reason?: string } extends Error ? Error : { stack?: unknown; reason?: string }; - -export function errorToast(error: Err, errorMessage?: string) { - delete error.stack; - const errorData = JSON.stringify(error, null, 2); +export function errorToast(error: MetaMaskError, errorMessage?: string) { + // If a custom error message is provided, use it if (errorMessage) { toaster.create("error", errorMessage); - } else if (error?.reason) { - // parse error data to get error message - const parsedError = JSON.parse(errorData); - const _errorMessage = parsedError?.error?.message ?? parsedError?.reason; - toaster.create("error", _errorMessage); + return; } + + toaster.create("error", error.reason); } + +export type MetaMaskError = { + reason: "user rejected transaction"; + code: "ACTION_REJECTED"; + action: "sendTransaction"; + transaction: { + data: "0x30f28b7a000000000000000000000000e91d153e0b41518a2ce8dd3d7944fa863463a97d0000000000000000000000000000000000000000000000056bc75e2d631000008defcc81869c636cbdd4c06c9247db239d4368d5e14d39793cfc2047c43d9532ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000004007ce2083c7f3e18097aeb3a39bb8ec149a341d0000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000044ca15db101fd1c194467db6af0c67c6bbf4ab510000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004165db9eaebb7ea1854531d5e23305ee72481845b6df34c458fbc4e5a0422c4c9d36a674a92f3c877a8ae7f0990e0f1b1e5a21d904d2be34fa75aa71905d940a451b00000000000000000000000000000000000000000000000000000000000000"; + to: "0x000000000022D473030F116dDEE9F6B43aC78BA3"; + from: "0x4007CE2083c7F3E18097aeB3A39bb8eC149a341d"; + gasLimit: { + type: "BigNumber"; + hex: "0x012c5a"; + }; + }; +}; diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index 3673cdc0..c01991ae 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -7,7 +7,7 @@ import invalidateButton from "../invalidate-component"; import { tokens } from "../render-transaction/render-token-symbol"; import { Erc20Permit } from "../render-transaction/tx-type"; import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; -import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; +import { MetaMaskError, claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; import { renderTransaction } from "../render-transaction/render-transaction"; import { connectWallet } from "./connect-wallet"; @@ -51,8 +51,9 @@ async function connectToWallet() { } } catch (error: unknown) { if (error instanceof Error) { - console.error("Error in connectWallet: ", error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + console.error("Error in connectWallet: ", e); + errorToast(e, e.reason); resetClaimButton(); } } @@ -65,8 +66,9 @@ async function checkPermitClaimability(permit: Erc20Permit, signer: JsonRpcSigne isPermitClaimable = await checkPermitClaimable(permit, signer, app.provider); } catch (error: unknown) { if (error instanceof Error) { - console.error("Error in checkPermitClaimable: ", error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + console.error("Error in checkPermitClaimable: ", e); + errorToast(e, e.reason); resetClaimButton(); } } @@ -79,8 +81,9 @@ async function createEthersContract(signer: JsonRpcSigner) { permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); } catch (error: unknown) { if (error instanceof Error) { - console.error("Error in creating ethers.Contract: ", error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + console.error("Error in creating ethers.Contract: ", e); + errorToast(e, e.reason); resetClaimButton(); } } @@ -88,18 +91,26 @@ async function createEthersContract(signer: JsonRpcSigner) { } async function transferFromPermit(permit2Contract: Contract, permit: Erc20Permit) { - let tx; try { - tx = await permit2Contract.permitTransferFrom(permit.permit, permit.transferDetails, permit.owner, permit.signature); + const tx = await permit2Contract.permitTransferFrom(permit.permit, permit.transferDetails, permit.owner, permit.signature); toaster.create("info", `Transaction sent`); + return tx; } catch (error: unknown) { if (error instanceof Error) { - console.error("Error in permitTransferFrom: ", error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + // Check if the error message indicates a user rejection + if (e.code == "ACTION_REJECTED") { + // Handle the user rejection case + toaster.create("info", `Transaction was not sent because it was rejected by the user.`); + } else { + // Handle other errors + console.error("Error in permitTransferFrom: ", e); + errorToast(e, e.reason); + } resetClaimButton(); } + return null; } - return tx; } async function waitForTransaction(tx: TransactionResponse) { @@ -110,8 +121,9 @@ async function waitForTransaction(tx: TransactionResponse) { console.log(receipt.transactionHash); // @TODO: post to database } catch (error: unknown) { if (error instanceof Error) { - console.error("Error in tx.wait: ", error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + console.error("Error in tx.wait: ", e); + errorToast(e, e.reason); resetClaimButton(); } } @@ -123,8 +135,9 @@ async function renderTx() { await renderTransaction(); } catch (error: unknown) { if (error instanceof Error) { - console.error("Error in renderTransaction: ", error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + console.error("Error in renderTransaction: ", e); + errorToast(e, e.reason); resetClaimButton(); } } @@ -251,8 +264,9 @@ export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) await invalidateNonce(signer, permit.permit.nonce); } catch (error: unknown) { if (error instanceof Error) { - console.error(error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + console.error(e); + errorToast(e, e.reason); return; } } @@ -268,7 +282,7 @@ export async function isNonceClaimed(permit: Erc20Permit): Promise { const { wordPos, bitPos } = nonceBitmap(BigNumber.from(permit.permit.nonce)); - const bitmap = await permit2Contract.nonceBitmap(permit.owner, wordPos).catch((error) => { + const bitmap = await permit2Contract.nonceBitmap(permit.owner, wordPos).catch((error: MetaMaskError) => { console.error("Error in nonceBitmap method: ", error); throw error; }); diff --git a/static/styles/rewards/claim-table.css b/static/styles/rewards/claim-table.css index 121a462a..f080a769 100644 --- a/static/styles/rewards/claim-table.css +++ b/static/styles/rewards/claim-table.css @@ -14,6 +14,7 @@ justify-content: center; align-items: center; } + main > div { /* border-collapse: collapse; */ /* width: 100%; */ diff --git a/tsconfig.json b/tsconfig.json index f001a6da..31f00cd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,7 +42,7 @@ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ From b9f9c77cf7da2fd329fb13d983d487fe463dc6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 07:10:44 +0900 Subject: [PATCH 13/14] fix: loader animation on claim button --- .../render-transaction/render-transaction.ts | 13 ++----- static/scripts/rewards/toaster.ts | 24 ++++-------- static/scripts/rewards/web3/connect-wallet.ts | 3 +- static/scripts/rewards/web3/erc20-permit.ts | 14 ++----- static/scripts/rewards/web3/erc721-permit.ts | 12 ++---- .../web3/handle-if-on-correct-network.ts | 5 +-- .../rewards/web3/not-on-correct-network.ts | 4 +- .../rewards/web3/verify-current-network.ts | 5 +-- static/styles/rewards/claim-table.css | 37 ++++++++++--------- 9 files changed, 45 insertions(+), 72 deletions(-) diff --git a/static/scripts/rewards/render-transaction/render-transaction.ts b/static/scripts/rewards/render-transaction/render-transaction.ts index 2cd78e68..960230ab 100644 --- a/static/scripts/rewards/render-transaction/render-transaction.ts +++ b/static/scripts/rewards/render-transaction/render-transaction.ts @@ -1,6 +1,6 @@ import { app } from "../app-state"; import { networkExplorers } from "../constants"; -import { claimButton, hideClaimButton, resetClaimButton } from "../toaster"; +import { claimButton, hideLoader } from "../toaster"; import { claimErc20PermitHandlerWrapper, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; import { claimErc721PermitHandler } from "../web3/erc721-permit"; import { verifyCurrentNetwork } from "../web3/verify-current-network"; @@ -13,7 +13,6 @@ type Success = boolean; export async function renderTransaction(nextTx?: boolean): Promise { const table = document.getElementsByTagName(`table`)[0]; - resetClaimButton(); if (nextTx) { app.nextTx(); @@ -29,7 +28,7 @@ export async function renderTransaction(nextTx?: boolean): Promise { } if (!app.transaction) { - hideClaimButton(); + hideLoader(); return false; } @@ -57,13 +56,7 @@ export async function renderTransaction(nextTx?: boolean): Promise { generateInvalidatePermitAdminControl(app.transaction).catch(console.error); - const wrapped = claimErc20PermitHandlerWrapper(app.transaction); - - claimButton.element.addEventListener("click", function () { - this.classList.add("clicked"); - wrapped(); - this.classList.remove("clicked"); - }); + claimButton.element.addEventListener("click", claimErc20PermitHandlerWrapper(app.transaction)); } else if (app.transaction.type === "erc721-permit") { const requestedAmountElement = insertErc721PermitTableData(app.transaction, table); table.setAttribute(`data-claim`, "ok"); diff --git a/static/scripts/rewards/toaster.ts b/static/scripts/rewards/toaster.ts index 341eb7a2..da19ac6f 100644 --- a/static/scripts/rewards/toaster.ts +++ b/static/scripts/rewards/toaster.ts @@ -10,14 +10,15 @@ export const toaster = { }; export const claimButton = { - loading: loadingClaimButton, - reset: resetClaimButton, + // loading: loadingClaimButton, + // reset: resetClaimButton, element: document.getElementById("claimButton") as HTMLButtonElement, }; const notifications = document.querySelector(".notifications") as HTMLUListElement; export function createToast(meaning: keyof typeof toaster.icons, text: string) { + hideLoader(); const toastDetails = { timer: 5000, } as { @@ -52,25 +53,14 @@ function removeToast(toast: HTMLElement, timeoutId?: NodeJS.Timeout) { setTimeout(() => toast.remove(), 500); // Removing the toast after 500ms } -export function loadingClaimButton(triggerLoader = true) { +export function showLoader() { claimButton.element.disabled = true; - // Adding this because not all disabling should trigger loading spinner - if (triggerLoader) { - claimButton.element.classList.add("show-cl"); - claimButton.element.classList.remove("hide-cl"); - } + claimButton.element.className = "show-cl"; } -export function resetClaimButton() { +export function hideLoader() { claimButton.element.disabled = false; - claimButton.element.classList.add("hide-cl"); - claimButton.element.classList.remove("show-cl"); -} - -export function hideClaimButton() { - claimButton.element.disabled = true; - claimButton.element.classList.add("hide-cl"); - claimButton.element.classList.remove("show-cl"); + claimButton.element.className = "hide-cl"; } export function errorToast(error: MetaMaskError, errorMessage?: string) { diff --git a/static/scripts/rewards/web3/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts index d22162f8..ad596df6 100644 --- a/static/scripts/rewards/web3/connect-wallet.ts +++ b/static/scripts/rewards/web3/connect-wallet.ts @@ -1,6 +1,6 @@ -import { claimButton, resetClaimButton, toaster } from "../toaster"; import { JsonRpcSigner } from "@ethersproject/providers"; import { ethers } from "ethers"; +import { claimButton, toaster } from "../toaster"; export async function connectWallet(): Promise { try { @@ -17,7 +17,6 @@ export async function connectWallet(): Promise { return null; } - resetClaimButton(); return signer; } catch (error: unknown) { if (error instanceof Error) { diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index c01991ae..ac08916a 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -5,10 +5,10 @@ import { app } from "../app-state"; import { permit2Address } from "../constants"; import invalidateButton from "../invalidate-component"; import { tokens } from "../render-transaction/render-token-symbol"; +import { renderTransaction } from "../render-transaction/render-transaction"; import { Erc20Permit } from "../render-transaction/tx-type"; import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; -import { MetaMaskError, claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; -import { renderTransaction } from "../render-transaction/render-transaction"; +import { MetaMaskError, claimButton, errorToast, showLoader, toaster } from "../toaster"; import { connectWallet } from "./connect-wallet"; export async function fetchTreasury( @@ -54,7 +54,6 @@ async function connectToWallet() { const e = error as unknown as MetaMaskError; console.error("Error in connectWallet: ", e); errorToast(e, e.reason); - resetClaimButton(); } } return signer; @@ -69,7 +68,6 @@ async function checkPermitClaimability(permit: Erc20Permit, signer: JsonRpcSigne const e = error as unknown as MetaMaskError; console.error("Error in checkPermitClaimable: ", e); errorToast(e, e.reason); - resetClaimButton(); } } return isPermitClaimable; @@ -84,7 +82,6 @@ async function createEthersContract(signer: JsonRpcSigner) { const e = error as unknown as MetaMaskError; console.error("Error in creating ethers.Contract: ", e); errorToast(e, e.reason); - resetClaimButton(); } } return permit2Contract; @@ -107,7 +104,6 @@ async function transferFromPermit(permit2Contract: Contract, permit: Erc20Permit console.error("Error in permitTransferFrom: ", e); errorToast(e, e.reason); } - resetClaimButton(); } return null; } @@ -124,7 +120,6 @@ async function waitForTransaction(tx: TransactionResponse) { const e = error as unknown as MetaMaskError; console.error("Error in tx.wait: ", e); errorToast(e, e.reason); - resetClaimButton(); } } return receipt; @@ -138,21 +133,20 @@ async function renderTx() { const e = error as unknown as MetaMaskError; console.error("Error in renderTransaction: ", e); errorToast(e, e.reason); - resetClaimButton(); } } } export function claimErc20PermitHandlerWrapper(permit: Erc20Permit) { return async function claimErc20PermitHandler() { + showLoader(); + const signer = await connectToWallet(); if (!signer) return; const isPermitClaimable = await checkPermitClaimability(permit, signer); if (!isPermitClaimable) return; - loadingClaimButton(); - const permit2Contract = await createEthersContract(signer); if (!permit2Contract) return; diff --git a/static/scripts/rewards/web3/erc721-permit.ts b/static/scripts/rewards/web3/erc721-permit.ts index ee01961b..ffd04ff9 100644 --- a/static/scripts/rewards/web3/erc721-permit.ts +++ b/static/scripts/rewards/web3/erc721-permit.ts @@ -1,10 +1,10 @@ import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers"; import { ethers } from "ethers"; -import { app } from "../app-state"; -import { Erc721Permit } from "../render-transaction/tx-type"; -import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; import { nftRewardAbi } from "../abis/nft-reward-abi"; +import { app } from "../app-state"; import { renderTransaction } from "../render-transaction/render-transaction"; +import { Erc721Permit } from "../render-transaction/tx-type"; +import { claimButton, errorToast, showLoader, toaster } from "../toaster"; import { connectWallet } from "./connect-wallet"; export function claimErc721PermitHandler(permit: Erc721Permit) { @@ -16,24 +16,21 @@ export function claimErc721PermitHandler(permit: Erc721Permit) { if ((await signer.getAddress()).toLowerCase() !== permit.request.beneficiary) { toaster.create("warning", `This NFT is not for you.`); - resetClaimButton(); return; } if (permit.request.deadline.lt(Math.floor(Date.now() / 1000))) { toaster.create("error", `This NFT has expired.`); - resetClaimButton(); return; } const isRedeemed = await isNonceRedeemed(permit, app.provider); if (isRedeemed) { toaster.create("error", `This NFT has already been redeemed.`); - resetClaimButton(); return; } - loadingClaimButton(); + showLoader(); try { const nftContract = new ethers.Contract(permit.nftAddress, nftRewardAbi, signer); @@ -53,7 +50,6 @@ export function claimErc721PermitHandler(permit: Erc721Permit) { if (error instanceof Error) { console.error(error); errorToast(error, error.message ?? error); - resetClaimButton(); } } }; diff --git a/static/scripts/rewards/web3/handle-if-on-correct-network.ts b/static/scripts/rewards/web3/handle-if-on-correct-network.ts index 27972424..eaffaa43 100644 --- a/static/scripts/rewards/web3/handle-if-on-correct-network.ts +++ b/static/scripts/rewards/web3/handle-if-on-correct-network.ts @@ -1,13 +1,12 @@ import invalidateButton from "../invalidate-component"; -import { loadingClaimButton, resetClaimButton } from "../toaster"; +import { showLoader } from "../toaster"; export function handleIfOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number) { if (desiredNetworkId === currentNetworkId) { // enable the button once on the correct network - resetClaimButton(); invalidateButton.disabled = false; } else { - loadingClaimButton(false); + showLoader(); invalidateButton.disabled = true; } } diff --git a/static/scripts/rewards/web3/not-on-correct-network.ts b/static/scripts/rewards/web3/not-on-correct-network.ts index d1d26bbd..137fb421 100644 --- a/static/scripts/rewards/web3/not-on-correct-network.ts +++ b/static/scripts/rewards/web3/not-on-correct-network.ts @@ -1,7 +1,7 @@ import { ethers } from "ethers"; import { getNetworkName } from "../constants"; import invalidateButton from "../invalidate-component"; -import { loadingClaimButton, toaster } from "../toaster"; +import { showLoader, toaster } from "../toaster"; import { switchNetwork } from "./switch-network"; export function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number, web3provider: ethers.providers.Web3Provider) { @@ -13,7 +13,7 @@ export function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: if (!networkName) { toaster.create("error", `This dApp currently does not support payouts for network ID ${desiredNetworkId}`); } - loadingClaimButton(false); + showLoader(); invalidateButton.disabled = true; switchNetwork(web3provider, desiredNetworkId).catch((error) => { console.error(error); diff --git a/static/scripts/rewards/web3/verify-current-network.ts b/static/scripts/rewards/web3/verify-current-network.ts index 3f723fad..bc658e1a 100644 --- a/static/scripts/rewards/web3/verify-current-network.ts +++ b/static/scripts/rewards/web3/verify-current-network.ts @@ -1,16 +1,15 @@ import { ethers } from "ethers"; import invalidateButton from "../invalidate-component"; -import { loadingClaimButton, toaster } from "../toaster"; +import { showLoader, toaster } from "../toaster"; import { handleIfOnCorrectNetwork } from "./handle-if-on-correct-network"; import { notOnCorrectNetwork } from "./not-on-correct-network"; // verifyCurrentNetwork checks if the user is on the correct network and displays an error if not - export async function verifyCurrentNetwork(desiredNetworkId: number) { const web3provider = new ethers.providers.Web3Provider(window.ethereum); if (!web3provider || !web3provider.provider.isMetaMask) { + showLoader(); toaster.create("info", "Please connect to MetaMask."); - loadingClaimButton(false); invalidateButton.disabled = true; } diff --git a/static/styles/rewards/claim-table.css b/static/styles/rewards/claim-table.css index f080a769..6c965bb4 100644 --- a/static/styles/rewards/claim-table.css +++ b/static/styles/rewards/claim-table.css @@ -152,9 +152,29 @@ table[data-claim-rendered] button:hover > div { display: unset; color: #fff; } + table[data-claim-rendered] button:hover > svg { display: none !important; } + +.show-cl { + display: block; +} + +table[data-claim-rendered] button.hide-cl > svg.claim-loader { + display: none; +} +table[data-claim-rendered] button.show-cl > svg.claim-icon { + display: none; +} + +table[data-claim-rendered] button.show-cl > svg.claim-loader { + display: unset; +} +table[data-claim-rendered] button.hide-cl > svg.claim-icon { + display: unset; +} + table[data-claim-rendered] button#additionalDetails { width: 100%; color: #fff; @@ -272,23 +292,6 @@ table[data-claim-rendered="true"][data-claim="none"][data-contract-loaded="true" color: #fff; } -.show-cl { - display: block; -} - -.hide-cl > svg.claim-loader { - display: none; -} -.show-cl > svg.claim-loader { - display: unset; -} -.hide-cl > svg.claim-icon { - display: unset; -} -.show-cl > svg.claim-icon { - display: none; -} - .show-pagination { display: flex; cursor: pointer; From f30722307163d436295d56b5107326d759e00fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= Date: Thu, 22 Feb 2024 22:50:40 +0900 Subject: [PATCH 14/14] refactor: move app into all functions caused some type issues related to the two different permit types and passing them to all UI functions. --- .github/workflows/conventional-commits.yml | 3 +- static/scripts/rewards/app-state.ts | 37 ++++-- static/scripts/rewards/init.ts | 3 +- .../claim-rewards-pagination.ts | 14 +- .../render-transaction/insert-table-data.ts | 12 +- .../read-claim-data-from-url.ts | 29 +++-- .../render-transaction/render-transaction.ts | 46 +++---- .../rewards/render-transaction/tx-type.ts | 34 +++-- .../rpc-optimization/get-optimal-provider.ts | 17 +-- .../rpc-optimization/test-rpc-performance.ts | 45 ++++--- static/scripts/rewards/web3/connect-wallet.ts | 5 - static/scripts/rewards/web3/erc20-permit.ts | 120 +++++++----------- .../rewards/web3/not-on-correct-network.ts | 5 +- static/styles/rewards/claim-table.css | 11 +- 14 files changed, 181 insertions(+), 200 deletions(-) diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index 6b3066e5..a6bba39b 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -2,11 +2,12 @@ name: Conventional Commits on: push: + pull_request: jobs: conventional-commits: name: Conventional Commits runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ubiquity/action-conventional-commits@master diff --git a/static/scripts/rewards/app-state.ts b/static/scripts/rewards/app-state.ts index 002b0ee6..4d5d7ebd 100644 --- a/static/scripts/rewards/app-state.ts +++ b/static/scripts/rewards/app-state.ts @@ -1,14 +1,23 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { networkExplorers } from "./constants"; -import { ClaimTx } from "./render-transaction/tx-type"; +import { RewardPermit } from "./render-transaction/tx-type"; export class AppState { - public claims: ClaimTx[] = []; + public claims: RewardPermit[] = []; private _provider!: JsonRpcProvider; private _currentIndex = 0; + private _signer; + + get signer() { + return this._signer; + } + + set signer(value) { + this._signer = value; + } get networkId(): number | null { - return this.transaction?.networkId || null; + return this.permit?.networkId || null; } get provider(): JsonRpcProvider { @@ -19,33 +28,33 @@ export class AppState { this._provider = value; } - get transactionIndex(): number { + get permitIndex(): number { return this._currentIndex; } - get transaction(): ClaimTx | null { - return this.transactionIndex < this.claims.length ? this.claims[this.transactionIndex] : null; + get permit(): RewardPermit { + return this.permitIndex < this.claims.length ? this.claims[this.permitIndex] : this.claims[0]; } - get transactionNetworkId() { - return this.transaction?.networkId; + get permitNetworkId() { + return this.permit?.networkId; } get currentExplorerUrl(): string { - if (!this.transaction) { + if (!this.permit) { return "https://etherscan.io"; } - return networkExplorers[this.transaction.networkId] || "https://etherscan.io"; + return networkExplorers[this.permit.networkId] || "https://etherscan.io"; } - nextTx(): ClaimTx | null { + nextPermit(): RewardPermit | null { this._currentIndex = Math.min(this.claims.length - 1, this._currentIndex + 1); - return this.transaction; + return this.permit; } - previousTx(): ClaimTx | null { + previousPermit(): RewardPermit | null { this._currentIndex = Math.max(0, this._currentIndex - 1); - return this.transaction; + return this.permit; } } diff --git a/static/scripts/rewards/init.ts b/static/scripts/rewards/init.ts index 8fd646ea..07141cf3 100644 --- a/static/scripts/rewards/init.ts +++ b/static/scripts/rewards/init.ts @@ -1,10 +1,11 @@ +import { app } from "./app-state"; import { readClaimDataFromUrl } from "./render-transaction/read-claim-data-from-url"; import { grid } from "./the-grid"; displayCommitHash(); // @DEV: display commit hash in footer grid(document.getElementById("grid") as HTMLElement); // @DEV: display grid background -readClaimDataFromUrl().catch(console.error); // @DEV: read claim data from URL +readClaimDataFromUrl(app).catch(console.error); // @DEV: read claim data from URL declare const commitHash: string; // @DEV: passed in at build time check build/esbuild-build.ts function displayCommitHash() { diff --git a/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts b/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts index 60f6d9c1..1559a1c9 100644 --- a/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts +++ b/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts @@ -6,15 +6,15 @@ import { setPagination } from "./set-pagination"; import { removeAllEventListeners } from "./utils"; export function claimRewardsPagination(rewardsCount: HTMLElement) { - rewardsCount.innerHTML = `${app.transactionIndex + 1}/${app.claims.length} reward`; + rewardsCount.innerHTML = `${app.permitIndex + 1}/${app.claims.length} reward`; const nextTxButton = document.getElementById("nextTx"); if (nextTxButton) { nextTxButton.addEventListener("click", () => { claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; - app.nextTx(); - rewardsCount.innerHTML = `${app.transactionIndex + 1}/${app.claims.length} reward`; - table.setAttribute(`data-claim`, "none"); + app.nextPermit(); + rewardsCount.innerHTML = `${app.permitIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "error"); renderTransaction(true).catch(console.error); }); } @@ -23,9 +23,9 @@ export function claimRewardsPagination(rewardsCount: HTMLElement) { if (prevTxButton) { prevTxButton.addEventListener("click", () => { claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; - app.previousTx(); - rewardsCount.innerHTML = `${app.transactionIndex + 1}/${app.claims.length} reward`; - table.setAttribute(`data-claim`, "none"); + app.previousPermit(); + rewardsCount.innerHTML = `${app.permitIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "error"); renderTransaction(true).catch(console.error); }); } diff --git a/static/scripts/rewards/render-transaction/insert-table-data.ts b/static/scripts/rewards/render-transaction/insert-table-data.ts index e19a306f..60ff1c01 100644 --- a/static/scripts/rewards/render-transaction/insert-table-data.ts +++ b/static/scripts/rewards/render-transaction/insert-table-data.ts @@ -1,16 +1,17 @@ import { BigNumber, ethers } from "ethers"; -import { app } from "../app-state"; -import { Erc20Permit, Erc721Permit } from "./tx-type"; +import { AppState, app } from "../app-state"; +import { Erc721Permit } from "./tx-type"; export function shortenAddress(address: string): string { return `${address.slice(0, 10)}...${address.slice(-8)}`; } export function insertErc20PermitTableData( - permit: Erc20Permit, + app: AppState, table: Element, treasury: { balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string } ): Element { + const permit = app.permit; const requestedAmountElement = document.getElementById("rewardAmount") as Element; renderToFields(permit.transferDetails.to, app.currentExplorerUrl); renderTokenFields(permit.permit.permitted.token, app.currentExplorerUrl); @@ -18,7 +19,10 @@ export function insertErc20PermitTableData( { name: "From", value: `${permit.owner}` }, { name: "Expiry", - value: permit.permit.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(permit.permit.deadline.toNumber()).toLocaleString() : undefined, + value: (() => { + const deadline = BigNumber.isBigNumber(permit.permit.deadline) ? permit.permit.deadline : BigNumber.from(permit.permit.deadline); + return deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(deadline.toNumber()).toLocaleString() : undefined; + })(), }, { name: "Balance", value: treasury.balance.gte(0) ? `${ethers.utils.formatUnits(treasury.balance, treasury.decimals)} ${treasury.symbol}` : "N/A" }, { name: "Allowance", value: treasury.allowance.gte(0) ? `${ethers.utils.formatUnits(treasury.allowance, treasury.decimals)} ${treasury.symbol}` : "N/A" }, 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 327e4a65..0763b8fc 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 @@ -1,44 +1,46 @@ import { Type } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; -import { app } from "../app-state"; -import { setClaimMessage } from "./set-claim-message"; -import { claimTxT } from "./tx-type"; +import { AppState, app } from "../app-state"; +import { useFastestRpc } from "../rpc-optimization/get-optimal-provider"; +import { connectWallet } from "../web3/connect-wallet"; +import { verifyCurrentNetwork } from "../web3/verify-current-network"; import { claimRewardsPagination } from "./claim-rewards-pagination"; import { renderTransaction } from "./render-transaction"; -import { getOptimalProvider } from "../rpc-optimization/get-optimal-provider"; +import { setClaimMessage } from "./set-claim-message"; +import { claimTxT } from "./tx-type"; export const table = document.getElementsByTagName(`table`)[0]; const urlParams = new URLSearchParams(window.location.search); const base64encodedTxData = urlParams.get("claim"); -export async function readClaimDataFromUrl() { +export async function readClaimDataFromUrl(app: AppState) { if (!base64encodedTxData) { // No claim data found setClaimMessage({ type: "Notice", message: `No claim data found.` }); - table.setAttribute(`data-claim`, "none"); + table.setAttribute(`data-claim`, "error"); return; } - decodeClaimData(base64encodedTxData); - - await getOptimalProvider(app); - + app.claims = decodeClaimData(base64encodedTxData); + app.provider = await useFastestRpc(app); + const networkId = app.permit?.networkId || app.networkId; + app.signer = await connectWallet(networkId).catch(console.error); displayRewardDetails(); displayRewardPagination(); renderTransaction(true) - // .then(() => verifyCurrentNetwork(app.transaction?.networkId || app.networkId)) // @todo: verifyCurrentNetwork + .then(() => verifyCurrentNetwork(networkId)) .catch(console.error); } function decodeClaimData(base64encodedTxData: string) { try { - const claimTxs = Value.Decode(Type.Array(claimTxT), JSON.parse(atob(base64encodedTxData))); - app.claims = claimTxs; + return Value.Decode(Type.Array(claimTxT), JSON.parse(atob(base64encodedTxData))); } catch (error) { console.error(error); setClaimMessage({ type: "Error", message: `Invalid claim data passed in URL` }); table.setAttribute(`data-claim`, "error"); + throw error; } } @@ -56,7 +58,6 @@ function displayRewardPagination() { function displayRewardDetails() { let isDetailsVisible = false; table.setAttribute(`data-details-visible`, isDetailsVisible.toString()); - const additionalDetails = document.getElementById(`additionalDetails`) as HTMLElement; additionalDetails.addEventListener("click", () => { isDetailsVisible = !isDetailsVisible; diff --git a/static/scripts/rewards/render-transaction/render-transaction.ts b/static/scripts/rewards/render-transaction/render-transaction.ts index 960230ab..768aa04c 100644 --- a/static/scripts/rewards/render-transaction/render-transaction.ts +++ b/static/scripts/rewards/render-transaction/render-transaction.ts @@ -1,7 +1,7 @@ import { app } from "../app-state"; import { networkExplorers } from "../constants"; import { claimButton, hideLoader } from "../toaster"; -import { claimErc20PermitHandlerWrapper, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; +import { claimErc20PermitHandlerWrapper, fetchFundingWallet, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; import { claimErc721PermitHandler } from "../web3/erc721-permit"; import { verifyCurrentNetwork } from "../web3/verify-current-network"; import { insertErc20PermitTableData, insertErc721PermitTableData } from "./insert-table-data"; @@ -15,64 +15,64 @@ export async function renderTransaction(nextTx?: boolean): Promise { const table = document.getElementsByTagName(`table`)[0]; if (nextTx) { - app.nextTx(); + app.nextPermit(); if (!app.claims || app.claims.length <= 1) { // already hidden } else { setPagination(document.getElementById("nextTx"), document.getElementById("previousTx")); const rewardsCount = document.getElementById("rewardsCount") as Element; - rewardsCount.innerHTML = `${app.transactionIndex + 1}/${app.claims.length} reward`; - table.setAttribute(`data-claim`, "none"); + rewardsCount.innerHTML = `${app.permitIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "error"); } } - if (!app.transaction) { + if (!app.permit) { hideLoader(); return false; } - verifyCurrentNetwork(app.transaction.networkId).catch(console.error); + verifyCurrentNetwork(app.permit.networkId).catch(console.error); - if (app.transaction.type === "erc20-permit") { - const treasury = await fetchTreasury(app.transaction, app.provider); + if (app.permit.type === "erc20-permit") { + const treasury = await fetchFundingWallet(app); // insert tx data into table - const requestedAmountElement = insertErc20PermitTableData(app.transaction, table, treasury); - table.setAttribute(`data-claim`, "ok"); + const requestedAmountElement = insertErc20PermitTableData(app, table, treasury); renderTokenSymbol({ - tokenAddress: app.transaction.permit.permitted.token, - ownerAddress: app.transaction.owner, - amount: app.transaction.transferDetails.requestedAmount, - explorerUrl: networkExplorers[app.transaction.networkId], + tokenAddress: app.permit.permit.permitted.token, + ownerAddress: app.permit.owner, + amount: app.permit.transferDetails.requestedAmount, + explorerUrl: networkExplorers[app.permit.networkId], table, requestedAmountElement, provider: app.provider, }).catch(console.error); const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.transaction.transferDetails.to }).catch(console.error); + renderEnsName({ element: toElement, address: app.permit.transferDetails.to }).catch(console.error); - generateInvalidatePermitAdminControl(app.transaction).catch(console.error); + generateInvalidatePermitAdminControl(app).catch(console.error); - claimButton.element.addEventListener("click", claimErc20PermitHandlerWrapper(app.transaction)); - } else if (app.transaction.type === "erc721-permit") { - const requestedAmountElement = insertErc721PermitTableData(app.transaction, table); + claimButton.element.addEventListener("click", claimErc20PermitHandlerWrapper(app)); + table.setAttribute(`data-claim`, "ok"); + } else if (app.permit.type === "erc721-permit") { + const requestedAmountElement = insertErc721PermitTableData(app.permit, table); table.setAttribute(`data-claim`, "ok"); renderNftSymbol({ - tokenAddress: app.transaction.nftAddress, - explorerUrl: networkExplorers[app.transaction.networkId], + tokenAddress: app.permit.nftAddress, + explorerUrl: networkExplorers[app.permit.networkId], table, requestedAmountElement, provider: app.provider, }).catch(console.error); const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.transaction.request.beneficiary }).catch(console.error); + renderEnsName({ element: toElement, address: app.permit.request.beneficiary }).catch(console.error); - claimButton.element.addEventListener("click", claimErc721PermitHandler(app.transaction)); + claimButton.element.addEventListener("click", claimErc721PermitHandler(app.permit)); } return true; diff --git a/static/scripts/rewards/render-transaction/tx-type.ts b/static/scripts/rewards/render-transaction/tx-type.ts index 23df1341..8891dcaf 100644 --- a/static/scripts/rewards/render-transaction/tx-type.ts +++ b/static/scripts/rewards/render-transaction/tx-type.ts @@ -23,19 +23,19 @@ const erc20PermitT = T.Object({ type: T.Literal("erc20-permit"), permit: T.Object({ permitted: T.Object({ - token: addressT, - amount: bigNumberT, + token: T.RegExp(/^0x[a-fA-F0-9]{40}$/), + amount: T.Union([T.RegExp(/^\d+$/), T.Number()]), }), - nonce: bigNumberT, - deadline: bigNumberT, + nonce: T.Union([T.RegExp(/^\d+$/), T.Number()]), + deadline: T.Union([T.RegExp(/^\d+$/), T.Number()]), }), transferDetails: T.Object({ - to: addressT, - requestedAmount: bigNumberT, + to: T.RegExp(/^0x[a-fA-F0-9]{40}$/), + requestedAmount: T.Union([T.RegExp(/^\d+$/), T.Number()]), }), - owner: addressT, - signature: signatureT, - networkId: networkIdT, + owner: T.RegExp(/^0x[a-fA-F0-9]{40}$/), + signature: T.RegExp(/^0x[a-fA-F0-9]+$/), + networkId: T.Number(), }); export type Erc20Permit = StaticDecode; @@ -59,10 +59,24 @@ const erc721Permit = T.Object({ nftAddress: addressT, networkId: networkIdT, signature: signatureT, + // @whilefoo: they should have matching key names. + owner: addressT, + permit: T.Object({ + permitted: T.Object({ + token: addressT, + amount: bigNumberT, + }), + nonce: bigNumberT, + deadline: bigNumberT, + }), + transferDetails: T.Object({ + to: T.RegExp(/^0x[a-fA-F0-9]{40}$/), + requestedAmount: T.Union([T.RegExp(/^\d+$/), T.Number()]), + }), }); export type Erc721Permit = StaticDecode; export const claimTxT = T.Union([erc20PermitT, erc721Permit]); -export type ClaimTx = StaticDecode; +export type RewardPermit = StaticDecode; diff --git a/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts index 76910d92..970ca565 100644 --- a/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts +++ b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts @@ -3,24 +3,19 @@ import { AppState } from "../app-state"; import { getFastestRpcProvider } from "./get-fastest-rpc-provider"; import { testRpcPerformance } from "./test-rpc-performance"; -let optimalProvider: JsonRpcProvider; let isTestStarted = false; let isTestCompleted = false; -export async function getOptimalProvider(app: AppState): Promise { - const networkId = app.transactionNetworkId; +export async function useFastestRpc(app: AppState): Promise { + const networkId = app.permitNetworkId; + if (!networkId) throw new Error("Network ID not found"); if (!isTestCompleted && !isTestStarted) { isTestStarted = true; - testRpcPerformance(networkId) - .then(() => (isTestCompleted = true)) - .catch(console.error); + await testRpcPerformance(networkId).catch(console.error); + isTestCompleted = true; } - if (!optimalProvider) { - optimalProvider = getFastestRpcProvider(networkId); - } - app.provider = optimalProvider; - return optimalProvider; + return getFastestRpcProvider(networkId); } diff --git a/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts b/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts index 700ffb94..11d42b80 100644 --- a/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts +++ b/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts @@ -32,33 +32,36 @@ const RPC_HEADER = { "Content-Type": "application/json", }; +function raceUntilSuccess(promises: Promise[]) { + return new Promise((resolve) => { + promises.forEach((promise: Promise) => { + promise.then(resolve).catch(() => {}); + }); + }); +} + export async function testRpcPerformance(networkId: number) { const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); const promises = networkRpcs[networkId].map(async (baseURL: string) => { - try { - const startTime = performance.now(); - const API = axios.create({ - baseURL, - headers: RPC_HEADER, - }); + const startTime = performance.now(); + const API = axios.create({ + baseURL, + headers: RPC_HEADER, + }); - const { data } = await API.post("", RPC_BODY).catch(() => ({ data: null })); - const endTime = performance.now(); - const latency = endTime - startTime; - if (verifyBlock(data)) { - // Save the latency in localStorage - latencies[`${baseURL}_${networkId}`] = latency; - } else { - // Save -1 in localStorage to indicate an error - latencies[`${baseURL}_${networkId}`] = -1; - } - } catch (error) { - // Save -1 in localStorage to indicate an error - latencies[`${baseURL}_${networkId}`] = -1; + const { data } = await API.post("", RPC_BODY); + const endTime = performance.now(); + const latency = endTime - startTime; + if (verifyBlock(data)) { + // Save the latency in localStorage + latencies[`${baseURL}_${networkId}`] = latency; + localStorage.setItem("rpcLatencies", JSON.stringify(latencies)); + } else { + // Throw an error to indicate an invalid block data + throw new Error(`Invalid block data from ${baseURL}`); } }); - await Promise.race(promises); - localStorage.setItem("rpcLatencies", JSON.stringify(latencies)); + await raceUntilSuccess(promises); } diff --git a/static/scripts/rewards/web3/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts index ad596df6..b1353134 100644 --- a/static/scripts/rewards/web3/connect-wallet.ts +++ b/static/scripts/rewards/web3/connect-wallet.ts @@ -4,11 +4,6 @@ import { claimButton, toaster } from "../toaster"; export async function connectWallet(): Promise { try { - if (!window.ethereum) { - console.error("Ethereum provider not found"); - return null; - } - const wallet = new ethers.providers.Web3Provider(window.ethereum); const signer = wallet.getSigner(); const address = await signer.getAddress(); diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index ac08916a..f6121862 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -1,23 +1,19 @@ -import { JsonRpcProvider, JsonRpcSigner, TransactionResponse } from "@ethersproject/providers"; +import { JsonRpcSigner, TransactionResponse } from "@ethersproject/providers"; import { BigNumber, BigNumberish, Contract, ethers } from "ethers"; import { permit2Abi } from "../abis"; -import { app } from "../app-state"; +import { AppState } from "../app-state"; import { permit2Address } from "../constants"; import invalidateButton from "../invalidate-component"; import { tokens } from "../render-transaction/render-token-symbol"; import { renderTransaction } from "../render-transaction/render-transaction"; -import { Erc20Permit } from "../render-transaction/tx-type"; import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; import { MetaMaskError, claimButton, errorToast, showLoader, toaster } from "../toaster"; -import { connectWallet } from "./connect-wallet"; -export async function fetchTreasury( - permit: Erc20Permit, - provider: JsonRpcProvider -): Promise<{ balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string }> { +export async function fetchFundingWallet(app: AppState): Promise<{ balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string }> { + const permit = app.permit; try { const tokenAddress = permit.permit.permitted.token.toLowerCase(); - const tokenContract = await getErc20Contract(tokenAddress, provider); + const tokenContract = await getErc20Contract(tokenAddress, app.provider); if (tokenAddress === tokens[0].address || tokenAddress === tokens[1].address) { const decimals = tokenAddress === tokens[0].address ? 18 : tokenAddress === tokens[1].address ? 18 : -1; @@ -42,27 +38,10 @@ export async function fetchTreasury( } } -async function connectToWallet() { - let signer: JsonRpcSigner | null = null; - try { - signer = await connectWallet(); - if (!signer) { - return null; - } - } catch (error: unknown) { - if (error instanceof Error) { - const e = error as unknown as MetaMaskError; - console.error("Error in connectWallet: ", e); - errorToast(e, e.reason); - } - } - return signer; -} - -async function checkPermitClaimability(permit: Erc20Permit, signer: JsonRpcSigner | null) { +async function checkPermitClaimability(app: AppState): Promise { let isPermitClaimable = false; try { - isPermitClaimable = await checkPermitClaimable(permit, signer, app.provider); + isPermitClaimable = await checkPermitClaimable(app); } catch (error: unknown) { if (error instanceof Error) { const e = error as unknown as MetaMaskError; @@ -87,9 +66,10 @@ async function createEthersContract(signer: JsonRpcSigner) { return permit2Contract; } -async function transferFromPermit(permit2Contract: Contract, permit: Erc20Permit) { +async function transferFromPermit(permit2Contract: Contract, app: AppState) { + const permit = app.permit; try { - const tx = await permit2Contract.permitTransferFrom(permit.permit, permit.transferDetails, permit.owner, permit.signature); + const tx = await permit2Contract.permitTransferFrom(permit, permit.transferDetails, permit.owner, permit.signature); toaster.create("info", `Transaction sent`); return tx; } catch (error: unknown) { @@ -137,20 +117,17 @@ async function renderTx() { } } -export function claimErc20PermitHandlerWrapper(permit: Erc20Permit) { +export function claimErc20PermitHandlerWrapper(app: AppState) { return async function claimErc20PermitHandler() { showLoader(); - const signer = await connectToWallet(); - if (!signer) return; - - const isPermitClaimable = await checkPermitClaimability(permit, signer); + const isPermitClaimable = await checkPermitClaimability(app); if (!isPermitClaimable) return; - const permit2Contract = await createEthersContract(signer); + const permit2Contract = await createEthersContract(app.signer); if (!permit2Contract) return; - const tx = await transferFromPermit(permit2Contract, permit); + const tx = await transferFromPermit(permit2Contract, app); if (!tx) return; const receipt = await waitForTransaction(tx); @@ -162,10 +139,10 @@ export function claimErc20PermitHandlerWrapper(permit: Erc20Permit) { }; } -export async function checkPermitClaimable(permit: Erc20Permit, signer: JsonRpcSigner | null, provider: JsonRpcProvider) { +export async function checkPermitClaimable(app: AppState): Promise { let isClaimed; try { - isClaimed = await isNonceClaimed(permit); + isClaimed = await isNonceClaimed(app); } catch (error: unknown) { console.error("Error in isNonceClaimed: ", error); return false; @@ -176,21 +153,23 @@ export async function checkPermitClaimable(permit: Erc20Permit, signer: JsonRpcS return false; } - if (permit.permit.deadline.lt(Math.floor(Date.now() / 1000))) { + const permit = app.permit.permit; + + if (permit.deadline.lt(Math.floor(Date.now() / 1000))) { toaster.create("error", `This reward has expired.`); return false; } let treasury; try { - treasury = await fetchTreasury(permit, provider); + treasury = await fetchFundingWallet(app); } catch (error: unknown) { console.error("Error in fetchTreasury: ", error); return false; } const { balance, allowance } = treasury; - const permitted = BigNumber.from(permit.permit.permitted.amount); + const permitted = BigNumber.from(permit.permitted.amount); const isSolvent = balance.gte(permitted); const isAllowed = allowance.gte(permitted); @@ -203,38 +182,33 @@ export async function checkPermitClaimable(permit: Erc20Permit, signer: JsonRpcS return false; } - if (signer) { - let user; - try { - user = (await signer.getAddress()).toLowerCase(); - } catch (error: unknown) { - console.error("Error in signer.getAddress: ", error); - return false; - } + let user; + try { + user = (await app.signer.getAddress()).toLowerCase(); + } catch (error: unknown) { + console.error("Error in signer.getAddress: ", error); + return false; + } - const beneficiary = permit.transferDetails.to.toLowerCase(); - if (beneficiary !== user) { - toaster.create("warning", `This reward is not for you.`); - return false; - } + const beneficiary = permit.transferDetails.to.toLowerCase(); + if (beneficiary !== user) { + toaster.create("warning", `This reward is not for you.`); + return false; } return true; } -export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) { - const signer = await connectWallet(); - if (!signer) { - console.log("Wallet not connected"); - return; - } - +export async function generateInvalidatePermitAdminControl(app: AppState) { try { - const address = await signer.getAddress(); + const address = await app.signer.getAddress(); const user = address.toLowerCase(); - const owner = permit.owner.toLowerCase(); - if (owner !== user) { - return; + + if (app.permit) { + const owner = app.permit.owner.toLowerCase(); + if (owner !== user) { + return; + } } } catch (error) { console.error("Error getting address from signer"); @@ -246,16 +220,12 @@ export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) invalidateButton.addEventListener("click", async function invalidateButtonClickHandler() { try { - const signer = await connectWallet(); - if (!signer) { - return; - } - const isClaimed = await isNonceClaimed(permit); + const isClaimed = await isNonceClaimed(app); if (isClaimed) { toaster.create("error", `This reward has already been claimed or invalidated.`); return; } - await invalidateNonce(signer, permit.permit.nonce); + await invalidateNonce(app.signer, app.permit.permit.nonce); } catch (error: unknown) { if (error instanceof Error) { const e = error as unknown as MetaMaskError; @@ -269,14 +239,14 @@ export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) } //mimics https://github.com/Uniswap/permit2/blob/a7cd186948b44f9096a35035226d7d70b9e24eaf/src/SignatureTransfer.sol#L150 -export async function isNonceClaimed(permit: Erc20Permit): Promise { +export async function isNonceClaimed(app: AppState): Promise { const provider = app.provider; const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, provider); - const { wordPos, bitPos } = nonceBitmap(BigNumber.from(permit.permit.nonce)); + const { wordPos, bitPos } = nonceBitmap(BigNumber.from(app.permit.permit.nonce)); - const bitmap = await permit2Contract.nonceBitmap(permit.owner, wordPos).catch((error: MetaMaskError) => { + const bitmap = await permit2Contract.nonceBitmap(app.permit.owner, wordPos).catch((error: MetaMaskError) => { console.error("Error in nonceBitmap method: ", error); throw error; }); diff --git a/static/scripts/rewards/web3/not-on-correct-network.ts b/static/scripts/rewards/web3/not-on-correct-network.ts index 137fb421..aff4c909 100644 --- a/static/scripts/rewards/web3/not-on-correct-network.ts +++ b/static/scripts/rewards/web3/not-on-correct-network.ts @@ -1,7 +1,6 @@ import { ethers } from "ethers"; import { getNetworkName } from "../constants"; -import invalidateButton from "../invalidate-component"; -import { showLoader, toaster } from "../toaster"; +import { toaster } from "../toaster"; import { switchNetwork } from "./switch-network"; export function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number, web3provider: ethers.providers.Web3Provider) { @@ -13,8 +12,6 @@ export function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: if (!networkName) { toaster.create("error", `This dApp currently does not support payouts for network ID ${desiredNetworkId}`); } - showLoader(); - invalidateButton.disabled = true; switchNetwork(web3provider, desiredNetworkId).catch((error) => { console.error(error); toaster.create("error", `Please switch to the ${networkName} network to claim this reward.`); diff --git a/static/styles/rewards/claim-table.css b/static/styles/rewards/claim-table.css index 6c965bb4..be7eb874 100644 --- a/static/styles/rewards/claim-table.css +++ b/static/styles/rewards/claim-table.css @@ -260,28 +260,19 @@ table thead { table tbody { display: none; } -table[data-claim="none"] thead { - display: table-row-group; -} table[data-claim="error"] thead { display: table-row-group; } table[data-claim="ok"] thead { display: none; } -table[data-claim="none"] tbody { - display: none; -} table[data-claim="error"] tbody { display: none; } table[data-claim="ok"] tbody { display: table-row-group; } -/* -table[data-claim-rendered="true"][data-claim="none"][data-contract-loaded="true"][data-details-visible="false"] { - border: none; -} */ + #rewardRecipient a div { opacity: 0.66; }