diff --git a/build/esbuild-build.ts b/build/esbuild-build.ts index 30d024c6..23d8bb48 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -33,7 +33,7 @@ export const esBuildContext: esbuild.BuildOptions = { ".svg": "dataurl", }, outdir: "static/out", - define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { allNetworkUrls }), + define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], { extraRpcs: allNetworkUrls }), }; esbuild @@ -62,6 +62,5 @@ function createEnvDefines(envVarNames: string[], extras: Record defines[key] = JSON.stringify(extras[key]); } } - defines["extraRpcs"] = JSON.stringify(extraRpcs); return defines; } diff --git a/static/scripts/rewards/helpers.ts b/static/scripts/rewards/helpers.ts index a75d2fb8..c4cd343a 100644 --- a/static/scripts/rewards/helpers.ts +++ b/static/scripts/rewards/helpers.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { Contract, ethers } from "ethers"; import { erc20Abi } from "./abis"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { networkRpcs } from "./constants"; +import { networkRpcs, networkExplorers } from "./constants"; type DataType = { jsonrpc: string; @@ -71,3 +71,20 @@ export async function getOptimalProvider(networkId: number) { ensAddress: "", }); } + +export function getExplorerLinkForTx(networkId: number, hash: string): string { + if (!hash) return "#"; + return `${networkExplorers[networkId]}/tx/${hash}`; +} + +export function shortenTxHash(hash: string | undefined, length = 10): 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; +} diff --git a/static/scripts/rewards/web3/erc20-permit.ts b/static/scripts/rewards/web3/erc20-permit.ts index 014f68fb..ee704d91 100644 --- a/static/scripts/rewards/web3/erc20-permit.ts +++ b/static/scripts/rewards/web3/erc20-permit.ts @@ -1,7 +1,7 @@ import { BigNumber, BigNumberish, ethers } from "ethers"; import { permit2Abi } from "../abis"; import { permit2Address } from "../constants"; -import { getErc20Contract, getOptimalProvider } from "../helpers"; +import { getErc20Contract, getOptimalProvider, getExplorerLinkForTx, shortenTxHash } from "../helpers"; import { Erc20Permit } from "../render-transaction/tx-type"; import { toaster, resetClaimButton, errorToast, loadingClaimButton, claimButton } from "../toaster"; import { renderTransaction } from "../render-transaction/render-transaction"; @@ -9,6 +9,12 @@ import { connectWallet } from "./wallet"; import invalidateButton from "../invalidate-component"; import { JsonRpcProvider } from "@ethersproject/providers"; import { tokens } from "../render-transaction/render-token-symbol"; +import { createClient } from "@supabase/supabase-js"; + +declare const SUPABASE_URL: string; +declare const SUPABASE_ANON_KEY: string; + +const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); export async function fetchTreasury( permit: Erc20Permit, @@ -59,7 +65,8 @@ export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcPr toaster.create("info", `Transaction sent`); const receipt = await tx.wait(); toaster.create("success", `Claim Complete.`); - console.log(receipt.transactionHash); // @TODO: post to database + + await updatePermitTxHash(permit, receipt.transactionHash); claimButton.element.removeEventListener("click", handler); renderTransaction(provider).catch(console.error); @@ -74,6 +81,13 @@ export function claimErc20PermitHandler(permit: Erc20Permit, provider: JsonRpcPr } export async function checkPermitClaimable(permit: Erc20Permit, signer: ethers.providers.JsonRpcSigner | null, provider: JsonRpcProvider) { + const permitHash = await doesPermitHaveTxHash(permit); + if (permitHash !== null) { + const explorerLink = `${shortenTxHash(permitHash)}`; + toaster.create("error", `This reward has already been claimed. Transaction hash: ${explorerLink}`); + return false; + } + const isClaimed = await isNonceClaimed(permit); if (isClaimed) { toaster.create("error", `Your reward for this task has already been claimed or invalidated.`); @@ -182,3 +196,37 @@ export function nonceBitmap(nonce: BigNumberish): { wordPos: BigNumber; bitPos: const bitPos = BigNumber.from(nonce).and(255).toNumber(); return { wordPos, bitPos }; } + +export async function doesPermitHaveTxHash(permit: Erc20Permit): Promise { + const { data, error } = await supabase + .from("permits") + .select("transaction") + // using only nonce in the condition as it's defined unique on db + .eq("nonce", permit.permit.nonce.toString()); + + if (data?.length == 1 && data[0].transaction !== null) { + return data[0].transaction; + } + + if (error !== null) { + console.error(error); + throw error; + } + + return null; +} + +export async function updatePermitTxHash(permit: Erc20Permit, hash: string): Promise { + const { error } = await supabase + .from("permits") + .update({ transaction: hash }) + // using only nonce in the condition as it's defined unique on db + .eq("nonce", permit.permit.nonce.toString()); + + if (error !== null) { + console.error(error); + throw error; + } else { + return; + } +}