diff --git a/config.development.js b/config.development.js index 6527099..3883583 100644 --- a/config.development.js +++ b/config.development.js @@ -8,5 +8,6 @@ module.exports = { }, CENTRALIZED_API_URL: "http://localhost:8010", IS_LIVE: false, - 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", }; diff --git a/config.js b/config.js index e9dd7df..8c0edea 100644 --- a/config.js +++ b/config.js @@ -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 { diff --git a/config.production.js b/config.production.js index e045471..ce76120 100644 --- a/config.production.js +++ b/config.production.js @@ -8,5 +8,6 @@ module.exports = { }, IS_LIVE: true, CENTRALIZED_API_URL: "https://api.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", }; diff --git a/config.staging.js b/config.staging.js index b9e780b..02aec1a 100644 --- a/config.staging.js +++ b/config.staging.js @@ -9,4 +9,5 @@ module.exports = { IS_LIVE: true, CENTRALIZED_API_URL: "https://staging.api.liberland.org", ONBOARDER_PHRASE: "REPLACE ME WITH AUTO LLD ONBOARDER ACCOUNT PHRASE", + EXPLORER_API_URL: "https://archive.testchain.liberland.org/graphql", }; diff --git a/package-lock.json b/package-lock.json index 891ffbc..352816e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,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", @@ -1385,6 +1386,11 @@ "node": ">= 8" } }, + "node_modules/csv-stringify": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.5.1.tgz", + "integrity": "sha512-+9lpZfwpLntpTIEpFbwQyWuW/hmI/eHuJZD1XzeZpfZTqkf1fyvBbBLXTJJMsBuuS11uTShMqPwzx4A6ffXgRQ==" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -5956,6 +5962,11 @@ "which": "^2.0.1" } }, + "csv-stringify": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.5.1.tgz", + "integrity": "sha512-+9lpZfwpLntpTIEpFbwQyWuW/hmI/eHuJZD1XzeZpfZTqkf1fyvBbBLXTJJMsBuuS11uTShMqPwzx4A6ffXgRQ==" + }, "data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", diff --git a/package.json b/package.json index b96f232..b131bef 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/routers/api.js b/src/routers/api.js index 2d5ec6b..ea8990e 100644 --- a/src/routers/api.js +++ b/src/routers/api.js @@ -7,6 +7,8 @@ const axios = require ('axios'); const {BN, BN_ONE} = require("@polkadot/util") const config = require("../../config"); const generateCertificate = require('./generate-certificate') +const { fetchAllCongressSpendings } = require('../utils/spendings'); +const { stringify } = require('csv-stringify/sync'); const provider = new WsProvider(config.RPC_NODE_URL); @@ -328,4 +330,25 @@ router.get( }) ); -module.exports = router; +router.get( + "/congress_spendings", + wrap(async (req, res) => { + try { + const allSpendings = await fetchAllCongressSpendings(); + const csv = stringify( + [ + ["Timestamp", "Block Number", "Recipient", "Asset", "Value", "Remark"], + ...allSpendings.map(v => [v.block.timestamp, v.block.number, v.toId, v.asset, v.value, 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; \ No newline at end of file diff --git a/src/utils/spendings.js b/src/utils/spendings.js new file mode 100644 index 0000000..4f10b7d --- /dev/null +++ b/src/utils/spendings.js @@ -0,0 +1,128 @@ +const config = require("../../config"); + +const CONGRESS_ADDRESS = "5EYCAe5g8CDuMsTief7QBxfvzDFEfws6ueXTUhsbx5V81nGH"; + +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 = { + fetchAllCongressSpendings, +};