Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion products/governance-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM node:20
WORKDIR /app

COPY package.json yarn.lock ./
RUN npm install
RUN yarn install

COPY . .

Expand Down
12 changes: 12 additions & 0 deletions products/governance-api/lib/app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import express from "express";
import * as bodyParser from "body-parser";
import errorhandler from "strong-error-handler";
import pinoHttp from "pino-http";
import router from "./routes";
import { logger } from "./logger";

export const app = express();

app.use(
pinoHttp({
logger,
customLogLevel: (_req, res, _err) => {
if (res.statusCode >= 400) return "error";
return "info";
},
})
);

// middleware for parsing application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }));

Expand Down
3 changes: 3 additions & 0 deletions products/governance-api/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const rootLogger = require('pino')()

export const logger = rootLogger.child({ level: "info" });
29 changes: 21 additions & 8 deletions products/governance-api/lib/routes/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const proposal = (res: any, msg: any) => {
});
}
};
const vote = async (res: any, msg: any, ts: string) => {
const vote = async (res: any, msg: any, ts: string, log: any) => {
if (msg.type !== "vote") {
return null;
}
Expand Down Expand Up @@ -118,6 +118,7 @@ const vote = async (res: any, msg: any, ts: string) => {
});

if (!proposal) {
log.error({ error_code: ErrorCodes.INCORRECT_PROPOSAL_FORMAT, token: msg.token }, "Proposal not found");
return res.status(400).json({
code: ErrorCodes.INCORRECT_PROPOSAL_FORMAT,
error_description: "incorect vote proposal",
Expand All @@ -126,6 +127,7 @@ const vote = async (res: any, msg: any, ts: string) => {
const payload = JSON.parse(proposal.payload);

if (Number(ts) > Number(payload.end) || Number(payload.start) > Number(ts)) {
log.error({ error_code: ErrorCodes.INCORRECT_VOTE_FORMAT, address: msg.address }, "Not in voting window");
return res.status(400).json({
code: ErrorCodes.INCORRECT_VOTE_FORMAT,
error_description: "not in voting window",
Expand All @@ -134,12 +136,14 @@ const vote = async (res: any, msg: any, ts: string) => {
};

message.post("/message", async (req, res) => {
const log = (req as any).log;
try {
const body = req.body;
const msg = JSON.parse(body.msg);
const ts = (Date.now() / 1e3).toFixed();

if (!body || !body.address || !body.msg || !body.sig) {
log.error({ error_code: ErrorCodes.INCORRECT_DATA, address: body && body.address }, "incorrect message body");
return res.status(400).json({
code: ErrorCodes.INCORRECT_DATA,
error_description: "incorect message body",
Expand All @@ -148,13 +152,15 @@ message.post("/message", async (req, res) => {

const spaceKey = body.space;
if (!spaceKey || !spaces[spaceKey]) {
log.error({ error_code: ErrorCodes.UNKNOWN_SPACE, address: body && body.address }, "unknown space");
return res.status(400).json({
code: ErrorCodes.UNKNOWN_SPACE,
error_description: "unknown space",
});
}

if (spaces[spaceKey].token !== msg.token) {
log.error({ error_code: ErrorCodes.UNKNOWN_SPACE, address: body && body.address }, "token does not match space");
return res.status(400).json({
code: ErrorCodes.UNKNOWN_SPACE,
error_description: "token does not match space",
Expand All @@ -164,20 +170,23 @@ message.post("/message", async (req, res) => {
msg.timestamp = Number(msg.timestamp);

if (!msg.timestamp || isNaN(msg.timestamp) || msg.timestamp > ts + 30) {
log.error({ error_code: ErrorCodes.INCORRECT_DATA, address: body && body.address }, "wrong timestamp");
return res.status(400).json({
code: ErrorCodes.INCORRECT_DATA,
error_description: "wrong timestamp",
});
}

if (!msg.version || msg.version !== pkg.version) {
log.error({ error_code: ErrorCodes.INCORRECT_VER, address: body && body.address }, "incorrect version");
return res.status(400).json({
code: ErrorCodes.INCORRECT_VER,
error_description: "incorrect version",
});
}

if (!msg.type || !["proposal", "vote"].includes(msg.type)) {
log.error({ error_code: ErrorCodes.INCORRECT_TYPE, address: body && body.address }, "incorrect type");
return res.status(400).json({
code: ErrorCodes.INCORRECT_TYPE,
error_description: "incorrect type",
Expand Down Expand Up @@ -208,14 +217,17 @@ message.post("/message", async (req, res) => {
}
if (!checked) throw new Error('signature mismatch');
} catch (err) {
log.error({ error_code: ErrorCodes.INCORRECT_SIGNATURE, address: body.address }, "Signature verification failed");
return res.status(400).json({
code: ErrorCodes.INCORRECT_SIGNATURE,
error_description: "incorrect signature",
});
}

log.info({ address: body.address, sigType: body.sigType || "schnorr" }, "Signature verified");

proposal(res, msg);
await vote(res, msg, ts);
await vote(res, msg, ts, log);

const space = spaceKey;
let authorIpfsRes: any | null = null;
Expand All @@ -230,10 +242,13 @@ message.post("/message", async (req, res) => {
base16owner
);

log.info({ token: base16Token, address: base16owner, userBalance }, "Zilliqa liquidity fetched");

const _balance = new BN(userBalance);
const _minGZIL = new BN("30000000000000000");

if (msg.token == gZIL && _balance.lt(_minGZIL)) {
log.error({ error_code: ErrorCodes.MIN_BALANCE_ERROR, balance: _balance.toString(), threshold: _minGZIL.toString() }, "Balance below minimum gZIL");
return res.status(400).json({
code: ErrorCodes.MIN_BALANCE_ERROR,
error_description:
Expand All @@ -260,6 +275,7 @@ message.post("/message", async (req, res) => {
payload: JSON.stringify(msg.payload),
sig: JSON.stringify(body.sig),
});
log.info({ type: msg.type, ipfsHash: authorIpfsRes, address: body.address }, "DB record created");
}

if (msg.type === "vote") {
Expand All @@ -281,17 +297,14 @@ message.post("/message", async (req, res) => {
payload: JSON.stringify(msg.payload),
sig: JSON.stringify(body.sig),
});
log.info({ type: msg.type, ipfsHash: authorIpfsRes, address: body.address }, "DB record created");
}

console.log(
`Address "${body.address}"\n`,
`Token "${msg.token}"\n`,
`Type "${msg.type}"\n`,
`IPFS hash "${authorIpfsRes}"`
);
log.info({ address: body.address, token: msg.token, type: msg.type, ipfsHash: authorIpfsRes }, "Message processed successfully");

return res.json({ ipfsHash: authorIpfsRes });
} catch (err) {
log.error({ err, error_code: 500 }, "Unhandled error in message handler");
return res.status(400).json({
code: 500,
error_description: err.message,
Expand Down
115 changes: 66 additions & 49 deletions products/governance-api/lib/routes/spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Router } from "express";
import fromentries from "object.fromentries";
import spaces from "@snapshot-labs/snapshot-spaces";
import { Message } from "../models";
import { logger } from "../logger";

export const spacesRouter = Router();

Expand All @@ -13,61 +14,77 @@ spacesRouter.get("/spaces/:key?", (req, res) => {

spacesRouter.get("/:space/proposals", async (req, res) => {
const { space } = req.params;
const messages = await Message.findAll({
where: {
space,
type: "proposal",
},
order: [["timestamp", "DESC"]],
});
const spaces = messages.map((message) => {
return [
message.id,
{
address: message.address,
msg: {
version: message.version,
timestamp: String(message.timestamp),
token: message.token,
type: message.type,
payload: JSON.parse(message.payload),
},
sig: message.sig,
authorIpfsHash: message.author_ipfs_hash,
try {
const messages = await Message.findAll({
where: {
space,
type: "proposal",
},
];
});
order: [["timestamp", "DESC"]],
});
const spaces = messages.map((message) => {
return [
message.id,
{
address: message.address,
msg: {
version: message.version,
timestamp: String(message.timestamp),
token: message.token,
type: message.type,
payload: JSON.parse(message.payload),
},
sig: message.sig,
authorIpfsHash: message.author_ipfs_hash,
},
];
});

return res.status(201).json(fromentries(spaces));
return res.status(201).json(fromentries(spaces));
} catch (err) {
logger.error(
{ err, space: req.params.space, error_code: "DB_QUERY_FAILED" },
"Failed to fetch proposals"
);
return res.status(500).json({ error: "Internal server error" });
}
});

spacesRouter.get("/:space/proposal/:id", async (req, res) => {
const { space, id } = req.params;
const messages = await Message.findAll({
where: {
space,
proposal_id: id,
type: "vote",
},
order: [["timestamp", "DESC"]],
});
const spaces = messages.map((message) => {
return [
message.address,
{
address: message.address,
msg: {
version: message.version,
timestamp: message.timestamp.toString(),
token: message.token,
type: message.type,
payload: JSON.parse(message.payload),
},
sig: message.sig,
authorIpfsHash: message.author_ipfs_hash,
try {
const messages = await Message.findAll({
where: {
space,
proposal_id: id,
type: "vote",
},
];
});
order: [["timestamp", "DESC"]],
});
const spaces = messages.map((message) => {
return [
message.address,
{
address: message.address,
msg: {
version: message.version,
timestamp: message.timestamp.toString(),
token: message.token,
type: message.type,
payload: JSON.parse(message.payload),
},
sig: message.sig,
authorIpfsHash: message.author_ipfs_hash,
},
];
});

return res.status(201).json(fromentries(spaces));
return res.status(201).json(fromentries(spaces));
} catch (err) {
logger.error(
{ err, space: req.params.space, id: req.params.id, error_code: "DB_QUERY_FAILED" },
"Failed to fetch proposal votes"
);
return res.status(500).json({ error: "Internal server error" });
}
});
15 changes: 11 additions & 4 deletions products/governance-api/lib/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { createServer } from "http";
import { app } from "./app";
import { logger } from "./logger";
import { sequelizeRun } from "./sequelize";

const port = process.env.PORT || 3000;

(async () => {
await sequelizeRun();
try {
await sequelizeRun();
logger.info("Database synced");

createServer(app).listen(port, () =>
console.info(`Server running on port ${port}`)
);
createServer(app).listen(port, () =>
logger.info({ port }, "Server started")
);
} catch (err) {
logger.error({ err }, "Startup failed");
process.exit(1);
}
})();
5 changes: 4 additions & 1 deletion products/governance-api/lib/utils/pin-ipfs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fetch from "node-fetch";
import pinataSDK from "@pinata/sdk";
import { logger } from "../logger";

export async function pinJson(body: object): Promise<string> {
if (process.env.IPFS_API_URL) {
Expand All @@ -11,6 +12,7 @@ export async function pinJson(body: object): Promise<string> {
String(process.env.PINATA_SECRET_API_KEY)
);
const result = await pinata.pinJSONToIPFS(body);
logger.info({ ipfsHash: result.IpfsHash }, "IPFS pin succeeded (Pinata)");
return result.IpfsHash;
}

Expand Down Expand Up @@ -39,12 +41,13 @@ async function pinToLocalNode(body: object): Promise<string> {
);

if (!response.ok) {
logger.error({ status: response.status }, "IPFS pin failed");
throw new Error(
`IPFS add failed: ${response.status} ${await response.text()}`
);
}

const data = (await response.json()) as { Hash: string };
console.log("IPFS pin success", data.Hash);
logger.info({ ipfsHash: data.Hash }, "IPFS pin succeeded (local node)");
return data.Hash;
}
Loading
Loading