diff --git a/.cspell.json b/.cspell.json index 09825ad7..d54182ac 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,6 +9,7 @@ "binsec", "chainlist", "cirip", + "Claimability", "dataurl", "devpool", "ethersproject", @@ -27,6 +28,7 @@ "servedir", "solmate", "sonarjs", + "SUPABASE", "typebox", "TYPEHASH", "ubiquibot", 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/.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/.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..47892d48 --- /dev/null +++ b/.github/workflows/scripts/kebabalize.sh @@ -0,0 +1,31 @@ +#!/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" + 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/build/esbuild-build.ts b/build/esbuild-build.ts index 30d024c6..ceba3423 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -1,8 +1,10 @@ -import extraRpcs from "../lib/chainlist/constants/extraRpcs"; -import esbuild from "esbuild"; +import { execSync } from "child_process"; 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/rewards/init.ts", "static/scripts/audit-report/audit.ts", "static/scripts/onboarding/onboarding.ts", "static/scripts/key-generator/keygen.ts", @@ -33,7 +35,10 @@ 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, + commitHash: execSync(`git rev-parse --short HEAD`).toString().trim(), + }), }; esbuild @@ -46,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); @@ -57,11 +62,10 @@ 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]); } } - defines["extraRpcs"] = JSON.stringify(extraRpcs); return defines; } 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 fb45a918..865c69d9 100644 --- a/package.json +++ b/package.json @@ -9,13 +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 .", @@ -55,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", @@ -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", @@ -77,7 +80,8 @@ "lint-staged": { "*.ts": [ "yarn prettier --write", - "eslint --fix" + "eslint --fix", + "bash .github/workflows/scripts/kebab-case.sh" ], "src/**.{ts,json}": [ "cspell" 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/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/app-state.ts b/static/scripts/rewards/app-state.ts new file mode 100644 index 00000000..4d5d7ebd --- /dev/null +++ b/static/scripts/rewards/app-state.ts @@ -0,0 +1,61 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { networkExplorers } from "./constants"; +import { RewardPermit } from "./render-transaction/tx-type"; + +export class AppState { + 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.permit?.networkId || null; + } + + get provider(): JsonRpcProvider { + return this._provider; + } + + set provider(value: JsonRpcProvider) { + this._provider = value; + } + + get permitIndex(): number { + return this._currentIndex; + } + + get permit(): RewardPermit { + return this.permitIndex < this.claims.length ? this.claims[this.permitIndex] : this.claims[0]; + } + + get permitNetworkId() { + return this.permit?.networkId; + } + + get currentExplorerUrl(): string { + if (!this.permit) { + return "https://etherscan.io"; + } + return networkExplorers[this.permit.networkId] || "https://etherscan.io"; + } + + nextPermit(): RewardPermit | null { + this._currentIndex = Math.min(this.claims.length - 1, this._currentIndex + 1); + return this.permit; + } + + previousPermit(): RewardPermit | null { + this._currentIndex = Math.max(0, this._currentIndex - 1); + return this.permit; + } +} + +export const app = new AppState(); 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 deleted file mode 100644 index a75d2fb8..00000000 --- a/static/scripts/rewards/helpers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import axios from "axios"; -import { Contract, ethers } from "ethers"; -import { erc20Abi } from "./abis"; -import { JsonRpcProvider } from "@ethersproject/providers"; -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 getOptimalProvider(networkId: number) { - 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)) { - return Promise.resolve({ - latency, - baseURL, - }); - } else { - return Promise.reject(); - } - } catch (error) { - return Promise.reject(); - } - }); - - const { baseURL: optimalRPC } = await Promise.any(promises); - return new ethers.providers.JsonRpcProvider(optimalRPC, { - name: optimalRPC, - chainId: networkId, - ensAddress: "", - }); -} diff --git a/static/scripts/rewards/index.ts b/static/scripts/rewards/index.ts deleted file mode 100644 index 9aa754c4..00000000 --- a/static/scripts/rewards/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { init } from "./render-transaction/render-transaction"; -import { grid } from "./the-grid"; - -(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); - -grid(document.getElementById("grid") as HTMLElement); diff --git a/static/scripts/rewards/init.ts b/static/scripts/rewards/init.ts new file mode 100644 index 00000000..07141cf3 --- /dev/null +++ b/static/scripts/rewards/init.ts @@ -0,0 +1,16 @@ +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(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() { + // 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/claim-rewards-pagination.ts b/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts new file mode 100644 index 00000000..1559a1c9 --- /dev/null +++ b/static/scripts/rewards/render-transaction/claim-rewards-pagination.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 "./render-transaction"; +import { setPagination } from "./set-pagination"; +import { removeAllEventListeners } from "./utils"; + +export function claimRewardsPagination(rewardsCount: HTMLElement) { + 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.nextPermit(); + rewardsCount.innerHTML = `${app.permitIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "error"); + renderTransaction(true).catch(console.error); + }); + } + + const prevTxButton = document.getElementById("previousTx"); + if (prevTxButton) { + prevTxButton.addEventListener("click", () => { + claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; + app.previousPermit(); + rewardsCount.innerHTML = `${app.permitIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "error"); + 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..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 "."; -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" }, @@ -88,8 +92,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)}
    `; 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..0763b8fc --- /dev/null +++ b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts @@ -0,0 +1,66 @@ +import { Type } from "@sinclair/typebox"; +import { Value } from "@sinclair/typebox/value"; +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 { 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(app: AppState) { + if (!base64encodedTxData) { + // No claim data found + setClaimMessage({ type: "Notice", message: `No claim data found.` }); + table.setAttribute(`data-claim`, "error"); + return; + } + + 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(networkId)) + .catch(console.error); +} + +function decodeClaimData(base64encodedTxData: string) { + try { + 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; + } +} + +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 index 7d416e6f..768aa04c 100644 --- a/static/scripts/rewards/render-transaction/render-transaction.ts +++ b/static/scripts/rewards/render-transaction/render-transaction.ts @@ -1,169 +1,78 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; -import { Type } from "@sinclair/typebox"; -import { Value } from "@sinclair/typebox/value"; +import { app } from "../app-state"; import { networkExplorers } from "../constants"; -import { getOptimalProvider } from "../helpers"; -import { claimButton, hideClaimButton, resetClaimButton } from "../toaster"; -import { claimErc20PermitHandler, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; +import { claimButton, hideLoader } from "../toaster"; +import { claimErc20PermitHandlerWrapper, fetchFundingWallet, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; import { claimErc721PermitHandler } from "../web3/erc721-permit"; -import { handleNetwork } from "../web3/wallet"; -import { app } from "./index"; +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 { 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"); - } -} +import { setPagination } from "./set-pagination"; type Success = boolean; -export async function renderTransaction(provider: JsonRpcProvider, nextTx?: boolean): Promise { + +export async function renderTransaction(nextTx?: boolean): Promise { const table = document.getElementsByTagName(`table`)[0]; - resetClaimButton(); if (nextTx) { - app.nextTx(); - if (!app.claimTxs || app.claimTxs.length <= 1) { + 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.currentIndex + 1}/${app.claimTxs.length} reward`; - table.setAttribute(`data-claim`, "none"); + rewardsCount.innerHTML = `${app.permitIndex + 1}/${app.claims.length} reward`; + table.setAttribute(`data-claim`, "error"); } } - if (!app.currentTx) { - hideClaimButton(); + if (!app.permit) { + hideLoader(); return false; } - handleNetwork(app.currentTx.networkId).catch(console.error); + verifyCurrentNetwork(app.permit.networkId).catch(console.error); - if (app.currentTx.type === "erc20-permit") { - const treasury = await fetchTreasury(app.currentTx, provider); + if (app.permit.type === "erc20-permit") { + const treasury = await fetchFundingWallet(app); // insert tx data into table - const requestedAmountElement = insertErc20PermitTableData(app.currentTx, table, treasury); - table.setAttribute(`data-claim`, "ok"); + const requestedAmountElement = insertErc20PermitTableData(app, table, treasury); renderTokenSymbol({ - tokenAddress: app.currentTx.permit.permitted.token, - ownerAddress: app.currentTx.owner, - amount: app.currentTx.transferDetails.requestedAmount, - explorerUrl: networkExplorers[app.currentTx.networkId], + tokenAddress: app.permit.permit.permitted.token, + ownerAddress: app.permit.owner, + amount: app.permit.transferDetails.requestedAmount, + explorerUrl: networkExplorers[app.permit.networkId], table, requestedAmountElement, - provider, + provider: app.provider, }).catch(console.error); const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.currentTx.transferDetails.to }).catch(console.error); + renderEnsName({ element: toElement, address: app.permit.transferDetails.to }).catch(console.error); - generateInvalidatePermitAdminControl(app.currentTx).catch(console.error); + generateInvalidatePermitAdminControl(app).catch(console.error); - claimButton.element.addEventListener("click", claimErc20PermitHandler(app.currentTx, optimalRPC)); - } else if (app.currentTx.type === "erc721-permit") { - const requestedAmountElement = insertErc721PermitTableData(app.currentTx, 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.currentTx.nftAddress, - explorerUrl: networkExplorers[app.currentTx.networkId], + tokenAddress: app.permit.nftAddress, + explorerUrl: networkExplorers[app.permit.networkId], table, requestedAmountElement, - provider, + provider: app.provider, }).catch(console.error); const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.currentTx.request.beneficiary }).catch(console.error); + renderEnsName({ element: toElement, address: app.permit.request.beneficiary }).catch(console.error); - claimButton.element.addEventListener("click", claimErc721PermitHandler(app.currentTx, provider)); + claimButton.element.addEventListener("click", claimErc721PermitHandler(app.permit)); } return true; diff --git a/static/scripts/rewards/render-transaction/set-pagination.ts b/static/scripts/rewards/render-transaction/set-pagination.ts new file mode 100644 index 00000000..d61977fb --- /dev/null +++ b/static/scripts/rewards/render-transaction/set-pagination.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/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-fastest-rpc-provider.ts b/static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.ts new file mode 100644 index 00000000..e0cf1e7e --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.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.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("_").slice(0, -1).join("_"); // 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/get-optimal-provider.ts b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts new file mode 100644 index 00000000..970ca565 --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts @@ -0,0 +1,21 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { AppState } from "../app-state"; +import { getFastestRpcProvider } from "./get-fastest-rpc-provider"; +import { testRpcPerformance } from "./test-rpc-performance"; + +let isTestStarted = false; +let isTestCompleted = false; + +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; + await testRpcPerformance(networkId).catch(console.error); + isTestCompleted = true; + } + + return getFastestRpcProvider(networkId); +} 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/test-rpc-performance.ts b/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts new file mode 100644 index 00000000..11d42b80 --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts @@ -0,0 +1,67 @@ +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", +}; + +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) => { + 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}_${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 raceUntilSuccess(promises); +} diff --git a/static/scripts/rewards/toaster.ts b/static/scripts/rewards/toaster.ts index 9294e720..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,38 +53,37 @@ 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"); + claimButton.element.className = "hide-cl"; } -export function hideClaimButton() { - claimButton.element.disabled = true; - claimButton.element.classList.add("hide-cl"); - 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/add-network.ts b/static/scripts/rewards/web3/add-network.ts new file mode 100644 index 00000000..1c9dd6f2 --- /dev/null +++ b/static/scripts/rewards/web3/add-network.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/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts new file mode 100644 index 00000000..b1353134 --- /dev/null +++ b/static/scripts/rewards/web3/connect-wallet.ts @@ -0,0 +1,29 @@ +import { JsonRpcSigner } from "@ethersproject/providers"; +import { ethers } from "ethers"; +import { claimButton, toaster } from "../toaster"; + +export async function connectWallet(): Promise { + try { + 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; + } + + 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 014f68fb..f6121862 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -1,22 +1,19 @@ -import { BigNumber, BigNumberish, ethers } from "ethers"; +import { JsonRpcSigner, TransactionResponse } from "@ethersproject/providers"; +import { BigNumber, BigNumberish, Contract, ethers } from "ethers"; import { permit2Abi } from "../abis"; +import { AppState } 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/render-transaction"; +import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; +import { MetaMaskError, claimButton, errorToast, showLoader, toaster } from "../toaster"; -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; @@ -41,52 +38,138 @@ export async function fetchTreasury( } } -export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcProvider) { - return async function handler() { - try { - const signer = await connectWallet(); - if (!signer) { - return; - } +async function checkPermitClaimability(app: AppState): Promise { + let isPermitClaimable = false; + try { + isPermitClaimable = await checkPermitClaimable(app); + } catch (error: unknown) { + if (error instanceof Error) { + const e = error as unknown as MetaMaskError; + console.error("Error in checkPermitClaimable: ", e); + errorToast(e, e.reason); + } + } + return isPermitClaimable; +} - if (!(await checkPermitClaimable(permit, signer, provider))) { - return; +async function createEthersContract(signer: JsonRpcSigner) { + let permit2Contract; + try { + permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); + } catch (error: unknown) { + if (error instanceof Error) { + const e = error as unknown as MetaMaskError; + console.error("Error in creating ethers.Contract: ", e); + errorToast(e, e.reason); + } + } + return permit2Contract; +} + +async function transferFromPermit(permit2Contract: Contract, app: AppState) { + const permit = app.permit; + try { + const tx = await permit2Contract.permitTransferFrom(permit, permit.transferDetails, permit.owner, permit.signature); + toaster.create("info", `Transaction sent`); + return tx; + } catch (error: unknown) { + if (error instanceof Error) { + 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); } + } + return null; + } +} - loadingClaimButton(); - const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); - const tx = await permit2Contract.permitTransferFrom(permit.permit, permit.transferDetails, permit.owner, permit.signature); - toaster.create("info", `Transaction sent`); - const receipt = await tx.wait(); - toaster.create("success", `Claim Complete.`); - console.log(receipt.transactionHash); // @TODO: post to database +async function waitForTransaction(tx: TransactionResponse) { + let receipt; + try { + receipt = await tx.wait(); + toaster.create("success", `Claim Complete.`); + console.log(receipt.transactionHash); // @TODO: post to database + } catch (error: unknown) { + if (error instanceof Error) { + const e = error as unknown as MetaMaskError; + console.error("Error in tx.wait: ", e); + errorToast(e, e.reason); + } + } + return receipt; +} - claimButton.element.removeEventListener("click", handler); - renderTransaction(provider).catch(console.error); - } catch (error: unknown) { - if (error instanceof Error) { - console.log(error); - errorToast(error, error.message); - resetClaimButton(); - } +async function renderTx() { + try { + await renderTransaction(); + } catch (error: unknown) { + if (error instanceof Error) { + const e = error as unknown as MetaMaskError; + console.error("Error in renderTransaction: ", e); + errorToast(e, e.reason); } + } +} + +export function claimErc20PermitHandlerWrapper(app: AppState) { + return async function claimErc20PermitHandler() { + showLoader(); + + const isPermitClaimable = await checkPermitClaimability(app); + if (!isPermitClaimable) return; + + const permit2Contract = await createEthersContract(app.signer); + if (!permit2Contract) return; + + const tx = await transferFromPermit(permit2Contract, app); + if (!tx) return; + + const receipt = await waitForTransaction(tx); + if (!receipt) return; + + claimButton.element.removeEventListener("click", claimErc20PermitHandler); + + await renderTx(); }; } -export async function checkPermitClaimable(permit: Erc20Permit, signer: ethers.providers.JsonRpcSigner | null, provider: JsonRpcProvider) { - const isClaimed = await isNonceClaimed(permit); +export async function checkPermitClaimable(app: AppState): Promise { + let isClaimed; + try { + isClaimed = await isNonceClaimed(app); + } catch (error: unknown) { + console.error("Error in isNonceClaimed: ", error); + return false; + } + if (isClaimed) { toaster.create("error", `Your reward for this task has already been claimed or invalidated.`); 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; } - const { balance, allowance } = await fetchTreasury(permit, provider); - const permitted = BigNumber.from(permit.permit.permitted.amount); + let treasury; + try { + treasury = await fetchFundingWallet(app); + } catch (error: unknown) { + console.error("Error in fetchTreasury: ", error); + return false; + } + + const { balance, allowance } = treasury; + const permitted = BigNumber.from(permit.permitted.amount); const isSolvent = balance.gte(permitted); const isAllowed = allowance.gte(permitted); @@ -99,28 +182,37 @@ export async function checkPermitClaimable(permit: Erc20Permit, signer: ethers.p return false; } - if (signer) { - const user = (await signer.getAddress()).toLowerCase(); - const beneficiary = permit.transferDetails.to.toLowerCase(); - if (beneficiary !== user) { - toaster.create("warning", `This reward is not for you.`); - 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; } return true; } -export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) { - const signer = await connectWallet(); - if (!signer) { - return; - } +export async function generateInvalidatePermitAdminControl(app: AppState) { + try { + const address = await app.signer.getAddress(); + const user = address.toLowerCase(); - const user = (await signer.getAddress()).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"); + console.error(error); } const controls = document.getElementById("controls") as HTMLDivElement; @@ -128,20 +220,17 @@ 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) { - console.log(error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + console.error(e); + errorToast(e, e.reason); return; } } @@ -150,13 +239,17 @@ 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); +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 bitmap = await permit2Contract.nonceBitmap(permit.owner, wordPos); + const { wordPos, bitPos } = nonceBitmap(BigNumber.from(app.permit.permit.nonce)); + + const bitmap = await permit2Contract.nonceBitmap(app.permit.owner, wordPos).catch((error: MetaMaskError) => { + console.error("Error in nonceBitmap method: ", error); + throw error; + }); const bit = BigNumber.from(1).shl(bitPos); const flipped = BigNumber.from(bitmap).xor(bit); @@ -164,7 +257,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..ffd04ff9 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 { 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, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; -import { connectWallet } from "./wallet"; +import { claimButton, errorToast, showLoader, toaster } from "../toaster"; +import { connectWallet } from "./connect-wallet"; -export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpcProvider) { +export function claimErc721PermitHandler(permit: Erc721Permit) { return async function claimButtonHandler() { const signer = await connectWallet(); if (!signer) { @@ -15,24 +16,21 @@ export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpc 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, provider); + 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); @@ -44,7 +42,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}`); }); @@ -52,7 +50,6 @@ export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpc if (error instanceof Error) { console.error(error); errorToast(error, error.message ?? error); - resetClaimButton(); } } }; 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/handle-if-on-correct-network.ts b/static/scripts/rewards/web3/handle-if-on-correct-network.ts new file mode 100644 index 00000000..eaffaa43 --- /dev/null +++ b/static/scripts/rewards/web3/handle-if-on-correct-network.ts @@ -0,0 +1,12 @@ +import invalidateButton from "../invalidate-component"; +import { showLoader } from "../toaster"; + +export function handleIfOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number) { + if (desiredNetworkId === currentNetworkId) { + // enable the button once on the correct network + invalidateButton.disabled = false; + } else { + 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 new file mode 100644 index 00000000..aff4c909 --- /dev/null +++ b/static/scripts/rewards/web3/not-on-correct-network.ts @@ -0,0 +1,20 @@ +import { ethers } from "ethers"; +import { getNetworkName } from "../constants"; +import { toaster } from "../toaster"; +import { switchNetwork } from "./switch-network"; + +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}`); + } + 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/switch-network.ts b/static/scripts/rewards/web3/switch-network.ts new file mode 100644 index 00000000..51ddae7c --- /dev/null +++ b/static/scripts/rewards/web3/switch-network.ts @@ -0,0 +1,16 @@ +import { ethers } from "ethers"; +import { addNetwork } from "./add-network"; + +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/verify-current-network.ts b/static/scripts/rewards/web3/verify-current-network.ts new file mode 100644 index 00000000..bc658e1a --- /dev/null +++ b/static/scripts/rewards/web3/verify-current-network.ts @@ -0,0 +1,24 @@ +import { ethers } from "ethers"; +import invalidateButton from "../invalidate-component"; +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."); + 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 39d67a39..00000000 --- a/static/scripts/rewards/web3/wallet.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { JsonRpcSigner } from "@ethersproject/providers"; -import { ethers } from "ethers"; -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"); - 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; - } -} - -export async function handleNetwork(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/static/styles/rewards/claim-table.css b/static/styles/rewards/claim-table.css index 121a462a..be7eb874 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%; */ @@ -151,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; @@ -239,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; } @@ -271,23 +283,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; diff --git a/tsconfig.json b/tsconfig.json index 45586ec1..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. */ diff --git a/yarn.lock b/yarn.lock index a1c70a2d..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" @@ -1752,6 +1759,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 +1843,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 +1948,16 @@ 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" + 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 +1996,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 +2072,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 +2440,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 +3090,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 +3181,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 +3397,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 +3488,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 +3544,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 +4185,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 +4238,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 +4531,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 +4598,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 +4668,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 +4918,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 +5132,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 +5212,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 +5347,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"