From 46529498b64b1340b138ef02fbe27e17467a4680 Mon Sep 17 00:00:00 2001 From: wfnuser Date: Fri, 13 Dec 2024 16:45:34 +0800 Subject: [PATCH 1/2] feat: update grant reward api --- src/pages/period/index.tsx | 10 ++++++++-- src/pages/period/reward-form.tsx | 3 ++- src/service/index.ts | 10 +++++----- src/types/index.ts | 4 ++-- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/pages/period/index.tsx b/src/pages/period/index.tsx index d0bb371..8d49789 100644 --- a/src/pages/period/index.tsx +++ b/src/pages/period/index.tsx @@ -3,9 +3,9 @@ import { fetchPeriod, getLoadMoreProjectList } from '@/service' import { IResultPagination, IResultPaginationData, Project, Period } from '@/types' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { LoadingCards } from '@/components/loading-cards' -import { useAccount } from 'wagmi' +import { useAccount, useSwitchChain } from 'wagmi' import { useInfiniteScroll } from 'ahooks' -import { DEFAULT_PAGINATION_LIMIT } from '@/constants/data' +import { DEFAULT_PAGINATION_LIMIT, paymentChain } from '@/constants/data' import PaginationFast from '@/components/pagination-fast' import { useQuery } from '@tanstack/react-query' import { useSearchParams } from 'react-router-dom' @@ -45,6 +45,7 @@ function ProjectList({ loading, loadingMore, data }: ProjectListProps) { function PeriodTable(): React.ReactElement { const [page, setPage] = useState(1) + const { switchChain } = useSwitchChain() const [urlParam] = useSearchParams('') const [projectId, setProjectId] = useState('66cd6e1bdcdcc63c6a64bec3') const [filterTags] = useState([]) @@ -89,6 +90,10 @@ function PeriodTable(): React.ReactElement { }, }) + useEffect(() => { + switchChain({ chainId: paymentChain.id }) + }, [switchChain]) + const totalPages = Math.ceil((periods?.pagination.totalCount || 0) / pageSize) const [hasAllowance, setHasAllowance] = useState(false) @@ -195,6 +200,7 @@ function PeriodTable(): React.ReactElement { variant="link" className="gap-2 p-0 text-blue-500" onClick={async () => { + await switchChain({ chainId: paymentChain.id }) await distributor.approveAllowance(ethers.parseEther('5000')) await checkAllowance() }} diff --git a/src/pages/period/reward-form.tsx b/src/pages/period/reward-form.tsx index c964723..a1633b0 100644 --- a/src/pages/period/reward-form.tsx +++ b/src/pages/period/reward-form.tsx @@ -19,6 +19,7 @@ import { distributor } from '@/constants/distributor' import _ from 'lodash' import { ethers } from 'ethers' +import { postGrantPeriodRewards } from '@/service' function randomDistribute(amount: number, people: number): number[] { const points = _.sortBy(_.times(people - 1, () => Math.random())) @@ -90,7 +91,7 @@ export const RewardDialogForm = ({ trigger, id, users, addressFrom, chain }: IRe await distributor.createRedPacket(id, githubIds, amountsInWei) - // await postGrantAggregationRewards({ id }) + await postGrantPeriodRewards({ id }) setOpen(false) diff --git a/src/service/index.ts b/src/service/index.ts index d1f0975..289ed76 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -14,8 +14,8 @@ import { PrRewardInfo, TaskState, UserInfo, - FetchPullRequestAggregationsParams, - FetchGrantAggregationRewardsParams, + FetchPeriodsParams, + GrantPeriodRewardsParams, Period, } from '@/types' import http from './instance' @@ -129,13 +129,13 @@ export async function fetchPullRequests(params: FetchPullRequestParams) { return response.data } -export async function fetchPeriod(params: FetchPullRequestAggregationsParams) { +export async function fetchPeriod(params: FetchPeriodsParams) { const response = await http.get>('/periods', { params }) return response.data } -export async function postGrantAggregationRewards(params: FetchGrantAggregationRewardsParams) { - const response = await http.post(`/aggregations/${params.id}/grant-rewards`) +export async function postGrantPeriodRewards(params: GrantPeriodRewardsParams) { + const response = await http.post(`/periods/${params.id}/grant-rewards`) return response.data } diff --git a/src/types/index.ts b/src/types/index.ts index 132d278..a1c8f83 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -131,11 +131,11 @@ export interface FetchPullRequestParams extends PaginationParams { sort?: string } -export interface FetchPullRequestAggregationsParams extends PaginationParams { +export interface FetchPeriodsParams extends PaginationParams { projectId: string } -export interface FetchGrantAggregationRewardsParams { +export interface GrantPeriodRewardsParams { id: string } From 183e1c75e234b5d4453d793be7153dc6ee66b5d7 Mon Sep 17 00:00:00 2001 From: wfnuser Date: Fri, 13 Dec 2024 23:19:04 +0800 Subject: [PATCH 2/2] feat: support period reward redpacket --- src/constants/distributor.ts | 9 +- src/hooks/useDistributorToken.ts | 44 +++++++ src/lib/utils.ts | 11 ++ src/pages/myrewards/index.tsx | 207 ++++++++----------------------- src/pages/period/reward-form.tsx | 32 +++-- src/service/index.ts | 21 +++- src/types/index.ts | 37 +++++- 7 files changed, 195 insertions(+), 166 deletions(-) create mode 100644 src/hooks/useDistributorToken.ts diff --git a/src/constants/distributor.ts b/src/constants/distributor.ts index fb3e926..90230d9 100644 --- a/src/constants/distributor.ts +++ b/src/constants/distributor.ts @@ -15,6 +15,8 @@ export class Distributor { 'function approve(address spender, uint256 amount) external returns (bool)', 'function allowance(address owner, address spender) external view returns (uint256)', 'function balanceOf(address account) external view returns (uint256)', + 'function symbol() external view returns (string)', + 'function decimals() external view returns (uint8)', ] constructor(distributorAddress: string) { @@ -52,6 +54,11 @@ export class Distributor { return await tx.wait() } + async getTokenSymbolAndDecimals() { + const tokenContract = await this.getTokenContract() + return [await tokenContract.symbol(), await tokenContract.decimals()] + } + async getAllowance(walletAddress: string) { const tokenContract = await this.getTokenContract() return await tokenContract.allowance(walletAddress, await this.getAddress()) @@ -81,7 +88,7 @@ export class Distributor { } } -const distributorAddress = import.meta.env.VITE_DISTRIBUTOR_ADDRESS || '0x1a48F5d414DDC79a79f519A665e03692B2a2c450' +const distributorAddress = import.meta.env.VITE_DISTRIBUTOR_ADDRESS || '0xBE639b42A3818875D59992d80F18280387cFB412' const distributor = new Distributor(distributorAddress) diff --git a/src/hooks/useDistributorToken.ts b/src/hooks/useDistributorToken.ts new file mode 100644 index 0000000..cf44647 --- /dev/null +++ b/src/hooks/useDistributorToken.ts @@ -0,0 +1,44 @@ +import { distributor } from '@/constants/distributor' + +import { useEffect, useState } from 'react' + +interface TokenInfo { + symbol: string + decimals: number + loading: boolean + error: Error | null +} + +export function useDistributorToken() { + const [tokenInfo, setTokenInfo] = useState({ + symbol: 'USDT', + decimals: 6, + loading: true, + error: null, + }) + + useEffect(() => { + const getTokenInfo = async () => { + try { + const [symbol, decimals] = await distributor.getTokenSymbolAndDecimals() + setTokenInfo({ + symbol, + decimals, + loading: false, + error: null, + }) + } catch (error) { + console.log(error) + setTokenInfo((prev) => ({ + ...prev, + loading: false, + error: error instanceof Error ? error : new Error('Unknown error'), + })) + } + } + + getTokenInfo() + }, []) + + return tokenInfo +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 037f4c5..e0f3228 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -25,3 +25,14 @@ export const getRandomColor = (colors: string[]) => { export const capitalize = (str: string) => { return _.capitalize(str) } + +export const formatDate = (date?: string) => { + if (!date) return '' + return new Date(date).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: 'numeric', + }) +} diff --git a/src/pages/myrewards/index.tsx b/src/pages/myrewards/index.tsx index 3c566a0..07d0475 100644 --- a/src/pages/myrewards/index.tsx +++ b/src/pages/myrewards/index.tsx @@ -1,198 +1,101 @@ -import React, { useEffect, useState } from 'react' -import { fetchPeriod, getLoadMoreProjectList } from '@/service' -import { IResultPagination, IResultPaginationData, Period, Project } from '@/types' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import React, { useState } from 'react' +import { claimReceipt, fetchReceipts, getRewardSignature } from '@/service' +import { IResultPaginationData, Receipt, ReceiptStatus } from '@/types' import { LoadingCards } from '@/components/loading-cards' import { useAccount } from 'wagmi' -import { useInfiniteScroll } from 'ahooks' -import { DEFAULT_PAGINATION_LIMIT } from '@/constants/data' import PaginationFast from '@/components/pagination-fast' -import { useQuery } from '@tanstack/react-query' -import { useSearchParams } from 'react-router-dom' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Button } from '@/components/ui/button' import { distributor } from '@/constants/distributor' import { toast } from '@/components/ui/use-toast' import { useAtom } from 'jotai' import { usernameAtom } from '@/store' - -interface ProjectListProps { - loading: boolean - loadingMore: boolean - data: IResultPagination | undefined -} - -function ProjectList({ loading, loadingMore, data }: ProjectListProps) { - if (loading) return - if (!data) return null - - return ( -
-
-
{data.pagination.totalCount} Projects
-
-
- {data.list.map((project) => ( - - {project.name} - - ))} - {loadingMore && } -
-
- ) -} +import { formatDate } from '@/lib/utils' function RewardsTable(): React.ReactElement { const [page, setPage] = useState(1) const [github] = useAtom(usernameAtom) - const [urlParam] = useSearchParams('') - const [projectId, setProjectId] = useState(undefined) - const [filterTags] = useState([]) const { address, chain } = useAccount() const pageSize = 10 + const queryClient = useQueryClient() - const { - data: projects, - loading: projectLoading, - loadingMore: projectLoadingMore, - reload, - } = useInfiniteScroll>( - async (d) => { - const res = await getLoadMoreProjectList({ - offset: d ? d.pagination.currentPage * DEFAULT_PAGINATION_LIMIT : 0, - limit: DEFAULT_PAGINATION_LIMIT, - filterTags, - search: decodeURIComponent(urlParam.get('search') || ''), - sort: decodeURIComponent(urlParam.get('sort') || ''), - onlyPeriodicReward: true, - }) - if (!projectId) setProjectId(res.list[0]._id.toString()) - return res - }, - { - manual: true, - target: document.querySelector('#scrollRef'), - isNoMore: (data) => { - return data ? !data.pagination.hasNextPage : false - }, - }, - ) - - const { data: periods, isLoading: isPullRequestsLoading } = useQuery | undefined>({ - queryKey: ['periods', projectId ?? ''], + const { data: periods, isLoading: isPullRequestsLoading } = useQuery | undefined>({ + queryKey: ['receipts'], queryFn: () => { - return fetchPeriod({ + return fetchReceipts({ offset: (page - 1) * pageSize, limit: pageSize, - projectId: projectId ?? '', }) }, }) const totalPages = Math.ceil((periods?.pagination.totalCount || 0) / pageSize) - useEffect(() => { - reload() - }, [filterTags, reload, urlParam]) - return (
- - {!isPullRequestsLoading && periods ? ( - From - To - Users - Pr count + Period + Amount Reward - {periods.data.map((periodPrs) => { + {periods.data.map((receipts) => { return ( - - - {new Date(periodPrs.from).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric', - })} - + - {new Date(periodPrs.to).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric', - })} + {formatDate(receipts.source.period?.from)} - {formatDate(receipts.source.period?.to)} -
- {periodPrs.contributors.map((user) => { - return ( - {user._id} - ) - })} -
+ {receipts.status == ReceiptStatus.CLAIMED + ? `${receipts.detail.amount} ${receipts.detail.symbol}` + : '***'}
- {periodPrs.pullRequests.length} - {!periodPrs.rewardGranted ? ( - address && - github && - chain && ( - + {receipts.source.period ? ( + receipts.status === ReceiptStatus.GRANTED ? ( + address && github && chain ? ( + + ) : ( +

Please connect wallet

+ ) + ) : ( +

Claimed

) ) : ( -

Granted

+

Not Supported

)}
diff --git a/src/pages/period/reward-form.tsx b/src/pages/period/reward-form.tsx index a1633b0..ebc4c78 100644 --- a/src/pages/period/reward-form.tsx +++ b/src/pages/period/reward-form.tsx @@ -3,7 +3,7 @@ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, For import { Input } from '@/components/ui/input' import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' -import { useConfig, useSwitchChain } from 'wagmi' +import { useSwitchChain } from 'wagmi' import { Chain } from 'viem' import { useToast } from '@/components/ui/use-toast' import React, { useState } from 'react' @@ -12,7 +12,6 @@ import { DialogContent, DialogFooter, DialogTitle, DialogTrigger } from '@/compo import { Dialog } from '@radix-ui/react-dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Label } from '@/components/ui/label' -import { USDT_SYMBOL } from '@/constants/contracts/usdt' import { paymentChain } from '@/constants/data' import { User } from '@/types' import { distributor } from '@/constants/distributor' @@ -20,6 +19,8 @@ import { distributor } from '@/constants/distributor' import _ from 'lodash' import { ethers } from 'ethers' import { postGrantPeriodRewards } from '@/service' +import { useDistributorToken } from '@/hooks/useDistributorToken' +import { useQueryClient } from '@tanstack/react-query' function randomDistribute(amount: number, people: number): number[] { const points = _.sortBy(_.times(people - 1, () => Math.random())) @@ -57,13 +58,14 @@ export const RewardDialogForm = ({ trigger, id, users, addressFrom, chain }: IRe const [loading, setLoading] = useState(false) const [isOpen, setOpen] = useState(false) const { switchChain } = useSwitchChain() - const { chains } = useConfig() + const chains = [paymentChain] const [amounts, setAmounts] = useState(Array(users.length).fill(0.0)) - + const { symbol, decimals } = useDistributorToken() + const queryClient = useQueryClient() const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - coin: USDT_SYMBOL, + coin: symbol, }, }) @@ -80,7 +82,7 @@ export const RewardDialogForm = ({ trigger, id, users, addressFrom, chain }: IRe try { // TODO: some github name is login, some is username in backend const githubIds = users.map((user) => user.username || user.login) - const amountsInWei = amounts.map((amount) => BigInt(Math.floor(amount * 1e18))) + const amountsInWei = amounts.map((amount) => BigInt(Math.floor(amount * 10 ** Number(decimals)))) const totalAmount = amountsInWei.reduce((a, b) => a + b, 0n) const currentAllowance = await distributor.getAllowance(addressFrom) @@ -91,10 +93,19 @@ export const RewardDialogForm = ({ trigger, id, users, addressFrom, chain }: IRe await distributor.createRedPacket(id, githubIds, amountsInWei) - await postGrantPeriodRewards({ id }) + await postGrantPeriodRewards({ + id, + contributors: users.map((user) => ({ + contributor: user._id, + amount: amounts[users.indexOf(user)], + symbol: symbol, + decimals: Number(decimals), + })), + }) setOpen(false) + queryClient.invalidateQueries({ queryKey: ['periods'] }) toast({ variant: 'default', title: 'Success', @@ -163,15 +174,14 @@ export const RewardDialogForm = ({ trigger, id, users, addressFrom, chain }: IRe name="coin" render={({ field }) => ( - - {chain.nativeCurrency.symbol} - {USDT_SYMBOL} + {symbol} @@ -201,7 +211,7 @@ export const RewardDialogForm = ({ trigger, id, users, addressFrom, chain }: IRe defaultValue={`${chain.id}`} onValueChange={(value) => { switchChain({ chainId: Number(value) }) - form.setValue('coin', USDT_SYMBOL) + form.setValue('coin', symbol) }} > diff --git a/src/service/index.ts b/src/service/index.ts index 289ed76..38b0754 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -17,6 +17,8 @@ import { FetchPeriodsParams, GrantPeriodRewardsParams, Period, + Receipt, + PaginationParams, } from '@/types' import http from './instance' @@ -134,8 +136,20 @@ export async function fetchPeriod(params: FetchPeriodsParams) { return response.data } +export async function fetchReceipts(params: PaginationParams) { + const response = await http.get>('/my-receipts', { params }) + return response.data +} + +export async function claimReceipt(id: string) { + const response = await http.post(`/receipt/${id}/claim`) + return response.data +} + export async function postGrantPeriodRewards(params: GrantPeriodRewardsParams) { - const response = await http.post(`/periods/${params.id}/grant-rewards`) + const response = await http.post(`/periods/${params.id}/grant-rewards`, { + contributors: params.contributors, + }) return response.data } @@ -200,3 +214,8 @@ export async function postPrRewardInfo(params: PrRewardInfo) { const response = await http.post('/rewards', params) return response.data } + +export async function getRewardSignature(uuid: string) { + const response = await http.get<{ signature: string }>(`/rewards/signature?uuid=${uuid}`) + return response.data +} diff --git a/src/types/index.ts b/src/types/index.ts index a1c8f83..6989a1e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -32,6 +32,35 @@ export interface Period { __v: number } +export enum ReceiptStatus { + GRANTED = 'granted', + CLAIMED = 'claimed', +} +export interface Receipt { + _id: string + user: User + source: { + period?: Period + } + detail: { + amount: number + decimals: number + symbol: string + } + transactionInfo?: { + network: string + from: string + to: string + amount: number + decimals: number + symbol: string + transactionId: string + } + status: ReceiptStatus + createdAt: string + updatedAt: string +} + export interface IResultPagination { list: T[] pagination: IPagination @@ -116,7 +145,7 @@ export interface User { avatarUrl: string wallet: `0x${string}` rewards?: number - _id?: string + _id: string } export interface FetchIssuesParams { @@ -137,6 +166,12 @@ export interface FetchPeriodsParams extends PaginationParams { export interface GrantPeriodRewardsParams { id: string + contributors: Array<{ + contributor: string + amount: number + symbol: string + decimals: number + }> } export interface PullRequest {