diff --git a/components/choices/index.js b/components/choices/index.js index d57d6ba..141745a 100644 --- a/components/choices/index.js +++ b/components/choices/index.js @@ -1,5 +1,5 @@ const express = require("express"); - +const md5 = require("md5"); // This will help us connect to the database const dbo = require("../../db/conn"); const { @@ -11,6 +11,10 @@ const { const { default: BigNumber } = require("bignumber.js"); const { getPkhfromPk } = require("@taquito/utils"); const { uploadToIPFS } = require("../../services/ipfs.service"); +const DAOModel = require("../../db/models/Dao.model"); +const TokenModel = require("../../db/models/Token.model"); +const PollModel = require("../../db/models/Poll.model"); +const ChoiceModel = require("../../db/models/Choice.model"); // This help convert the id from string to ObjectId for the _id. const ObjectId = require("mongodb").ObjectId; @@ -37,266 +41,404 @@ const getChoiceById = async (req, response) => { const updateChoiceById = async (req, response) => { const { payloadBytes, publicKey, signature } = req.body; - + const network = req.body.network; let j = 0; let i = 0; + const timeNow = new Date().valueOf(); - try { - let oldVote = null; - const values = getInputFromSigPayload(payloadBytes); - - const payloadDate = getTimestampFromPayloadBytes(payloadBytes); + if (network?.startsWith("etherlink")) { + try { + console.log('[payload]', req.payloadObj) + 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 + const poll = await PollModel.findById(pollId) - let db_connect = dbo.getDb("Lite"); + if(!poll) throw new Error("Poll not found") - const pollID = values[0].pollID; + if (timeNow > Number(poll.endTime)) { + throw new Error("Proposal Already Ended"); + } - const poll = await db_connect - .collection("Polls") - .findOne({ _id: ObjectId(pollID) }); + const dao = await DAOModel.findById(poll.daoID) + if (!dao) throw new Error(`DAO not found: ${poll.daoID}`) - const timeNow = new Date().valueOf(); + const token = await TokenModel.findOne({ tokenAddress: dao.tokenAddress }) + const block = poll.referenceBlock; - if (timeNow > poll.endTime) { - throw new Error("Proposal Already Ended"); - } + castedChoices.forEach((value) => { + if (value.address !== address) { + throw new Error("Invalid Proposal Body, Invalid Address in choices"); + } + if (value.pollID !== pollId) { + throw new Error("Invalid Proposal Body, Invalid Poll ID in choices"); + } + }); - const dao = await db_connect - .collection("DAOs") - .findOne({ _id: ObjectId(poll.daoID) }); + const choiceIds = castedChoices.map((value) => value.choiceId); + let duplicates = choiceIds.filter( + (item, index) => choiceIds.indexOf(item.trim()) !== index + ); + if (duplicates.length > 0) throw new Error("Duplicate choices found"); + + // TODO: Check if the user has enough balance to vote + // const total = await getUserTotalVotingPowerAtReferenceBlock( + // dao.network, + // dao.tokenAddress, + // dao.daoContract, + // token.tokenID, + // block, + // address, + // poll.isXTZ + // ); + + // if (!total) { + // throw new Error("Could not get total power at reference block"); + // } + + // if (total.eq(0)) { + // throw new Error("No balance at proposal level"); + // } + const isVoted = await ChoiceModel.find({ + pollId: poll._id, + walletAddresses: { $elemMatch: { address: address } } + }); - const token = await db_connect - .collection("Tokens") - .findOne({ tokenAddress: dao.tokenAddress }); - const block = poll.referenceBlock; + const walletVote = { + address, + balanceAtReferenceBlock: 1, + // balanceAtReferenceBlock: total.toString(), + payloadBytes, + payloadBytesHash: md5(payloadBytes), + signature, + }; + + if (isVoted.length > 0) { + const oldVoteObj = isVoted[0].walletAddresses.find(x => x.address === address); + oldVote = await ChoiceModel.findById(oldVoteObj.choiceId); + + // TODO: Enable Repeat Vote + // const oldSignaturePayload = oldVote.walletAddresses[0].payloadBytes + // if (oldSignaturePayload) { + // const oldSignatureDate = + // getTimestampFromPayloadBytes(oldSignaturePayload); + + // if (payloadDate <= oldSignatureDate) { + // throw new Error("Invalid Signature"); + // } + // } + + for (value of castedChoices) { + const choiceId = value.choiceId + const updatePayload = { + $push: { + walletAddresses: { + ...walletVote, + choiceId, + } + }, + } + if (oldVote) updatePayload.$pull = { walletAddresses: { address: address } } - const address = getPkhfromPk(publicKey); + if (poll.votingStrategy === 0) { + await ChoiceModel.updateOne( + { _id: choiceId }, + updatePayload + ) + } else { + await ChoiceModel.updateMany( + { pollID: poll._id }, + { $pull: { walletAddresses: { address } } }, + { remove: true } + ) + await ChoiceModel.updateOne( + { _id: choiceId }, + updatePayload, + { upsert: true } + ) + } + } - // Validate values - if (values.length === 0) { - throw new Error("No choices sent in the request"); + } else { + if (castedChoices.length > 1) { + // const distributedWeight = total.div(new BigNumber(values.length)); + // walletVote.balanceAtReferenceBlock = distributedWeight.toString(); + } + for(const choice of castedChoices){ + const choiceId = choice.choiceId + await ChoiceModel.updateOne( + {_id: ObjectId(choiceId)}, + {$push: {walletAddresses: walletVote} + }) + } + } + return response.json({ success: true }); } + catch (error) { + console.log("error: ", error); + return response.status(400).send({ + message: error.message, + }); + } + } + else { + try { + let oldVote = null; + const values = getInputFromSigPayload(payloadBytes); - values.forEach((value) => { - if (value.address !== address) { - throw new Error("Invalid Proposal Body, Invalid Address in choices"); - } - if (value.pollID !== pollID) { - throw new Error("Invalid Proposal Body, Invalid Poll ID in choices"); + const payloadDate = getTimestampFromPayloadBytes(payloadBytes); + + let db_connect = dbo.getDb("Lite"); + + const pollID = values[0].pollID; + + const poll = await db_connect + .collection("Polls") + .findOne({ _id: ObjectId(pollID) }); + + if (timeNow > poll.endTime) { + throw new Error("Proposal Already Ended"); } - }); - const choiceIds = values.map((value) => value.choiceId); - let duplicates = choiceIds.filter( - (item, index) => choiceIds.indexOf(item.trim()) !== index - ); - if (duplicates.length > 0) { - throw new Error("Duplicate choices found"); - } + const dao = await db_connect + .collection("DAOs") + .findOne({ _id: ObjectId(poll.daoID) }); - const total = await getUserTotalVotingPowerAtReferenceBlock( - dao.network, - dao.tokenAddress, - dao.daoContract, - token.tokenID, - block, - address, - poll.isXTZ - ); - - if (!total) { - throw new Error("Could not get total power at reference block"); - } + const token = await db_connect + .collection("Tokens") + .findOne({ tokenAddress: dao.tokenAddress }); - if (total.eq(0)) { - throw new Error("No balance at proposal level"); - } - const isVoted = await db_connect - .collection('Choices') - .find({ - pollID: poll._id, - walletAddresses: { $elemMatch: { address: address } }, - }) - .toArray(); - - - 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), + const block = poll.referenceBlock; + + const address = getPkhfromPk(publicKey); + + // Validate values + if (values.length === 0) { + throw new Error("No choices sent in the request"); + } + + values.forEach((value) => { + if (value.address !== address) { + throw new Error("Invalid Proposal Body, Invalid Address in choices"); + } + if (value.pollID !== pollID) { + throw new Error("Invalid Proposal Body, Invalid Poll ID in choices"); + } }); - const oldSignaturePayload = oldVote.walletAddresses[0].payloadBytes - if (oldSignaturePayload) { - const oldSignatureDate = - getTimestampFromPayloadBytes(oldSignaturePayload); + const choiceIds = values.map((value) => value.choiceId); + let duplicates = choiceIds.filter( + (item, index) => choiceIds.indexOf(item.trim()) !== index + ); + if (duplicates.length > 0) { + throw new Error("Duplicate choices found"); + } - if (payloadDate <= oldSignatureDate) { - throw new Error("Invalid Signature"); + const total = await getUserTotalVotingPowerAtReferenceBlock( + dao.network, + dao.tokenAddress, + dao.daoContract, + token.tokenID, + block, + address, + poll.isXTZ + ); + + if (!total) { + throw new Error("Could not get total power at reference block"); + } + + if (total.eq(0)) { + throw new Error("No balance at proposal level"); + } + const isVoted = await db_connect + .collection('Choices') + .find({ + pollID: poll._id, + walletAddresses: { $elemMatch: { address: address } }, + }) + .toArray(); + + + 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), + }); + + const oldSignaturePayload = oldVote.walletAddresses[0].payloadBytes + if (oldSignaturePayload) { + const oldSignatureDate = + getTimestampFromPayloadBytes(oldSignaturePayload); + + if (payloadDate <= oldSignatureDate) { + throw new Error("Invalid Signature"); + } } } - } - // 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; - - let walletVote = { - address, - balanceAtReferenceBlock: total.toString(), - choiceId, - payloadBytes, - signature, - }; - - // TODO: Enable this when the IPFS CID is added to the walletVote object - // walletVote.cidLink = cidLink; - - 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(); + // 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; + + let walletVote = { + address, + balanceAtReferenceBlock: total.toString(), + choiceId, + payloadBytes, + signature, + }; - let newData = { - $push: { - walletAddresses: walletVote, - }, - }; + // TODO: Enable this when the IPFS CID is added to the walletVote object + // walletVote.cidLink = cidLink; - let remove = { - $pull: { - walletAddresses: { - address, + 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, }, - }, - }; + }; - try { - await session.withTransaction(async () => { - const coll1 = db_connect.collection("Choices"); - // const coll2 = db_connect.collection("Polls"); + let remove = { + $pull: { + walletAddresses: { + address, + }, + }, + }; + + 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 } - ); - } + // 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, + await coll1.updateOne({ _id: ObjectId(choice._id) }, newData, { + session, + }); }); - }); - // .then((res) => response.json({ success: true })); - } catch (e) { - result = e.Message; - console.log(e); - await session.abortTransaction(); - throw new Error(e); - } finally { - await session.endSession(); + // .then((res) => response.json({ success: true })); + } catch (e) { + result = e.Message; + console.log(e); + await session.abortTransaction(); + throw new Error(e); + } finally { + await session.endSession(); + } + } else { + const mongoClient = dbo.getClient(); + const session = mongoClient.startSession(); + + 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 }); + } + }); + } catch (e) { + result = e.Message; + console.log(e); + await session.abortTransaction(); + throw new Error(e); + } finally { + await session.endSession(); + } } } else { - const mongoClient = dbo.getClient(); - const session = mongoClient.startSession(); - - const distributedWeight = total.div(new BigNumber(values.length)); + let newId = { _id: ObjectId(choice._id) }; - walletVote.balanceAtReferenceBlock = distributedWeight.toString(); - - let remove = { - $pull: { - walletAddresses: { address: address }, + 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 }); - try { - // FIRST REMOVE OLD ADDRESS VOTES - // Fix All polls votes removed - await db_connect - .collection("Choices") - .updateMany({ pollID: poll._id }, remove, { remove: true }); + j++; - 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 }); - } - }); - } catch (e) { - result = e.Message; - console.log(e); - await session.abortTransaction(); - throw new Error(e); - } finally { - await session.endSession(); + if (j === values.length) { + // response.json({ success: true }); + } else { + return; } } - } 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++; - - if (j === values.length) { - // response.json({ success: true }); - } else { - return; - } - } - }) - ); - - response.json({ success: true }); - } catch (error) { - console.log("error: ", error); - response.status(400).send({ - message: error.message, - }); + }) + ); + + response.json({ success: true }); + } catch (error) { + console.log("error: ", error); + response.status(400).send({ + message: error.message, + }); + } } }; diff --git a/components/daos/index.js b/components/daos/index.js index d59785c..94456b0 100644 --- a/components/daos/index.js +++ b/components/daos/index.js @@ -8,7 +8,7 @@ const { getTokenHoldersCount, } = require("../../utils"); const { - getEthTokenMetadata, + getEthTokenHoldersCount, getEthCurrentBlock, getEthUserBalanceAtLevel, } = require("../../utils-eth"); @@ -22,22 +22,24 @@ const getAllLiteOnlyDAOs = async (req, response) => { const network = req.body?.network || req.query.network; // Implementation with Mongoose with go live with Etherlink - if(req.method === 'GET'){ + if (req.method === 'GET') { const sortOrder = req.query.order || "desc"; - const allDaos = await DaoModel.find({network}).sort({ + const allDaos = await DaoModel.find({ network }).sort({ _id: sortOrder }).lean(); - + const allDaoIds = allDaos.map(dao => new mongoose.Types.ObjectId(dao._id)); - const allTokens = await TokenModel.find({daoID: {$in: allDaoIds}}).lean(); + const allTokens = await TokenModel.find({ daoID: { $in: allDaoIds } }).lean(); // console.log('All Tokens DAO', [...new Set(allTokens.map(token => token.daoID))]) // console.log('Found Tokens',allDaoIds, allTokens.length) const results = allDaos.map(dao => { const token = allTokens.find(token => token.daoID.toString() === dao._id.toString()); + if (token) delete token._id; // console.log('Token', token) return { + _id: dao._id, ...dao, ...token } @@ -116,6 +118,11 @@ const getDAOFromContractAddress = async (req, response) => { const getDAOById = async (req, response) => { const { id } = req.params; + const daoDao = await DaoModel.findById(id); + console.log({ id, daoDao }) + if (daoDao) { + return response.json(daoDao); + } try { let db_connect = dbo.getDb(); @@ -150,12 +157,19 @@ const updateTotalCount = async (req, response) => { if (!token) { throw new Error("DAO Token Does not exist in system"); } - - const count = await getTokenHoldersCount( - dao.network, - token.tokenAddress, - token.tokenID - ); + let count = 0; + if (dao.network?.startsWith("etherlink")) { + count = await getEthTokenHoldersCount( + dao.network, + token.tokenAddress, + ); + } else { + count = await getTokenHoldersCount( + dao.network, + token.tokenAddress, + token.tokenID + ); + } let data = { $set: { @@ -200,14 +214,14 @@ const updateTotalHolders = async (req, response) => { }; const createDAO = async (req, response) => { - const { payloadBytes, publicKey, } = req.body; + const { payloadBytes, publicKey, } = req.body; const network = req.body.network - if(network && network?.startsWith("etherlink")) { + if (network && network?.startsWith("etherlink")) { const payload = req.payloadObj; const { tokenAddress, tokenID, - symbol:tokenSymbol, + symbol: tokenSymbol, network, name, description, @@ -230,14 +244,14 @@ const createDAO = async (req, response) => { const address = publicKey const block = await getEthCurrentBlock(network); - console.log({block}) + console.log({ block }) const userBalanceAtCurrentLevel = await getEthUserBalanceAtLevel( network, address, tokenAddress, block, ); - console.log({userBalanceAtCurrentLevel}) + console.log({ userBalanceAtCurrentLevel }) // if (userBalanceAtCurrentLevel.eq(0)) { // throw new Error("User does not have balance for this DAO token"); @@ -259,7 +273,7 @@ const createDAO = async (req, response) => { votingAddressesCount: 0, }; - console.log({ethDaoData}) + console.log({ ethDaoData }) const createdDao = await DaoModel.create(ethDaoData); const createdToken = await TokenModel.create({ tokenAddress, diff --git a/components/polls/index.js b/components/polls/index.js index 09ef86c..50bee1f 100644 --- a/components/polls/index.js +++ b/components/polls/index.js @@ -1,3 +1,5 @@ +const md5 = require('md5'); + // This will help us connect to the database const { getPkhfromPk } = require("@taquito/utils"); const dbo = require("../../db/conn"); @@ -10,6 +12,12 @@ const { } = require("../../utils"); const { uploadToIPFS } = require("../../services/ipfs.service"); +const DaoModel = require("../../db/models/Dao.model"); +const TokenModel = require("../../db/models/Token.model"); +const PollModel = require("../../db/models/Poll.model"); +const ChoiceModel = require("../../db/models/Choice.model"); + +const { getEthCurrentBlock, getEthTotalSupply } = require("../../utils-eth"); const ObjectId = require("mongodb").ObjectId; @@ -53,174 +61,289 @@ const getPollsById = async (req, response) => { const addPoll = async (req, response) => { const { payloadBytes, publicKey, signature } = req.body; + const network = req.body.network; - try { - const values = getInputFromSigPayload(payloadBytes); - - const { - choices, - daoID, - name, - description, - externalLink, - endTime, - votingStrategy, - isXTZ - } = values; - - const author = getPkhfromPk(publicKey); - - const mongoClient = dbo.getClient(); - const session = mongoClient.startSession(); - let db_connect = dbo.getDb(); + if (network?.startsWith("etherlink")) { + try { + const payload = req.payloadObj; + const { + choices, + daoID, + name, + description, + externalLink, + endTime, + votingStrategy, + isXTZ, + } = payload; + + if (choices.length === 0) { + throw new Error("No choices sent in the request"); + } + + const currentTime = new Date().valueOf(); + console.log({currentTime, endTime, daoID, payloadBytes}) + if (Number(endTime) <= currentTime) { + throw new Error("End Time has to be in future"); + } + + const duplicates = choices.filter( + (item, index) => choices.indexOf(item.trim()) !== index + ); + if (duplicates.length > 0) { + throw new Error("Duplicate choices found"); + } - const poll_id = ObjectId(); + const dao = await DaoModel.findById(daoID); + if (!dao) throw new Error("DAO Does not exist"); - const currentTime = new Date().valueOf(); + const token = await TokenModel.findOne({ tokenAddress: dao.tokenAddress }); + if (!token) throw new Error("DAO Token Does not exist in system"); - const startTime = currentTime; + const block = await getEthCurrentBlock(dao.network); + const author = publicKey; + const startTime = currentTime; + const totalSupply = await getEthTotalSupply( + dao.network, + dao.tokenAddress, + block + ); - if (choices.length === 0) { - throw new Error("No choices sent in the request"); - } + // TODO: @ashutoshpw To be Implemented + // const userVotingPowerAtCurrentLevel = + // await getUserTotalVotingPowerAtReferenceBlock( + // dao.network, + // dao.tokenAddress, + // dao.daoContract, + // token.tokenID, + // block, + // author + // ); + + // if (userVotingPowerAtCurrentLevel.eq(0) && dao.requiredTokenOwnership) { + // throw new Error( + // "User Doesnt have balance at this level to create proposal" + // ); + // } + + const payloadBytesHash = md5(payloadBytes); + const doesPollExists = await PollModel.findOne({ payloadBytesHash }); + if (doesPollExists) + throw new Error("Invalid Signature, Poll already exists"); + + const PollData = { + name, + author, + description, + externalLink, + startTime, + endTime, + daoID, + referenceBlock: block, + totalSupplyAtReferenceBlock: totalSupply, + signature, + votingStrategy, + payloadBytes, + payloadBytesHash, + cidLink: "", + }; - if (Number(endTime) <= currentTime) { - throw new Error("End Time has to be in future"); - } + const createdPoll = await PollModel.create(PollData); + const pollId = createdPoll._id; - let duplicates = choices.filter( - (item, index) => choices.indexOf(item.trim()) !== index - ); - if (duplicates.length > 0) { - throw new Error("Duplicate choices found"); - } + const choicesData = choices.map((element) => { + return { + name: element, + walletAddresses: [], + pollID: pollId, + }; + }); - const dao = await db_connect - .collection("DAOs") - .findOne({ _id: ObjectId(daoID) }); - if (!dao) { - throw new Error("DAO Does not exist"); - } + await ChoiceModel.insertMany(choicesData); - const token = await db_connect - .collection("Tokens") - .findOne({ tokenAddress: dao.tokenAddress }); - if (!token) { - throw new Error("DAO Token Does not exist in system"); + await DaoModel.updateOne( + { _id: ObjectId(daoID) }, + { + $push: { polls: pollId }, + } + ); + return response.status(200).send({ + message: "Poll Created Successfully", + pollId, + }); + } catch (error) { + console.log("error: ", error); + return response.status(400).send({ + message: error.message, + }); } + } else { + try { + const values = getInputFromSigPayload(payloadBytes); - const block = await getCurrentBlock(dao.network); - const total = await getTotalSupplyAtCurrentBlock( - dao.network, - dao.tokenAddress, - token.tokenID - ); + const { + choices, + daoID, + name, + description, + externalLink, + endTime, + votingStrategy, + isXTZ, + } = values; - const userVotingPowerAtCurrentLevel = - await getUserTotalVotingPowerAtReferenceBlock( - dao.network, - dao.tokenAddress, - dao.daoContract, - token.tokenID, - block, - author - ); + const author = getPkhfromPk(publicKey); - if (userVotingPowerAtCurrentLevel.eq(0) && dao.requiredTokenOwnership) { - throw new Error( - "User Doesnt have balance at this level to create proposal" - ); - } + const mongoClient = dbo.getClient(); + const session = mongoClient.startSession(); + let db_connect = dbo.getDb(); - if (!total) { - await session.abortTransaction(); - } + const poll_id = ObjectId(); - const choicesData = choices.map((element) => { - return { - name: element, - pollID: poll_id, - walletAddresses: [], - _id: ObjectId(), - }; - }); - const choicesPoll = choicesData.map((element) => { - return element._id; - }); + const currentTime = new Date().valueOf(); - const doesPollExists = await db_connect - .collection("Polls") - .findOne({ payloadBytes }); + const startTime = currentTime; - if (doesPollExists) { - throw new Error("Invalid Signature, Poll already exists"); - } + if (choices.length === 0) { + throw new Error("No choices sent in the request"); + } - // 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" - // ); - // } - - - - let PollData = { - name, - description, - externalLink, - startTime, - endTime, - daoID, - referenceBlock: block, - totalSupplyAtReferenceBlock: total, - _id: poll_id, - choices: choicesPoll, - author, - votingStrategy, - isXTZ, - payloadBytes, - signature, - cidLink:"" - }; - - let data = { - $push: { - polls: poll_id, - }, - }; - - let id = { _id: ObjectId(daoID) }; + if (Number(endTime) <= currentTime) { + throw new Error("End Time has to be in future"); + } - 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 })); - } catch (e) { - result = e.Message; - console.log(e); - await session.abortTransaction(); - throw new Error(e); - } finally { - await session.endSession(); + let duplicates = choices.filter( + (item, index) => choices.indexOf(item.trim()) !== index + ); + if (duplicates.length > 0) { + throw new Error("Duplicate choices found"); + } + + const dao = await db_connect + .collection("DAOs") + .findOne({ _id: ObjectId(daoID) }); + if (!dao) { + throw new Error("DAO Does not exist"); + } + + const token = await db_connect + .collection("Tokens") + .findOne({ tokenAddress: dao.tokenAddress }); + if (!token) { + throw new Error("DAO Token Does not exist in system"); + } + + const block = await getCurrentBlock(dao.network); + const total = await getTotalSupplyAtCurrentBlock( + dao.network, + dao.tokenAddress, + token.tokenID + ); + + const userVotingPowerAtCurrentLevel = + await getUserTotalVotingPowerAtReferenceBlock( + dao.network, + dao.tokenAddress, + dao.daoContract, + token.tokenID, + block, + author + ); + + if (userVotingPowerAtCurrentLevel.eq(0) && dao.requiredTokenOwnership) { + throw new Error( + "User Doesnt have balance at this level to create proposal" + ); + } + + if (!total) { + await session.abortTransaction(); + } + + 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 }); + + 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" + // ); + // } + + let PollData = { + name, + description, + externalLink, + startTime, + endTime, + daoID, + referenceBlock: block, + totalSupplyAtReferenceBlock: total, + _id: poll_id, + choices: choicesPoll, + author, + votingStrategy, + isXTZ, + payloadBytes, + signature, + cidLink: "", + }; + + let data = { + $push: { + polls: poll_id, + }, + }; + + let id = { _id: ObjectId(daoID) }; + + 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 })); + } catch (e) { + result = e.Message; + console.log(e); + await session.abortTransaction(); + throw new Error(e); + } finally { + await session.endSession(); + } + } catch (error) { + console.log("error: ", error); + response.status(400).send({ + message: error.message, + }); } - } catch (error) { - console.log("error: ", error); - response.status(400).send({ - message: error.message, - }); } }; diff --git a/db/models/Choice.model.js b/db/models/Choice.model.js new file mode 100644 index 0000000..221f90a --- /dev/null +++ b/db/models/Choice.model.js @@ -0,0 +1,30 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const WalletAddressSchema = new Schema({ + address: { + type: String, + required: true, + }, + balanceAtReferenceBlock: { + type: String, + required: true, + }, +}); + +const ChoiceSchema = new Schema({ + name: { + type: String, + default: '', + }, + pollID: { + type: mongoose.Schema.Types.ObjectId, + required: true, + }, + walletAddresses: [WalletAddressSchema], +}); + + +const ChoiceModel = mongoose.model('Choice', ChoiceSchema,'Choices'); +module.exports = ChoiceModel; \ No newline at end of file diff --git a/db/models/Dao.model.js b/db/models/Dao.model.js index 319ee43..8354ced 100644 --- a/db/models/Dao.model.js +++ b/db/models/Dao.model.js @@ -19,6 +19,10 @@ const DaoModelSchema = new Schema({ type: String, default: '', }, + // Contract Address of deployed DAO + daoContract:{ + type:String, + }, linkToTerms: { type: String, default: '', diff --git a/db/models/Poll.model.js b/db/models/Poll.model.js new file mode 100644 index 0000000..38c1ac9 --- /dev/null +++ b/db/models/Poll.model.js @@ -0,0 +1,68 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const PollModelSchema = new Schema({ + description: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + }, + daoID: { + type: String, + required: true, + }, + endTime: { + type: String, + required: true, + }, + startTime: { + type: String, + required: true, + }, + referenceBlock: { + type: String, + required: true, + }, + choices: [{ + type: mongoose.Schema.Types.ObjectId, + required: true, + }], + totalSupplyAtReferenceBlock: { + type: String, + required: true, + }, + externalLink: { + type: String, + default: '', + }, + author: { + type: String, + required: true, + }, + // 0 - Single Choice, 1 - Multi Choice + votingStrategy: { + type: Number, + required: true, + }, + isXTZ: { + type: Boolean, + default: false, + }, + payloadBytes:{ + type: String, + }, + payloadBytesHash:{ + type: String, + index: true, + sparse: true, + } +},{ + timestamps: true, +}); + +const PollModel = mongoose.model('Poll', PollModelSchema,'Polls'); +module.exports = PollModel; \ No newline at end of file diff --git a/db/models/Token.model.js b/db/models/Token.model.js index dbdf8e0..8662e73 100644 --- a/db/models/Token.model.js +++ b/db/models/Token.model.js @@ -13,7 +13,7 @@ const TokenModelSchema = new Schema({ }, symbol: { type: String, - required: true, + required: false, }, tokenID: { type: Number diff --git a/middlewares/index.js b/middlewares/index.js index 8ca6ca1..2eec905 100644 --- a/middlewares/index.js +++ b/middlewares/index.js @@ -2,16 +2,29 @@ const { verifySignature, bytes2Char } = require("@taquito/utils"); const { verityEthSignture } = require("../utils-eth"); function splitAtBrace(inputString) { + const squareBracketIndex = inputString.indexOf('['); const braceIndex = inputString.indexOf('{'); - if (braceIndex === -1) { + // Find the minimum between square bracket and brace indices, but > 0 + let minIndex = -1; + if (squareBracketIndex > 0 && braceIndex > 0) { + minIndex = Math.min(squareBracketIndex, braceIndex); + } else if (squareBracketIndex > 0) { + minIndex = squareBracketIndex; + } else if (braceIndex > 0) { + minIndex = braceIndex; + } + + + if (minIndex === -1) { // If '{' is not found, return the original string and an empty string return [inputString, '']; } - + + // Split the string at the brace position - const firstPart = inputString.slice(0, braceIndex); - const secondPart = inputString.slice(braceIndex); + const firstPart = inputString.slice(0, minIndex); + const secondPart = inputString.slice(minIndex); return [firstPart, secondPart]; } @@ -25,7 +38,7 @@ const requireSignature = async (request, response, next) => { const isVerified = verityEthSignture(signature, payloadBytes) if(isVerified){ try{ - const [firstPart, secondPart] = splitAtBrace(payloadBytes) + const [_, secondPart] = splitAtBrace(payloadBytes) const jsonString = secondPart console.log({jsonString, secondPart}) const payloadObj = JSON.parse(jsonString) diff --git a/package-lock.json b/package-lock.json index f0e00ee..a57db64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "dotenv": "^16.0.3", "ethers": "^6.13.2", "express": "^4.18.1", + "md5": "^2.3.0", "mime": "^4.0.1", "mongodb": "^4.10.0", "mongoose": "^8.5.2", @@ -2669,6 +2670,14 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2936,6 +2945,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/data-uri-to-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", @@ -4568,6 +4585,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -5646,6 +5668,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/package.json b/package.json index 351ed5a..ddba4ac 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dotenv": "^16.0.3", "ethers": "^6.13.2", "express": "^4.18.1", + "md5": "^2.3.0", "mime": "^4.0.1", "mongodb": "^4.10.0", "mongoose": "^8.5.2", diff --git a/utils-eth.js b/utils-eth.js index 0db59ec..362e1ed 100644 --- a/utils-eth.js +++ b/utils-eth.js @@ -322,11 +322,13 @@ const tokenAbiForErc20 = [ } ] +// ⚠️ To be Fixed function _getEthProvider(network) { return new JsonRpcProvider("https://node.ghostnet.etherlink.com"); // return new JsonRpcProvider("https://eth-sepolia.blockscout.com"); } +// ⚠️ To be Implemented function verityEthSignture(signature, payloadBytes) { return true; @@ -355,6 +357,7 @@ function verityEthSignture(signature, payloadBytes) { } } +// ⚠️ To be Implemented async function getEthTokenMetadata(network, tokenAddress) { const provider = _getEthProvider(network); const tokenContract = new ethers.Contract(tokenAddress, tokenAbiForErc20, provider); @@ -369,12 +372,14 @@ async function getEthTokenMetadata(network, tokenAddress) { }; } +// ✅ Working async function getEthCurrentBlock(network) { const provider = _getEthProvider(network); const block = await provider.getBlock('latest'); return block.number; } +// ✅ Working async function getEthUserBalanceAtLevel(network, walletAddress, tokenAddress, block = 0) { if(!block) block = await getEthCurrentBlock(network); const provider = _getEthProvider(network); @@ -384,11 +389,58 @@ async function getEthUserBalanceAtLevel(network, walletAddress, tokenAddress, bl return balance; } +async function getEthTotalSupply(network, tokenAddress, block = 0) { + if(!block) block = await getEthCurrentBlock(network); + const provider = _getEthProvider(network); + const tokenContract = new ethers.Contract(tokenAddress, tokenAbiForErc20, provider); + const totalSupply = await tokenContract.totalSupply({blockTag: block}); + return totalSupply; +} + +// This won't work efficiently for large block ranges, Indexer needs to be used for this +async function getEthTokenHoldersCount(network, tokenAddress, block = 0) { + if(!block) block = await getEthCurrentBlock(network); + const provider = _getEthProvider(network); + const contract = new ethers.Contract(tokenAddress, tokenAbiForErc20, provider); + + const latestBlock = await provider.getBlockNumber(); + const startBlock = Math.max(0, latestBlock - 999); // Ensure we don't go below block 0 + const holders = new Set(); + + console.log(`Querying blocks ${startBlock} to ${latestBlock}`); + + const filter = contract.filters.Transfer(); + const events = await contract.queryFilter(filter, startBlock, latestBlock); + + for (let event of events) { + const { from, to } = event.args; + holders.add(from); + holders.add(to); + } + + // Remove zero-balance holders + for (let holder of holders) { + const balance = await contract.balanceOf(holder); + if (balance.eq(0)) { + holders.delete(holder); + } + } + + return holders.size; +} + +getEthTokenHoldersCount("sepolia","0x336bfd0356f6babec084f9120901c0296db1967e").then(console.log) + // ✅ Working -getEthUserBalanceAtLevel("sepoplia","0xA0E9D286a88C544C8b474275de4d1b8D97C2a81a","0x336bfd0356f6babec084f9120901c0296db1967e").then(console.log) +// getEthTotalSupply("sepoplia","0x336bfd0356f6babec084f9120901c0296db1967e").then((x)=>console.log("Total Suplpy",x)) + + +// ✅ Working +// getEthUserBalanceAtLevel("sepoplia","0xA0E9D286a88C544C8b474275de4d1b8D97C2a81a","0x336bfd0356f6babec084f9120901c0296db1967e").then(console.log) + // ✅ Working -getEthCurrentBlock("sepolia").then(console.log) +// getEthCurrentBlock("sepolia").then(console.log) console.log("from ETH") @@ -396,5 +448,7 @@ module.exports = { verityEthSignture, getEthTokenMetadata, getEthCurrentBlock, - getEthUserBalanceAtLevel + getEthUserBalanceAtLevel, + getEthTotalSupply, + getEthTokenHoldersCount, } \ No newline at end of file diff --git a/utils.js b/utils.js index a6d3d99..5185ae6 100644 --- a/utils.js +++ b/utils.js @@ -222,6 +222,17 @@ const getIPFSProofFromPayload = (payloadBytes, signature) => { ); }; +// getUserTotalVotingPowerAtReferenceBlock( +// "ghostnet", +// "KT1E7jkyAWhCoMbPZbVUJMo7xAfKcqYyCG6Z", +// null, +// "0", +// "tz1egScqJ6F4MvjDQsqLfa3rvCZctcbh9srw", +// false +// ).then((res)=>{ +// console.log("getUserTotalVotingPowerAtReferenceBlock: ", res); +// }) + module.exports = { getInputFromSigPayload, getTotalSupplyAtCurrentBlock,