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 { 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,
|
||||||
|
|
|
||||||
|
|
@ -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 { 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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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";
|
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",
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"MainModule#MessageBox": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
|
"MainModule#MessageBox": "0x998abeb3E57409262aE5b751f60747921B33613E",
|
||||||
"MainModule#PADToken": "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
|
"MainModule#PADToken": "0x70e0bA845a1A0F2DA3359C97E0285013525FFC49",
|
||||||
"MainModule#PADTokenStake": "0xc5a5C42992dECbae36851359345FE25997F5C42d"
|
"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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
if (!options?.successSilent) {
|
||||||
toastSuccess(taskName);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue