diff --git a/build/esbuild-build.ts b/build/esbuild-build.ts index 3004b1d..94f52ed 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -1,10 +1,9 @@ import esbuild from "esbuild"; -const typescriptEntries = ["static/main.ts"]; -// const cssEntries = ["static/style.css"]; -const entries = [ - ...typescriptEntries, - // ...cssEntries -]; +import * as dotenv from "dotenv"; + +const typescriptEntries = ["static/scripts/rewards/index.ts", "static/scripts/audit-report/audit.ts"]; +const cssEntries = ["static/styles/rewards/rewards.css", "static/styles/audit-report/audit.css"]; +export const entries = [...typescriptEntries, ...cssEntries]; export const esBuildContext: esbuild.BuildOptions = { sourcemap: true, @@ -19,7 +18,8 @@ export const esBuildContext: esbuild.BuildOptions = { ".ttf": "dataurl", ".svg": "dataurl", }, - outdir: "static/dist", + outdir: "static/out", + define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"]), }; esbuild @@ -31,3 +31,17 @@ esbuild console.error(err); process.exit(1); }); + +function createEnvDefines(envVarNames: string[]): Record { + const defines: Record = {}; + dotenv.config(); + for (const name of envVarNames) { + const envVar = process.env[name]; + if (envVar !== undefined) { + defines[name] = JSON.stringify(envVar); + } else { + throw new Error(`Missing environment variable: ${name}`); + } + } + return defines; +} diff --git a/package.json b/package.json index 73e995c..26edb62 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ts-template", "version": "1.0.0", - "description": "Template repository with TypeScript support.", + "description": "audit.ubq.fi", "main": "build/index.ts", "author": "Ubiquity DAO", "license": "MIT", @@ -27,7 +27,14 @@ "open-source" ], "dependencies": { - "dotenv": "^16.4.4" + "@octokit/plugin-throttling": "^8.1.3", + "@octokit/rest": "^20.0.2", + "@sinclair/typebox": "^0.32.14", + "@supabase/supabase-js": "2.39.7", + "axios": "^1.6.7", + "dotenv": "^16.4.4", + "ethers": "^5.7.2", + "godb": "^0.6.2" }, "devDependencies": { "@commitlint/cli": "^18.6.1", diff --git a/static/index.html b/static/index.html index dbae5f4..bf505dc 100644 --- a/static/index.html +++ b/static/index.html @@ -1,13 +1,73 @@ - + - - - Ubiquity TypeScript Template - + Audit report + + + + -

Ubiquity TypeScript Template

- +
+

Audit report

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
⚡Cache
+
+
+ +
+ + + + + + + + + + + + + + + +
+
+ + diff --git a/static/scripts/audit-report/audit.ts b/static/scripts/audit-report/audit.ts new file mode 100644 index 0000000..ed9f862 --- /dev/null +++ b/static/scripts/audit-report/audit.ts @@ -0,0 +1,750 @@ +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 { + getCurrency, + getGitHubUrlPartsArray, + getOptimalRPC, + getRandomAPIKey, + populateTable, + primaryRateLimitHandler, + RateLimitOptions, + secondaryRateLimitHandler, +} from "./helpers"; +import { + ChainScanResult, + ElemInterface, + EtherInterface, + GitHubUrlParts, + GitInterface, + GoDBSchema, + ObserverKeys, + QuickImport, + SavedData, + StandardInterface, + TxData, +} from "./types"; +import { getTxInfo } from "./utils/getTransaction"; + +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 new file mode 100644 index 0000000..b828268 --- /dev/null +++ b/static/scripts/audit-report/constants.ts @@ -0,0 +1,30 @@ +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 new file mode 100644 index 0000000..38c620b --- /dev/null +++ b/static/scripts/audit-report/helpers.ts @@ -0,0 +1,187 @@ +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 new file mode 100644 index 0000000..86c3b64 --- /dev/null +++ b/static/scripts/audit-report/types/audit.d.ts @@ -0,0 +1,130 @@ +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 new file mode 100644 index 0000000..df50688 --- /dev/null +++ b/static/scripts/audit-report/types/index.ts @@ -0,0 +1 @@ +export * from "./audit"; diff --git a/static/scripts/audit-report/types/transaction.ts b/static/scripts/audit-report/types/transaction.ts new file mode 100644 index 0000000..0d2cd07 --- /dev/null +++ b/static/scripts/audit-report/types/transaction.ts @@ -0,0 +1,6 @@ +export interface Transaction { + input: string; + hash: string; + blockNumber: string; + timestamp: string; +} diff --git a/static/scripts/audit-report/utils/blockInfo.ts b/static/scripts/audit-report/utils/blockInfo.ts new file mode 100644 index 0000000..ee008c6 --- /dev/null +++ b/static/scripts/audit-report/utils/blockInfo.ts @@ -0,0 +1,9 @@ +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/getTransaction.ts b/static/scripts/audit-report/utils/getTransaction.ts new file mode 100644 index 0000000..e165e21 --- /dev/null +++ b/static/scripts/audit-report/utils/getTransaction.ts @@ -0,0 +1,35 @@ +import axios from "axios"; +import { Chain } from "../constants"; +import { Transaction } from "../types/transaction"; +import { getBlockInfo, updateBlockInfo } from "./blockInfo"; + +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/rewards/abis/cirip.json b/static/scripts/rewards/abis/cirip.json new file mode 100644 index 0000000..ecbdc6f --- /dev/null +++ b/static/scripts/rewards/abis/cirip.json @@ -0,0 +1,32 @@ +[ + { + "inputs": [ + { + "internalType": "contract ENS", + "name": "_ens", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "addresses", + "type": "address[]" + } + ], + "name": "getNames", + "outputs": [ + { + "internalType": "string[]", + "name": "r", + "type": "string[]" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/static/scripts/rewards/abis/erc20Abi.ts b/static/scripts/rewards/abis/erc20Abi.ts new file mode 100644 index 0000000..8813685 --- /dev/null +++ b/static/scripts/rewards/abis/erc20Abi.ts @@ -0,0 +1,271 @@ +export const erc20Abi = [ + { inputs: [{ internalType: "uint256", name: "chainId_", type: "uint256" }], payable: false, stateMutability: "nonpayable", type: "constructor" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "src", type: "address" }, + { indexed: true, internalType: "address", name: "guy", type: "address" }, + { indexed: false, internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: true, + inputs: [ + { indexed: true, internalType: "bytes4", name: "sig", type: "bytes4" }, + { indexed: true, internalType: "address", name: "usr", type: "address" }, + { indexed: true, internalType: "bytes32", name: "arg1", type: "bytes32" }, + { indexed: true, internalType: "bytes32", name: "arg2", type: "bytes32" }, + { indexed: false, internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "LogNote", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "src", type: "address" }, + { indexed: true, internalType: "address", name: "dst", type: "address" }, + { indexed: false, internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "Transfer", + type: "event", + }, + { + constant: true, + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [], + name: "PERMIT_TYPEHASH", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "address", name: "", type: "address" }, + ], + name: "allowance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "usr", type: "address" }, + { internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "usr", type: "address" }, + { internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "burn", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "decimals", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [{ internalType: "address", name: "guy", type: "address" }], + name: "deny", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "usr", type: "address" }, + { internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "mint", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "src", type: "address" }, + { internalType: "address", name: "dst", type: "address" }, + { internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "move", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "nonces", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "holder", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "bool", name: "allowed", type: "bool" }, + { internalType: "uint8", name: "v", type: "uint8" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + name: "permit", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "usr", type: "address" }, + { internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "pull", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "usr", type: "address" }, + { internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "push", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [{ internalType: "address", name: "guy", type: "address" }], + name: "rely", + outputs: [], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [], + name: "totalSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "dst", type: "address" }, + { internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [ + { internalType: "address", name: "src", type: "address" }, + { internalType: "address", name: "dst", type: "address" }, + { internalType: "uint256", name: "wad", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "version", + outputs: [{ internalType: "string", name: "", type: "string" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "wards", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, +]; diff --git a/static/scripts/rewards/abis/index.ts b/static/scripts/rewards/abis/index.ts new file mode 100644 index 0000000..de30ac1 --- /dev/null +++ b/static/scripts/rewards/abis/index.ts @@ -0,0 +1,2 @@ +export * from "./erc20Abi"; +export * from "./permit2Abi"; diff --git a/static/scripts/rewards/abis/nftRewardAbi.ts b/static/scripts/rewards/abis/nftRewardAbi.ts new file mode 100644 index 0000000..eb89aac --- /dev/null +++ b/static/scripts/rewards/abis/nftRewardAbi.ts @@ -0,0 +1,992 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +export const nftRewardAbi = [ + { + inputs: [ + { + internalType: "string", + name: "_tokenName", + type: "string", + }, + { + internalType: "string", + name: "_tokenSymbol", + type: "string", + }, + { + internalType: "address", + name: "_initialOwner", + type: "address", + }, + { + internalType: "address", + name: "_minter", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "ECDSAInvalidSignature", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "length", + type: "uint256", + }, + ], + name: "ECDSAInvalidSignatureLength", + type: "error", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "ECDSAInvalidSignatureS", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + internalType: "address", + name: "owner", + type: "address", + }, + ], + name: "ERC721IncorrectOwner", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "operator", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "ERC721InsufficientApproval", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "approver", + type: "address", + }, + ], + name: "ERC721InvalidApprover", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "operator", + type: "address", + }, + ], + name: "ERC721InvalidOperator", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + ], + name: "ERC721InvalidOwner", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "receiver", + type: "address", + }, + ], + name: "ERC721InvalidReceiver", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "ERC721InvalidSender", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "ERC721NonexistentToken", + type: "error", + }, + { + inputs: [], + name: "EnforcedPause", + type: "error", + }, + { + inputs: [], + name: "ExpectedPause", + type: "error", + }, + { + inputs: [], + name: "InvalidShortString", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + ], + name: "OwnableInvalidOwner", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "OwnableUnauthorizedAccount", + type: "error", + }, + { + inputs: [ + { + internalType: "string", + name: "str", + type: "string", + }, + ], + name: "StringTooLong", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "approved", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: false, + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "ApprovalForAll", + type: "event", + }, + { + anonymous: false, + inputs: [], + name: "EIP712DomainChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Unpaused", + type: "event", + }, + { + inputs: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "baseUri", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "eip712Domain", + outputs: [ + { + internalType: "bytes1", + name: "fields", + type: "bytes1", + }, + { + internalType: "string", + name: "name", + type: "string", + }, + { + internalType: "string", + name: "version", + type: "string", + }, + { + internalType: "uint256", + name: "chainId", + type: "uint256", + }, + { + internalType: "address", + name: "verifyingContract", + type: "address", + }, + { + internalType: "bytes32", + name: "salt", + type: "bytes32", + }, + { + internalType: "uint256[]", + name: "extensions", + type: "uint256[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "getApproved", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "bytes32[]", + name: "keys", + type: "bytes32[]", + }, + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "string[]", + name: "values", + type: "string[]", + }, + ], + internalType: "struct NftReward.MintRequest", + name: "_mintRequest", + type: "tuple", + }, + ], + name: "getMintRequestDigest", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTokenDataKeys", + outputs: [ + { + internalType: "bytes32[]", + name: "", + type: "bytes32[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "address", + name: "operator", + type: "address", + }, + ], + name: "isApprovedForAll", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minter", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + ], + name: "nonceRedeemed", + outputs: [ + { + internalType: "bool", + name: "isRedeemed", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "ownerOf", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "bytes32[]", + name: "keys", + type: "bytes32[]", + }, + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "string[]", + name: "values", + type: "string[]", + }, + ], + internalType: "struct NftReward.MintRequest", + name: "_mintRequest", + type: "tuple", + }, + { + internalType: "bytes", + name: "_signature", + type: "bytes", + }, + ], + name: "recover", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "bytes32[]", + name: "keys", + type: "bytes32[]", + }, + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "string[]", + name: "values", + type: "string[]", + }, + ], + internalType: "struct NftReward.MintRequest", + name: "_mintRequest", + type: "tuple", + }, + { + internalType: "bytes", + name: "_signature", + type: "bytes", + }, + ], + name: "safeMint", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "operator", + type: "address", + }, + { + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "_newBaseUri", + type: "string", + }, + ], + name: "setBaseUri", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_newMinter", + type: "address", + }, + ], + name: "setMinter", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + internalType: "bytes32", + name: "key", + type: "bytes32", + }, + ], + name: "tokenData", + outputs: [ + { + internalType: "string", + name: "value", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "tokenDataKey", + type: "bytes32", + }, + ], + name: "tokenDataKeyExists", + outputs: [ + { + internalType: "bool", + name: "isTokenDataKeyExists", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "tokenDataKeys", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "tokenIdCounter", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "tokenURI", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "unpause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +]; diff --git a/static/scripts/rewards/abis/permit2Abi.ts b/static/scripts/rewards/abis/permit2Abi.ts new file mode 100644 index 0000000..5d17050 --- /dev/null +++ b/static/scripts/rewards/abis/permit2Abi.ts @@ -0,0 +1,353 @@ +// cspell: word lockdown +export const permit2Abi = [ + { inputs: [{ internalType: "uint256", name: "deadline", type: "uint256" }], name: "AllowanceExpired", type: "error" }, + { inputs: [], name: "ExcessiveInvalidation", type: "error" }, + { inputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], name: "InsufficientAllowance", type: "error" }, + { inputs: [{ internalType: "uint256", name: "maxAmount", type: "uint256" }], name: "InvalidAmount", type: "error" }, + { inputs: [], name: "InvalidContractSignature", type: "error" }, + { inputs: [], name: "InvalidNonce", type: "error" }, + { inputs: [], name: "InvalidSignature", type: "error" }, + { inputs: [], name: "InvalidSignatureLength", type: "error" }, + { inputs: [], name: "InvalidSigner", type: "error" }, + { inputs: [], name: "LengthMismatch", type: "error" }, + { inputs: [{ internalType: "uint256", name: "signatureDeadline", type: "uint256" }], name: "SignatureExpired", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: true, internalType: "address", name: "spender", type: "address" }, + { indexed: false, internalType: "uint160", name: "amount", type: "uint160" }, + { indexed: false, internalType: "uint48", name: "expiration", type: "uint48" }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: false, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "spender", type: "address" }, + ], + name: "Lockdown", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: true, internalType: "address", name: "spender", type: "address" }, + { indexed: false, internalType: "uint48", name: "newNonce", type: "uint48" }, + { indexed: false, internalType: "uint48", name: "oldNonce", type: "uint48" }, + ], + name: "NonceInvalidation", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: true, internalType: "address", name: "spender", type: "address" }, + { indexed: false, internalType: "uint160", name: "amount", type: "uint160" }, + { indexed: false, internalType: "uint48", name: "expiration", type: "uint48" }, + { indexed: false, internalType: "uint48", name: "nonce", type: "uint48" }, + ], + name: "Permit", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: false, internalType: "uint256", name: "word", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "mask", type: "uint256" }, + ], + name: "UnorderedNonceInvalidation", + type: "event", + }, + { inputs: [], name: "DOMAIN_SEPARATOR", outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], stateMutability: "view", type: "function" }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "address", name: "", type: "address" }, + { internalType: "address", name: "", type: "address" }, + ], + name: "allowance", + outputs: [ + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "uint48", name: "expiration", type: "uint48" }, + { internalType: "uint48", name: "nonce", type: "uint48" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "uint48", name: "expiration", type: "uint48" }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint48", name: "newNonce", type: "uint48" }, + ], + name: "invalidateNonces", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "wordPos", type: "uint256" }, + { internalType: "uint256", name: "mask", type: "uint256" }, + ], + name: "invalidateUnorderedNonces", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + ], + internalType: "struct IAllowanceTransfer.TokenSpenderPair[]", + name: "approvals", + type: "tuple[]", + }, + ], + name: "lockdown", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "nonceBitmap", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "uint48", name: "expiration", type: "uint48" }, + { internalType: "uint48", name: "nonce", type: "uint48" }, + ], + internalType: "struct IAllowanceTransfer.PermitDetails[]", + name: "details", + type: "tuple[]", + }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "sigDeadline", type: "uint256" }, + ], + internalType: "struct IAllowanceTransfer.PermitBatch", + name: "permitBatch", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "uint48", name: "expiration", type: "uint48" }, + { internalType: "uint48", name: "nonce", type: "uint48" }, + ], + internalType: "struct IAllowanceTransfer.PermitDetails", + name: "details", + type: "tuple", + }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "sigDeadline", type: "uint256" }, + ], + internalType: "struct IAllowanceTransfer.PermitSingle", + name: "permitSingle", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "requestedAmount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails", + name: "transferDetails", + type: "tuple", + }, + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permitTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "requestedAmount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails", + name: "transferDetails", + type: "tuple", + }, + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes32", name: "witness", type: "bytes32" }, + { internalType: "string", name: "witnessTypeString", type: "string" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permitWitnessTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions[]", + name: "permitted", + type: "tuple[]", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitBatchTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "requestedAmount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails[]", + name: "transferDetails", + type: "tuple[]", + }, + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes32", name: "witness", type: "bytes32" }, + { internalType: "string", name: "witnessTypeString", type: "string" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permitWitnessTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "address", name: "token", type: "address" }, + ], + internalType: "struct IAllowanceTransfer.AllowanceTransferDetails[]", + name: "transferDetails", + type: "tuple[]", + }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "address", name: "token", type: "address" }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +]; diff --git a/static/scripts/rewards/cirip/ens-lookup.ts b/static/scripts/rewards/cirip/ens-lookup.ts new file mode 100644 index 0000000..67a772f --- /dev/null +++ b/static/scripts/rewards/cirip/ens-lookup.ts @@ -0,0 +1,67 @@ +import { ethers } from "ethers"; +import abi from "../abis/cirip.json"; +import { fetchEns } from "./fetch-ens"; +import { queryReverseEns } from "./query-reverse-ens"; + +export const UBIQUITY_RPC_ENDPOINT = "https://rpc-pay.ubq.fi/v1/mainnet"; +export const reverseEnsInterface = new ethers.utils.Interface(abi); + +// addEventListener("fetch", event => { +// event.respondWith(handleRequest(event.request).catch(err => new Response(err.stack, { status: 500 }))); +// }); + +export async function ensLookup(addr: string) { + const _address = "/".concat(addr); // quick adapter + + // try { + const start = _address.indexOf("/0x"); + if (start == -1) throw "No ethereum address provided."; + if (_address.length <= 42 + start) { + throw "Invalid ethereum address provided."; + } + const address = _address.substring(start + 1, start + 43).toLowerCase(); + + let reverseRecord = null as null | string; + // let response = ""; + try { + reverseRecord = await queryReverseEns(address); + const responseParsed = JSON.parse(reverseRecord).result; + const _reverseRecord = ethers.utils.defaultAbiCoder.decode([ethers.utils.ParamType.from("string[]")], responseParsed); + reverseRecord = _reverseRecord[0][0]; + } catch (e) { + console.error(e); + // throw "Error contacting ethereum node. \nCause: '" + e + "'. \nResponse: " + response; + } + + const allDomains = await fetchEns(address); + + if (reverseRecord == "") { + reverseRecord = null; + } + + // if reverse record is set, validate addr owns this domain. + if (reverseRecord != null && !allDomains.includes(reverseRecord)) { + console.warn("Failed to validate! Reverse record set to " + reverseRecord + ", but user does not own this name."); + reverseRecord = null; + } + + return { + reverseRecord: reverseRecord, + domains: allDomains, + }; + // new Response(JSON.stringify(response), { + // headers: { + // "Content-Type": "application/json;charset=UTF-8", + // "Access-Control-Allow-Origin": "*", + // }, + // }); + // } catch (e) { + // return new Response("Error: " + e, { + // status: 400, + // headers: { + // "Content-Type": "text/raw;charset=UTF-8", + // "Access-Control-Allow-Origin": "*", + // }, + // }); + // } +} diff --git a/static/scripts/rewards/cirip/fetch-ens.ts b/static/scripts/rewards/cirip/fetch-ens.ts new file mode 100644 index 0000000..ba3ba46 --- /dev/null +++ b/static/scripts/rewards/cirip/fetch-ens.ts @@ -0,0 +1,12 @@ +import { queryGraph } from "./query-graph"; + +export async function fetchEns(address: string) { + const endpoint = "https://api.thegraph.com/subgraphs/name/ensdomains/ens"; + const query = `{ + domains(where:{owner:"${address.toLowerCase()}"}) { + name + } + }`; + const res = await queryGraph(endpoint, query); + return res.data.domains.map((domain: { name: string }) => domain.name); +} diff --git a/static/scripts/rewards/cirip/query-graph.ts b/static/scripts/rewards/cirip/query-graph.ts new file mode 100644 index 0000000..8df99d5 --- /dev/null +++ b/static/scripts/rewards/cirip/query-graph.ts @@ -0,0 +1,12 @@ +export async function queryGraph(endpoint: string | URL | Request, query: string) { + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ query }), + }); + + return response.json(); +} diff --git a/static/scripts/rewards/cirip/query-reverse-ens.ts b/static/scripts/rewards/cirip/query-reverse-ens.ts new file mode 100644 index 0000000..5b14fd7 --- /dev/null +++ b/static/scripts/rewards/cirip/query-reverse-ens.ts @@ -0,0 +1,20 @@ +import { reverseEnsInterface, UBIQUITY_RPC_ENDPOINT } from "./ens-lookup"; + +export async function queryReverseEns(address: string) { + const data = reverseEnsInterface.encodeFunctionData("getNames", [[address.substring(2)]]); + + const response = await fetch(UBIQUITY_RPC_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: "1", + method: "eth_call", + params: [{ to: "0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C", data: data }, "latest"], + }), + }); + + return response.text(); +} diff --git a/static/scripts/rewards/constants.ts b/static/scripts/rewards/constants.ts new file mode 100644 index 0000000..891a651 --- /dev/null +++ b/static/scripts/rewards/constants.ts @@ -0,0 +1,56 @@ +// type RPC = { url: string; tracking?: string; trackingDetails?: string }; +// type Network = { name?: string; rpcs: RPC[]; websiteDead?: boolean; rpcWorking?: boolean }; +// type Networks = { [key: string]: Network }; + +declare const extraRpcs: Record; // @DEV: passed in at build time check build/esbuild-build.ts + +export enum NetworkIds { + Mainnet = 1, + Goerli = 5, + Gnosis = 100, + Anvil = 31337, +} + +export enum Tokens { + DAI = "0x6b175474e89094c44da98b954eedeac495271d0f", + WXDAI = "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", +} + +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) { + const networkName = networkNames[networkId as keyof typeof networkNames]; + if (!networkName) { + console.error(`Unknown network ID: ${networkId}`); + } + return networkName ?? "Unknown Network"; +} + +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]: ["https://rpc.ankr.com/gnosis", ...(extraRpcs[NetworkIds.Gnosis] || [])], + [NetworkIds.Anvil]: ["http://127.0.0.1:8545", ""], +}; + +export const permit2Address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; +export const nftAddress = "0xAa1bfC0e51969415d64d6dE74f27CDa0587e645b"; diff --git a/static/scripts/rewards/helpers.ts b/static/scripts/rewards/helpers.ts new file mode 100644 index 0000000..b9c3896 --- /dev/null +++ b/static/scripts/rewards/helpers.ts @@ -0,0 +1,80 @@ +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) { + if (networkId === 31337) + return new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545", { + name: "http://127.0.0.1:8545", + chainId: 31337, + ensAddress: "", + }); + + const promises = networkRpcs[networkId].map(async (baseURL: string) => { + try { + const startTime = performance.now(); + const API = axios.create({ + baseURL, + headers: RPC_HEADER, + }); + + const { data } = await API.post("", RPC_BODY); + const endTime = performance.now(); + const latency = endTime - startTime; + if (verifyBlock(data)) { + return Promise.resolve({ + latency, + baseURL, + }); + } else { + return Promise.reject(); + } + } catch (error) { + return Promise.reject(); + } + }); + + const { baseURL: optimalRPC } = await Promise.any(promises); + return new ethers.providers.JsonRpcProvider(optimalRPC, { + name: optimalRPC, + chainId: networkId, + ensAddress: "", + }); +} diff --git a/static/scripts/rewards/index.ts b/static/scripts/rewards/index.ts new file mode 100644 index 0000000..9aa754c --- /dev/null +++ b/static/scripts/rewards/index.ts @@ -0,0 +1,20 @@ +import { init } from "./render-transaction/render-transaction"; +import { grid } from "./the-grid"; + +(async function appAsyncWrapper() { + try { + // display commit hash + const commit = await fetch("commit.txt"); + if (commit.ok) { + const commitHash = await commit.text(); + const buildElement = document.querySelector(`#build a`) as HTMLAnchorElement; + buildElement.innerHTML = commitHash; + buildElement.href = `https://github.com/ubiquity/pay.ubq.fi/commit/${commitHash}`; + } + init().catch(console.error); + } catch (error) { + console.error(error); + } +})().catch(console.error); + +grid(document.getElementById("grid") as HTMLElement); diff --git a/static/scripts/rewards/invalidate-component.ts b/static/scripts/rewards/invalidate-component.ts new file mode 100644 index 0000000..8687d32 --- /dev/null +++ b/static/scripts/rewards/invalidate-component.ts @@ -0,0 +1,12 @@ +const invalidateBtnInnerHTML = `
Void
+`; + +// parse string and turn into an html entity +function parseHtml(html: string) { + const button = document.createElement("button"); + button.id = "invalidateBtn"; + button.innerHTML = html; + return button.cloneNode(true) as HTMLButtonElement; +} + +export default parseHtml(invalidateBtnInnerHTML); diff --git a/static/scripts/rewards/render-transaction/index.ts b/static/scripts/rewards/render-transaction/index.ts new file mode 100644 index 0000000..ee56eba --- /dev/null +++ b/static/scripts/rewards/render-transaction/index.ts @@ -0,0 +1,42 @@ +import { networkExplorers } from "../constants"; +import { getOptimalProvider } from "../helpers"; +import { ClaimTx } from "./tx-type"; + +class AppState { + public claimTxs: ClaimTx[] = []; + private _currentIndex = 0; + + get currentIndex(): number { + return this._currentIndex; + } + + get currentTx(): ClaimTx | null { + return this.currentIndex < this.claimTxs.length ? this.claimTxs[this.currentIndex] : null; + } + + async currentNetworkRpc(): Promise { + if (!this.currentTx) { + return (await getOptimalProvider(1)).connection.url; + } + return (await getOptimalProvider(this.currentTx.networkId)).connection.url; + } + + get currentExplorerUrl(): string { + if (!this.currentTx) { + return "https://etherscan.io"; + } + return networkExplorers[this.currentTx.networkId] || "https://etherscan.io"; + } + + nextTx(): ClaimTx | null { + this._currentIndex = Math.min(this.claimTxs.length - 1, this._currentIndex + 1); + return this.currentTx; + } + + previousTx(): ClaimTx | null { + this._currentIndex = Math.max(0, this._currentIndex - 1); + return this.currentTx; + } +} + +export const app = new AppState(); diff --git a/static/scripts/rewards/render-transaction/insert-table-data.ts b/static/scripts/rewards/render-transaction/insert-table-data.ts new file mode 100644 index 0000000..0da3719 --- /dev/null +++ b/static/scripts/rewards/render-transaction/insert-table-data.ts @@ -0,0 +1,129 @@ +import { ethers } from "ethers"; +import { app } from "."; +import { Erc20Permit, Erc721Permit } from "./tx-type"; +import { fetchTreasury } from "../web3/erc20-permit"; +import { renderTokenSymbol } from "./render-token-symbol"; +import { networkExplorers } from "../constants"; + +export function shortenAddress(address: string): string { + return `${address.slice(0, 10)}...${address.slice(-8)}`; +} + +export async function insertErc20PermitTableData( + permit: Erc20Permit, + provider: ethers.providers.JsonRpcProvider, + symbol: string, + decimals: number, + table: Element +) { + const requestedAmountElement = document.getElementById("rewardAmount") as Element; + renderToFields(permit.transferDetails.to, app.currentExplorerUrl); + renderTokenFields(permit.permit.permitted.token, app.currentExplorerUrl); + + renderDetailsFields([ + { name: "From", value: `${permit.owner}` }, + { + name: "Expiry", + value: permit.permit.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(permit.permit.deadline.toNumber()).toLocaleString() : undefined, + }, + { name: "Balance", value: "Loading..." }, + { name: "Allowance", value: "Loading..." }, + ]); + + renderTokenSymbol({ + requestedAmountElement, + tokenAddress: permit.permit.permitted.token, + ownerAddress: permit.owner, + amount: permit.transferDetails.requestedAmount, + explorerUrl: networkExplorers[permit.networkId], + symbol, + decimals, + }); + + // Optimistically rendered what we can so consider it loaded + table.setAttribute(`data-claim`, "ok"); + table.setAttribute(`data-contract-loaded`, "true"); + table.setAttribute(`data-claim-rendered`, "true"); + + const { balance, allowance } = await fetchTreasury(permit.permit.permitted.token, permit.owner, provider); + + renderDetailsFields([ + { name: "From", value: `${permit.owner}` }, + { + name: "Expiry", + value: permit.permit.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(permit.permit.deadline.toNumber()).toLocaleString() : undefined, + }, + { name: "Balance", value: balance.gte(0) ? `${ethers.utils.formatUnits(balance, decimals)} ${symbol}` : "N/A" }, + { name: "Allowance", value: allowance.gte(0) ? `${ethers.utils.formatUnits(allowance, decimals)} ${symbol}` : "N/A" }, + ]); +} + +export function insertErc721PermitTableData(permit: 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; + renderDetailsFields([ + { + name: "NFT address", + value: `${permit.nftAddress}`, + }, + { + name: "Expiry", + value: permit.request.deadline.lte(Number.MAX_SAFE_INTEGER.toString()) ? new Date(permit.request.deadline.toNumber()).toLocaleString() : undefined, + }, + { + name: "GitHub Organization", + value: `${GITHUB_ORGANIZATION_NAME}`, + }, + { + name: "GitHub Repository", + value: `${GITHUB_REPOSITORY_NAME}`, + }, + { + name: "GitHub Issue", + value: `${GITHUB_ISSUE_ID}`, + }, + { + name: "GitHub Username", + value: `${GITHUB_USERNAME}`, + }, + { name: "Contribution Type", value: GITHUB_CONTRIBUTION_TYPE.split(",").join(", ") }, + ]); + table.setAttribute(`data-claim-rendered`, "true"); + return requestedAmountElement; +} + +function renderDetailsFields(additionalDetails: { name: string; value: string | undefined }[]) { + const additionalDetailsDiv = document.getElementById("additionalDetailsTable") as Element; + let additionalDetailsHtml = ""; + for (const { name, value } of additionalDetails) { + if (!value) continue; + additionalDetailsHtml += ` +
${name}
+
${value}
+ `; + } + + additionalDetailsDiv.innerHTML = additionalDetailsHtml; +} + +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)}
`; + + const tokenBoth = document.getElementById(`rewardToken`) as Element; + tokenBoth.innerHTML = `${tokenBoth.innerHTML}`; +} + +function renderToFields(receiverAddress: string, explorerUrl: string) { + const toFull = document.querySelector("#To .full") as Element; + const toShort = document.querySelector("#To .short") as Element; + toFull.innerHTML = `
${receiverAddress}
`; + toShort.innerHTML = `
${shortenAddress(receiverAddress)}
`; + + const toBoth = document.getElementById(`rewardRecipient`) as Element; + toBoth.innerHTML = `${toBoth.innerHTML}`; +} diff --git a/static/scripts/rewards/render-transaction/render-ens-name.ts b/static/scripts/rewards/render-transaction/render-ens-name.ts new file mode 100644 index 0000000..8be3262 --- /dev/null +++ b/static/scripts/rewards/render-transaction/render-ens-name.ts @@ -0,0 +1,42 @@ +import { ensLookup } from "../cirip/ens-lookup"; +import { app } from "./index"; + +type EnsParams = + | { + element: Element; + address: string; + tokenAddress: string; + tokenView: true; + } + | { + element: Element; + address: string; + tokenAddress?: undefined; + tokenView?: false; + }; + +export async function renderEnsName({ element, address, tokenAddress, tokenView }: EnsParams): Promise { + let href: string = ""; + try { + const resolved = await ensLookup(address); + let ensName: undefined | string; + if (resolved.reverseRecord) { + ensName = resolved.reverseRecord; + } else if (resolved.domains.length) { + const domain = resolved.domains.shift(); + if (domain) { + ensName = domain; + } + } + if (ensName) { + if (tokenView) { + href = `${app.currentExplorerUrl}/token/${tokenAddress}?a=${address}`; + } else { + href = `${app.currentExplorerUrl}/address/${address}"`; + } + element.innerHTML = `${ensName}`; + } + } catch (error) { + console.error(error); + } +} diff --git a/static/scripts/rewards/render-transaction/render-token-symbol.ts b/static/scripts/rewards/render-transaction/render-token-symbol.ts new file mode 100644 index 0000000..429bda7 --- /dev/null +++ b/static/scripts/rewards/render-transaction/render-token-symbol.ts @@ -0,0 +1,56 @@ +import { BigNumberish, utils } from "ethers"; +import { getErc20Contract } from "../helpers"; +import { JsonRpcProvider } from "@ethersproject/providers"; + +export const tokens = [ + { + name: "WXDAI", + address: "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", + }, + { + name: "DAI", + address: "0x6b175474e89094c44da98b954eedeac495271d0f", + }, +]; + +export function renderTokenSymbol({ + requestedAmountElement, + tokenAddress, + ownerAddress, + amount, + explorerUrl, + symbol, + decimals, +}: { + requestedAmountElement: Element; + tokenAddress: string; + ownerAddress: string; + amount: BigNumberish; + explorerUrl: string; + symbol: string; + decimals: number; +}) { + return (requestedAmountElement.innerHTML = `${utils.formatUnits( + amount, + decimals + )} ${symbol}`); +} + +export async function renderNftSymbol({ + table, + requestedAmountElement, + tokenAddress, + explorerUrl, + provider, +}: { + table: Element; + requestedAmountElement: Element; + tokenAddress: string; + explorerUrl: string; + provider: JsonRpcProvider; +}): Promise { + const contract = await getErc20Contract(tokenAddress, provider); + const symbol = await contract.symbol(); + table.setAttribute(`data-contract-loaded`, "true"); + requestedAmountElement.innerHTML = `1 ${symbol}`; +} diff --git a/static/scripts/rewards/render-transaction/render-transaction.ts b/static/scripts/rewards/render-transaction/render-transaction.ts new file mode 100644 index 0000000..4f3d015 --- /dev/null +++ b/static/scripts/rewards/render-transaction/render-transaction.ts @@ -0,0 +1,155 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Type } from "@sinclair/typebox"; +import { Value } from "@sinclair/typebox/value"; +import { networkExplorers } from "../constants"; +import { getOptimalProvider } from "../helpers"; +import { claimButton, hideClaimButton, resetClaimButton } from "../toaster"; +import { claimErc20PermitHandler, generateInvalidatePermitAdminControl, processERC20 } from "../web3/erc20-permit"; +import { claimErc721PermitHandler } from "../web3/erc721-permit"; +import { app } from "./index"; +import { insertErc721PermitTableData } from "./insert-table-data"; +import { renderEnsName } from "./render-ens-name"; +import { renderNftSymbol } from "./render-token-symbol"; +import { setClaimMessage } from "./set-claim-message"; +import { claimTxT } from "./tx-type"; +import { removeAllEventListeners } from "./utils"; +import { handleNetwork } from "../web3/wallet"; + +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).catch(console.error); +} + +function setPagination(nextTxButton: Element | null, prevTxButton: Element | null) { + if (!nextTxButton || !prevTxButton) return; + if (app.claimTxs.length > 1) { + prevTxButton.classList.remove("hide-pagination"); + nextTxButton.classList.remove("hide-pagination"); + + prevTxButton.classList.add("show-pagination"); + nextTxButton.classList.add("show-pagination"); + } +} + +type Success = boolean; +export async function renderTransaction(provider: JsonRpcProvider, nextTx?: boolean): Promise { + const table = document.getElementsByTagName(`table`)[0]; + resetClaimButton(); + + if (nextTx) { + app.nextTx(); + if (!app.claimTxs || app.claimTxs.length <= 1) { + // already hidden + } else { + setPagination(document.getElementById("nextTx"), document.getElementById("previousTx")); + + const rewardsCount = document.getElementById("rewardsCount") as Element; + rewardsCount.innerHTML = `${app.currentIndex + 1}/${app.claimTxs.length} reward`; + table.setAttribute(`data-claim`, "none"); + } + } + + if (!app.currentTx) { + hideClaimButton(); + return false; + } + + if (app.currentTx.type === "erc20-permit") { + await processERC20(app.currentTx.permit.permitted.token, provider, app.currentTx, table); + + // insert tx data into table + const toElement = document.getElementById(`rewardRecipient`) as Element; + renderEnsName({ element: toElement, address: app.currentTx.transferDetails.to }).catch(console.error); + + generateInvalidatePermitAdminControl(app.currentTx).catch(console.error); + + claimButton.element.addEventListener("click", claimErc20PermitHandler(app.currentTx, optimalRPC)); + } else if (app.currentTx.type === "erc721-permit") { + const requestedAmountElement = insertErc721PermitTableData(app.currentTx, table); + table.setAttribute(`data-claim`, "ok"); + + renderNftSymbol({ + tokenAddress: app.currentTx.nftAddress, + explorerUrl: networkExplorers[app.currentTx.networkId], + table, + requestedAmountElement, + provider, + }).catch(console.error); + + const toElement = document.getElementById(`rewardRecipient`) as Element; + renderEnsName({ element: toElement, address: app.currentTx.request.beneficiary }).catch(console.error); + + claimButton.element.addEventListener("click", claimErc721PermitHandler(app.currentTx, provider)); + } + + return true; +} diff --git a/static/scripts/rewards/render-transaction/set-claim-message.ts b/static/scripts/rewards/render-transaction/set-claim-message.ts new file mode 100644 index 0000000..51d38d1 --- /dev/null +++ b/static/scripts/rewards/render-transaction/set-claim-message.ts @@ -0,0 +1,6 @@ +export function setClaimMessage({ type, message }: { type: string; message: string }): void { + const claimMessageType = document.querySelector(`table > thead th`) as Element; + const claimMessageBody = document.querySelector(`table > thead td`) as Element; + claimMessageType.innerHTML = `
${type}
`; + claimMessageBody.innerHTML = `
${message}
`; +} diff --git a/static/scripts/rewards/render-transaction/tx-type.ts b/static/scripts/rewards/render-transaction/tx-type.ts new file mode 100644 index 0000000..23df134 --- /dev/null +++ b/static/scripts/rewards/render-transaction/tx-type.ts @@ -0,0 +1,68 @@ +import { StaticDecode, Type as T } from "@sinclair/typebox"; +import { BigNumber } from "ethers"; + +const bigNumberT = T.Transform(T.Union([T.RegExp(/^\d+$/), T.Number()])) + .Decode((value) => BigNumber.from(value)) + .Encode((value) => value.toString()); + +// const networkIdT = T.Transform(T.Union([T.RegExp(/^0x\d+$/), T.Number()])) +// .Decode(value => (typeof value === "number" ? "0x" + value.toString(16) : value)) +// .Encode(value => value); + +const networkIdT = T.Number(); + +const addressT = T.Transform(T.RegExp(/^0x[a-fA-F0-9]{40}$/)) + .Decode((value) => value.toLowerCase()) + .Encode((value) => value); + +const signatureT = T.Transform(T.RegExp(/^0x[a-fA-F0-9]+$/)) + .Decode((value) => value.toLowerCase()) + .Encode((value) => value); + +const erc20PermitT = T.Object({ + type: T.Literal("erc20-permit"), + permit: T.Object({ + permitted: T.Object({ + token: addressT, + amount: bigNumberT, + }), + nonce: bigNumberT, + deadline: bigNumberT, + }), + transferDetails: T.Object({ + to: addressT, + requestedAmount: bigNumberT, + }), + owner: addressT, + signature: signatureT, + networkId: networkIdT, +}); + +export type Erc20Permit = StaticDecode; + +const erc721Permit = T.Object({ + type: T.Literal("erc721-permit"), + request: T.Object({ + beneficiary: addressT, + deadline: bigNumberT, + keys: T.Array(T.String()), + nonce: bigNumberT, + values: T.Array(T.String()), + }), + nftMetadata: T.Object({ + GITHUB_ORGANIZATION_NAME: T.String(), + GITHUB_REPOSITORY_NAME: T.String(), + GITHUB_ISSUE_ID: T.String(), + GITHUB_USERNAME: T.String(), + GITHUB_CONTRIBUTION_TYPE: T.String(), + }), + nftAddress: addressT, + networkId: networkIdT, + signature: signatureT, +}); + +export type Erc721Permit = StaticDecode; + +export const claimTxT = T.Union([erc20PermitT, erc721Permit]); + +export type ClaimTx = StaticDecode; diff --git a/static/scripts/rewards/render-transaction/utils.ts b/static/scripts/rewards/render-transaction/utils.ts new file mode 100644 index 0000000..80281a6 --- /dev/null +++ b/static/scripts/rewards/render-transaction/utils.ts @@ -0,0 +1,5 @@ +export function removeAllEventListeners(element: Element): Element { + const clone = element.cloneNode(true) as Element; + element.replaceWith(clone); + return clone; +} diff --git a/static/scripts/rewards/the-grid.ts b/static/scripts/rewards/the-grid.ts new file mode 100644 index 0000000..bd9832e --- /dev/null +++ b/static/scripts/rewards/the-grid.ts @@ -0,0 +1,160 @@ +export function grid(node = document.body) { + // Create canvas and WebGL context + const canvas = document.createElement("canvas"); + const devicePixelRatio = window.devicePixelRatio || 1; + canvas.width = window.innerWidth * devicePixelRatio; + canvas.height = window.innerHeight * devicePixelRatio; + node.appendChild(canvas); + + const gl = canvas.getContext("webgl") as WebGLRenderingContext; + + // Enable alpha blending + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // Define shader sources + const vertexShaderSource = ` + attribute vec2 a_position; + + void main() { + gl_Position = vec4(a_position, 0, 1); + } +`; + + // cspell:ignore mediump + const fragmentShaderSource = ` + precision mediump float; + + uniform vec2 u_resolution; + uniform float u_time; + + float rand(vec2 n) { + return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453); + } + + void main() { + vec3 color = vec3(128.0/255.0, 128.0/255.0, 128.0/255.0); // #808080 + vec2 tilePosition = mod(gl_FragCoord.xy, 24.0); + vec2 tileNumber = floor(gl_FragCoord.xy / 24.0); + + float period = rand(tileNumber) * 9.0 + 1.0; // Random value in the range [1, 10] + float phase = fract(u_time / period / 8.0); // Animation eight times slower + float opacity = (1.0 - abs(phase * 2.0 - 1.0)) * 0.125; // Limit maximum opacity to 0.25 + + vec4 backgroundColor = vec4(color, opacity); + + if (tilePosition.x > 23.0 && tilePosition.y < 1.0) { + gl_FragColor = vec4(color, 1.0); // Full opacity for the dot + } else { + gl_FragColor = backgroundColor; + } + } +`; + + // Define shader creation function + function createShader(gl: WebGLRenderingContext, type: number, source: string) { + const shader = gl.createShader(type); + if (!shader) { + console.error("An error occurred creating the shaders"); + return null; + } + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); + return null; + } + return shader; + } + + // Create vertex and fragment shaders + const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); + if (!vertexShader) { + console.error("An error occurred creating the vertex shader"); + return; + } + const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); + if (!fragmentShader) { + console.error("An error occurred creating the fragment shader"); + return; + } + + // Create program, attach shaders, and link + const program = gl.createProgram(); + if (!program) { + console.error("An error occurred creating the program"); + return; + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + // Verify program link status + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error("Unable to initialize the shader program: " + gl.getProgramInfoLog(program)); + return; + } + + // Use the program + gl.useProgram(program); + + // Get location of time and resolution uniforms + const timeUniformLocation = gl.getUniformLocation(program, "u_time"); + const resolutionUniformLocation = gl.getUniformLocation(program, "u_resolution"); + + // Bind the position buffer and set attribute pointer + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); + + const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); + gl.enableVertexAttribArray(positionAttributeLocation); + gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0); + + // Resize function + function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement) { + // Lookup the size the browser is displaying the canvas. + const displayWidth = window.innerWidth; + const displayHeight = window.innerHeight; + + // Check if the canvas is not the same size. + if (canvas.width != displayWidth || canvas.height != displayHeight) { + // Make the canvas the same size + canvas.width = displayWidth; + canvas.height = displayHeight; + + // Update WebGL viewport to match + gl.viewport(0, 0, canvas.width, canvas.height); + } + } + + // Render function + function render() { + resizeCanvasToDisplaySize(canvas); // Check and update canvas size each frame + + // Update resolution uniform + gl.uniform2f(resolutionUniformLocation, canvas.width, canvas.height); + + gl.clearColor(0.0, 0.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Update time uniform + gl.uniform1f(timeUniformLocation, performance.now() / 1000.0); + + // Draw + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + // Request next frame + requestAnimationFrame(render); + } + + // Handle window resize + window.addEventListener("resize", () => { + resizeCanvasToDisplaySize(canvas); + }); + + // Start the render loop + render(); +} diff --git a/static/scripts/rewards/toaster.ts b/static/scripts/rewards/toaster.ts new file mode 100644 index 0000000..9294e72 --- /dev/null +++ b/static/scripts/rewards/toaster.ts @@ -0,0 +1,89 @@ +export const toaster = { + create: createToast, + error: errorToast, + icons: { + success: "fa-circle-check", + error: "fa-circle-xmark", + warning: "fa-triangle-exclamation", + info: "fa-circle-info", + }, +}; + +export const claimButton = { + 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) { + const toastDetails = { + timer: 5000, + } as { + timer: number; + timeoutId?: NodeJS.Timeout; + }; + // Getting the icon and text for the toast based on the id passed + const _icon = toaster.icons[meaning]; + const toastContent = document.createElement("li"); // Creating a new 'li' element for the toast + toastContent.className = `toast .${_icon} ${meaning}`; // Setting the classes for the toast + + // Setting the inner HTML for the toast + toastContent.innerHTML = `
${text}
`; + + // attaching a click event listener to the toast to remove it when the close icon is clicked + const i = document.createElement("i"); + i.className = "fa-solid fa-xmark"; + i.onclick = () => removeToast(toastContent, toastDetails.timeoutId); + toastContent.appendChild(i); + + notifications.appendChild(toastContent); // Append the toast to the notification ul + + // Setting a timeout to remove the toast after the specified duration + toastDetails.timeoutId = setTimeout(() => removeToast(toastContent, toastDetails.timeoutId), toastDetails.timer); +} + +function removeToast(toast: HTMLElement, timeoutId?: NodeJS.Timeout) { + toast.classList.add("hide"); + if (timeoutId) { + clearTimeout(timeoutId); // Clearing the timeout for the toast + } + setTimeout(() => toast.remove(), 500); // Removing the toast after 500ms +} + +export function loadingClaimButton(triggerLoader = true) { + 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"); + } +} + +export function resetClaimButton() { + claimButton.element.disabled = false; + claimButton.element.classList.add("hide-cl"); + claimButton.element.classList.remove("show-cl"); +} + +export function hideClaimButton() { + claimButton.element.disabled = true; + claimButton.element.classList.add("hide-cl"); + claimButton.element.classList.remove("show-cl"); +} + +type Err = { stack?: unknown; reason?: string } extends Error ? Error : { stack?: unknown; reason?: string }; + +export function errorToast(error: Err, errorMessage?: string) { + delete error.stack; + const errorData = JSON.stringify(error, null, 2); + if (errorMessage) { + toaster.create("error", errorMessage); + } 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); + } +} diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts new file mode 100644 index 0000000..fd26d9f --- /dev/null +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -0,0 +1,183 @@ +import { BigNumber, BigNumberish, ethers } from "ethers"; +import { permit2Abi } from "../abis"; +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 { insertErc20PermitTableData } from "../render-transaction/insert-table-data"; + +export async function processERC20(tokenAddress: string, provider: JsonRpcProvider, permit: Erc20Permit, table: Element) { + let symbol = tokenAddress === tokens[0].address ? tokens[0].name : tokenAddress === tokens[1].address ? tokens[1].name : ""; + let decimals = tokenAddress === tokens[0].address ? 18 : tokenAddress === tokens[1].address ? 18 : -1; + + if (!symbol || decimals === -1) { + try { + const contract = await getErc20Contract(tokenAddress, provider); + symbol = contract.symbol(); + decimals = contract.decimals(); + } catch (err) { + throw new Error(`Error fetching symbol and decimals for token address: ${tokenAddress}`); + } + } + + await insertErc20PermitTableData(permit, provider, symbol, decimals, table); +} + +export async function fetchTreasury(contractAddr: string, owner: string, provider: JsonRpcProvider) { + try { + const contract = await getErc20Contract(contractAddr, provider); + const [balance, allowance] = await Promise.all([contract.balanceOf(owner), contract.allowance(owner, permit2Address)]); + return { balance, allowance } as { balance: BigNumber; allowance: BigNumber }; + } catch (err) { + console.log(err); + } + + return { balance: BigNumber.from(0), allowance: BigNumber.from(0) }; +} + +export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcProvider) { + return async function handler() { + try { + const signer = await connectWallet(); + if (!signer) { + return; + } + + if (!(await checkPermitClaimable(permit, signer, provider))) { + return; + } + + 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 + + claimButton.element.removeEventListener("click", handler); + renderTransaction(provider).catch(console.error); + } catch (error: unknown) { + if (error instanceof Error) { + console.log(error); + errorToast(error, error.message); + resetClaimButton(); + } + } + }; +} + +export async function checkPermitClaimable(permit: Erc20Permit, signer: ethers.providers.JsonRpcSigner | null, provider: JsonRpcProvider) { + const isClaimed = await isNonceClaimed(permit); + if (isClaimed) { + toaster.create("error", `Your reward for this task has already been claimed or invalidated.`); + return false; + } + + if (permit.permit.deadline.lt(Math.floor(Date.now() / 1000))) { + toaster.create("error", `This reward has expired.`); + return false; + } + + const { balance, allowance } = await fetchTreasury(permit.permit.permitted.token, permit.owner, provider); + const permitted = BigNumber.from(permit.permit.permitted.amount); + const isSolvent = balance.gte(permitted); + const isAllowed = allowance.gte(permitted); + + if (!isSolvent) { + toaster.create("error", `Not enough funds on funding wallet to collect this reward. Please let the financier know.`); + return false; + } + if (!isAllowed) { + toaster.create("error", `Not enough allowance on the funding wallet to collect this reward. Please let the financier know.`); + return false; + } + + if (signer) { + const user = (await signer.getAddress()).toLowerCase(); + const beneficiary = permit.transferDetails.to.toLowerCase(); + if (beneficiary !== user) { + toaster.create("warning", `This reward is not for you.`); + return false; + } + } + + return true; +} + +export async function generateInvalidatePermitAdminControl(permit: Erc20Permit) { + const signer = await connectWallet(); + if (!signer) { + return; + } + + const user = (await signer.getAddress()).toLowerCase(); + const owner = permit.owner.toLowerCase(); + if (owner !== user) { + return; + } + + const controls = document.getElementById("controls") as HTMLDivElement; + controls.appendChild(invalidateButton); + + invalidateButton.addEventListener("click", async function invalidateButtonClickHandler() { + try { + const signer = await connectWallet(); + if (!signer) { + return; + } + const isClaimed = await isNonceClaimed(permit); + if (isClaimed) { + toaster.create("error", `This reward has already been claimed or invalidated.`); + return; + } + await invalidateNonce(signer, permit.permit.nonce); + } catch (error: unknown) { + if (error instanceof Error) { + console.log(error); + errorToast(error, error.message); + return; + } + } + toaster.create("info", "Nonce invalidation transaction sent"); + }); +} + +//mimics https://github.com/Uniswap/permit2/blob/a7cd186948b44f9096a35035226d7d70b9e24eaf/src/SignatureTransfer.sol#L150 +export async function isNonceClaimed(permit: Erc20Permit): Promise { + const provider = await getOptimalProvider(permit.networkId); + + const 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 bit = BigNumber.from(1).shl(bitPos); + const flipped = BigNumber.from(bitmap).xor(bit); + + return bit.and(flipped).eq(0); +} + +export async function invalidateNonce(signer: ethers.providers.JsonRpcSigner, nonce: BigNumberish): Promise { + const permit2Contract = new ethers.Contract(permit2Address, permit2Abi, signer); + const { wordPos, bitPos } = nonceBitmap(nonce); + // mimics https://github.com/ubiquity/pay.ubq.fi/blob/c9e7ed90718fe977fd9f348db27adf31d91d07fb/scripts/solidity/test/Permit2.t.sol#L428 + const bit = BigNumber.from(1).shl(bitPos); + const sourceBitmap = await permit2Contract.nonceBitmap(await signer.getAddress(), wordPos.toString()); + const mask = sourceBitmap.or(bit); + await permit2Contract.invalidateUnorderedNonces(wordPos, mask); +} + +// mimics https://github.com/Uniswap/permit2/blob/db96e06278b78123970183d28f502217bef156f4/src/SignatureTransfer.sol#L142 +export function nonceBitmap(nonce: BigNumberish): { wordPos: BigNumber; bitPos: number } { + // wordPos is the first 248 bits of the nonce + const wordPos = BigNumber.from(nonce).shr(8); + // bitPos is the last 8 bits of the nonce + const bitPos = BigNumber.from(nonce).and(255).toNumber(); + return { wordPos, bitPos }; +} diff --git a/static/scripts/rewards/web3/erc721-permit.ts b/static/scripts/rewards/web3/erc721-permit.ts new file mode 100644 index 0000000..0dafff6 --- /dev/null +++ b/static/scripts/rewards/web3/erc721-permit.ts @@ -0,0 +1,64 @@ +import { JsonRpcProvider, TransactionResponse } from "@ethersproject/providers"; +import { ethers } from "ethers"; +import { nftRewardAbi } from "../abis/nftRewardAbi"; +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) { + return async function claimButtonHandler() { + const signer = await connectWallet(); + if (!signer) { + return; + } + + if ((await signer.getAddress()).toLowerCase() !== permit.request.beneficiary) { + toaster.create("warning", `This NFT is not for you.`); + resetClaimButton(); + return; + } + + if (permit.request.deadline.lt(Math.floor(Date.now() / 1000))) { + toaster.create("error", `This NFT has expired.`); + resetClaimButton(); + return; + } + + const isRedeemed = await isNonceRedeemed(permit, provider); + if (isRedeemed) { + toaster.create("error", `This NFT has already been redeemed.`); + resetClaimButton(); + return; + } + + loadingClaimButton(); + try { + const nftContract = new ethers.Contract(permit.nftAddress, nftRewardAbi, signer); + + const tx: TransactionResponse = await nftContract.safeMint(permit.request, permit.signature); + toaster.create("info", `Transaction sent. Waiting for confirmation...`); + const receipt = await tx.wait(); + toaster.create("success", `Claim Complete.`); + console.log(receipt.transactionHash); // @TODO: post to database + + claimButton.element.removeEventListener("click", claimButtonHandler); + + renderTransaction(provider, 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(); + } + } + }; +} + +export async function isNonceRedeemed(nftMint: Erc721Permit, provider: JsonRpcProvider): Promise { + const nftContract = new ethers.Contract(nftMint.nftAddress, nftRewardAbi, provider); + return nftContract.nonceRedeemed(nftMint.request.nonce); +} diff --git a/static/scripts/rewards/web3/wallet.ts b/static/scripts/rewards/web3/wallet.ts new file mode 100644 index 0000000..34766b8 --- /dev/null +++ b/static/scripts/rewards/web3/wallet.ts @@ -0,0 +1,103 @@ +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 currentNetworkId = (await web3provider.getNetwork()).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/audit-report/audit.css b/static/styles/audit-report/audit.css new file mode 100644 index 0000000..ef039e1 --- /dev/null +++ b/static/styles/audit-report/audit.css @@ -0,0 +1,185 @@ +@import url("../rewards/pay.css"); +@import url("../proxima.css"); +@import url("checkbox.css"); +@import url("toggle.css"); +@import url("../fa.css"); + +body { + margin: 0; + padding: 0; +} + +input, +textarea { + all: unset; + outline: 0; + border-radius: 5px; + width: 90vw; + border: 1px solid #d2d2e7; + height: 40px; + padding-left: 10px; + box-sizing: border-box; + text-transform: none; +} + +select { + outline: 0; + border-radius: 5px; + width: 90vw; + border: 1px solid #d2d2e7; + height: 40px; + padding-left: 10px; + box-sizing: border-box; + text-transform: none; +} + +textarea { + height: 140px; + white-space: break-spaces; +} + +input:focus, +textarea:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 40px; + padding: 32px 0; +} + +.mb-3 { + display: flex; + flex-direction: column; + gap: 10px; +} + +#getReport { + all: unset; + background: #0d6efd; + border-radius: 6px; + padding: 14px 20px; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 5px; + user-select: none; +} + +#getReport:not(:disabled):hover { + background: #0a58ca; +} + +#getReport:disabled { + cursor: default; +} + +.audit-tag { + font-size: 26px; + color: black; + font-weight: 900; +} + +a { + text-transform: none; +} + +a:hover { + color: black; +} + +#searchInput { + margin-bottom: 20px; + width: 100%; +} + +#resultTable { + border-spacing: 0.8em; +} + +.resultContainer tbody:before { + content: "@"; + display: block; + line-height: 10px; + text-indent: -99999px; +} + +.resultContainer { + width: 87vw; + border-spacing: 14px; + border: 1px solid #dfdfdf; + border-radius: 12px; + box-shadow: 0px 0px 6px #0089ff59; + display: flex; + flex-direction: column; + padding: 20px; +} + +td { + text-align: center; +} + +span { + color: white; +} + +.btn-container { + display: flex; + flex-direction: row; + align-self: flex-start; + align-items: center; + justify-content: center; + gap: 24px; +} + +.cache-title { + font-size: 15px; + font-weight: 900; + color: #0072ff; + user-select: none; +} + +.btn-loader { + border: 5px solid #fff; + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +#report-loader { + display: none; +} + +#amountHeader { + position: relative; + cursor: pointer; +} + +#sortArrow { + color: black; + font-size: 16px; + margin-left: 5px; + /* Adjust the spacing between the text and arrows */ + position: absolute; + top: 50%; + transform: translateY(-50%); +} diff --git a/static/styles/audit-report/checkbox.css b/static/styles/audit-report/checkbox.css new file mode 100644 index 0000000..90173bc --- /dev/null +++ b/static/styles/audit-report/checkbox.css @@ -0,0 +1,99 @@ +.tgl { + display: none; +} + +.tg-list-item { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 8px; +} + +.tgl, +.tgl:after, +.tgl:before, +.tgl *, +.tgl *:after, +.tgl *:before, +.tgl + .tgl-btn { + box-sizing: border-box; +} + +.tgl::-moz-selection, +.tgl:after::-moz-selection, +.tgl:before::-moz-selection, +.tgl *::-moz-selection, +.tgl *:after::-moz-selection, +.tgl *:before::-moz-selection, +.tgl + .tgl-btn::-moz-selection { + background: none; +} + +.tgl::selection, +.tgl:after::selection, +.tgl:before::selection, +.tgl *::selection, +.tgl *:after::selection, +.tgl *:before::selection, +.tgl + .tgl-btn::selection { + background: none; +} + +.tgl + .tgl-btn { + outline: 0; + display: block; + width: 4em; + height: 2em; + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.tgl + .tgl-btn:after, +.tgl + .tgl-btn:before { + position: relative; + display: block; + content: ""; + width: 50%; + height: 100%; +} + +.tgl + .tgl-btn:after { + left: 0; +} + +.tgl + .tgl-btn:before { + display: none; +} + +.tgl:checked + .tgl-btn:after { + left: 50%; +} + +.tgl-flat + .tgl-btn { + padding: 2px; + transition: all 0.2s ease; + background: #fff; + border: 4px solid #eaeaeb; + border-radius: 2em; +} + +.tgl-flat + .tgl-btn:after { + transition: all 0.2s ease; + background: #e5e5e5; + content: ""; + border-radius: 1em; +} + +.tgl-flat:checked + .tgl-btn { + border: 4px solid #723ae5; +} + +.tgl-flat:checked + .tgl-btn:after { + left: 50%; + background: #6e67d9; +} diff --git a/static/styles/audit-report/toggle.css b/static/styles/audit-report/toggle.css new file mode 100644 index 0000000..686026b --- /dev/null +++ b/static/styles/audit-report/toggle.css @@ -0,0 +1,132 @@ +:root { + --switches-bg-color: black; + --switches-label-color: white; + --switch-bg-color: white; + --switch-text-color: black; +} + +/* resize font-size on html and body level. html is required for widths based on rem */ +@media screen and (min-width: 1024px) { + html, + body { + font-size: 24px; + } +} + +@media screen and (max-width: 1024px) { + html, + body { + font-size: 16px; + } +} + +@media screen and (max-width: 600px) { + html, + body { + font-size: 12px; + } +} + +/* p - decorative, not required */ +p { + margin-top: 2rem; + font-size: 0.75rem; + text-align: center; +} + +/* container for all of the switch elements + - adjust "width" to fit the content accordingly +*/ +.switches-container { + width: 100%; + position: relative; + display: flex; + padding: 0; + position: relative; + background: var(--switches-bg-color); + line-height: 3rem; + margin-left: auto; + margin-right: auto; + height: 2rem; +} + +/* input (radio) for toggling. hidden - use labels for clicking on */ +.switches-container input { + visibility: hidden; + position: absolute; + top: 0; +} + +/* labels for the input (radio) boxes - something to click on */ +.switches-container label { + width: 50%; + padding: 0; + margin: 0; + text-align: center; + cursor: pointer; + color: var(--switches-label-color); + height: 2rem; + display: flex; + justify-content: center; + align-items: center; +} + +/* switch highlighters wrapper (sliding left / right) + - need wrapper to enable the even margins around the highlight box +*/ +.switch-wrapper { + position: absolute; + top: 0; + bottom: 0; + width: 50%; + padding: 0.15rem; + z-index: 3; + transition: transform 0.5s cubic-bezier(0.77, 0, 0.175, 1); + /* transition: transform 1s; */ +} + +/* switch box highlighter */ +.switch { + background: var(--switch-bg-color); + height: 100%; +} + +/* switch box labels + - default setup + - toggle afterwards based on radio:checked status +*/ +.switch div { + width: 100%; + text-align: center; + opacity: 0; + color: var(--switch-text-color); + transition: opacity 0.2s cubic-bezier(0.77, 0, 0.175, 1) 0.125s; + will-change: opacity; + position: absolute; + height: 2rem; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; +} + +/* slide the switch box from right to left */ +.switches-container input:nth-of-type(1):checked ~ .switch-wrapper { + transform: translateX(0%); +} + +/* slide the switch box from left to right */ +.switches-container input:nth-of-type(2):checked ~ .switch-wrapper { + transform: translateX(100%); +} + +/* toggle the switch box labels - first checkbox:checked - show first switch div */ +.switches-container input:nth-of-type(1):checked ~ .switch-wrapper .switch div:nth-of-type(1) { + opacity: 1; +} + +/* toggle the switch box labels - second checkbox:checked - show second switch div */ +.switches-container input:nth-of-type(2):checked ~ .switch-wrapper .switch div:nth-of-type(2) { + opacity: 1; +} diff --git a/static/styles/fa.css b/static/styles/fa.css new file mode 100644 index 0000000..4568cf7 --- /dev/null +++ b/static/styles/fa.css @@ -0,0 +1,8798 @@ +/*! + * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2022 Fonticons, Inc. + */ +.fa { + font-family: var(--fa-style-family, "Font Awesome 6 Free"); + font-weight: var(--fa-style, 900); +} + +.fa, +.fa-brands, +.fa-classic, +.fa-regular, +.fa-sharp, +.fa-solid, +.fab, +.far, +.fas { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: var(--fa-display, inline-block); + font-style: normal; + font-variant: normal; + line-height: 1; + text-rendering: auto; +} + +.fa-classic, +.fa-regular, +.fa-solid, +.far, +.fas { + font-family: "Font Awesome 6 Free"; +} + +.fa-brands, +.fab { + font-family: "Font Awesome 6 Brands"; +} + +.fa-1x { + font-size: 1em; +} + +.fa-2x { + font-size: 2em; +} + +.fa-3x { + font-size: 3em; +} + +.fa-4x { + font-size: 4em; +} + +.fa-5x { + font-size: 5em; +} + +.fa-6x { + font-size: 6em; +} + +.fa-7x { + font-size: 7em; +} + +.fa-8x { + font-size: 8em; +} + +.fa-9x { + font-size: 9em; +} + +.fa-10x { + font-size: 10em; +} + +.fa-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; +} + +.fa-xs { + font-size: 0.75em; + line-height: 0.08333em; + vertical-align: 0.125em; +} + +.fa-sm { + font-size: 0.875em; + line-height: 0.07143em; + vertical-align: 0.05357em; +} + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; +} + +.fa-xl { + font-size: 1.5em; + line-height: 0.04167em; + vertical-align: -0.125em; +} + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; +} + +.fa-fw { + text-align: center; + width: 1.25em; +} + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; +} + +.fa-ul > li { + position: relative; +} + +.fa-li { + left: calc(var(--fa-li-width, 2em) * -1); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; +} + +.fa-border { + border-radius: var(--fa-border-radius, 0.1em); + border: var(--fa-border-width, 0.08em) var(--fa-border-style, solid) var(--fa-border-color, #eee); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); +} + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); +} + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); +} + +.fa-beat { + -webkit-animation-name: fa-beat; + animation-name: fa-beat; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); +} + +.fa-bounce { + -webkit-animation-name: fa-bounce; + animation-name: fa-bounce; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); +} + +.fa-fade { + -webkit-animation-name: fa-fade; + animation-name: fa-fade; + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); +} + +.fa-beat-fade, +.fa-fade { + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); +} + +.fa-beat-fade { + -webkit-animation-name: fa-beat-fade; + animation-name: fa-beat-fade; + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); +} + +.fa-flip { + -webkit-animation-name: fa-flip; + animation-name: fa-flip; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); +} + +.fa-shake { + -webkit-animation-name: fa-shake; + animation-name: fa-shake; + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); +} + +.fa-shake, +.fa-spin { + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); +} + +.fa-spin { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-duration: var(--fa-animation-duration, 2s); + animation-duration: var(--fa-animation-duration, 2s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); +} + +.fa-spin-reverse { + --fa-animation-direction: reverse; +} + +.fa-pulse, +.fa-spin-pulse { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); + animation-timing-function: var(--fa-animation-timing, steps(8)); +} + +@media (prefers-reduced-motion: reduce) { + .fa-beat, + .fa-beat-fade, + .fa-bounce, + .fa-fade, + .fa-flip, + .fa-pulse, + .fa-shake, + .fa-spin, + .fa-spin-pulse { + -webkit-animation-delay: -1ms; + animation-delay: -1ms; + -webkit-animation-duration: 1ms; + animation-duration: 1ms; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; + transition-delay: 0s; + transition-duration: 0s; + } +} + +@-webkit-keyframes fa-beat { + 0%, + 90% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); + } +} + +@keyframes fa-beat { + 0%, + 90% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); + } +} + +@-webkit-keyframes fa-bounce { + 0% { + -webkit-transform: scale(1) translateY(0); + transform: scale(1) translateY(0); + } + + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + } + + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + } + + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + } + + 57% { + -webkit-transform: scale(1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1) translateY(var(--fa-bounce-rebound, -0.125em)); + } + + 64% { + -webkit-transform: scale(1) translateY(0); + transform: scale(1) translateY(0); + } + + to { + -webkit-transform: scale(1) translateY(0); + transform: scale(1) translateY(0); + } +} + +@keyframes fa-bounce { + 0% { + -webkit-transform: scale(1) translateY(0); + transform: scale(1) translateY(0); + } + + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + } + + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + } + + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + } + + 57% { + -webkit-transform: scale(1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1) translateY(var(--fa-bounce-rebound, -0.125em)); + } + + 64% { + -webkit-transform: scale(1) translateY(0); + transform: scale(1) translateY(0); + } + + to { + -webkit-transform: scale(1) translateY(0); + transform: scale(1) translateY(0); + } +} + +@-webkit-keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); + } +} + +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); + } +} + +@-webkit-keyframes fa-beat-fade { + 0%, + to { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); + } +} + +@keyframes fa-beat-fade { + 0%, + to { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); + } +} + +@-webkit-keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + } +} + +@keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + } +} + +@-webkit-keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); + } + + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); + } + + 8%, + 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); + } + + 12%, + 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); + } + + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); + } + + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); + } + + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); + } + + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); + } + + 40%, + to { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} + +@keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); + } + + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); + } + + 8%, + 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); + } + + 12%, + 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); + } + + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); + } + + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); + } + + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); + } + + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); + } + + 40%, + to { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } +} + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -webkit-transform: rotate(1turn); + transform: rotate(1turn); + } +} + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -webkit-transform: rotate(1turn); + transform: rotate(1turn); + } +} + +.fa-rotate-90 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.fa-rotate-180 { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); +} + +.fa-rotate-270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); +} + +.fa-flip-horizontal { + -webkit-transform: scaleX(-1); + transform: scaleX(-1); +} + +.fa-flip-vertical { + -webkit-transform: scaleY(-1); + transform: scaleY(-1); +} + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + -webkit-transform: scale(-1); + transform: scale(-1); +} + +.fa-rotate-by { + -webkit-transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, none)); +} + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2.5em; +} + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; + z-index: var(--fa-stack-z-index, auto); +} + +.fa-stack-1x { + line-height: inherit; +} + +.fa-stack-2x { + font-size: 2em; +} + +.fa-inverse { + color: var(--fa-inverse, #fff); +} + +.fa-0:before { + content: "\30"; +} + +.fa-1:before { + content: "\31"; +} + +.fa-2:before { + content: "\32"; +} + +.fa-3:before { + content: "\33"; +} + +.fa-4:before { + content: "\34"; +} + +.fa-5:before { + content: "\35"; +} + +.fa-6:before { + content: "\36"; +} + +.fa-7:before { + content: "\37"; +} + +.fa-8:before { + content: "\38"; +} + +.fa-9:before { + content: "\39"; +} + +.fa-fill-drip:before { + content: "\f576"; +} + +.fa-arrows-to-circle:before { + content: "\e4bd"; +} + +.fa-chevron-circle-right:before, +.fa-circle-chevron-right:before { + content: "\f138"; +} + +.fa-at:before { + content: "\40"; +} + +.fa-trash-alt:before, +.fa-trash-can:before { + content: "\f2ed"; +} + +.fa-text-height:before { + content: "\f034"; +} + +.fa-user-times:before, +.fa-user-xmark:before { + content: "\f235"; +} + +.fa-stethoscope:before { + content: "\f0f1"; +} + +.fa-comment-alt:before, +.fa-message:before { + content: "\f27a"; +} + +.fa-info:before { + content: "\f129"; +} + +.fa-compress-alt:before, +.fa-down-left-and-up-right-to-center:before { + content: "\f422"; +} + +.fa-explosion:before { + content: "\e4e9"; +} + +.fa-file-alt:before, +.fa-file-lines:before, +.fa-file-text:before { + content: "\f15c"; +} + +.fa-wave-square:before { + content: "\f83e"; +} + +.fa-ring:before { + content: "\f70b"; +} + +.fa-building-un:before { + content: "\e4d9"; +} + +.fa-dice-three:before { + content: "\f527"; +} + +.fa-calendar-alt:before, +.fa-calendar-days:before { + content: "\f073"; +} + +.fa-anchor-circle-check:before { + content: "\e4aa"; +} + +.fa-building-circle-arrow-right:before { + content: "\e4d1"; +} + +.fa-volleyball-ball:before, +.fa-volleyball:before { + content: "\f45f"; +} + +.fa-arrows-up-to-line:before { + content: "\e4c2"; +} + +.fa-sort-desc:before, +.fa-sort-down:before { + content: "\f0dd"; +} + +.fa-circle-minus:before, +.fa-minus-circle:before { + content: "\f056"; +} + +.fa-door-open:before { + content: "\f52b"; +} + +.fa-right-from-bracket:before, +.fa-sign-out-alt:before { + content: "\f2f5"; +} + +.fa-atom:before { + content: "\f5d2"; +} + +.fa-soap:before { + content: "\e06e"; +} + +.fa-heart-music-camera-bolt:before, +.fa-icons:before { + content: "\f86d"; +} + +.fa-microphone-alt-slash:before, +.fa-microphone-lines-slash:before { + content: "\f539"; +} + +.fa-bridge-circle-check:before { + content: "\e4c9"; +} + +.fa-pump-medical:before { + content: "\e06a"; +} + +.fa-fingerprint:before { + content: "\f577"; +} + +.fa-hand-point-right:before { + content: "\f0a4"; +} + +.fa-magnifying-glass-location:before, +.fa-search-location:before { + content: "\f689"; +} + +.fa-forward-step:before, +.fa-step-forward:before { + content: "\f051"; +} + +.fa-face-smile-beam:before, +.fa-smile-beam:before { + content: "\f5b8"; +} + +.fa-flag-checkered:before { + content: "\f11e"; +} + +.fa-football-ball:before, +.fa-football:before { + content: "\f44e"; +} + +.fa-school-circle-exclamation:before { + content: "\e56c"; +} + +.fa-crop:before { + content: "\f125"; +} + +.fa-angle-double-down:before, +.fa-angles-down:before { + content: "\f103"; +} + +.fa-users-rectangle:before { + content: "\e594"; +} + +.fa-people-roof:before { + content: "\e537"; +} + +.fa-people-line:before { + content: "\e534"; +} + +.fa-beer-mug-empty:before, +.fa-beer:before { + content: "\f0fc"; +} + +.fa-diagram-predecessor:before { + content: "\e477"; +} + +.fa-arrow-up-long:before, +.fa-long-arrow-up:before { + content: "\f176"; +} + +.fa-burn:before, +.fa-fire-flame-simple:before { + content: "\f46a"; +} + +.fa-male:before, +.fa-person:before { + content: "\f183"; +} + +.fa-laptop:before { + content: "\f109"; +} + +.fa-file-csv:before { + content: "\f6dd"; +} + +.fa-menorah:before { + content: "\f676"; +} + +.fa-truck-plane:before { + content: "\e58f"; +} + +.fa-record-vinyl:before { + content: "\f8d9"; +} + +.fa-face-grin-stars:before, +.fa-grin-stars:before { + content: "\f587"; +} + +.fa-bong:before { + content: "\f55c"; +} + +.fa-pastafarianism:before, +.fa-spaghetti-monster-flying:before { + content: "\f67b"; +} + +.fa-arrow-down-up-across-line:before { + content: "\e4af"; +} + +.fa-spoon:before, +.fa-utensil-spoon:before { + content: "\f2e5"; +} + +.fa-jar-wheat:before { + content: "\e517"; +} + +.fa-envelopes-bulk:before, +.fa-mail-bulk:before { + content: "\f674"; +} + +.fa-file-circle-exclamation:before { + content: "\e4eb"; +} + +.fa-circle-h:before, +.fa-hospital-symbol:before { + content: "\f47e"; +} + +.fa-pager:before { + content: "\f815"; +} + +.fa-address-book:before, +.fa-contact-book:before { + content: "\f2b9"; +} + +.fa-strikethrough:before { + content: "\f0cc"; +} + +.fa-k:before { + content: "\4b"; +} + +.fa-landmark-flag:before { + content: "\e51c"; +} + +.fa-pencil-alt:before, +.fa-pencil:before { + content: "\f303"; +} + +.fa-backward:before { + content: "\f04a"; +} + +.fa-caret-right:before { + content: "\f0da"; +} + +.fa-comments:before { + content: "\f086"; +} + +.fa-file-clipboard:before, +.fa-paste:before { + content: "\f0ea"; +} + +.fa-code-pull-request:before { + content: "\e13c"; +} + +.fa-clipboard-list:before { + content: "\f46d"; +} + +.fa-truck-loading:before, +.fa-truck-ramp-box:before { + content: "\f4de"; +} + +.fa-user-check:before { + content: "\f4fc"; +} + +.fa-vial-virus:before { + content: "\e597"; +} + +.fa-sheet-plastic:before { + content: "\e571"; +} + +.fa-blog:before { + content: "\f781"; +} + +.fa-user-ninja:before { + content: "\f504"; +} + +.fa-person-arrow-up-from-line:before { + content: "\e539"; +} + +.fa-scroll-torah:before, +.fa-torah:before { + content: "\f6a0"; +} + +.fa-broom-ball:before, +.fa-quidditch-broom-ball:before, +.fa-quidditch:before { + content: "\f458"; +} + +.fa-toggle-off:before { + content: "\f204"; +} + +.fa-archive:before, +.fa-box-archive:before { + content: "\f187"; +} + +.fa-person-drowning:before { + content: "\e545"; +} + +.fa-arrow-down-9-1:before, +.fa-sort-numeric-desc:before, +.fa-sort-numeric-down-alt:before { + content: "\f886"; +} + +.fa-face-grin-tongue-squint:before, +.fa-grin-tongue-squint:before { + content: "\f58a"; +} + +.fa-spray-can:before { + content: "\f5bd"; +} + +.fa-truck-monster:before { + content: "\f63b"; +} + +.fa-w:before { + content: "\57"; +} + +.fa-earth-africa:before, +.fa-globe-africa:before { + content: "\f57c"; +} + +.fa-rainbow:before { + content: "\f75b"; +} + +.fa-circle-notch:before { + content: "\f1ce"; +} + +.fa-tablet-alt:before, +.fa-tablet-screen-button:before { + content: "\f3fa"; +} + +.fa-paw:before { + content: "\f1b0"; +} + +.fa-cloud:before { + content: "\f0c2"; +} + +.fa-trowel-bricks:before { + content: "\e58a"; +} + +.fa-face-flushed:before, +.fa-flushed:before { + content: "\f579"; +} + +.fa-hospital-user:before { + content: "\f80d"; +} + +.fa-tent-arrow-left-right:before { + content: "\e57f"; +} + +.fa-gavel:before, +.fa-legal:before { + content: "\f0e3"; +} + +.fa-binoculars:before { + content: "\f1e5"; +} + +.fa-microphone-slash:before { + content: "\f131"; +} + +.fa-box-tissue:before { + content: "\e05b"; +} + +.fa-motorcycle:before { + content: "\f21c"; +} + +.fa-bell-concierge:before, +.fa-concierge-bell:before { + content: "\f562"; +} + +.fa-pen-ruler:before, +.fa-pencil-ruler:before { + content: "\f5ae"; +} + +.fa-people-arrows-left-right:before, +.fa-people-arrows:before { + content: "\e068"; +} + +.fa-mars-and-venus-burst:before { + content: "\e523"; +} + +.fa-caret-square-right:before, +.fa-square-caret-right:before { + content: "\f152"; +} + +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} + +.fa-sun-plant-wilt:before { + content: "\e57a"; +} + +.fa-toilets-portable:before { + content: "\e584"; +} + +.fa-hockey-puck:before { + content: "\f453"; +} + +.fa-table:before { + content: "\f0ce"; +} + +.fa-magnifying-glass-arrow-right:before { + content: "\e521"; +} + +.fa-digital-tachograph:before, +.fa-tachograph-digital:before { + content: "\f566"; +} + +.fa-users-slash:before { + content: "\e073"; +} + +.fa-clover:before { + content: "\e139"; +} + +.fa-mail-reply:before, +.fa-reply:before { + content: "\f3e5"; +} + +.fa-star-and-crescent:before { + content: "\f699"; +} + +.fa-house-fire:before { + content: "\e50c"; +} + +.fa-minus-square:before, +.fa-square-minus:before { + content: "\f146"; +} + +.fa-helicopter:before { + content: "\f533"; +} + +.fa-compass:before { + content: "\f14e"; +} + +.fa-caret-square-down:before, +.fa-square-caret-down:before { + content: "\f150"; +} + +.fa-file-circle-question:before { + content: "\e4ef"; +} + +.fa-laptop-code:before { + content: "\f5fc"; +} + +.fa-swatchbook:before { + content: "\f5c3"; +} + +.fa-prescription-bottle:before { + content: "\f485"; +} + +.fa-bars:before, +.fa-navicon:before { + content: "\f0c9"; +} + +.fa-people-group:before { + content: "\e533"; +} + +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} + +.fa-heart-broken:before, +.fa-heart-crack:before { + content: "\f7a9"; +} + +.fa-external-link-square-alt:before, +.fa-square-up-right:before { + content: "\f360"; +} + +.fa-face-kiss-beam:before, +.fa-kiss-beam:before { + content: "\f597"; +} + +.fa-film:before { + content: "\f008"; +} + +.fa-ruler-horizontal:before { + content: "\f547"; +} + +.fa-people-robbery:before { + content: "\e536"; +} + +.fa-lightbulb:before { + content: "\f0eb"; +} + +.fa-caret-left:before { + content: "\f0d9"; +} + +.fa-circle-exclamation:before, +.fa-exclamation-circle:before { + content: "\f06a"; +} + +.fa-school-circle-xmark:before { + content: "\e56d"; +} + +.fa-arrow-right-from-bracket:before, +.fa-sign-out:before { + content: "\f08b"; +} + +.fa-chevron-circle-down:before, +.fa-circle-chevron-down:before { + content: "\f13a"; +} + +.fa-unlock-alt:before, +.fa-unlock-keyhole:before { + content: "\f13e"; +} + +.fa-cloud-showers-heavy:before { + content: "\f740"; +} + +.fa-headphones-alt:before, +.fa-headphones-simple:before { + content: "\f58f"; +} + +.fa-sitemap:before { + content: "\f0e8"; +} + +.fa-circle-dollar-to-slot:before, +.fa-donate:before { + content: "\f4b9"; +} + +.fa-memory:before { + content: "\f538"; +} + +.fa-road-spikes:before { + content: "\e568"; +} + +.fa-fire-burner:before { + content: "\e4f1"; +} + +.fa-flag:before { + content: "\f024"; +} + +.fa-hanukiah:before { + content: "\f6e6"; +} + +.fa-feather:before { + content: "\f52d"; +} + +.fa-volume-down:before, +.fa-volume-low:before { + content: "\f027"; +} + +.fa-comment-slash:before { + content: "\f4b3"; +} + +.fa-cloud-sun-rain:before { + content: "\f743"; +} + +.fa-compress:before { + content: "\f066"; +} + +.fa-wheat-alt:before, +.fa-wheat-awn:before { + content: "\e2cd"; +} + +.fa-ankh:before { + content: "\f644"; +} + +.fa-hands-holding-child:before { + content: "\e4fa"; +} + +.fa-asterisk:before { + content: "\2a"; +} + +.fa-check-square:before, +.fa-square-check:before { + content: "\f14a"; +} + +.fa-peseta-sign:before { + content: "\e221"; +} + +.fa-header:before, +.fa-heading:before { + content: "\f1dc"; +} + +.fa-ghost:before { + content: "\f6e2"; +} + +.fa-list-squares:before, +.fa-list:before { + content: "\f03a"; +} + +.fa-phone-square-alt:before, +.fa-square-phone-flip:before { + content: "\f87b"; +} + +.fa-cart-plus:before { + content: "\f217"; +} + +.fa-gamepad:before { + content: "\f11b"; +} + +.fa-circle-dot:before, +.fa-dot-circle:before { + content: "\f192"; +} + +.fa-dizzy:before, +.fa-face-dizzy:before { + content: "\f567"; +} + +.fa-egg:before { + content: "\f7fb"; +} + +.fa-house-medical-circle-xmark:before { + content: "\e513"; +} + +.fa-campground:before { + content: "\f6bb"; +} + +.fa-folder-plus:before { + content: "\f65e"; +} + +.fa-futbol-ball:before, +.fa-futbol:before, +.fa-soccer-ball:before { + content: "\f1e3"; +} + +.fa-paint-brush:before, +.fa-paintbrush:before { + content: "\f1fc"; +} + +.fa-lock:before { + content: "\f023"; +} + +.fa-gas-pump:before { + content: "\f52f"; +} + +.fa-hot-tub-person:before, +.fa-hot-tub:before { + content: "\f593"; +} + +.fa-map-location:before, +.fa-map-marked:before { + content: "\f59f"; +} + +.fa-house-flood-water:before { + content: "\e50e"; +} + +.fa-tree:before { + content: "\f1bb"; +} + +.fa-bridge-lock:before { + content: "\e4cc"; +} + +.fa-sack-dollar:before { + content: "\f81d"; +} + +.fa-edit:before, +.fa-pen-to-square:before { + content: "\f044"; +} + +.fa-car-side:before { + content: "\f5e4"; +} + +.fa-share-alt:before, +.fa-share-nodes:before { + content: "\f1e0"; +} + +.fa-heart-circle-minus:before { + content: "\e4ff"; +} + +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} + +.fa-microscope:before { + content: "\f610"; +} + +.fa-sink:before { + content: "\e06d"; +} + +.fa-bag-shopping:before, +.fa-shopping-bag:before { + content: "\f290"; +} + +.fa-arrow-down-z-a:before, +.fa-sort-alpha-desc:before, +.fa-sort-alpha-down-alt:before { + content: "\f881"; +} + +.fa-mitten:before { + content: "\f7b5"; +} + +.fa-person-rays:before { + content: "\e54d"; +} + +.fa-users:before { + content: "\f0c0"; +} + +.fa-eye-slash:before { + content: "\f070"; +} + +.fa-flask-vial:before { + content: "\e4f3"; +} + +.fa-hand-paper:before, +.fa-hand:before { + content: "\f256"; +} + +.fa-om:before { + content: "\f679"; +} + +.fa-worm:before { + content: "\e599"; +} + +.fa-house-circle-xmark:before { + content: "\e50b"; +} + +.fa-plug:before { + content: "\f1e6"; +} + +.fa-chevron-up:before { + content: "\f077"; +} + +.fa-hand-spock:before { + content: "\f259"; +} + +.fa-stopwatch:before { + content: "\f2f2"; +} + +.fa-face-kiss:before, +.fa-kiss:before { + content: "\f596"; +} + +.fa-bridge-circle-xmark:before { + content: "\e4cb"; +} + +.fa-face-grin-tongue:before, +.fa-grin-tongue:before { + content: "\f589"; +} + +.fa-chess-bishop:before { + content: "\f43a"; +} + +.fa-face-grin-wink:before, +.fa-grin-wink:before { + content: "\f58c"; +} + +.fa-deaf:before, +.fa-deafness:before, +.fa-ear-deaf:before, +.fa-hard-of-hearing:before { + content: "\f2a4"; +} + +.fa-road-circle-check:before { + content: "\e564"; +} + +.fa-dice-five:before { + content: "\f523"; +} + +.fa-rss-square:before, +.fa-square-rss:before { + content: "\f143"; +} + +.fa-land-mine-on:before { + content: "\e51b"; +} + +.fa-i-cursor:before { + content: "\f246"; +} + +.fa-stamp:before { + content: "\f5bf"; +} + +.fa-stairs:before { + content: "\e289"; +} + +.fa-i:before { + content: "\49"; +} + +.fa-hryvnia-sign:before, +.fa-hryvnia:before { + content: "\f6f2"; +} + +.fa-pills:before { + content: "\f484"; +} + +.fa-face-grin-wide:before, +.fa-grin-alt:before { + content: "\f581"; +} + +.fa-tooth:before { + content: "\f5c9"; +} + +.fa-v:before { + content: "\56"; +} + +.fa-bangladeshi-taka-sign:before { + content: "\e2e6"; +} + +.fa-bicycle:before { + content: "\f206"; +} + +.fa-rod-asclepius:before, +.fa-rod-snake:before, +.fa-staff-aesculapius:before, +.fa-staff-snake:before { + content: "\e579"; +} + +.fa-head-side-cough-slash:before { + content: "\e062"; +} + +.fa-ambulance:before, +.fa-truck-medical:before { + content: "\f0f9"; +} + +.fa-wheat-awn-circle-exclamation:before { + content: "\e598"; +} + +.fa-snowman:before { + content: "\f7d0"; +} + +.fa-mortar-pestle:before { + content: "\f5a7"; +} + +.fa-road-barrier:before { + content: "\e562"; +} + +.fa-school:before { + content: "\f549"; +} + +.fa-igloo:before { + content: "\f7ae"; +} + +.fa-joint:before { + content: "\f595"; +} + +.fa-angle-right:before { + content: "\f105"; +} + +.fa-horse:before { + content: "\f6f0"; +} + +.fa-q:before { + content: "\51"; +} + +.fa-g:before { + content: "\47"; +} + +.fa-notes-medical:before { + content: "\f481"; +} + +.fa-temperature-2:before, +.fa-temperature-half:before, +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} + +.fa-dong-sign:before { + content: "\e169"; +} + +.fa-capsules:before { + content: "\f46b"; +} + +.fa-poo-bolt:before, +.fa-poo-storm:before { + content: "\f75a"; +} + +.fa-face-frown-open:before, +.fa-frown-open:before { + content: "\f57a"; +} + +.fa-hand-point-up:before { + content: "\f0a6"; +} + +.fa-money-bill:before { + content: "\f0d6"; +} + +.fa-bookmark:before { + content: "\f02e"; +} + +.fa-align-justify:before { + content: "\f039"; +} + +.fa-umbrella-beach:before { + content: "\f5ca"; +} + +.fa-helmet-un:before { + content: "\e503"; +} + +.fa-bullseye:before { + content: "\f140"; +} + +.fa-bacon:before { + content: "\f7e5"; +} + +.fa-hand-point-down:before { + content: "\f0a7"; +} + +.fa-arrow-up-from-bracket:before { + content: "\e09a"; +} + +.fa-folder-blank:before, +.fa-folder:before { + content: "\f07b"; +} + +.fa-file-medical-alt:before, +.fa-file-waveform:before { + content: "\f478"; +} + +.fa-radiation:before { + content: "\f7b9"; +} + +.fa-chart-simple:before { + content: "\e473"; +} + +.fa-mars-stroke:before { + content: "\f229"; +} + +.fa-vial:before { + content: "\f492"; +} + +.fa-dashboard:before, +.fa-gauge-med:before, +.fa-gauge:before, +.fa-tachometer-alt-average:before { + content: "\f624"; +} + +.fa-magic-wand-sparkles:before, +.fa-wand-magic-sparkles:before { + content: "\e2ca"; +} + +.fa-e:before { + content: "\45"; +} + +.fa-pen-alt:before, +.fa-pen-clip:before { + content: "\f305"; +} + +.fa-bridge-circle-exclamation:before { + content: "\e4ca"; +} + +.fa-user:before { + content: "\f007"; +} + +.fa-school-circle-check:before { + content: "\e56b"; +} + +.fa-dumpster:before { + content: "\f793"; +} + +.fa-shuttle-van:before, +.fa-van-shuttle:before { + content: "\f5b6"; +} + +.fa-building-user:before { + content: "\e4da"; +} + +.fa-caret-square-left:before, +.fa-square-caret-left:before { + content: "\f191"; +} + +.fa-highlighter:before { + content: "\f591"; +} + +.fa-key:before { + content: "\f084"; +} + +.fa-bullhorn:before { + content: "\f0a1"; +} + +.fa-globe:before { + content: "\f0ac"; +} + +.fa-synagogue:before { + content: "\f69b"; +} + +.fa-person-half-dress:before { + content: "\e548"; +} + +.fa-road-bridge:before { + content: "\e563"; +} + +.fa-location-arrow:before { + content: "\f124"; +} + +.fa-c:before { + content: "\43"; +} + +.fa-tablet-button:before { + content: "\f10a"; +} + +.fa-building-lock:before { + content: "\e4d6"; +} + +.fa-pizza-slice:before { + content: "\f818"; +} + +.fa-money-bill-wave:before { + content: "\f53a"; +} + +.fa-area-chart:before, +.fa-chart-area:before { + content: "\f1fe"; +} + +.fa-house-flag:before { + content: "\e50d"; +} + +.fa-person-circle-minus:before { + content: "\e540"; +} + +.fa-ban:before, +.fa-cancel:before { + content: "\f05e"; +} + +.fa-camera-rotate:before { + content: "\e0d8"; +} + +.fa-air-freshener:before, +.fa-spray-can-sparkles:before { + content: "\f5d0"; +} + +.fa-star:before { + content: "\f005"; +} + +.fa-repeat:before { + content: "\f363"; +} + +.fa-cross:before { + content: "\f654"; +} + +.fa-box:before { + content: "\f466"; +} + +.fa-venus-mars:before { + content: "\f228"; +} + +.fa-arrow-pointer:before, +.fa-mouse-pointer:before { + content: "\f245"; +} + +.fa-expand-arrows-alt:before, +.fa-maximize:before { + content: "\f31e"; +} + +.fa-charging-station:before { + content: "\f5e7"; +} + +.fa-shapes:before, +.fa-triangle-circle-square:before { + content: "\f61f"; +} + +.fa-random:before, +.fa-shuffle:before { + content: "\f074"; +} + +.fa-person-running:before, +.fa-running:before { + content: "\f70c"; +} + +.fa-mobile-retro:before { + content: "\e527"; +} + +.fa-grip-lines-vertical:before { + content: "\f7a5"; +} + +.fa-spider:before { + content: "\f717"; +} + +.fa-hands-bound:before { + content: "\e4f9"; +} + +.fa-file-invoice-dollar:before { + content: "\f571"; +} + +.fa-plane-circle-exclamation:before { + content: "\e556"; +} + +.fa-x-ray:before { + content: "\f497"; +} + +.fa-spell-check:before { + content: "\f891"; +} + +.fa-slash:before { + content: "\f715"; +} + +.fa-computer-mouse:before, +.fa-mouse:before { + content: "\f8cc"; +} + +.fa-arrow-right-to-bracket:before, +.fa-sign-in:before { + content: "\f090"; +} + +.fa-shop-slash:before, +.fa-store-alt-slash:before { + content: "\e070"; +} + +.fa-server:before { + content: "\f233"; +} + +.fa-virus-covid-slash:before { + content: "\e4a9"; +} + +.fa-shop-lock:before { + content: "\e4a5"; +} + +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} + +.fa-blender-phone:before { + content: "\f6b6"; +} + +.fa-building-wheat:before { + content: "\e4db"; +} + +.fa-person-breastfeeding:before { + content: "\e53a"; +} + +.fa-right-to-bracket:before, +.fa-sign-in-alt:before { + content: "\f2f6"; +} + +.fa-venus:before { + content: "\f221"; +} + +.fa-passport:before { + content: "\f5ab"; +} + +.fa-heart-pulse:before, +.fa-heartbeat:before { + content: "\f21e"; +} + +.fa-people-carry-box:before, +.fa-people-carry:before { + content: "\f4ce"; +} + +.fa-temperature-high:before { + content: "\f769"; +} + +.fa-microchip:before { + content: "\f2db"; +} + +.fa-crown:before { + content: "\f521"; +} + +.fa-weight-hanging:before { + content: "\f5cd"; +} + +.fa-xmarks-lines:before { + content: "\e59a"; +} + +.fa-file-prescription:before { + content: "\f572"; +} + +.fa-weight-scale:before, +.fa-weight:before { + content: "\f496"; +} + +.fa-user-friends:before, +.fa-user-group:before { + content: "\f500"; +} + +.fa-arrow-up-a-z:before, +.fa-sort-alpha-up:before { + content: "\f15e"; +} + +.fa-chess-knight:before { + content: "\f441"; +} + +.fa-face-laugh-squint:before, +.fa-laugh-squint:before { + content: "\f59b"; +} + +.fa-wheelchair:before { + content: "\f193"; +} + +.fa-arrow-circle-up:before, +.fa-circle-arrow-up:before { + content: "\f0aa"; +} + +.fa-toggle-on:before { + content: "\f205"; +} + +.fa-person-walking:before, +.fa-walking:before { + content: "\f554"; +} + +.fa-l:before { + content: "\4c"; +} + +.fa-fire:before { + content: "\f06d"; +} + +.fa-bed-pulse:before, +.fa-procedures:before { + content: "\f487"; +} + +.fa-shuttle-space:before, +.fa-space-shuttle:before { + content: "\f197"; +} + +.fa-face-laugh:before, +.fa-laugh:before { + content: "\f599"; +} + +.fa-folder-open:before { + content: "\f07c"; +} + +.fa-heart-circle-plus:before { + content: "\e500"; +} + +.fa-code-fork:before { + content: "\e13b"; +} + +.fa-city:before { + content: "\f64f"; +} + +.fa-microphone-alt:before, +.fa-microphone-lines:before { + content: "\f3c9"; +} + +.fa-pepper-hot:before { + content: "\f816"; +} + +.fa-unlock:before { + content: "\f09c"; +} + +.fa-colon-sign:before { + content: "\e140"; +} + +.fa-headset:before { + content: "\f590"; +} + +.fa-store-slash:before { + content: "\e071"; +} + +.fa-road-circle-xmark:before { + content: "\e566"; +} + +.fa-user-minus:before { + content: "\f503"; +} + +.fa-mars-stroke-up:before, +.fa-mars-stroke-v:before { + content: "\f22a"; +} + +.fa-champagne-glasses:before, +.fa-glass-cheers:before { + content: "\f79f"; +} + +.fa-clipboard:before { + content: "\f328"; +} + +.fa-house-circle-exclamation:before { + content: "\e50a"; +} + +.fa-file-arrow-up:before, +.fa-file-upload:before { + content: "\f574"; +} + +.fa-wifi-3:before, +.fa-wifi-strong:before, +.fa-wifi:before { + content: "\f1eb"; +} + +.fa-bath:before, +.fa-bathtub:before { + content: "\f2cd"; +} + +.fa-underline:before { + content: "\f0cd"; +} + +.fa-user-edit:before, +.fa-user-pen:before { + content: "\f4ff"; +} + +.fa-signature:before { + content: "\f5b7"; +} + +.fa-stroopwafel:before { + content: "\f551"; +} + +.fa-bold:before { + content: "\f032"; +} + +.fa-anchor-lock:before { + content: "\e4ad"; +} + +.fa-building-ngo:before { + content: "\e4d7"; +} + +.fa-manat-sign:before { + content: "\e1d5"; +} + +.fa-not-equal:before { + content: "\f53e"; +} + +.fa-border-style:before, +.fa-border-top-left:before { + content: "\f853"; +} + +.fa-map-location-dot:before, +.fa-map-marked-alt:before { + content: "\f5a0"; +} + +.fa-jedi:before { + content: "\f669"; +} + +.fa-poll:before, +.fa-square-poll-vertical:before { + content: "\f681"; +} + +.fa-mug-hot:before { + content: "\f7b6"; +} + +.fa-battery-car:before, +.fa-car-battery:before { + content: "\f5df"; +} + +.fa-gift:before { + content: "\f06b"; +} + +.fa-dice-two:before { + content: "\f528"; +} + +.fa-chess-queen:before { + content: "\f445"; +} + +.fa-glasses:before { + content: "\f530"; +} + +.fa-chess-board:before { + content: "\f43c"; +} + +.fa-building-circle-check:before { + content: "\e4d2"; +} + +.fa-person-chalkboard:before { + content: "\e53d"; +} + +.fa-mars-stroke-h:before, +.fa-mars-stroke-right:before { + content: "\f22b"; +} + +.fa-hand-back-fist:before, +.fa-hand-rock:before { + content: "\f255"; +} + +.fa-caret-square-up:before, +.fa-square-caret-up:before { + content: "\f151"; +} + +.fa-cloud-showers-water:before { + content: "\e4e4"; +} + +.fa-bar-chart:before, +.fa-chart-bar:before { + content: "\f080"; +} + +.fa-hands-bubbles:before, +.fa-hands-wash:before { + content: "\e05e"; +} + +.fa-less-than-equal:before { + content: "\f537"; +} + +.fa-train:before { + content: "\f238"; +} + +.fa-eye-low-vision:before, +.fa-low-vision:before { + content: "\f2a8"; +} + +.fa-crow:before { + content: "\f520"; +} + +.fa-sailboat:before { + content: "\e445"; +} + +.fa-window-restore:before { + content: "\f2d2"; +} + +.fa-plus-square:before, +.fa-square-plus:before { + content: "\f0fe"; +} + +.fa-torii-gate:before { + content: "\f6a1"; +} + +.fa-frog:before { + content: "\f52e"; +} + +.fa-bucket:before { + content: "\e4cf"; +} + +.fa-image:before { + content: "\f03e"; +} + +.fa-microphone:before { + content: "\f130"; +} + +.fa-cow:before { + content: "\f6c8"; +} + +.fa-caret-up:before { + content: "\f0d8"; +} + +.fa-screwdriver:before { + content: "\f54a"; +} + +.fa-folder-closed:before { + content: "\e185"; +} + +.fa-house-tsunami:before { + content: "\e515"; +} + +.fa-square-nfi:before { + content: "\e576"; +} + +.fa-arrow-up-from-ground-water:before { + content: "\e4b5"; +} + +.fa-glass-martini-alt:before, +.fa-martini-glass:before { + content: "\f57b"; +} + +.fa-rotate-back:before, +.fa-rotate-backward:before, +.fa-rotate-left:before, +.fa-undo-alt:before { + content: "\f2ea"; +} + +.fa-columns:before, +.fa-table-columns:before { + content: "\f0db"; +} + +.fa-lemon:before { + content: "\f094"; +} + +.fa-head-side-mask:before { + content: "\e063"; +} + +.fa-handshake:before { + content: "\f2b5"; +} + +.fa-gem:before { + content: "\f3a5"; +} + +.fa-dolly-box:before, +.fa-dolly:before { + content: "\f472"; +} + +.fa-smoking:before { + content: "\f48d"; +} + +.fa-compress-arrows-alt:before, +.fa-minimize:before { + content: "\f78c"; +} + +.fa-monument:before { + content: "\f5a6"; +} + +.fa-snowplow:before { + content: "\f7d2"; +} + +.fa-angle-double-right:before, +.fa-angles-right:before { + content: "\f101"; +} + +.fa-cannabis:before { + content: "\f55f"; +} + +.fa-circle-play:before, +.fa-play-circle:before { + content: "\f144"; +} + +.fa-tablets:before { + content: "\f490"; +} + +.fa-ethernet:before { + content: "\f796"; +} + +.fa-eur:before, +.fa-euro-sign:before, +.fa-euro:before { + content: "\f153"; +} + +.fa-chair:before { + content: "\f6c0"; +} + +.fa-check-circle:before, +.fa-circle-check:before { + content: "\f058"; +} + +.fa-circle-stop:before, +.fa-stop-circle:before { + content: "\f28d"; +} + +.fa-compass-drafting:before, +.fa-drafting-compass:before { + content: "\f568"; +} + +.fa-plate-wheat:before { + content: "\e55a"; +} + +.fa-icicles:before { + content: "\f7ad"; +} + +.fa-person-shelter:before { + content: "\e54f"; +} + +.fa-neuter:before { + content: "\f22c"; +} + +.fa-id-badge:before { + content: "\f2c1"; +} + +.fa-marker:before { + content: "\f5a1"; +} + +.fa-face-laugh-beam:before, +.fa-laugh-beam:before { + content: "\f59a"; +} + +.fa-helicopter-symbol:before { + content: "\e502"; +} + +.fa-universal-access:before { + content: "\f29a"; +} + +.fa-chevron-circle-up:before, +.fa-circle-chevron-up:before { + content: "\f139"; +} + +.fa-lari-sign:before { + content: "\e1c8"; +} + +.fa-volcano:before { + content: "\f770"; +} + +.fa-person-walking-dashed-line-arrow-right:before { + content: "\e553"; +} + +.fa-gbp:before, +.fa-pound-sign:before, +.fa-sterling-sign:before { + content: "\f154"; +} + +.fa-viruses:before { + content: "\e076"; +} + +.fa-square-person-confined:before { + content: "\e577"; +} + +.fa-user-tie:before { + content: "\f508"; +} + +.fa-arrow-down-long:before, +.fa-long-arrow-down:before { + content: "\f175"; +} + +.fa-tent-arrow-down-to-line:before { + content: "\e57e"; +} + +.fa-certificate:before { + content: "\f0a3"; +} + +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} + +.fa-suitcase:before { + content: "\f0f2"; +} + +.fa-person-skating:before, +.fa-skating:before { + content: "\f7c5"; +} + +.fa-filter-circle-dollar:before, +.fa-funnel-dollar:before { + content: "\f662"; +} + +.fa-camera-retro:before { + content: "\f083"; +} + +.fa-arrow-circle-down:before, +.fa-circle-arrow-down:before { + content: "\f0ab"; +} + +.fa-arrow-right-to-file:before, +.fa-file-import:before { + content: "\f56f"; +} + +.fa-external-link-square:before, +.fa-square-arrow-up-right:before { + content: "\f14c"; +} + +.fa-box-open:before { + content: "\f49e"; +} + +.fa-scroll:before { + content: "\f70e"; +} + +.fa-spa:before { + content: "\f5bb"; +} + +.fa-location-pin-lock:before { + content: "\e51f"; +} + +.fa-pause:before { + content: "\f04c"; +} + +.fa-hill-avalanche:before { + content: "\e507"; +} + +.fa-temperature-0:before, +.fa-temperature-empty:before, +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} + +.fa-bomb:before { + content: "\f1e2"; +} + +.fa-registered:before { + content: "\f25d"; +} + +.fa-address-card:before, +.fa-contact-card:before, +.fa-vcard:before { + content: "\f2bb"; +} + +.fa-balance-scale-right:before, +.fa-scale-unbalanced-flip:before { + content: "\f516"; +} + +.fa-subscript:before { + content: "\f12c"; +} + +.fa-diamond-turn-right:before, +.fa-directions:before { + content: "\f5eb"; +} + +.fa-burst:before { + content: "\e4dc"; +} + +.fa-house-laptop:before, +.fa-laptop-house:before { + content: "\e066"; +} + +.fa-face-tired:before, +.fa-tired:before { + content: "\f5c8"; +} + +.fa-money-bills:before { + content: "\e1f3"; +} + +.fa-smog:before { + content: "\f75f"; +} + +.fa-crutch:before { + content: "\f7f7"; +} + +.fa-cloud-arrow-up:before, +.fa-cloud-upload-alt:before, +.fa-cloud-upload:before { + content: "\f0ee"; +} + +.fa-palette:before { + content: "\f53f"; +} + +.fa-arrows-turn-right:before { + content: "\e4c0"; +} + +.fa-vest:before { + content: "\e085"; +} + +.fa-ferry:before { + content: "\e4ea"; +} + +.fa-arrows-down-to-people:before { + content: "\e4b9"; +} + +.fa-seedling:before, +.fa-sprout:before { + content: "\f4d8"; +} + +.fa-arrows-alt-h:before, +.fa-left-right:before { + content: "\f337"; +} + +.fa-boxes-packing:before { + content: "\e4c7"; +} + +.fa-arrow-circle-left:before, +.fa-circle-arrow-left:before { + content: "\f0a8"; +} + +.fa-group-arrows-rotate:before { + content: "\e4f6"; +} + +.fa-bowl-food:before { + content: "\e4c6"; +} + +.fa-candy-cane:before { + content: "\f786"; +} + +.fa-arrow-down-wide-short:before, +.fa-sort-amount-asc:before, +.fa-sort-amount-down:before { + content: "\f160"; +} + +.fa-cloud-bolt:before, +.fa-thunderstorm:before { + content: "\f76c"; +} + +.fa-remove-format:before, +.fa-text-slash:before { + content: "\f87d"; +} + +.fa-face-smile-wink:before, +.fa-smile-wink:before { + content: "\f4da"; +} + +.fa-file-word:before { + content: "\f1c2"; +} + +.fa-file-powerpoint:before { + content: "\f1c4"; +} + +.fa-arrows-h:before, +.fa-arrows-left-right:before { + content: "\f07e"; +} + +.fa-house-lock:before { + content: "\e510"; +} + +.fa-cloud-arrow-down:before, +.fa-cloud-download-alt:before, +.fa-cloud-download:before { + content: "\f0ed"; +} + +.fa-children:before { + content: "\e4e1"; +} + +.fa-blackboard:before, +.fa-chalkboard:before { + content: "\f51b"; +} + +.fa-user-alt-slash:before, +.fa-user-large-slash:before { + content: "\f4fa"; +} + +.fa-envelope-open:before { + content: "\f2b6"; +} + +.fa-handshake-alt-slash:before, +.fa-handshake-simple-slash:before { + content: "\e05f"; +} + +.fa-mattress-pillow:before { + content: "\e525"; +} + +.fa-guarani-sign:before { + content: "\e19a"; +} + +.fa-arrows-rotate:before, +.fa-refresh:before, +.fa-sync:before { + content: "\f021"; +} + +.fa-fire-extinguisher:before { + content: "\f134"; +} + +.fa-cruzeiro-sign:before { + content: "\e152"; +} + +.fa-greater-than-equal:before { + content: "\f532"; +} + +.fa-shield-alt:before, +.fa-shield-halved:before { + content: "\f3ed"; +} + +.fa-atlas:before, +.fa-book-atlas:before { + content: "\f558"; +} + +.fa-virus:before { + content: "\e074"; +} + +.fa-envelope-circle-check:before { + content: "\e4e8"; +} + +.fa-layer-group:before { + content: "\f5fd"; +} + +.fa-arrows-to-dot:before { + content: "\e4be"; +} + +.fa-archway:before { + content: "\f557"; +} + +.fa-heart-circle-check:before { + content: "\e4fd"; +} + +.fa-house-chimney-crack:before, +.fa-house-damage:before { + content: "\f6f1"; +} + +.fa-file-archive:before, +.fa-file-zipper:before { + content: "\f1c6"; +} + +.fa-square:before { + content: "\f0c8"; +} + +.fa-glass-martini:before, +.fa-martini-glass-empty:before { + content: "\f000"; +} + +.fa-couch:before { + content: "\f4b8"; +} + +.fa-cedi-sign:before { + content: "\e0df"; +} + +.fa-italic:before { + content: "\f033"; +} + +.fa-church:before { + content: "\f51d"; +} + +.fa-comments-dollar:before { + content: "\f653"; +} + +.fa-democrat:before { + content: "\f747"; +} + +.fa-z:before { + content: "\5a"; +} + +.fa-person-skiing:before, +.fa-skiing:before { + content: "\f7c9"; +} + +.fa-road-lock:before { + content: "\e567"; +} + +.fa-a:before { + content: "\41"; +} + +.fa-temperature-arrow-down:before, +.fa-temperature-down:before { + content: "\e03f"; +} + +.fa-feather-alt:before, +.fa-feather-pointed:before { + content: "\f56b"; +} + +.fa-p:before { + content: "\50"; +} + +.fa-snowflake:before { + content: "\f2dc"; +} + +.fa-newspaper:before { + content: "\f1ea"; +} + +.fa-ad:before, +.fa-rectangle-ad:before { + content: "\f641"; +} + +.fa-arrow-circle-right:before, +.fa-circle-arrow-right:before { + content: "\f0a9"; +} + +.fa-filter-circle-xmark:before { + content: "\e17b"; +} + +.fa-locust:before { + content: "\e520"; +} + +.fa-sort:before, +.fa-unsorted:before { + content: "\f0dc"; +} + +.fa-list-1-2:before, +.fa-list-numeric:before, +.fa-list-ol:before { + content: "\f0cb"; +} + +.fa-person-dress-burst:before { + content: "\e544"; +} + +.fa-money-check-alt:before, +.fa-money-check-dollar:before { + content: "\f53d"; +} + +.fa-vector-square:before { + content: "\f5cb"; +} + +.fa-bread-slice:before { + content: "\f7ec"; +} + +.fa-language:before { + content: "\f1ab"; +} + +.fa-face-kiss-wink-heart:before, +.fa-kiss-wink-heart:before { + content: "\f598"; +} + +.fa-filter:before { + content: "\f0b0"; +} + +.fa-question:before { + content: "\3f"; +} + +.fa-file-signature:before { + content: "\f573"; +} + +.fa-arrows-alt:before, +.fa-up-down-left-right:before { + content: "\f0b2"; +} + +.fa-house-chimney-user:before { + content: "\e065"; +} + +.fa-hand-holding-heart:before { + content: "\f4be"; +} + +.fa-puzzle-piece:before { + content: "\f12e"; +} + +.fa-money-check:before { + content: "\f53c"; +} + +.fa-star-half-alt:before, +.fa-star-half-stroke:before { + content: "\f5c0"; +} + +.fa-code:before { + content: "\f121"; +} + +.fa-glass-whiskey:before, +.fa-whiskey-glass:before { + content: "\f7a0"; +} + +.fa-building-circle-exclamation:before { + content: "\e4d3"; +} + +.fa-magnifying-glass-chart:before { + content: "\e522"; +} + +.fa-arrow-up-right-from-square:before, +.fa-external-link:before { + content: "\f08e"; +} + +.fa-cubes-stacked:before { + content: "\e4e6"; +} + +.fa-krw:before, +.fa-won-sign:before, +.fa-won:before { + content: "\f159"; +} + +.fa-virus-covid:before { + content: "\e4a8"; +} + +.fa-austral-sign:before { + content: "\e0a9"; +} + +.fa-f:before { + content: "\46"; +} + +.fa-leaf:before { + content: "\f06c"; +} + +.fa-road:before { + content: "\f018"; +} + +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} + +.fa-person-circle-plus:before { + content: "\e541"; +} + +.fa-chart-pie:before, +.fa-pie-chart:before { + content: "\f200"; +} + +.fa-bolt-lightning:before { + content: "\e0b7"; +} + +.fa-sack-xmark:before { + content: "\e56a"; +} + +.fa-file-excel:before { + content: "\f1c3"; +} + +.fa-file-contract:before { + content: "\f56c"; +} + +.fa-fish-fins:before { + content: "\e4f2"; +} + +.fa-building-flag:before { + content: "\e4d5"; +} + +.fa-face-grin-beam:before, +.fa-grin-beam:before { + content: "\f582"; +} + +.fa-object-ungroup:before { + content: "\f248"; +} + +.fa-poop:before { + content: "\f619"; +} + +.fa-location-pin:before, +.fa-map-marker:before { + content: "\f041"; +} + +.fa-kaaba:before { + content: "\f66b"; +} + +.fa-toilet-paper:before { + content: "\f71e"; +} + +.fa-hard-hat:before, +.fa-hat-hard:before, +.fa-helmet-safety:before { + content: "\f807"; +} + +.fa-eject:before { + content: "\f052"; +} + +.fa-arrow-alt-circle-right:before, +.fa-circle-right:before { + content: "\f35a"; +} + +.fa-plane-circle-check:before { + content: "\e555"; +} + +.fa-face-rolling-eyes:before, +.fa-meh-rolling-eyes:before { + content: "\f5a5"; +} + +.fa-object-group:before { + content: "\f247"; +} + +.fa-chart-line:before, +.fa-line-chart:before { + content: "\f201"; +} + +.fa-mask-ventilator:before { + content: "\e524"; +} + +.fa-arrow-right:before { + content: "\f061"; +} + +.fa-map-signs:before, +.fa-signs-post:before { + content: "\f277"; +} + +.fa-cash-register:before { + content: "\f788"; +} + +.fa-person-circle-question:before { + content: "\e542"; +} + +.fa-h:before { + content: "\48"; +} + +.fa-tarp:before { + content: "\e57b"; +} + +.fa-screwdriver-wrench:before, +.fa-tools:before { + content: "\f7d9"; +} + +.fa-arrows-to-eye:before { + content: "\e4bf"; +} + +.fa-plug-circle-bolt:before { + content: "\e55b"; +} + +.fa-heart:before { + content: "\f004"; +} + +.fa-mars-and-venus:before { + content: "\f224"; +} + +.fa-home-user:before, +.fa-house-user:before { + content: "\e1b0"; +} + +.fa-dumpster-fire:before { + content: "\f794"; +} + +.fa-house-crack:before { + content: "\e3b1"; +} + +.fa-cocktail:before, +.fa-martini-glass-citrus:before { + content: "\f561"; +} + +.fa-face-surprise:before, +.fa-surprise:before { + content: "\f5c2"; +} + +.fa-bottle-water:before { + content: "\e4c5"; +} + +.fa-circle-pause:before, +.fa-pause-circle:before { + content: "\f28b"; +} + +.fa-toilet-paper-slash:before { + content: "\e072"; +} + +.fa-apple-alt:before, +.fa-apple-whole:before { + content: "\f5d1"; +} + +.fa-kitchen-set:before { + content: "\e51a"; +} + +.fa-r:before { + content: "\52"; +} + +.fa-temperature-1:before, +.fa-temperature-quarter:before, +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} + +.fa-cube:before { + content: "\f1b2"; +} + +.fa-bitcoin-sign:before { + content: "\e0b4"; +} + +.fa-shield-dog:before { + content: "\e573"; +} + +.fa-solar-panel:before { + content: "\f5ba"; +} + +.fa-lock-open:before { + content: "\f3c1"; +} + +.fa-elevator:before { + content: "\e16d"; +} + +.fa-money-bill-transfer:before { + content: "\e528"; +} + +.fa-money-bill-trend-up:before { + content: "\e529"; +} + +.fa-house-flood-water-circle-arrow-right:before { + content: "\e50f"; +} + +.fa-poll-h:before, +.fa-square-poll-horizontal:before { + content: "\f682"; +} + +.fa-circle:before { + content: "\f111"; +} + +.fa-backward-fast:before, +.fa-fast-backward:before { + content: "\f049"; +} + +.fa-recycle:before { + content: "\f1b8"; +} + +.fa-user-astronaut:before { + content: "\f4fb"; +} + +.fa-plane-slash:before { + content: "\e069"; +} + +.fa-trademark:before { + content: "\f25c"; +} + +.fa-basketball-ball:before, +.fa-basketball:before { + content: "\f434"; +} + +.fa-satellite-dish:before { + content: "\f7c0"; +} + +.fa-arrow-alt-circle-up:before, +.fa-circle-up:before { + content: "\f35b"; +} + +.fa-mobile-alt:before, +.fa-mobile-screen-button:before { + content: "\f3cd"; +} + +.fa-volume-high:before, +.fa-volume-up:before { + content: "\f028"; +} + +.fa-users-rays:before { + content: "\e593"; +} + +.fa-wallet:before { + content: "\f555"; +} + +.fa-clipboard-check:before { + content: "\f46c"; +} + +.fa-file-audio:before { + content: "\f1c7"; +} + +.fa-burger:before, +.fa-hamburger:before { + content: "\f805"; +} + +.fa-wrench:before { + content: "\f0ad"; +} + +.fa-bugs:before { + content: "\e4d0"; +} + +.fa-rupee-sign:before, +.fa-rupee:before { + content: "\f156"; +} + +.fa-file-image:before { + content: "\f1c5"; +} + +.fa-circle-question:before, +.fa-question-circle:before { + content: "\f059"; +} + +.fa-plane-departure:before { + content: "\f5b0"; +} + +.fa-handshake-slash:before { + content: "\e060"; +} + +.fa-book-bookmark:before { + content: "\e0bb"; +} + +.fa-code-branch:before { + content: "\f126"; +} + +.fa-hat-cowboy:before { + content: "\f8c0"; +} + +.fa-bridge:before { + content: "\e4c8"; +} + +.fa-phone-alt:before, +.fa-phone-flip:before { + content: "\f879"; +} + +.fa-truck-front:before { + content: "\e2b7"; +} + +.fa-cat:before { + content: "\f6be"; +} + +.fa-anchor-circle-exclamation:before { + content: "\e4ab"; +} + +.fa-truck-field:before { + content: "\e58d"; +} + +.fa-route:before { + content: "\f4d7"; +} + +.fa-clipboard-question:before { + content: "\e4e3"; +} + +.fa-panorama:before { + content: "\e209"; +} + +.fa-comment-medical:before { + content: "\f7f5"; +} + +.fa-teeth-open:before { + content: "\f62f"; +} + +.fa-file-circle-minus:before { + content: "\e4ed"; +} + +.fa-tags:before { + content: "\f02c"; +} + +.fa-wine-glass:before { + content: "\f4e3"; +} + +.fa-fast-forward:before, +.fa-forward-fast:before { + content: "\f050"; +} + +.fa-face-meh-blank:before, +.fa-meh-blank:before { + content: "\f5a4"; +} + +.fa-parking:before, +.fa-square-parking:before { + content: "\f540"; +} + +.fa-house-signal:before { + content: "\e012"; +} + +.fa-bars-progress:before, +.fa-tasks-alt:before { + content: "\f828"; +} + +.fa-faucet-drip:before { + content: "\e006"; +} + +.fa-cart-flatbed:before, +.fa-dolly-flatbed:before { + content: "\f474"; +} + +.fa-ban-smoking:before, +.fa-smoking-ban:before { + content: "\f54d"; +} + +.fa-terminal:before { + content: "\f120"; +} + +.fa-mobile-button:before { + content: "\f10b"; +} + +.fa-house-medical-flag:before { + content: "\e514"; +} + +.fa-basket-shopping:before, +.fa-shopping-basket:before { + content: "\f291"; +} + +.fa-tape:before { + content: "\f4db"; +} + +.fa-bus-alt:before, +.fa-bus-simple:before { + content: "\f55e"; +} + +.fa-eye:before { + content: "\f06e"; +} + +.fa-face-sad-cry:before, +.fa-sad-cry:before { + content: "\f5b3"; +} + +.fa-audio-description:before { + content: "\f29e"; +} + +.fa-person-military-to-person:before { + content: "\e54c"; +} + +.fa-file-shield:before { + content: "\e4f0"; +} + +.fa-user-slash:before { + content: "\f506"; +} + +.fa-pen:before { + content: "\f304"; +} + +.fa-tower-observation:before { + content: "\e586"; +} + +.fa-file-code:before { + content: "\f1c9"; +} + +.fa-signal-5:before, +.fa-signal-perfect:before, +.fa-signal:before { + content: "\f012"; +} + +.fa-bus:before { + content: "\f207"; +} + +.fa-heart-circle-xmark:before { + content: "\e501"; +} + +.fa-home-lg:before, +.fa-house-chimney:before { + content: "\e3af"; +} + +.fa-window-maximize:before { + content: "\f2d0"; +} + +.fa-face-frown:before, +.fa-frown:before { + content: "\f119"; +} + +.fa-prescription:before { + content: "\f5b1"; +} + +.fa-shop:before, +.fa-store-alt:before { + content: "\f54f"; +} + +.fa-floppy-disk:before, +.fa-save:before { + content: "\f0c7"; +} + +.fa-vihara:before { + content: "\f6a7"; +} + +.fa-balance-scale-left:before, +.fa-scale-unbalanced:before { + content: "\f515"; +} + +.fa-sort-asc:before, +.fa-sort-up:before { + content: "\f0de"; +} + +.fa-comment-dots:before, +.fa-commenting:before { + content: "\f4ad"; +} + +.fa-plant-wilt:before { + content: "\e5aa"; +} + +.fa-diamond:before { + content: "\f219"; +} + +.fa-face-grin-squint:before, +.fa-grin-squint:before { + content: "\f585"; +} + +.fa-hand-holding-dollar:before, +.fa-hand-holding-usd:before { + content: "\f4c0"; +} + +.fa-bacterium:before { + content: "\e05a"; +} + +.fa-hand-pointer:before { + content: "\f25a"; +} + +.fa-drum-steelpan:before { + content: "\f56a"; +} + +.fa-hand-scissors:before { + content: "\f257"; +} + +.fa-hands-praying:before, +.fa-praying-hands:before { + content: "\f684"; +} + +.fa-arrow-right-rotate:before, +.fa-arrow-rotate-forward:before, +.fa-arrow-rotate-right:before, +.fa-redo:before { + content: "\f01e"; +} + +.fa-biohazard:before { + content: "\f780"; +} + +.fa-location-crosshairs:before, +.fa-location:before { + content: "\f601"; +} + +.fa-mars-double:before { + content: "\f227"; +} + +.fa-child-dress:before { + content: "\e59c"; +} + +.fa-users-between-lines:before { + content: "\e591"; +} + +.fa-lungs-virus:before { + content: "\e067"; +} + +.fa-face-grin-tears:before, +.fa-grin-tears:before { + content: "\f588"; +} + +.fa-phone:before { + content: "\f095"; +} + +.fa-calendar-times:before, +.fa-calendar-xmark:before { + content: "\f273"; +} + +.fa-child-reaching:before { + content: "\e59d"; +} + +.fa-head-side-virus:before { + content: "\e064"; +} + +.fa-user-cog:before, +.fa-user-gear:before { + content: "\f4fe"; +} + +.fa-arrow-up-1-9:before, +.fa-sort-numeric-up:before { + content: "\f163"; +} + +.fa-door-closed:before { + content: "\f52a"; +} + +.fa-shield-virus:before { + content: "\e06c"; +} + +.fa-dice-six:before { + content: "\f526"; +} + +.fa-mosquito-net:before { + content: "\e52c"; +} + +.fa-bridge-water:before { + content: "\e4ce"; +} + +.fa-person-booth:before { + content: "\f756"; +} + +.fa-text-width:before { + content: "\f035"; +} + +.fa-hat-wizard:before { + content: "\f6e8"; +} + +.fa-pen-fancy:before { + content: "\f5ac"; +} + +.fa-digging:before, +.fa-person-digging:before { + content: "\f85e"; +} + +.fa-trash:before { + content: "\f1f8"; +} + +.fa-gauge-simple-med:before, +.fa-gauge-simple:before, +.fa-tachometer-average:before { + content: "\f629"; +} + +.fa-book-medical:before { + content: "\f7e6"; +} + +.fa-poo:before { + content: "\f2fe"; +} + +.fa-quote-right-alt:before, +.fa-quote-right:before { + content: "\f10e"; +} + +.fa-shirt:before, +.fa-t-shirt:before, +.fa-tshirt:before { + content: "\f553"; +} + +.fa-cubes:before { + content: "\f1b3"; +} + +.fa-divide:before { + content: "\f529"; +} + +.fa-tenge-sign:before, +.fa-tenge:before { + content: "\f7d7"; +} + +.fa-headphones:before { + content: "\f025"; +} + +.fa-hands-holding:before { + content: "\f4c2"; +} + +.fa-hands-clapping:before { + content: "\e1a8"; +} + +.fa-republican:before { + content: "\f75e"; +} + +.fa-arrow-left:before { + content: "\f060"; +} + +.fa-person-circle-xmark:before { + content: "\e543"; +} + +.fa-ruler:before { + content: "\f545"; +} + +.fa-align-left:before { + content: "\f036"; +} + +.fa-dice-d6:before { + content: "\f6d1"; +} + +.fa-restroom:before { + content: "\f7bd"; +} + +.fa-j:before { + content: "\4a"; +} + +.fa-users-viewfinder:before { + content: "\e595"; +} + +.fa-file-video:before { + content: "\f1c8"; +} + +.fa-external-link-alt:before, +.fa-up-right-from-square:before { + content: "\f35d"; +} + +.fa-table-cells:before, +.fa-th:before { + content: "\f00a"; +} + +.fa-file-pdf:before { + content: "\f1c1"; +} + +.fa-bible:before, +.fa-book-bible:before { + content: "\f647"; +} + +.fa-o:before { + content: "\4f"; +} + +.fa-medkit:before, +.fa-suitcase-medical:before { + content: "\f0fa"; +} + +.fa-user-secret:before { + content: "\f21b"; +} + +.fa-otter:before { + content: "\f700"; +} + +.fa-female:before, +.fa-person-dress:before { + content: "\f182"; +} + +.fa-comment-dollar:before { + content: "\f651"; +} + +.fa-briefcase-clock:before, +.fa-business-time:before { + content: "\f64a"; +} + +.fa-table-cells-large:before, +.fa-th-large:before { + content: "\f009"; +} + +.fa-book-tanakh:before, +.fa-tanakh:before { + content: "\f827"; +} + +.fa-phone-volume:before, +.fa-volume-control-phone:before { + content: "\f2a0"; +} + +.fa-hat-cowboy-side:before { + content: "\f8c1"; +} + +.fa-clipboard-user:before { + content: "\f7f3"; +} + +.fa-child:before { + content: "\f1ae"; +} + +.fa-lira-sign:before { + content: "\f195"; +} + +.fa-satellite:before { + content: "\f7bf"; +} + +.fa-plane-lock:before { + content: "\e558"; +} + +.fa-tag:before { + content: "\f02b"; +} + +.fa-comment:before { + content: "\f075"; +} + +.fa-birthday-cake:before, +.fa-cake-candles:before, +.fa-cake:before { + content: "\f1fd"; +} + +.fa-envelope:before { + content: "\f0e0"; +} + +.fa-angle-double-up:before, +.fa-angles-up:before { + content: "\f102"; +} + +.fa-paperclip:before { + content: "\f0c6"; +} + +.fa-arrow-right-to-city:before { + content: "\e4b3"; +} + +.fa-ribbon:before { + content: "\f4d6"; +} + +.fa-lungs:before { + content: "\f604"; +} + +.fa-arrow-up-9-1:before, +.fa-sort-numeric-up-alt:before { + content: "\f887"; +} + +.fa-litecoin-sign:before { + content: "\e1d3"; +} + +.fa-border-none:before { + content: "\f850"; +} + +.fa-circle-nodes:before { + content: "\e4e2"; +} + +.fa-parachute-box:before { + content: "\f4cd"; +} + +.fa-indent:before { + content: "\f03c"; +} + +.fa-truck-field-un:before { + content: "\e58e"; +} + +.fa-hourglass-empty:before, +.fa-hourglass:before { + content: "\f254"; +} + +.fa-mountain:before { + content: "\f6fc"; +} + +.fa-user-doctor:before, +.fa-user-md:before { + content: "\f0f0"; +} + +.fa-circle-info:before, +.fa-info-circle:before { + content: "\f05a"; +} + +.fa-cloud-meatball:before { + content: "\f73b"; +} + +.fa-camera-alt:before, +.fa-camera:before { + content: "\f030"; +} + +.fa-square-virus:before { + content: "\e578"; +} + +.fa-meteor:before { + content: "\f753"; +} + +.fa-car-on:before { + content: "\e4dd"; +} + +.fa-sleigh:before { + content: "\f7cc"; +} + +.fa-arrow-down-1-9:before, +.fa-sort-numeric-asc:before, +.fa-sort-numeric-down:before { + content: "\f162"; +} + +.fa-hand-holding-droplet:before, +.fa-hand-holding-water:before { + content: "\f4c1"; +} + +.fa-water:before { + content: "\f773"; +} + +.fa-calendar-check:before { + content: "\f274"; +} + +.fa-braille:before { + content: "\f2a1"; +} + +.fa-prescription-bottle-alt:before, +.fa-prescription-bottle-medical:before { + content: "\f486"; +} + +.fa-landmark:before { + content: "\f66f"; +} + +.fa-truck:before { + content: "\f0d1"; +} + +.fa-crosshairs:before { + content: "\f05b"; +} + +.fa-person-cane:before { + content: "\e53c"; +} + +.fa-tent:before { + content: "\e57d"; +} + +.fa-vest-patches:before { + content: "\e086"; +} + +.fa-check-double:before { + content: "\f560"; +} + +.fa-arrow-down-a-z:before, +.fa-sort-alpha-asc:before, +.fa-sort-alpha-down:before { + content: "\f15d"; +} + +.fa-money-bill-wheat:before { + content: "\e52a"; +} + +.fa-cookie:before { + content: "\f563"; +} + +.fa-arrow-left-rotate:before, +.fa-arrow-rotate-back:before, +.fa-arrow-rotate-backward:before, +.fa-arrow-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} + +.fa-hard-drive:before, +.fa-hdd:before { + content: "\f0a0"; +} + +.fa-face-grin-squint-tears:before, +.fa-grin-squint-tears:before { + content: "\f586"; +} + +.fa-dumbbell:before { + content: "\f44b"; +} + +.fa-list-alt:before, +.fa-rectangle-list:before { + content: "\f022"; +} + +.fa-tarp-droplet:before { + content: "\e57c"; +} + +.fa-house-medical-circle-check:before { + content: "\e511"; +} + +.fa-person-skiing-nordic:before, +.fa-skiing-nordic:before { + content: "\f7ca"; +} + +.fa-calendar-plus:before { + content: "\f271"; +} + +.fa-plane-arrival:before { + content: "\f5af"; +} + +.fa-arrow-alt-circle-left:before, +.fa-circle-left:before { + content: "\f359"; +} + +.fa-subway:before, +.fa-train-subway:before { + content: "\f239"; +} + +.fa-chart-gantt:before { + content: "\e0e4"; +} + +.fa-indian-rupee-sign:before, +.fa-indian-rupee:before, +.fa-inr:before { + content: "\e1bc"; +} + +.fa-crop-alt:before, +.fa-crop-simple:before { + content: "\f565"; +} + +.fa-money-bill-1:before, +.fa-money-bill-alt:before { + content: "\f3d1"; +} + +.fa-left-long:before, +.fa-long-arrow-alt-left:before { + content: "\f30a"; +} + +.fa-dna:before { + content: "\f471"; +} + +.fa-virus-slash:before { + content: "\e075"; +} + +.fa-minus:before, +.fa-subtract:before { + content: "\f068"; +} + +.fa-chess:before { + content: "\f439"; +} + +.fa-arrow-left-long:before, +.fa-long-arrow-left:before { + content: "\f177"; +} + +.fa-plug-circle-check:before { + content: "\e55c"; +} + +.fa-street-view:before { + content: "\f21d"; +} + +.fa-franc-sign:before { + content: "\e18f"; +} + +.fa-volume-off:before { + content: "\f026"; +} + +.fa-american-sign-language-interpreting:before, +.fa-asl-interpreting:before, +.fa-hands-american-sign-language-interpreting:before, +.fa-hands-asl-interpreting:before { + content: "\f2a3"; +} + +.fa-cog:before, +.fa-gear:before { + content: "\f013"; +} + +.fa-droplet-slash:before, +.fa-tint-slash:before { + content: "\f5c7"; +} + +.fa-mosque:before { + content: "\f678"; +} + +.fa-mosquito:before { + content: "\e52b"; +} + +.fa-star-of-david:before { + content: "\f69a"; +} + +.fa-person-military-rifle:before { + content: "\e54b"; +} + +.fa-cart-shopping:before, +.fa-shopping-cart:before { + content: "\f07a"; +} + +.fa-vials:before { + content: "\f493"; +} + +.fa-plug-circle-plus:before { + content: "\e55f"; +} + +.fa-place-of-worship:before { + content: "\f67f"; +} + +.fa-grip-vertical:before { + content: "\f58e"; +} + +.fa-arrow-turn-up:before, +.fa-level-up:before { + content: "\f148"; +} + +.fa-u:before { + content: "\55"; +} + +.fa-square-root-alt:before, +.fa-square-root-variable:before { + content: "\f698"; +} + +.fa-clock-four:before, +.fa-clock:before { + content: "\f017"; +} + +.fa-backward-step:before, +.fa-step-backward:before { + content: "\f048"; +} + +.fa-pallet:before { + content: "\f482"; +} + +.fa-faucet:before { + content: "\e005"; +} + +.fa-baseball-bat-ball:before { + content: "\f432"; +} + +.fa-s:before { + content: "\53"; +} + +.fa-timeline:before { + content: "\e29c"; +} + +.fa-keyboard:before { + content: "\f11c"; +} + +.fa-caret-down:before { + content: "\f0d7"; +} + +.fa-clinic-medical:before, +.fa-house-chimney-medical:before { + content: "\f7f2"; +} + +.fa-temperature-3:before, +.fa-temperature-three-quarters:before, +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} + +.fa-mobile-android-alt:before, +.fa-mobile-screen:before { + content: "\f3cf"; +} + +.fa-plane-up:before { + content: "\e22d"; +} + +.fa-piggy-bank:before { + content: "\f4d3"; +} + +.fa-battery-3:before, +.fa-battery-half:before { + content: "\f242"; +} + +.fa-mountain-city:before { + content: "\e52e"; +} + +.fa-coins:before { + content: "\f51e"; +} + +.fa-khanda:before { + content: "\f66d"; +} + +.fa-sliders-h:before, +.fa-sliders:before { + content: "\f1de"; +} + +.fa-folder-tree:before { + content: "\f802"; +} + +.fa-network-wired:before { + content: "\f6ff"; +} + +.fa-map-pin:before { + content: "\f276"; +} + +.fa-hamsa:before { + content: "\f665"; +} + +.fa-cent-sign:before { + content: "\e3f5"; +} + +.fa-flask:before { + content: "\f0c3"; +} + +.fa-person-pregnant:before { + content: "\e31e"; +} + +.fa-wand-sparkles:before { + content: "\f72b"; +} + +.fa-ellipsis-v:before, +.fa-ellipsis-vertical:before { + content: "\f142"; +} + +.fa-ticket:before { + content: "\f145"; +} + +.fa-power-off:before { + content: "\f011"; +} + +.fa-long-arrow-alt-right:before, +.fa-right-long:before { + content: "\f30b"; +} + +.fa-flag-usa:before { + content: "\f74d"; +} + +.fa-laptop-file:before { + content: "\e51d"; +} + +.fa-teletype:before, +.fa-tty:before { + content: "\f1e4"; +} + +.fa-diagram-next:before { + content: "\e476"; +} + +.fa-person-rifle:before { + content: "\e54e"; +} + +.fa-house-medical-circle-exclamation:before { + content: "\e512"; +} + +.fa-closed-captioning:before { + content: "\f20a"; +} + +.fa-hiking:before, +.fa-person-hiking:before { + content: "\f6ec"; +} + +.fa-venus-double:before { + content: "\f226"; +} + +.fa-images:before { + content: "\f302"; +} + +.fa-calculator:before { + content: "\f1ec"; +} + +.fa-people-pulling:before { + content: "\e535"; +} + +.fa-n:before { + content: "\4e"; +} + +.fa-cable-car:before, +.fa-tram:before { + content: "\f7da"; +} + +.fa-cloud-rain:before { + content: "\f73d"; +} + +.fa-building-circle-xmark:before { + content: "\e4d4"; +} + +.fa-ship:before { + content: "\f21a"; +} + +.fa-arrows-down-to-line:before { + content: "\e4b8"; +} + +.fa-download:before { + content: "\f019"; +} + +.fa-face-grin:before, +.fa-grin:before { + content: "\f580"; +} + +.fa-backspace:before, +.fa-delete-left:before { + content: "\f55a"; +} + +.fa-eye-dropper-empty:before, +.fa-eye-dropper:before, +.fa-eyedropper:before { + content: "\f1fb"; +} + +.fa-file-circle-check:before { + content: "\e5a0"; +} + +.fa-forward:before { + content: "\f04e"; +} + +.fa-mobile-android:before, +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f3ce"; +} + +.fa-face-meh:before, +.fa-meh:before { + content: "\f11a"; +} + +.fa-align-center:before { + content: "\f037"; +} + +.fa-book-dead:before, +.fa-book-skull:before { + content: "\f6b7"; +} + +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} + +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} + +.fa-heart-circle-exclamation:before { + content: "\e4fe"; +} + +.fa-home-alt:before, +.fa-home-lg-alt:before, +.fa-home:before, +.fa-house:before { + content: "\f015"; +} + +.fa-calendar-week:before { + content: "\f784"; +} + +.fa-laptop-medical:before { + content: "\f812"; +} + +.fa-b:before { + content: "\42"; +} + +.fa-file-medical:before { + content: "\f477"; +} + +.fa-dice-one:before { + content: "\f525"; +} + +.fa-kiwi-bird:before { + content: "\f535"; +} + +.fa-arrow-right-arrow-left:before, +.fa-exchange:before { + content: "\f0ec"; +} + +.fa-redo-alt:before, +.fa-rotate-forward:before, +.fa-rotate-right:before { + content: "\f2f9"; +} + +.fa-cutlery:before, +.fa-utensils:before { + content: "\f2e7"; +} + +.fa-arrow-up-wide-short:before, +.fa-sort-amount-up:before { + content: "\f161"; +} + +.fa-mill-sign:before { + content: "\e1ed"; +} + +.fa-bowl-rice:before { + content: "\e2eb"; +} + +.fa-skull:before { + content: "\f54c"; +} + +.fa-broadcast-tower:before, +.fa-tower-broadcast:before { + content: "\f519"; +} + +.fa-truck-pickup:before { + content: "\f63c"; +} + +.fa-long-arrow-alt-up:before, +.fa-up-long:before { + content: "\f30c"; +} + +.fa-stop:before { + content: "\f04d"; +} + +.fa-code-merge:before { + content: "\f387"; +} + +.fa-upload:before { + content: "\f093"; +} + +.fa-hurricane:before { + content: "\f751"; +} + +.fa-mound:before { + content: "\e52d"; +} + +.fa-toilet-portable:before { + content: "\e583"; +} + +.fa-compact-disc:before { + content: "\f51f"; +} + +.fa-file-arrow-down:before, +.fa-file-download:before { + content: "\f56d"; +} + +.fa-caravan:before { + content: "\f8ff"; +} + +.fa-shield-cat:before { + content: "\e572"; +} + +.fa-bolt:before, +.fa-zap:before { + content: "\f0e7"; +} + +.fa-glass-water:before { + content: "\e4f4"; +} + +.fa-oil-well:before { + content: "\e532"; +} + +.fa-vault:before { + content: "\e2c5"; +} + +.fa-mars:before { + content: "\f222"; +} + +.fa-toilet:before { + content: "\f7d8"; +} + +.fa-plane-circle-xmark:before { + content: "\e557"; +} + +.fa-cny:before, +.fa-jpy:before, +.fa-rmb:before, +.fa-yen-sign:before, +.fa-yen:before { + content: "\f157"; +} + +.fa-rouble:before, +.fa-rub:before, +.fa-ruble-sign:before, +.fa-ruble:before { + content: "\f158"; +} + +.fa-sun:before { + content: "\f185"; +} + +.fa-guitar:before { + content: "\f7a6"; +} + +.fa-face-laugh-wink:before, +.fa-laugh-wink:before { + content: "\f59c"; +} + +.fa-horse-head:before { + content: "\f7ab"; +} + +.fa-bore-hole:before { + content: "\e4c3"; +} + +.fa-industry:before { + content: "\f275"; +} + +.fa-arrow-alt-circle-down:before, +.fa-circle-down:before { + content: "\f358"; +} + +.fa-arrows-turn-to-dots:before { + content: "\e4c1"; +} + +.fa-florin-sign:before { + content: "\e184"; +} + +.fa-arrow-down-short-wide:before, +.fa-sort-amount-desc:before, +.fa-sort-amount-down-alt:before { + content: "\f884"; +} + +.fa-less-than:before { + content: "\3c"; +} + +.fa-angle-down:before { + content: "\f107"; +} + +.fa-car-tunnel:before { + content: "\e4de"; +} + +.fa-head-side-cough:before { + content: "\e061"; +} + +.fa-grip-lines:before { + content: "\f7a4"; +} + +.fa-thumbs-down:before { + content: "\f165"; +} + +.fa-user-lock:before { + content: "\f502"; +} + +.fa-arrow-right-long:before, +.fa-long-arrow-right:before { + content: "\f178"; +} + +.fa-anchor-circle-xmark:before { + content: "\e4ac"; +} + +.fa-ellipsis-h:before, +.fa-ellipsis:before { + content: "\f141"; +} + +.fa-chess-pawn:before { + content: "\f443"; +} + +.fa-first-aid:before, +.fa-kit-medical:before { + content: "\f479"; +} + +.fa-person-through-window:before { + content: "\e5a9"; +} + +.fa-toolbox:before { + content: "\f552"; +} + +.fa-hands-holding-circle:before { + content: "\e4fb"; +} + +.fa-bug:before { + content: "\f188"; +} + +.fa-credit-card-alt:before, +.fa-credit-card:before { + content: "\f09d"; +} + +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} + +.fa-hand-holding-hand:before { + content: "\e4f7"; +} + +.fa-book-open-reader:before, +.fa-book-reader:before { + content: "\f5da"; +} + +.fa-mountain-sun:before { + content: "\e52f"; +} + +.fa-arrows-left-right-to-line:before { + content: "\e4ba"; +} + +.fa-dice-d20:before { + content: "\f6cf"; +} + +.fa-truck-droplet:before { + content: "\e58c"; +} + +.fa-file-circle-xmark:before { + content: "\e5a1"; +} + +.fa-temperature-arrow-up:before, +.fa-temperature-up:before { + content: "\e040"; +} + +.fa-medal:before { + content: "\f5a2"; +} + +.fa-bed:before { + content: "\f236"; +} + +.fa-h-square:before, +.fa-square-h:before { + content: "\f0fd"; +} + +.fa-podcast:before { + content: "\f2ce"; +} + +.fa-temperature-4:before, +.fa-temperature-full:before, +.fa-thermometer-4:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} + +.fa-bell:before { + content: "\f0f3"; +} + +.fa-superscript:before { + content: "\f12b"; +} + +.fa-plug-circle-xmark:before { + content: "\e560"; +} + +.fa-star-of-life:before { + content: "\f621"; +} + +.fa-phone-slash:before { + content: "\f3dd"; +} + +.fa-paint-roller:before { + content: "\f5aa"; +} + +.fa-hands-helping:before, +.fa-handshake-angle:before { + content: "\f4c4"; +} + +.fa-location-dot:before, +.fa-map-marker-alt:before { + content: "\f3c5"; +} + +.fa-file:before { + content: "\f15b"; +} + +.fa-greater-than:before { + content: "\3e"; +} + +.fa-person-swimming:before, +.fa-swimmer:before { + content: "\f5c4"; +} + +.fa-arrow-down:before { + content: "\f063"; +} + +.fa-droplet:before, +.fa-tint:before { + content: "\f043"; +} + +.fa-eraser:before { + content: "\f12d"; +} + +.fa-earth-america:before, +.fa-earth-americas:before, +.fa-earth:before, +.fa-globe-americas:before { + content: "\f57d"; +} + +.fa-person-burst:before { + content: "\e53b"; +} + +.fa-dove:before { + content: "\f4ba"; +} + +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} + +.fa-socks:before { + content: "\f696"; +} + +.fa-inbox:before { + content: "\f01c"; +} + +.fa-section:before { + content: "\e447"; +} + +.fa-gauge-high:before, +.fa-tachometer-alt-fast:before, +.fa-tachometer-alt:before { + content: "\f625"; +} + +.fa-envelope-open-text:before { + content: "\f658"; +} + +.fa-hospital-alt:before, +.fa-hospital-wide:before, +.fa-hospital:before { + content: "\f0f8"; +} + +.fa-wine-bottle:before { + content: "\f72f"; +} + +.fa-chess-rook:before { + content: "\f447"; +} + +.fa-bars-staggered:before, +.fa-reorder:before, +.fa-stream:before { + content: "\f550"; +} + +.fa-dharmachakra:before { + content: "\f655"; +} + +.fa-hotdog:before { + content: "\f80f"; +} + +.fa-blind:before, +.fa-person-walking-with-cane:before { + content: "\f29d"; +} + +.fa-drum:before { + content: "\f569"; +} + +.fa-ice-cream:before { + content: "\f810"; +} + +.fa-heart-circle-bolt:before { + content: "\e4fc"; +} + +.fa-fax:before { + content: "\f1ac"; +} + +.fa-paragraph:before { + content: "\f1dd"; +} + +.fa-check-to-slot:before, +.fa-vote-yea:before { + content: "\f772"; +} + +.fa-star-half:before { + content: "\f089"; +} + +.fa-boxes-alt:before, +.fa-boxes-stacked:before, +.fa-boxes:before { + content: "\f468"; +} + +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} + +.fa-assistive-listening-systems:before, +.fa-ear-listen:before { + content: "\f2a2"; +} + +.fa-tree-city:before { + content: "\e587"; +} + +.fa-play:before { + content: "\f04b"; +} + +.fa-font:before { + content: "\f031"; +} + +.fa-rupiah-sign:before { + content: "\e23d"; +} + +.fa-magnifying-glass:before, +.fa-search:before { + content: "\f002"; +} + +.fa-ping-pong-paddle-ball:before, +.fa-table-tennis-paddle-ball:before, +.fa-table-tennis:before { + content: "\f45d"; +} + +.fa-diagnoses:before, +.fa-person-dots-from-line:before { + content: "\f470"; +} + +.fa-trash-can-arrow-up:before, +.fa-trash-restore-alt:before { + content: "\f82a"; +} + +.fa-naira-sign:before { + content: "\e1f6"; +} + +.fa-cart-arrow-down:before { + content: "\f218"; +} + +.fa-walkie-talkie:before { + content: "\f8ef"; +} + +.fa-file-edit:before, +.fa-file-pen:before { + content: "\f31c"; +} + +.fa-receipt:before { + content: "\f543"; +} + +.fa-pen-square:before, +.fa-pencil-square:before, +.fa-square-pen:before { + content: "\f14b"; +} + +.fa-suitcase-rolling:before { + content: "\f5c1"; +} + +.fa-person-circle-exclamation:before { + content: "\e53f"; +} + +.fa-chevron-down:before { + content: "\f078"; +} + +.fa-battery-5:before, +.fa-battery-full:before, +.fa-battery:before { + content: "\f240"; +} + +.fa-skull-crossbones:before { + content: "\f714"; +} + +.fa-code-compare:before { + content: "\e13a"; +} + +.fa-list-dots:before, +.fa-list-ul:before { + content: "\f0ca"; +} + +.fa-school-lock:before { + content: "\e56f"; +} + +.fa-tower-cell:before { + content: "\e585"; +} + +.fa-down-long:before, +.fa-long-arrow-alt-down:before { + content: "\f309"; +} + +.fa-ranking-star:before { + content: "\e561"; +} + +.fa-chess-king:before { + content: "\f43f"; +} + +.fa-person-harassing:before { + content: "\e549"; +} + +.fa-brazilian-real-sign:before { + content: "\e46c"; +} + +.fa-landmark-alt:before, +.fa-landmark-dome:before { + content: "\f752"; +} + +.fa-arrow-up:before { + content: "\f062"; +} + +.fa-television:before, +.fa-tv-alt:before, +.fa-tv:before { + content: "\f26c"; +} + +.fa-shrimp:before { + content: "\e448"; +} + +.fa-list-check:before, +.fa-tasks:before { + content: "\f0ae"; +} + +.fa-jug-detergent:before { + content: "\e519"; +} + +.fa-circle-user:before, +.fa-user-circle:before { + content: "\f2bd"; +} + +.fa-user-shield:before { + content: "\f505"; +} + +.fa-wind:before { + content: "\f72e"; +} + +.fa-car-burst:before, +.fa-car-crash:before { + content: "\f5e1"; +} + +.fa-y:before { + content: "\59"; +} + +.fa-person-snowboarding:before, +.fa-snowboarding:before { + content: "\f7ce"; +} + +.fa-shipping-fast:before, +.fa-truck-fast:before { + content: "\f48b"; +} + +.fa-fish:before { + content: "\f578"; +} + +.fa-user-graduate:before { + content: "\f501"; +} + +.fa-adjust:before, +.fa-circle-half-stroke:before { + content: "\f042"; +} + +.fa-clapperboard:before { + content: "\e131"; +} + +.fa-circle-radiation:before, +.fa-radiation-alt:before { + content: "\f7ba"; +} + +.fa-baseball-ball:before, +.fa-baseball:before { + content: "\f433"; +} + +.fa-jet-fighter-up:before { + content: "\e518"; +} + +.fa-diagram-project:before, +.fa-project-diagram:before { + content: "\f542"; +} + +.fa-copy:before { + content: "\f0c5"; +} + +.fa-volume-mute:before, +.fa-volume-times:before, +.fa-volume-xmark:before { + content: "\f6a9"; +} + +.fa-hand-sparkles:before { + content: "\e05d"; +} + +.fa-grip-horizontal:before, +.fa-grip:before { + content: "\f58d"; +} + +.fa-share-from-square:before, +.fa-share-square:before { + content: "\f14d"; +} + +.fa-child-combatant:before, +.fa-child-rifle:before { + content: "\e4e0"; +} + +.fa-gun:before { + content: "\e19b"; +} + +.fa-phone-square:before, +.fa-square-phone:before { + content: "\f098"; +} + +.fa-add:before, +.fa-plus:before { + content: "\2b"; +} + +.fa-expand:before { + content: "\f065"; +} + +.fa-computer:before { + content: "\e4e5"; +} + +.fa-close:before, +.fa-multiply:before, +.fa-remove:before, +.fa-times:before, +.fa-xmark:before { + content: "\f00d"; +} + +.fa-arrows-up-down-left-right:before, +.fa-arrows:before { + content: "\f047"; +} + +.fa-chalkboard-teacher:before, +.fa-chalkboard-user:before { + content: "\f51c"; +} + +.fa-peso-sign:before { + content: "\e222"; +} + +.fa-building-shield:before { + content: "\e4d8"; +} + +.fa-baby:before { + content: "\f77c"; +} + +.fa-users-line:before { + content: "\e592"; +} + +.fa-quote-left-alt:before, +.fa-quote-left:before { + content: "\f10d"; +} + +.fa-tractor:before { + content: "\f722"; +} + +.fa-trash-arrow-up:before, +.fa-trash-restore:before { + content: "\f829"; +} + +.fa-arrow-down-up-lock:before { + content: "\e4b0"; +} + +.fa-lines-leaning:before { + content: "\e51e"; +} + +.fa-ruler-combined:before { + content: "\f546"; +} + +.fa-copyright:before { + content: "\f1f9"; +} + +.fa-equals:before { + content: "\3d"; +} + +.fa-blender:before { + content: "\f517"; +} + +.fa-teeth:before { + content: "\f62e"; +} + +.fa-ils:before, +.fa-shekel-sign:before, +.fa-shekel:before, +.fa-sheqel-sign:before, +.fa-sheqel:before { + content: "\f20b"; +} + +.fa-map:before { + content: "\f279"; +} + +.fa-rocket:before { + content: "\f135"; +} + +.fa-photo-film:before, +.fa-photo-video:before { + content: "\f87c"; +} + +.fa-folder-minus:before { + content: "\f65d"; +} + +.fa-store:before { + content: "\f54e"; +} + +.fa-arrow-trend-up:before { + content: "\e098"; +} + +.fa-plug-circle-minus:before { + content: "\e55e"; +} + +.fa-sign-hanging:before, +.fa-sign:before { + content: "\f4d9"; +} + +.fa-bezier-curve:before { + content: "\f55b"; +} + +.fa-bell-slash:before { + content: "\f1f6"; +} + +.fa-tablet-android:before, +.fa-tablet:before { + content: "\f3fb"; +} + +.fa-school-flag:before { + content: "\e56e"; +} + +.fa-fill:before { + content: "\f575"; +} + +.fa-angle-up:before { + content: "\f106"; +} + +.fa-drumstick-bite:before { + content: "\f6d7"; +} + +.fa-holly-berry:before { + content: "\f7aa"; +} + +.fa-chevron-left:before { + content: "\f053"; +} + +.fa-bacteria:before { + content: "\e059"; +} + +.fa-hand-lizard:before { + content: "\f258"; +} + +.fa-notdef:before { + content: "\e1fe"; +} + +.fa-disease:before { + content: "\f7fa"; +} + +.fa-briefcase-medical:before { + content: "\f469"; +} + +.fa-genderless:before { + content: "\f22d"; +} + +.fa-chevron-right:before { + content: "\f054"; +} + +.fa-retweet:before { + content: "\f079"; +} + +.fa-car-alt:before, +.fa-car-rear:before { + content: "\f5de"; +} + +.fa-pump-soap:before { + content: "\e06b"; +} + +.fa-video-slash:before { + content: "\f4e2"; +} + +.fa-battery-2:before, +.fa-battery-quarter:before { + content: "\f243"; +} + +.fa-radio:before { + content: "\f8d7"; +} + +.fa-baby-carriage:before, +.fa-carriage-baby:before { + content: "\f77d"; +} + +.fa-traffic-light:before { + content: "\f637"; +} + +.fa-thermometer:before { + content: "\f491"; +} + +.fa-vr-cardboard:before { + content: "\f729"; +} + +.fa-hand-middle-finger:before { + content: "\f806"; +} + +.fa-percent:before, +.fa-percentage:before { + content: "\25"; +} + +.fa-truck-moving:before { + content: "\f4df"; +} + +.fa-glass-water-droplet:before { + content: "\e4f5"; +} + +.fa-display:before { + content: "\e163"; +} + +.fa-face-smile:before, +.fa-smile:before { + content: "\f118"; +} + +.fa-thumb-tack:before, +.fa-thumbtack:before { + content: "\f08d"; +} + +.fa-trophy:before { + content: "\f091"; +} + +.fa-person-praying:before, +.fa-pray:before { + content: "\f683"; +} + +.fa-hammer:before { + content: "\f6e3"; +} + +.fa-hand-peace:before { + content: "\f25b"; +} + +.fa-rotate:before, +.fa-sync-alt:before { + content: "\f2f1"; +} + +.fa-spinner:before { + content: "\f110"; +} + +.fa-robot:before { + content: "\f544"; +} + +.fa-peace:before { + content: "\f67c"; +} + +.fa-cogs:before, +.fa-gears:before { + content: "\f085"; +} + +.fa-warehouse:before { + content: "\f494"; +} + +.fa-arrow-up-right-dots:before { + content: "\e4b7"; +} + +.fa-splotch:before { + content: "\f5bc"; +} + +.fa-face-grin-hearts:before, +.fa-grin-hearts:before { + content: "\f584"; +} + +.fa-dice-four:before { + content: "\f524"; +} + +.fa-sim-card:before { + content: "\f7c4"; +} + +.fa-transgender-alt:before, +.fa-transgender:before { + content: "\f225"; +} + +.fa-mercury:before { + content: "\f223"; +} + +.fa-arrow-turn-down:before, +.fa-level-down:before { + content: "\f149"; +} + +.fa-person-falling-burst:before { + content: "\e547"; +} + +.fa-award:before { + content: "\f559"; +} + +.fa-ticket-alt:before, +.fa-ticket-simple:before { + content: "\f3ff"; +} + +.fa-building:before { + content: "\f1ad"; +} + +.fa-angle-double-left:before, +.fa-angles-left:before { + content: "\f100"; +} + +.fa-qrcode:before { + content: "\f029"; +} + +.fa-clock-rotate-left:before, +.fa-history:before { + content: "\f1da"; +} + +.fa-face-grin-beam-sweat:before, +.fa-grin-beam-sweat:before { + content: "\f583"; +} + +.fa-arrow-right-from-file:before, +.fa-file-export:before { + content: "\f56e"; +} + +.fa-shield-blank:before, +.fa-shield:before { + content: "\f132"; +} + +.fa-arrow-up-short-wide:before, +.fa-sort-amount-up-alt:before { + content: "\f885"; +} + +.fa-house-medical:before { + content: "\e3b2"; +} + +.fa-golf-ball-tee:before, +.fa-golf-ball:before { + content: "\f450"; +} + +.fa-chevron-circle-left:before, +.fa-circle-chevron-left:before { + content: "\f137"; +} + +.fa-house-chimney-window:before { + content: "\e00d"; +} + +.fa-pen-nib:before { + content: "\f5ad"; +} + +.fa-tent-arrow-turn-left:before { + content: "\e580"; +} + +.fa-tents:before { + content: "\e582"; +} + +.fa-magic:before, +.fa-wand-magic:before { + content: "\f0d0"; +} + +.fa-dog:before { + content: "\f6d3"; +} + +.fa-carrot:before { + content: "\f787"; +} + +.fa-moon:before { + content: "\f186"; +} + +.fa-wine-glass-alt:before, +.fa-wine-glass-empty:before { + content: "\f5ce"; +} + +.fa-cheese:before { + content: "\f7ef"; +} + +.fa-yin-yang:before { + content: "\f6ad"; +} + +.fa-music:before { + content: "\f001"; +} + +.fa-code-commit:before { + content: "\f386"; +} + +.fa-temperature-low:before { + content: "\f76b"; +} + +.fa-biking:before, +.fa-person-biking:before { + content: "\f84a"; +} + +.fa-broom:before { + content: "\f51a"; +} + +.fa-shield-heart:before { + content: "\e574"; +} + +.fa-gopuram:before { + content: "\f664"; +} + +.fa-earth-oceania:before, +.fa-globe-oceania:before { + content: "\e47b"; +} + +.fa-square-xmark:before, +.fa-times-square:before, +.fa-xmark-square:before { + content: "\f2d3"; +} + +.fa-hashtag:before { + content: "\23"; +} + +.fa-expand-alt:before, +.fa-up-right-and-down-left-from-center:before { + content: "\f424"; +} + +.fa-oil-can:before { + content: "\f613"; +} + +.fa-t:before { + content: "\54"; +} + +.fa-hippo:before { + content: "\f6ed"; +} + +.fa-chart-column:before { + content: "\e0e3"; +} + +.fa-infinity:before { + content: "\f534"; +} + +.fa-vial-circle-check:before { + content: "\e596"; +} + +.fa-person-arrow-down-to-line:before { + content: "\e538"; +} + +.fa-voicemail:before { + content: "\f897"; +} + +.fa-fan:before { + content: "\f863"; +} + +.fa-person-walking-luggage:before { + content: "\e554"; +} + +.fa-arrows-alt-v:before, +.fa-up-down:before { + content: "\f338"; +} + +.fa-cloud-moon-rain:before { + content: "\f73c"; +} + +.fa-calendar:before { + content: "\f133"; +} + +.fa-trailer:before { + content: "\e041"; +} + +.fa-bahai:before, +.fa-haykal:before { + content: "\f666"; +} + +.fa-sd-card:before { + content: "\f7c2"; +} + +.fa-dragon:before { + content: "\f6d5"; +} + +.fa-shoe-prints:before { + content: "\f54b"; +} + +.fa-circle-plus:before, +.fa-plus-circle:before { + content: "\f055"; +} + +.fa-face-grin-tongue-wink:before, +.fa-grin-tongue-wink:before { + content: "\f58b"; +} + +.fa-hand-holding:before { + content: "\f4bd"; +} + +.fa-plug-circle-exclamation:before { + content: "\e55d"; +} + +.fa-chain-broken:before, +.fa-chain-slash:before, +.fa-link-slash:before, +.fa-unlink:before { + content: "\f127"; +} + +.fa-clone:before { + content: "\f24d"; +} + +.fa-person-walking-arrow-loop-left:before { + content: "\e551"; +} + +.fa-arrow-up-z-a:before, +.fa-sort-alpha-up-alt:before { + content: "\f882"; +} + +.fa-fire-alt:before, +.fa-fire-flame-curved:before { + content: "\f7e4"; +} + +.fa-tornado:before { + content: "\f76f"; +} + +.fa-file-circle-plus:before { + content: "\e494"; +} + +.fa-book-quran:before, +.fa-quran:before { + content: "\f687"; +} + +.fa-anchor:before { + content: "\f13d"; +} + +.fa-border-all:before { + content: "\f84c"; +} + +.fa-angry:before, +.fa-face-angry:before { + content: "\f556"; +} + +.fa-cookie-bite:before { + content: "\f564"; +} + +.fa-arrow-trend-down:before { + content: "\e097"; +} + +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} + +.fa-draw-polygon:before { + content: "\f5ee"; +} + +.fa-balance-scale:before, +.fa-scale-balanced:before { + content: "\f24e"; +} + +.fa-gauge-simple-high:before, +.fa-tachometer-fast:before, +.fa-tachometer:before { + content: "\f62a"; +} + +.fa-shower:before { + content: "\f2cc"; +} + +.fa-desktop-alt:before, +.fa-desktop:before { + content: "\f390"; +} + +.fa-m:before { + content: "\4d"; +} + +.fa-table-list:before, +.fa-th-list:before { + content: "\f00b"; +} + +.fa-comment-sms:before, +.fa-sms:before { + content: "\f7cd"; +} + +.fa-book:before { + content: "\f02d"; +} + +.fa-user-plus:before { + content: "\f234"; +} + +.fa-check:before { + content: "\f00c"; +} + +.fa-battery-4:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} + +.fa-house-circle-check:before { + content: "\e509"; +} + +.fa-angle-left:before { + content: "\f104"; +} + +.fa-diagram-successor:before { + content: "\e47a"; +} + +.fa-truck-arrow-right:before { + content: "\e58b"; +} + +.fa-arrows-split-up-and-left:before { + content: "\e4bc"; +} + +.fa-fist-raised:before, +.fa-hand-fist:before { + content: "\f6de"; +} + +.fa-cloud-moon:before { + content: "\f6c3"; +} + +.fa-briefcase:before { + content: "\f0b1"; +} + +.fa-person-falling:before { + content: "\e546"; +} + +.fa-image-portrait:before, +.fa-portrait:before { + content: "\f3e0"; +} + +.fa-user-tag:before { + content: "\f507"; +} + +.fa-rug:before { + content: "\e569"; +} + +.fa-earth-europe:before, +.fa-globe-europe:before { + content: "\f7a2"; +} + +.fa-cart-flatbed-suitcase:before, +.fa-luggage-cart:before { + content: "\f59d"; +} + +.fa-rectangle-times:before, +.fa-rectangle-xmark:before, +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f410"; +} + +.fa-baht-sign:before { + content: "\e0ac"; +} + +.fa-book-open:before { + content: "\f518"; +} + +.fa-book-journal-whills:before, +.fa-journal-whills:before { + content: "\f66a"; +} + +.fa-handcuffs:before { + content: "\e4f8"; +} + +.fa-exclamation-triangle:before, +.fa-triangle-exclamation:before, +.fa-warning:before { + content: "\f071"; +} + +.fa-database:before { + content: "\f1c0"; +} + +.fa-arrow-turn-right:before, +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} + +.fa-bottle-droplet:before { + content: "\e4c4"; +} + +.fa-mask-face:before { + content: "\e1d7"; +} + +.fa-hill-rockslide:before { + content: "\e508"; +} + +.fa-exchange-alt:before, +.fa-right-left:before { + content: "\f362"; +} + +.fa-paper-plane:before { + content: "\f1d8"; +} + +.fa-road-circle-exclamation:before { + content: "\e565"; +} + +.fa-dungeon:before { + content: "\f6d9"; +} + +.fa-align-right:before { + content: "\f038"; +} + +.fa-money-bill-1-wave:before, +.fa-money-bill-wave-alt:before { + content: "\f53b"; +} + +.fa-life-ring:before { + content: "\f1cd"; +} + +.fa-hands:before, +.fa-sign-language:before, +.fa-signing:before { + content: "\f2a7"; +} + +.fa-calendar-day:before { + content: "\f783"; +} + +.fa-ladder-water:before, +.fa-swimming-pool:before, +.fa-water-ladder:before { + content: "\f5c5"; +} + +.fa-arrows-up-down:before, +.fa-arrows-v:before { + content: "\f07d"; +} + +.fa-face-grimace:before, +.fa-grimace:before { + content: "\f57f"; +} + +.fa-wheelchair-alt:before, +.fa-wheelchair-move:before { + content: "\e2ce"; +} + +.fa-level-down-alt:before, +.fa-turn-down:before { + content: "\f3be"; +} + +.fa-person-walking-arrow-right:before { + content: "\e552"; +} + +.fa-envelope-square:before, +.fa-square-envelope:before { + content: "\f199"; +} + +.fa-dice:before { + content: "\f522"; +} + +.fa-bowling-ball:before { + content: "\f436"; +} + +.fa-brain:before { + content: "\f5dc"; +} + +.fa-band-aid:before, +.fa-bandage:before { + content: "\f462"; +} + +.fa-calendar-minus:before { + content: "\f272"; +} + +.fa-circle-xmark:before, +.fa-times-circle:before, +.fa-xmark-circle:before { + content: "\f057"; +} + +.fa-gifts:before { + content: "\f79c"; +} + +.fa-hotel:before { + content: "\f594"; +} + +.fa-earth-asia:before, +.fa-globe-asia:before { + content: "\f57e"; +} + +.fa-id-card-alt:before, +.fa-id-card-clip:before { + content: "\f47f"; +} + +.fa-magnifying-glass-plus:before, +.fa-search-plus:before { + content: "\f00e"; +} + +.fa-thumbs-up:before { + content: "\f164"; +} + +.fa-user-clock:before { + content: "\f4fd"; +} + +.fa-allergies:before, +.fa-hand-dots:before { + content: "\f461"; +} + +.fa-file-invoice:before { + content: "\f570"; +} + +.fa-window-minimize:before { + content: "\f2d1"; +} + +.fa-coffee:before, +.fa-mug-saucer:before { + content: "\f0f4"; +} + +.fa-brush:before { + content: "\f55d"; +} + +.fa-mask:before { + content: "\f6fa"; +} + +.fa-magnifying-glass-minus:before, +.fa-search-minus:before { + content: "\f010"; +} + +.fa-ruler-vertical:before { + content: "\f548"; +} + +.fa-user-alt:before, +.fa-user-large:before { + content: "\f406"; +} + +.fa-train-tram:before { + content: "\e5b4"; +} + +.fa-user-nurse:before { + content: "\f82f"; +} + +.fa-syringe:before { + content: "\f48e"; +} + +.fa-cloud-sun:before { + content: "\f6c4"; +} + +.fa-stopwatch-20:before { + content: "\e06f"; +} + +.fa-square-full:before { + content: "\f45c"; +} + +.fa-magnet:before { + content: "\f076"; +} + +.fa-jar:before { + content: "\e516"; +} + +.fa-note-sticky:before, +.fa-sticky-note:before { + content: "\f249"; +} + +.fa-bug-slash:before { + content: "\e490"; +} + +.fa-arrow-up-from-water-pump:before { + content: "\e4b6"; +} + +.fa-bone:before { + content: "\f5d7"; +} + +.fa-user-injured:before { + content: "\f728"; +} + +.fa-face-sad-tear:before, +.fa-sad-tear:before { + content: "\f5b4"; +} + +.fa-plane:before { + content: "\f072"; +} + +.fa-tent-arrows-down:before { + content: "\e581"; +} + +.fa-exclamation:before { + content: "\21"; +} + +.fa-arrows-spin:before { + content: "\e4bb"; +} + +.fa-print:before { + content: "\f02f"; +} + +.fa-try:before, +.fa-turkish-lira-sign:before, +.fa-turkish-lira:before { + content: "\e2bb"; +} + +.fa-dollar-sign:before, +.fa-dollar:before, +.fa-usd:before { + content: "\24"; +} + +.fa-x:before { + content: "\58"; +} + +.fa-magnifying-glass-dollar:before, +.fa-search-dollar:before { + content: "\f688"; +} + +.fa-users-cog:before, +.fa-users-gear:before { + content: "\f509"; +} + +.fa-person-military-pointing:before { + content: "\e54a"; +} + +.fa-bank:before, +.fa-building-columns:before, +.fa-institution:before, +.fa-museum:before, +.fa-university:before { + content: "\f19c"; +} + +.fa-umbrella:before { + content: "\f0e9"; +} + +.fa-trowel:before { + content: "\e589"; +} + +.fa-d:before { + content: "\44"; +} + +.fa-stapler:before { + content: "\e5af"; +} + +.fa-masks-theater:before, +.fa-theater-masks:before { + content: "\f630"; +} + +.fa-kip-sign:before { + content: "\e1c4"; +} + +.fa-hand-point-left:before { + content: "\f0a5"; +} + +.fa-handshake-alt:before, +.fa-handshake-simple:before { + content: "\f4c6"; +} + +.fa-fighter-jet:before, +.fa-jet-fighter:before { + content: "\f0fb"; +} + +.fa-share-alt-square:before, +.fa-square-share-nodes:before { + content: "\f1e1"; +} + +.fa-barcode:before { + content: "\f02a"; +} + +.fa-plus-minus:before { + content: "\e43c"; +} + +.fa-video-camera:before, +.fa-video:before { + content: "\f03d"; +} + +.fa-graduation-cap:before, +.fa-mortar-board:before { + content: "\f19d"; +} + +.fa-hand-holding-medical:before { + content: "\e05c"; +} + +.fa-person-circle-check:before { + content: "\e53e"; +} + +.fa-level-up-alt:before, +.fa-turn-up:before { + content: "\f3bf"; +} + +.fa-sr-only, +.fa-sr-only-focusable:not(:focus), +.sr-only, +.sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +:host, +:root { + --fa-style-family-brands: "Font Awesome 6 Brands"; + --fa-font-brands: normal 400 1em/1 "Font Awesome 6 Brands"; +} + +@font-face { + font-family: "Font Awesome 6 Brands"; + font-style: normal; + font-weight: 400; + font-display: block; + src: + url(./webfonts/fa-brands-400.woff2) format("woff2"), + url(./webfonts/fa-brands-400.ttf) format("truetype"); +} + +.fa-brands, +.fab { + font-weight: 400; +} + +.fa-monero:before { + content: "\f3d0"; +} + +.fa-hooli:before { + content: "\f427"; +} + +.fa-yelp:before { + content: "\f1e9"; +} + +.fa-cc-visa:before { + content: "\f1f0"; +} + +.fa-lastfm:before { + content: "\f202"; +} + +.fa-shopware:before { + content: "\f5b5"; +} + +.fa-creative-commons-nc:before { + content: "\f4e8"; +} + +.fa-aws:before { + content: "\f375"; +} + +.fa-redhat:before { + content: "\f7bc"; +} + +.fa-yoast:before { + content: "\f2b1"; +} + +.fa-cloudflare:before { + content: "\e07d"; +} + +.fa-ups:before { + content: "\f7e0"; +} + +.fa-wpexplorer:before { + content: "\f2de"; +} + +.fa-dyalog:before { + content: "\f399"; +} + +.fa-bity:before { + content: "\f37a"; +} + +.fa-stackpath:before { + content: "\f842"; +} + +.fa-buysellads:before { + content: "\f20d"; +} + +.fa-first-order:before { + content: "\f2b0"; +} + +.fa-modx:before { + content: "\f285"; +} + +.fa-guilded:before { + content: "\e07e"; +} + +.fa-vnv:before { + content: "\f40b"; +} + +.fa-js-square:before, +.fa-square-js:before { + content: "\f3b9"; +} + +.fa-microsoft:before { + content: "\f3ca"; +} + +.fa-qq:before { + content: "\f1d6"; +} + +.fa-orcid:before { + content: "\f8d2"; +} + +.fa-java:before { + content: "\f4e4"; +} + +.fa-invision:before { + content: "\f7b0"; +} + +.fa-creative-commons-pd-alt:before { + content: "\f4ed"; +} + +.fa-centercode:before { + content: "\f380"; +} + +.fa-glide-g:before { + content: "\f2a6"; +} + +.fa-drupal:before { + content: "\f1a9"; +} + +.fa-hire-a-helper:before { + content: "\f3b0"; +} + +.fa-creative-commons-by:before { + content: "\f4e7"; +} + +.fa-unity:before { + content: "\e049"; +} + +.fa-whmcs:before { + content: "\f40d"; +} + +.fa-rocketchat:before { + content: "\f3e8"; +} + +.fa-vk:before { + content: "\f189"; +} + +.fa-untappd:before { + content: "\f405"; +} + +.fa-mailchimp:before { + content: "\f59e"; +} + +.fa-css3-alt:before { + content: "\f38b"; +} + +.fa-reddit-square:before, +.fa-square-reddit:before { + content: "\f1a2"; +} + +.fa-vimeo-v:before { + content: "\f27d"; +} + +.fa-contao:before { + content: "\f26d"; +} + +.fa-square-font-awesome:before { + content: "\e5ad"; +} + +.fa-deskpro:before { + content: "\f38f"; +} + +.fa-sistrix:before { + content: "\f3ee"; +} + +.fa-instagram-square:before, +.fa-square-instagram:before { + content: "\e055"; +} + +.fa-battle-net:before { + content: "\f835"; +} + +.fa-the-red-yeti:before { + content: "\f69d"; +} + +.fa-hacker-news-square:before, +.fa-square-hacker-news:before { + content: "\f3af"; +} + +.fa-edge:before { + content: "\f282"; +} + +.fa-napster:before { + content: "\f3d2"; +} + +.fa-snapchat-square:before, +.fa-square-snapchat:before { + content: "\f2ad"; +} + +.fa-google-plus-g:before { + content: "\f0d5"; +} + +.fa-artstation:before { + content: "\f77a"; +} + +.fa-markdown:before { + content: "\f60f"; +} + +.fa-sourcetree:before { + content: "\f7d3"; +} + +.fa-google-plus:before { + content: "\f2b3"; +} + +.fa-diaspora:before { + content: "\f791"; +} + +.fa-foursquare:before { + content: "\f180"; +} + +.fa-stack-overflow:before { + content: "\f16c"; +} + +.fa-github-alt:before { + content: "\f113"; +} + +.fa-phoenix-squadron:before { + content: "\f511"; +} + +.fa-pagelines:before { + content: "\f18c"; +} + +.fa-algolia:before { + content: "\f36c"; +} + +.fa-red-river:before { + content: "\f3e3"; +} + +.fa-creative-commons-sa:before { + content: "\f4ef"; +} + +.fa-safari:before { + content: "\f267"; +} + +.fa-google:before { + content: "\f1a0"; +} + +.fa-font-awesome-alt:before, +.fa-square-font-awesome-stroke:before { + content: "\f35c"; +} + +.fa-atlassian:before { + content: "\f77b"; +} + +.fa-linkedin-in:before { + content: "\f0e1"; +} + +.fa-digital-ocean:before { + content: "\f391"; +} + +.fa-nimblr:before { + content: "\f5a8"; +} + +.fa-chromecast:before { + content: "\f838"; +} + +.fa-evernote:before { + content: "\f839"; +} + +.fa-hacker-news:before { + content: "\f1d4"; +} + +.fa-creative-commons-sampling:before { + content: "\f4f0"; +} + +.fa-adversal:before { + content: "\f36a"; +} + +.fa-creative-commons:before { + content: "\f25e"; +} + +.fa-watchman-monitoring:before { + content: "\e087"; +} + +.fa-fonticons:before { + content: "\f280"; +} + +.fa-weixin:before { + content: "\f1d7"; +} + +.fa-shirtsinbulk:before { + content: "\f214"; +} + +.fa-codepen:before { + content: "\f1cb"; +} + +.fa-git-alt:before { + content: "\f841"; +} + +.fa-lyft:before { + content: "\f3c3"; +} + +.fa-rev:before { + content: "\f5b2"; +} + +.fa-windows:before { + content: "\f17a"; +} + +.fa-wizards-of-the-coast:before { + content: "\f730"; +} + +.fa-square-viadeo:before, +.fa-viadeo-square:before { + content: "\f2aa"; +} + +.fa-meetup:before { + content: "\f2e0"; +} + +.fa-centos:before { + content: "\f789"; +} + +.fa-adn:before { + content: "\f170"; +} + +.fa-cloudsmith:before { + content: "\f384"; +} + +.fa-pied-piper-alt:before { + content: "\f1a8"; +} + +.fa-dribbble-square:before, +.fa-square-dribbble:before { + content: "\f397"; +} + +.fa-codiepie:before { + content: "\f284"; +} + +.fa-node:before { + content: "\f419"; +} + +.fa-mix:before { + content: "\f3cb"; +} + +.fa-steam:before { + content: "\f1b6"; +} + +.fa-cc-apple-pay:before { + content: "\f416"; +} + +.fa-scribd:before { + content: "\f28a"; +} + +.fa-openid:before { + content: "\f19b"; +} + +.fa-instalod:before { + content: "\e081"; +} + +.fa-expeditedssl:before { + content: "\f23e"; +} + +.fa-sellcast:before { + content: "\f2da"; +} + +.fa-square-twitter:before, +.fa-twitter-square:before { + content: "\f081"; +} + +.fa-r-project:before { + content: "\f4f7"; +} + +.fa-delicious:before { + content: "\f1a5"; +} + +.fa-freebsd:before { + content: "\f3a4"; +} + +.fa-vuejs:before { + content: "\f41f"; +} + +.fa-accusoft:before { + content: "\f369"; +} + +.fa-ioxhost:before { + content: "\f208"; +} + +.fa-fonticons-fi:before { + content: "\f3a2"; +} + +.fa-app-store:before { + content: "\f36f"; +} + +.fa-cc-mastercard:before { + content: "\f1f1"; +} + +.fa-itunes-note:before { + content: "\f3b5"; +} + +.fa-golang:before { + content: "\e40f"; +} + +.fa-kickstarter:before { + content: "\f3bb"; +} + +.fa-grav:before { + content: "\f2d6"; +} + +.fa-weibo:before { + content: "\f18a"; +} + +.fa-uncharted:before { + content: "\e084"; +} + +.fa-firstdraft:before { + content: "\f3a1"; +} + +.fa-square-youtube:before, +.fa-youtube-square:before { + content: "\f431"; +} + +.fa-wikipedia-w:before { + content: "\f266"; +} + +.fa-rendact:before, +.fa-wpressr:before { + content: "\f3e4"; +} + +.fa-angellist:before { + content: "\f209"; +} + +.fa-galactic-republic:before { + content: "\f50c"; +} + +.fa-nfc-directional:before { + content: "\e530"; +} + +.fa-skype:before { + content: "\f17e"; +} + +.fa-joget:before { + content: "\f3b7"; +} + +.fa-fedora:before { + content: "\f798"; +} + +.fa-stripe-s:before { + content: "\f42a"; +} + +.fa-meta:before { + content: "\e49b"; +} + +.fa-laravel:before { + content: "\f3bd"; +} + +.fa-hotjar:before { + content: "\f3b1"; +} + +.fa-bluetooth-b:before { + content: "\f294"; +} + +.fa-sticker-mule:before { + content: "\f3f7"; +} + +.fa-creative-commons-zero:before { + content: "\f4f3"; +} + +.fa-hips:before { + content: "\f452"; +} + +.fa-behance:before { + content: "\f1b4"; +} + +.fa-reddit:before { + content: "\f1a1"; +} + +.fa-discord:before { + content: "\f392"; +} + +.fa-chrome:before { + content: "\f268"; +} + +.fa-app-store-ios:before { + content: "\f370"; +} + +.fa-cc-discover:before { + content: "\f1f2"; +} + +.fa-wpbeginner:before { + content: "\f297"; +} + +.fa-confluence:before { + content: "\f78d"; +} + +.fa-mdb:before { + content: "\f8ca"; +} + +.fa-dochub:before { + content: "\f394"; +} + +.fa-accessible-icon:before { + content: "\f368"; +} + +.fa-ebay:before { + content: "\f4f4"; +} + +.fa-amazon:before { + content: "\f270"; +} + +.fa-unsplash:before { + content: "\e07c"; +} + +.fa-yarn:before { + content: "\f7e3"; +} + +.fa-square-steam:before, +.fa-steam-square:before { + content: "\f1b7"; +} + +.fa-500px:before { + content: "\f26e"; +} + +.fa-square-vimeo:before, +.fa-vimeo-square:before { + content: "\f194"; +} + +.fa-asymmetrik:before { + content: "\f372"; +} + +.fa-font-awesome-flag:before, +.fa-font-awesome-logo-full:before, +.fa-font-awesome:before { + content: "\f2b4"; +} + +.fa-gratipay:before { + content: "\f184"; +} + +.fa-apple:before { + content: "\f179"; +} + +.fa-hive:before { + content: "\e07f"; +} + +.fa-gitkraken:before { + content: "\f3a6"; +} + +.fa-keybase:before { + content: "\f4f5"; +} + +.fa-apple-pay:before { + content: "\f415"; +} + +.fa-padlet:before { + content: "\e4a0"; +} + +.fa-amazon-pay:before { + content: "\f42c"; +} + +.fa-github-square:before, +.fa-square-github:before { + content: "\f092"; +} + +.fa-stumbleupon:before { + content: "\f1a4"; +} + +.fa-fedex:before { + content: "\f797"; +} + +.fa-phoenix-framework:before { + content: "\f3dc"; +} + +.fa-shopify:before { + content: "\e057"; +} + +.fa-neos:before { + content: "\f612"; +} + +.fa-hackerrank:before { + content: "\f5f7"; +} + +.fa-researchgate:before { + content: "\f4f8"; +} + +.fa-swift:before { + content: "\f8e1"; +} + +.fa-angular:before { + content: "\f420"; +} + +.fa-speakap:before { + content: "\f3f3"; +} + +.fa-angrycreative:before { + content: "\f36e"; +} + +.fa-y-combinator:before { + content: "\f23b"; +} + +.fa-empire:before { + content: "\f1d1"; +} + +.fa-envira:before { + content: "\f299"; +} + +.fa-gitlab-square:before, +.fa-square-gitlab:before { + content: "\e5ae"; +} + +.fa-studiovinari:before { + content: "\f3f8"; +} + +.fa-pied-piper:before { + content: "\f2ae"; +} + +.fa-wordpress:before { + content: "\f19a"; +} + +.fa-product-hunt:before { + content: "\f288"; +} + +.fa-firefox:before { + content: "\f269"; +} + +.fa-linode:before { + content: "\f2b8"; +} + +.fa-goodreads:before { + content: "\f3a8"; +} + +.fa-odnoklassniki-square:before, +.fa-square-odnoklassniki:before { + content: "\f264"; +} + +.fa-jsfiddle:before { + content: "\f1cc"; +} + +.fa-sith:before { + content: "\f512"; +} + +.fa-themeisle:before { + content: "\f2b2"; +} + +.fa-page4:before { + content: "\f3d7"; +} + +.fa-hashnode:before { + content: "\e499"; +} + +.fa-react:before { + content: "\f41b"; +} + +.fa-cc-paypal:before { + content: "\f1f4"; +} + +.fa-squarespace:before { + content: "\f5be"; +} + +.fa-cc-stripe:before { + content: "\f1f5"; +} + +.fa-creative-commons-share:before { + content: "\f4f2"; +} + +.fa-bitcoin:before { + content: "\f379"; +} + +.fa-keycdn:before { + content: "\f3ba"; +} + +.fa-opera:before { + content: "\f26a"; +} + +.fa-itch-io:before { + content: "\f83a"; +} + +.fa-umbraco:before { + content: "\f8e8"; +} + +.fa-galactic-senate:before { + content: "\f50d"; +} + +.fa-ubuntu:before { + content: "\f7df"; +} + +.fa-draft2digital:before { + content: "\f396"; +} + +.fa-stripe:before { + content: "\f429"; +} + +.fa-houzz:before { + content: "\f27c"; +} + +.fa-gg:before { + content: "\f260"; +} + +.fa-dhl:before { + content: "\f790"; +} + +.fa-pinterest-square:before, +.fa-square-pinterest:before { + content: "\f0d3"; +} + +.fa-xing:before { + content: "\f168"; +} + +.fa-blackberry:before { + content: "\f37b"; +} + +.fa-creative-commons-pd:before { + content: "\f4ec"; +} + +.fa-playstation:before { + content: "\f3df"; +} + +.fa-quinscape:before { + content: "\f459"; +} + +.fa-less:before { + content: "\f41d"; +} + +.fa-blogger-b:before { + content: "\f37d"; +} + +.fa-opencart:before { + content: "\f23d"; +} + +.fa-vine:before { + content: "\f1ca"; +} + +.fa-paypal:before { + content: "\f1ed"; +} + +.fa-gitlab:before { + content: "\f296"; +} + +.fa-typo3:before { + content: "\f42b"; +} + +.fa-reddit-alien:before { + content: "\f281"; +} + +.fa-yahoo:before { + content: "\f19e"; +} + +.fa-dailymotion:before { + content: "\e052"; +} + +.fa-affiliatetheme:before { + content: "\f36b"; +} + +.fa-pied-piper-pp:before { + content: "\f1a7"; +} + +.fa-bootstrap:before { + content: "\f836"; +} + +.fa-odnoklassniki:before { + content: "\f263"; +} + +.fa-nfc-symbol:before { + content: "\e531"; +} + +.fa-ethereum:before { + content: "\f42e"; +} + +.fa-speaker-deck:before { + content: "\f83c"; +} + +.fa-creative-commons-nc-eu:before { + content: "\f4e9"; +} + +.fa-patreon:before { + content: "\f3d9"; +} + +.fa-avianex:before { + content: "\f374"; +} + +.fa-ello:before { + content: "\f5f1"; +} + +.fa-gofore:before { + content: "\f3a7"; +} + +.fa-bimobject:before { + content: "\f378"; +} + +.fa-facebook-f:before { + content: "\f39e"; +} + +.fa-google-plus-square:before, +.fa-square-google-plus:before { + content: "\f0d4"; +} + +.fa-mandalorian:before { + content: "\f50f"; +} + +.fa-first-order-alt:before { + content: "\f50a"; +} + +.fa-osi:before { + content: "\f41a"; +} + +.fa-google-wallet:before { + content: "\f1ee"; +} + +.fa-d-and-d-beyond:before { + content: "\f6ca"; +} + +.fa-periscope:before { + content: "\f3da"; +} + +.fa-fulcrum:before { + content: "\f50b"; +} + +.fa-cloudscale:before { + content: "\f383"; +} + +.fa-forumbee:before { + content: "\f211"; +} + +.fa-mizuni:before { + content: "\f3cc"; +} + +.fa-schlix:before { + content: "\f3ea"; +} + +.fa-square-xing:before, +.fa-xing-square:before { + content: "\f169"; +} + +.fa-bandcamp:before { + content: "\f2d5"; +} + +.fa-wpforms:before { + content: "\f298"; +} + +.fa-cloudversify:before { + content: "\f385"; +} + +.fa-usps:before { + content: "\f7e1"; +} + +.fa-megaport:before { + content: "\f5a3"; +} + +.fa-magento:before { + content: "\f3c4"; +} + +.fa-spotify:before { + content: "\f1bc"; +} + +.fa-optin-monster:before { + content: "\f23c"; +} + +.fa-fly:before { + content: "\f417"; +} + +.fa-aviato:before { + content: "\f421"; +} + +.fa-itunes:before { + content: "\f3b4"; +} + +.fa-cuttlefish:before { + content: "\f38c"; +} + +.fa-blogger:before { + content: "\f37c"; +} + +.fa-flickr:before { + content: "\f16e"; +} + +.fa-viber:before { + content: "\f409"; +} + +.fa-soundcloud:before { + content: "\f1be"; +} + +.fa-digg:before { + content: "\f1a6"; +} + +.fa-tencent-weibo:before { + content: "\f1d5"; +} + +.fa-symfony:before { + content: "\f83d"; +} + +.fa-maxcdn:before { + content: "\f136"; +} + +.fa-etsy:before { + content: "\f2d7"; +} + +.fa-facebook-messenger:before { + content: "\f39f"; +} + +.fa-audible:before { + content: "\f373"; +} + +.fa-think-peaks:before { + content: "\f731"; +} + +.fa-bilibili:before { + content: "\e3d9"; +} + +.fa-erlang:before { + content: "\f39d"; +} + +.fa-cotton-bureau:before { + content: "\f89e"; +} + +.fa-dashcube:before { + content: "\f210"; +} + +.fa-42-group:before, +.fa-innosoft:before { + content: "\e080"; +} + +.fa-stack-exchange:before { + content: "\f18d"; +} + +.fa-elementor:before { + content: "\f430"; +} + +.fa-pied-piper-square:before, +.fa-square-pied-piper:before { + content: "\e01e"; +} + +.fa-creative-commons-nd:before { + content: "\f4eb"; +} + +.fa-palfed:before { + content: "\f3d8"; +} + +.fa-superpowers:before { + content: "\f2dd"; +} + +.fa-resolving:before { + content: "\f3e7"; +} + +.fa-xbox:before { + content: "\f412"; +} + +.fa-searchengin:before { + content: "\f3eb"; +} + +.fa-tiktok:before { + content: "\e07b"; +} + +.fa-facebook-square:before, +.fa-square-facebook:before { + content: "\f082"; +} + +.fa-renren:before { + content: "\f18b"; +} + +.fa-linux:before { + content: "\f17c"; +} + +.fa-glide:before { + content: "\f2a5"; +} + +.fa-linkedin:before { + content: "\f08c"; +} + +.fa-hubspot:before { + content: "\f3b2"; +} + +.fa-deploydog:before { + content: "\f38e"; +} + +.fa-twitch:before { + content: "\f1e8"; +} + +.fa-ravelry:before { + content: "\f2d9"; +} + +.fa-mixer:before { + content: "\e056"; +} + +.fa-lastfm-square:before, +.fa-square-lastfm:before { + content: "\f203"; +} + +.fa-vimeo:before { + content: "\f40a"; +} + +.fa-mendeley:before { + content: "\f7b3"; +} + +.fa-uniregistry:before { + content: "\f404"; +} + +.fa-figma:before { + content: "\f799"; +} + +.fa-creative-commons-remix:before { + content: "\f4ee"; +} + +.fa-cc-amazon-pay:before { + content: "\f42d"; +} + +.fa-dropbox:before { + content: "\f16b"; +} + +.fa-instagram:before { + content: "\f16d"; +} + +.fa-cmplid:before { + content: "\e360"; +} + +.fa-facebook:before { + content: "\f09a"; +} + +.fa-gripfire:before { + content: "\f3ac"; +} + +.fa-jedi-order:before { + content: "\f50e"; +} + +.fa-uikit:before { + content: "\f403"; +} + +.fa-fort-awesome-alt:before { + content: "\f3a3"; +} + +.fa-phabricator:before { + content: "\f3db"; +} + +.fa-ussunnah:before { + content: "\f407"; +} + +.fa-earlybirds:before { + content: "\f39a"; +} + +.fa-trade-federation:before { + content: "\f513"; +} + +.fa-autoprefixer:before { + content: "\f41c"; +} + +.fa-whatsapp:before { + content: "\f232"; +} + +.fa-slideshare:before { + content: "\f1e7"; +} + +.fa-google-play:before { + content: "\f3ab"; +} + +.fa-viadeo:before { + content: "\f2a9"; +} + +.fa-line:before { + content: "\f3c0"; +} + +.fa-google-drive:before { + content: "\f3aa"; +} + +.fa-servicestack:before { + content: "\f3ec"; +} + +.fa-simplybuilt:before { + content: "\f215"; +} + +.fa-bitbucket:before { + content: "\f171"; +} + +.fa-imdb:before { + content: "\f2d8"; +} + +.fa-deezer:before { + content: "\e077"; +} + +.fa-raspberry-pi:before { + content: "\f7bb"; +} + +.fa-jira:before { + content: "\f7b1"; +} + +.fa-docker:before { + content: "\f395"; +} + +.fa-screenpal:before { + content: "\e570"; +} + +.fa-bluetooth:before { + content: "\f293"; +} + +.fa-gitter:before { + content: "\f426"; +} + +.fa-d-and-d:before { + content: "\f38d"; +} + +.fa-microblog:before { + content: "\e01a"; +} + +.fa-cc-diners-club:before { + content: "\f24c"; +} + +.fa-gg-circle:before { + content: "\f261"; +} + +.fa-pied-piper-hat:before { + content: "\f4e5"; +} + +.fa-kickstarter-k:before { + content: "\f3bc"; +} + +.fa-yandex:before { + content: "\f413"; +} + +.fa-readme:before { + content: "\f4d5"; +} + +.fa-html5:before { + content: "\f13b"; +} + +.fa-sellsy:before { + content: "\f213"; +} + +.fa-sass:before { + content: "\f41e"; +} + +.fa-wirsindhandwerk:before, +.fa-wsh:before { + content: "\e2d0"; +} + +.fa-buromobelexperte:before { + content: "\f37f"; +} + +.fa-salesforce:before { + content: "\f83b"; +} + +.fa-octopus-deploy:before { + content: "\e082"; +} + +.fa-medapps:before { + content: "\f3c6"; +} + +.fa-ns8:before { + content: "\f3d5"; +} + +.fa-pinterest-p:before { + content: "\f231"; +} + +.fa-apper:before { + content: "\f371"; +} + +.fa-fort-awesome:before { + content: "\f286"; +} + +.fa-waze:before { + content: "\f83f"; +} + +.fa-cc-jcb:before { + content: "\f24b"; +} + +.fa-snapchat-ghost:before, +.fa-snapchat:before { + content: "\f2ab"; +} + +.fa-fantasy-flight-games:before { + content: "\f6dc"; +} + +.fa-rust:before { + content: "\e07a"; +} + +.fa-wix:before { + content: "\f5cf"; +} + +.fa-behance-square:before, +.fa-square-behance:before { + content: "\f1b5"; +} + +.fa-supple:before { + content: "\f3f9"; +} + +.fa-rebel:before { + content: "\f1d0"; +} + +.fa-css3:before { + content: "\f13c"; +} + +.fa-staylinked:before { + content: "\f3f5"; +} + +.fa-kaggle:before { + content: "\f5fa"; +} + +.fa-space-awesome:before { + content: "\e5ac"; +} + +.fa-deviantart:before { + content: "\f1bd"; +} + +.fa-cpanel:before { + content: "\f388"; +} + +.fa-goodreads-g:before { + content: "\f3a9"; +} + +.fa-git-square:before, +.fa-square-git:before { + content: "\f1d2"; +} + +.fa-square-tumblr:before, +.fa-tumblr-square:before { + content: "\f174"; +} + +.fa-trello:before { + content: "\f181"; +} + +.fa-creative-commons-nc-jp:before { + content: "\f4ea"; +} + +.fa-get-pocket:before { + content: "\f265"; +} + +.fa-perbyte:before { + content: "\e083"; +} + +.fa-grunt:before { + content: "\f3ad"; +} + +.fa-weebly:before { + content: "\f5cc"; +} + +.fa-connectdevelop:before { + content: "\f20e"; +} + +.fa-leanpub:before { + content: "\f212"; +} + +.fa-black-tie:before { + content: "\f27e"; +} + +.fa-themeco:before { + content: "\f5c6"; +} + +.fa-python:before { + content: "\f3e2"; +} + +.fa-android:before { + content: "\f17b"; +} + +.fa-bots:before { + content: "\e340"; +} + +.fa-free-code-camp:before { + content: "\f2c5"; +} + +.fa-hornbill:before { + content: "\f592"; +} + +.fa-js:before { + content: "\f3b8"; +} + +.fa-ideal:before { + content: "\e013"; +} + +.fa-git:before { + content: "\f1d3"; +} + +.fa-dev:before { + content: "\f6cc"; +} + +.fa-sketch:before { + content: "\f7c6"; +} + +.fa-yandex-international:before { + content: "\f414"; +} + +.fa-cc-amex:before { + content: "\f1f3"; +} + +.fa-uber:before { + content: "\f402"; +} + +.fa-github:before { + content: "\f09b"; +} + +.fa-php:before { + content: "\f457"; +} + +.fa-alipay:before { + content: "\f642"; +} + +.fa-youtube:before { + content: "\f167"; +} + +.fa-skyatlas:before { + content: "\f216"; +} + +.fa-firefox-browser:before { + content: "\e007"; +} + +.fa-replyd:before { + content: "\f3e6"; +} + +.fa-suse:before { + content: "\f7d6"; +} + +.fa-jenkins:before { + content: "\f3b6"; +} + +.fa-twitter:before { + content: "\f099"; +} + +.fa-rockrms:before { + content: "\f3e9"; +} + +.fa-pinterest:before { + content: "\f0d2"; +} + +.fa-buffer:before { + content: "\f837"; +} + +.fa-npm:before { + content: "\f3d4"; +} + +.fa-yammer:before { + content: "\f840"; +} + +.fa-btc:before { + content: "\f15a"; +} + +.fa-dribbble:before { + content: "\f17d"; +} + +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} + +.fa-internet-explorer:before { + content: "\f26b"; +} + +.fa-telegram-plane:before, +.fa-telegram:before { + content: "\f2c6"; +} + +.fa-old-republic:before { + content: "\f510"; +} + +.fa-square-whatsapp:before, +.fa-whatsapp-square:before { + content: "\f40c"; +} + +.fa-node-js:before { + content: "\f3d3"; +} + +.fa-edge-legacy:before { + content: "\e078"; +} + +.fa-slack-hash:before, +.fa-slack:before { + content: "\f198"; +} + +.fa-medrt:before { + content: "\f3c8"; +} + +.fa-usb:before { + content: "\f287"; +} + +.fa-tumblr:before { + content: "\f173"; +} + +.fa-vaadin:before { + content: "\f408"; +} + +.fa-quora:before { + content: "\f2c4"; +} + +.fa-reacteurope:before { + content: "\f75d"; +} + +.fa-medium-m:before, +.fa-medium:before { + content: "\f23a"; +} + +.fa-amilia:before { + content: "\f36d"; +} + +.fa-mixcloud:before { + content: "\f289"; +} + +.fa-flipboard:before { + content: "\f44d"; +} + +.fa-viacoin:before { + content: "\f237"; +} + +.fa-critical-role:before { + content: "\f6c9"; +} + +.fa-sitrox:before { + content: "\e44a"; +} + +.fa-discourse:before { + content: "\f393"; +} + +.fa-joomla:before { + content: "\f1aa"; +} + +.fa-mastodon:before { + content: "\f4f6"; +} + +.fa-airbnb:before { + content: "\f834"; +} + +.fa-wolf-pack-battalion:before { + content: "\f514"; +} + +.fa-buy-n-large:before { + content: "\f8a6"; +} + +.fa-gulp:before { + content: "\f3ae"; +} + +.fa-creative-commons-sampling-plus:before { + content: "\f4f1"; +} + +.fa-strava:before { + content: "\f428"; +} + +.fa-ember:before { + content: "\f423"; +} + +.fa-canadian-maple-leaf:before { + content: "\f785"; +} + +.fa-teamspeak:before { + content: "\f4f9"; +} + +.fa-pushed:before { + content: "\f3e1"; +} + +.fa-wordpress-simple:before { + content: "\f411"; +} + +.fa-nutritionix:before { + content: "\f3d6"; +} + +.fa-wodu:before { + content: "\e088"; +} + +.fa-google-pay:before { + content: "\e079"; +} + +.fa-intercom:before { + content: "\f7af"; +} + +.fa-zhihu:before { + content: "\f63f"; +} + +.fa-korvue:before { + content: "\f42f"; +} + +.fa-pix:before { + content: "\e43a"; +} + +.fa-steam-symbol:before { + content: "\f3f6"; +} + +:host, +:root { + --fa-font-regular: normal 400 1em/1 "Font Awesome 6 Free"; +} + +@font-face { + font-family: "Font Awesome 6 Free"; + font-style: normal; + font-weight: 400; + font-display: block; + src: + url(./webfonts/fa-regular-400.woff2) format("woff2"), + url(./webfonts/fa-regular-400.ttf) format("truetype"); +} + +.fa-regular, +.far { + font-weight: 400; +} + +:host, +:root { + --fa-style-family-classic: "Font Awesome 6 Free"; + --fa-font-solid: normal 900 1em/1 "Font Awesome 6 Free"; +} + +@font-face { + font-family: "Font Awesome 6 Free"; + font-style: normal; + font-weight: 900; + font-display: block; + src: + url(./webfonts/fa-solid-900.woff2) format("woff2"), + url(./webfonts/fa-solid-900.ttf) format("truetype"); +} + +.fa-solid, +.fas { + font-weight: 900; +} + +@font-face { + font-family: "Font Awesome 5 Brands"; + font-display: block; + font-weight: 400; + src: + url(./webfonts/fa-brands-400.woff2) format("woff2"), + url(./webfonts/fa-brands-400.ttf) format("truetype"); +} + +@font-face { + font-family: "Font Awesome 5 Free"; + font-display: block; + font-weight: 900; + src: + url(./webfonts/fa-solid-900.woff2) format("woff2"), + url(./webfonts/fa-solid-900.ttf) format("truetype"); +} + +@font-face { + font-family: "Font Awesome 5 Free"; + font-display: block; + font-weight: 400; + src: + url(./webfonts/fa-regular-400.woff2) format("woff2"), + url(./webfonts/fa-regular-400.ttf) format("truetype"); +} + +@font-face { + font-family: "FontAwesome"; + font-display: block; + src: + url(./webfonts/fa-solid-900.woff2) format("woff2"), + url(./webfonts/fa-solid-900.ttf) format("truetype"); +} + +@font-face { + font-family: "FontAwesome"; + font-display: block; + src: + url(./webfonts/fa-brands-400.woff2) format("woff2"), + url(./webfonts/fa-brands-400.ttf) format("truetype"); +} + +@font-face { + font-family: "FontAwesome"; + font-display: block; + src: + url(./webfonts/fa-regular-400.woff2) format("woff2"), + url(./webfonts/fa-regular-400.ttf) format("truetype"); + unicode-range: u+f003, u+f006, u+f014, u+f016-f017, u+f01a-f01b, u+f01d, u+f022, u+f03e, u+f044, u+f046, u+f05c-f05d, u+f06e, u+f070, u+f087-f088, u+f08a, + u+f094, u+f096-f097, u+f09d, u+f0a0, u+f0a2, u+f0a4-f0a7, u+f0c5, u+f0c7, u+f0e5-f0e6, u+f0eb, u+f0f6-f0f8, u+f10c, u+f114-f115, u+f118-f11a, u+f11c-f11d, + u+f133, u+f147, u+f14e, u+f150-f152, u+f185-f186, u+f18e, u+f190-f192, u+f196, u+f1c1-f1c9, u+f1d9, u+f1db, u+f1e3, u+f1ea, u+f1f7, u+f1f9, u+f20a, + u+f247-f248, u+f24a, u+f24d, u+f255-f25b, u+f25d, u+f271-f274, u+f278, u+f27b, u+f28c, u+f28e, u+f29c, u+f2b5, u+f2b7, u+f2ba, u+f2bc, u+f2be, u+f2c0-f2c1, + u+f2c3, u+f2d0, u+f2d2, u+f2d4, u+f2dc; +} + +@font-face { + font-family: "FontAwesome"; + font-display: block; + src: + url(./webfonts/fa-v4compatibility.woff2) format("woff2"), + url(./webfonts/fa-v4compatibility.ttf) format("truetype"); + unicode-range: u+f041, u+f047, u+f065-f066, u+f07d-f07e, u+f080, u+f08b, u+f08e, u+f090, u+f09a, u+f0ac, u+f0ae, u+f0b2, u+f0d0, u+f0d6, u+f0e4, u+f0ec, + u+f10a-f10b, u+f123, u+f13e, u+f148-f149, u+f14c, u+f156, u+f15e, u+f160-f161, u+f163, u+f175-f178, u+f195, u+f1f8, u+f219, u+f27a; +} diff --git a/static/styles/proxima.css b/static/styles/proxima.css new file mode 100644 index 0000000..123ae36 --- /dev/null +++ b/static/styles/proxima.css @@ -0,0 +1,54 @@ +@font-face { + font-family: "Ubiquity Nova"; + font-style: normal; + font-weight: 100; + src: url(./proxima/2B04A9_A_0.eot); + src: + url(./proxima/2B04A9_A_0.eot#iefix) format("embedded-opentype"), + url(./proxima/2B04A9_A_0.woff) format("woff"), + url(./proxima/2B04A9_A_0.ttf) format("truetype"); +} + +@font-face { + font-family: "Ubiquity Nova"; + font-style: normal; + font-weight: 200; + src: url(./proxima/2B04A9_B_0.eot); + src: + url(./proxima/2B04A9_B_0.eot#iefix) format("embedded-opentype"), + url(./proxima/2B04A9_B_0.woff) format("woff"), + url(./proxima/2B04A9_B_0.ttf) format("truetype"); +} + +@font-face { + font-family: "Ubiquity Nova"; + font-style: normal; + font-weight: 400; + src: url(./proxima/2B04A9_E_0.eot); + src: + url(./proxima/2B04A9_E_0.eot#iefix) format("embedded-opentype"), + url(./proxima/2B04A9_E_0.woff) format("woff"), + url(./proxima/2B04A9_E_0.ttf) format("truetype"); +} + +@font-face { + font-family: "Ubiquity Nova"; + font-style: italic; + font-weight: 400; + src: url(./proxima/2B04A9_C_0.eot); + src: + url(./proxima/2B04A9_C_0.eot#iefix) format("embedded-opentype"), + url(./proxima/2B04A9_C_0.woff) format("woff"), + url(./proxima/2B04A9_C_0.ttf) format("truetype"); +} + +@font-face { + font-family: "Ubiquity Nova"; + font-style: normal; + font-weight: 700; + src: url(./proxima/2B04A9_D_0.eot); + src: + url(./proxima/2B04A9_D_0.eot#iefix) format("embedded-opentype"), + url(./proxima/2B04A9_D_0.woff) format("woff"), + url(./proxima/2B04A9_D_0.ttf) format("truetype"); +} diff --git a/static/styles/proxima/2B04A9_A_0.eot b/static/styles/proxima/2B04A9_A_0.eot new file mode 100644 index 0000000..736c1c0 Binary files /dev/null and b/static/styles/proxima/2B04A9_A_0.eot differ diff --git a/static/styles/proxima/2B04A9_A_0.ttf b/static/styles/proxima/2B04A9_A_0.ttf new file mode 100644 index 0000000..3702fd6 Binary files /dev/null and b/static/styles/proxima/2B04A9_A_0.ttf differ diff --git a/static/styles/proxima/2B04A9_A_0.woff b/static/styles/proxima/2B04A9_A_0.woff new file mode 100644 index 0000000..b8e7802 Binary files /dev/null and b/static/styles/proxima/2B04A9_A_0.woff differ diff --git a/static/styles/proxima/2B04A9_B_0.eot b/static/styles/proxima/2B04A9_B_0.eot new file mode 100644 index 0000000..4f493c5 Binary files /dev/null and b/static/styles/proxima/2B04A9_B_0.eot differ diff --git a/static/styles/proxima/2B04A9_B_0.ttf b/static/styles/proxima/2B04A9_B_0.ttf new file mode 100644 index 0000000..48faa7a Binary files /dev/null and b/static/styles/proxima/2B04A9_B_0.ttf differ diff --git a/static/styles/proxima/2B04A9_B_0.woff b/static/styles/proxima/2B04A9_B_0.woff new file mode 100644 index 0000000..b022624 Binary files /dev/null and b/static/styles/proxima/2B04A9_B_0.woff differ diff --git a/static/styles/proxima/2B04A9_C_0.eot b/static/styles/proxima/2B04A9_C_0.eot new file mode 100644 index 0000000..940cd93 Binary files /dev/null and b/static/styles/proxima/2B04A9_C_0.eot differ diff --git a/static/styles/proxima/2B04A9_C_0.ttf b/static/styles/proxima/2B04A9_C_0.ttf new file mode 100644 index 0000000..815bc3e Binary files /dev/null and b/static/styles/proxima/2B04A9_C_0.ttf differ diff --git a/static/styles/proxima/2B04A9_C_0.woff b/static/styles/proxima/2B04A9_C_0.woff new file mode 100644 index 0000000..643e354 Binary files /dev/null and b/static/styles/proxima/2B04A9_C_0.woff differ diff --git a/static/styles/proxima/2B04A9_D_0.eot b/static/styles/proxima/2B04A9_D_0.eot new file mode 100644 index 0000000..10e8880 Binary files /dev/null and b/static/styles/proxima/2B04A9_D_0.eot differ diff --git a/static/styles/proxima/2B04A9_D_0.ttf b/static/styles/proxima/2B04A9_D_0.ttf new file mode 100644 index 0000000..2779c93 Binary files /dev/null and b/static/styles/proxima/2B04A9_D_0.ttf differ diff --git a/static/styles/proxima/2B04A9_D_0.woff b/static/styles/proxima/2B04A9_D_0.woff new file mode 100644 index 0000000..a3219db Binary files /dev/null and b/static/styles/proxima/2B04A9_D_0.woff differ diff --git a/static/styles/proxima/2B04A9_E_0.eot b/static/styles/proxima/2B04A9_E_0.eot new file mode 100644 index 0000000..1e46099 Binary files /dev/null and b/static/styles/proxima/2B04A9_E_0.eot differ diff --git a/static/styles/proxima/2B04A9_E_0.ttf b/static/styles/proxima/2B04A9_E_0.ttf new file mode 100644 index 0000000..d953486 Binary files /dev/null and b/static/styles/proxima/2B04A9_E_0.ttf differ diff --git a/static/styles/proxima/2B04A9_E_0.woff b/static/styles/proxima/2B04A9_E_0.woff new file mode 100644 index 0000000..0c2691e Binary files /dev/null and b/static/styles/proxima/2B04A9_E_0.woff differ diff --git a/static/styles/rewards/background.css b/static/styles/rewards/background.css new file mode 100644 index 0000000..7125a18 --- /dev/null +++ b/static/styles/rewards/background.css @@ -0,0 +1,21 @@ +background, +background #grid { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; +} +html, +background { + background-color: #000410; +} + +background #grid { + -webkit-mask-image: radial-gradient(#00000020 0, #00000080 100%); + mask-image: radial-gradient(#00000020 0, #00000080 100%); + /* opacity: 1; */ + pointer-events: none; + /* background-image: url(../../media/grid-1.png); */ + /* background-repeat: repeat; */ +} diff --git a/static/styles/rewards/claim-table.css b/static/styles/rewards/claim-table.css new file mode 100644 index 0000000..121a462 --- /dev/null +++ b/static/styles/rewards/claim-table.css @@ -0,0 +1,331 @@ +:root { + --left-table-column-width: 120px; + + --background-color-default-brightness: 3%; + --background-color-light-brightness: 6%; + --border-brightness: 9%; + + --background-color-default: hsl(225 50% var(--background-color-default-brightness) / 1); + --background-color-light: hsl(225 50% var(--background-color-light-brightness) / 1); + --border-color: hsl(225 25% var(--border-brightness) / 1); +} +#claimButton { + display: flex; + justify-content: center; + align-items: center; +} +main > div { + /* border-collapse: collapse; */ + /* width: 100%; */ + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: center; +} +table { + border-collapse: collapse; + width: 100vw; + max-width: 640px; + background-color: var(--background-color-default); + border: 1px solid var(--border-color); +} +table tr { + /* width: 100%; */ + /* border: 1px solid; */ +} + +table a:hover, +table a:hover > div { + color: #fff; +} +table #controls { + display: none; + color: #fff; +} +table #controls > button { + flex: 1; + padding: 0 24px; +} +table aside { + text-transform: uppercase; + font-weight: 100; + letter-spacing: 2px; + font-size: 16px; + color: grey; + display: inline; + vertical-align: middle; + text-align: center; + padding: 16px; +} +table button { + user-select: none; + appearance: none; + border: none; + background: 0 0; + cursor: pointer; + outline: 0; + height: 48px; + color: currentColor; +} +table button div { + text-transform: uppercase; + /* white-space: nowrap; */ + word-break: break-all; + text-rendering: geometricPrecision; + color: #fff; +} +table hr { + background-color: #80808040; + height: 1px; + border: none; +} +table svg path { + fill: currentColor; +} +table td div { + word-break: break-all; +} +table th div { + text-align: right; + color: #80808080; +} +table td, +table th { + padding: 8px; + text-rendering: geometricPrecision; + /* max-width: 320px; */ + line-height: 1.25; + color: #808080; +} +table td div { + /* width: calc(100vw - var(--left-table-column-width)); */ + /* white-space: nowrap; */ + word-break: break-all; + overflow: hidden; + text-overflow: ellipsis; +} +table td div svg ~ div { + width: unset; +} +table tr:first-of-type > * { + padding-top: 24px; +} +table tr:last-child td div > div { + width: 50%; + display: inline-block; + vertical-align: middle; +} +table tr:last-of-type > * { + padding-bottom: 24px; +} +table tr#additional-details-border > * { + padding: 0; + margin: 0; +} +table[data-claim-rendered] { + /* border: 1px solid #80808020; */ + /* border-right: 0; */ + /* border-left: 0; */ +} +table[data-claim-rendered] #controls { + display: flex; +} +table[data-claim-rendered] button { + opacity: 0.5; +} +table[data-claim-rendered] button:disabled { + opacity: 0.5; + pointer-events: none; +} +table[data-claim-rendered] button:hover { + background-color: #80808020; + opacity: 1; +} +table[data-claim-rendered] button > div { + display: none; +} +table[data-claim-rendered] button:disabled > svg.claim-icon { + display: none; +} +table[data-claim-rendered] button:hover > div { + display: unset; + color: #fff; +} +table[data-claim-rendered] button:hover > svg { + display: none !important; +} +table[data-claim-rendered] button#additionalDetails { + width: 100%; + color: #fff; + /* opacity: 0.25; */ +} +table[data-claim-rendered] button#additionalDetails:hover { + /* opacity: 1; */ +} +table[data-claim-rendered] tr { + background-color: #80808000; + transition: 125ms ease-in-out opacity; +} +table[data-claim-rendered] tr#additional-details-border { + /* background-color: #80808010; */ + background-color: var(--background-color-light); + /* -webkit-backdrop-filter: blur(4px); */ + /* backdrop-filter: blur(4px); */ + /* border: 1px solid #80808020; */ + /* border-left: 0; */ + /* border-right: 0; */ +} +table[data-claim-rendered] tr#additional-details-border + tr > * { + padding-top: 24px; +} +table #additionalDetailsTable tr, +table[data-details-visible="false"] #additionalDetailsTable tr { + opacity: 0; + pointer-events: none; + display: none; +} +table[data-details-visible="false"] { + /* border-bottom: 1px solid #80808000; */ +} +table[data-details-visible="true"] #additionalDetailsTable tr { + opacity: 1; + pointer-events: unset; + display: table-row; + background-color: #80808000; + /* -webkit-backdrop-filter: blur(4px); */ + /* backdrop-filter: blur(4px); */ +} +table[data-details-visible="true"] { + /* border-bottom: 1px solid #80808020; */ +} +table[data-contract-loaded] #Token { + display: none; +} +#rewardAmount a { + font-size: 24px; +} +#rewardAmount a, +#rewardRecipient div, +#rewardsCount { + color: #fff; +} +#nextTx, +#previousTx { + fill: #fff; +} +table[data-details-visible="false"] #rewardToken .full, +table[data-details-visible="true"] #rewardToken .short { + display: none; +} +table[data-details-visible="false"] #rewardToken .short, +table[data-details-visible="true"] #rewardToken .full { + display: initial; +} +table[data-details-visible="false"] #rewardRecipient .full, +table[data-details-visible="true"] #rewardRecipient .short { + display: none; +} +table[data-details-visible="false"] #rewardRecipient .short, +table[data-details-visible="true"] #rewardRecipient .full { + display: initial; +} +#To > td, +#To > th { + padding-bottom: 24px; +} +table thead { + display: table-row-group; +} +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; +} +#rewardRecipient a:hover div { + opacity: 1; +} +#rewardRecipient div { + 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; + padding: 10px; +} + +.hide-pagination { + display: none; +} + +#additionalDetails svg { + /* display: none; */ +} +table[data-details-visible="true"] #additionalDetails svg.opener { + display: none; +} +table[data-details-visible="false"] #additionalDetails svg.opener { + display: unset; +} + +table[data-details-visible="true"] #additionalDetails svg.closer { + display: unset; +} +table[data-details-visible="false"] #additionalDetails svg.closer { + display: none; +} + +table .full { + /* word-break: break-all; */ +} + +td#owner > a { + word-break: break-all; +} + +table td { + padding-right: 16px; +} +table th { + padding-left: 16px; +} diff --git a/static/styles/rewards/light-mode.css b/static/styles/rewards/light-mode.css new file mode 100644 index 0000000..c148ed7 --- /dev/null +++ b/static/styles/rewards/light-mode.css @@ -0,0 +1,84 @@ +:root { + --light-mode-background-color-default-brightness: calc(100% - var(--background-color-default-brightness)); + --light-mode-background-color-dark-brightness: calc(100% - var(--background-color-light-brightness)); + + --light-mode-background-color-default: hsl(225 0% var(--light-mode-background-color-default-brightness) / 1); + --light-mode-background-color-dark: hsl(225 0% var(--light-mode-background-color-dark-brightness) / 1); + + --light-mode-border-color: hsl(225 0% calc(100% - var(--border-brightness)) / 1); +} +@media (prefers-color-scheme: light) { + background { + /* dark mode styles */ + background-color: #fff; + color: #000; + } + + table a:hover > div { + color: #000; + } + + #rewardAmount > a { + color: #000; + } + + table svg path { + fill: #000; + } + + #logo-icon > svg { + fill: #000; + } + + #logo > div#logo-text > span { + color: #000; + } + + table button div { + color: #000; + } + table[data-claim-rendered] button:hover > div { + color: #000; + } + + table a:hover, + table a:hover > div { + color: #000; + } + + div#build > a { + color: #000; + } + #rewardAmount div, + #rewardRecipient div, + #rewardsCount { + color: #000; + } + #nextTx, + #previousTx { + fill: #000; + } + + html { + background-color: #fff; + } + #grid { + opacity: 0.25; + } + + table { + background-color: var(--light-mode-background-color-default); + border: 1px solid var(--light-mode-border-color); + } + + table[data-claim-rendered] tr#additional-details-border { + background-color: var(--light-mode-background-color-dark); + } + table[data-details-visible="true"] #additional-details-border ~ tr { + background-color: var(--light-mode-background-color-dark); + } + .notifications .toast { + border: 1px solid var(--light-mode-border-color); + background-color: var(--light-mode-background-color-default); + } +} diff --git a/static/styles/rewards/media-queries.css b/static/styles/rewards/media-queries.css new file mode 100644 index 0000000..3cdf401 --- /dev/null +++ b/static/styles/rewards/media-queries.css @@ -0,0 +1,40 @@ +header span:first-child::after { + /* content: " | "; */ +} + +@media screen and (max-width: 640px) { + table { + border-left-width: 0px; + border-right-width: 0px; + } +} +@media screen and (max-width: 768px) { + header span:first-child { + display: none; + } + + header span:first-child::after { + content: ""; + } +} + +/* +@media screen and (min-height: 512px) { + table[data-details-visible="false"] #additional-details-border ~ tr { + display: none; + } + table[data-details-visible="true"] #additional-details-border ~ tr { + display: table-row; + } +} */ + +/* Landscape */ +@media screen and (orientation: landscape) { + body { + width: 100vw; /* 100% of viewport width */ + max-width: 100vw; /* prevents any overflow issues */ + overflow-x: hidden; /* prevents horizontal scrolling */ + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); + } +} diff --git a/static/styles/rewards/pay.css b/static/styles/rewards/pay.css new file mode 100644 index 0000000..76cf477 --- /dev/null +++ b/static/styles/rewards/pay.css @@ -0,0 +1,147 @@ +* { + -webkit-text-size-adjust: 100%; + font-family: + "Ubiquity Nova", + FT Base, + -apple-system, + system-ui, + BlinkMacSystemFont, + SF Pro Text, + Segoe UI, + Roboto, + Helvetica, + Arial, + sans-serif; + line-height: 1; + padding: 0; + margin: 0; + font-size: 12px; + font-weight: 400; + text-transform: uppercase; + letter-spacing: 2px; +} + +a { + color: #808080; +} + +body { + /* min-height: 100vh; */ + /* height: calc(100 * var(--vh)); */ +} + +body, +main { + max-width: 100vw; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +main { + width: 100vw; + position: relative; +} + +a { + text-decoration: none; +} + +#logo a { + display: block; + border: 1px solid transparent; + border-radius: 3px; + flex-direction: row; +} + +#logo { + letter-spacing: 2px; + padding: 48px 0; + -ms-flex-pack: justify; + -ms-flex-align: center; + line-height: 1; + text-align: center; +} + +#logo > #logo-icon { + display: inline-block; + vertical-align: middle; + padding: 4px; + text-rendering: geometricPrecision; + /* color: #fff; */ +} + +#logo-icon > svg { + height: 36px; + display: block; + margin: 0; + width: 36px; + padding: 0; + margin-right: 10px; + fill: #fff; +} + +#logo > div#logo-text { + display: inline-block; + vertical-align: middle; +} + +#logo > div#logo-text > span { + font-size: 20px; + letter-spacing: 5px; + text-transform: uppercase; + text-rendering: geometricPrecision; + color: #fff; + /* font-weight: 400; */ +} + +div#build { + /* bottom: 48px; */ + padding: 48px 0; + text-align: center; + text-transform: uppercase; + letter-spacing: 2px; + text-rendering: geometricPrecision; + /* font-weight: 100; */ + /* width: 48px; */ + margin: auto; +} + +div#build > a { + opacity: 0.25; + font-size: 12px; + color: #fff; +} + +div#build > a:hover { + opacity: 1; +} +header a #logo { + opacity: 0.5; +} +header a:hover #logo { + opacity: 1; +} + +main { + display: flex; + flex-direction: column; + height: 100vh; /* adjust this according to your needs */ +} + +header { + /* height: 140px; */ + /* width: 100%; */ +} + +footer { + /* height: 108px; */ + /* width: 100%; */ + /* padding-bottom: env(safe-area-inset-bottom); */ +} + +canvas { + width: 100vw; + height: 100vh; +} diff --git a/static/styles/rewards/rewards.css b/static/styles/rewards/rewards.css new file mode 100644 index 0000000..707ab0f --- /dev/null +++ b/static/styles/rewards/rewards.css @@ -0,0 +1,8 @@ +@import url("pay.css"); +@import url("background.css"); +@import url("claim-table.css"); +@import url("media-queries.css"); +@import url("../proxima.css"); +@import url("../toast.css"); +@import url("../fa.css"); +@import url("light-mode.css"); diff --git a/static/styles/toast.css b/static/styles/toast.css new file mode 100644 index 0000000..03582f7 --- /dev/null +++ b/static/styles/toast.css @@ -0,0 +1,140 @@ +:root { + --toast-success: hsl(120, 50%, 50%); + --toast-error: hsl(0, 50%, 50%); + --toast-warning: hsl(50, 100%, 50%); + --toast-info: hsl(0, 0%, 50%); +} + +.fa-circle-check { + color: var(--toast-success); +} +.fa-circle-xmark { + color: var(--toast-error); +} +.fa-triangle-exclamation { + color: var(--toast-warning); +} +.fa-circle-info { + color: var(--toast-info); +} + +.notifications { + position: fixed; + bottom: 0; + /* right: 20px; */ +} + +.notifications :where(.toast, .column) { + display: flex; + align-items: center; +} + +.notifications .toast { + position: relative; + overflow: hidden; + list-style: none; + padding: 24px; + margin: 12px; + justify-content: space-between; + animation: show_toast 0.5s ease-in-out; + /* border: 1px solid #80808020; */ + border: 1px solid var(--border-color); + /* backdrop-filter: blur(24px); */ + /* -webkit-backdrop-filter: blur(24px); */ + background-color: var(--background-color-default); +} + +@keyframes show_toast { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.notifications .toast.hide { + animation: hide_toast 0.5s ease-in-out; +} + +@keyframes hide_toast { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.toast::before { + position: absolute; + content: ""; + height: 3px; + width: 100%; + bottom: 0px; + left: 0px; + animation: progress 5s linear forwards; +} + +@keyframes progress { + 100% { + width: 0%; + } +} + +.toast.success::before, +.btn#success { + background: var(--toast-success); +} + +.toast.error::before, +.btn#error { + background: var(--toast-error); +} + +.toast.warning::before, +.btn#warning { + background: var(--toast-warning); +} + +.toast.info::before, +.btn#info { + background: var(--toast-info); +} + +.toast .column i { + font-size: 1.75rem; +} + +.toast.success .column i { + color: var(--toast-success); +} + +.toast.error .column i { + color: var(--toast-error); +} + +.toast.warning .column i { + color: var(--toast-warning); +} + +.toast.info .column i { + color: var(--toast-info); +} + +.toast .column span { + /* font-size: 1.07rem; */ + margin-left: 12px; + color: #808080; + line-height: 1.5; +} + +.toast i:last-child { + color: #80808080; + cursor: pointer; + margin-left: 12px; +} + +.toast i:last-child:hover { + color: #808080; +} diff --git a/static/styles/webfonts/fa-brands-400.ttf b/static/styles/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000..cf6a98f Binary files /dev/null and b/static/styles/webfonts/fa-brands-400.ttf differ diff --git a/static/styles/webfonts/fa-brands-400.woff2 b/static/styles/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000..c740267 Binary files /dev/null and b/static/styles/webfonts/fa-brands-400.woff2 differ diff --git a/static/styles/webfonts/fa-regular-400.ttf b/static/styles/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000..9ef8a37 Binary files /dev/null and b/static/styles/webfonts/fa-regular-400.ttf differ diff --git a/static/styles/webfonts/fa-regular-400.woff2 b/static/styles/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..a865b2f Binary files /dev/null and b/static/styles/webfonts/fa-regular-400.woff2 differ diff --git a/static/styles/webfonts/fa-solid-900.ttf b/static/styles/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000..2b96436 Binary files /dev/null and b/static/styles/webfonts/fa-solid-900.ttf differ diff --git a/static/styles/webfonts/fa-solid-900.woff2 b/static/styles/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000..021d33f Binary files /dev/null and b/static/styles/webfonts/fa-solid-900.woff2 differ diff --git a/static/styles/webfonts/fa-v4compatibility.ttf b/static/styles/webfonts/fa-v4compatibility.ttf new file mode 100644 index 0000000..f07e670 Binary files /dev/null and b/static/styles/webfonts/fa-v4compatibility.ttf differ diff --git a/static/styles/webfonts/fa-v4compatibility.woff2 b/static/styles/webfonts/fa-v4compatibility.woff2 new file mode 100644 index 0000000..6f96a11 Binary files /dev/null and b/static/styles/webfonts/fa-v4compatibility.woff2 differ