diff --git a/.gitignore b/.gitignore
index 6ef0146..d2de37f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,12 @@ bun.lockb
test.js
.vscode
bun.lockb
+
+# Local Netlify folder
+.netlify
+
+# Claude
+.claude/
+
+# Misc
+deno.lock
diff --git a/components/choices/index.js b/components/choices/index.js
index 74c1c40..caa0701 100644
--- a/components/choices/index.js
+++ b/components/choices/index.js
@@ -1,8 +1,6 @@
const mongoose = require("mongoose");
const express = require("express");
const md5 = require("md5");
-// This will help us connect to the database
-const dbo = require("../../db/conn");
const {
getInputFromSigPayload,
getTimestampFromPayloadBytes,
@@ -18,20 +16,30 @@ const PollModel = require("../../db/models/Poll.model");
const ChoiceModel = require("../../db/models/Choice.model");
const { getEthUserBalanceAtLevel } = require("../../utils-eth");
-// This help convert the id from string to ObjectId for the _id.
-const ObjectId = require("mongodb").ObjectId;
+// Simple in-memory lock to prevent race conditions on concurrent votes
+const voteLocks = new Map();
+
+async function withVoteLock(pollID, address, fn) {
+ const key = `${pollID}:${address}`;
+
+ // Wait if another request is processing this voter
+ while (voteLocks.has(key)) {
+ await new Promise(resolve => setTimeout(resolve, 10));
+ }
+
+ voteLocks.set(key, true);
+ try {
+ return await fn();
+ } finally {
+ voteLocks.delete(key);
+ }
+}
const getChoiceById = async (req, response) => {
const { id } = req.params;
try {
- const choices = [];
- let db_connect = dbo.getDb("Lite");
- const cursor = await db_connect
- .collection("Choices")
- .find({ pollID: ObjectId(id) });
-
- await cursor.forEach((elem) => choices.push(elem));
+ const choices = await ChoiceModel.find({ pollID: id }).lean();
return response.json(choices);
} catch (error) {
console.log("error: ", error);
@@ -44,17 +52,20 @@ const getChoiceById = async (req, response) => {
const updateChoiceById = async (req, response) => {
const { payloadBytes, publicKey, signature } = req.body;
const network = req.body.network;
+ const reqId = req.id || "no-reqid";
+ console.log("[choices.update:start]", { reqId, network, path: req.originalUrl });
let j = 0;
let i = 0;
const timeNow = new Date().valueOf();
if (network?.startsWith("etherlink")) {
try {
- console.log('[payload]', req.payloadObj)
+ console.log("[choices.update:eth:payload]", { reqId, length: Array.isArray(req.payloadObj) ? req.payloadObj.length : -1 });
const castedChoices = req.payloadObj;
if (castedChoices.length === 0) throw new Error("No choices sent in the request");
const address = castedChoices[0].address
const pollId = castedChoices[0].pollID
+ console.log("[choices.update:eth:fetch-poll]", { reqId, pollId });
const poll = await PollModel.findById(pollId)
if(!poll) throw new Error("Poll not found")
@@ -68,6 +79,7 @@ const updateChoiceById = async (req, response) => {
} else {
daoFindQuery.address = { $regex: new RegExp(`^${poll.daoID}$`, 'i') };
}
+ console.log("[choices.update:eth:find-dao]", { reqId, daoFindQuery });
const dao = await DAOModel.findOne(daoFindQuery)
if (!dao) throw new Error(`DAO not found: ${poll.daoID}`)
@@ -89,8 +101,9 @@ const updateChoiceById = async (req, response) => {
);
if (duplicates.length > 0) throw new Error("Duplicate choices found");
+ console.log("[choices.update:eth:balance-request]", { reqId, net: dao.network || network, address, token: dao.tokenAddress, block });
const total = await getEthUserBalanceAtLevel(dao.network || network, address, dao.tokenAddress, block)
- console.log("EthTotal_UserBalance: ", total)
+ console.log("[choices.update:eth:balance-response]", { reqId, total: total?.toString?.() || total });
if (!total) {
throw new Error("Could not get total power at reference block");
@@ -104,6 +117,7 @@ const updateChoiceById = async (req, response) => {
pollId: poll._id,
walletAddresses: { $elemMatch: { address: address } }
});
+ console.log("[choices.update:eth:is-voted]", { reqId, count: isVoted?.length || 0 });
const walletVote = {
@@ -117,6 +131,7 @@ const updateChoiceById = async (req, response) => {
if (isVoted.length > 0) {
const oldVoteObj = isVoted[0].walletAddresses.find(x => x.address === address);
oldVote = await ChoiceModel.findById(oldVoteObj.choiceId);
+ console.log("[choices.update:eth:old-vote]", { reqId, hasOld: Boolean(oldVote) });
// TODO: Enable Repeat Vote
// const oldSignaturePayload = oldVote.walletAddresses[0].payloadBytes
@@ -146,6 +161,7 @@ const updateChoiceById = async (req, response) => {
{ _id: choiceId },
updatePayload
)
+ console.log("[choices.update:eth:update-one]", { reqId, choiceId });
} else {
await ChoiceModel.updateMany(
{ pollID: poll._id },
@@ -157,6 +173,7 @@ const updateChoiceById = async (req, response) => {
updatePayload,
{ upsert: true }
)
+ console.log("[choices.update:eth:update-many-one]", { reqId, choiceId });
}
}
@@ -168,15 +185,16 @@ const updateChoiceById = async (req, response) => {
for(const choice of castedChoices){
const choiceId = choice.choiceId
await ChoiceModel.updateOne(
- {_id: ObjectId(choiceId)},
+ {_id: choiceId},
{$push: {walletAddresses: walletVote}
})
+ console.log("[choices.update:eth:initial-vote]", { reqId, choiceId });
}
}
return response.json({ success: true });
}
catch (error) {
- console.log("error: ", error);
+ console.error("[choices.update:eth:error]", { reqId, error: error?.message, stack: error?.stack });
return response.status(400).send({
message: error.message,
});
@@ -185,35 +203,34 @@ const updateChoiceById = async (req, response) => {
else {
try {
let oldVote = null;
+ console.log("[choices.update:tz:parse]", { reqId, payloadBytesLen: payloadBytes?.length });
const values = getInputFromSigPayload(payloadBytes);
+ console.log("[choices.update:tz:values]", { reqId, count: values?.length || 0 });
const payloadDate = getTimestampFromPayloadBytes(payloadBytes);
-
- let db_connect = dbo.getDb("Lite");
+ console.log("[choices.update:tz:payload-date]", { reqId, payloadDate });
const pollID = values[0].pollID;
+ console.log("[choices.update:tz:poll-id]", { reqId, pollID });
- const poll = await db_connect
- .collection("Polls")
- .findOne({ _id: ObjectId(pollID) });
+ const poll = await PollModel.findById(pollID);
+ console.log("[choices.update:tz:poll]", { reqId, found: Boolean(poll) });
if (timeNow > poll.endTime) {
throw new Error("Proposal Already Ended");
}
- const dao = await db_connect
- .collection("DAOs")
- .findOne({ _id: ObjectId(poll.daoID) });
+ const dao = await DAOModel.findById(poll.daoID);
+ console.log("[choices.update:tz:dao]", { reqId, found: Boolean(dao) });
- const token = await db_connect
- .collection("Tokens")
- .findOne({ tokenAddress: dao.tokenAddress });
+ const token = await TokenModel.findOne({ tokenAddress: dao.tokenAddress });
+ console.log("[choices.update:tz:token]", { reqId, tokenAddress: token?.tokenAddress });
const block = poll.referenceBlock;
const address = getPkhfromPk(publicKey);
+ console.log("[choices.update:tz:address]", { reqId, address });
- // Validate values
if (values.length === 0) {
throw new Error("No choices sent in the request");
}
@@ -244,6 +261,7 @@ const updateChoiceById = async (req, response) => {
address,
poll.isXTZ
);
+ console.log("[choices.update:tz:total]", { reqId, total: total?.toString?.() || total });
if (!total) {
throw new Error("Could not get total power at reference block");
@@ -252,22 +270,21 @@ const updateChoiceById = async (req, response) => {
if (total.eq(0)) {
throw new Error("No balance at proposal level");
}
- const isVoted = await db_connect
- .collection('Choices')
- .find({
+
+ // Acquire lock to prevent race condition on concurrent votes
+ await withVoteLock(pollID, address, async () => {
+ const isVoted = await ChoiceModel.find({
pollID: poll._id,
walletAddresses: { $elemMatch: { address: address } },
- })
- .toArray();
-
+ }).lean();
+ console.log("[choices.update:tz:is-voted]", { reqId, count: isVoted?.length || 0 });
- if (isVoted.length > 0) {
+ if (isVoted.length > 0) {
const oldVoteObj = isVoted[0].walletAddresses.find(x => x.address === address);
- oldVote = await db_connect.collection("Choices").findOne({
- _id: ObjectId(oldVoteObj.choiceId),
- });
+ // isVoted[0] is already the Choice document containing the old vote
+ oldVote = isVoted[0];
- const oldSignaturePayload = oldVote.walletAddresses[0].payloadBytes
+ const oldSignaturePayload = oldVoteObj?.payloadBytes;
if (oldSignaturePayload) {
const oldSignatureDate =
getTimestampFromPayloadBytes(oldSignaturePayload);
@@ -278,18 +295,6 @@ const updateChoiceById = async (req, response) => {
}
}
- // const ipfsProof = getIPFSProofFromPayload(payloadBytes, signature)
- // const cidLink = await uploadToIPFS(ipfsProof).catch(error => {
- // console.error('IPFS Error', error)
- // return null;
- // });
- // if (!cidLink) {
- // throw new Error(
- // "Could not upload proof to IPFS, Vote was not registered. Please try again later"
- // );
- // }
-
- // TODO: Optimize this Promise.all
await Promise.all(
values.map(async (value) => {
const { choiceId } = value;
@@ -302,138 +307,91 @@ const updateChoiceById = async (req, response) => {
signature,
};
- // TODO: Enable this when the IPFS CID is added to the walletVote object
- // walletVote.cidLink = cidLink;
+ const choice = await ChoiceModel.findById(choiceId);
- const choice = await db_connect
- .collection("Choices")
- .findOne({ _id: ObjectId(choiceId) });
if (isVoted.length > 0) {
if (poll.votingStrategy === 0) {
- const mongoClient = dbo.getClient();
- const session = mongoClient.startSession();
-
- let newData = {
- $push: {
- walletAddresses: walletVote,
- },
- };
-
- let remove = {
- $pull: {
- walletAddresses: {
- address,
- },
- },
- };
+ const session = await mongoose.startSession();
+ session.startTransaction();
try {
- await session.withTransaction(async () => {
- const coll1 = db_connect.collection("Choices");
- // const coll2 = db_connect.collection("Polls");
-
-
- // Important:: You must pass the session to the operations
- if (oldVote) {
- await coll1.updateOne(
- { _id: ObjectId(oldVote._id) },
- remove,
- { remove: true },
- { session }
- );
- }
-
- await coll1.updateOne({ _id: ObjectId(choice._id) }, newData, {
- session,
- });
- });
- // .then((res) => response.json({ success: true }));
+ if (oldVote) {
+ await ChoiceModel.updateOne(
+ { _id: oldVote._id },
+ { $pull: { walletAddresses: { address } } },
+ { session }
+ );
+ }
+
+ await ChoiceModel.updateOne(
+ { _id: choice._id },
+ { $push: { walletAddresses: walletVote } },
+ { session }
+ );
+
+ await session.commitTransaction();
} catch (e) {
- result = e.Message;
- console.log(e);
+ console.error("[choices.update:tz:tx-error]", { reqId, error: e?.message, stack: e?.stack });
await session.abortTransaction();
- throw new Error(e);
+ console.log(e);
+ throw e;
} finally {
await session.endSession();
}
} else {
- const mongoClient = dbo.getClient();
- const session = mongoClient.startSession();
+ const session = await mongoose.startSession();
+ session.startTransaction();
const distributedWeight = total.div(new BigNumber(values.length));
-
walletVote.balanceAtReferenceBlock = distributedWeight.toString();
- let remove = {
- $pull: {
- walletAddresses: { address: address },
- },
- };
-
try {
- // FIRST REMOVE OLD ADDRESS VOTES
- // Fix All polls votes removed
- await db_connect
- .collection("Choices")
- .updateMany({ pollID: poll._id }, remove, { remove: true });
-
- await session
- .withTransaction(async () => {
- const coll1 = db_connect.collection("Choices");
- await coll1.updateOne(
- {
- _id: choice._id,
- },
- { $push: { walletAddresses: walletVote } },
- { upsert: true }
- );
-
- i++;
- })
- .then((res) => {
- if (i === values.length) {
- // response.json({ success: true });
- }
- });
+ await ChoiceModel.updateMany(
+ { pollID: poll._id },
+ { $pull: { walletAddresses: { address } } },
+ { session }
+ );
+
+ await ChoiceModel.updateOne(
+ { _id: choice._id },
+ { $push: { walletAddresses: walletVote } },
+ { session, upsert: true }
+ );
+
+ await session.commitTransaction();
+ i++;
} catch (e) {
- result = e.Message;
- console.log(e);
+ console.error("[choices.update:tz:tx-error]", { reqId, error: e?.message, stack: e?.stack });
await session.abortTransaction();
- throw new Error(e);
+ console.log(e);
+ throw e;
} finally {
await session.endSession();
}
}
} else {
- let newId = { _id: ObjectId(choice._id) };
-
if (values.length > 1) {
const distributedWeight = total.div(new BigNumber(values.length));
walletVote.balanceAtReferenceBlock = distributedWeight.toString();
}
- let data = {
- $push: {
- walletAddresses: walletVote,
- },
- };
- const res = await db_connect
- .collection("Choices")
- .updateOne(newId, data, { upsert: true });
- j++;
+ await ChoiceModel.updateOne(
+ { _id: choice._id },
+ { $push: { walletAddresses: walletVote } },
+ { upsert: true }
+ );
+ console.log("[choices.update:tz:initial-vote]", { reqId, choiceId: choice._id });
- if (j === values.length) {
- // response.json({ success: true });
- } else {
- return;
- }
+ j++;
}
})
);
+ }); // end withVoteLock
+ console.log("[choices.update:tz:success]", { reqId });
response.json({ success: true });
} catch (error) {
- console.log("error: ", error);
+ console.error("[choices.update:tz:error]", { reqId, error: error?.message, stack: error?.stack });
response.status(400).send({
message: error.message,
});
@@ -441,18 +399,12 @@ const updateChoiceById = async (req, response) => {
}
};
-// Get the user's choice
const choicesByUser = async (req, response) => {
- const { id } = req.params.id;
+ const { id } = req.params;
try {
- let db_connect = dbo.getDb();
- const res = await db_connect
- .collection("Choices")
- .findOne({ "walletAddresses.address": id });
-
+ const res = await ChoiceModel.findOne({ "walletAddresses.address": id }).lean();
response.json(res);
-
} catch (error) {
console.log("error: ", error);
response.status(400).send({
@@ -465,12 +417,8 @@ const votesByUser = async (req, response) => {
const { id } = req.params;
try {
- const choices = [];
- let db_connect = dbo.getDb("Lite");
- const cursor = await db_connect.collection("Choices").find({ "walletAddresses.address": id });
- await cursor.forEach((elem) => choices.push(elem));
+ const choices = await ChoiceModel.find({ "walletAddresses.address": id }).lean();
return response.json(choices);
-
} catch (error) {
console.log("error: ", error);
response.status(400).send({
@@ -484,13 +432,7 @@ const getPollVotes = async (req, response) => {
let total = 0;
try {
- const choices = [];
- let db_connect = dbo.getDb("Lite");
- const cursor = await db_connect.collection("Choices").find({
- pollID: ObjectId(id),
- });
-
- await cursor.forEach((elem) => choices.push(elem));
+ const choices = await ChoiceModel.find({ pollID: id }).lean();
choices.forEach((choice) => (total += choice.walletAddresses.length));
return response.json(total);
} catch (error) {
diff --git a/components/daos/index.js b/components/daos/index.js
index 4eaee3d..30dfe22 100644
--- a/components/daos/index.js
+++ b/components/daos/index.js
@@ -1,4 +1,3 @@
-const ObjectId = require("mongodb").ObjectId;
const mongoose = require("mongoose");
const { getTokenMetadata } = require("../../services");
const {
@@ -14,7 +13,6 @@ const {
getEthTokenMetadata,
} = require("../../utils-eth");
-const dbo = require("../../db/conn");
const { getPkhfromPk } = require("@taquito/utils");
const DaoModel = require("../../db/models/Dao.model");
const TokenModel = require("../../db/models/Token.model");
@@ -50,28 +48,18 @@ const getAllLiteOnlyDAOs = async (req, response) => {
}
try {
- let db_connect = dbo.getDb();
+ const allDaos = await DaoModel.find({ network, daoContract: null }).lean();
+ const allDaoIds = allDaos.map(dao => dao._id);
+ const allTokens = await TokenModel.find({ daoID: { $in: allDaoIds } }).lean();
- const TokensCollection = db_connect.collection("Tokens");
- const DAOCollection = db_connect.collection("DAOs");
- const result = await DAOCollection.find({
- network,
- daoContract: null,
- }).toArray();
-
- const newResult = await Promise.all(
- result.map(async (result) => {
- const token = await TokensCollection.findOne({
- daoID: result._id,
- });
-
- return {
- _id: result._id,
- ...token,
- ...result,
- };
- })
- );
+ const newResult = allDaos.map(dao => {
+ const token = allTokens.find(token => token.daoID.toString() === dao._id.toString());
+ return {
+ _id: dao._id,
+ ...token,
+ ...dao,
+ };
+ });
response.json(newResult);
} catch (error) {
@@ -87,17 +75,12 @@ const getDAOFromContractAddress = async (req, response) => {
const { daoContract } = req.params;
try {
- let db_connect = dbo.getDb();
-
- const TokensCollection = db_connect.collection("Tokens");
- const DAOCollection = db_connect.collection("DAOs");
-
- const result = await DAOCollection.findOne({ network, daoContract });
+ const result = await DaoModel.findOne({ network, daoContract }).lean();
if (result) {
- const token = await TokensCollection.findOne({
- daoID: result.id,
- });
+ const token = await TokenModel.findOne({
+ daoID: result._id,
+ }).lean();
const newResult = {
_id: result._id,
@@ -129,6 +112,9 @@ const getDAOById = async (req, response) => {
query.address = { $regex: new RegExp(`^${id}$`, 'i') };
}
let daoDao = await DaoModel.findOne(query)
+ if (!daoDao) {
+ return response.status(404).json({ error: 'DAO not found' });
+ }
daoDao = await daoDao.toObject()
if(include === "polls"){
@@ -150,11 +136,7 @@ const getDAOById = async (req, response) => {
}
try {
- let db_connect = dbo.getDb();
- const DAOCollection = db_connect.collection("DAOs");
- let daoId = { _id: ObjectId(id) };
- const result = await DAOCollection.findOne(daoId);
-
+ const result = await DaoModel.findById(id).lean();
response.json(result);
} catch (error) {
console.log("error: ", error);
@@ -167,18 +149,12 @@ const getDAOById = async (req, response) => {
const updateTotalCount = async (req, response) => {
const { id } = req.params;
try {
- let db_connect = dbo.getDb();
-
- const DAOCollection = db_connect.collection("DAOs");
- let communityId = { _id: ObjectId(id) };
- const dao = await DAOCollection.findOne(communityId);
+ const dao = await DaoModel.findById(id);
if (!dao) {
throw new Error("DAO not found");
}
- const token = await db_connect
- .collection("Tokens")
- .findOne({ tokenAddress: dao.tokenAddress });
+ const token = await TokenModel.findOne({ tokenAddress: dao.tokenAddress });
if (!token) {
throw new Error("DAO Token Does not exist in system");
}
@@ -197,14 +173,10 @@ const updateTotalCount = async (req, response) => {
);
}
- let data = {
- $set: {
- votingAddressesCount: count,
- },
- };
- const res = await db_connect
- .collection("DAOs")
- .updateOne(communityId, data, { upsert: true });
+ const res = await DaoModel.updateOne(
+ { _id: id },
+ { $set: { votingAddressesCount: count } }
+ );
response.json(res);
} catch (error) {
@@ -217,20 +189,22 @@ const updateTotalCount = async (req, response) => {
const updateTotalHolders = async (req, response) => {
try {
- let db_connect = dbo.getDb();
- const DAOCollection = db_connect.collection("DAOs");
-
- const result = await DAOCollection.find({}).forEach(function (item) {
- DAOCollection.updateOne(
- { _id: ObjectId(item._id) },
- {
- $set: {
- votingAddressesCount: item.members ? item.members.length : 0,
- },
- }
- );
- });
- response.json(result);
+ const allDaos = await DaoModel.find({}).lean();
+
+ await Promise.all(
+ allDaos.map(async (item) => {
+ await DaoModel.updateOne(
+ { _id: item._id },
+ {
+ $set: {
+ votingAddressesCount: item.members ? item.members.length : 0,
+ },
+ }
+ );
+ })
+ );
+
+ response.json({ success: true });
} catch (error) {
console.log("error: ", error);
response.status(400).send({
@@ -323,13 +297,6 @@ const createDAO = async (req, response) => {
daoContract,
} = values;
- let db_connect = dbo.getDb();
-
- const mongoClient = dbo.getClient();
- const session = mongoClient.startSession();
-
- const original_id = ObjectId();
-
const tokenData = await getTokenMetadata(tokenAddress, network, tokenID);
const address = getPkhfromPk(publicKey);
@@ -344,7 +311,6 @@ const createDAO = async (req, response) => {
tokenType: tokenData.standard,
requiredTokenOwnership,
allowPublicAccess,
- _id: original_id,
network,
daoContract,
votingAddressesCount: 0,
@@ -364,31 +330,27 @@ const createDAO = async (req, response) => {
throw new Error("User does not have balance for this DAO token");
}
+ const session = await mongoose.startSession();
+ session.startTransaction();
+
try {
- await session
- .withTransaction(async () => {
- const DAOCollection = db_connect.collection("DAOs");
- const TokenCollection = db_connect.collection("Tokens");
- // Important:: You must pass the session to the operations
- await DAOCollection.insertOne(DAOData, { session });
-
- await TokenCollection.insertOne(
- {
- tokenAddress,
- tokenType: tokenData.standard,
- symbol: tokenData.metadata.symbol,
- tokenID: Number(tokenID),
- daoID: original_id,
- decimals: Number(tokenData.metadata.decimals),
- },
- { session }
- );
- })
- .then((res) => response.json(res));
+ const createdDao = await DaoModel.create([DAOData], { session });
+
+ await TokenModel.create([{
+ tokenAddress,
+ tokenType: tokenData.standard,
+ symbol: tokenData.metadata.symbol,
+ tokenID: Number(tokenID),
+ daoID: createdDao[0]._id,
+ decimals: Number(tokenData.metadata.decimals),
+ }], { session });
+
+ await session.commitTransaction();
+ response.json({ dao: createdDao[0] });
} catch (e) {
- result = e.Message;
- console.log(e);
await session.abortTransaction();
+ console.log(e);
+ throw e;
} finally {
await session.endSession();
}
@@ -404,37 +366,22 @@ const joinDAO = async (req, response) => {
const { payloadBytes, publicKey } = req.body;
try {
- let db_connect = dbo.getDb();
- const DAOCollection = db_connect.collection("DAOs");
const values = getInputFromSigPayload(payloadBytes);
const { daoId } = values;
const address = getPkhfromPk(publicKey);
- let id = { _id: ObjectId(daoId) };
- let data = [
- {
- $set: {
- members: {
- $cond: [
- {
- $in: [address, "$members"],
- },
- {
- $setDifference: ["$members", [address]],
- },
- {
- $concatArrays: ["$members", [address]],
- },
- ],
- },
- },
- },
- ];
-
- await DAOCollection.updateOne(id, data);
+ const dao = await DaoModel.findById(daoId);
+
+ if (dao.members.includes(address)) {
+ dao.members = dao.members.filter(m => m !== address);
+ } else {
+ dao.members.push(address);
+ }
+
+ await dao.save();
- response.json(res);
+ response.json({ success: true });
} catch (error) {
console.log("error: ", error);
response.status(400).send({
diff --git a/components/polls/index.js b/components/polls/index.js
index 6cd2173..98139b1 100644
--- a/components/polls/index.js
+++ b/components/polls/index.js
@@ -1,8 +1,7 @@
const md5 = require('md5');
+const mongoose = require("mongoose");
-// This will help us connect to the database
const { getPkhfromPk } = require("@taquito/utils");
-const dbo = require("../../db/conn");
const {
getInputFromSigPayload,
getCurrentBlock,
@@ -20,7 +19,12 @@ const ChoiceModel = require("../../db/models/Choice.model");
const { getEthCurrentBlockNumber, getEthTotalSupply } = require("../../utils-eth");
-const ObjectId = require("mongodb").ObjectId;
+function validateExternalLink(externalLink) {
+ if (!externalLink || typeof externalLink !== 'string') {
+ return '';
+ }
+ return externalLink.startsWith('https://') ? externalLink : '';
+}
async function _getPollData(mode="lite", {
daoId, network, tokenAddress = null, authorAddress = null, payloadBytes = null
@@ -105,14 +109,22 @@ const getPollById = async (req, response) => {
const { id } = req.params;
try {
- let db_connect = dbo.getDb();
- let pollId = { _id: ObjectId(id) };
+ const result = await PollModel.findById(id).lean();
+
+ if (!result) {
+ return response.status(404).json({
+ message: "Poll not found",
+ });
+ }
+
+ // No Sanitization for Tezos Ecosystem
+ let shouldSkipSanitzation = result?.daoID === "64ef1c7d514de7b078cb8ed2"
- const result = await db_connect.collection("Polls").findOne(pollId);
response.json({
...result,
name: result.name?.replace(/<[^>]*>/g, ''),
- description: result.description?.replace(/<[^>]*>/g, ''),
+ description: shouldSkipSanitzation ? result.description : result.description?.replace(/<[^>]*>/g, ''),
+ externalLink: validateExternalLink(result.externalLink),
});
} catch (error) {
console.log("error: ", error);
@@ -124,17 +136,23 @@ const getPollById = async (req, response) => {
const getPollsById = async (req, response) => {
const { id } = req.params;
+ let shouldSkipSanitzation = false;
try {
- let db_connect = dbo.getDb();
-
- const polls = await db_connect
- .collection("Polls")
- .find({ daoID: id })
+ const polls = await PollModel.find({ daoID: id })
.sort({ _id: -1 })
- .toArray();
+ .lean();
- response.json(polls);
+ const pollsFilltered = polls.map(poll => {
+ return {
+ ...poll,
+ name: poll.name.replace(/<[^>]*>/g, ''),
+ description: poll.description.replace(/<[^>]*>/g, ''),
+ externalLink: validateExternalLink(poll.externalLink),
+ }
+ })
+
+ response.json(pollsFilltered);
} catch (error) {
console.log("error: ", error);
response.status(400).send({
@@ -208,7 +226,7 @@ const addPoll = async (req, response) => {
name,
author,
description,
- externalLink,
+ externalLink: validateExternalLink(externalLink),
startTime,
endTime,
daoID,
@@ -244,7 +262,7 @@ const addPoll = async (req, response) => {
if(daoMode == "lite"){
await DaoModel.updateOne(
- { _id: ObjectId(daoID) },
+ { _id: daoID },
{
$push: { polls: pollId },
}
@@ -257,7 +275,7 @@ const addPoll = async (req, response) => {
tokenAddress: payload?.tokenAddress,
tokenType:"erc20",
$push: { polls: pollId },
- votingAddressesCount: 0 // TODO: @ashutoshpw
+ votingAddressesCount: 0
},
{ upsert: true, new: true }
);
@@ -289,14 +307,7 @@ const addPoll = async (req, response) => {
const author = getPkhfromPk(publicKey);
- const mongoClient = dbo.getClient();
- const session = mongoClient.startSession();
- let db_connect = dbo.getDb();
-
- const poll_id = ObjectId();
-
const currentTime = new Date().valueOf();
-
const startTime = currentTime;
if (choices.length === 0) {
@@ -314,16 +325,12 @@ const addPoll = async (req, response) => {
throw new Error("Duplicate choices found");
}
- const dao = await db_connect
- .collection("DAOs")
- .findOne({ _id: ObjectId(daoID) });
+ const dao = await DaoModel.findById(daoID);
if (!dao) {
throw new Error("DAO Does not exist");
}
- const token = await db_connect
- .collection("Tokens")
- .findOne({ tokenAddress: dao.tokenAddress });
+ const token = await TokenModel.findOne({ tokenAddress: dao.tokenAddress });
if (!token) {
throw new Error("DAO Token Does not exist in system");
}
@@ -352,84 +359,68 @@ const addPoll = async (req, response) => {
}
if (!total) {
- await session.abortTransaction();
+ throw new Error("Could not fetch total supply");
}
- const choicesData = choices.map((element) => {
- return {
- name: element,
- pollID: poll_id,
- walletAddresses: [],
- _id: ObjectId(),
- };
- });
- const choicesPoll = choicesData.map((element) => {
- return element._id;
- });
-
- const doesPollExists = await db_connect
- .collection("Polls")
- .findOne({ payloadBytes });
+ const doesPollExists = await PollModel.findOne({ payloadBytes });
if (doesPollExists) {
throw new Error("Invalid Signature, Poll already exists");
}
- // const cidLink = await uploadToIPFS(
- // getIPFSProofFromPayload(payloadBytes, signature)
- // );
- // if (!cidLink) {
- // throw new Error(
- // "Could not upload proof to IPFS, Vote was not registered. Please try again later"
- // );
- // }
+ const session = await mongoose.startSession();
+ session.startTransaction();
- let PollData = {
- name,
- description,
- externalLink,
- startTime,
- endTime,
- daoID,
- referenceBlock: block,
- totalSupplyAtReferenceBlock: total,
- _id: poll_id,
- choices: choicesPoll,
- author,
- votingStrategy,
- isXTZ,
- payloadBytes,
- signature,
- cidLink: "",
- };
+ try {
+ const PollData = {
+ name,
+ description,
+ externalLink: validateExternalLink(externalLink),
+ startTime,
+ endTime,
+ daoID,
+ referenceBlock: block,
+ totalSupplyAtReferenceBlock: total,
+ author,
+ votingStrategy,
+ isXTZ,
+ payloadBytes,
+ signature,
+ cidLink: "",
+ };
- let data = {
- $push: {
- polls: poll_id,
- },
- };
+ const createdPoll = await PollModel.create([PollData], { session });
+ const poll_id = createdPoll[0]._id;
- let id = { _id: ObjectId(daoID) };
+ const choicesData = choices.map((element) => {
+ return {
+ name: element,
+ pollID: poll_id,
+ walletAddresses: [],
+ };
+ });
- try {
- await session
- .withTransaction(async () => {
- const coll1 = db_connect.collection("Polls");
- const coll2 = db_connect.collection("Choices");
- const coll3 = db_connect.collection("DAOs");
- // Important:: You must pass the session to the operations
- await coll1.insertOne(PollData, { session });
-
- await coll2.insertMany(choicesData, { session });
-
- await coll3.updateOne(id, data, { session });
- })
- .then((res) => response.json({ res, pollId: poll_id }));
+ const createdChoices = await ChoiceModel.insertMany(choicesData, { session });
+ const choicesPoll = createdChoices.map((element) => element._id);
+
+ await PollModel.updateOne(
+ { _id: poll_id },
+ { $set: { choices: choicesPoll } },
+ { session }
+ );
+
+ await DaoModel.updateOne(
+ { _id: daoID },
+ { $push: { polls: poll_id } },
+ { session }
+ );
+
+ await session.commitTransaction();
+ response.json({ pollId: poll_id });
} catch (e) {
- result = e.Message;
- console.log(e);
await session.abortTransaction();
- throw new Error(e);
+ console.log(e);
+ throw e;
} finally {
await session.endSession();
}
diff --git a/components/tokens/index.js b/components/tokens/index.js
index 525b04f..53d0416 100644
--- a/components/tokens/index.js
+++ b/components/tokens/index.js
@@ -1,30 +1,23 @@
-// This will help us connect to the database
const mongoose = require("mongoose");
-const mongodb = require("mongodb");
-const dbo = require("../../db/conn");
const TokenModel = require("../../db/models/Token.model");
const DAOModel = require("../../db/models/Dao.model");
const { getUserTotalVotingPowerAtReferenceBlock } = require("../../utils");
const { getEthTokenMetadata, getEthUserBalanceAtLevel } = require("../../utils-eth");
-
-const ObjectId = mongodb.ObjectId;
const addToken = async (req, response) => {
const { daoID, tokenID, symbol, tokenAddress } = req.body;
try {
- let db_connect = dbo.getDb();
- const TokensCollection = db_connect.collection("Tokens");
-
- let data = {
+ const data = {
daoID,
tokenID,
symbol,
tokenAddress,
+ tokenType: "FA2",
+ decimals: "0"
};
- await TokensCollection.insertOne(data);
-
- response.json(data);
+ const createdToken = await TokenModel.create(data);
+ response.json(createdToken);
} catch (error) {
console.log("error: ", error);
response.status(400).send({
@@ -68,20 +61,13 @@ const getVotingPowerAtLevel = async (req, response) => {
}
try {
- let db_connect = dbo.getDb();
-
- const TokensCollection = db_connect.collection("Tokens");
- const DAOCollection = db_connect.collection("DAOs");
-
- let tokenAddress = { tokenAddress: address };
- const token = await TokensCollection.findOne(tokenAddress);
+ const token = await TokenModel.findOne({ tokenAddress: address });
if (!token) {
throw new Error("Could not find token");
}
- let daoId = { _id: ObjectId(token.daoID) };
- const dao = await DAOCollection.findOne(daoId);
+ const dao = await DAOModel.findById(token.daoID);
const daoContract = dao?.daoContract;
diff --git a/db/cache.db.js b/db/cache.db.js
index 486c62b..a5d86b9 100644
--- a/db/cache.db.js
+++ b/db/cache.db.js
@@ -1,8 +1,35 @@
-const cache = require('persistent-cache');
+const isServerless = process.env.NETLIFY || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.VERCEL || process.env.NETLIFY_DEV;
-const dbCache = cache({
- base:'./node_modules/.cache/',
- name:'mongo',
-})
+const noOpCache = {
+ getSync: () => null,
+ put: (key, value, callback) => {
+ if (callback) callback(null);
+ },
+ clear: (callback) => {
+ if (callback) callback(null);
+ }
+};
-module.exports = dbCache
\ No newline at end of file
+let dbCache;
+
+if (isServerless) {
+ dbCache = noOpCache;
+} else {
+ try {
+ const cache = require('persistent-cache');
+ dbCache = cache({
+ base: './node_modules/.cache/',
+ name: 'mongo',
+ });
+ } catch (error) {
+ if (error.code === 'EROFS' || error.message.includes('read-only file system')) {
+ console.warn('Read-only filesystem detected, disabling persistent cache');
+ dbCache = noOpCache;
+ } else {
+ console.warn('Failed to initialize persistent cache, using in-memory fallback:', error.message);
+ dbCache = noOpCache;
+ }
+ }
+}
+
+module.exports = dbCache;
\ No newline at end of file
diff --git a/db/conn.js b/db/conn.js
deleted file mode 100644
index 9ce27c8..0000000
--- a/db/conn.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const { MongoClient } = require("mongodb");
-
-const dbURI = process.env.NODE_ENV === 'test' ? process.env.TEST_MONGO_URI : process.env.ATLAS_URI;
-
-const client = new MongoClient(dbURI, {
- useNewUrlParser: true,
- useUnifiedTopology: true,
-});
-
-let _db;
-
-async function connectToServer() {
- const db = await client.connect();
- // Verify we got a good "db" object
- if (db) {
- _db = db.db("Lite");
- console.log("Successfully connected to MongoDB.");
- }
-}
-
-function getDb() {
- return _db;
-}
-
-function getClient() {
- return client;
-}
-
-module.exports = {
- connectToServer,
- getDb,
- getClient,
-};
\ No newline at end of file
diff --git a/db/mongoose-connection.js b/db/mongoose-connection.js
new file mode 100644
index 0000000..d7f5ae4
--- /dev/null
+++ b/db/mongoose-connection.js
@@ -0,0 +1,47 @@
+const mongoose = require('mongoose');
+
+let cachedConnection = null;
+
+function getMongoDBDatabaseName(url) {
+ const dbNameMatch = url.match(/\/([^/?]+)(\?|$)/);
+ return dbNameMatch ? dbNameMatch[1] : null;
+}
+
+async function connectToMongoose() {
+ if (cachedConnection && mongoose.connection.readyState === 1) {
+ console.log('Using cached MongoDB connection');
+ return cachedConnection;
+ }
+
+ try {
+ let connUrl = process.env.NODE_ENV === 'test'
+ ? process.env.TEST_MONGO_URI
+ : process.env.ATLAS_URI;
+
+ if (!connUrl) {
+ throw new Error('MongoDB connection string (ATLAS_URI) is not set. Please configure it in Netlify environment variables.');
+ }
+
+ const database = getMongoDBDatabaseName(connUrl);
+ if (!database) {
+ const urlParts = connUrl.split('?');
+ connUrl = `${urlParts[0]}Lite?${urlParts[1] || ''}`;
+ }
+
+ await mongoose.connect(connUrl, {
+ serverSelectionTimeoutMS: 5000,
+ socketTimeoutMS: 45000,
+ });
+
+ cachedConnection = mongoose.connection;
+ console.log('Connected to MongoDB using Mongoose');
+ return cachedConnection;
+ } catch (error) {
+ console.error('Error connecting to MongoDB:', error);
+ throw error;
+ }
+}
+
+module.exports = { connectToMongoose };
+
+
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..17392db
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,20 @@
+module.exports = {
+ apps: [
+ {
+ name: "homebase-api",
+ script: "server.js",
+ // Run with Node (project uses CommonJS and dotenv via config.js)
+ interpreter: "node",
+ instances: 1,
+ exec_mode: "fork",
+ watch: false,
+ env: {
+ NODE_ENV: "development",
+ },
+ env_production: {
+ NODE_ENV: "production",
+ },
+ },
+ ],
+};
+
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..3b1dcb6
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,36 @@
+module.exports = {
+ testEnvironment: 'node',
+ globalSetup: '',
+ linkToTerms: 'javascript:alert("xss")',
+ picUri: 'ipfs://test',
+ requiredTokenOwnership: true,
+ allowPublicAccess: true,
+ daoContract: null,
+ decimals: '18',
+ publicKey: '0xUser1'
+ };
+
+ const response = await request(app)
+ .post('/dao/add')
+ .send(maliciousPayload)
+ .expect(200);
+
+ const dao = await DaoModel.findById(response.body.dao._id);
+ expect(dao.name).not.toContain('
Test description
' + })); + + const response = await request(app) + .get(`/polls/${poll._id}`) + .expect(200); + + expect(response.body.name).not.toContain('