app.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import 'dotenv/config'
  2. import grpc from "grpc"
  3. import express from "express"
  4. import bodyParser from "body-parser"
  5. import basicAuth from "express-basic-auth"
  6. import {
  7. CommandService_v1Client as CommandService,
  8. QueryService_v1Client as QueryService
  9. } from 'iroha-helpers/lib/proto/endpoint_grpc_pb'
  10. import { queries } from 'iroha-helpers'
  11. import util from 'util'
  12. import { exec } from 'child_process'
  13. import cors from 'cors'
  14. import * as sql from 'sqlite3'
  15. import fs from 'fs'
  16. const app = express();
  17. app.use(bodyParser.json());
  18. //TODO is cors package necesary? basic middleware could suffice
  19. app.use(cors()); //TODO: set only safe origins
  20. app.use(basicAuth({
  21. users: { admin: 'superPasswd' }
  22. }));
  23. const asyncExec = util.promisify(exec);
  24. const IROHA_ADMIN_PRIV = "f101537e319568c765b2cc89698325604991dca57b9716b58016b253506cab70";
  25. const IROHA_ADMIN = "admin@test";
  26. const IROHA_ADDRESS = process.env.IROHA_ADDRESS || "localhost:50051";
  27. const queryService = new QueryService(IROHA_ADDRESS, grpc.credentials.createInsecure());
  28. const CHAIN4ALL_RASTER_CLIP_SCRIPT_PATH = process.env.CHAIN4ALL_RASTER_CLIP_SCRIPT_PATH || '/home/kunickyd/Documents/chain4all/chain4all_raster_clip.sh';
  29. const CHAIN4ALL_SERVICE_PORT = process.env.CHAIN4ALL_SERVICE_PORT || 3000;
  30. const PRICE_MODIFIER: number = parseFloat(process.env.PRICE_MODIFIER || "0.5");
  31. const DB_FILE_NAME: string = "data/transfers.db";
  32. app.get("/", (req, res) => {
  33. res.send("Chain4All Blockchain service");
  34. });
  35. app.get("/transfers/:userId", async (req, res, next) => {
  36. let db = getDbConnection();
  37. db.all(
  38. "SELECT * " +
  39. "FROM transfers " +
  40. "WHERE user=? " +
  41. "ORDER BY id DESC " +
  42. "LIMIT 10;",
  43. [req.params.userId],
  44. (err, rows) => {
  45. if (err) {
  46. next(err);
  47. return;
  48. }
  49. res.send(rows);
  50. }
  51. );
  52. db.close();
  53. });
  54. app.post("/transfer", async (req, res, next) => {
  55. try {
  56. if (!req.body) {
  57. res.status(400);
  58. throw Error(JSON.stringify({ error: { name: "Error, request has no body!" } }));
  59. }
  60. if (!req.body.txHash) {
  61. res.status(400);
  62. throw Error(JSON.stringify({ error: { name: "Error, request body has no \"txHash\" property!" } }));
  63. }
  64. if (!req.body.user) {
  65. res.status(400);
  66. throw Error(JSON.stringify({ error: { name: "Error, request body has no \"user\" property!" } }));
  67. }
  68. if (!req.body.extent) {
  69. res.status(400);
  70. throw Error(JSON.stringify({ error: { name: "Error, request body has no \"extent\" property!" } }));
  71. }
  72. if (!req.body.amount) {
  73. res.status(400);
  74. throw Error(JSON.stringify({ error: { name: "Error, request body has no \"amount\" property!" } }));
  75. }
  76. if (!req.body.timestamp) {
  77. res.status(400);
  78. throw Error(JSON.stringify({ error: { name: "Error, request body has no \"timestamp\" property!" } }));
  79. }
  80. let db = getDbConnection();
  81. db.run(
  82. "INSERT INTO transfers (hash, user, extent, amount, timestamp)" +
  83. "VALUES (?, ?, ?, ?, ?);",
  84. [req.body.txHash, req.body.user, req.body.extent, req.body.amount, req.body.timestamp],
  85. (err) => {
  86. if (err) {
  87. next(err);
  88. return;
  89. }
  90. res.status(201);
  91. res.send();
  92. }
  93. );
  94. db.close();
  95. }
  96. catch (err) {
  97. next(err);
  98. }
  99. });
  100. app.post("/price", (req, res) => { //add caching of same requests
  101. if (req.body && req.body.area) {
  102. res.send({ price: getPrice(req.body.area) });
  103. }
  104. else {
  105. res.status(400);
  106. throw Error(JSON.stringify({ error: { name: "Error, request has no body with \"area\" property!" } }));
  107. }
  108. });
  109. app.post("/buy", async (req, res, next) => {
  110. try {
  111. if (!req.body) {
  112. res.status(400);
  113. throw Error(JSON.stringify({ error: { name: "Error, request has no body!" } }));
  114. }
  115. if (!req.body.txHash) {
  116. res.status(400);
  117. throw Error(JSON.stringify({ error: { name: "Error, request body has no \"txHash\" property!" } }));
  118. }
  119. //TODO load from http headers, or something like that??
  120. if (!req.body.user) {
  121. res.status(400);
  122. throw Error(JSON.stringify({ error: { name: "Error, request body has no \"user\" property!" } }));
  123. }
  124. //TODO validate this properly
  125. let txDetail = await getTransactionDetail(req.body.txHash, req.body.user);
  126. let extent: number[] = JSON.parse(txDetail.description).extent as number[];
  127. let dataFileId: string = Date.now().toString();
  128. let dataCommand = CHAIN4ALL_RASTER_CLIP_SCRIPT_PATH + ' ' + extent[0] + ' ' + extent[1] + ' ' + extent[2] + ' ' + extent[3] + ' ' + dataFileId;
  129. console.debug(dataCommand);
  130. const { stdout, stderr } = await asyncExec(dataCommand);
  131. console.debug(stdout);
  132. if (stderr) {
  133. console.warn(stderr);
  134. }
  135. res.send({ dataUrl: "https://gis.lesprojekt.cz/chain4all/raster_" + dataFileId + ".tif" });
  136. }
  137. catch (err) {
  138. next(err);
  139. }
  140. });
  141. function getDbConnection(): sql.Database {
  142. return new sql.Database(DB_FILE_NAME);
  143. }
  144. function initDatabase(): Promise<void>{
  145. return new Promise<void>((resolve, reject) => {
  146. if(!fs.existsSync("./data")){
  147. fs.mkdirSync("data");
  148. };
  149. let db = getDbConnection();
  150. db.run(
  151. "CREATE TABLE IF NOT EXISTS transfers( " +
  152. "id INTEGER PRIMARY KEY, " +
  153. "hash TEXT NOT NULL, " +
  154. "user TEXT NOT NULL, " +
  155. "timestamp INTEGER NOT NULL, " +
  156. "amount INTEGER NOT NULL, " +
  157. "extent TEXT NOT NULL " +
  158. ");",
  159. (err) => {
  160. if (err) {
  161. reject(err);
  162. }
  163. else{
  164. resolve();
  165. }
  166. }
  167. );
  168. });
  169. }
  170. async function getTransactionDetail(txHash: string, user: string) {
  171. let quer: any = await queries.getAccountTransactions({
  172. privateKey: IROHA_ADMIN_PRIV,
  173. creatorAccountId: IROHA_ADMIN,
  174. queryService,
  175. timeoutLimit: 5000
  176. }, {
  177. accountId: user,
  178. pageSize: 10,
  179. firstTxHash: txHash,
  180. ordering: {
  181. field: undefined,
  182. direction: undefined
  183. },
  184. firstTxTime: undefined,
  185. lastTxTime: undefined,
  186. firstTxHeight: 1,
  187. lastTxHeight: 3
  188. });
  189. //TODO find better way to look for transferAssets command in transaction
  190. return quer.transactionsList[0].payload.reducedPayload.commandsList[0].transferAsset;
  191. }
  192. function getPrice(area: number): number {
  193. return area * PRICE_MODIFIER;
  194. }
  195. function errorMiddleware(err: any, req: any, res: any, next: any): void { //TODO: add custom Exception class
  196. console.log(err);
  197. res.status(500);
  198. res.send(err);
  199. }
  200. app.use(errorMiddleware);
  201. app.listen(CHAIN4ALL_SERVICE_PORT,async () => {
  202. try{
  203. await initDatabase();
  204. console.log(`Listening at http://localhost:${CHAIN4ALL_SERVICE_PORT}`);
  205. }catch(err){
  206. console.error("Cannot start service!");
  207. console.error(err);
  208. }
  209. });