From 6a6f90a64e57bb57af0878d972031a61d528af10 Mon Sep 17 00:00:00 2001 From: Paul <41552663+molecula451@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:42:19 -0400 Subject: [PATCH] Revert "Sync ERC permits" --- .cspell.json | 2 - .eslintrc | 42 +-- .github/workflows/conventional-commits.yml | 1 - .github/workflows/kebab-case.yml | 15 -- .github/workflows/scripts/kebab-case.sh | 30 --- .github/workflows/scripts/kebabalize.sh | 31 --- build/esbuild-build.ts | 23 +- globals.d.ts | 14 +- package.json | 12 +- .../typescript/generate-erc20-permit-url.ts | 102 -------- .../typescript/generate-erc721-permit-url.ts | 170 ------------ scripts/typescript/generate-permit2-url.ts | 79 ++++-- static/index.html | 2 +- static/scripts/audit-report/audit.ts | 4 +- .../utils/{block-info.ts => blockInfo.ts} | 0 .../{get-transaction.ts => getTransaction.ts} | 2 +- .../{nft-reward-abi.ts => nftRewardAbi.ts} | 0 static/scripts/rewards/app-state.ts | 61 ----- static/scripts/rewards/constants.ts | 6 +- static/scripts/rewards/helpers.ts | 73 ++++++ static/scripts/rewards/index.ts | 20 ++ static/scripts/rewards/init.ts | 16 -- .../claim-rewards-pagination.ts | 34 --- .../rewards/render-transaction/index.ts | 42 +++ .../render-transaction/insert-table-data.ts | 37 +-- .../read-claim-data-from-url.ts | 76 ------ .../render-transaction/render-ens-name.ts | 2 +- .../render-transaction/render-token-symbol.ts | 6 +- .../render-transaction/render-transaction.ts | 163 +++++++++--- .../render-transaction/set-pagination.ts | 12 - .../rewards/render-transaction/tx-type.ts | 38 +-- .../get-fastest-rpc-provider.ts | 18 -- .../rpc-optimization/get-optimal-provider.ts | 26 -- .../rpc-optimization/getErc20Contract.ts | 7 - .../rpc-optimization/test-rpc-performance.ts | 67 ----- static/scripts/rewards/toaster.ts | 54 ++-- static/scripts/rewards/web3/add-network.ts | 19 -- static/scripts/rewards/web3/connect-wallet.ts | 34 --- static/scripts/rewards/web3/erc20-permit.ts | 242 ++++++------------ static/scripts/rewards/web3/erc721-permit.ts | 40 +-- .../rewards/web3/get-erc20-contract.ts | 7 - .../web3/handle-if-on-correct-network.ts | 12 - .../rewards/web3/not-on-correct-network.ts | 20 -- static/scripts/rewards/web3/switch-network.ts | 16 -- .../rewards/web3/verify-current-network.ts | 24 -- static/scripts/rewards/web3/wallet.ts | 104 ++++++++ static/styles/rewards/claim-table.css | 49 ++-- tsconfig.json | 2 +- yarn.lock | 127 +-------- 49 files changed, 669 insertions(+), 1314 deletions(-) delete mode 100644 .github/workflows/kebab-case.yml delete mode 100755 .github/workflows/scripts/kebab-case.sh delete mode 100755 .github/workflows/scripts/kebabalize.sh delete mode 100644 scripts/typescript/generate-erc20-permit-url.ts delete mode 100644 scripts/typescript/generate-erc721-permit-url.ts rename static/scripts/audit-report/utils/{block-info.ts => blockInfo.ts} (100%) rename static/scripts/audit-report/utils/{get-transaction.ts => getTransaction.ts} (94%) rename static/scripts/rewards/abis/{nft-reward-abi.ts => nftRewardAbi.ts} (100%) delete mode 100644 static/scripts/rewards/app-state.ts create mode 100644 static/scripts/rewards/helpers.ts create mode 100644 static/scripts/rewards/index.ts delete mode 100644 static/scripts/rewards/init.ts delete mode 100644 static/scripts/rewards/render-transaction/claim-rewards-pagination.ts create mode 100644 static/scripts/rewards/render-transaction/index.ts delete mode 100644 static/scripts/rewards/render-transaction/read-claim-data-from-url.ts delete mode 100644 static/scripts/rewards/render-transaction/set-pagination.ts delete mode 100644 static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.ts delete mode 100644 static/scripts/rewards/rpc-optimization/get-optimal-provider.ts delete mode 100644 static/scripts/rewards/rpc-optimization/getErc20Contract.ts delete mode 100644 static/scripts/rewards/rpc-optimization/test-rpc-performance.ts delete mode 100644 static/scripts/rewards/web3/add-network.ts delete mode 100644 static/scripts/rewards/web3/connect-wallet.ts delete mode 100644 static/scripts/rewards/web3/get-erc20-contract.ts delete mode 100644 static/scripts/rewards/web3/handle-if-on-correct-network.ts delete mode 100644 static/scripts/rewards/web3/not-on-correct-network.ts delete mode 100644 static/scripts/rewards/web3/switch-network.ts delete mode 100644 static/scripts/rewards/web3/verify-current-network.ts create mode 100644 static/scripts/rewards/web3/wallet.ts diff --git a/.cspell.json b/.cspell.json index d54182ac..09825ad7 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,7 +9,6 @@ "binsec", "chainlist", "cirip", - "Claimability", "dataurl", "devpool", "ethersproject", @@ -28,7 +27,6 @@ "servedir", "solmate", "sonarjs", - "SUPABASE", "typebox", "TYPEHASH", "ubiquibot", diff --git a/.eslintrc b/.eslintrc index 5236ba3d..22340c8a 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 a6bba39b..8d175682 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -2,7 +2,6 @@ name: Conventional Commits on: push: - pull_request: jobs: conventional-commits: diff --git a/.github/workflows/kebab-case.yml b/.github/workflows/kebab-case.yml deleted file mode 100644 index 1adda494..00000000 --- a/.github/workflows/kebab-case.yml +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100755 index fa93d7fe..00000000 --- a/.github/workflows/scripts/kebab-case.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -non_compliant_files=() -ignoreList=("^\.\/.git" "^\.\/\..*" "^\.\/[^\/]*$") -ignoreList+=("^\.\/node_modules") -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 deleted file mode 100755 index 47892d48..00000000 --- a/.github/workflows/scripts/kebabalize.sh +++ /dev/null @@ -1,31 +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" - 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 ceba3423..23d8bb48 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -1,10 +1,8 @@ -import { execSync } from "child_process"; -import * as dotenv from "dotenv"; -import esbuild from "esbuild"; import extraRpcs from "../lib/chainlist/constants/extraRpcs"; - +import esbuild from "esbuild"; +import * as dotenv from "dotenv"; const typescriptEntries = [ - "static/scripts/rewards/init.ts", + "static/scripts/rewards/index.ts", "static/scripts/audit-report/audit.ts", "static/scripts/onboarding/onboarding.ts", "static/scripts/key-generator/keygen.ts", @@ -35,10 +33,7 @@ export const esBuildContext: esbuild.BuildOptions = { ".svg": "dataurl", }, outdir: "static/out", - define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { - extraRpcs: allNetworkUrls, - commitHash: execSync(`git rev-parse --short HEAD`).toString().trim(), - }), + define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { extraRpcs: allNetworkUrls }), }; esbuild @@ -51,10 +46,10 @@ esbuild process.exit(1); }); -function createEnvDefines(environmentVariables: string[], generatedAtBuild: Record): Record { +function createEnvDefines(envVarNames: string[], extras: Record): Record { const defines: Record = {}; dotenv.config(); - for (const name of environmentVariables) { + for (const name of envVarNames) { const envVar = process.env[name]; if (envVar !== undefined) { defines[name] = JSON.stringify(envVar); @@ -62,9 +57,9 @@ function createEnvDefines(environmentVariables: string[], generatedAtBuild: Reco throw new Error(`Missing environment variable: ${name}`); } } - for (const key in generatedAtBuild) { - if (Object.prototype.hasOwnProperty.call(generatedAtBuild, key)) { - defines[key] = JSON.stringify(generatedAtBuild[key]); + for (const key in extras) { + if (Object.prototype.hasOwnProperty.call(extras, key)) { + defines[key] = JSON.stringify(extras[key]); } } return defines; diff --git a/globals.d.ts b/globals.d.ts index 1a9ff4d3..4ab599ac 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -1,7 +1,17 @@ -import { Ethereum } from "ethereum-protocol"; +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; +} declare global { interface Window { - ethereum: Ethereum; + ethereum: EthereumIsh; } } diff --git a/package.json b/package.json index 865c69d9..fb45a918 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,13 @@ "node": ">=20.10.0" }, "scripts": { - "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", + "start": "run-s utils:hash start:sign start:ui", "format": "run-s format:lint format:prettier format:cspell", - "build": "run-s utils:build", + "build": "run-s utils:hash 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 .", @@ -56,7 +55,6 @@ "@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", @@ -70,7 +68,6 @@ "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", @@ -80,8 +77,7 @@ "lint-staged": { "*.ts": [ "yarn prettier --write", - "eslint --fix", - "bash .github/workflows/scripts/kebab-case.sh" + "eslint --fix" ], "src/**.{ts,json}": [ "cspell" diff --git a/scripts/typescript/generate-erc20-permit-url.ts b/scripts/typescript/generate-erc20-permit-url.ts deleted file mode 100644 index b3d67cd2..00000000 --- a/scripts/typescript/generate-erc20-permit-url.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { MaxUint256, PermitTransferFrom, SignatureTransfer } from "@uniswap/permit2-sdk"; -import { randomBytes } from "crypto"; -import * as dotenv from "dotenv"; -import { BigNumber, ethers } from "ethers"; -import { log } from "./utils"; -dotenv.config(); - -const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all chains - -export async function generateERC20Permit() { - const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_PROVIDER_URL); - const myWallet = new ethers.Wallet(process.env.UBIQUIBOT_PRIVATE_KEY || "", provider); - - const permitTransferFromData: PermitTransferFrom = { - permitted: { - // token we are permitting to be transferred - token: process.env.PAYMENT_TOKEN_ADDRESS || "", - // amount we are permitting to be transferred - amount: ethers.utils.parseUnits(process.env.AMOUNT_IN_ETH || "", 18), - }, - // who can transfer the tokens - spender: process.env.BENEFICIARY_ADDRESS || "", - nonce: BigNumber.from(`0x${randomBytes(32).toString("hex")}`), - // signature deadline - deadline: MaxUint256, - }; - - const { domain, types, values } = SignatureTransfer.getPermitData( - permitTransferFromData, - PERMIT2_ADDRESS, - process.env.CHAIN_ID ? Number(process.env.CHAIN_ID) : 1 - ); - const signature = await myWallet._signTypedData(domain, types, values); - - const permitTransferFromData2: PermitTransferFrom = { - permitted: { - // token we are permitting to be transferred - token: process.env.PAYMENT_TOKEN_ADDRESS || "", - // amount we are permitting to be transferred - amount: ethers.utils.parseUnits("9" || "", 18), - }, - // who can transfer the tokens - spender: process.env.BENEFICIARY_ADDRESS || "", - nonce: BigNumber.from(`0x${randomBytes(32).toString("hex")}`), - // signature deadline - deadline: MaxUint256, - }; - - const { - domain: d, - types: t, - values: v, - } = SignatureTransfer.getPermitData(permitTransferFromData2, PERMIT2_ADDRESS, process.env.CHAIN_ID ? Number(process.env.CHAIN_ID) : 1); - const sig = await myWallet._signTypedData(d, t, v); - - const txData = [ - { - type: "erc20-permit", - permit: { - permitted: { - token: permitTransferFromData.permitted.token, - amount: permitTransferFromData.permitted.amount.toString(), - }, - nonce: permitTransferFromData.nonce.toString(), - deadline: permitTransferFromData.deadline.toString(), - }, - transferDetails: { - to: permitTransferFromData.spender, - requestedAmount: permitTransferFromData.permitted.amount.toString(), - }, - owner: myWallet.address, - signature: signature, - networkId: Number(process.env.CHAIN_ID), - }, - { - type: "erc20-permit", - permit: { - permitted: { - token: permitTransferFromData2.permitted.token, - amount: permitTransferFromData2.permitted.amount.toString(), - }, - nonce: permitTransferFromData2.nonce.toString(), - deadline: permitTransferFromData2.deadline.toString(), - }, - transferDetails: { - to: permitTransferFromData2.spender, - requestedAmount: permitTransferFromData2.permitted.amount.toString(), - }, - owner: myWallet.address, - signature: sig, - networkId: Number(process.env.CHAIN_ID), - }, - ]; - - const base64encodedTxData = Buffer.from(JSON.stringify(txData)).toString("base64"); - - // log.ok("ERC20 Public URL:"); - // console.log(`https://pay.ubq.fi?claim=${base64encodedTxData}`); - - log.ok("ERC20 Testing URL:"); - console.log(`${process.env.FRONTEND_URL}?claim=${base64encodedTxData}`); -} diff --git a/scripts/typescript/generate-erc721-permit-url.ts b/scripts/typescript/generate-erc721-permit-url.ts deleted file mode 100644 index 3678caf8..00000000 --- a/scripts/typescript/generate-erc721-permit-url.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { MaxUint256, PermitTransferFrom } from "@uniswap/permit2-sdk"; -import * as dotenv from "dotenv"; -import { ethers } from "ethers"; -import { log } from "./utils"; -import { solidityKeccak256 } from "ethers/lib/utils"; -dotenv.config(); - -const NFT_REWARDS_ANVIL_DEPLOYMENT = "0x38a70c040ca5f5439ad52d0e821063b0ec0b52b6"; -const ANVIL_ACC_2_PRIVATE_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; -const ANVIL_ACC_1_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; -const NFT_ADDRESS = "0xAa1bfC0e51969415d64d6dE74f27CDa0587e645b"; - -export async function generateERC721Permit() { - const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_PROVIDER_URL); - const myWallet = new ethers.Wallet(ANVIL_ACC_2_PRIVATE_KEY, provider); - - const CHAIN_ID = Number(process.env.CHAIN_ID); - const network = CHAIN_ID === 1 ? "mainnet" : CHAIN_ID === 100 ? "gnosis" : CHAIN_ID === 31337 ? "localhost" : "unknown"; - - const SIGNING_DOMAIN_NAME = "NftReward-Domain"; - const SIGNING_DOMAIN_VERSION = "1"; - const VERIFYING_CONTRACT_ADDRESS = network == "localhost" ? NFT_REWARDS_ANVIL_DEPLOYMENT : NFT_ADDRESS; - const GITHUB_CONTRIBUTION_TYPE = "issue"; - const GITHUB_ISSUE_ID = "1"; - const GITHUB_ORGANIZATION_NAME = "ubiquity"; - const GITHUB_REPOSITORY_NAME = "pay.ubq.fi"; - const GITHUB_USERNAME = "testing"; - - const erc721TransferFromData: PermitTransferFrom = { - permitted: { - token: network == "localhost" ? NFT_REWARDS_ANVIL_DEPLOYMENT : NFT_ADDRESS, - amount: 1, - }, - spender: network == "localhost" ? ANVIL_ACC_1_ADDRESS : myWallet.address, - nonce: 313327, - deadline: MaxUint256, - }; - - const erc721TransferFromData2: PermitTransferFrom = { - permitted: { - token: network == "localhost" ? NFT_REWARDS_ANVIL_DEPLOYMENT : NFT_ADDRESS, - amount: 1, - }, - spender: network == "localhost" ? ANVIL_ACC_1_ADDRESS : myWallet.address, - nonce: 3137, - deadline: MaxUint256, - }; - - const domain = { - name: SIGNING_DOMAIN_NAME, - version: SIGNING_DOMAIN_VERSION, - verifyingContract: VERIFYING_CONTRACT_ADDRESS, - chainId: CHAIN_ID, - }; - - const types = { - MintRequest: [ - { name: "beneficiary", type: "address" }, - { name: "deadline", type: "uint256" }, - { name: "keys", type: "bytes32[]" }, - { name: "nonce", type: "uint256" }, - { name: "values", type: "string[]" }, - ], - }; - - const valueBytes = [ - solidityKeccak256(["string"], [GITHUB_ORGANIZATION_NAME]), - solidityKeccak256(["string"], [GITHUB_REPOSITORY_NAME]), - solidityKeccak256(["string"], [GITHUB_ISSUE_ID]), - solidityKeccak256(["string"], [GITHUB_USERNAME]), - solidityKeccak256(["string"], [GITHUB_CONTRIBUTION_TYPE]), - ]; - - const mintRequest = { - beneficiary: network == "localhost" ? ANVIL_ACC_1_ADDRESS : myWallet.address, - deadline: MaxUint256, - keys: valueBytes, - nonce: 313327, - values: [GITHUB_ORGANIZATION_NAME, GITHUB_REPOSITORY_NAME, GITHUB_ISSUE_ID, GITHUB_USERNAME, GITHUB_CONTRIBUTION_TYPE], - }; - - const mintRequest2 = { - beneficiary: network == "localhost" ? ANVIL_ACC_1_ADDRESS : myWallet.address, - deadline: MaxUint256, - keys: valueBytes, - nonce: 3137, - values: [GITHUB_ORGANIZATION_NAME, GITHUB_REPOSITORY_NAME, GITHUB_ISSUE_ID, GITHUB_USERNAME, GITHUB_CONTRIBUTION_TYPE], - }; - - const sig = await myWallet._signTypedData(domain, types, mintRequest2); - - const signature = await myWallet._signTypedData(domain, types, mintRequest); - - const txData721 = [ - { - type: "erc721-permit", - permit: { - permitted: { - token: erc721TransferFromData.permitted.token, - amount: erc721TransferFromData.permitted.amount.toString(), - }, - nonce: erc721TransferFromData.nonce.toString(), - deadline: erc721TransferFromData.deadline.toString(), - }, - transferDetails: { - to: erc721TransferFromData.spender, - requestedAmount: erc721TransferFromData.permitted.amount.toString(), - }, - owner: myWallet.address, - signature: signature, - networkId: CHAIN_ID, - nftMetadata: { - GITHUB_ORGANIZATION_NAME, - GITHUB_REPOSITORY_NAME, - GITHUB_ISSUE_ID, - GITHUB_USERNAME, - GITHUB_CONTRIBUTION_TYPE, - }, - request: { - beneficiary: network == "localhost" ? ANVIL_ACC_1_ADDRESS : myWallet.address, - deadline: erc721TransferFromData.deadline.toString(), - keys: valueBytes, - nonce: erc721TransferFromData.nonce.toString(), - values: [GITHUB_ORGANIZATION_NAME, GITHUB_REPOSITORY_NAME, GITHUB_ISSUE_ID, GITHUB_USERNAME, GITHUB_CONTRIBUTION_TYPE], - }, - }, - { - type: "erc721-permit", - permit: { - permitted: { - token: erc721TransferFromData2.permitted.token, - amount: erc721TransferFromData2.permitted.amount.toString(), - }, - nonce: erc721TransferFromData2.nonce.toString(), - deadline: erc721TransferFromData2.deadline.toString(), - }, - transferDetails: { - to: erc721TransferFromData2.spender, - requestedAmount: erc721TransferFromData2.permitted.amount.toString(), - }, - owner: myWallet.address, - signature: sig, - networkId: CHAIN_ID, - nftMetadata: { - GITHUB_ORGANIZATION_NAME, - GITHUB_REPOSITORY_NAME, - GITHUB_ISSUE_ID, - GITHUB_USERNAME, - GITHUB_CONTRIBUTION_TYPE, - }, - request: { - beneficiary: network == "localhost" ? ANVIL_ACC_1_ADDRESS : myWallet.address, - deadline: erc721TransferFromData2.deadline.toString(), - keys: valueBytes, - nonce: erc721TransferFromData2.nonce.toString(), - values: [GITHUB_ORGANIZATION_NAME, GITHUB_REPOSITORY_NAME, GITHUB_ISSUE_ID, GITHUB_USERNAME, GITHUB_CONTRIBUTION_TYPE], - }, - }, - ]; - - const base64encodedTxData721 = Buffer.from(JSON.stringify(txData721)).toString("base64"); - - // log.ok("ERC721 Public URL:"); - // console.log(`https://pay.ubq.fi?claim=${base64encodedTxData721}`); - - // console.log("\n") - - log.ok("ERC721 Local URL:"); - console.log(`${process.env.FRONTEND_URL}?claim=${base64encodedTxData721}`); -} diff --git a/scripts/typescript/generate-permit2-url.ts b/scripts/typescript/generate-permit2-url.ts index 81512a48..7abf435d 100644 --- a/scripts/typescript/generate-permit2-url.ts +++ b/scripts/typescript/generate-permit2-url.ts @@ -1,18 +1,67 @@ -import {generateERC20Permit} from "./generate-erc20-permit-url"; -import {generateERC721Permit} from "./generate-erc721-permit-url"; -import { verifyEnvironmentVariables } from "./utils"; +import { MaxUint256, PermitTransferFrom, SignatureTransfer } from "@uniswap/permit2-sdk"; +import { randomBytes } from "crypto"; +import * as dotenv from "dotenv"; +import { BigNumber, ethers } from "ethers"; +import { log, verifyEnvironmentVariables } from "./utils"; +dotenv.config(); -(async () => { +const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all chains - generateERC721Permit().catch((error) => { - console.error(error); - verifyEnvironmentVariables(); - process.exitCode = 1; - }); +generate().catch((error) => { + console.error(error); + verifyEnvironmentVariables(); + process.exitCode = 1; +}); - generateERC20Permit().catch((error) => { - console.error(error); - verifyEnvironmentVariables(); - process.exitCode = 1; - }); -})().catch(console.error); \ No newline at end of file +async function generate() { + const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_PROVIDER_URL); + const myWallet = new ethers.Wallet(process.env.UBIQUIBOT_PRIVATE_KEY || "", provider); + + const permitTransferFromData: PermitTransferFrom = { + permitted: { + // token we are permitting to be transferred + token: process.env.PAYMENT_TOKEN_ADDRESS || "", + // amount we are permitting to be transferred + amount: ethers.utils.parseUnits(process.env.AMOUNT_IN_ETH || "", 18), + }, + // who can transfer the tokens + spender: process.env.BENEFICIARY_ADDRESS || "", + nonce: BigNumber.from(`0x${randomBytes(32).toString("hex")}`), + // signature deadline + deadline: MaxUint256, + }; + + const { domain, types, values } = SignatureTransfer.getPermitData( + permitTransferFromData, + PERMIT2_ADDRESS, + process.env.CHAIN_ID ? Number(process.env.CHAIN_ID) : 1 + ); + const signature = await myWallet._signTypedData(domain, types, values); + const txData = [ + { + type: "erc20-permit", + permit: { + permitted: { + token: permitTransferFromData.permitted.token, + amount: permitTransferFromData.permitted.amount.toString(), + }, + nonce: permitTransferFromData.nonce.toString(), + deadline: permitTransferFromData.deadline.toString(), + }, + transferDetails: { + to: permitTransferFromData.spender, + requestedAmount: permitTransferFromData.permitted.amount.toString(), + }, + owner: myWallet.address, + signature: signature, + networkId: Number(process.env.CHAIN_ID), + }, + ]; + + const base64encodedTxData = Buffer.from(JSON.stringify(txData)).toString("base64"); + log.ok("Testing URL:"); + console.log(`${process.env.FRONTEND_URL}?claim=${base64encodedTxData}`); + log.ok("Public URL:"); + console.log(`https://pay.ubq.fi?claim=${base64encodedTxData}`); + console.log(); +} diff --git a/static/index.html b/static/index.html index 4209cb33..d839f159 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 5a6a150b..ed9f862a 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/get-transaction"; +import { getTxInfo } from "./utils/getTransaction"; declare const SUPABASE_URL: string; declare const SUPABASE_ANON_KEY: string; diff --git a/static/scripts/audit-report/utils/block-info.ts b/static/scripts/audit-report/utils/blockInfo.ts similarity index 100% rename from static/scripts/audit-report/utils/block-info.ts rename to static/scripts/audit-report/utils/blockInfo.ts diff --git a/static/scripts/audit-report/utils/get-transaction.ts b/static/scripts/audit-report/utils/getTransaction.ts similarity index 94% rename from static/scripts/audit-report/utils/get-transaction.ts rename to static/scripts/audit-report/utils/getTransaction.ts index d230c710..e165e21b 100644 --- a/static/scripts/audit-report/utils/get-transaction.ts +++ b/static/scripts/audit-report/utils/getTransaction.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { Chain } from "../constants"; import { Transaction } from "../types/transaction"; -import { getBlockInfo, updateBlockInfo } from "./block-info"; +import { getBlockInfo, updateBlockInfo } from "./blockInfo"; export async function getTxInfo(hash: string, url: string, chain: Chain): Promise { try { diff --git a/static/scripts/rewards/abis/nft-reward-abi.ts b/static/scripts/rewards/abis/nftRewardAbi.ts similarity index 100% rename from static/scripts/rewards/abis/nft-reward-abi.ts rename to static/scripts/rewards/abis/nftRewardAbi.ts diff --git a/static/scripts/rewards/app-state.ts b/static/scripts/rewards/app-state.ts deleted file mode 100644 index 9a95075a..00000000 --- a/static/scripts/rewards/app-state.ts +++ /dev/null @@ -1,61 +0,0 @@ -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.reward?.networkId || null; - } - - get provider(): JsonRpcProvider { - return this._provider; - } - - set provider(value: JsonRpcProvider) { - this._provider = value; - } - - get rewardIndex(): number { - return this._currentIndex; - } - - get reward(): RewardPermit { - return this.rewardIndex < this.claims.length ? this.claims[this.rewardIndex] : this.claims[0]; - } - - get permitNetworkId() { - return this.reward?.networkId; - } - - get currentExplorerUrl(): string { - if (!this.reward) { - return "https://etherscan.io"; - } - return networkExplorers[this.reward.networkId] || "https://etherscan.io"; - } - - nextPermit(): RewardPermit | null { - this._currentIndex = Math.min(this.claims.length - 1, this.rewardIndex + 1); - return this.reward; - } - - previousPermit(): RewardPermit | null { - this._currentIndex = Math.max(0, this._currentIndex - 1); - return this.reward; - } -} - -export const app = new AppState(); diff --git a/static/scripts/rewards/constants.ts b/static/scripts/rewards/constants.ts index 54fd92d0..2794e84b 100644 --- a/static/scripts/rewards/constants.ts +++ b/static/scripts/rewards/constants.ts @@ -8,8 +8,8 @@ export enum NetworkIds { Mainnet = 1, Goerli = 5, Gnosis = 100, - Anvil = 31337, } +console.trace({ extraRpcs }); export enum Tokens { DAI = "0x6b175474e89094c44da98b954eedeac495271d0f", @@ -20,14 +20,12 @@ export const networkNames = { [NetworkIds.Mainnet]: "Ethereum Mainnet", [NetworkIds.Goerli]: "Goerli Testnet", [NetworkIds.Gnosis]: "Gnosis Chain", - [NetworkIds.Anvil]: "http://127.0.0.1:8545", }; export const networkCurrencies: Record = { [NetworkIds.Mainnet]: { symbol: "ETH", decimals: 18 }, [NetworkIds.Goerli]: { symbol: "GoerliETH", decimals: 18 }, [NetworkIds.Gnosis]: { symbol: "XDAI", decimals: 18 }, - [NetworkIds.Anvil]: { symbol: "XDAI", decimals: 18 }, }; export function getNetworkName(networkId?: number) { @@ -42,14 +40,12 @@ export const networkExplorers: Record = { [NetworkIds.Mainnet]: "https://etherscan.io", [NetworkIds.Goerli]: "https://goerli.etherscan.io", [NetworkIds.Gnosis]: "https://gnosisscan.io", - [NetworkIds.Anvil]: "https://gnosisscan.io", }; export const networkRpcs: Record = { [NetworkIds.Mainnet]: ["https://rpc-pay.ubq.fi/v1/mainnet", ...(extraRpcs[NetworkIds.Mainnet] || [])], [NetworkIds.Goerli]: ["https://rpc-pay.ubq.fi/v1/goerli", ...(extraRpcs[NetworkIds.Goerli] || [])], [NetworkIds.Gnosis]: [...(extraRpcs[NetworkIds.Gnosis] || [])], - [NetworkIds.Anvil]: ["http://127.0.0.1:8545"], }; export const permit2Address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; diff --git a/static/scripts/rewards/helpers.ts b/static/scripts/rewards/helpers.ts new file mode 100644 index 00000000..a75d2fb8 --- /dev/null +++ b/static/scripts/rewards/helpers.ts @@ -0,0 +1,73 @@ +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 new file mode 100644 index 00000000..9aa754c4 --- /dev/null +++ b/static/scripts/rewards/index.ts @@ -0,0 +1,20 @@ +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 deleted file mode 100644 index 07141cf3..00000000 --- a/static/scripts/rewards/init.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 146414ea..00000000 --- a/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts +++ /dev/null @@ -1,34 +0,0 @@ -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.rewardIndex + 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.rewardIndex + 1}/${app.claims.length} reward`; - table.setAttribute(`data-claim`, "error"); - renderTransaction(app).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.rewardIndex + 1}/${app.claims.length} reward`; - table.setAttribute(`data-claim`, "error"); - renderTransaction(app, 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 new file mode 100644 index 00000000..ee56eba6 --- /dev/null +++ b/static/scripts/rewards/render-transaction/index.ts @@ -0,0 +1,42 @@ +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 c8485b20..565bc1ce 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 { AppState, app } from "../app-state"; +import { app } from "."; import { Erc20Permit, Erc721Permit } from "./tx-type"; export function shortenAddress(address: string): string { @@ -7,22 +7,18 @@ export function shortenAddress(address: string): string { } export function insertErc20PermitTableData( - app: AppState, + permit: Erc20Permit, table: Element, treasury: { balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string } ): Element { - const reward = app.reward as Erc20Permit; const requestedAmountElement = document.getElementById("rewardAmount") as Element; - renderToFields(reward.transferDetails.to, app.currentExplorerUrl); - renderTokenFields(reward.permit.permitted.token, app.currentExplorerUrl); + renderToFields(permit.transferDetails.to, app.currentExplorerUrl); + renderTokenFields(permit.permit.permitted.token, app.currentExplorerUrl); renderDetailsFields([ - { name: "From", value: `${reward.owner}` }, + { name: "From", value: `${permit.owner}` }, { name: "Expiry", - value: (() => { - const deadline = BigNumber.isBigNumber(reward.permit.deadline) ? reward.permit.deadline : BigNumber.from(reward.permit.deadline); - return deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(deadline.toNumber()).toLocaleString() : undefined; - })(), + value: permit.permit.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(permit.permit.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" }, @@ -31,19 +27,19 @@ export function insertErc20PermitTableData( return requestedAmountElement; } -export function insertErc721PermitTableData(reward: Erc721Permit, table: Element): Element { +export function insertErc721PermitTableData(permit: Erc721Permit, table: Element): Element { const requestedAmountElement = document.getElementById("rewardAmount") as Element; - renderToFields(reward.transferDetails.to, app.currentExplorerUrl); - renderTokenFields(reward.permit.permitted.token, app.currentExplorerUrl); - const { GITHUB_REPOSITORY_NAME, GITHUB_CONTRIBUTION_TYPE, GITHUB_ISSUE_ID, GITHUB_ORGANIZATION_NAME, GITHUB_USERNAME } = reward.nftMetadata; + renderToFields(permit.request.beneficiary, app.currentExplorerUrl); + renderTokenFields(permit.nftAddress, app.currentExplorerUrl); + const { GITHUB_REPOSITORY_NAME, GITHUB_CONTRIBUTION_TYPE, GITHUB_ISSUE_ID, GITHUB_ORGANIZATION_NAME, GITHUB_USERNAME } = permit.nftMetadata; renderDetailsFields([ { name: "NFT address", - value: `${reward.permit.permitted.token}`, + value: `${permit.nftAddress}`, }, { name: "Expiry", - value: reward.permit.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(reward.permit.deadline.toNumber()).toLocaleString() : undefined, + value: permit.request.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(permit.request.deadline.toNumber()).toLocaleString() : undefined, }, { name: "GitHub Organization", @@ -84,7 +80,6 @@ function renderDetailsFields(additionalDetails: { name: string; value: string | function renderTokenFields(tokenAddress: string, explorerUrl: string) { const tokenFull = document.querySelector("#Token .full") as Element; const tokenShort = document.querySelector("#Token .short") as Element; - tokenFull.innerHTML = `
    ${tokenAddress}
    `; tokenShort.innerHTML = `
    ${shortenAddress(tokenAddress)}
    `; @@ -93,12 +88,8 @@ function renderTokenFields(tokenAddress: string, explorerUrl: string) { } function renderToFields(receiverAddress: string, explorerUrl: string) { - const toFull = document.querySelector("#rewardRecipient .full") as Element; - const toShort = document.querySelector("#rewardRecipient .short") as Element; - - // if the for address is an ENS name neither will be found - if (!toFull || !toShort) return; - + const toFull = document.querySelector("#To .full") as Element; + const toShort = document.querySelector("#To .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 deleted file mode 100644 index 6115f7f0..00000000 --- a/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts +++ /dev/null @@ -1,76 +0,0 @@ -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 { RewardPermit, claimTxT } from "./tx-type"; -import { Type } from "@sinclair/typebox"; - -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).flat(); - app.provider = await useFastestRpc(app); - const networkId = app.reward?.networkId || app.networkId; - app.signer = await connectWallet().catch(console.error); - displayRewardDetails(); - displayRewardPagination(); - - renderTransaction(app) - .then(() => verifyCurrentNetwork(networkId as number)) - .catch(console.error); -} - -function decodeClaimData(base64encodedTxData: string): RewardPermit[] { - let permit; - - try { - permit = JSON.parse(atob(base64encodedTxData)); - } catch (error) { - console.error(error); - setClaimMessage({ type: "Error", message: `1. Invalid claim data passed in URL` }); - table.setAttribute(`data-claim`, "error"); - throw error; - } - try { - return [Value.Decode(Type.Array(claimTxT), permit)]; - } catch (error) { - console.error(error); - setClaimMessage({ type: "Error", message: `2. 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 df4043ed..8be32621 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 a8fe4227..d7af472a 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 { JsonRpcProvider } from "@ethersproject/providers"; -import { MaxUint256 } from "@uniswap/permit2-sdk"; import { BigNumberish, Contract, utils } from "ethers"; -import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; +import { getErc20Contract } from "../helpers"; +import { MaxUint256 } from "@uniswap/permit2-sdk"; +import { JsonRpcProvider } from "@ethersproject/providers"; export const tokens = [ { diff --git a/static/scripts/rewards/render-transaction/render-transaction.ts b/static/scripts/rewards/render-transaction/render-transaction.ts index 4a1ef014..7d416e6f 100644 --- a/static/scripts/rewards/render-transaction/render-transaction.ts +++ b/static/scripts/rewards/render-transaction/render-transaction.ts @@ -1,85 +1,170 @@ -import { AppState } from "../app-state"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Type } from "@sinclair/typebox"; +import { Value } from "@sinclair/typebox/value"; import { networkExplorers } from "../constants"; -import { claimButton, hideLoader } from "../toaster"; -import { claimErc20PermitHandlerWrapper, fetchFundingWallet, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; +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 { verifyCurrentNetwork } from "../web3/verify-current-network"; +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 { setPagination } from "./set-pagination"; -import { Erc20Permit, RewardPermit } from "./tx-type"; +import { setClaimMessage } from "./set-claim-message"; +import { claimTxT } from "./tx-type"; +import { removeAllEventListeners } from "./utils"; -type Success = boolean; +let optimalRPC: JsonRpcProvider; -export async function renderTransaction(app: AppState, nextTx?: boolean): Promise { +export async function init() { const table = document.getElementsByTagName(`table`)[0]; - if (nextTx) { - app.nextPermit(); + // 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()); + }); - if (!app.claims || app.claims.length <= 1) { + 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.rewardIndex + 1}/${app.claims.length} reward`; + rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; + table.setAttribute(`data-claim`, "none"); } } - if (!app.reward) { - hideLoader(); - console.log("No reward found"); + if (!app.currentTx) { + hideClaimButton(); return false; } - verifyCurrentNetwork(app.reward.networkId).catch(console.error); + handleNetwork(app.currentTx.networkId).catch(console.error); - if (permitCheck(app.reward)) { - const treasury = await fetchFundingWallet(app); + if (app.currentTx.type === "erc20-permit") { + const treasury = await fetchTreasury(app.currentTx, provider); // insert tx data into table - const requestedAmountElement = insertErc20PermitTableData(app, table, treasury); + const requestedAmountElement = insertErc20PermitTableData(app.currentTx, table, treasury); + table.setAttribute(`data-claim`, "ok"); renderTokenSymbol({ - tokenAddress: app.reward.permit.permitted.token, - ownerAddress: app.reward.owner, - amount: app.reward.transferDetails.requestedAmount, - explorerUrl: networkExplorers[app.reward.networkId], + tokenAddress: app.currentTx.permit.permitted.token, + ownerAddress: app.currentTx.owner, + amount: app.currentTx.transferDetails.requestedAmount, + explorerUrl: networkExplorers[app.currentTx.networkId], table, requestedAmountElement, - provider: app.provider, + provider, }).catch(console.error); const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.reward.transferDetails.to }).catch(console.error); + renderEnsName({ element: toElement, address: app.currentTx.transferDetails.to }).catch(console.error); - generateInvalidatePermitAdminControl(app).catch(console.error); + generateInvalidatePermitAdminControl(app.currentTx).catch(console.error); - claimButton.element.addEventListener("click", claimErc20PermitHandlerWrapper(app)); - table.setAttribute(`data-claim`, "ok"); - } else { - const requestedAmountElement = insertErc721PermitTableData(app.reward, table); + 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.reward.permit.permitted.token, - explorerUrl: networkExplorers[app.reward.networkId], + tokenAddress: app.currentTx.nftAddress, + explorerUrl: networkExplorers[app.currentTx.networkId], table, requestedAmountElement, - provider: app.provider, + provider, }).catch(console.error); const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.reward.transferDetails.to }).catch(console.error); + renderEnsName({ element: toElement, address: app.currentTx.request.beneficiary }).catch(console.error); - claimButton.element.addEventListener("click", claimErc721PermitHandler(app.reward)); + claimButton.element.addEventListener("click", claimErc721PermitHandler(app.currentTx, provider)); } return true; } - -function permitCheck(permit: RewardPermit): permit is Erc20Permit { - return permit.type === "erc20-permit"; -} diff --git a/static/scripts/rewards/render-transaction/set-pagination.ts b/static/scripts/rewards/render-transaction/set-pagination.ts deleted file mode 100644 index d61977fb..00000000 --- a/static/scripts/rewards/render-transaction/set-pagination.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 93bdd438..23df1341 100644 --- a/static/scripts/rewards/render-transaction/tx-type.ts +++ b/static/scripts/rewards/render-transaction/tx-type.ts @@ -35,28 +35,20 @@ const erc20PermitT = T.Object({ }), owner: addressT, signature: signatureT, - networkId: T.Number(), + networkId: networkIdT, }); export type Erc20Permit = StaticDecode; -const erc721PermitT = T.Object({ +const erc721Permit = T.Object({ type: T.Literal("erc721-permit"), - permit: T.Object({ - permitted: T.Object({ - token: addressT, - amount: bigNumberT, - }), - nonce: bigNumberT, + request: T.Object({ + beneficiary: addressT, deadline: bigNumberT, + keys: T.Array(T.String()), + nonce: bigNumberT, + values: T.Array(T.String()), }), - transferDetails: T.Object({ - to: addressT, - requestedAmount: bigNumberT, - }), - owner: addressT, - signature: signatureT, - networkId: networkIdT, nftMetadata: T.Object({ GITHUB_ORGANIZATION_NAME: T.String(), GITHUB_REPOSITORY_NAME: T.String(), @@ -64,17 +56,13 @@ const erc721PermitT = T.Object({ GITHUB_USERNAME: T.String(), GITHUB_CONTRIBUTION_TYPE: T.String(), }), - request: T.Object({ - beneficiary: addressT, - deadline: bigNumberT, - keys: T.Array(T.String()), - nonce: bigNumberT, - values: T.Array(T.String()), - }), + nftAddress: addressT, + networkId: networkIdT, + signature: signatureT, }); -export type Erc721Permit = StaticDecode; +export type Erc721Permit = StaticDecode; -export const claimTxT = T.Union([erc20PermitT, erc721PermitT]); +export const claimTxT = T.Union([erc20PermitT, erc721Permit]); -export type RewardPermit = StaticDecode; +export type ClaimTx = 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 deleted file mode 100644 index e0cf1e7e..00000000 --- a/static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index b47a3ede..00000000 --- a/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts +++ /dev/null @@ -1,26 +0,0 @@ -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.reward.networkId || app.networkId || app.claims[0].networkId; - if (!networkId) throw new Error("Network ID not found"); - - if (networkId === 31337) - return new JsonRpcProvider("http://127.0.0.1:8545", { - name: "http://127.0.0.1:8545", - chainId: 31337, - }); - - 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 deleted file mode 100644 index f9145644..00000000 --- a/static/scripts/rewards/rpc-optimization/getErc20Contract.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 11d42b80..00000000 --- a/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts +++ /dev/null @@ -1,67 +0,0 @@ -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 da19ac6f..9294e720 100644 --- a/static/scripts/rewards/toaster.ts +++ b/static/scripts/rewards/toaster.ts @@ -10,15 +10,14 @@ 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 { @@ -53,37 +52,38 @@ function removeToast(toast: HTMLElement, timeoutId?: NodeJS.Timeout) { setTimeout(() => toast.remove(), 500); // Removing the toast after 500ms } -export function showLoader() { +export function loadingClaimButton(triggerLoader = true) { claimButton.element.disabled = true; - claimButton.element.className = "show-cl"; + // Adding this because not all disabling should trigger loading spinner + if (triggerLoader) { + claimButton.element.classList.add("show-cl"); + claimButton.element.classList.remove("hide-cl"); + } } -export function hideLoader() { +export function resetClaimButton() { claimButton.element.disabled = false; - claimButton.element.className = "hide-cl"; + claimButton.element.classList.add("hide-cl"); + claimButton.element.classList.remove("show-cl"); } -export function errorToast(error: MetaMaskError, errorMessage?: string) { - // If a custom error message is provided, use it +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); if (errorMessage) { toaster.create("error", errorMessage); - return; + } 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); } - - 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 deleted file mode 100644 index 1c9dd6f2..00000000 --- a/static/scripts/rewards/web3/add-network.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 4c949c33..00000000 --- a/static/scripts/rewards/web3/connect-wallet.ts +++ /dev/null @@ -1,34 +0,0 @@ -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); - - await wallet.send("eth_requestAccounts", []); - - 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 33dabe9a..014f68fb 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -1,32 +1,35 @@ -import { JsonRpcSigner, TransactionResponse } from "@ethersproject/providers"; -import { BigNumber, BigNumberish, Contract, ethers } from "ethers"; +import { BigNumber, BigNumberish, 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 fetchFundingWallet(app: AppState): Promise<{ balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string }> { - const reward = app.reward; +export async function fetchTreasury( + permit: Erc20Permit, + provider: JsonRpcProvider +): Promise<{ balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string }> { try { - const tokenAddress = reward.permit.permitted.token.toLowerCase(); - const tokenContract = await getErc20Contract(tokenAddress, app.provider); + const tokenAddress = permit.permit.permitted.token.toLowerCase(); + const tokenContract = await getErc20Contract(tokenAddress, provider); if (tokenAddress === tokens[0].address || tokenAddress === tokens[1].address) { const decimals = tokenAddress === tokens[0].address ? 18 : tokenAddress === tokens[1].address ? 18 : -1; const symbol = tokenAddress === tokens[0].address ? tokens[0].name : tokenAddress === tokens[1].address ? tokens[1].name : ""; - const [balance, allowance] = await Promise.all([tokenContract.balanceOf(reward.owner), tokenContract.allowance(reward.owner, permit2Address)]); + const [balance, allowance] = await Promise.all([tokenContract.balanceOf(permit.owner), tokenContract.allowance(permit.owner, permit2Address)]); return { balance, allowance, decimals, symbol }; } else { console.log(`Hardcode this token in render-token-symbol.ts and save two calls: ${tokenAddress}`); const [balance, allowance, decimals, symbol] = await Promise.all([ - tokenContract.balanceOf(reward.owner), - tokenContract.allowance(reward.owner, permit2Address), + tokenContract.balanceOf(permit.owner), + tokenContract.allowance(permit.owner, permit2Address), tokenContract.decimals(), tokenContract.symbol(), ]); @@ -38,139 +41,52 @@ export async function fetchFundingWallet(app: AppState): Promise<{ balance: BigN } } -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; -} - -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; -} +export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcProvider) { + return async function handler() { + try { + const signer = await connectWallet(); + if (!signer) { + return; + } -async function transferFromPermit(permit2Contract: Contract, app: AppState) { - const reward = app.reward; - try { - const tx = await permit2Contract.permitTransferFrom(reward.permit, reward.transferDetails, reward.owner, reward.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); + if (!(await checkPermitClaimable(permit, signer, provider))) { + return; } - } - return null; - } -} -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; -} + 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 renderTx(app: AppState) { - try { - app.claims.slice(0, 1); - await renderTransaction(app, true); - } catch (error: unknown) { - if (error instanceof Error) { - const e = error as unknown as MetaMaskError; - console.error("Error in renderTransaction: ", e); - errorToast(e, e.reason); + 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(); + } } - } -} - -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(app); }; } -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; - } - +export async function checkPermitClaimable(permit: Erc20Permit, signer: ethers.providers.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.`); return false; } - const reward = app.reward; - - if (reward.permit.deadline.lt(Math.floor(Date.now() / 1000))) { + if (permit.permit.deadline.lt(Math.floor(Date.now() / 1000))) { toaster.create("error", `This reward has expired.`); return false; } - 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(reward.permit.permitted.amount); + const { balance, allowance } = await fetchTreasury(permit, provider); + const permitted = BigNumber.from(permit.permit.permitted.amount); const isSolvent = balance.gte(permitted); const isAllowed = allowance.gte(permitted); @@ -183,37 +99,28 @@ export async function checkPermitClaimable(app: AppState): Promise { 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 = reward.transferDetails.to.toLowerCase(); - if (beneficiary !== user) { - toaster.create("warning", `This reward is not for you.`); - 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; + } } return true; } -export async function generateInvalidatePermitAdminControl(app: AppState) { - try { - const address = await app.signer.getAddress(); - const user = address.toLowerCase(); +export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) { + const signer = await connectWallet(); + if (!signer) { + return; + } - if (app.reward) { - const owner = app.reward.owner.toLowerCase(); - if (owner !== user) { - return; - } - } - } catch (error) { - console.error("Error getting address from signer"); - console.error(error); + const user = (await signer.getAddress()).toLowerCase(); + const owner = permit.owner.toLowerCase(); + if (owner !== user) { + return; } const controls = document.getElementById("controls") as HTMLDivElement; @@ -221,17 +128,20 @@ export async function generateInvalidatePermitAdminControl(app: AppState) { invalidateButton.addEventListener("click", async function invalidateButtonClickHandler() { try { - const isClaimed = await isNonceClaimed(app); + const signer = await connectWallet(); + if (!signer) { + return; + } + const isClaimed = await isNonceClaimed(permit); if (isClaimed) { toaster.create("error", `This reward has already been claimed or invalidated.`); return; } - await invalidateNonce(app.signer, app.reward.permit.nonce); + await invalidateNonce(signer, permit.permit.nonce); } catch (error: unknown) { if (error instanceof Error) { - const e = error as unknown as MetaMaskError; - console.error(e); - errorToast(e, e.reason); + console.log(error); + errorToast(error, error.message); return; } } @@ -240,17 +150,13 @@ export async function generateInvalidatePermitAdminControl(app: AppState) { } //mimics https://github.com/Uniswap/permit2/blob/a7cd186948b44f9096a35035226d7d70b9e24eaf/src/SignatureTransfer.sol#L150 -export async function isNonceClaimed(app: AppState): Promise { - const provider = app.provider; +export async function isNonceClaimed(permit: Erc20Permit): Promise { + const provider = await getOptimalProvider(permit.networkId); const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, provider); - const { wordPos, bitPos } = nonceBitmap(BigNumber.from(app.reward.permit.nonce)); - - const bitmap = await permit2Contract.nonceBitmap(app.reward.owner, wordPos).catch((error: MetaMaskError) => { - console.error("Error in nonceBitmap method: ", error); - throw error; - }); + const { wordPos, bitPos } = nonceBitmap(BigNumber.from(permit.permit.nonce)); + const bitmap = await permit2Contract.nonceBitmap(permit.owner, wordPos); const bit = BigNumber.from(1).shl(bitPos); const flipped = BigNumber.from(bitmap).xor(bit); @@ -258,7 +164,7 @@ export async function isNonceClaimed(app: AppState): Promise { return bit.and(flipped).eq(0); } -export async function invalidateNonce(signer: JsonRpcSigner, nonce: BigNumberish): Promise { +export async function invalidateNonce(signer: ethers.providers.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 25af2fb0..0dafff68 100644 --- a/static/scripts/rewards/web3/erc721-permit.ts +++ b/static/scripts/rewards/web3/erc721-permit.ts @@ -1,39 +1,42 @@ import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers"; import { ethers } from "ethers"; -import { nftRewardAbi } from "../abis/nft-reward-abi"; -import { app } from "../app-state"; +import { nftRewardAbi } from "../abis/nftRewardAbi"; import { renderTransaction } from "../render-transaction/render-transaction"; import { Erc721Permit } from "../render-transaction/tx-type"; -import { claimButton, showLoader, toaster } from "../toaster"; -import { connectWallet } from "./connect-wallet"; -export function claimErc721PermitHandler(reward: Erc721Permit) { +import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; +import { connectWallet } from "./wallet"; + +export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpcProvider) { return async function claimButtonHandler() { const signer = await connectWallet(); if (!signer) { return; } - if ((await signer.getAddress()).toLowerCase() !== reward.request.beneficiary) { + if ((await signer.getAddress()).toLowerCase() !== permit.request.beneficiary) { toaster.create("warning", `This NFT is not for you.`); + resetClaimButton(); return; } - if (reward.permit.deadline.lt(Math.floor(Date.now() / 1000))) { + if (permit.request.deadline.lt(Math.floor(Date.now() / 1000))) { toaster.create("error", `This NFT has expired.`); + resetClaimButton(); return; } - const isRedeemed = await isNonceRedeemed(reward, app.provider); + const isRedeemed = await isNonceRedeemed(permit, provider); if (isRedeemed) { toaster.create("error", `This NFT has already been redeemed.`); + resetClaimButton(); return; } - showLoader(); + loadingClaimButton(); try { - const nftContract = new ethers.Contract(reward.permit.permitted.token, nftRewardAbi, signer); + const nftContract = new ethers.Contract(permit.nftAddress, nftRewardAbi, signer); - const tx: TransactionResponse = await nftContract.safeMint(reward.request, reward.signature); + const tx: TransactionResponse = await nftContract.safeMint(permit.request, permit.signature); toaster.create("info", `Transaction sent. Waiting for confirmation...`); const receipt = await tx.wait(); toaster.create("success", `Claim Complete.`); @@ -41,18 +44,21 @@ export function claimErc721PermitHandler(reward: Erc721Permit) { claimButton.element.removeEventListener("click", claimButtonHandler); - renderTransaction(app, true).catch((error) => { + renderTransaction(provider, true).catch((error) => { console.error(error); toaster.create("error", `Error rendering transaction: ${error.message}`); }); } catch (error: unknown) { - console.error(error); - toaster.create("error", `Error claiming NFT: ${typeof error === "string" ? error : error.message ? error.message : "Unknown error"}`); + if (error instanceof Error) { + console.error(error); + errorToast(error, error.message ?? error); + resetClaimButton(); + } } }; } -export async function isNonceRedeemed(reward: Erc721Permit, provider: JsonRpcProvider): Promise { - const nftContract = new ethers.Contract(reward.permit.permitted.token, nftRewardAbi, provider); - return nftContract.nonceRedeemed(reward.request.nonce); +export async function isNonceRedeemed(nftMint: Erc721Permit, provider: JsonRpcProvider): Promise { + const nftContract = new ethers.Contract(nftMint.nftAddress, nftRewardAbi, provider); + return nftContract.nonceRedeemed(nftMint.request.nonce); } diff --git a/static/scripts/rewards/web3/get-erc20-contract.ts b/static/scripts/rewards/web3/get-erc20-contract.ts deleted file mode 100644 index f9145644..00000000 --- a/static/scripts/rewards/web3/get-erc20-contract.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index eaffaa43..00000000 --- a/static/scripts/rewards/web3/handle-if-on-correct-network.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index aff4c909..00000000 --- a/static/scripts/rewards/web3/not-on-correct-network.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 51ddae7c..00000000 --- a/static/scripts/rewards/web3/switch-network.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index bc658e1a..00000000 --- a/static/scripts/rewards/web3/verify-current-network.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 new file mode 100644 index 00000000..39d67a39 --- /dev/null +++ b/static/scripts/rewards/web3/wallet.ts @@ -0,0 +1,104 @@ +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 be7eb874..121a462a 100644 --- a/static/styles/rewards/claim-table.css +++ b/static/styles/rewards/claim-table.css @@ -14,7 +14,6 @@ justify-content: center; align-items: center; } - main > div { /* border-collapse: collapse; */ /* width: 100%; */ @@ -152,29 +151,9 @@ 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; @@ -260,19 +239,28 @@ 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; } @@ -283,6 +271,23 @@ table[data-claim="ok"] tbody { 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 31f00cd4..45586ec1 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 4de35c4b..a1c70a2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1584,13 +1584,6 @@ "@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" @@ -1759,11 +1752,6 @@ 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" @@ -1843,14 +1831,6 @@ 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" @@ -1948,16 +1928,6 @@ 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" @@ -1996,7 +1966,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== @@ -2072,21 +2042,6 @@ 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" @@ -2440,7 +2395,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, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4.3.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== @@ -3090,7 +3045,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.2, fsevents@~2.3.3: +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== @@ -3181,7 +3136,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== @@ -3397,11 +3352,6 @@ 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" @@ -3488,13 +3438,6 @@ 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" @@ -3544,7 +3487,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.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -4185,29 +4128,6 @@ 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" @@ -4238,7 +4158,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== @@ -4531,7 +4451,7 @@ picomatch@4.0.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.1.tgz#68c26c8837399e5819edce48590412ea07f17a07" integrity sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.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== @@ -4598,11 +4518,6 @@ 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" @@ -4668,13 +4583,6 @@ 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" @@ -4918,13 +4826,6 @@ 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" @@ -5132,7 +5033,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.5.0: +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -5212,13 +5113,6 @@ 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" @@ -5347,11 +5241,6 @@ 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"