diff --git a/.cspell.json b/.cspell.json index 09825ad7..d54182ac 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,6 +9,7 @@ "binsec", "chainlist", "cirip", + "Claimability", "dataurl", "devpool", "ethersproject", @@ -27,6 +28,7 @@ "servedir", "solmate", "sonarjs", + "SUPABASE", "typebox", "TYPEHASH", "ubiquibot", diff --git a/.eslintrc b/.eslintrc index 22340c8a..5236ba3d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,7 @@ "root": true, "parser": "@typescript-eslint/parser", "parserOptions": { - "project": ["./tsconfig.json"] + "project": ["./tsconfig.json"], }, "plugins": ["@typescript-eslint", "sonarjs"], "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:sonarjs/recommended"], @@ -11,15 +11,15 @@ "prefer-arrow-callback": [ "warn", { - "allowNamedFunctions": true - } + "allowNamedFunctions": true, + }, ], "func-style": [ "warn", "declaration", { - "allowArrowFunctions": false - } + "allowArrowFunctions": false, + }, ], "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-non-null-assertion": "error", @@ -35,8 +35,8 @@ "ignoreRestSiblings": true, "vars": "all", "varsIgnorePattern": "^_", - "argsIgnorePattern": "^_" - } + "argsIgnorePattern": "^_", + }, ], "@typescript-eslint/await-thenable": "error", "@typescript-eslint/no-misused-new": "error", @@ -54,55 +54,55 @@ "format": ["PascalCase"], "custom": { "regex": "^I[A-Z]", - "match": false - } + "match": false, + }, }, { "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], - "leadingUnderscore": "require" + "leadingUnderscore": "require", }, { "selector": "typeLike", - "format": ["PascalCase"] + "format": ["PascalCase"], }, { "selector": "typeParameter", "format": ["PascalCase"], - "prefix": ["T"] + "prefix": ["T"], }, { "selector": "variable", "format": ["camelCase", "UPPER_CASE"], "leadingUnderscore": "allow", - "trailingUnderscore": "allow" + "trailingUnderscore": "allow", }, { "selector": "variable", "format": ["camelCase"], "leadingUnderscore": "allow", - "trailingUnderscore": "allow" + "trailingUnderscore": "allow", }, { "selector": "variable", "modifiers": ["destructured"], - "format": null + "format": null, }, { "selector": "variable", "types": ["boolean"], "format": ["PascalCase"], - "prefix": ["is", "should", "has", "can", "did", "will", "does"] + "prefix": ["is", "should", "has", "can", "did", "will", "does"], }, { "selector": "variableLike", - "format": ["camelCase"] + "format": ["camelCase"], }, { "selector": ["function", "variable"], - "format": ["camelCase"] - } - ] - } + "format": ["camelCase"], + }, + ], + }, } diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index 8d175682..a6bba39b 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -2,6 +2,7 @@ name: Conventional Commits on: push: + pull_request: jobs: conventional-commits: diff --git a/.github/workflows/kebab-case.yml b/.github/workflows/kebab-case.yml new file mode 100644 index 00000000..1adda494 --- /dev/null +++ b/.github/workflows/kebab-case.yml @@ -0,0 +1,15 @@ +name: Enforce kebab-case + +on: + push: + pull_request: + +jobs: + check-filenames: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Check For Non Kebab-Cased TypeScript Files + run: .github/workflows/scripts/kebab-case.sh diff --git a/.github/workflows/scripts/kebab-case.sh b/.github/workflows/scripts/kebab-case.sh new file mode 100755 index 00000000..fa93d7fe --- /dev/null +++ b/.github/workflows/scripts/kebab-case.sh @@ -0,0 +1,30 @@ +#!/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 new file mode 100755 index 00000000..47892d48 --- /dev/null +++ b/.github/workflows/scripts/kebabalize.sh @@ -0,0 +1,31 @@ +#!/bin/bash +non_compliant_files=() +ignoreList=("^\.\/.git" "^\.\/\..*" "^\.\/[^\/]*$") +while IFS= read -r line; do +ignoreList+=(".*$line") +done < .gitignore +while read -r file; do +basefile=$(basename "$file") +ignoreFile=false +for pattern in "${ignoreList[@]}"; do + if [[ "$file" =~ $pattern ]]; then + ignoreFile=true + break + fi +done +if $ignoreFile; then + continue +elif ! echo "$basefile" | grep -q -E "^([a-z0-9]+-)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$|^([a-z0-9]+_)*[a-z0-9]+(\.[a-zA-Z0-9]+)?$"; then + non_compliant_files+=("$file") + echo "::warning file=$file::This file is not in kebab-case or snake_case" + newfile=$(dirname "$file")/$(echo "$basefile" | sed -r 's/([a-z0-9])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g') + mv "$file" "$newfile" +fi +done < <(find . -type f -name '*.ts' -print | grep -E '/[a-z]+[a-zA-Z]*\.ts$') +if [ ${#non_compliant_files[@]} -ne 0 ]; then +echo "The following files are not in kebab-case or snake_case:" +for file in "${non_compliant_files[@]}"; do + echo " - $file" +done +exit 1 +fi \ No newline at end of file diff --git a/build/esbuild-build.ts b/build/esbuild-build.ts index 23d8bb48..ceba3423 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -1,8 +1,10 @@ -import extraRpcs from "../lib/chainlist/constants/extraRpcs"; -import esbuild from "esbuild"; +import { execSync } from "child_process"; import * as dotenv from "dotenv"; +import esbuild from "esbuild"; +import extraRpcs from "../lib/chainlist/constants/extraRpcs"; + const typescriptEntries = [ - "static/scripts/rewards/index.ts", + "static/scripts/rewards/init.ts", "static/scripts/audit-report/audit.ts", "static/scripts/onboarding/onboarding.ts", "static/scripts/key-generator/keygen.ts", @@ -33,7 +35,10 @@ export const esBuildContext: esbuild.BuildOptions = { ".svg": "dataurl", }, outdir: "static/out", - define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { extraRpcs: allNetworkUrls }), + define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { + extraRpcs: allNetworkUrls, + commitHash: execSync(`git rev-parse --short HEAD`).toString().trim(), + }), }; esbuild @@ -46,10 +51,10 @@ esbuild process.exit(1); }); -function createEnvDefines(envVarNames: string[], extras: Record): Record { +function createEnvDefines(environmentVariables: string[], generatedAtBuild: Record): Record { const defines: Record = {}; dotenv.config(); - for (const name of envVarNames) { + for (const name of environmentVariables) { const envVar = process.env[name]; if (envVar !== undefined) { defines[name] = JSON.stringify(envVar); @@ -57,9 +62,9 @@ function createEnvDefines(envVarNames: string[], extras: Record throw new Error(`Missing environment variable: ${name}`); } } - for (const key in extras) { - if (Object.prototype.hasOwnProperty.call(extras, key)) { - defines[key] = JSON.stringify(extras[key]); + for (const key in generatedAtBuild) { + if (Object.prototype.hasOwnProperty.call(generatedAtBuild, key)) { + defines[key] = JSON.stringify(generatedAtBuild[key]); } } return defines; diff --git a/globals.d.ts b/globals.d.ts index 4ab599ac..1a9ff4d3 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -1,17 +1,7 @@ -export interface EthereumIsh { - autoRefreshOnNetworkChange: boolean; - chainId: string; - isMetaMask?: boolean; - isStatus?: boolean; - networkVersion: string; - selectedAddress: string; - - on(event: "close" | "accountsChanged" | "chainChanged" | "networkChanged", callback: (payload: unknown) => void): void; - once(event: "close" | "accountsChanged" | "chainChanged" | "networkChanged", callback: (payload: unknown) => void): void; -} +import { Ethereum } from "ethereum-protocol"; declare global { interface Window { - ethereum: EthereumIsh; + ethereum: Ethereum; } } diff --git a/package.json b/package.json index fb45a918..865c69d9 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,14 @@ "node": ">=20.10.0" }, "scripts": { - "start": "run-s utils:hash start:sign start:ui", + "start": "run-s start:sign start:ui", + "watch": "nodemon -e ts,tsx --exec yarn start", + "watch:ui": "nodemon -e ts,tsx --exec yarn start:ui", "format": "run-s format:lint format:prettier format:cspell", - "build": "run-s utils:hash utils:build", + "build": "run-s utils:build", "start:ui": "tsx build/esbuild-server.ts", "start:sign": "tsx scripts/typescript/generate-permit2-url.ts", "utils:build": "tsx build/esbuild-build.ts", - "utils:hash": "git rev-parse --short HEAD > static/commit.txt", "utils:get-invalidate-params": "forge script --via-ir scripts/solidity/GetInvalidateNonceParams.s.sol", "format:lint": "eslint --fix .", "format:prettier": "prettier --write .", @@ -55,6 +56,7 @@ "@cspell/dict-node": "^4.0.3", "@cspell/dict-software-terms": "^3.3.18", "@cspell/dict-typescript": "^3.1.2", + "@types/ethereum-protocol": "^1.0.5", "@types/node": "^20.11.19", "@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/parser": "^7.0.1", @@ -68,6 +70,7 @@ "husky": "^9.0.11", "knip": "^5.0.1", "lint-staged": "^15.2.2", + "nodemon": "^3.0.3", "npm-run-all": "^4.1.5", "prettier": "^3.2.5", "tsx": "^4.7.1", @@ -77,7 +80,8 @@ "lint-staged": { "*.ts": [ "yarn prettier --write", - "eslint --fix" + "eslint --fix", + "bash .github/workflows/scripts/kebab-case.sh" ], "src/**.{ts,json}": [ "cspell" diff --git a/scripts/typescript/generate-erc20-permit-url.ts b/scripts/typescript/generate-erc20-permit-url.ts new file mode 100644 index 00000000..b3d67cd2 --- /dev/null +++ b/scripts/typescript/generate-erc20-permit-url.ts @@ -0,0 +1,102 @@ +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 new file mode 100644 index 00000000..3678caf8 --- /dev/null +++ b/scripts/typescript/generate-erc721-permit-url.ts @@ -0,0 +1,170 @@ +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 7abf435d..81512a48 100644 --- a/scripts/typescript/generate-permit2-url.ts +++ b/scripts/typescript/generate-permit2-url.ts @@ -1,67 +1,18 @@ -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(); +import {generateERC20Permit} from "./generate-erc20-permit-url"; +import {generateERC721Permit} from "./generate-erc721-permit-url"; +import { verifyEnvironmentVariables } from "./utils"; -const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all chains +(async () => { -generate().catch((error) => { - console.error(error); - verifyEnvironmentVariables(); - process.exitCode = 1; -}); + generateERC721Permit().catch((error) => { + console.error(error); + verifyEnvironmentVariables(); + process.exitCode = 1; + }); -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(); -} + generateERC20Permit().catch((error) => { + console.error(error); + verifyEnvironmentVariables(); + process.exitCode = 1; + }); +})().catch(console.error); \ No newline at end of file diff --git a/static/index.html b/static/index.html index d839f159..4209cb33 100644 --- a/static/index.html +++ b/static/index.html @@ -178,6 +178,6 @@
    - + diff --git a/static/scripts/audit-report/audit.ts b/static/scripts/audit-report/audit.ts index ed9f862a..5a6a150b 100644 --- a/static/scripts/audit-report/audit.ts +++ b/static/scripts/audit-report/audit.ts @@ -7,13 +7,13 @@ import GoDB from "godb"; import { permit2Abi } from "../rewards/abis"; import { Chain, ChainScan, DATABASE_NAME, NULL_HASH, NULL_ID } from "./constants"; import { + RateLimitOptions, getCurrency, getGitHubUrlPartsArray, getOptimalRPC, getRandomAPIKey, populateTable, primaryRateLimitHandler, - RateLimitOptions, secondaryRateLimitHandler, } from "./helpers"; import { @@ -29,7 +29,7 @@ import { StandardInterface, TxData, } from "./types"; -import { getTxInfo } from "./utils/getTransaction"; +import { getTxInfo } from "./utils/get-transaction"; declare const SUPABASE_URL: string; declare const SUPABASE_ANON_KEY: string; diff --git a/static/scripts/audit-report/utils/blockInfo.ts b/static/scripts/audit-report/utils/block-info.ts similarity index 100% rename from static/scripts/audit-report/utils/blockInfo.ts rename to static/scripts/audit-report/utils/block-info.ts diff --git a/static/scripts/audit-report/utils/getTransaction.ts b/static/scripts/audit-report/utils/get-transaction.ts similarity index 94% rename from static/scripts/audit-report/utils/getTransaction.ts rename to static/scripts/audit-report/utils/get-transaction.ts index e165e21b..d230c710 100644 --- a/static/scripts/audit-report/utils/getTransaction.ts +++ b/static/scripts/audit-report/utils/get-transaction.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { Chain } from "../constants"; import { Transaction } from "../types/transaction"; -import { getBlockInfo, updateBlockInfo } from "./blockInfo"; +import { getBlockInfo, updateBlockInfo } from "./block-info"; export async function getTxInfo(hash: string, url: string, chain: Chain): Promise { try { diff --git a/static/scripts/rewards/abis/nftRewardAbi.ts b/static/scripts/rewards/abis/nft-reward-abi.ts similarity index 100% rename from static/scripts/rewards/abis/nftRewardAbi.ts rename to static/scripts/rewards/abis/nft-reward-abi.ts diff --git a/static/scripts/rewards/app-state.ts b/static/scripts/rewards/app-state.ts new file mode 100644 index 00000000..9a95075a --- /dev/null +++ b/static/scripts/rewards/app-state.ts @@ -0,0 +1,61 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { networkExplorers } from "./constants"; +import { RewardPermit } from "./render-transaction/tx-type"; + +export class AppState { + public claims: RewardPermit[] = []; + private _provider!: JsonRpcProvider; + private _currentIndex = 0; + private _signer; + + get signer() { + return this._signer; + } + + set signer(value) { + this._signer = value; + } + + get networkId(): number | null { + return this.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 2794e84b..54fd92d0 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,12 +20,14 @@ 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) { @@ -40,12 +42,14 @@ 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 deleted file mode 100644 index a75d2fb8..00000000 --- a/static/scripts/rewards/helpers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import axios from "axios"; -import { Contract, ethers } from "ethers"; -import { erc20Abi } from "./abis"; -import { JsonRpcProvider } from "@ethersproject/providers"; -import { networkRpcs } from "./constants"; - -type DataType = { - jsonrpc: string; - id: number; - result: { - number: string; - timestamp: string; - hash: string; - }; -}; - -function verifyBlock(data: DataType) { - try { - const { jsonrpc, id, result } = data; - const { number, timestamp, hash } = result; - return jsonrpc === "2.0" && id === 1 && parseInt(number, 16) > 0 && parseInt(timestamp, 16) > 0 && hash.match(/[0-9|a-f|A-F|x]/gm)?.join("").length === 66; - } catch (error) { - return false; - } -} - -const RPC_BODY = JSON.stringify({ - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: ["latest", false], - id: 1, -}); - -const RPC_HEADER = { - "Content-Type": "application/json", -}; - -export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { - return new ethers.Contract(contractAddress, erc20Abi, provider); -} - -export async function getOptimalProvider(networkId: number) { - const promises = networkRpcs[networkId].map(async (baseURL: string) => { - try { - const startTime = performance.now(); - const API = axios.create({ - baseURL, - headers: RPC_HEADER, - }); - - const { data } = await API.post("", RPC_BODY); - const endTime = performance.now(); - const latency = endTime - startTime; - if (verifyBlock(data)) { - return Promise.resolve({ - latency, - baseURL, - }); - } else { - return Promise.reject(); - } - } catch (error) { - return Promise.reject(); - } - }); - - const { baseURL: optimalRPC } = await Promise.any(promises); - return new ethers.providers.JsonRpcProvider(optimalRPC, { - name: optimalRPC, - chainId: networkId, - ensAddress: "", - }); -} diff --git a/static/scripts/rewards/index.ts b/static/scripts/rewards/index.ts deleted file mode 100644 index 9aa754c4..00000000 --- a/static/scripts/rewards/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { init } from "./render-transaction/render-transaction"; -import { grid } from "./the-grid"; - -(async function appAsyncWrapper() { - try { - // display commit hash - const commit = await fetch("commit.txt"); - if (commit.ok) { - const commitHash = await commit.text(); - const buildElement = document.querySelector(`#build a`) as HTMLAnchorElement; - buildElement.innerHTML = commitHash; - buildElement.href = `https://github.com/ubiquity/pay.ubq.fi/commit/${commitHash}`; - } - init().catch(console.error); - } catch (error) { - console.error(error); - } -})().catch(console.error); - -grid(document.getElementById("grid") as HTMLElement); diff --git a/static/scripts/rewards/init.ts b/static/scripts/rewards/init.ts new file mode 100644 index 00000000..07141cf3 --- /dev/null +++ b/static/scripts/rewards/init.ts @@ -0,0 +1,16 @@ +import { app } from "./app-state"; +import { readClaimDataFromUrl } from "./render-transaction/read-claim-data-from-url"; +import { grid } from "./the-grid"; + +displayCommitHash(); // @DEV: display commit hash in footer +grid(document.getElementById("grid") as HTMLElement); // @DEV: display grid background + +readClaimDataFromUrl(app).catch(console.error); // @DEV: read claim data from URL + +declare const commitHash: string; // @DEV: passed in at build time check build/esbuild-build.ts +function displayCommitHash() { + // display commit hash in footer + const buildElement = document.querySelector(`#build a`) as HTMLAnchorElement; + buildElement.innerHTML = commitHash; + buildElement.href = `https://github.com/ubiquity/pay.ubq.fi/commit/${commitHash}`; +} diff --git a/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts b/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts new file mode 100644 index 00000000..146414ea --- /dev/null +++ b/static/scripts/rewards/render-transaction/claim-rewards-pagination.ts @@ -0,0 +1,34 @@ +import { app } from "../app-state"; +import { claimButton } from "../toaster"; +import { table } from "./read-claim-data-from-url"; +import { renderTransaction } from "./render-transaction"; +import { setPagination } from "./set-pagination"; +import { removeAllEventListeners } from "./utils"; + +export function claimRewardsPagination(rewardsCount: HTMLElement) { + rewardsCount.innerHTML = `${app.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 deleted file mode 100644 index ee56eba6..00000000 --- a/static/scripts/rewards/render-transaction/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { networkExplorers } from "../constants"; -import { getOptimalProvider } from "../helpers"; -import { ClaimTx } from "./tx-type"; - -class AppState { - public claimTxs: ClaimTx[] = []; - private _currentIndex = 0; - - get currentIndex(): number { - return this._currentIndex; - } - - get currentTx(): ClaimTx | null { - return this.currentIndex < this.claimTxs.length ? this.claimTxs[this.currentIndex] : null; - } - - async currentNetworkRpc(): Promise { - if (!this.currentTx) { - return (await getOptimalProvider(1)).connection.url; - } - return (await getOptimalProvider(this.currentTx.networkId)).connection.url; - } - - get currentExplorerUrl(): string { - if (!this.currentTx) { - return "https://etherscan.io"; - } - return networkExplorers[this.currentTx.networkId] || "https://etherscan.io"; - } - - nextTx(): ClaimTx | null { - this._currentIndex = Math.min(this.claimTxs.length - 1, this._currentIndex + 1); - return this.currentTx; - } - - previousTx(): ClaimTx | null { - this._currentIndex = Math.max(0, this._currentIndex - 1); - return this.currentTx; - } -} - -export const app = new AppState(); diff --git a/static/scripts/rewards/render-transaction/insert-table-data.ts b/static/scripts/rewards/render-transaction/insert-table-data.ts index 565bc1ce..c8485b20 100644 --- a/static/scripts/rewards/render-transaction/insert-table-data.ts +++ b/static/scripts/rewards/render-transaction/insert-table-data.ts @@ -1,5 +1,5 @@ import { BigNumber, ethers } from "ethers"; -import { app } from "."; +import { AppState, app } from "../app-state"; import { Erc20Permit, Erc721Permit } from "./tx-type"; export function shortenAddress(address: string): string { @@ -7,18 +7,22 @@ export function shortenAddress(address: string): string { } export function insertErc20PermitTableData( - permit: Erc20Permit, + app: AppState, 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(permit.transferDetails.to, app.currentExplorerUrl); - renderTokenFields(permit.permit.permitted.token, app.currentExplorerUrl); + renderToFields(reward.transferDetails.to, app.currentExplorerUrl); + renderTokenFields(reward.permit.permitted.token, app.currentExplorerUrl); renderDetailsFields([ - { name: "From", value: `${permit.owner}` }, + { name: "From", value: `${reward.owner}` }, { name: "Expiry", - value: permit.permit.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(permit.permit.deadline.toNumber()).toLocaleString() : undefined, + value: (() => { + const deadline = BigNumber.isBigNumber(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; + })(), }, { 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" }, @@ -27,19 +31,19 @@ export function insertErc20PermitTableData( return requestedAmountElement; } -export function insertErc721PermitTableData(permit: Erc721Permit, table: Element): Element { +export function insertErc721PermitTableData(reward: Erc721Permit, table: Element): Element { const requestedAmountElement = document.getElementById("rewardAmount") as Element; - 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; + 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; renderDetailsFields([ { name: "NFT address", - value: `${permit.nftAddress}`, + value: `${reward.permit.permitted.token}`, }, { name: "Expiry", - value: permit.request.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(permit.request.deadline.toNumber()).toLocaleString() : undefined, + value: reward.permit.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(reward.permit.deadline.toNumber()).toLocaleString() : undefined, }, { name: "GitHub Organization", @@ -80,6 +84,7 @@ 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)}
    `; @@ -88,8 +93,12 @@ function renderTokenFields(tokenAddress: string, explorerUrl: string) { } function renderToFields(receiverAddress: string, explorerUrl: string) { - const toFull = document.querySelector("#To .full") as Element; - const toShort = document.querySelector("#To .short") as Element; + const toFull = document.querySelector("#rewardRecipient .full") as Element; + const toShort = document.querySelector("#rewardRecipient .short") as Element; + + // if the for address is an ENS name neither will be found + if (!toFull || !toShort) return; + toFull.innerHTML = `
    ${receiverAddress}
    `; toShort.innerHTML = `
    ${shortenAddress(receiverAddress)}
    `; diff --git a/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts new file mode 100644 index 00000000..6115f7f0 --- /dev/null +++ b/static/scripts/rewards/render-transaction/read-claim-data-from-url.ts @@ -0,0 +1,76 @@ +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 8be32621..df4043ed 100644 --- a/static/scripts/rewards/render-transaction/render-ens-name.ts +++ b/static/scripts/rewards/render-transaction/render-ens-name.ts @@ -1,5 +1,5 @@ +import { app } from "../app-state"; import { ensLookup } from "../cirip/ens-lookup"; -import { app } from "./index"; type EnsParams = | { diff --git a/static/scripts/rewards/render-transaction/render-token-symbol.ts b/static/scripts/rewards/render-transaction/render-token-symbol.ts index d7af472a..a8fe4227 100644 --- a/static/scripts/rewards/render-transaction/render-token-symbol.ts +++ b/static/scripts/rewards/render-transaction/render-token-symbol.ts @@ -1,7 +1,7 @@ -import { BigNumberish, Contract, utils } from "ethers"; -import { getErc20Contract } from "../helpers"; -import { MaxUint256 } from "@uniswap/permit2-sdk"; import { JsonRpcProvider } from "@ethersproject/providers"; +import { MaxUint256 } from "@uniswap/permit2-sdk"; +import { BigNumberish, Contract, utils } from "ethers"; +import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; export const tokens = [ { diff --git a/static/scripts/rewards/render-transaction/render-transaction.ts b/static/scripts/rewards/render-transaction/render-transaction.ts index 7d416e6f..4a1ef014 100644 --- a/static/scripts/rewards/render-transaction/render-transaction.ts +++ b/static/scripts/rewards/render-transaction/render-transaction.ts @@ -1,170 +1,85 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; -import { Type } from "@sinclair/typebox"; -import { Value } from "@sinclair/typebox/value"; +import { AppState } from "../app-state"; import { networkExplorers } from "../constants"; -import { getOptimalProvider } from "../helpers"; -import { claimButton, hideClaimButton, resetClaimButton } from "../toaster"; -import { claimErc20PermitHandler, fetchTreasury, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; +import { claimButton, hideLoader } from "../toaster"; +import { claimErc20PermitHandlerWrapper, fetchFundingWallet, generateInvalidatePermitAdminControl } from "../web3/erc20-permit"; import { claimErc721PermitHandler } from "../web3/erc721-permit"; -import { handleNetwork } from "../web3/wallet"; -import { app } from "./index"; +import { verifyCurrentNetwork } from "../web3/verify-current-network"; import { insertErc20PermitTableData, insertErc721PermitTableData } from "./insert-table-data"; import { renderEnsName } from "./render-ens-name"; import { renderNftSymbol, renderTokenSymbol } from "./render-token-symbol"; -import { setClaimMessage } from "./set-claim-message"; -import { claimTxT } from "./tx-type"; -import { removeAllEventListeners } from "./utils"; - -let optimalRPC: JsonRpcProvider; - -export async function init() { - const table = document.getElementsByTagName(`table`)[0]; - - // decode base64 to get tx data - const urlParams = new URLSearchParams(window.location.search); - const base64encodedTxData = urlParams.get("claim"); - - if (!base64encodedTxData) { - setClaimMessage({ type: "Notice", message: `No claim data found.` }); - table.setAttribute(`data-claim`, "none"); - return false; - } - - try { - const claimTxs = Value.Decode(Type.Array(claimTxT), JSON.parse(atob(base64encodedTxData))); - app.claimTxs = claimTxs; - optimalRPC = await getOptimalProvider(app.currentTx?.networkId ?? app.claimTxs[0].networkId); - - handleNetwork(app.currentTx?.networkId ?? app.claimTxs[0].networkId).catch(console.error); - } catch (error) { - console.error(error); - setClaimMessage({ type: "Error", message: `Invalid claim data passed in URL` }); - table.setAttribute(`data-claim`, "error"); - return false; - } - - let isDetailsVisible = false; - - table.setAttribute(`data-details-visible`, isDetailsVisible.toString()); - - const additionalDetails = document.getElementById(`additionalDetails`) as Element; - additionalDetails.addEventListener("click", () => { - isDetailsVisible = !isDetailsVisible; - table.setAttribute(`data-details-visible`, isDetailsVisible.toString()); - }); - - const rewardsCount = document.getElementById("rewardsCount"); - if (rewardsCount) { - if (!app.claimTxs || app.claimTxs.length <= 1) { - // already hidden - } else { - rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; - - const nextTxButton = document.getElementById("nextTx"); - if (nextTxButton) { - nextTxButton.addEventListener("click", () => { - claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; - app.nextTx(); - rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; - table.setAttribute(`data-claim`, "none"); - renderTransaction(optimalRPC, true).catch(console.error); - }); - } - - const prevTxButton = document.getElementById("previousTx"); - if (prevTxButton) { - prevTxButton.addEventListener("click", () => { - claimButton.element = removeAllEventListeners(claimButton.element) as HTMLButtonElement; - app.previousTx(); - rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; - table.setAttribute(`data-claim`, "none"); - renderTransaction(optimalRPC, true).catch(console.error); - }); - } - - setPagination(nextTxButton, prevTxButton); - } - } - - renderTransaction(optimalRPC, true).catch(console.error); -} - -function setPagination(nextTxButton: Element | null, prevTxButton: Element | null) { - if (!nextTxButton || !prevTxButton) return; - if (app.claimTxs.length > 1) { - prevTxButton.classList.remove("hide-pagination"); - nextTxButton.classList.remove("hide-pagination"); - - prevTxButton.classList.add("show-pagination"); - nextTxButton.classList.add("show-pagination"); - } -} +import { setPagination } from "./set-pagination"; +import { Erc20Permit, RewardPermit } from "./tx-type"; type Success = boolean; -export async function renderTransaction(provider: JsonRpcProvider, nextTx?: boolean): Promise { + +export async function renderTransaction(app: AppState, nextTx?: boolean): Promise { const table = document.getElementsByTagName(`table`)[0]; - resetClaimButton(); if (nextTx) { - app.nextTx(); - if (!app.claimTxs || app.claimTxs.length <= 1) { + app.nextPermit(); + + if (!app.claims || app.claims.length <= 1) { // already hidden } else { setPagination(document.getElementById("nextTx"), document.getElementById("previousTx")); const rewardsCount = document.getElementById("rewardsCount") as Element; - rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; - table.setAttribute(`data-claim`, "none"); + rewardsCount.innerHTML = `${app.rewardIndex + 1}/${app.claims.length} reward`; } } - if (!app.currentTx) { - hideClaimButton(); + if (!app.reward) { + hideLoader(); + console.log("No reward found"); return false; } - handleNetwork(app.currentTx.networkId).catch(console.error); + verifyCurrentNetwork(app.reward.networkId).catch(console.error); - if (app.currentTx.type === "erc20-permit") { - const treasury = await fetchTreasury(app.currentTx, provider); + if (permitCheck(app.reward)) { + const treasury = await fetchFundingWallet(app); // insert tx data into table - const requestedAmountElement = insertErc20PermitTableData(app.currentTx, table, treasury); - table.setAttribute(`data-claim`, "ok"); + const requestedAmountElement = insertErc20PermitTableData(app, table, treasury); renderTokenSymbol({ - tokenAddress: app.currentTx.permit.permitted.token, - ownerAddress: app.currentTx.owner, - amount: app.currentTx.transferDetails.requestedAmount, - explorerUrl: networkExplorers[app.currentTx.networkId], + tokenAddress: app.reward.permit.permitted.token, + ownerAddress: app.reward.owner, + amount: app.reward.transferDetails.requestedAmount, + explorerUrl: networkExplorers[app.reward.networkId], table, requestedAmountElement, - provider, + provider: app.provider, }).catch(console.error); const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.currentTx.transferDetails.to }).catch(console.error); + renderEnsName({ element: toElement, address: app.reward.transferDetails.to }).catch(console.error); - generateInvalidatePermitAdminControl(app.currentTx).catch(console.error); + generateInvalidatePermitAdminControl(app).catch(console.error); - claimButton.element.addEventListener("click", claimErc20PermitHandler(app.currentTx, optimalRPC)); - } else if (app.currentTx.type === "erc721-permit") { - const requestedAmountElement = insertErc721PermitTableData(app.currentTx, table); + claimButton.element.addEventListener("click", claimErc20PermitHandlerWrapper(app)); + table.setAttribute(`data-claim`, "ok"); + } else { + const requestedAmountElement = insertErc721PermitTableData(app.reward, table); table.setAttribute(`data-claim`, "ok"); renderNftSymbol({ - tokenAddress: app.currentTx.nftAddress, - explorerUrl: networkExplorers[app.currentTx.networkId], + tokenAddress: app.reward.permit.permitted.token, + explorerUrl: networkExplorers[app.reward.networkId], table, requestedAmountElement, - provider, + provider: app.provider, }).catch(console.error); const toElement = document.getElementById(`rewardRecipient`) as Element; - renderEnsName({ element: toElement, address: app.currentTx.request.beneficiary }).catch(console.error); + renderEnsName({ element: toElement, address: app.reward.transferDetails.to }).catch(console.error); - claimButton.element.addEventListener("click", claimErc721PermitHandler(app.currentTx, provider)); + claimButton.element.addEventListener("click", claimErc721PermitHandler(app.reward)); } 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 new file mode 100644 index 00000000..d61977fb --- /dev/null +++ b/static/scripts/rewards/render-transaction/set-pagination.ts @@ -0,0 +1,12 @@ +import { app } from "../app-state"; + +export function setPagination(nextTxButton: Element | null, prevTxButton: Element | null) { + if (!nextTxButton || !prevTxButton) return; + if (app.claims.length > 1) { + prevTxButton.classList.remove("hide-pagination"); + nextTxButton.classList.remove("hide-pagination"); + + prevTxButton.classList.add("show-pagination"); + nextTxButton.classList.add("show-pagination"); + } +} diff --git a/static/scripts/rewards/render-transaction/tx-type.ts b/static/scripts/rewards/render-transaction/tx-type.ts index 23df1341..93bdd438 100644 --- a/static/scripts/rewards/render-transaction/tx-type.ts +++ b/static/scripts/rewards/render-transaction/tx-type.ts @@ -35,20 +35,28 @@ const erc20PermitT = T.Object({ }), owner: addressT, signature: signatureT, - networkId: networkIdT, + networkId: T.Number(), }); export type Erc20Permit = StaticDecode; -const erc721Permit = T.Object({ +const erc721PermitT = T.Object({ type: T.Literal("erc721-permit"), - request: T.Object({ - beneficiary: addressT, - deadline: bigNumberT, - keys: T.Array(T.String()), + permit: T.Object({ + permitted: T.Object({ + token: addressT, + amount: bigNumberT, + }), nonce: bigNumberT, - values: T.Array(T.String()), + deadline: bigNumberT, + }), + 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(), @@ -56,13 +64,17 @@ const erc721Permit = T.Object({ GITHUB_USERNAME: T.String(), GITHUB_CONTRIBUTION_TYPE: T.String(), }), - nftAddress: addressT, - networkId: networkIdT, - signature: signatureT, + request: T.Object({ + beneficiary: addressT, + deadline: bigNumberT, + keys: T.Array(T.String()), + nonce: bigNumberT, + values: T.Array(T.String()), + }), }); -export type Erc721Permit = StaticDecode; +export type Erc721Permit = StaticDecode; -export const claimTxT = T.Union([erc20PermitT, erc721Permit]); +export const claimTxT = T.Union([erc20PermitT, erc721PermitT]); -export type ClaimTx = StaticDecode; +export type RewardPermit = StaticDecode; diff --git a/static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.ts b/static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.ts new file mode 100644 index 00000000..e0cf1e7e --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/get-fastest-rpc-provider.ts @@ -0,0 +1,18 @@ +import { ethers } from "ethers"; + +export function getFastestRpcProvider(networkId: number) { + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + + // Filter out latencies with a value of less than 0 because -1 means it failed + // Also filter out latencies that do not belong to the desired network + const validLatencies = Object.entries(latencies).filter(([key, latency]) => latency >= 0 && key.endsWith(`_${networkId}`)); + + // Get all valid latencies from localStorage and find the fastest RPC + const sortedLatencies = validLatencies.sort((a, b) => a[1] - b[1]); + const optimalRPC = sortedLatencies[0][0].split("_").slice(0, -1).join("_"); // Remove the network ID from the key + + return new ethers.providers.JsonRpcProvider(optimalRPC, { + name: optimalRPC, + chainId: networkId, + }); +} diff --git a/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts new file mode 100644 index 00000000..b47a3ede --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/get-optimal-provider.ts @@ -0,0 +1,26 @@ +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 new file mode 100644 index 00000000..f9145644 --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/getErc20Contract.ts @@ -0,0 +1,7 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Contract, ethers } from "ethers"; +import { erc20Abi } from "../abis"; + +export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { + return new ethers.Contract(contractAddress, erc20Abi, provider); +} diff --git a/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts b/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts new file mode 100644 index 00000000..11d42b80 --- /dev/null +++ b/static/scripts/rewards/rpc-optimization/test-rpc-performance.ts @@ -0,0 +1,67 @@ +import axios from "axios"; +import { networkRpcs } from "../constants"; + +type DataType = { + jsonrpc: string; + id: number; + result: { + number: string; + timestamp: string; + hash: string; + }; +}; + +function verifyBlock(data: DataType) { + try { + const { jsonrpc, id, result } = data; + const { number, timestamp, hash } = result; + return jsonrpc === "2.0" && id === 1 && parseInt(number, 16) > 0 && parseInt(timestamp, 16) > 0 && hash.match(/[0-9|a-f|A-F|x]/gm)?.join("").length === 66; + } catch (error) { + return false; + } +} + +const RPC_BODY = JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: ["latest", false], + id: 1, +}); + +const RPC_HEADER = { + "Content-Type": "application/json", +}; + +function raceUntilSuccess(promises: Promise[]) { + return new Promise((resolve) => { + promises.forEach((promise: Promise) => { + promise.then(resolve).catch(() => {}); + }); + }); +} + +export async function testRpcPerformance(networkId: number) { + const latencies: Record = JSON.parse(localStorage.getItem("rpcLatencies") || "{}"); + + const promises = networkRpcs[networkId].map(async (baseURL: string) => { + const startTime = performance.now(); + const API = axios.create({ + baseURL, + headers: RPC_HEADER, + }); + + const { data } = await API.post("", RPC_BODY); + const endTime = performance.now(); + const latency = endTime - startTime; + if (verifyBlock(data)) { + // Save the latency in localStorage + latencies[`${baseURL}_${networkId}`] = latency; + localStorage.setItem("rpcLatencies", JSON.stringify(latencies)); + } else { + // Throw an error to indicate an invalid block data + throw new Error(`Invalid block data from ${baseURL}`); + } + }); + + await raceUntilSuccess(promises); +} diff --git a/static/scripts/rewards/toaster.ts b/static/scripts/rewards/toaster.ts index 9294e720..da19ac6f 100644 --- a/static/scripts/rewards/toaster.ts +++ b/static/scripts/rewards/toaster.ts @@ -10,14 +10,15 @@ export const toaster = { }; export const claimButton = { - loading: loadingClaimButton, - reset: resetClaimButton, + // loading: loadingClaimButton, + // reset: resetClaimButton, element: document.getElementById("claimButton") as HTMLButtonElement, }; const notifications = document.querySelector(".notifications") as HTMLUListElement; export function createToast(meaning: keyof typeof toaster.icons, text: string) { + hideLoader(); const toastDetails = { timer: 5000, } as { @@ -52,38 +53,37 @@ function removeToast(toast: HTMLElement, timeoutId?: NodeJS.Timeout) { setTimeout(() => toast.remove(), 500); // Removing the toast after 500ms } -export function loadingClaimButton(triggerLoader = true) { +export function showLoader() { claimButton.element.disabled = true; - // Adding this because not all disabling should trigger loading spinner - if (triggerLoader) { - claimButton.element.classList.add("show-cl"); - claimButton.element.classList.remove("hide-cl"); - } + claimButton.element.className = "show-cl"; } -export function resetClaimButton() { +export function hideLoader() { claimButton.element.disabled = false; - claimButton.element.classList.add("hide-cl"); - claimButton.element.classList.remove("show-cl"); + claimButton.element.className = "hide-cl"; } -export function hideClaimButton() { - claimButton.element.disabled = true; - claimButton.element.classList.add("hide-cl"); - claimButton.element.classList.remove("show-cl"); -} - -type Err = { stack?: unknown; reason?: string } extends Error ? Error : { stack?: unknown; reason?: string }; - -export function errorToast(error: Err, errorMessage?: string) { - delete error.stack; - const errorData = JSON.stringify(error, null, 2); +export function errorToast(error: MetaMaskError, errorMessage?: string) { + // If a custom error message is provided, use it if (errorMessage) { toaster.create("error", errorMessage); - } else if (error?.reason) { - // parse error data to get error message - const parsedError = JSON.parse(errorData); - const _errorMessage = parsedError?.error?.message ?? parsedError?.reason; - toaster.create("error", _errorMessage); + return; } + + toaster.create("error", error.reason); } + +export type MetaMaskError = { + reason: "user rejected transaction"; + code: "ACTION_REJECTED"; + action: "sendTransaction"; + transaction: { + data: "0x30f28b7a000000000000000000000000e91d153e0b41518a2ce8dd3d7944fa863463a97d0000000000000000000000000000000000000000000000056bc75e2d631000008defcc81869c636cbdd4c06c9247db239d4368d5e14d39793cfc2047c43d9532ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000004007ce2083c7f3e18097aeb3a39bb8ec149a341d0000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000044ca15db101fd1c194467db6af0c67c6bbf4ab510000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000004165db9eaebb7ea1854531d5e23305ee72481845b6df34c458fbc4e5a0422c4c9d36a674a92f3c877a8ae7f0990e0f1b1e5a21d904d2be34fa75aa71905d940a451b00000000000000000000000000000000000000000000000000000000000000"; + to: "0x000000000022D473030F116dDEE9F6B43aC78BA3"; + from: "0x4007CE2083c7F3E18097aeB3A39bb8eC149a341d"; + gasLimit: { + type: "BigNumber"; + hex: "0x012c5a"; + }; + }; +}; diff --git a/static/scripts/rewards/web3/add-network.ts b/static/scripts/rewards/web3/add-network.ts new file mode 100644 index 00000000..1c9dd6f2 --- /dev/null +++ b/static/scripts/rewards/web3/add-network.ts @@ -0,0 +1,19 @@ +import { ethers } from "ethers"; +import { getNetworkName, networkCurrencies, networkExplorers, networkRpcs } from "../constants"; + +export async function addNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { + try { + await provider.send("wallet_addEthereumChain", [ + { + chainId: "0x" + networkId.toString(16), + chainName: getNetworkName(networkId), + rpcUrls: networkRpcs[networkId], + blockExplorerUrls: [networkExplorers[networkId]], + nativeCurrency: networkCurrencies[networkId], + }, + ]); + return true; + } catch (error: unknown) { + return false; + } +} diff --git a/static/scripts/rewards/web3/connect-wallet.ts b/static/scripts/rewards/web3/connect-wallet.ts new file mode 100644 index 00000000..4c949c33 --- /dev/null +++ b/static/scripts/rewards/web3/connect-wallet.ts @@ -0,0 +1,34 @@ +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 014f68fb..33dabe9a 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -1,35 +1,32 @@ -import { BigNumber, BigNumberish, ethers } from "ethers"; +import { JsonRpcSigner, TransactionResponse } from "@ethersproject/providers"; +import { BigNumber, BigNumberish, Contract, ethers } from "ethers"; import { permit2Abi } from "../abis"; +import { AppState } from "../app-state"; import { permit2Address } from "../constants"; -import { getErc20Contract, getOptimalProvider } from "../helpers"; -import { Erc20Permit } from "../render-transaction/tx-type"; -import { toaster, resetClaimButton, errorToast, loadingClaimButton, claimButton } from "../toaster"; -import { renderTransaction } from "../render-transaction/render-transaction"; -import { connectWallet } from "./wallet"; import invalidateButton from "../invalidate-component"; -import { JsonRpcProvider } from "@ethersproject/providers"; import { tokens } from "../render-transaction/render-token-symbol"; +import { renderTransaction } from "../render-transaction/render-transaction"; +import { getErc20Contract } from "../rpc-optimization/getErc20Contract"; +import { MetaMaskError, claimButton, errorToast, showLoader, toaster } from "../toaster"; -export async function fetchTreasury( - permit: Erc20Permit, - provider: JsonRpcProvider -): Promise<{ balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string }> { +export async function fetchFundingWallet(app: AppState): Promise<{ balance: BigNumber; allowance: BigNumber; decimals: number; symbol: string }> { + const reward = app.reward; try { - const tokenAddress = permit.permit.permitted.token.toLowerCase(); - const tokenContract = await getErc20Contract(tokenAddress, provider); + const tokenAddress = reward.permit.permitted.token.toLowerCase(); + const tokenContract = await getErc20Contract(tokenAddress, app.provider); if (tokenAddress === tokens[0].address || tokenAddress === tokens[1].address) { const decimals = tokenAddress === tokens[0].address ? 18 : tokenAddress === tokens[1].address ? 18 : -1; const symbol = tokenAddress === tokens[0].address ? tokens[0].name : tokenAddress === tokens[1].address ? tokens[1].name : ""; - const [balance, allowance] = await Promise.all([tokenContract.balanceOf(permit.owner), tokenContract.allowance(permit.owner, permit2Address)]); + const [balance, allowance] = await Promise.all([tokenContract.balanceOf(reward.owner), tokenContract.allowance(reward.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(permit.owner), - tokenContract.allowance(permit.owner, permit2Address), + tokenContract.balanceOf(reward.owner), + tokenContract.allowance(reward.owner, permit2Address), tokenContract.decimals(), tokenContract.symbol(), ]); @@ -41,52 +38,139 @@ export async function fetchTreasury( } } -export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcProvider) { - return async function handler() { - try { - const signer = await connectWallet(); - if (!signer) { - return; - } +async function checkPermitClaimability(app: AppState): Promise { + let isPermitClaimable = false; + try { + isPermitClaimable = await checkPermitClaimable(app); + } catch (error: unknown) { + if (error instanceof Error) { + const e = error as unknown as MetaMaskError; + console.error("Error in checkPermitClaimable: ", e); + errorToast(e, e.reason); + } + } + return isPermitClaimable; +} - if (!(await checkPermitClaimable(permit, signer, provider))) { - return; +async function createEthersContract(signer: JsonRpcSigner) { + let permit2Contract; + try { + permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); + } catch (error: unknown) { + if (error instanceof Error) { + const e = error as unknown as MetaMaskError; + console.error("Error in creating ethers.Contract: ", e); + errorToast(e, e.reason); + } + } + return permit2Contract; +} + +async function transferFromPermit(permit2Contract: Contract, app: AppState) { + const 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); } + } + return null; + } +} - loadingClaimButton(); - const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); - const tx = await permit2Contract.permitTransferFrom(permit.permit, permit.transferDetails, permit.owner, permit.signature); - toaster.create("info", `Transaction sent`); - const receipt = await tx.wait(); - toaster.create("success", `Claim Complete.`); - console.log(receipt.transactionHash); // @TODO: post to database +async function waitForTransaction(tx: TransactionResponse) { + let receipt; + try { + receipt = await tx.wait(); + toaster.create("success", `Claim Complete.`); + console.log(receipt.transactionHash); // @TODO: post to database + } catch (error: unknown) { + if (error instanceof Error) { + const e = error as unknown as MetaMaskError; + console.error("Error in tx.wait: ", e); + errorToast(e, e.reason); + } + } + return receipt; +} - claimButton.element.removeEventListener("click", handler); - renderTransaction(provider).catch(console.error); - } catch (error: unknown) { - if (error instanceof Error) { - console.log(error); - errorToast(error, error.message); - resetClaimButton(); - } +async function renderTx(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); } + } +} + +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(permit: Erc20Permit, signer: ethers.providers.JsonRpcSigner | null, provider: JsonRpcProvider) { - const isClaimed = await isNonceClaimed(permit); +export async function checkPermitClaimable(app: AppState): Promise { + let isClaimed; + try { + isClaimed = await isNonceClaimed(app); + } catch (error: unknown) { + console.error("Error in isNonceClaimed: ", error); + return false; + } + if (isClaimed) { toaster.create("error", `Your reward for this task has already been claimed or invalidated.`); return false; } - if (permit.permit.deadline.lt(Math.floor(Date.now() / 1000))) { + const reward = app.reward; + + if (reward.permit.deadline.lt(Math.floor(Date.now() / 1000))) { toaster.create("error", `This reward has expired.`); return false; } - const { balance, allowance } = await fetchTreasury(permit, provider); - const permitted = BigNumber.from(permit.permit.permitted.amount); + let treasury; + try { + treasury = await fetchFundingWallet(app); + } catch (error: unknown) { + console.error("Error in fetchTreasury: ", error); + return false; + } + + const { balance, allowance } = treasury; + const permitted = BigNumber.from(reward.permit.permitted.amount); const isSolvent = balance.gte(permitted); const isAllowed = allowance.gte(permitted); @@ -99,28 +183,37 @@ export async function checkPermitClaimable(permit: Erc20Permit, signer: ethers.p return false; } - if (signer) { - const user = (await signer.getAddress()).toLowerCase(); - const beneficiary = permit.transferDetails.to.toLowerCase(); - if (beneficiary !== user) { - toaster.create("warning", `This reward is not for you.`); - return false; - } + let user; + try { + user = (await app.signer.getAddress()).toLowerCase(); + } catch (error: unknown) { + console.error("Error in signer.getAddress: ", error); + return false; + } + + const beneficiary = reward.transferDetails.to.toLowerCase(); + if (beneficiary !== user) { + toaster.create("warning", `This reward is not for you.`); + return false; } return true; } -export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) { - const signer = await connectWallet(); - if (!signer) { - return; - } +export async function generateInvalidatePermitAdminControl(app: AppState) { + try { + const address = await app.signer.getAddress(); + const user = address.toLowerCase(); - const user = (await signer.getAddress()).toLowerCase(); - const owner = permit.owner.toLowerCase(); - if (owner !== user) { - return; + if (app.reward) { + const owner = app.reward.owner.toLowerCase(); + if (owner !== user) { + return; + } + } + } catch (error) { + console.error("Error getting address from signer"); + console.error(error); } const controls = document.getElementById("controls") as HTMLDivElement; @@ -128,20 +221,17 @@ export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) invalidateButton.addEventListener("click", async function invalidateButtonClickHandler() { try { - const signer = await connectWallet(); - if (!signer) { - return; - } - const isClaimed = await isNonceClaimed(permit); + const isClaimed = await isNonceClaimed(app); if (isClaimed) { toaster.create("error", `This reward has already been claimed or invalidated.`); return; } - await invalidateNonce(signer, permit.permit.nonce); + await invalidateNonce(app.signer, app.reward.permit.nonce); } catch (error: unknown) { if (error instanceof Error) { - console.log(error); - errorToast(error, error.message); + const e = error as unknown as MetaMaskError; + console.error(e); + errorToast(e, e.reason); return; } } @@ -150,13 +240,17 @@ export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) } //mimics https://github.com/Uniswap/permit2/blob/a7cd186948b44f9096a35035226d7d70b9e24eaf/src/SignatureTransfer.sol#L150 -export async function isNonceClaimed(permit: Erc20Permit): Promise { - const provider = await getOptimalProvider(permit.networkId); +export async function isNonceClaimed(app: AppState): Promise { + const provider = app.provider; const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, provider); - const { wordPos, bitPos } = nonceBitmap(BigNumber.from(permit.permit.nonce)); - const bitmap = await permit2Contract.nonceBitmap(permit.owner, wordPos); + const { wordPos, bitPos } = nonceBitmap(BigNumber.from(app.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 bit = BigNumber.from(1).shl(bitPos); const flipped = BigNumber.from(bitmap).xor(bit); @@ -164,7 +258,7 @@ export async function isNonceClaimed(permit: Erc20Permit): Promise { return bit.and(flipped).eq(0); } -export async function invalidateNonce(signer: ethers.providers.JsonRpcSigner, nonce: BigNumberish): Promise { +export async function invalidateNonce(signer: JsonRpcSigner, nonce: BigNumberish): Promise { const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); const { wordPos, bitPos } = nonceBitmap(nonce); // mimics https://github.com/ubiquity/pay.ubq.fi/blob/c9e7ed90718fe977fd9f348db27adf31d91d07fb/scripts/solidity/test/Permit2.t.sol#L428 diff --git a/static/scripts/rewards/web3/erc721-permit.ts b/static/scripts/rewards/web3/erc721-permit.ts index 0dafff68..25af2fb0 100644 --- a/static/scripts/rewards/web3/erc721-permit.ts +++ b/static/scripts/rewards/web3/erc721-permit.ts @@ -1,42 +1,39 @@ import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers"; import { ethers } from "ethers"; -import { nftRewardAbi } from "../abis/nftRewardAbi"; +import { nftRewardAbi } from "../abis/nft-reward-abi"; +import { app } from "../app-state"; import { renderTransaction } from "../render-transaction/render-transaction"; import { Erc721Permit } from "../render-transaction/tx-type"; -import { claimButton, errorToast, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; -import { connectWallet } from "./wallet"; - -export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpcProvider) { +import { claimButton, showLoader, toaster } from "../toaster"; +import { connectWallet } from "./connect-wallet"; +export function claimErc721PermitHandler(reward: Erc721Permit) { return async function claimButtonHandler() { const signer = await connectWallet(); if (!signer) { return; } - if ((await signer.getAddress()).toLowerCase() !== permit.request.beneficiary) { + if ((await signer.getAddress()).toLowerCase() !== reward.request.beneficiary) { toaster.create("warning", `This NFT is not for you.`); - resetClaimButton(); return; } - if (permit.request.deadline.lt(Math.floor(Date.now() / 1000))) { + if (reward.permit.deadline.lt(Math.floor(Date.now() / 1000))) { toaster.create("error", `This NFT has expired.`); - resetClaimButton(); return; } - const isRedeemed = await isNonceRedeemed(permit, provider); + const isRedeemed = await isNonceRedeemed(reward, app.provider); if (isRedeemed) { toaster.create("error", `This NFT has already been redeemed.`); - resetClaimButton(); return; } - loadingClaimButton(); + showLoader(); try { - const nftContract = new ethers.Contract(permit.nftAddress, nftRewardAbi, signer); + const nftContract = new ethers.Contract(reward.permit.permitted.token, nftRewardAbi, signer); - const tx: TransactionResponse = await nftContract.safeMint(permit.request, permit.signature); + const tx: TransactionResponse = await nftContract.safeMint(reward.request, reward.signature); toaster.create("info", `Transaction sent. Waiting for confirmation...`); const receipt = await tx.wait(); toaster.create("success", `Claim Complete.`); @@ -44,21 +41,18 @@ export function claimErc721PermitHandler(permit: Erc721Permit, provider: JsonRpc claimButton.element.removeEventListener("click", claimButtonHandler); - renderTransaction(provider, true).catch((error) => { + renderTransaction(app, true).catch((error) => { console.error(error); toaster.create("error", `Error rendering transaction: ${error.message}`); }); } catch (error: unknown) { - if (error instanceof Error) { - console.error(error); - errorToast(error, error.message ?? error); - resetClaimButton(); - } + console.error(error); + toaster.create("error", `Error claiming NFT: ${typeof error === "string" ? error : error.message ? error.message : "Unknown error"}`); } }; } -export async function isNonceRedeemed(nftMint: Erc721Permit, provider: JsonRpcProvider): Promise { - const nftContract = new ethers.Contract(nftMint.nftAddress, nftRewardAbi, provider); - return nftContract.nonceRedeemed(nftMint.request.nonce); +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); } diff --git a/static/scripts/rewards/web3/get-erc20-contract.ts b/static/scripts/rewards/web3/get-erc20-contract.ts new file mode 100644 index 00000000..f9145644 --- /dev/null +++ b/static/scripts/rewards/web3/get-erc20-contract.ts @@ -0,0 +1,7 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Contract, ethers } from "ethers"; +import { erc20Abi } from "../abis"; + +export async function getErc20Contract(contractAddress: string, provider: JsonRpcProvider): Promise { + return new ethers.Contract(contractAddress, erc20Abi, provider); +} diff --git a/static/scripts/rewards/web3/handle-if-on-correct-network.ts b/static/scripts/rewards/web3/handle-if-on-correct-network.ts new file mode 100644 index 00000000..eaffaa43 --- /dev/null +++ b/static/scripts/rewards/web3/handle-if-on-correct-network.ts @@ -0,0 +1,12 @@ +import invalidateButton from "../invalidate-component"; +import { showLoader } from "../toaster"; + +export function handleIfOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number) { + if (desiredNetworkId === currentNetworkId) { + // enable the button once on the correct network + invalidateButton.disabled = false; + } else { + showLoader(); + invalidateButton.disabled = true; + } +} diff --git a/static/scripts/rewards/web3/not-on-correct-network.ts b/static/scripts/rewards/web3/not-on-correct-network.ts new file mode 100644 index 00000000..aff4c909 --- /dev/null +++ b/static/scripts/rewards/web3/not-on-correct-network.ts @@ -0,0 +1,20 @@ +import { ethers } from "ethers"; +import { getNetworkName } from "../constants"; +import { toaster } from "../toaster"; +import { switchNetwork } from "./switch-network"; + +export function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number, web3provider: ethers.providers.Web3Provider) { + if (currentNetworkId !== desiredNetworkId) { + if (desiredNetworkId == void 0) { + console.error(`You must pass in an EVM network ID in the URL query parameters using the key 'network' e.g. '?network=1'`); + } + const networkName = getNetworkName(desiredNetworkId); + if (!networkName) { + toaster.create("error", `This dApp currently does not support payouts for network ID ${desiredNetworkId}`); + } + switchNetwork(web3provider, desiredNetworkId).catch((error) => { + console.error(error); + toaster.create("error", `Please switch to the ${networkName} network to claim this reward.`); + }); + } +} diff --git a/static/scripts/rewards/web3/switch-network.ts b/static/scripts/rewards/web3/switch-network.ts new file mode 100644 index 00000000..51ddae7c --- /dev/null +++ b/static/scripts/rewards/web3/switch-network.ts @@ -0,0 +1,16 @@ +import { ethers } from "ethers"; +import { addNetwork } from "./add-network"; + +export async function switchNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { + try { + await provider.send("wallet_switchEthereumChain", [{ chainId: "0x" + networkId.toString(16) }]); + return true; + } catch (error: unknown) { + // Add network if it doesn't exist. + const code = (error as { code: number }).code; + if (code == 4902) { + return await addNetwork(provider, networkId); + } + return false; + } +} diff --git a/static/scripts/rewards/web3/verify-current-network.ts b/static/scripts/rewards/web3/verify-current-network.ts new file mode 100644 index 00000000..bc658e1a --- /dev/null +++ b/static/scripts/rewards/web3/verify-current-network.ts @@ -0,0 +1,24 @@ +import { ethers } from "ethers"; +import invalidateButton from "../invalidate-component"; +import { showLoader, toaster } from "../toaster"; +import { handleIfOnCorrectNetwork } from "./handle-if-on-correct-network"; +import { notOnCorrectNetwork } from "./not-on-correct-network"; + +// verifyCurrentNetwork checks if the user is on the correct network and displays an error if not +export async function verifyCurrentNetwork(desiredNetworkId: number) { + const web3provider = new ethers.providers.Web3Provider(window.ethereum); + if (!web3provider || !web3provider.provider.isMetaMask) { + showLoader(); + toaster.create("info", "Please connect to MetaMask."); + invalidateButton.disabled = true; + } + + const network = await web3provider.getNetwork(); + const currentNetworkId = network.chainId; + + // watch for network changes + window.ethereum.on("chainChanged", (newNetworkId: T | string) => handleIfOnCorrectNetwork(parseInt(newNetworkId as string, 16), desiredNetworkId)); + + // if its not on ethereum mainnet, gnosis, or goerli, display error + notOnCorrectNetwork(currentNetworkId, desiredNetworkId, web3provider); +} diff --git a/static/scripts/rewards/web3/wallet.ts b/static/scripts/rewards/web3/wallet.ts deleted file mode 100644 index 39d67a39..00000000 --- a/static/scripts/rewards/web3/wallet.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { JsonRpcSigner } from "@ethersproject/providers"; -import { ethers } from "ethers"; -import { getNetworkName, networkCurrencies, networkExplorers, networkRpcs } from "../constants"; -import invalidateButton from "../invalidate-component"; -import { claimButton, loadingClaimButton, resetClaimButton, toaster } from "../toaster"; - -export async function connectWallet(): Promise { - try { - const provider = new ethers.providers.Web3Provider(window.ethereum, "any"); - await provider.send("eth_requestAccounts", []); - const signer = provider.getSigner(); - resetClaimButton(); - return signer; - } catch (error: unknown) { - if (error instanceof Error) { - if (error?.message?.includes("missing provider")) { - toaster.create("info", "Please use a web3 enabled browser to collect this reward."); - claimButton.element.disabled = true; - } else { - toaster.create("info", "Please connect your wallet to collect this reward."); - claimButton.element.disabled = true; - } - } - return null; - } -} - -export async function handleNetwork(desiredNetworkId: number) { - const web3provider = new ethers.providers.Web3Provider(window.ethereum); - if (!web3provider || !web3provider.provider.isMetaMask) { - toaster.create("info", "Please connect to MetaMask."); - loadingClaimButton(false); - invalidateButton.disabled = true; - } - - const network = await web3provider.getNetwork(); - const currentNetworkId = network.chainId; - - // watch for network changes - window.ethereum.on("chainChanged", (newNetworkId: T | string) => handleIfOnCorrectNetwork(parseInt(newNetworkId as string, 16), desiredNetworkId)); - - // if its not on ethereum mainnet, gnosis, or goerli, display error - notOnCorrectNetwork(currentNetworkId, desiredNetworkId, web3provider); -} - -function notOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number, web3provider: ethers.providers.Web3Provider) { - if (currentNetworkId !== desiredNetworkId) { - if (desiredNetworkId == void 0) { - console.error(`You must pass in an EVM network ID in the URL query parameters using the key 'network' e.g. '?network=1'`); - } - const networkName = getNetworkName(desiredNetworkId); - if (!networkName) { - toaster.create("error", `This dApp currently does not support payouts for network ID ${desiredNetworkId}`); - } - loadingClaimButton(false); - invalidateButton.disabled = true; - switchNetwork(web3provider, desiredNetworkId).catch((error) => { - console.error(error); - toaster.create("error", `Please switch to the ${networkName} network to claim this reward.`); - }); - } -} - -function handleIfOnCorrectNetwork(currentNetworkId: number, desiredNetworkId: number) { - if (desiredNetworkId === currentNetworkId) { - // enable the button once on the correct network - resetClaimButton(); - invalidateButton.disabled = false; - } else { - loadingClaimButton(false); - invalidateButton.disabled = true; - } -} - -export async function switchNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { - try { - await provider.send("wallet_switchEthereumChain", [{ chainId: "0x" + networkId.toString(16) }]); - return true; - } catch (error: unknown) { - // Add network if it doesn't exist. - const code = (error as { code: number }).code; - if (code == 4902) { - return await addNetwork(provider, networkId); - } - return false; - } -} - -export async function addNetwork(provider: ethers.providers.Web3Provider, networkId: number): Promise { - try { - await provider.send("wallet_addEthereumChain", [ - { - chainId: "0x" + networkId.toString(16), - chainName: getNetworkName(networkId), - rpcUrls: networkRpcs[networkId], - blockExplorerUrls: [networkExplorers[networkId]], - nativeCurrency: networkCurrencies[networkId], - }, - ]); - return true; - } catch (error: unknown) { - return false; - } -} diff --git a/static/styles/rewards/claim-table.css b/static/styles/rewards/claim-table.css index 121a462a..be7eb874 100644 --- a/static/styles/rewards/claim-table.css +++ b/static/styles/rewards/claim-table.css @@ -14,6 +14,7 @@ justify-content: center; align-items: center; } + main > div { /* border-collapse: collapse; */ /* width: 100%; */ @@ -151,9 +152,29 @@ table[data-claim-rendered] button:hover > div { display: unset; color: #fff; } + table[data-claim-rendered] button:hover > svg { display: none !important; } + +.show-cl { + display: block; +} + +table[data-claim-rendered] button.hide-cl > svg.claim-loader { + display: none; +} +table[data-claim-rendered] button.show-cl > svg.claim-icon { + display: none; +} + +table[data-claim-rendered] button.show-cl > svg.claim-loader { + display: unset; +} +table[data-claim-rendered] button.hide-cl > svg.claim-icon { + display: unset; +} + table[data-claim-rendered] button#additionalDetails { width: 100%; color: #fff; @@ -239,28 +260,19 @@ table thead { table tbody { display: none; } -table[data-claim="none"] thead { - display: table-row-group; -} table[data-claim="error"] thead { display: table-row-group; } table[data-claim="ok"] thead { display: none; } -table[data-claim="none"] tbody { - display: none; -} table[data-claim="error"] tbody { display: none; } table[data-claim="ok"] tbody { display: table-row-group; } -/* -table[data-claim-rendered="true"][data-claim="none"][data-contract-loaded="true"][data-details-visible="false"] { - border: none; -} */ + #rewardRecipient a div { opacity: 0.66; } @@ -271,23 +283,6 @@ table[data-claim-rendered="true"][data-claim="none"][data-contract-loaded="true" color: #fff; } -.show-cl { - display: block; -} - -.hide-cl > svg.claim-loader { - display: none; -} -.show-cl > svg.claim-loader { - display: unset; -} -.hide-cl > svg.claim-icon { - display: unset; -} -.show-cl > svg.claim-icon { - display: none; -} - .show-pagination { display: flex; cursor: pointer; diff --git a/tsconfig.json b/tsconfig.json index 45586ec1..31f00cd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,7 +42,7 @@ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ diff --git a/yarn.lock b/yarn.lock index a1c70a2d..4de35c4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1584,6 +1584,13 @@ "@supabase/realtime-js" "2.9.3" "@supabase/storage-js" "2.5.5" +"@types/ethereum-protocol@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/ethereum-protocol/-/ethereum-protocol-1.0.5.tgz#6ad4c2c722d440d1f59e0d7e44a0fbb5fad2c41b" + integrity sha512-4wr+t2rYbwMmDrT447SGzE/43Z0EN++zyHCBoruIx32fzXQDxVa1rnQbYwPO8sLP2OugE/L8KaAIJC5kieUuBg== + dependencies: + bignumber.js "7.2.1" + "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1752,6 +1759,11 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1831,6 +1843,14 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -1928,6 +1948,16 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== +bignumber.js@7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" + integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -1966,7 +1996,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2042,6 +2072,21 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2395,7 +2440,7 @@ data-uri-to-buffer@^3.0.1: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== -debug@4.3.4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4.3.4, debug@^4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -3045,7 +3090,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.3: +fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3136,7 +3181,7 @@ git-raw-commits@^2.0.11: split2 "^3.0.0" through2 "^4.0.0" -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -3352,6 +3397,11 @@ identity-function@^1.0.0: resolved "https://registry.yarnpkg.com/identity-function/-/identity-function-1.0.0.tgz#bea1159f0985239be3ca348edf40ce2f0dd2c21d" integrity sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + ignore@^5.1.8, ignore@^5.2.0, ignore@^5.2.4: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -3438,6 +3488,13 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -3487,7 +3544,7 @@ is-fullwidth-code-point@^5.0.0: dependencies: get-east-asian-width "^1.0.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -4128,6 +4185,29 @@ node-fetch@3.0.0-beta.9: data-uri-to-buffer "^3.0.1" fetch-blob "^2.1.1" +nodemon@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.0.3.tgz#244a62d1c690eece3f6165c6cdb0db03ebd80b76" + integrity sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== + dependencies: + abbrev "1" + normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -4158,7 +4238,7 @@ normalize-package-data@^6.0.0: semver "^7.3.5" validate-npm-package-license "^3.0.4" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -4451,7 +4531,7 @@ picomatch@4.0.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.1.tgz#68c26c8837399e5819edce48590412ea07f17a07" integrity sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg== -picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -4518,6 +4598,11 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -4583,6 +4668,13 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -4826,6 +4918,13 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -5033,7 +5132,7 @@ summary@2.1.0: resolved "https://registry.yarnpkg.com/summary/-/summary-2.1.0.tgz#be8a49a0aa34eb6ceea56042cae88f8add4b0885" integrity sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw== -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -5113,6 +5212,13 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -5241,6 +5347,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"