achieve a whole new level of insanity by imitating a blockchain on the db

This commit is contained in:
osmannyildiz 2024-05-26 14:42:59 +03:00
parent 08d5eb1897
commit 031c92c34b
28 changed files with 756 additions and 73 deletions

View File

View File

@ -1,9 +1,10 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Campaign } from "../../db/campaign"; import { CampaignService } from "../../services/CampaignService";
export const getMany: RequestHandler = async (req, res, next) => { export const getMany: RequestHandler = async (req, res, next) => {
try { try {
const campaigns = await Campaign.find(); const campaignService = new CampaignService();
const campaigns = await campaignService.getAll();
return res.json({ return res.json({
ok: true, ok: true,
@ -16,16 +17,17 @@ export const getMany: RequestHandler = async (req, res, next) => {
export const create: RequestHandler = async (req, res, next) => { export const create: RequestHandler = async (req, res, next) => {
try { try {
const { name, contractAddress } = req.body; const { name, tokenId, maxDistributionAmount } = req.body;
if (!name || !contractAddress) { if (!name || !tokenId || !maxDistributionAmount) {
throw new Error("Missing field(s)."); throw new Error("Missing field(s).");
} }
const campaign = new Campaign({ const campaignService = new CampaignService();
name: req.body.name, const campaign = await campaignService.create({
contractAddress: req.body.contractAddress, name,
tokenId,
maxDistributionAmount,
}); });
await campaign.save();
return res.json({ return res.json({
ok: true, ok: true,

View File

@ -0,0 +1,29 @@
import { RequestHandler } from "express";
import { InvestorService } from "../../services/InvestorService";
export const getBalances: RequestHandler = async (req, res, next) => {
try {
const { walletAddress } = req.params;
if (!walletAddress) {
throw new Error("Missing field(s).");
}
const investorService = new InvestorService();
const investor = await investorService.getByWalletAddress(
walletAddress.toLowerCase()
);
if (!investor) {
throw new Error("Investor not found.");
}
const balances = await investorService.getBalances(
investor._id.toHexString()
);
return res.json({
ok: true,
data: balances,
});
} catch (error) {
return next(error);
}
};

View File

@ -1,7 +1,112 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { TokenService } from "../../services/TokenService";
import { InvestorService } from "./../../services/InvestorService";
import { TokenBalanceService } from "./../../services/TokenBalanceService";
export const getIndex: RequestHandler = async (req, res) => { export const getIndex: RequestHandler = async (req, res) => {
// await initTokens();
// await initInvestors();
// await testBalances1();
// await testBalances2();
// await testBalances3();
return res.json({ return res.json({
message: "It works!", message: "It works!",
}); });
}; };
async function initTokens() {
const tokenService = new TokenService();
tokenService.create({ name: "PAD Token", symbol: "PAD" });
tokenService.create({ name: "Kitty Token", symbol: "KITTY" });
tokenService.create({ name: "Doggo Token", symbol: "DOGGO" });
tokenService.create({ name: "Birb Token", symbol: "BIRB" });
}
async function initInvestors() {
const investorService = new InvestorService();
await investorService.createIfNotExists({
walletAddress: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".toLowerCase(),
});
await investorService.createIfNotExists({
walletAddress: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8".toLowerCase(),
});
}
async function testBalances1() {
const investorService = new InvestorService();
const tokenService = new TokenService();
const tokenBalanceService = new TokenBalanceService();
const investor1 = await investorService.getByWalletAddress(
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".toLowerCase()
);
const investor2 = await investorService.getByWalletAddress(
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8".toLowerCase()
);
const padToken = await tokenService.getBySymbol("PAD");
if (!investor1 || !investor2 || !padToken) {
throw new Error("error 1");
}
await tokenBalanceService.mint(
padToken._id.toHexString(),
investor1._id.toHexString(),
100e18
);
await tokenBalanceService.mint(
padToken._id.toHexString(),
investor2._id.toHexString(),
200e18
);
await tokenBalanceService.burn(
padToken._id.toHexString(),
investor2._id.toHexString(),
50e18
);
}
async function testBalances2() {
const investorService = new InvestorService();
const tokenService = new TokenService();
const tokenBalanceService = new TokenBalanceService();
const investor1 = await investorService.getByWalletAddress(
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".toLowerCase()
);
const investor2 = await investorService.getByWalletAddress(
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8".toLowerCase()
);
const kittyToken = await tokenService.getBySymbol("KITTY");
if (!investor1 || !investor2 || !kittyToken) {
throw new Error("error 1");
}
await tokenBalanceService.mint(
kittyToken._id.toHexString(),
investor1._id.toHexString(),
100e18
);
await tokenBalanceService.transfer(
kittyToken._id.toHexString(),
investor1._id.toHexString(),
investor2._id.toHexString(),
6.66e18
);
}
async function testBalances3() {
const investorService = new InvestorService();
const investor1 = await investorService.getByWalletAddress(
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".toLowerCase()
);
if (!investor1) {
throw new Error("error 1");
}
const balances = await investorService.getBalances(
investor1._id.toHexString()
);
console.log("==== hey", balances);
}

View File

@ -1,6 +1,7 @@
import cors from "cors"; import cors from "cors";
import express, { ErrorRequestHandler } from "express"; import express, { ErrorRequestHandler } from "express";
import { campaignsRouter } from "./routes/campaigns"; import { campaignsRouter } from "./routes/campaigns";
import { investorsRouter } from "./routes/investors";
import { testRouter } from "./routes/test"; import { testRouter } from "./routes/test";
export const api = express(); export const api = express();
@ -12,6 +13,7 @@ api.use(express.json());
// Routes // Routes
api.use("/test", testRouter); api.use("/test", testRouter);
api.use("/campaigns", campaignsRouter); api.use("/campaigns", campaignsRouter);
api.use("/investors", investorsRouter);
// Error Handler // Error Handler
const errorHandler: ErrorRequestHandler = (error, req, res, next) => { const errorHandler: ErrorRequestHandler = (error, req, res, next) => {

View File

@ -0,0 +1,6 @@
import express from "express";
import * as controllers from "../controllers/investors";
export const investorsRouter = express.Router();
investorsRouter.route("/:walletAddress/balances").get(controllers.getBalances);

View File

@ -1,7 +1,7 @@
import { SECRETS } from "./secrets"; import { SECRETS } from "./secrets";
export const CONFIG = { export const CONFIG = {
DEBUG: true,
API_PORT: 7231, API_PORT: 7231,
MONGODB_URI: SECRETS.MONGODB_URI, MONGODB_URI: SECRETS.MONGODB_URI,
MONGODB_DB_NAME: "osipad",
}; };

14
backend/src/db/admin.ts Normal file
View File

@ -0,0 +1,14 @@
import mongoose from "mongoose";
export interface IAdmin {
walletAddress: string;
}
const adminSchema = new mongoose.Schema<IAdmin>({
walletAddress: {
type: String,
required: true,
},
});
export const Admin = mongoose.model<IAdmin>("Admin", adminSchema);

View File

@ -1,8 +1,10 @@
import mongoose from "mongoose"; import mongoose, { Schema } from "mongoose";
interface ICampaign { export interface ICampaign {
name: string; name: string;
contractAddress: string; tokenId: mongoose.Types.ObjectId;
maxDistributionAmount: number;
// contractAddress: string;
} }
const campaignSchema = new mongoose.Schema<ICampaign>({ const campaignSchema = new mongoose.Schema<ICampaign>({
@ -10,8 +12,13 @@ const campaignSchema = new mongoose.Schema<ICampaign>({
type: String, type: String,
required: true, required: true,
}, },
contractAddress: { tokenId: {
type: String, type: Schema.Types.ObjectId,
ref: "Token",
required: true,
},
maxDistributionAmount: {
type: Number,
required: true, required: true,
}, },
}); });

View File

@ -5,6 +5,10 @@ export async function connectToDb() {
try { try {
await mongoose.connect(CONFIG.MONGODB_URI); await mongoose.connect(CONFIG.MONGODB_URI);
console.log("✅ Connected to MongoDB."); console.log("✅ Connected to MongoDB.");
if (CONFIG.DEBUG) {
mongoose.set("debug", true);
}
} catch (error) { } catch (error) {
console.error("❌ Couldn't connect to MongoDB.", error); console.error("❌ Couldn't connect to MongoDB.", error);
} }

View File

@ -0,0 +1,19 @@
import mongoose from "mongoose";
export interface IInvestor {
walletAddress: string;
ethBalance: number;
}
const investorSchema = new mongoose.Schema<IInvestor>({
walletAddress: {
type: String,
required: true,
},
ethBalance: {
type: Number,
required: true,
},
});
export const Investor = mongoose.model<IInvestor>("Investor", investorSchema);

30
backend/src/db/token.ts Normal file
View File

@ -0,0 +1,30 @@
import mongoose from "mongoose";
export interface IToken {
name: string;
symbol: string;
// decimals: number;
totalSupply: number;
// contractAddress: string;
}
const tokenSchema = new mongoose.Schema<IToken>({
name: {
type: String,
required: true,
},
symbol: {
type: String,
required: true,
},
// decimals: {
// type: Number,
// required: true,
// },
totalSupply: {
type: Number,
required: true,
},
});
export const Token = mongoose.model<IToken>("Token", tokenSchema);

View File

@ -0,0 +1,29 @@
import mongoose, { Schema } from "mongoose";
export interface ITokenBalance {
investorId: mongoose.Types.ObjectId;
tokenId: mongoose.Types.ObjectId;
amount: number;
}
const tokenBalanceSchema = new mongoose.Schema<ITokenBalance>({
investorId: {
type: Schema.Types.ObjectId,
ref: "Investor",
required: true,
},
tokenId: {
type: Schema.Types.ObjectId,
ref: "Token",
required: true,
},
amount: {
type: Number,
required: true,
},
});
export const TokenBalance = mongoose.model<ITokenBalance>(
"TokenBalance",
tokenBalanceSchema
);

View File

@ -0,0 +1,22 @@
import { Campaign, ICampaign } from "../db/campaign";
type ICreateCampaignPayload = Pick<
ICampaign,
"name" | "tokenId" | "maxDistributionAmount"
> & {};
export class CampaignService {
async getAll() {
return await Campaign.find();
}
async create(payload: ICreateCampaignPayload) {
const campaign = new Campaign({
name: payload.name,
tokenId: payload.tokenId,
maxDistributionAmount: payload.maxDistributionAmount,
});
await campaign.save();
return campaign;
}
}

View File

@ -0,0 +1,50 @@
import { IInvestor, Investor } from "../db/investor";
import { IToken } from "../db/token";
import { TokenBalance } from "../db/tokenBalance";
type ICreateInvestorPayload = Pick<IInvestor, "walletAddress"> & {};
export class InvestorService {
async getAll() {
return await Investor.find();
}
async getById(id: string) {
return await Investor.findOne({ _id: id });
}
async getByWalletAddress(walletAddress: string) {
return await Investor.findOne({ walletAddress });
}
async create(payload: ICreateInvestorPayload) {
const investor = new Investor({
walletAddress: payload.walletAddress,
ethBalance: 0,
});
await investor.save();
return investor;
}
async createIfNotExists(payload: ICreateInvestorPayload) {
const existingInvestor = await Investor.findOne({
walletAddress: payload.walletAddress,
});
if (existingInvestor) {
return { data: existingInvestor, created: false };
}
const investor = await this.create(payload);
return { data: investor, created: true };
}
async getBalances(investorId: string) {
const results = await TokenBalance.find({ investorId }).populate("tokenId");
const balances: Record<string, number> = {};
for (const result of results) {
const symbol = (result.tokenId as unknown as IToken).symbol;
balances[symbol] = result.amount;
}
return balances;
}
}

View File

@ -0,0 +1,78 @@
import { ITokenBalance, TokenBalance } from "../db/tokenBalance";
import { hexStringToObjectId } from "../utils/db";
type ICreateTokenBalancePayload = Pick<
ITokenBalance,
"investorId" | "tokenId"
> & {};
export class TokenBalanceService {
async create(payload: ICreateTokenBalancePayload) {
const tokenBalance = new TokenBalance({
investorId: payload.investorId,
tokenId: payload.tokenId,
amount: 0,
});
await tokenBalance.save();
return tokenBalance;
}
async createIfNotExists(payload: ICreateTokenBalancePayload) {
const existingTokenBalance = await TokenBalance.findOne({
investorId: payload.investorId,
tokenId: payload.tokenId,
});
if (existingTokenBalance) {
return { data: existingTokenBalance, created: false };
}
const tokenBalance = await this.create(payload);
return { data: tokenBalance, created: true };
}
async mint(tokenId: string, toInvestorId: string, amount: number) {
const tokenBalance = (
await this.createIfNotExists({
investorId: hexStringToObjectId(toInvestorId),
tokenId: hexStringToObjectId(tokenId),
})
).data;
tokenBalance.amount += amount;
await tokenBalance.save();
}
async burn(tokenId: string, fromInvestorId: string, amount: number) {
const tokenBalance = (
await this.createIfNotExists({
investorId: hexStringToObjectId(fromInvestorId),
tokenId: hexStringToObjectId(tokenId),
})
).data;
tokenBalance.amount -= amount;
await tokenBalance.save();
}
async transfer(
tokenId: string,
fromInvestorId: string,
toInvestorId: string,
amount: number
) {
const fromTokenBalance = (
await this.createIfNotExists({
investorId: hexStringToObjectId(fromInvestorId),
tokenId: hexStringToObjectId(tokenId),
})
).data;
const toTokenBalance = (
await this.createIfNotExists({
investorId: hexStringToObjectId(toInvestorId),
tokenId: hexStringToObjectId(tokenId),
})
).data;
fromTokenBalance.amount -= amount;
toTokenBalance.amount += amount;
await fromTokenBalance.save();
await toTokenBalance.save();
}
}

View File

@ -0,0 +1,27 @@
import { IToken, Token } from "../db/token";
type ICreateTokenPayload = Pick<IToken, "name" | "symbol"> & {};
export class TokenService {
async getAll() {
return await Token.find();
}
async getById(id: string) {
return await Token.findOne({ _id: id });
}
async getBySymbol(symbol: string) {
return await Token.findOne({ symbol });
}
async create(payload: ICreateTokenPayload) {
const token = new Token({
name: payload.name,
symbol: payload.symbol,
totalSupply: 0,
});
await token.save();
return token;
}
}

7
backend/src/utils/db.ts Normal file
View File

@ -0,0 +1,7 @@
import mongoose from "mongoose";
export function hexStringToObjectId(
hexString: string
): mongoose.Types.ObjectId {
return new mongoose.Types.ObjectId(hexString);
}

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.24; pragma solidity ^0.8.24;
import "hardhat/console.sol";
import "./PADToken.sol"; import "./PADToken.sol";
contract PADTokenStake { contract PADTokenStake {
@ -16,6 +17,7 @@ contract PADTokenStake {
uint id; uint id;
uint lockdownDays; uint lockdownDays;
uint rewardPercentage; uint rewardPercentage;
uint lockedAmount;
} }
struct Deposit { struct Deposit {
@ -27,6 +29,8 @@ contract PADTokenStake {
uint releasedAt; uint releasedAt;
} }
event Staked(address indexed staker, uint indexed poolId, uint amount, uint lockedAt);
constructor(PADToken _padToken) { constructor(PADToken _padToken) {
padToken = _padToken; padToken = _padToken;
_initPools(); _initPools();
@ -36,44 +40,65 @@ contract PADTokenStake {
pools[0] = Pool({ pools[0] = Pool({
id: nextPoolId, id: nextPoolId,
lockdownDays: 30, lockdownDays: 30,
rewardPercentage: 25 rewardPercentage: 25,
lockedAmount: 0
}); });
nextPoolId++; nextPoolId++;
pools[1] = Pool({ pools[1] = Pool({
id: nextPoolId, id: nextPoolId,
lockdownDays: 60, lockdownDays: 60,
rewardPercentage: 50 rewardPercentage: 50,
lockedAmount: 0
}); });
nextPoolId++; nextPoolId++;
} }
function stake(uint poolId, uint amount) external { function getPools() public view returns (Pool[] memory _pools) {
Pool memory pool = pools[poolId]; _pools = new Pool[](nextPoolId);
Deposit memory newDeposit = Deposit({ for (uint i = 0; i < nextPoolId; i++) {
id: nextDepositId, _pools[i] = pools[i];
staker: msg.sender, }
poolId: poolId,
amount: amount,
lockedAt: block.timestamp,
releasedAt: block.timestamp + (pool.lockdownDays * 1 days)
});
deposits[msg.sender].push(newDeposit);
} }
function getDeposit(uint depositId) public view returns (Deposit memory) { function stake(uint _poolId, uint _amount) external {
address staker = msg.sender;
uint lockedAt = block.timestamp;
Pool storage pool = pools[_poolId];
padToken.transferFrom(staker, address(this), _amount);
Deposit memory newDeposit = Deposit({
id: nextDepositId,
staker: staker,
poolId: _poolId,
amount: _amount,
lockedAt: lockedAt,
releasedAt: lockedAt + (pool.lockdownDays * 1 days)
});
deposits[staker].push(newDeposit);
console.log("hey1");
pool.lockedAmount += _amount;
console.log("hey2");
emit Staked(staker, _poolId, _amount, lockedAt);
console.log(staker, _poolId, _amount, lockedAt);
}
function getDeposit(uint _depositId) public view returns (Deposit memory) {
Deposit memory deposit; Deposit memory deposit;
for (uint i = 0; i < deposits[msg.sender].length; i++) { for (uint i = 0; i < deposits[msg.sender].length; i++) {
deposit = deposits[msg.sender][i]; deposit = deposits[msg.sender][i];
if (deposit.id == depositId) { if (deposit.id == _depositId) {
return deposit; return deposit;
} }
} }
revert("Deposit with the given ID not found."); revert("Deposit with the given ID not found.");
} }
function getWithdrawPenalty(uint depositId) public view returns (uint) { function getWithdrawPenalty(uint _depositId) public view returns (uint) {
Deposit memory deposit = getDeposit(depositId); Deposit memory deposit = getDeposit(_depositId);
uint remainingMs = deposit.releasedAt - block.timestamp; uint remainingMs = deposit.releasedAt - block.timestamp;
if (remainingMs <= 0) { if (remainingMs <= 0) {
@ -84,13 +109,13 @@ contract PADTokenStake {
return 100; return 100;
} }
function withdraw(uint depositId, bool acceptPenaltyCut) external { function withdraw(uint _depositId, bool _acceptPenaltyCut) external {
uint penalty = getWithdrawPenalty(depositId); uint penalty = getWithdrawPenalty(_depositId);
if (penalty > 0) { if (penalty > 0) {
require(acceptPenaltyCut, "You should accept the penalty cut."); require(_acceptPenaltyCut, "You should accept the penalty cut.");
} }
Deposit memory deposit = getDeposit(depositId); Deposit memory deposit = getDeposit(_depositId);
Pool memory pool = pools[deposit.poolId]; Pool memory pool = pools[deposit.poolId];
uint amountToTransfer = (deposit.amount * pool.rewardPercentage / 100) - penalty; uint amountToTransfer = (deposit.amount * pool.rewardPercentage / 100) - penalty;
padToken.transfer(deposit.staker, amountToTransfer); padToken.transfer(deposit.staker, amountToTransfer);

View File

@ -16,5 +16,6 @@ module.exports = {
], ],
// My additions // My additions
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": "off", // TODO Remove later
}, },
}; };

View File

@ -1,3 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { fetchBalances } from "../query/fetchers/investors";
import { useStore } from "../store"; import { useStore } from "../store";
import { tryWithToast } from "../utils/toast"; import { tryWithToast } from "../utils/toast";
import { formatPadTokenAmount, shortenAddress } from "../utils/ui"; import { formatPadTokenAmount, shortenAddress } from "../utils/ui";
@ -9,7 +11,12 @@ export function ConnectMetamaskButton() {
const unsetConnectedAccount = useStore( const unsetConnectedAccount = useStore(
(state) => state.unsetConnectedAccount (state) => state.unsetConnectedAccount
); );
const padTokenBalance = useStore((state) => state.padTokenBalance); // const padTokenBalance = useStore((state) => state.padTokenBalance);
const balancesQuery = useQuery({
queryKey: ["userBalances", connectedAccount],
queryFn: () => fetchBalances(connectedAccount),
});
const padTokenBalance = balancesQuery?.data?.PAD || null;
// https://docs.web3js.org/guides/getting_started/metamask/#react-app // https://docs.web3js.org/guides/getting_started/metamask/#react-app
async function connectMetamask() { async function connectMetamask() {

View File

@ -1,39 +1,58 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { useEffect } from "react"; // import { useEffect } from "react";
import { useStore } from "../store"; import { useStore } from "../store";
import buildContext from "../utils/context"; import buildContext from "../utils/context";
import { padToken } from "../web3"; // import { padToken } from "../web3";
interface IMainContext { interface IMainContext {
fetchPadTokenBalance: () => Promise<void>; // fetchPadTokenBalance: () => Promise<void>;
} }
export const [MainContext, useMainContext] = buildContext<IMainContext>(); export const [MainContext, useMainContext] = buildContext<IMainContext>();
export function buildMainContextValue(): IMainContext { export function buildMainContextValue(): IMainContext {
const connectedAccount = useStore((state) => state.connectedAccount); const connectedAccount = useStore((state) => state.connectedAccount);
const setPadTokenBalance = useStore((state) => state.setPadTokenBalance); // const setPadTokenBalance = useStore((state) => state.setPadTokenBalance);
const unsetPadTokenBalance = useStore((state) => state.unsetPadTokenBalance); // const unsetPadTokenBalance = useStore((state) => state.unsetPadTokenBalance);
const fetchPadTokenBalance = async () => { // const fetchPadTokenBalance = async () => {
const balance: bigint = await padToken.methods // const balance: bigint = await padToken.methods
.balanceOf(connectedAccount) // .balanceOf(connectedAccount)
.call(); // .call();
if (typeof balance === "bigint") { // if (typeof balance === "bigint") {
setPadTokenBalance(balance); // setPadTokenBalance(balance);
} else { // } else {
unsetPadTokenBalance(); // unsetPadTokenBalance();
} // }
}; // };
useEffect(() => { // useEffect(() => {
if (connectedAccount) { // if (connectedAccount) {
fetchPadTokenBalance(); // fetchPadTokenBalance();
}
}, [connectedAccount]); // const subscription1 = padToken.events.Transfer({
// filter: { to: connectedAccount },
// });
// subscription1.on("data", (event) => {
// fetchPadTokenBalance();
// });
// const subscription2 = padToken.events.Transfer({
// filter: { from: connectedAccount },
// });
// subscription2.on("data", (event) => {
// fetchPadTokenBalance();
// });
// return () => {
// subscription1.unsubscribe();
// subscription2.unsubscribe();
// };
// }
// }, [connectedAccount]);
return { return {
fetchPadTokenBalance, // fetchPadTokenBalance,
}; };
} }

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"MainModule#MessageBox": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "MainModule#MessageBox": "0x998abeb3E57409262aE5b751f60747921B33613E",
"MainModule#PADToken": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "MainModule#PADToken": "0x70e0bA845a1A0F2DA3359C97E0285013525FFC49",
"MainModule#PADTokenStake": "0xc5a5C42992dECbae36851359345FE25997F5C42d" "MainModule#PADTokenStake": "0x4826533B4897376654Bb4d4AD88B7faFD0C98528"
} }

View File

@ -0,0 +1,21 @@
import { CONFIG } from "../../config";
export const fetchBalances = async (walletAddress: string) => {
if (!walletAddress) {
return null;
}
const resp = await fetch(
`${CONFIG.API_BASE_URL}/investors/${walletAddress}/balances`
);
if (!resp.ok) {
throw new Error("Network error.");
}
const respBody = await resp.json();
if (!respBody.ok) {
throw new Error(respBody.message || "Something went wrong.");
}
return respBody.data;
};

View File

@ -1,9 +1,10 @@
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react"; import { useEffect, useState } from "react";
import { useStore } from "../store"; import { useStore } from "../store";
import { getFormInputValue } from "../utils/form";
import { toastError, tryWithToast } from "../utils/toast"; import { toastError, tryWithToast } from "../utils/toast";
import { formatPadTokenAmount } from "../utils/ui"; import { formatPadTokenAmount } from "../utils/ui";
import { padToken, padTokenStake } from "../web3"; import { padToken, padTokenStake, web3 } from "../web3";
export const Route = createLazyFileRoute("/pad-token-stake")({ export const Route = createLazyFileRoute("/pad-token-stake")({
component: PadTokenStakePage, component: PadTokenStakePage,
@ -12,6 +13,7 @@ export const Route = createLazyFileRoute("/pad-token-stake")({
function PadTokenStakePage() { function PadTokenStakePage() {
const connectedAccount = useStore((state) => state.connectedAccount); const connectedAccount = useStore((state) => state.connectedAccount);
const [allowance, setAllowance] = useState("(nothingness)"); const [allowance, setAllowance] = useState("(nothingness)");
const [pools, setPools] = useState<any[]>([]);
const getAllowanceAmount = async () => { const getAllowanceAmount = async () => {
if (!connectedAccount) { if (!connectedAccount) {
@ -42,6 +44,71 @@ function PadTokenStakePage() {
}); });
}; };
const getPools = async () => {
await tryWithToast(
"Get Pools",
async () => {
const _pools: any[] = await padTokenStake.methods.getPools().call();
setPools(_pools);
},
{ onError: () => setPools([]), successSilent: true }
);
};
const onStakeFormSubmit: React.FormEventHandler<HTMLFormElement> = async (
event
) => {
event.preventDefault();
if (!connectedAccount) {
toastError("Stake", "Connect your wallet first.");
return;
}
const poolId = getFormInputValue(
event.target as HTMLFormElement,
"poolId"
).trim();
if (!poolId) {
toastError("Stake", "Something went wrong.");
return;
}
const amount = getFormInputValue(
event.target as HTMLFormElement,
"amount"
).trim();
if (!amount) {
toastError("Stake", "Enter an amount.");
return;
}
await tryWithToast("Stake", async () => {
await padToken.methods
.approve(
padTokenStake.options.address,
web3.utils.toWei(amount, "ether")
)
.send({ from: connectedAccount });
await padTokenStake.methods
.stake(poolId, web3.utils.toWei(amount, "ether"))
.send({ from: connectedAccount });
});
};
useEffect(() => {
getPools();
const subscription = padTokenStake.events.Staked();
subscription.on("data", (event) => {
getPools();
});
return () => {
subscription.unsubscribe();
};
}, []);
return ( return (
<div className="container mx-auto p-4 grid lg:grid-cols-2 xl:grid-cols-3 gap-4"> <div className="container mx-auto p-4 grid lg:grid-cols-2 xl:grid-cols-3 gap-4">
<div className="card bg-base-200"> <div className="card bg-base-200">
@ -69,6 +136,44 @@ function PadTokenStakePage() {
</button> </button>
</div> </div>
</div> </div>
<div className="card bg-base-200">
<div className="card-body items-start">
<h2 className="text-2xl font-bold">Pools</h2>
<div className="flex flex-col self-stretch gap-y-2">
{pools.map((pool) => (
<div
key={pool.id}
className="bg-base-300 rounded-2xl p-2 text-center"
>
{console.log(pool)}
<div className="font-bold">Pool #{Number(pool.id)}</div>
<ul className="text-sm">
<li>Lockdown period: {Number(pool.lockdownDays)} days</li>
<li>Reward: +{Number(pool.rewardPercentage)}%</li>
<li>
Total locked value:{" "}
{formatPadTokenAmount(pool.lockedAmount)}
</li>
</ul>
<form onSubmit={onStakeFormSubmit}>
<input type="hidden" name="poolId" value={Number(pool.id)} />
<input
type="number"
name="amount"
className="input input-sm flex-grow"
placeholder="Amount"
required
/>
<button type="submit" className="btn btn-primary btn-sm">
Stake
</button>
</form>
</div>
))}
</div>
</div>
</div>
</div> </div>
); );
} }

View File

@ -15,11 +15,17 @@ export function toastError(taskName: string, errorMessage: string) {
export async function tryWithToast( export async function tryWithToast(
taskName: string, taskName: string,
taskFn: () => Promise<void> taskFn: () => Promise<void>,
options?: {
onError?: (error: any) => void;
successSilent: boolean;
}
) { ) {
try { try {
await taskFn(); await taskFn();
toastSuccess(taskName); if (!options?.successSilent) {
toastSuccess(taskName);
}
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
toastError( toastError(
@ -27,5 +33,6 @@ export async function tryWithToast(
error.message || error.message ||
"No error message. Check the DevTools console for details." "No error message. Check the DevTools console for details."
); );
options?.onError?.(error);
} }
} }

View File

@ -15,7 +15,8 @@
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, /* TODO Set strict to true later */
"strict": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true