From d4e01337ccdccb5537c1c9ff0edd37ec8ff15dd9 Mon Sep 17 00:00:00 2001 From: Nikita Kolmogorov Date: Fri, 5 Jan 2024 10:38:16 -0800 Subject: [PATCH] Add fetchOlderTickets function and OlderTickets component --- src/atoms/lastClaimedTimestamp.ts | 3 + src/components/ClaimDashboard.tsx | 31 +++++++++- src/components/OlderTickets.tsx | 73 ++++++++++++++++++++++ src/components/TicketClaimButton.tsx | 90 ++++++++++++++++++++++++++++ src/helpers/api.ts | 26 +++++++- src/helpers/getDateString.ts | 9 +++ src/models/OlderTicket.ts | 13 ++++ 7 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 src/atoms/lastClaimedTimestamp.ts create mode 100644 src/components/OlderTickets.tsx create mode 100644 src/components/TicketClaimButton.tsx create mode 100644 src/helpers/getDateString.ts create mode 100644 src/models/OlderTicket.ts diff --git a/src/atoms/lastClaimedTimestamp.ts b/src/atoms/lastClaimedTimestamp.ts new file mode 100644 index 0000000..53a2791 --- /dev/null +++ b/src/atoms/lastClaimedTimestamp.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai' + +export default atom | null>(null) diff --git a/src/components/ClaimDashboard.tsx b/src/components/ClaimDashboard.tsx index 6f942f6..d309cce 100644 --- a/src/components/ClaimDashboard.tsx +++ b/src/components/ClaimDashboard.tsx @@ -1,17 +1,20 @@ import { Spam__factory } from '@borodutch/spam-contract' import { ethers } from 'ethers' +import { generateTicket, getOlderTickets } from 'helpers/api' import { useAccount } from 'wagmi' import { useEthersSigner } from 'hooks/useEthers' import { useState } from 'preact/hooks' import GeneratedTicket from 'models/GeneratedTIcket' +import OlderTicket from 'models/OlderTicket' +import OlderTickets from 'components/OlderTickets' import env from 'helpers/env' -import generateTicket from 'helpers/api' export default function ({ signature }: { signature: string }) { const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [success, setSuccess] = useState(false) const [ticket, setTicket] = useState(null) + const [olderTickets, setOlderTickets] = useState(null) const { address } = useAccount() const signer = useEthersSigner() @@ -32,6 +35,22 @@ export default function ({ signature }: { signature: string }) { } } + async function fetchOlderTickets() { + setLoading(true) + setError('') + try { + if (!address) throw new Error('No address found') + const tickets = await getOlderTickets(address, signature) + setOlderTickets(tickets) + } catch (error) { + console.error(error) + setOlderTickets(null) + setError(error instanceof Error ? error.message : `${error}`) + } finally { + setLoading(false) + } + } + async function claimSpam() { setLoading(true) setSuccess(false) @@ -100,11 +119,21 @@ export default function ({ signature }: { signature: string }) { You successfully claimed $SPAM! Check your wallet for details 🚀 )} + {error && ( )} + {olderTickets?.length && } ) } diff --git a/src/components/OlderTickets.tsx b/src/components/OlderTickets.tsx new file mode 100644 index 0000000..6803836 --- /dev/null +++ b/src/components/OlderTickets.tsx @@ -0,0 +1,73 @@ +import { Spam__factory } from '@borodutch/spam-contract' +import { useAccount } from 'wagmi' +import { useAtomValue, useSetAtom } from 'jotai' +import { useEffect } from 'preact/hooks' +import { useEthersSigner } from 'hooks/useEthers' +import OlderTicket from 'models/OlderTicket' +import SuspenseWithError from 'components/SuspenseWithError' +import TicketClaimButton from 'components/TicketClaimButton' +import env from 'helpers/env' +import getDateString from 'helpers/getDateString' +import lastClaimedTimestampAtom from 'atoms/lastClaimedTimestamp' + +function OlderTicketsSuspended({ tickets }: { tickets: OlderTicket[] }) { + const lastClaimedTimestamp = useAtomValue(lastClaimedTimestampAtom) + return ( + <> +

+ {lastClaimedTimestamp + ? `The last time you've claimed spam was at ${getDateString( + lastClaimedTimestamp + )}.` + : "You haven't claimed $SPAM yet!"} +

+
+ + + + + + + + + + {tickets.map((ticket) => ( + + + + + + + ))} + +
FromToAmount
{getDateString(ticket.fromDate)}{getDateString(ticket.toDate)}{ticket.total} + +
+
+ + ) +} + +export default function ({ tickets }: { tickets: OlderTicket[] }) { + const { address } = useAccount() + const signer = useEthersSigner() + + if (!address || !signer) { + return

Doesn't look like there is an address connected!

+ } + const setLastClaimedTimestamp = useSetAtom(lastClaimedTimestampAtom) + useEffect(() => { + if (!address) { + setLastClaimedTimestamp(null) + return + } + const bigintAddress = BigInt(address) + const contract = Spam__factory.connect(env.VITE_CONTRACT, signer) + setLastClaimedTimestamp(contract.lastClaimTimestamps(bigintAddress, 0n)) + }, [address, setLastClaimedTimestamp, signer]) + return ( + + + + ) +} diff --git a/src/components/TicketClaimButton.tsx b/src/components/TicketClaimButton.tsx new file mode 100644 index 0000000..4264e8c --- /dev/null +++ b/src/components/TicketClaimButton.tsx @@ -0,0 +1,90 @@ +import { Spam__factory } from '@borodutch/spam-contract' +import { ethers } from 'ethers' +import { useAccount } from 'wagmi' +import { useAtomValue, useSetAtom } from 'jotai' +import { useEthersSigner } from 'hooks/useEthers' +import { useState } from 'preact/hooks' +import OlderTicket from 'models/OlderTicket' +import env from 'helpers/env' +import lastClaimedTimestampAtom from 'atoms/lastClaimedTimestamp' + +function evenPad(value: string) { + return value.length % 2 === 0 ? value : `0${value}` +} + +function turnIntoBytes(value: bigint) { + return ethers.getBytes( + ethers.zeroPadValue(`0x${evenPad(value.toString(16))}`, 32) + ) +} + +export default function ({ + ticket, +}: { + ticket: OlderTicket + refreshClaimTimestamp: () => void +}) { + const [loading, setLoading] = useState(false) + const { address } = useAccount() + const signer = useEthersSigner() + const setLastClaimedTimestamp = useSetAtom(lastClaimedTimestampAtom) + + async function claimSpam() { + setLoading(true) + try { + if (!ticket) { + throw new Error('No ticket found') + } + if (!address) { + throw new Error('No address found') + } + const contract = Spam__factory.connect(env.VITE_CONTRACT, signer) + const { r, yParityAndS } = ethers.Signature.from(ticket.signature) + const spammerBytes = turnIntoBytes(BigInt(address)) + const ticketTypeBytes = turnIntoBytes(0n) + const spamAmountBytes = turnIntoBytes( + ethers.parseEther(`${ticket.total}`) + ) + const fromTimestampBytes = turnIntoBytes( + BigInt(new Date(ticket.fromDate).getTime()) + ) + const toTimestampBytes = turnIntoBytes( + BigInt(new Date(ticket.toDate).getTime()) + ) + const message = [ + ...spammerBytes, + ...ticketTypeBytes, + ...spamAmountBytes, + ...fromTimestampBytes, + ...toTimestampBytes, + ] + const tx = await contract.claimSpam( + new Uint8Array(message), + r, + yParityAndS + ) + await tx.wait() + const bigintAddress = BigInt(address) + setLastClaimedTimestamp(contract.lastClaimTimestamps(bigintAddress, 0n)) + } catch (error) { + console.error(error) + } finally { + setLoading(false) + } + } + + const lastClaimedTimestamp = useAtomValue(lastClaimedTimestampAtom) + const disabled = + !!lastClaimedTimestamp && + new Date(Number(lastClaimedTimestamp)).getTime() > + new Date(ticket.fromDate).getTime() + return ( + + ) +} diff --git a/src/helpers/api.ts b/src/helpers/api.ts index 731b19c..e157869 100644 --- a/src/helpers/api.ts +++ b/src/helpers/api.ts @@ -1,7 +1,8 @@ import GeneratedTicket from 'models/GeneratedTIcket' +import OlderTicket from 'models/OlderTicket' import env from 'helpers/env' -export default async function generateTicket( +export async function generateTicket( address: `0x${string}`, signature: string ) { @@ -23,3 +24,26 @@ export default async function generateTicket( return res })) as GeneratedTicket } + +export async function getOlderTickets( + address: `0x${string}`, + signature: string +) { + return (await ( + await fetch(`${env.VITE_BACKEND}/tickets`, { + headers: { + address, + signature, + 'Content-Type': 'application/json', + }, + }) + ) + .json() + .then((res) => { + if (res.statusCode >= 400) { + console.log(res) + throw new Error(res.message) + } + return res + })) as OlderTicket[] +} diff --git a/src/helpers/getDateString.ts b/src/helpers/getDateString.ts new file mode 100644 index 0000000..f8f99a8 --- /dev/null +++ b/src/helpers/getDateString.ts @@ -0,0 +1,9 @@ +export default function (date: Date | string | bigint) { + if (typeof date === 'bigint') { + date = new Date(Number(date)) + } + if (typeof date === 'string') { + date = new Date(date) + } + return date.toLocaleString() +} diff --git a/src/models/OlderTicket.ts b/src/models/OlderTicket.ts new file mode 100644 index 0000000..cddb34e --- /dev/null +++ b/src/models/OlderTicket.ts @@ -0,0 +1,13 @@ +interface OlderTicket { + address: string + signature: string + ticketType: number + fromDate: string + toDate: string + baseAmount: number + additionalForLikes: number + additionalForRecasts: number + total: number + createdAt: string +} +export default OlderTicket