Skip to content

Commit

Permalink
sync merkl app features (#11)
Browse files Browse the repository at this point in the history
* sync merkl app features

* update package.json
  • Loading branch information
hugolxt authored Jan 13, 2025
1 parent b90a143 commit 42c8157
Show file tree
Hide file tree
Showing 24 changed files with 499 additions and 97 deletions.
Binary file modified bun.lockb
Binary file not shown.
12 changes: 2 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@elysiajs/eden": "^1.1.3",
"@emotion/css": "^11.13.4",
"@lifi/widget": "^3.13.1",
"@merkl/api": "0.10.277",
"@merkl/api": "0.10.307",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@remix-run/dev": "^2.11.2",
Expand Down Expand Up @@ -62,18 +62,10 @@
"@types/bun": "latest",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"autoprefixer": "^10.4.19",
"elysia": "^1.1.19",
"eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.38",
"typescript": "^5.6.2",
"typescript": "^5.7.2",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1"
},
Expand Down
55 changes: 52 additions & 3 deletions src/api/services/reward.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ export abstract class RewardService {
}

/**
* Retrieves opportunities query params from page request
* Retrieves query params from page request
* @param request request containing query params such as chains, status, pagination...
* @param override params for which to override value
* @returns query
*/
static #getQueryFromRequest(request: Request, override?: Parameters<typeof api.v4.rewards.index.get>[0]["query"]) {
static #getCampaignLeaderboardQueryFromRequest(
request: Request,
override?: Parameters<typeof api.v4.rewards.index.get>[0]["query"],
) {
const campaignId = new URL(request.url).searchParams.get("campaignId");
const page = new URL(request.url).searchParams.get("page");
const items = new URL(request.url).searchParams.get("items");
Expand Down Expand Up @@ -63,12 +66,58 @@ export abstract class RewardService {
);
}

static #getTokenLeaderboardQueryFromRequest(
request: Request,
override?: Parameters<typeof api.v4.rewards.token.get>[0]["query"],
) {
const page = new URL(request.url).searchParams.get("page");
const items = new URL(request.url).searchParams.get("items");

const filters = Object.assign(
{
items: items ?? DEFAULT_ITEMS_PER_PAGE,
page,
},
override ?? {},
page !== null && { page: Number(page) - 1 },
);

const query = Object.entries(filters).reduce(
(_query, [key, filter]) => Object.assign(_query, filter == null ? {} : { [key]: filter }),
{},
);

return query;
}

static async getTokenLeaderboard(
request: Request,
overrides?: Parameters<typeof api.v4.rewards.token.get>[0]["query"],
) {
return RewardService.getByToken(
Object.assign(RewardService.#getTokenLeaderboardQueryFromRequest(request), overrides ?? undefined),
);
}

static async getByToken(query: Parameters<typeof api.v4.rewards.index.get>[0]["query"]) {
const rewards = await RewardService.#fetch(async () =>
api.v4.rewards.token.get({
query,
}),
);

const count = await RewardService.#fetch(async () => api.v4.rewards.token.count.get({ query }));
const { amount } = await RewardService.#fetch(async () => api.v4.rewards.token.total.get({ query }));

return { count, rewards, total: amount };
}

static async getManyFromRequest(
request: Request,
overrides?: Parameters<typeof api.v4.rewards.index.get>[0]["query"],
) {
return RewardService.getByParams(
Object.assign(RewardService.#getQueryFromRequest(request), overrides ?? undefined),
Object.assign(RewardService.#getCampaignLeaderboardQueryFromRequest(request), overrides ?? undefined),
);
}

Expand Down
4 changes: 4 additions & 0 deletions src/api/services/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export abstract class TokenService {
return tokens;
}

static async findUniqueOrThrow(chainId: number, address: string) {
return await TokenService.#fetch(async () => api.v4.tokens({ id: `${chainId}-${address}` }).get());
}

static async getSymbol(symbol: string | undefined): Promise<Token[]> {
if (!symbol) throw new Response("Token not found");

Expand Down
22 changes: 14 additions & 8 deletions src/components/element/leaderboard/LeaderboardLibrary.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
import type { Campaign } from "@merkl/api";
import type { Token } from "@merkl/api";
import { useSearchParams } from "@remix-run/react";
import { Group, Text, Title } from "dappkit";
import { useMemo } from "react";
import type { RewardService } from "src/api/services/reward.service";
import { v4 as uuidv4 } from "uuid";
import Pagination from "../opportunity/Pagination";
import { LeaderboardTable } from "./LeaderboardTable";
import { LeaderboardTable, LeaderboardTableWithoutReason } from "./LeaderboardTable";
import LeaderboardTableRow from "./LeaderboardTableRow";

export type IProps = {
leaderboard: Awaited<ReturnType<(typeof RewardService)["getManyFromRequest"]>>["rewards"];
count?: number;
total?: bigint;
campaign: Campaign;
withReason: boolean;
token: Token;
chain: number;
};

export default function LeaderboardLibrary(props: IProps) {
const { leaderboard, count, total, campaign } = props;
const { leaderboard, count, total, token, chain, withReason } = props;
const [searchParams] = useSearchParams();

const items = searchParams.get("items");
const page = searchParams.get("page");

const Table = withReason ? LeaderboardTable : LeaderboardTableWithoutReason;

const rows = useMemo(() => {
return leaderboard?.map((row, index) => (
<LeaderboardTableRow
key={uuidv4()}
total={BigInt(total ?? 0n)}
row={row}
withReason={withReason}
rank={index + 1 + Math.max(Number(page) - 1, 0) * Number(items)}
campaign={campaign}
token={token}
chain={chain}
/>
));
}, [leaderboard, page, items, total, campaign]);
}, [leaderboard, page, items, total, token, chain, withReason]);

return (
<Group className="flex-row w-full [&>*]:flex-grow">
{!!rows?.length ? (
<LeaderboardTable
<Table
dividerClassName={index => (index < 2 ? "bg-accent-8" : "bg-main-8")}
header={
<Title h={5} className="!text-main-11 w-full">
Expand All @@ -46,7 +52,7 @@ export default function LeaderboardLibrary(props: IProps) {
}
footer={count !== undefined && <Pagination count={count} />}>
{rows}
</LeaderboardTable>
</Table>
) : (
<Text className="p-xl">No rewarded users</Text>
)}
Expand Down
23 changes: 23 additions & 0 deletions src/components/element/leaderboard/LeaderboardTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,26 @@ export const [LeaderboardTable, LeaderboardRow, LeaderboardColumns] = createTabl
className: "justify-end",
},
});

export const [LeaderboardTableWithoutReason, LeaderboardRowWithoutReason, LeaderboardColumnsWithoutReason] =
createTable({
rank: {
name: "Rank",
size: "minmax(120px,150px)",
compact: "1fr",
className: "justify-start",
main: true,
},
address: {
name: "Address",
size: "minmax(170px,1fr)",
compactSize: "1fr",
className: "justify-start",
},
rewards: {
name: "Rewards earned",
size: "minmax(30px,1fr)",
compactSize: "minmax(20px,1fr)",
className: "justify-start",
},
});
36 changes: 19 additions & 17 deletions src/components/element/leaderboard/LeaderboardTableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
import type { Campaign } from "@merkl/api";
import type { Token as TokenType } from "@merkl/api";
import { type Component, Group, PrimitiveTag, Text, Value, mergeClass } from "dappkit";
import { useWalletContext } from "packages/dappkit/src/context/Wallet.context";
import { useMemo } from "react";
import type { RewardService } from "src/api/services/reward.service";
import { formatUnits, parseUnits } from "viem";
import { formatUnits } from "viem";
import Token from "../token/Token";
import User from "../user/User";
import { LeaderboardRow } from "./LeaderboardTable";
import { LeaderboardRow, LeaderboardRowWithoutReason } from "./LeaderboardTable";

export type CampaignTableRowProps = Component<{
export type LeaderboardTableRowProps = Component<{
row: Awaited<ReturnType<typeof RewardService.getManyFromRequest>>["rewards"][0];
total: bigint;
rank: number;
campaign: Campaign;
token: TokenType;
chain: number;
withReason: boolean;
}>;

export default function LeaderboardTableRow({ row, rank, total, className, ...props }: CampaignTableRowProps) {
const { campaign } = props;
export default function LeaderboardTableRow({ row, rank, total, className, ...props }: LeaderboardTableRowProps) {
const { token, chain: chainId, withReason } = props;
const { chains } = useWalletContext();

const Row = withReason ? LeaderboardRow : LeaderboardRowWithoutReason;

const share = useMemo(() => {
const amount = formatUnits(BigInt(row?.amount) + BigInt(row?.pending ?? 0), campaign.rewardToken.decimals);
const all = formatUnits(total, campaign.rewardToken.decimals);
const amount = formatUnits(BigInt(row?.amount) + BigInt(row?.pending ?? 0), token.decimals);
const all = formatUnits(total, token.decimals);

return Number.parseFloat(amount) / Number.parseFloat(all);
}, [row, total, campaign]);
}, [row, total, token]);

const chain = useMemo(() => {
return chains?.find(c => c.id === campaign.computeChainId);
}, [chains, campaign]);
return chains?.find(c => c.id === chainId);
}, [chains, chainId]);

return (
<LeaderboardRow
<Row
{...props}
className={mergeClass("cursor-pointer", className)}
rankColumn={
Expand All @@ -45,10 +49,8 @@ export default function LeaderboardTableRow({ row, rank, total, className, ...pr
</Group>
}
addressColumn={<User chain={chain} address={row.recipient} />}
rewardsColumn={
<Token token={campaign.rewardToken} format="amount_price" amount={parseUnits(row?.amount + row?.pending, 0)} />
}
protocolColumn={<Text>{row?.reason?.split("_")[0]}</Text>}
rewardsColumn={<Token token={token} format="amount_price" amount={BigInt(row?.amount) + BigInt(row?.pending)} />}
protocolColumn={withReason ? <Text>{row?.reason?.split("_")[0]}</Text> : undefined}
/>
);
}
Loading

0 comments on commit 42c8157

Please sign in to comment.