Skip to content

Commit 0058145

Browse files
committed
Staking Page and Basic Queries and Create Pool Mutation
1 parent 582522b commit 0058145

18 files changed

+5065
-123
lines changed
219 KB
Loading
128 KB
Loading
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"use client";
2+
3+
import { PublicKey } from "@solana/web3.js";
4+
import { useState } from "react";
5+
import { useNavigate } from "react-router";
6+
import Loader from "../loader";
7+
// import { useStakingProgram } from "../stake/staking-data-access";
8+
import { useCreateStakingPool } from "@/hooks/use-create-staking-pool";
9+
import { useProgramStaking } from "@/utils/programStaking";
10+
import DurationSelectSection from "./DurationSelectSelection";
11+
12+
export default function CreateStakingPoolModal() {
13+
let navigate = useNavigate();
14+
let stakingProgram = useProgramStaking();
15+
16+
const { mutateAsync } = useCreateStakingPool();
17+
const programId = stakingProgram?.programId; // Placeholder for actual programId
18+
// const programId = useMemo(
19+
// () => getStakingProgramId(cluster.network as Cluster),
20+
// [cluster],
21+
// );
22+
const [mint, setMint] = useState("");
23+
const [rewardMint, setRewardMint] = useState("");
24+
const [isInitializing, setIsInitializing] = useState(false);
25+
const [totalSeconds, setTotalSeconds] = useState(0);
26+
27+
async function initializePool() {
28+
// Throw error if mint is not a valid public key
29+
if (!programId) return;
30+
let mintPubkey = new PublicKey(mint);
31+
let rewardMintPubkey = new PublicKey(rewardMint);
32+
33+
try {
34+
setIsInitializing(true);
35+
36+
let [poolId] = PublicKey.findProgramAddressSync(
37+
[
38+
Buffer.from("pool"),
39+
mintPubkey.toBuffer(),
40+
rewardMintPubkey.toBuffer(),
41+
],
42+
programId,
43+
);
44+
45+
await mutateAsync({
46+
mint,
47+
rewardMint,
48+
duration: totalSeconds,
49+
});
50+
51+
navigate(`/stake/${poolId}`);
52+
} catch (error) {
53+
console.log(error);
54+
} finally {
55+
setIsInitializing(false);
56+
}
57+
}
58+
59+
return (
60+
<dialog id="create-staking-pool-modal" className="modal">
61+
<div className="modal-box border border-neutral">
62+
<form method="dialog">
63+
{/* if there is a button in form, it will close the modal */}
64+
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
65+
66+
</button>
67+
</form>
68+
<h3 className="font-bold text-lg">Create A Staking Pool</h3>
69+
<div className="mt-5 space-y-4">
70+
<div className="flex flex-col mb-2 space-y-2">
71+
<input
72+
value={mint}
73+
onChange={(e) => setMint(e.target.value)}
74+
disabled={isInitializing}
75+
className="input input-bordered join-item"
76+
placeholder="Mint Address"
77+
/>
78+
79+
<input
80+
value={rewardMint}
81+
onChange={(e) => setRewardMint(e.target.value)}
82+
disabled={isInitializing}
83+
className="input input-bordered join-item"
84+
placeholder="Reward Mint Address"
85+
/>
86+
87+
<DurationSelectSection
88+
totalSeconds={totalSeconds}
89+
setTotalSeconds={setTotalSeconds}
90+
isCreatingPool={isInitializing}
91+
/>
92+
</div>
93+
94+
<button
95+
onClick={initializePool}
96+
disabled={isInitializing}
97+
className="btn btn-secondary w-full mt-20 flex items-center justify-center"
98+
>
99+
{isInitializing && <Loader />}
100+
{isInitializing ? "Creating Pool..." : "Create Pool"}
101+
</button>
102+
</div>
103+
</div>
104+
</dialog>
105+
);
106+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"use client";
2+
3+
import { Minus, Plus } from "lucide-react";
4+
import { useEffect, useState } from "react";
5+
import { secondsToTimeUnits } from "./utils/secondsToTimeUnits";
6+
7+
type Props = {
8+
totalSeconds: number;
9+
setTotalSeconds: (totalSeconds: number) => void;
10+
isCreatingPool?: boolean;
11+
};
12+
13+
type TimeUnit = "weeks" | "days" | "hours" | "minutes";
14+
15+
export default function DurationSelectSection({
16+
totalSeconds,
17+
setTotalSeconds,
18+
isCreatingPool = false,
19+
}: Props) {
20+
const [time, setTime] = useState(secondsToTimeUnits(totalSeconds));
21+
22+
useEffect(() => {
23+
console.log("time", time);
24+
}, [time]);
25+
26+
useEffect(() => {
27+
const newTotalSeconds =
28+
time.weeks * 7 * 24 * 60 * 60 +
29+
time.days * 24 * 60 * 60 +
30+
time.hours * 60 * 60 +
31+
time.minutes * 60;
32+
setTotalSeconds(newTotalSeconds);
33+
}, [time]);
34+
35+
const incrementTime = (unit: TimeUnit, amount: number) => {
36+
setTime((prevTime) => {
37+
const newTime = { ...prevTime };
38+
newTime[unit] += amount;
39+
40+
// Ensure no negative values
41+
if (newTime[unit] < 0) {
42+
newTime[unit] = 0;
43+
}
44+
45+
// Handle overflow
46+
if (unit === "minutes" && newTime.minutes >= 60) {
47+
newTime.hours += Math.floor(newTime.minutes / 60);
48+
newTime.minutes %= 60;
49+
}
50+
if (unit === "hours" || newTime.hours >= 24) {
51+
newTime.days += Math.floor(newTime.hours / 24);
52+
newTime.hours %= 24;
53+
}
54+
if (unit === "days" || newTime.days >= 7) {
55+
newTime.weeks += Math.floor(newTime.days / 7);
56+
newTime.days %= 7;
57+
}
58+
59+
return newTime;
60+
});
61+
};
62+
63+
const formatTime = (time: {
64+
weeks: number;
65+
days: number;
66+
hours: number;
67+
minutes: number;
68+
}) => {
69+
const parts = [];
70+
if (time.weeks)
71+
parts.push(`${time.weeks} week${time.weeks !== 1 ? "s" : ""}`);
72+
if (time.days) parts.push(`${time.days} day${time.days !== 1 ? "s" : ""}`);
73+
if (time.hours)
74+
parts.push(`${time.hours} hour${time.hours !== 1 ? "s" : ""}`);
75+
if (time.minutes)
76+
parts.push(`${time.minutes} minute${time.minutes !== 1 ? "s" : ""}`);
77+
return parts.join(", ") || "0 minutes";
78+
};
79+
80+
const TimeUnitControl = ({
81+
unit,
82+
label,
83+
}: {
84+
unit: TimeUnit;
85+
label: string;
86+
}) => (
87+
<div className="flex flex-col items-center">
88+
<div className="font-semibold mb-2">{label}</div>
89+
<div className="flex space-x-2">
90+
<button
91+
disabled={isCreatingPool}
92+
className="flex items-center justify-center text-primary hover:text-black outline outline-primary hover:bg-primary transition ease-in-out h-5 w-5 md:h-8 md:w-8 disabled:bg-gray-700 disabled:cursor-not-allowed"
93+
onClick={() => incrementTime(unit, -1)}
94+
>
95+
<Minus className="h-4 w-4" />
96+
</button>
97+
<button
98+
disabled={isCreatingPool}
99+
className="flex items-center justify-center text-primary hover:text-black outline outline-primary hover:bg-primary transition ease-in-out h-5 w-5 md:h-8 md:w-8 disabled:bg-gray-700 disabled:cursor-not-allowed"
100+
onClick={() => incrementTime(unit, 1)}
101+
>
102+
<Plus className="h-4 w-4" />
103+
</button>
104+
</div>
105+
</div>
106+
);
107+
108+
return (
109+
<div className="">
110+
<div className="text-xl font-semibold text-center mb-2">
111+
{formatTime(time)}
112+
</div>
113+
<div className="text-neutral text-center text-muted-foreground mb-3">
114+
Total seconds: {totalSeconds}
115+
</div>
116+
<div className="grid grid-cols-4 gap-4">
117+
<TimeUnitControl key="weeks" unit="weeks" label="Weeks" />
118+
<TimeUnitControl key="days" unit="days" label="Days" />
119+
<TimeUnitControl key="hours" unit="hours" label="Hours" />
120+
<TimeUnitControl key="minutes" unit="minutes" label="Minutes" />
121+
</div>
122+
</div>
123+
);
124+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { StakingPool } from "@/pages/staking";
2+
import { formatNumber } from "@/utils";
3+
import { useEffect, useState } from "react";
4+
import { Link } from "react-router";
5+
6+
type PoolListItemProps = {
7+
isTop?: boolean;
8+
pool: StakingPool;
9+
};
10+
11+
function formatUnits(amount: string, decimals: number) {
12+
return parseFloat(amount) / Math.pow(10, decimals);
13+
}
14+
15+
function getTokenLogo(tokenAddress: string | null) {
16+
if (!tokenAddress) return "/default-token-logo.svg";
17+
return `/tokens/${tokenAddress}.png`; // Assuming token logos are stored in this path
18+
}
19+
20+
export default function PoolListItem({
21+
pool,
22+
isTop = false,
23+
}: PoolListItemProps) {
24+
let [apy, setApy] = useState(Math.floor(Math.random() * 100) + 1);
25+
useEffect(() => {
26+
console.log(pool);
27+
}, []);
28+
return (
29+
<tr key={pool.id}>
30+
<td>
31+
<div className="flex items-center gap-3">
32+
{pool.mint.isLpToken ? (
33+
<div className="flex items-center -space-x-2">
34+
<img
35+
src={getTokenLogo(pool.mint.pairTokenAddress0)}
36+
alt=""
37+
className="h-6 w-6 rounded-full"
38+
/>
39+
<img
40+
src={getTokenLogo(pool.mint.pairTokenAddress1)}
41+
alt=""
42+
className="h-6 w-6 rounded-full"
43+
/>
44+
</div>
45+
) : (
46+
<div className="">
47+
<img
48+
src={pool.mint.image}
49+
alt=""
50+
className="h-6 w-6 rounded-full"
51+
/>
52+
</div>
53+
)}
54+
<div>
55+
<Link
56+
to={`/stake/${pool.id}`}
57+
className="font-bold hover:underline"
58+
>
59+
{pool.mint.isLpToken ? (
60+
<p>ai16z LP Token {pool.id}</p>
61+
) : (
62+
<p>{pool.mint.symbol}</p>
63+
)}
64+
</Link>
65+
</div>
66+
</div>
67+
</td>
68+
<td>
69+
<div className="flex items-center gap-3">
70+
{pool.rewardMint.isLpToken ? (
71+
<div className="flex items-center -space-x-2">
72+
<img
73+
src={getTokenLogo(pool.rewardMint.pairTokenAddress0)}
74+
alt=""
75+
className="h-6 w-6 rounded-full"
76+
/>
77+
<img
78+
src={getTokenLogo(pool.rewardMint.pairTokenAddress1)}
79+
alt=""
80+
className="h-6 w-6 rounded-full"
81+
/>
82+
</div>
83+
) : (
84+
<div className="">
85+
<img
86+
src={pool.rewardMint.image}
87+
alt=""
88+
className="h-6 w-6 rounded-full"
89+
/>
90+
</div>
91+
)}
92+
<div className="flex items-center">
93+
<p className="font-bold">
94+
{`${formatNumber(
95+
formatUnits(
96+
pool.rewardAmount,
97+
parseInt(pool.rewardMint.decimals),
98+
),
99+
)} ${
100+
pool.rewardMint.isLpToken
101+
? `ai16z LP Token ${pool.id}`
102+
: pool.rewardMint.symbol
103+
}`}
104+
</p>
105+
</div>
106+
</div>
107+
</td>
108+
<td>
109+
<div className="flex items-center">
110+
<p className="font-bold">{apy}%</p>
111+
</div>
112+
</td>
113+
<td>
114+
<div className="flex items-center justify-center">
115+
<p className="font-bold">{pool.participants}</p>
116+
</div>
117+
</td>
118+
<th>
119+
<div className="flex justify-end">
120+
<Link to={`/stake/${pool.id}`} className="btn btn-secondary btn-sm">
121+
View Pool
122+
</Link>
123+
</div>
124+
</th>
125+
</tr>
126+
);
127+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function secondsToTimeUnits(totalSeconds: number) {
2+
const weeks = Math.floor(totalSeconds / (7 * 24 * 60 * 60));
3+
const remainingAfterWeeks = totalSeconds % (7 * 24 * 60 * 60);
4+
5+
const days = Math.floor(remainingAfterWeeks / (24 * 60 * 60));
6+
const remainingAfterDays = remainingAfterWeeks % (24 * 60 * 60);
7+
8+
const hours = Math.floor(remainingAfterDays / (60 * 60));
9+
const remainingAfterHours = remainingAfterDays % (60 * 60);
10+
11+
const minutes = Math.floor(remainingAfterHours / 60);
12+
const seconds = remainingAfterHours % 60;
13+
14+
return {
15+
weeks,
16+
days,
17+
hours,
18+
minutes,
19+
seconds,
20+
};
21+
}

0 commit comments

Comments
 (0)