achieve a whole new level of insanity by imitating a blockchain on the db
This commit is contained in:
parent
08d5eb1897
commit
031c92c34b
|
|
@ -1,9 +1,10 @@
|
|||
import { RequestHandler } from "express";
|
||||
import { Campaign } from "../../db/campaign";
|
||||
import { CampaignService } from "../../services/CampaignService";
|
||||
|
||||
export const getMany: RequestHandler = async (req, res, next) => {
|
||||
try {
|
||||
const campaigns = await Campaign.find();
|
||||
const campaignService = new CampaignService();
|
||||
const campaigns = await campaignService.getAll();
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
|
|
@ -16,16 +17,17 @@ export const getMany: RequestHandler = async (req, res, next) => {
|
|||
|
||||
export const create: RequestHandler = async (req, res, next) => {
|
||||
try {
|
||||
const { name, contractAddress } = req.body;
|
||||
if (!name || !contractAddress) {
|
||||
const { name, tokenId, maxDistributionAmount } = req.body;
|
||||
if (!name || !tokenId || !maxDistributionAmount) {
|
||||
throw new Error("Missing field(s).");
|
||||
}
|
||||
|
||||
const campaign = new Campaign({
|
||||
name: req.body.name,
|
||||
contractAddress: req.body.contractAddress,
|
||||
const campaignService = new CampaignService();
|
||||
const campaign = await campaignService.create({
|
||||
name,
|
||||
tokenId,
|
||||
maxDistributionAmount,
|
||||
});
|
||||
await campaign.save();
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +1,112 @@
|
|||
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) => {
|
||||
// await initTokens();
|
||||
// await initInvestors();
|
||||
// await testBalances1();
|
||||
// await testBalances2();
|
||||
// await testBalances3();
|
||||
|
||||
return res.json({
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import cors from "cors";
|
||||
import express, { ErrorRequestHandler } from "express";
|
||||
import { campaignsRouter } from "./routes/campaigns";
|
||||
import { investorsRouter } from "./routes/investors";
|
||||
import { testRouter } from "./routes/test";
|
||||
|
||||
export const api = express();
|
||||
|
|
@ -12,6 +13,7 @@ api.use(express.json());
|
|||
// Routes
|
||||
api.use("/test", testRouter);
|
||||
api.use("/campaigns", campaignsRouter);
|
||||
api.use("/investors", investorsRouter);
|
||||
|
||||
// Error Handler
|
||||
const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { SECRETS } from "./secrets";
|
||||
|
||||
export const CONFIG = {
|
||||
DEBUG: true,
|
||||
API_PORT: 7231,
|
||||
MONGODB_URI: SECRETS.MONGODB_URI,
|
||||
MONGODB_DB_NAME: "osipad",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import mongoose from "mongoose";
|
||||
import mongoose, { Schema } from "mongoose";
|
||||
|
||||
interface ICampaign {
|
||||
export interface ICampaign {
|
||||
name: string;
|
||||
contractAddress: string;
|
||||
tokenId: mongoose.Types.ObjectId;
|
||||
maxDistributionAmount: number;
|
||||
// contractAddress: string;
|
||||
}
|
||||
|
||||
const campaignSchema = new mongoose.Schema<ICampaign>({
|
||||
|
|
@ -10,8 +12,13 @@ const campaignSchema = new mongoose.Schema<ICampaign>({
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contractAddress: {
|
||||
type: String,
|
||||
tokenId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Token",
|
||||
required: true,
|
||||
},
|
||||
maxDistributionAmount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export async function connectToDb() {
|
|||
try {
|
||||
await mongoose.connect(CONFIG.MONGODB_URI);
|
||||
console.log("✅ Connected to MongoDB.");
|
||||
|
||||
if (CONFIG.DEBUG) {
|
||||
mongoose.set("debug", true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Couldn't connect to MongoDB.", error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import mongoose from "mongoose";
|
||||
|
||||
export function hexStringToObjectId(
|
||||
hexString: string
|
||||
): mongoose.Types.ObjectId {
|
||||
return new mongoose.Types.ObjectId(hexString);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "hardhat/console.sol";
|
||||
import "./PADToken.sol";
|
||||
|
||||
contract PADTokenStake {
|
||||
|
|
@ -16,6 +17,7 @@ contract PADTokenStake {
|
|||
uint id;
|
||||
uint lockdownDays;
|
||||
uint rewardPercentage;
|
||||
uint lockedAmount;
|
||||
}
|
||||
|
||||
struct Deposit {
|
||||
|
|
@ -27,6 +29,8 @@ contract PADTokenStake {
|
|||
uint releasedAt;
|
||||
}
|
||||
|
||||
event Staked(address indexed staker, uint indexed poolId, uint amount, uint lockedAt);
|
||||
|
||||
constructor(PADToken _padToken) {
|
||||
padToken = _padToken;
|
||||
_initPools();
|
||||
|
|
@ -36,44 +40,65 @@ contract PADTokenStake {
|
|||
pools[0] = Pool({
|
||||
id: nextPoolId,
|
||||
lockdownDays: 30,
|
||||
rewardPercentage: 25
|
||||
rewardPercentage: 25,
|
||||
lockedAmount: 0
|
||||
});
|
||||
nextPoolId++;
|
||||
|
||||
pools[1] = Pool({
|
||||
id: nextPoolId,
|
||||
lockdownDays: 60,
|
||||
rewardPercentage: 50
|
||||
rewardPercentage: 50,
|
||||
lockedAmount: 0
|
||||
});
|
||||
nextPoolId++;
|
||||
}
|
||||
|
||||
function stake(uint poolId, uint amount) external {
|
||||
Pool memory pool = pools[poolId];
|
||||
Deposit memory newDeposit = Deposit({
|
||||
id: nextDepositId,
|
||||
staker: msg.sender,
|
||||
poolId: poolId,
|
||||
amount: amount,
|
||||
lockedAt: block.timestamp,
|
||||
releasedAt: block.timestamp + (pool.lockdownDays * 1 days)
|
||||
});
|
||||
deposits[msg.sender].push(newDeposit);
|
||||
function getPools() public view returns (Pool[] memory _pools) {
|
||||
_pools = new Pool[](nextPoolId);
|
||||
for (uint i = 0; i < nextPoolId; i++) {
|
||||
_pools[i] = pools[i];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
for (uint i = 0; i < deposits[msg.sender].length; i++) {
|
||||
deposit = deposits[msg.sender][i];
|
||||
if (deposit.id == depositId) {
|
||||
if (deposit.id == _depositId) {
|
||||
return deposit;
|
||||
}
|
||||
}
|
||||
revert("Deposit with the given ID not found.");
|
||||
}
|
||||
|
||||
function getWithdrawPenalty(uint depositId) public view returns (uint) {
|
||||
Deposit memory deposit = getDeposit(depositId);
|
||||
function getWithdrawPenalty(uint _depositId) public view returns (uint) {
|
||||
Deposit memory deposit = getDeposit(_depositId);
|
||||
uint remainingMs = deposit.releasedAt - block.timestamp;
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
|
|
@ -84,13 +109,13 @@ contract PADTokenStake {
|
|||
return 100;
|
||||
}
|
||||
|
||||
function withdraw(uint depositId, bool acceptPenaltyCut) external {
|
||||
uint penalty = getWithdrawPenalty(depositId);
|
||||
function withdraw(uint _depositId, bool _acceptPenaltyCut) external {
|
||||
uint penalty = getWithdrawPenalty(_depositId);
|
||||
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];
|
||||
uint amountToTransfer = (deposit.amount * pool.rewardPercentage / 100) - penalty;
|
||||
padToken.transfer(deposit.staker, amountToTransfer);
|
||||
|
|
|
|||
|
|
@ -16,5 +16,6 @@ module.exports = {
|
|||
],
|
||||
// My additions
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off", // TODO Remove later
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchBalances } from "../query/fetchers/investors";
|
||||
import { useStore } from "../store";
|
||||
import { tryWithToast } from "../utils/toast";
|
||||
import { formatPadTokenAmount, shortenAddress } from "../utils/ui";
|
||||
|
|
@ -9,7 +11,12 @@ export function ConnectMetamaskButton() {
|
|||
const unsetConnectedAccount = useStore(
|
||||
(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
|
||||
async function connectMetamask() {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,58 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
|
||||
import { useEffect } from "react";
|
||||
// import { useEffect } from "react";
|
||||
import { useStore } from "../store";
|
||||
import buildContext from "../utils/context";
|
||||
import { padToken } from "../web3";
|
||||
// import { padToken } from "../web3";
|
||||
|
||||
interface IMainContext {
|
||||
fetchPadTokenBalance: () => Promise<void>;
|
||||
// fetchPadTokenBalance: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const [MainContext, useMainContext] = buildContext<IMainContext>();
|
||||
|
||||
export function buildMainContextValue(): IMainContext {
|
||||
const connectedAccount = useStore((state) => state.connectedAccount);
|
||||
const setPadTokenBalance = useStore((state) => state.setPadTokenBalance);
|
||||
const unsetPadTokenBalance = useStore((state) => state.unsetPadTokenBalance);
|
||||
// const setPadTokenBalance = useStore((state) => state.setPadTokenBalance);
|
||||
// const unsetPadTokenBalance = useStore((state) => state.unsetPadTokenBalance);
|
||||
|
||||
const fetchPadTokenBalance = async () => {
|
||||
const balance: bigint = await padToken.methods
|
||||
.balanceOf(connectedAccount)
|
||||
.call();
|
||||
if (typeof balance === "bigint") {
|
||||
setPadTokenBalance(balance);
|
||||
} else {
|
||||
unsetPadTokenBalance();
|
||||
}
|
||||
};
|
||||
// const fetchPadTokenBalance = async () => {
|
||||
// const balance: bigint = await padToken.methods
|
||||
// .balanceOf(connectedAccount)
|
||||
// .call();
|
||||
// if (typeof balance === "bigint") {
|
||||
// setPadTokenBalance(balance);
|
||||
// } else {
|
||||
// unsetPadTokenBalance();
|
||||
// }
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
if (connectedAccount) {
|
||||
fetchPadTokenBalance();
|
||||
}
|
||||
}, [connectedAccount]);
|
||||
// useEffect(() => {
|
||||
// if (connectedAccount) {
|
||||
// fetchPadTokenBalance();
|
||||
|
||||
// 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 {
|
||||
fetchPadTokenBalance,
|
||||
// fetchPadTokenBalance,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"MainModule#MessageBox": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
|
||||
"MainModule#PADToken": "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
|
||||
"MainModule#PADTokenStake": "0xc5a5C42992dECbae36851359345FE25997F5C42d"
|
||||
"MainModule#MessageBox": "0x998abeb3E57409262aE5b751f60747921B33613E",
|
||||
"MainModule#PADToken": "0x70e0bA845a1A0F2DA3359C97E0285013525FFC49",
|
||||
"MainModule#PADTokenStake": "0x4826533B4897376654Bb4d4AD88B7faFD0C98528"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useStore } from "../store";
|
||||
import { getFormInputValue } from "../utils/form";
|
||||
import { toastError, tryWithToast } from "../utils/toast";
|
||||
import { formatPadTokenAmount } from "../utils/ui";
|
||||
import { padToken, padTokenStake } from "../web3";
|
||||
import { padToken, padTokenStake, web3 } from "../web3";
|
||||
|
||||
export const Route = createLazyFileRoute("/pad-token-stake")({
|
||||
component: PadTokenStakePage,
|
||||
|
|
@ -12,6 +13,7 @@ export const Route = createLazyFileRoute("/pad-token-stake")({
|
|||
function PadTokenStakePage() {
|
||||
const connectedAccount = useStore((state) => state.connectedAccount);
|
||||
const [allowance, setAllowance] = useState("(nothingness)");
|
||||
const [pools, setPools] = useState<any[]>([]);
|
||||
|
||||
const getAllowanceAmount = async () => {
|
||||
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 (
|
||||
<div className="container mx-auto p-4 grid lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<div className="card bg-base-200">
|
||||
|
|
@ -69,6 +136,44 @@ function PadTokenStakePage() {
|
|||
</button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,17 @@ export function toastError(taskName: string, errorMessage: string) {
|
|||
|
||||
export async function tryWithToast(
|
||||
taskName: string,
|
||||
taskFn: () => Promise<void>
|
||||
taskFn: () => Promise<void>,
|
||||
options?: {
|
||||
onError?: (error: any) => void;
|
||||
successSilent: boolean;
|
||||
}
|
||||
) {
|
||||
try {
|
||||
await taskFn();
|
||||
if (!options?.successSilent) {
|
||||
toastSuccess(taskName);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastError(
|
||||
|
|
@ -27,5 +33,6 @@ export async function tryWithToast(
|
|||
error.message ||
|
||||
"No error message. Check the DevTools console for details."
|
||||
);
|
||||
options?.onError?.(error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
/* TODO Set strict to true later */
|
||||
"strict": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
|
|
|
|||
Loading…
Reference in New Issue