From 53de54f2868f33dce55ce6d2f0d84d96d8a608a7 Mon Sep 17 00:00:00 2001 From: Keyrxng <106303466+Keyrxng@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:16:34 +0000 Subject: [PATCH] chore: remove other apps, update readme (#186) * chore: remove other apps, update readme * chore: the htmls too --- README.md | 25 +- build/esbuild-build.ts | 9 +- scripts/typescript/generate-permit2-url.ts | 27 +- static/audit.html | 73 -- static/keygen.html | 40 - static/onboarding.html | 76 -- static/scripts/audit-report/audit.ts | 750 ------------------ static/scripts/audit-report/constants.ts | 30 - static/scripts/audit-report/helpers.ts | 187 ----- static/scripts/audit-report/types/audit.d.ts | 130 --- static/scripts/audit-report/types/index.ts | 1 - .../scripts/audit-report/types/transaction.ts | 6 - .../scripts/audit-report/utils/block-info.ts | 9 - .../audit-report/utils/get-transaction.ts | 35 - static/scripts/key-generator/keygen.ts | 134 ---- static/scripts/onboarding/onboarding.ts | 513 ------------ 16 files changed, 35 insertions(+), 2010 deletions(-) delete mode 100644 static/audit.html delete mode 100644 static/keygen.html delete mode 100644 static/onboarding.html delete mode 100644 static/scripts/audit-report/audit.ts delete mode 100644 static/scripts/audit-report/constants.ts delete mode 100644 static/scripts/audit-report/helpers.ts delete mode 100644 static/scripts/audit-report/types/audit.d.ts delete mode 100644 static/scripts/audit-report/types/index.ts delete mode 100644 static/scripts/audit-report/types/transaction.ts delete mode 100644 static/scripts/audit-report/utils/block-info.ts delete mode 100644 static/scripts/audit-report/utils/get-transaction.ts delete mode 100644 static/scripts/key-generator/keygen.ts delete mode 100644 static/scripts/onboarding/onboarding.ts diff --git a/README.md b/README.md index 8038e097..57c60dcb 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,26 @@ http://localhost:8080?claim=eyJwZXJtaXQiOnsicGVybWl0dGVkIjp7InRva2VuIjoiMHgxMWZF ## How to test locally 1. Set `.env` variables. -2. In the project root run `npx http-server`. Set `env.FRONTEND_URL` to `http://localhost:8080`. -3. Run `npx tsx generate-permit2-url.ts` to generate an offline permit URL. -4. Open the generated permit URL in the `localhost`. -5. Connect the bounty hunter's address. -6. Click the "withdraw" button to get a reward. +2. Run `anvil --chain-id 31337 --fork-url https://rpc.gnosis.gateway.fm` in a separate terminal. +3. Run the Anvil commands (uses the Anvil default wallets). +4. In the project root run `yarn start`. +5. A permit URL for both ERC20 and ERC721 is generated in the terminal. +6. Open the generated permit URL defaulting to the variable values in the `.env` file. +7. Connect the bounty hunter's address. +8. Click the "withdraw" button to get a reward. +9. Testing the ERC721 permit is easiest deploying the `nft-rewards` contract from the [repository](https://github.com/ubiquity/nft-rewards) + +#### Anvil commands + +###### Using any other `--chain-id` will hit real RPC endpoints. + +``` +cast rpc anvil_impersonateAccount 0xba12222222228d8ba445958a75a0704d566bf2c8 & +cast send 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d --unlocked --from 0xba12222222228d8ba445958a75a0704d566bf2c8 "transfer(address,uint256)(bool)" 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 337888400000000000000000 & +cast send 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d --unlocked --from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 "approve(address,uint256)(bool)" 0x000000000022D473030F116dDEE9F6B43aC78BA3 9999999999999991111111119999999999999999 & +cast send 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d --unlocked --from 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 "approve(address,uint256)(bool)" 0x000000000022D473030F116dDEE9F6B43aC78BA3 999999999999999111119999999999999999 + +``` ## CloudFlare Setup (GitHub Secrets) diff --git a/build/esbuild-build.ts b/build/esbuild-build.ts index ceba3423..921a3014 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -3,13 +3,8 @@ import * as dotenv from "dotenv"; import esbuild from "esbuild"; import extraRpcs from "../lib/chainlist/constants/extraRpcs"; -const typescriptEntries = [ - "static/scripts/rewards/init.ts", - "static/scripts/audit-report/audit.ts", - "static/scripts/onboarding/onboarding.ts", - "static/scripts/key-generator/keygen.ts", -]; -const cssEntries = ["static/styles/rewards/rewards.css", "static/styles/audit-report/audit.css", "static/styles/onboarding/onboarding.css"]; +const typescriptEntries = ["static/scripts/rewards/init.ts"]; +const cssEntries = ["static/styles/rewards/rewards.css"]; export const entries = [...typescriptEntries, ...cssEntries]; const allNetworkUrls: Record = {}; diff --git a/scripts/typescript/generate-permit2-url.ts b/scripts/typescript/generate-permit2-url.ts index 81512a48..5fa91f88 100644 --- a/scripts/typescript/generate-permit2-url.ts +++ b/scripts/typescript/generate-permit2-url.ts @@ -1,18 +1,17 @@ -import {generateERC20Permit} from "./generate-erc20-permit-url"; -import {generateERC721Permit} from "./generate-erc721-permit-url"; +import { generateERC20Permit } from "./generate-erc20-permit-url"; +import { generateERC721Permit } from "./generate-erc721-permit-url"; import { verifyEnvironmentVariables } from "./utils"; (async () => { + generateERC721Permit().catch((error) => { + console.error(error); + verifyEnvironmentVariables(); + process.exitCode = 1; + }); - generateERC721Permit().catch((error) => { - console.error(error); - verifyEnvironmentVariables(); - process.exitCode = 1; - }); - - generateERC20Permit().catch((error) => { - console.error(error); - verifyEnvironmentVariables(); - process.exitCode = 1; - }); -})().catch(console.error); \ No newline at end of file + generateERC20Permit().catch((error) => { + console.error(error); + verifyEnvironmentVariables(); + process.exitCode = 1; + }); +})().catch(console.error); diff --git a/static/audit.html b/static/audit.html deleted file mode 100644 index bf505dc1..00000000 --- a/static/audit.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - Audit report - - - - - - -
-

Audit report

- -
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - -
⚑Cache
-
-
- -
- - - - - - - - - - - - - - - -
-
- - - - diff --git a/static/keygen.html b/static/keygen.html deleted file mode 100644 index 331f075d..00000000 --- a/static/keygen.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - Key Generator - - - - - -
-

Key Generator

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - - -
-
- - - diff --git a/static/onboarding.html b/static/onboarding.html deleted file mode 100644 index f4e32aba..00000000 --- a/static/onboarding.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - Onboarding - - - - - -
-

Onboarding

-
-
- 1 - CONFIG -
-
- 2 - APPROVE -
-
-
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- -
ubiquibot-config.yml
- -
-
- - -
- - -
- -
- -
-
- - - diff --git a/static/scripts/audit-report/audit.ts b/static/scripts/audit-report/audit.ts deleted file mode 100644 index 5a6a150b..00000000 --- a/static/scripts/audit-report/audit.ts +++ /dev/null @@ -1,750 +0,0 @@ -import { throttling } from "@octokit/plugin-throttling"; -import { Octokit } from "@octokit/rest"; -import { createClient } from "@supabase/supabase-js"; -import axios from "axios"; -import { ethers } from "ethers"; -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, - secondaryRateLimitHandler, -} from "./helpers"; -import { - ChainScanResult, - ElemInterface, - EtherInterface, - GitHubUrlParts, - GitInterface, - GoDBSchema, - ObserverKeys, - QuickImport, - SavedData, - StandardInterface, - TxData, -} from "./types"; -import { getTxInfo } from "./utils/get-transaction"; - -declare const SUPABASE_URL: string; -declare const SUPABASE_ANON_KEY: string; - -const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); - -const rateOctokit = Octokit.plugin(throttling); - -let octokit: Octokit; - -let BOT_WALLET_ADDRESS = ""; -let REPOSITORY_URL = ""; -let GITHUB_PAT = ""; - -const repoArray: string[] = []; - -const resultTableElem = document.querySelector("#resultTable") as HTMLElement; -const resultTableTbodyElem = document.querySelector("#resultTable tbody") as HTMLTableCellElement; -const getReportElem = document.querySelector("#getReport") as HTMLButtonElement; -const reportLoader = document.querySelector("#report-loader") as HTMLElement; -const tgBtnInput = document.querySelector("#cb4") as HTMLInputElement; - -let isCache = true; - -// TODO: should be generated directly from the Supabase db schema -interface Permit { - id: number; - created: Date; - updated: Date; - amount: string; - nonce: string; - deadline: string; - signature: string; - token_id: number; - partner_id: null | number; - beneficiary_id: number; - transaction: string; - location_id: number; - locations: { - id: number; - node_id: string; - node_type: string; - updated: Date; - created: Date; - node_url: string; - user_id: number; - repository_id: number; - organization_id: number; - comment_id: number; - issue_id: number; - }; - users: { - id: number; - created: string; - updated: string; - wallet_id: number; - location_id: number; - wallets: { - id: number; - created: string; - updated: Date | null; - address: string; - location_id: number | null; - }; - }; - tokens: { - id: 1; - created: string; - updated: string; - network: number; - address: string; - location_id: null | number; - }; - owner: string; - repo: string; - network_id: number; -} - -const permitList: Permit[] = []; - -let isGit = true; -const offset = 100; - -let isEther = true; -const ETHER_INTERVAL = 250; -let etherPageNumber = 1; - -let isRPC = true; -const RPC_INTERVAL = 50; -const permit2Interface = new ethers.utils.Interface(permit2Abi); -const permitTransferFromSelector = "0x30f28b7a"; -const permitFunctionName = "permitTransferFrom"; -const elemList: ElemInterface[] = []; - -let gitID = NULL_ID; -let etherHash = NULL_HASH; - -let lastGitID: number | boolean = false; -let lastEtherHash: string | boolean = false; - -function getDataSchema(storeHash: string) { - const schema: GoDBSchema = { - [NULL_HASH]: { - id: { - type: String, - unique: true, - }, - hash: { - type: String, - unique: false, - }, - issue: { - type: Number, - unique: false, - }, - }, - [storeHash]: { - id: { - type: Number, - unique: true, - }, - tx: { - type: String, - unique: false, - }, - amount: { - type: String, - unique: false, - }, - title: { - type: String, - unique: false, - }, - }, - }; - - return schema; -} - -function parseAndAddUrls(input: string): void { - const urls = input.split(",").map((url) => url.trim()); - repoArray.push(...urls); -} - -async function updateDB(storeHash: string) { - const schema = getDataSchema(storeHash); - const cacheDB = new GoDB(DATABASE_NAME, schema); - const metaTable = cacheDB.table(NULL_HASH); - const storeTable = cacheDB.table(storeHash); - - const metaData = { - // unknown as number because the only time it receives a string is initiating the db - // and it is always a number after that according to the schema definition - // [NULL_HASH]: id: storeHash - // [STORE_HASH]: id: storeHash - id: storeHash as unknown as number, - hash: lastEtherHash !== etherHash ? (lastEtherHash as string) : (etherHash as string), - issue: lastGitID !== gitID ? (lastGitID as number) : (gitID as number), - }; - - await metaTable.put(metaData); - if (elemList.length > 0) { - for (const elem of elemList) { - const { id, tx, amount, title, bounty_hunter, network, owner, repo } = elem; - await storeTable.put({ - id, - tx, - amount, - title, - bounty_hunter, - network, - owner, - repo, - }); - } - } - return cacheDB.close(); -} - -async function readDB(storeHash: string) { - const schema = getDataSchema(storeHash); - const cacheDB = new GoDB(DATABASE_NAME, schema); - const storeTable = cacheDB.table(storeHash); - const tableData = await storeTable.getAll(); - cacheDB.close(); - return tableData; -} - -async function readMeta(storeHash: string) { - const schema = getDataSchema(storeHash); - const cacheDB = new GoDB(DATABASE_NAME, schema); - const metaTable = cacheDB.table(NULL_HASH); - const metaData = await metaTable.get({ id: storeHash }); - cacheDB.close(); - return metaData; -} - -function toggleLoader(type: "none" | "block") { - getReportElem.disabled = type === "block" ? true : false; - reportLoader.style.display = type; -} - -class QueueObserver { - private readonly _queueObject: { - isRPC: boolean; - isComment: boolean; - isGit: boolean; - isEther: boolean; - }; - private _isException; - - constructor() { - this._queueObject = { - isRPC: false, - isComment: false, - isGit: false, - isEther: false, - }; - this._isException = false; - } - - private _databaseCallback() { - const storeHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`${REPOSITORY_URL}_${BOT_WALLET_ADDRESS}`)); - updateDB(storeHash).catch((error) => console.error(error)); - } - - private _callback() { - toggleLoader("none"); - if (!this._isException) { - this._databaseCallback(); - } - } - - mutate(key: ObserverKeys, value: boolean) { - this._queueObject[key] = value; - const { isRPC, isComment, isGit, isEther } = this._queueObject; - const isUpdateFinished = isRPC && isComment && isGit && isEther; - if (isUpdateFinished) { - this._callback(); - } - } - - clearQueue() { - this._queueObject.isComment = false; - this._queueObject.isEther = false; - this._queueObject.isGit = false; - this._queueObject.isRPC = false; - } - - raise() { - this._isException = true; - } -} - -const finishedQueue = new QueueObserver(); - -class SmartQueue { - private readonly _queue: Map; - - constructor() { - this._queue = new Map(); - } - - add(key: string, value: StandardInterface) { - if (this._queue.has(key)) { - const queueValue = this._queue.get(key) as StandardInterface; - queueValue.s[value.t] = value.s[value.t] as object extends GitInterface ? GitInterface : object extends EtherInterface ? EtherInterface : never; - const { - s: { ether, git, network }, - c: { amount }, - } = queueValue; - - // check for undefined - if (git?.issue_number) { - elemList.push({ - id: git.issue_number, - tx: ether?.txHash || "N/A", // @TODO - handle this better - amount: ethers.utils.formatEther(amount), - title: git.issue_title, - bounty_hunter: git.bounty_hunter, - owner: git.owner, - repo: git.repo, - network, - }); - if (elemList.length > 0) { - resultTableTbodyElem.innerHTML = ""; - for (const data of elemList) { - populateTable(data?.owner, data?.repo, data?.id, data?.network, data?.tx, data?.title, data?.amount, data?.bounty_hunter); - } - } - } - this._queue.delete(key); - } else { - this._queue.set(key, value); - } - } -} -type QueueItem = ChainScanResult; -type Queue = QueueItem extends string ? string[] : QueueItem[]; - -class QueueSet { - private readonly _queue: Queue; - private readonly _set: Set>; - - constructor() { - this._queue = []; - this._set = new Set(); - } - - add(item: NonNullable) { - if (!this._set.has(item)) { - this._set.add(item); - this._queue.push(item as QueueItem); - } - } - - remove() { - const v = this._queue.shift(); - if (v) this._set.delete(v); - } - - read(): ChainScanResult { - return this._queue[0]; - } - - isEmpty() { - return this._queue.length === 0; - } -} - -const updateQueue = new SmartQueue(); -const rpcQueue = new QueueSet(); - -async function getPermitsForRepo(owner: string, repo: string) { - const permitList: Permit[] = []; - try { - const { data: gitData } = await octokit.rest.repos.get({ - owner, - repo, - }); - const { data } = await supabase - .from("permits") - .select("*, locations(*), users(*, wallets(*)), tokens(*)") - .eq("locations.repository_id", gitData?.id) - .not("locations", "is", null); - if (data) { - permitList.push(...data.map((d) => ({ ...d, owner, repo }))); - } - } catch (error) { - console.error(error); - throw error; - } - - return permitList; -} - -async function gitFetcher(repoUrls: GitHubUrlParts[]) { - if (isGit) { - try { - const permitsPromises = repoUrls.map((repoUrl) => getPermitsForRepo(repoUrl.owner, repoUrl.repo)); - const allPermits = await Promise.all(permitsPromises); - - for (let i = 0; i < allPermits.length; i++) { - const issues = allPermits[i]; - permitList.push(...issues); - console.log(`Fetched ${issues.length} permits for repository ${repoUrls[i].owner}/${repoUrls[i].repo}`); - } - isGit = false; - finishedQueue.mutate("isGit", true); - for (const permit of permitList) { - const { data: userData } = await octokit.request("GET /user/:id", { id: permit.locations.user_id }); - const { data } = await supabase.from("locations").select("*").eq("issue_id", permit.locations.issue_id).single(); - const lastSlashIndex = data.node_url.lastIndexOf("/"); - const hashIndex = data.node_url.lastIndexOf("#") || data.node_url.length; - const issueNumber = Number(data.node_url.substring(lastSlashIndex + 1, hashIndex)); - const { data: issueData } = await octokit.rest.issues.get({ - issue_number: issueNumber, - owner: permit.owner, - repo: permit.repo, - }); - updateQueue.add(permit.signature, { - c: { - amount: permit.amount, - deadline: permit.deadline, - nonce: permit.nonce, - owner: permit.owner, - signature: permit.signature, - to: permit.users.wallets.address, - token: permit.tokens.address, - }, - k: permit.signature, - s: { - ether: undefined, - git: { - bounty_hunter: { - name: userData.login, - url: userData.html_url, - }, - issue_number: issueData.number, - issue_title: issueData.title, - owner: permit.owner, - repo: permit.repo, - }, - network: getCurrency(permit.network_id) || Chain.Ethereum, - }, - t: "git", - }); - } - finishedQueue.mutate("isComment", true); - } catch (error) { - console.error(`Error fetching issues: ${error}`); - finishedQueue.mutate("isComment", true); - } - - return permitList; - } -} - -async function fetchDataFromChainScanAPI(url: string, chain: string) { - try { - const { data } = await axios.get(url); - return data.result.map((item: NonNullable) => ({ ...item, chain })); - } catch (error: unknown) { - console.error(error); - throw error; - } -} - -async function etherFetcher() { - const ethereumURL = `https://api.${ChainScan.Ethereum}/api?module=account&action=tokentx&address=${BOT_WALLET_ADDRESS}&apikey=${getRandomAPIKey( - Chain.Ethereum - )}&page=${etherPageNumber}&offset=${offset}&sort=desc`; - - const gnosisURL = `https://api.${ChainScan.Gnosis}/api?module=account&action=tokentx&address=${BOT_WALLET_ADDRESS}&apikey=${getRandomAPIKey( - Chain.Gnosis - )}&page=${etherPageNumber}&offset=${offset}&sort=desc`; - - if (isEther) { - const etherIntervalID = setInterval(async () => { - clearInterval(etherIntervalID); - try { - const [ethereumData, gnosisData] = await Promise.all([ - fetchDataFromChainScanAPI(ethereumURL, Chain.Ethereum), - fetchDataFromChainScanAPI(gnosisURL, Chain.Gnosis), - ]); - - const combinedData: ChainScanResult[] = [...ethereumData, ...gnosisData]; - await handleCombinedData(combinedData); - } catch (error: unknown) { - console.error(error); - finishedQueue.raise(); - isEther = false; - finishedQueue.mutate("isEther", true); - } - }, ETHER_INTERVAL); - } -} - -async function handleCombinedData(combinedData: ChainScanResult[]) { - if (combinedData.length > 0) { - if (!lastEtherHash) { - lastEtherHash = combinedData[0].hash; - } - let isIEF = true; - for (const e of combinedData) { - if (e.hash !== etherHash) { - rpcQueue.add({ hash: e.hash, chain: e.chain }); - } else { - isIEF = false; - break; - } - } - - if (isIEF) { - etherPageNumber++; - await etherFetcher(); - } else { - isEther = false; - finishedQueue.mutate("isEther", true); - } - } else { - isEther = false; - finishedQueue.mutate("isEther", true); - } -} - -async function rpcFetcher() { - if (isRPC) { - const rpcIntervalID = setInterval(async () => { - clearInterval(rpcIntervalID); - try { - const data = rpcQueue.read(); - await handleRPCData(data); - rpcQueue.remove(); - if (isEther || !rpcQueue.isEmpty()) { - await rpcFetcher(); - } else { - isRPC = false; - finishedQueue.mutate("isRPC", true); - } - } catch (error: unknown) { - console.error(error); - finishedQueue.raise(); - rpcQueue.remove(); - if (isEther || !rpcQueue.isEmpty()) { - await rpcFetcher(); - } else { - isRPC = false; - finishedQueue.mutate("isRPC", true); - } - } - }, RPC_INTERVAL); - } -} - -async function handleRPCData(data: ChainScanResult) { - if (data) { - const { hash, chain } = data as { hash: string; chain: string }; - const providerUrl = await getOptimalRPC(chain as Chain); - const txInfo = await getTxInfo(hash, providerUrl, chain as Chain); - - if (txInfo.input.startsWith(permitTransferFromSelector)) { - const decodedFunctionData = permit2Interface.decodeFunctionData(permitFunctionName, txInfo.input); - const { - permit: { - permitted: { token, amount }, - nonce, - deadline, - }, - transferDetails: { to }, - owner, - signature, - } = decodedFunctionData as unknown as TxData; - updateQueue.add(signature, { - k: signature, - t: "ether", - c: { - nonce, - owner, - token, - amount, - to, - deadline, - signature, - }, - s: { - ether: { - txHash: txInfo.hash, - timestamp: parseInt(txInfo.timestamp, 16), - block_number: parseInt(txInfo.blockNumber, 16), - }, - git: undefined, - network: chain as string, - }, - }); - } - } -} - -async function dbInit() { - if (isCache) { - const storeHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`${REPOSITORY_URL}_${BOT_WALLET_ADDRESS}`)); - const metaData = await readMeta(storeHash); - - if (metaData !== undefined) { - const { hash, issue } = metaData; - gitID = issue as number; - etherHash = hash as string; - - const tableData = await readDB(storeHash); - - if (tableData.length > 0) { - for (const data of tableData) { - const { owner, repo, id, network, tx, bounty_hunter, amount, title } = data as unknown as SavedData; - populateTable(owner, repo, id, network, tx, title, amount, bounty_hunter); - // for filtering - elemList.push({ - id, - tx, - amount, - title, - bounty_hunter, - owner, - repo, - network, - }); - } - } - } - } -} - -async function resetInit() { - permitList.splice(0, permitList.length); - isGit = true; - isEther = true; - etherPageNumber = 1; - isRPC = true; - elemList.splice(0, elemList.length); - gitID = NULL_ID; - etherHash = NULL_HASH; - lastGitID = false; - lastEtherHash = false; - repoArray.splice(0, repoArray.length); - finishedQueue.clearQueue(); -} - -async function asyncInit() { - await resetInit(); - await dbInit(); -} - -function tabInit(repoUrls: GitHubUrlParts[]) { - etherFetcher().catch((error) => console.error(error)); - gitFetcher(repoUrls).catch((error) => console.error(error)); - rpcFetcher().catch((error) => console.error(error)); -} - -function auditInit() { - tgBtnInput.checked = true; - getReportElem.addEventListener("click", async () => { - isCache = tgBtnInput.checked; - toggleLoader("block"); - resultTableElem.style.display = "table"; - resultTableTbodyElem.innerHTML = ""; - const quickImportValue = (document.querySelector("#quickName") as HTMLTextAreaElement).value; - if (quickImportValue !== "") { - const { WALLET, REPO, PAT }: QuickImport = JSON.parse(quickImportValue); - BOT_WALLET_ADDRESS = WALLET.toLocaleLowerCase(); - REPOSITORY_URL = REPO.toLocaleLowerCase(); - GITHUB_PAT = PAT; - parseAndAddUrls(REPOSITORY_URL); - } else { - BOT_WALLET_ADDRESS = (document.querySelector("#botWalletAddress") as HTMLInputElement).value.toLocaleLowerCase(); - REPOSITORY_URL = (document.querySelector("#repoURLs") as HTMLInputElement).value.toLocaleLowerCase(); - GITHUB_PAT = (document.querySelector("#githubPAT") as HTMLInputElement).value; - parseAndAddUrls(REPOSITORY_URL); - } - - const REPOS = getGitHubUrlPartsArray(repoArray); - - if (BOT_WALLET_ADDRESS !== "" && REPOSITORY_URL !== "" && GITHUB_PAT !== "" && REPOS.length > 0) { - await asyncInit(); - octokit = new rateOctokit({ - auth: GITHUB_PAT, - throttle: { - onRateLimit: (retryAfter, options) => { - return primaryRateLimitHandler(retryAfter, options as RateLimitOptions); - }, - onSecondaryRateLimit: (retryAfter, options) => { - return secondaryRateLimitHandler(retryAfter, options as RateLimitOptions); - }, - }, - }); - tabInit(REPOS); - } else { - toggleLoader("none"); - } - }); -} - -/** - * - * Filter Logics - * - */ - -// Function to filter the table based on search input -function filterTable() { - const input = document.getElementById("searchInput") as HTMLInputElement; - const value = input.value.toLowerCase(); - const filteredData = elemList.filter( - (row) => - row.owner.toLowerCase().includes(value) || - row.repo.toLowerCase().includes(value) || - row.amount.toLowerCase().includes(value) || - row.tx.toLowerCase().includes(value) || - row.title.toLowerCase().includes(value) || - row.network.toLowerCase().includes(value) || - row.bounty_hunter.name.toLowerCase().includes(value) - ); - resultTableTbodyElem.innerHTML = ""; // Clear the existing rows - for (const data of filteredData) { - const { owner, repo, id, network, tx, bounty_hunter, amount, title } = data as unknown as SavedData; - populateTable(owner, repo, id, network, tx, title, amount, bounty_hunter); - } -} - -// Variables to track sorting -let sortDirection = 1; // 1 for ascending, -1 for descending - -// Function to sort the table by the "Amount" column -function sortTableByAmount() { - elemList.sort((a, b) => sortDirection * (Number(a.amount) - Number(b.amount))); - sortDirection *= -1; - updateSortArrow(); - resultTableTbodyElem.innerHTML = ""; // Clear the existing rows - for (const data of elemList) { - const { owner, repo, id, network, tx, bounty_hunter, amount, title } = data as unknown as SavedData; - populateTable(owner, repo, id, network, tx, title, amount, bounty_hunter); - } -} - -// Function to update the sort arrow indicator -function updateSortArrow() { - const sortArrow = document.getElementById("sortArrow") as HTMLElement; - sortArrow.textContent = sortDirection === 1 ? "\u2191" : "\u2193"; -} - -const searchInput = document.getElementById("searchInput") as HTMLInputElement; -const amountHeader = document.getElementById("amountHeader") as HTMLTableCellElement; - -// Add event listener for the search button -searchInput.addEventListener("keyup", filterTable); -// Add event listener for the "Amount" column header -amountHeader.addEventListener("click", sortTableByAmount); - -auditInit(); diff --git a/static/scripts/audit-report/constants.ts b/static/scripts/audit-report/constants.ts deleted file mode 100644 index b8282685..00000000 --- a/static/scripts/audit-report/constants.ts +++ /dev/null @@ -1,30 +0,0 @@ -export enum ChainScan { - Ethereum = "etherscan.io", - Gnosis = "gnosisscan.io", -} - -export enum Chain { - Ethereum = "Ethereum", - Gnosis = "Gnosis", -} - -export const CHAIN_MAP = { - Ethereum: 1, - Gnosis: 100, -}; - -export const NULL_ID = 0; -export const NULL_HASH = "0x0000000000000000000000000000000000000000"; -export const DATABASE_NAME = "file_cache"; - -// hardcoded values -// cspell:ignore 35G6PRE7U54QWZMXYGUSI3YWU27TP2TTBK R75N38X1Y5KP8CRPPDWBRT3EM5VDJ73MUK -export const API_KEYS = { - [Chain.Ethereum]: ["35G6PRE7U54QWZMXYGUSI3YWU27TP2TTBK"], - [Chain.Gnosis]: ["R75N38X1Y5KP8CRPPDWBRT3EM5VDJ73MUK"], -}; - -export const RPC_URLS = { - [Chain.Ethereum]: ["https://rpc.builder0x69.io", "https://eth.meowrpc.com"], - [Chain.Gnosis]: ["https://rpc.ankr.com/gnosis"], -}; diff --git a/static/scripts/audit-report/helpers.ts b/static/scripts/audit-report/helpers.ts deleted file mode 100644 index 38c620b9..00000000 --- a/static/scripts/audit-report/helpers.ts +++ /dev/null @@ -1,187 +0,0 @@ -import axios from "axios"; -import { ethers } from "ethers"; -import { API_KEYS, Chain, ChainScan, RPC_URLS } from "./constants"; -import { BountyHunter, GitHubUrlParts } from "./types"; - -export interface RateLimitOptions { - method: string; - url: string; -} - -const resultTableTbodyElem = document.querySelector("#resultTable tbody") as HTMLTableCellElement; - -type DataType = { - jsonrpc: string; - id: number; - result: { - number: string; - timestamp: string; - hash: string; - }; -}; - -const RPC_BODY = JSON.stringify({ - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: ["latest", false], - id: 1, -}); - -const RPC_HEADER = { - "Content-Type": "application/json", -}; - -export function shortenTransactionHash(hash: string | undefined, length = 8): string { - if (!hash) return ""; - const prefixLength = Math.floor(length / 2); - const suffixLength = length - prefixLength; - - const prefix = hash.slice(0, prefixLength); - const suffix = hash.slice(-suffixLength); - - return prefix + "..." + suffix; -} - -export function populateTable( - owner: string, - repo: string, - issueNumber: number, - network: string, - txHash: string, - issueTitle: string, - amount: string, - bountyHunter: BountyHunter -) { - if (!txHash) return; // permit not claimed - const issueUrl = `https://github.com/${owner}/${repo}/issues/${issueNumber}`; - const txUrl = `https://${getChainScan(network)}/tx/${txHash}`; - const rows = ` - - ${owner}/${repo} - #${issueNumber} - ${issueTitle} - ${bountyHunter?.name} - ${ethers.BigNumber.isBigNumber(amount) ? ethers.utils.formatEther(amount) : amount} ${ - network === Chain.Ethereum ? "DAI" : "WXDAI" - } - ${shortenTransactionHash(txHash)} - `; - - resultTableTbodyElem.insertAdjacentHTML("beforeend", rows); -} - -export function getChainScan(chain: string) { - return chain === Chain.Ethereum ? ChainScan.Ethereum : ChainScan.Gnosis; -} - -export function getRandomAPIKey(chain: Chain): string { - const keys = API_KEYS[chain]; - if (!keys || keys.length === 0) { - throw new Error(`No API Keys found for chain: ${chain}`); - } - - const randomIndex = Math.floor(Math.random() * keys.length); - return keys[randomIndex]; -} - -export function getRandomRpcUrl(chain: Chain): string { - const urls = RPC_URLS[chain]; - if (!urls || urls.length === 0) { - throw new Error(`No RPC URLs found for chain: ${chain}`); - } - - const randomIndex = Math.floor(Math.random() * urls.length); - return urls[randomIndex]; -} - -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; - } -} - -export async function getOptimalRPC(chain: Chain): Promise { - const promises = RPC_URLS[chain].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 optimalRPC; -} - -export function parseRepoUrl(issueUrl: string): [string, string] { - const match = issueUrl.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/\d+/i); - if (match) { - const owner = match[1]; - const repo = match[2]; - return [owner, repo]; - } else { - throw new Error("Invalid GitHub issue URL format"); - } -} - -export function getGitHubUrlPartsArray(urls: string[]): GitHubUrlParts[] { - const githubUrlPartsArray: GitHubUrlParts[] = []; - - for (const url of urls) { - const regex = /([^/]+)\/([^/]+)$/i; - const matches = url.match(regex); - if (matches && matches.length === 3) { - const owner = matches[1]; - const repo = matches[2]; - githubUrlPartsArray.push({ owner, repo }); - } - } - - return githubUrlPartsArray; -} - -export function primaryRateLimitHandler(retryAfter: number, options: RateLimitOptions) { - console.warn(`Request quota exhausted for request ${options.method} ${options.url}\nRetrying after ${retryAfter} seconds!`); - return true; -} - -export function secondaryRateLimitHandler(retryAfter: number, options: RateLimitOptions) { - console.warn(`Secondary quota detected for request ${options.method} ${options.url}\nRetrying after ${retryAfter} seconds!`); - return true; -} - -export function isValidUrl(urlString: string) { - try { - return Boolean(new URL(urlString)); - } catch (e) { - return false; - } -} - -export function getCurrency(id: number) { - if (id === 100) { - return Chain.Gnosis; - } else if (id === 1) { - return Chain.Ethereum; - } - return null; -} diff --git a/static/scripts/audit-report/types/audit.d.ts b/static/scripts/audit-report/types/audit.d.ts deleted file mode 100644 index 86c3b64c..00000000 --- a/static/scripts/audit-report/types/audit.d.ts +++ /dev/null @@ -1,130 +0,0 @@ -export type ObserverKeys = "isRPC" | "isComment" | "isGit" | "isEther"; - -export interface BountyHunter { - name: string; - url: string; -} - -export interface ElemInterface { - id: number; - tx: string; - amount: string; - title: string; - bounty_hunter: BountyHunter; - owner: string; - repo: string; - network: string; -} - -export interface GitHubUrlParts { - owner: string; - repo: string; -} - -export interface SavedData { - owner: string; - repo: string; - id: number; - network: string; - tx: string; - bounty_hunter: { - url: string; - name: string; - }; - amount: string; - title: string; -} - -export interface ChainScanResult { - blockNumber: string; - timeStamp: string; - hash: string; - nonce: string; - blockHash: string; - from: string; - contractAddress: string; - to: string; - value: string; - tokenName: string; - tokenSymbol: string; - tokenDecimal: string; - transactionIndex: string; - gas: string; - gasPrice: string; - gasUsed: string; - cumulativeGasUsed: string; - input: string; - confirmations: string; - chain: string; -} - -export interface GitInterface { - owner: string; - repo: string; - issue_number: number; - issue_title: string; - bounty_hunter: BountyHunter; -} - -export interface EtherInterface { - txHash: string; - timestamp: number; - block_number: number; -} - -export interface StandardInterface { - k: string; - t: "git" | "ether"; - c: { - nonce: string; - owner: string; - token: string; - amount: string; - to: string; - deadline: string; - signature: string; - }; - s: { - ether: EtherInterface | undefined; - git: GitInterface | undefined; - network: string; - }; -} - -export interface TxData { - permit: { - permitted: { - token: string; - amount: string; - }; - nonce: string; - deadline: string; - }; - transferDetails: { - to: string; - requestedAmount: string; - }; - owner: string; - signature: string; -} - -export interface QuickImport { - WALLET: string; - REPO: string; - PAT: string; -} - -declare type TableIndexTypes = NumberConstructor | StringConstructor | BooleanConstructor | DateConstructor | ObjectConstructor | ArrayConstructor; -interface TableIndex { - type: TableIndexTypes; - multiEntry?: boolean; - unique?: boolean; - default?: NonNullable; - ref?: string; -} -interface GoDBTableSchema { - [key: string]: TableIndex | TableIndexTypes; -} -export interface GoDBSchema { - [table: string]: GoDBTableSchema; -} diff --git a/static/scripts/audit-report/types/index.ts b/static/scripts/audit-report/types/index.ts deleted file mode 100644 index df50688d..00000000 --- a/static/scripts/audit-report/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./audit"; diff --git a/static/scripts/audit-report/types/transaction.ts b/static/scripts/audit-report/types/transaction.ts deleted file mode 100644 index 0d2cd07e..00000000 --- a/static/scripts/audit-report/types/transaction.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Transaction { - input: string; - hash: string; - blockNumber: string; - timestamp: string; -} diff --git a/static/scripts/audit-report/utils/block-info.ts b/static/scripts/audit-report/utils/block-info.ts deleted file mode 100644 index ee008c6c..00000000 --- a/static/scripts/audit-report/utils/block-info.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Chain, CHAIN_MAP } from "../constants"; - -export async function getBlockInfo(blockNumber: string, chain: Chain) { - return localStorage.getItem(`${CHAIN_MAP[chain]}:${blockNumber}`); -} - -export async function updateBlockInfo(blockNumber: string, timestamp: string, chain: Chain) { - localStorage.setItem(`${CHAIN_MAP[chain]}:${blockNumber}`, timestamp); -} diff --git a/static/scripts/audit-report/utils/get-transaction.ts b/static/scripts/audit-report/utils/get-transaction.ts deleted file mode 100644 index d230c710..00000000 --- a/static/scripts/audit-report/utils/get-transaction.ts +++ /dev/null @@ -1,35 +0,0 @@ -import axios from "axios"; -import { Chain } from "../constants"; -import { Transaction } from "../types/transaction"; -import { getBlockInfo, updateBlockInfo } from "./block-info"; - -export async function getTxInfo(hash: string, url: string, chain: Chain): Promise { - try { - const transactionResponse = await axios.post(url, { - jsonrpc: "2.0", - id: 1, - method: "eth_getTransactionByHash", - params: [hash], - }); - const transaction = transactionResponse.data.result as Transaction; - - const timestamp = await getBlockInfo(transaction.blockNumber, chain); - if (timestamp !== null) { - transaction.timestamp = timestamp; - } else { - const blockResponse = await axios.post(url, { - jsonrpc: "2.0", - id: 1, - method: "eth_getBlockByNumber", - params: [transaction.blockNumber, false], - }); - transaction.timestamp = blockResponse.data.result.timestamp; - await updateBlockInfo(transaction.blockNumber, transaction.timestamp, chain); - } - - return transaction; - } catch (error) { - console.error(error); - throw error; - } -} diff --git a/static/scripts/key-generator/keygen.ts b/static/scripts/key-generator/keygen.ts deleted file mode 100644 index 6853de7c..00000000 --- a/static/scripts/key-generator/keygen.ts +++ /dev/null @@ -1,134 +0,0 @@ -import _sodium from "libsodium-wrappers"; - -const classes = ["error", "warning", "success"]; -const CYPHER_KEY = "#cipherKey"; - -function statusToggle(target: "error" | "warning" | "success", value: string) { - const statusKey = document.querySelector("#statusKey") as HTMLInputElement; - - classes.forEach((e) => { - if (e !== target) { - statusKey.classList.remove(e); - } - }); - statusKey.classList.add(target); - statusKey.value = value; -} - -async function sodiumKeyBox() { - const privKey = document.querySelector("#privKey") as HTMLInputElement; - const pubKey = document.querySelector("#pubKey") as HTMLInputElement; - const cipherKey = document.querySelector(CYPHER_KEY) as HTMLInputElement; - cipherKey.value = ""; - try { - await _sodium.ready; - const sodium = _sodium; - - const { privateKey, publicKey } = sodium.crypto_box_keypair("base64"); - privKey.value = privateKey; - pubKey.value = publicKey; - statusToggle("success", `Success: Key Generation is ok.`); - } catch (error: unknown) { - if (error instanceof Error) { - statusToggle("error", `Error: ${error.message}`); - } else { - statusToggle("error", `Error: ${JSON.stringify(error)}`); - } - } -} - -async function sodiumEncryptedSeal() { - const pubKey = document.querySelector("#pubKey") as HTMLInputElement; - const privKey = document.querySelector("#privKey") as HTMLInputElement; - const plainKey = document.querySelector("#plainKey") as HTMLInputElement; - const cipherKey = document.querySelector(CYPHER_KEY) as HTMLInputElement; - try { - await _sodium.ready; - const sodium = _sodium; - - if (!privKey.value && !pubKey.value) { - statusToggle("error", `Error: You need to enter either public or private key.`); - return; - } - if (!pubKey.value && privKey.value) { - // derive public key from private key - const binPriv = sodium.from_base64(privKey.value, sodium.base64_variants.URLSAFE_NO_PADDING); - const binPub = sodium.crypto_scalarmult_base(binPriv); - const output = sodium.to_base64(binPub, sodium.base64_variants.URLSAFE_NO_PADDING); - pubKey.value = output; - } - const binkey = sodium.from_base64(pubKey.value, sodium.base64_variants.URLSAFE_NO_PADDING); - const binsec = sodium.from_string(plainKey.value); - const encBytes = sodium.crypto_box_seal(binsec, binkey); - const output = sodium.to_base64(encBytes, sodium.base64_variants.URLSAFE_NO_PADDING); - cipherKey.value = output; - statusToggle("success", `Success: Key Encryption is ok.`); - } catch (error: unknown) { - if (error instanceof Error) { - statusToggle("error", `Error: ${error.message}`); - } else { - statusToggle("error", `Error: ${JSON.stringify(error)}`); - } - } -} - -async function sodiumOpenSeal() { - const pubKey = document.querySelector("#pubKey") as HTMLInputElement; - const privKey = document.querySelector("#privKey") as HTMLInputElement; - const cipherKey = document.querySelector("#cipherKey") as HTMLInputElement; - const plainKey = document.querySelector("#plainKey") as HTMLInputElement; - try { - await _sodium.ready; - const sodium = _sodium; - - if (!privKey.value) { - statusToggle("error", `Error: You need to enter private key.`); - return; - } - if (!pubKey.value && privKey.value) { - // derive public key from private key - const binPriv = sodium.from_base64(privKey.value, sodium.base64_variants.URLSAFE_NO_PADDING); - const binPub = sodium.crypto_scalarmult_base(binPriv); - const output = sodium.to_base64(binPub, sodium.base64_variants.URLSAFE_NO_PADDING); - pubKey.value = output; - } - const binPub = sodium.from_base64(pubKey.value, sodium.base64_variants.URLSAFE_NO_PADDING); - const binPriv = sodium.from_base64(privKey.value, sodium.base64_variants.URLSAFE_NO_PADDING); - const binCipher = sodium.from_base64(cipherKey.value, sodium.base64_variants.URLSAFE_NO_PADDING); - const outText = sodium.crypto_box_seal_open(binCipher, binPub, binPriv, "text"); - plainKey.value = outText; - statusToggle("success", `Success: Key Decryption is ok.`); - } catch (error: unknown) { - if (error instanceof Error) { - statusToggle("error", `Error: ${error.message}`); - } else { - statusToggle("error", `Error: ${JSON.stringify(error)}`); - } - } -} - -function init() { - const genBtn = document.querySelector("#genBtn") as HTMLButtonElement; - const encryptBtn = document.querySelector("#encryptBtn") as HTMLButtonElement; - const decryptBtn = document.querySelector("#decryptBtn") as HTMLButtonElement; - - genBtn.addEventListener("click", () => { - sodiumKeyBox().catch((error) => { - statusToggle("error", `Error: ${error.message}`); - }); - }); - - encryptBtn.addEventListener("click", () => { - sodiumEncryptedSeal().catch((error) => { - statusToggle("error", `Error: ${error.message}`); - }); - }); - - decryptBtn.addEventListener("click", () => { - sodiumOpenSeal().catch((error) => { - statusToggle("error", `Error: ${error.message}`); - }); - }); -} - -init(); diff --git a/static/scripts/onboarding/onboarding.ts b/static/scripts/onboarding/onboarding.ts deleted file mode 100644 index 4f263bbe..00000000 --- a/static/scripts/onboarding/onboarding.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { JsonRpcSigner } from "@ethersproject/providers"; -import { createOrUpdateTextFile } from "@octokit/plugin-create-or-update-text-file"; -import { Octokit } from "@octokit/rest"; -import { PERMIT2_ADDRESS } from "@uniswap/permit2-sdk"; -import { ethers } from "ethers"; -import { parseUnits } from "ethers/lib/utils"; -import _sodium from "libsodium-wrappers"; -import YAML from "yaml"; -import { BotConfig } from "../../../lib/ubiquibot/src/types/configuration-types"; -import { erc20Abi } from "../rewards/abis/erc20Abi"; -import { getNetworkName, NetworkIds, Tokens } from "../rewards/constants"; - -const classes = ["error", "warn", "success"]; -const inputClasses = ["input-warn", "input-error", "input-success"]; -const outKey = document.getElementById("outKey") as HTMLInputElement; -const githubPAT = document.getElementById("githubPat") as HTMLInputElement; -const orgName = document.getElementById("orgName") as HTMLInputElement; -const walletPrivateKey = document.getElementById("walletPrivateKey") as HTMLInputElement; -// cspell: word ress // weird cspell bug seperating add and ress -const setBtn = document.getElementById("setBtn") as HTMLButtonElement; -const allowanceInput = document.getElementById("allowance") as HTMLInputElement; -const chainIdSelect = document.getElementById("chainId") as HTMLSelectElement; -const loader = document.querySelector(".loader-wrap") as HTMLElement; - -const APP_ID = 236521; -const REPO_NAME = "ubiquibot-config"; -const KEY_PATH = ".github/ubiquibot-config.yml"; -const PRIVATE_ENCRYPTED_KEY_NAME = "evmPrivateEncrypted"; -const EVM_NETWORK_KEY_NAME = "evmNetworkId"; -const KEY_PREFIX = "HSK_"; -// cspell:disable-next-line -const X25519_KEY = "5ghIlfGjz_ChcYlBDOG7dzmgAgBPuTahpvTMBipSH00"; -const STATUS_LOG = ".status-log"; - -let encryptedValue = ""; - -const defaultConf = {} as BotConfig; - -export async function parseYAML(data: string | undefined) { - if (!data) return undefined; - try { - const parsedData = await YAML.parse(data); - if (parsedData !== null) { - return parsedData as T; - } else { - return undefined; - } - } catch (error) { - return undefined; - } -} - -export async function parseJSON(data: string) { - try { - const parsedData = await JSON.parse(data); - return parsedData as T; - } catch (error) { - return undefined; - } -} - -export function stringifyYAML(value: BotConfig): string { - return YAML.stringify(value, { defaultKeyType: "PLAIN", defaultStringType: "QUOTE_DOUBLE", lineWidth: 0 }); -} - -export async function getConf(): Promise { - try { - const octokit = new Octokit({ auth: githubPAT.value }); - const { data } = await octokit.rest.repos.getContent({ - owner: orgName.value, - repo: REPO_NAME, - path: KEY_PATH, - mediaType: { - format: "raw", - }, - }); - return data as unknown as string; - } catch (error: unknown) { - return undefined; - } -} - -function getTextBox(text: string) { - const strLen = text.split("\n").length * 22; - return `${strLen > 140 ? strLen : 140}px`; -} - -function resetToggle() { - (walletPrivateKey.parentNode?.querySelector(STATUS_LOG) as HTMLElement).innerHTML = ""; - (githubPAT.parentNode?.querySelector(STATUS_LOG) as HTMLElement).innerHTML = ""; - (orgName.parentNode?.querySelector(STATUS_LOG) as HTMLElement).innerHTML = ""; -} - -function classListToggle(targetElem: HTMLElement, target: "error" | "warn" | "success", inputElem?: HTMLInputElement | HTMLTextAreaElement) { - classes.forEach((className) => targetElem.classList.remove(className)); - targetElem.classList.add(target); - - if (inputElem) { - inputClasses.forEach((className) => inputElem.classList.remove(className)); - inputElem.classList.add(`input-${target}`); - } -} - -function statusToggle(type: "error" | "warn" | "success", message: string) { - resetToggle(); - const statusKey = document.getElementById("statusKey") as HTMLInputElement; - classListToggle(statusKey, type); - statusKey.value = message; -} - -function focusToggle(targetElem: HTMLInputElement | HTMLTextAreaElement, type: "error" | "warn" | "success", message: string) { - resetToggle(); - const infoElem = targetElem.parentNode?.querySelector(STATUS_LOG) as HTMLElement; - infoElem.innerHTML = message; - classListToggle(infoElem, type, targetElem); - targetElem.focus(); -} - -function toggleLoader(state: "start" | "end") { - if (state === "start") { - setBtn.disabled = true; - loader.style.display = "flex"; - } else { - setBtn.disabled = false; - loader.style.display = "none"; - } -} - -function singleToggle(type: "error" | "warn" | "success", message: string, focusElem?: HTMLInputElement | HTMLTextAreaElement) { - statusToggle(type, message); - - if (focusElem) { - focusToggle(focusElem, type, message); - } - - toggleLoader("end"); -} - -async function sodiumEncryptedSeal(publicKey: string, secret: string) { - outKey.value = ""; - encryptedValue = ""; - try { - await _sodium.ready; - const sodium = _sodium; - - const binkey = sodium.from_base64(publicKey, sodium.base64_variants.URLSAFE_NO_PADDING); - const binsec = sodium.from_string(secret); - const encBytes = sodium.crypto_box_seal(binsec, binkey); - const output = sodium.to_base64(encBytes, sodium.base64_variants.URLSAFE_NO_PADDING); - defaultConf.keys[PRIVATE_ENCRYPTED_KEY_NAME] = output; - defaultConf.payments[EVM_NETWORK_KEY_NAME] = Number(chainIdSelect.value); - outKey.value = stringifyYAML(defaultConf); - outKey.style.height = getTextBox(outKey.value); - encryptedValue = output; - singleToggle("success", `Success: Key Encryption is ok.`); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(error); - singleToggle("error", `Error: ${error.message}`); - } - } -} - -async function setConfig() { - try { - toggleLoader("start"); - const pluginKit = Octokit.plugin(createOrUpdateTextFile); - const octokit = new pluginKit({ auth: githubPAT.value }); - const { data: userInfo } = await octokit.rest.users.getByUsername({ - username: orgName.value, - }); - if (userInfo.type === "Organization") { - const repositoryId = await getRepoID(octokit, orgName.value, REPO_NAME); - - const { data: appInstallations } = await octokit.rest.orgs.listAppInstallations({ - org: orgName.value, - per_page: 100, - }); - const ins = appInstallations.installations.filter((installation) => installation.app_id === APP_ID); - - await handleInstall(octokit, orgName, repositoryId, ins, chainIdSelect); - } else { - singleToggle("error", `Error: Not an organization.`, orgName); - } - } catch (error) { - if (!(error instanceof Error)) { - return console.error(error); - } - console.error(error); - singleToggle("error", `Error: ${error.message}`); - } -} - -async function handleInstall( - octokit: Octokit, - orgName: HTMLInputElement, - repositoryId: number | null, - ins: { id: number }[], - chainIdSelect: HTMLSelectElement -) { - if (ins.length > 0) { - const installationId = ins[0].id; - const { data: installedRepos } = await octokit.rest.apps.listInstallationReposForAuthenticatedUser({ - installation_id: installationId, - }); - const irs = installedRepos.repositories.filter((installedRepo) => installedRepo.id === repositoryId); - - if (irs.length === 0) { - if (!repositoryId) { - singleToggle("error", `Error: Repo initialization failed, try again later.`); - return; - } - await octokit.rest.apps.addRepoToInstallationForAuthenticatedUser({ - installation_id: installationId, - repository_id: repositoryId, - }); - } - - const conf = await getConf(); - - const updatedConf = defaultConf; - const parsedConf = await parseYAML(conf); - updatedConf.keys[PRIVATE_ENCRYPTED_KEY_NAME] = encryptedValue; - updatedConf.payments[EVM_NETWORK_KEY_NAME] = Number(chainIdSelect.value); - - // combine configs (default + remote org wide) - const combinedConf = Object.assign(updatedConf, parsedConf); - - const stringified = stringifyYAML(combinedConf); - outKey.value = stringified; - const { status } = await octokit.repos.createOrUpdateFileContents({ - owner: orgName.value, - repo: REPO_NAME, - path: KEY_PATH, - content: stringified, - message: `${crypto.randomUUID()}`, - }); - - if (status === 201 || status === 200) { - singleToggle("success", `Success: private key is updated.`); - } else { - singleToggle("success", `Success: private key is upto date.`); - } - - await nextStep(); - } else { - singleToggle("warn", `Warn: Please install the app first.`); - } -} - -async function getRepoID(octokit: Octokit, orgName: string, repoName: string): Promise { - let repositoryId: number | null = null; - - try { - const { data: repositoryInfo } = await octokit.rest.repos.get({ - owner: orgName, - repo: repoName, - }); - repositoryId = repositoryInfo.id; - } catch (error) { - if (!(error instanceof Error)) { - console.error(error); - return null; - } - - console.error(error.message); - try { - const { data: repoRes } = await octokit.rest.repos.createInOrg({ - org: orgName, - name: repoName, - auto_init: true, - private: true, - visibility: "private", - has_downloads: true, - }); - repositoryId = repoRes.id; - } catch (error) { - if (!(error instanceof Error)) { - console.error(error); - return null; - } - console.error(error.message); - singleToggle("error", `Error: Repo initialization failed, try again later.`); - return null; - } - } - return repositoryId; -} - -function setInputListeners() { - const inputs = document.querySelectorAll("input") as NodeListOf; - - inputs.forEach((input) => { - input.addEventListener("input", (e) => { - inputClasses.forEach((className) => (e.target as HTMLInputElement).classList.remove(className)); - (((e.target as HTMLInputElement).parentNode as HTMLElement).querySelector(STATUS_LOG) as HTMLElement).innerHTML = ""; - }); - }); -} - -let currentStep = 1; -let signer: JsonRpcSigner | undefined = undefined; - -async function nextStep() { - const configChainId = Number(chainIdSelect.value); - - const tokenNameSpan = document.getElementById("allowance + span"); - if (tokenNameSpan) { - if (configChainId === NetworkIds.Mainnet) { - tokenNameSpan.innerHTML = "DAI"; - } else if (configChainId === NetworkIds.Gnosis) { - tokenNameSpan.innerHTML = "WXDAI"; - } - } - - const step1 = document.getElementById("step1") as HTMLElement; - step1.classList.add("hidden"); - const step2 = document.getElementById("step2") as HTMLElement; - step2.classList.remove("hidden"); - const stepper = document.getElementById("stepper") as HTMLElement; - const steps = stepper.querySelectorAll("div.step"); - steps[0].classList.remove("active"); - steps[1].classList.add("active"); - setBtn.innerText = "Approve"; - currentStep = 2; - - if (!window.ethereum) { - singleToggle("error", `Error: Please install MetaMask or any other Ethereum wallet.`); - return; - } - - const provider = new ethers.providers.Web3Provider(window.ethereum); - signer = await connectWallet(); - if (!signer) { - singleToggle("error", `Error: Please connect to MetaMask.`); - return; - } - - const currentChainId = await signer.getChainId(); - - if (configChainId !== currentChainId) { - singleToggle("error", `Error: Please connect to ${getNetworkName(configChainId)}.`); - if (await switchNetwork(provider, configChainId)) { - singleToggle("success", ``); - } - } - - // watch for chain changes making this generic suppresses the unknown comparison - window.ethereum.on("chainChanged", async (currentChainId: T | string) => { - if (configChainId === parseInt(currentChainId as string, 16)) { - singleToggle("success", ``); - } else { - singleToggle("error", `Error: Please connect to ${getNetworkName(configChainId)}.`); - switchNetwork(provider, configChainId).catch((error) => { - console.error(error); - }); - } - }); -} - -async function connectWallet(): Promise { - try { - const provider = new ethers.providers.Web3Provider(window.ethereum, "any"); - await provider.send("eth_requestAccounts", []); - return provider.getSigner(); - } catch (error: unknown) { - if (error instanceof Error) { - if (error?.message?.includes("missing provider")) { - singleToggle("error", "Error: Please install MetaMask."); - } else { - singleToggle("error", "Error: Please connect your wallet."); - } - return undefined; - } - } -} - -async function switchNetwork(provider: ethers.providers.Web3Provider, chainId: string | number): Promise { - try { - // if chainId is a number then convert it to hex - if (typeof chainId === "number") { - chainId = `0x${chainId.toString(16)}`; - } - // if chainId is a string but doesn't start with 0x then convert it to hex - if (typeof chainId === "string" && !chainId.startsWith("0x")) { - chainId = `0x${Number(chainId).toString(16)}`; - } - await provider.send("wallet_switchEthereumChain", [{ chainId: chainId }]); - return true; - } catch (error: unknown) { - return false; - } -} - -function isHex(str: string): boolean { - const regexp = /^[0-9a-fA-F]+$/; - return regexp.test(str); -} - -async function step1Handler() { - if (walletPrivateKey.value === "") { - singleToggle("warn", `Warn: Private_Key is not set.`, walletPrivateKey); - return; - } - if (!isHex(walletPrivateKey.value)) { - singleToggle("warn", `Warn: Private_Key is not a valid hex string.`, walletPrivateKey); - return; - } - if (walletPrivateKey.value.length !== 64) { - singleToggle("warn", `Warn: Private_Key must be 32 bytes long.`, walletPrivateKey); - return; - } - if (orgName.value === "") { - singleToggle("warn", `Warn: Org Name is not set.`, orgName); - return; - } - if (githubPAT.value === "") { - singleToggle("warn", `Warn: GitHub PAT is not set.`, githubPAT); - return; - } - - await sodiumEncryptedSeal(X25519_KEY, `${KEY_PREFIX}${walletPrivateKey.value}`); - setConfig().catch((error) => { - console.error(error); - }); -} - -async function step2Handler() { - try { - if (!window.ethereum) { - singleToggle("error", `Error: Please install MetaMask or any other Ethereum wallet.`); - return; - } - - const provider = new ethers.providers.Web3Provider(window.ethereum); - - // if wallet is still not connected then retry connecting - if (!signer) { - signer = await connectWallet(); - if (!signer) { - singleToggle("error", `Error: Please connect to MetaMask.`); - return; - } - } - - const walletChainId = await signer.getChainId(); - const configChainId = Number(chainIdSelect.value); - - window.ethereum.on("chainChanged", async (currentChainId: T | string) => { - if (configChainId === parseInt(currentChainId as string, 16)) { - singleToggle("success", ``); - } else { - singleToggle("error", `Error: Please connect to ${chainIdSelect.value}.`); - switchNetwork(provider, configChainId).catch((error) => { - console.error(error); - }); - } - }); - - if (walletChainId !== configChainId && !(await switchNetwork(provider, configChainId))) { - singleToggle("error", `Error: Switch to the correct chain.`); - return; - } - - // load token contract - let token = ""; - if (configChainId === NetworkIds.Mainnet) { - token = Tokens.DAI; - } else if (configChainId === NetworkIds.Gnosis) { - token = Tokens.WXDAI; - } - const erc20 = new ethers.Contract(token, erc20Abi, signer); - const decimals = await erc20.decimals(); - const allowance = Number(allowanceInput.value); - if (allowance <= 0) { - singleToggle("error", `Error: Allowance should be greater than 0.`); - return; - } - - await erc20.approve(PERMIT2_ADDRESS, parseUnits(allowance.toString(), decimals)); - singleToggle("success", `Success`); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(error); - singleToggle("error", `Error: ${error.message}`); - } - } -} - -async function init() { - if (defaultConf !== undefined) { - try { - defaultConf.keys[PRIVATE_ENCRYPTED_KEY_NAME] = undefined; - setInputListeners(); - - setBtn.addEventListener("click", async () => { - if (currentStep === 1) { - await step1Handler(); - } else if (currentStep === 2) { - await step2Handler(); - } - }); - } catch (error) { - console.error(error); - } - } else { - throw new Error("Default config fetch failed"); - } -} - -init().catch((error) => { - console.error(error); -});