Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Endpoint for fetching congress spendings as CSV #6

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions config.development.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ module.exports = {
},
CENTRALIZED_API_URL: "http://localhost:8010",
IS_LIVE: false,
EXPLORER_GRAPHQL: "http://localhost:3000",
ONBOARDER_PHRASE: 'REPLACE ME WITH AUTO LLD ONBOARDER ACCOUNT PHRASE'
ONBOARDER_PHRASE: 'REPLACE ME WITH AUTO LLD ONBOARDER ACCOUNT PHRASE',
EXPLORER_API_URL: "http://localhost:3000",
};
1 change: 1 addition & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const config = {
LAND_NFTs_ID: 0,
ONBOARDER_PHRASE: 'REPLACE ME WITH AUTO LLD ONBOARDER ACCOUNT PHRASE',
CENTRALIZED_API_URL: "http://localhost:8010",
EXPLORER_API_URL: "http://localhost:3000",
};

try {
Expand Down
4 changes: 2 additions & 2 deletions config.production.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ module.exports = {
},
IS_LIVE: true,
CENTRALIZED_API_URL: "https://api.liberland.org",
EXPLORER_GRAPHQL: "https://archive.mainnet.liberland.org",
ONBOARDER_PHRASE: 'REPLACE ME WITH AUTO LLD ONBOARDER ACCOUNT PHRASE'
ONBOARDER_PHRASE: 'REPLACE ME WITH AUTO LLD ONBOARDER ACCOUNT PHRASE',
EXPLORER_API_URL: "https://archive.mainnet.liberland.org",
};
2 changes: 1 addition & 1 deletion config.staging.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ module.exports = {
},
IS_LIVE: true,
CENTRALIZED_API_URL: "https://staging.api.liberland.org",
EXPLORER_GRAPHQL: "https://archive.testchain.liberland.org/graphql",
ONBOARDER_PHRASE: "REPLACE ME WITH AUTO LLD ONBOARDER ACCOUNT PHRASE",
EXPLORER_API_URL: "https://archive.testchain.liberland.org/graphql",
};
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@polkadot/util": "*11.1.2",
"axios": "^1.6.2",
"cors": "^2.8.5",
"csv-stringify": "^6.5.1",
"debug": "^4.3.4",
"ejs": "^3.1.9",
"express": "^4.18.2",
Expand Down
93 changes: 90 additions & 3 deletions src/routers/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ const router = require("express").Router();
const wrap = require("express-async-handler");
const { ApiPromise, WsProvider, Keyring } = require("@polkadot/api");
const axios = require ('axios');
const {BN, BN_ONE, BN_ZERO, BN_MILLION} = require("@polkadot/util")
const {BN, BN_ONE, BN_ZERO, BN_MILLION, hexToU8a} = require("@polkadot/util")
const config = require("../../config");
const generateCertificate = require('./generate-certificate');
const { getLastWeekEraPaidEvents } = require("../utils/explorer");
const generateCertificate = require('./generate-certificate')
const { getLastWeekEraPaidEvents, fetchAllCongressSpendings } = require("../utils/explorer");
const { stringify } = require('csv-stringify/sync');
const pako = require('pako');


const provider = new WsProvider(config.RPC_NODE_URL);
Expand All @@ -27,6 +29,16 @@ const apiPromise = ApiPromise.create({
name: "Text",
purpose: "Text",
},
RemarkInfo: {
category: 'Text',
project: 'Text',
supplier: 'Text',
description: 'Text',
finalDestination: 'Text',
amountInUSDAtDateOfPayment: 'u64',
date: 'u64',
currency: 'Text',
},
},
});

Expand Down Expand Up @@ -364,4 +376,79 @@ router.get(
})
);

router.get(
"/congress-spendings",
wrap(async (req, res) => {
try {
const allSpendings = await fetchAllCongressSpendings();
const api = await apiPromise;
const csv = stringify([
[
"Timestamp",
"Block Number",
"Recipient",
"Asset",
"Value",
"Category",
"Project",
"Supplier",
"Description",
"Final Destination",
"Amount In USD At Date Of Payment",
"Date",
"Currency",
"Text Remark",
"Raw Remark",
],
...allSpendings.map((v) => {
let parsedRemark;
let textRemark;
try {
if (v.remark) {
const compressedData = hexToU8a(v.remark);
const decompressed = pako.inflate(compressedData);
parsedRemark = api
.createType("RemarkInfo", decompressed)
.toJSON();
parsedRemark.date = new Date(
parsedRemark.date
).toISOString();
}
} catch (e) {
textRemark = Buffer.from(
v.remark.substring(2),
"hex"
).toString("utf-8");
}
return [
v.block.timestamp,
v.block.number,
v.toId,
v.asset,
v.value,
parsedRemark?.category ?? "-",
parsedRemark?.project ?? "-",
parsedRemark?.supplier ?? "-",
parsedRemark?.description ?? "-",
parsedRemark?.finalDestination ?? "-",
parsedRemark?.amountInUSDAtDateOfPayment ?? "-",
parsedRemark?.date ?? "-",
parsedRemark?.currency ?? "-",
textRemark ?? "-",
v.remark,
];
}),
]);
res.set(
"Content-Disposition",
'attachment; filename="congress-spendings.csv"'
)
.status(200)
.send(csv);
} catch (e) {
res.status(400).json({ error: e.message });
}
})
);

module.exports = router;
161 changes: 143 additions & 18 deletions src/utils/explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,160 @@
const axios = require("axios");
const config = require("../../config");

const CONGRESS_ADDRESS = "5EYCAe5g8CDuMsTief7QBxfvzDFEfws6ueXTUhsbx5V81nGH";

const eraPaidEventsQuery = `
query EraPaidEvents {
events(
orderBy: BLOCK_NUMBER_DESC,
first: 28,
filter: {
method: { equalTo: "EraPaid" },
section: { equalTo: "staking" }
}
) {
nodes {
data
}
}
events(
orderBy: BLOCK_NUMBER_DESC,
first: 28,
filter: {
method: { equalTo: "EraPaid" },
section: { equalTo: "staking" }
}
) {
nodes {
data
}
}
}
`;

const getApi = () => axios.create({
baseURL: config.EXPLORER_GRAPHQL,
baseURL: config.EXPLORER_API_URL,
});


const getLastWeekEraPaidEvents = async () => {
const { data } = await getApi().post('', {
query: eraPaidEventsQuery
});
return data.data.events.nodes.map(v => v.data);
const { data } = await getApi().post('', {
query: eraPaidEventsQuery
});
return data.data.events.nodes.map(v => v.data);
};


async function queryAllPages(query, variables, key) {
let allData = [];
let after = undefined;
while (true) {
const result = await fetch(config.EXPLORER_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables: { after, ...variables } }),
});
const data = await result.json();
allData.push(data.data[key].nodes);
if (data.data[key].pageInfo.hasNextPage) {
after = data.data[key].pageInfo.endCursor;
} else {
break;
}
}
return allData;
}

async function getLLMSpendings() {
const data = await queryAllPages(
`
query LLM($after: Cursor, $userId: String) {
merits(first: 50, after: $after, filter: { fromId: { equalTo: $userId } }) {
nodes {
id
toId
value
remark
block {
number
timestamp
}
}
pageInfo {
hasNextPage,
endCursor
}
}
}
`,
{ userId: CONGRESS_ADDRESS },
"merits"
);
return data.flat().map((v) => ({ asset: "LLM", ...v }));
}

async function getAssetsSpendings() {
const data = await queryAllPages(
`
query Assets($after: Cursor, $userId: String) {
assetTransfers(first: 50, after: $after, filter: { asset: { notEqualTo: "1" }, fromId: { equalTo: $userId } }) {
nodes {
id
asset
toId
value
remark
block {
number
timestamp
}
}
pageInfo {
hasNextPage,
endCursor
}
}
}
`,
{ userId: CONGRESS_ADDRESS },
"assetTransfers"
);
return data.flat();
}

async function getLLDSpendings() {
const data = await queryAllPages(
`
query LLD($after: Cursor, $userId: String) {
transfers(first: 50, after: $after, filter: { fromId: { equalTo: $userId } }) {
nodes {
id
toId
value
remark
block {
number
timestamp
}
}
pageInfo {
hasNextPage,
endCursor
}
}
}
`,
{ userId: CONGRESS_ADDRESS },
"transfers"
);
return data.flat().map((v) => ({ asset: "LLD", ...v }));
}

async function fetchAllCongressSpendings() {
const allSpendings = [
await getLLDSpendings(),
await getLLMSpendings(),
await getAssetsSpendings(),
]
.flat()
.sort((a, b) =>
parseInt(a.block.number) > parseInt(b.block.number) ? -1 : 1
);

return allSpendings;
}

module.exports = {
getLastWeekEraPaidEvents,
fetchAllCongressSpendings,
getLastWeekEraPaidEvents,
}