From 2146a398f74e86e1ee40f37a9476774fffcb23e3 Mon Sep 17 00:00:00 2001
From: theeurbanlegend <mbuto152@gmail.com>
Date: Sat, 17 Aug 2024 19:30:53 +0300
Subject: [PATCH] Saved metadata to ipfs

---
 backend/.env.example                          |  3 +-
 .../src/attachment/attachment.controller.ts   | 11 +++
 backend/src/attachment/attachment.module.ts   | 12 +++-
 backend/src/attachment/attachment.service.ts  | 56 ++++++++++++++-
 backend/src/common/types.ts                   |  5 ++
 backend/src/drep/drep.controller.ts           |  7 +-
 backend/src/drep/drep.service.ts              | 72 +++++++++++++------
 backend/src/entities/metadata.entity.ts       |  4 +-
 .../src/components/molecules/DrepTimeline.tsx |  2 +-
 9 files changed, 140 insertions(+), 32 deletions(-)

diff --git a/backend/.env.example b/backend/.env.example
index 89f01f3ee..32cdd4ec1 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -1 +1,2 @@
-BLOCKFROST_SANCHONET_PROJECT_ID=
\ No newline at end of file
+BLOCKFROST_SANCHONET_PROJECT_ID=
+BLOCKFROST_IPFS_PROJECT_ID=
\ No newline at end of file
diff --git a/backend/src/attachment/attachment.controller.ts b/backend/src/attachment/attachment.controller.ts
index 2b605d6ca..32d9f9223 100644
--- a/backend/src/attachment/attachment.controller.ts
+++ b/backend/src/attachment/attachment.controller.ts
@@ -59,4 +59,15 @@ export class AttachmentController {
       parentEntity,
     );
   }
+  @Get('ipfs/:IpfsHash')
+  async getAttachmentByIpfsHash(@Param('IpfsHash') IpfsHash: string, @Res() res: Response) {
+    return this.attachmentService.getAttachmentFromIPFS(IpfsHash, res);
+  }
+  @Post('ipfs/add')
+  async uploadAttachmentToIpfs(
+    @Body('attachment')
+    attachment: Express.Multer.File | Buffer | Uint8Array | Blob,
+  ) {
+    return this.attachmentService.uploadAttachmentToIPFS(attachment);
+  }
 }
diff --git a/backend/src/attachment/attachment.module.ts b/backend/src/attachment/attachment.module.ts
index 61ebdb359..0c4a64a37 100644
--- a/backend/src/attachment/attachment.module.ts
+++ b/backend/src/attachment/attachment.module.ts
@@ -3,10 +3,18 @@ import { AttachmentService } from './attachment.service';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { Attachment } from 'src/entities/attachment.entity';
 import { AttachmentController } from './attachment.controller';
+import { HttpModule } from '@nestjs/axios';
+import { ConfigService } from '@nestjs/config';
 
 @Module({
-  imports: [TypeOrmModule.forFeature([Attachment])],
+  imports: [
+    TypeOrmModule.forFeature([Attachment]),
+    HttpModule.register({
+      timeout: 5000,
+      maxRedirects: 5,
+    }),
+  ],
   controllers: [AttachmentController],
-  providers: [AttachmentService],
+  providers: [AttachmentService, ConfigService],
 })
 export class AttachmentModule {}
diff --git a/backend/src/attachment/attachment.service.ts b/backend/src/attachment/attachment.service.ts
index af1297000..be6d62d55 100644
--- a/backend/src/attachment/attachment.service.ts
+++ b/backend/src/attachment/attachment.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { HttpException, Injectable } from '@nestjs/common';
 import Jimp from 'jimp';
 import {
   Attachment,
@@ -7,12 +7,19 @@ import {
 } from 'src/entities/attachment.entity';
 import { InjectDataSource } from '@nestjs/typeorm';
 import { DataSource } from 'typeorm';
+import { lastValueFrom } from 'rxjs';
+import { IPFSResponse } from 'src/common/types';
+import { HttpService } from '@nestjs/axios';
+import { ConfigService } from '@nestjs/config';
+import { Response } from 'express';
 
 @Injectable()
 export class AttachmentService {
   constructor(
     @InjectDataSource('default')
     private voltaireService: DataSource,
+    private httpService: HttpService,
+    private configService: ConfigService,
   ) {}
   async parseMimeType(mimeType: string) {
     switch (mimeType) {
@@ -181,4 +188,51 @@ export class AttachmentService {
       console.log(error);
     }
   }
+  async uploadAttachmentToIPFS(attachment:Express.Multer.File | Buffer | Uint8Array | Blob | FormData ): Promise<IPFSResponse> {
+    try {
+      const res = await lastValueFrom(
+        this.httpService.post(
+          'https://ipfs.blockfrost.io/api/v0/ipfs/add',
+          attachment,
+          {
+            headers: {
+              project_id: this.configService.get<string>(
+                'BLOCKFROST_IPFS_PROJECT_ID',
+              )
+            },
+          },
+        ),
+      );
+      console.log(res.data);
+      return res.data;
+    } catch (error) {
+      console.error(error);
+      throw new HttpException(error.response.data, error.response.status);
+    }
+  }
+  async getAttachmentFromIPFS(hash: string, res:Response): Promise<any> {
+    try {
+      const response = await lastValueFrom(
+        this.httpService.get(`https://ipfs.blockfrost.io/api/v0/ipfs/gateway/${hash}`, {
+          headers: {
+            project_id: this.configService.get<string>(
+              'BLOCKFROST_IPFS_PROJECT_ID',
+            ),
+          },
+          responseType: 'stream', // Used stream to handle large files or non-JSON data
+        },
+      ),
+    );
+    for (const [key, value] of Object.entries(response.headers)) {
+      res.setHeader(key, value as string);
+    }
+
+    // Stream the data directly to the client
+    response.data.pipe(res);
+      //return response.data;
+    } catch (error) {
+      console.error(error);
+      throw new HttpException(error.response.data, error.response.status);
+    }
+  }
 }
diff --git a/backend/src/common/types.ts b/backend/src/common/types.ts
index 1f4c75adb..c753edb6c 100644
--- a/backend/src/common/types.ts
+++ b/backend/src/common/types.ts
@@ -33,3 +33,8 @@ export type ValidateMetadataResult = {
   valid: boolean;
   metadata?: any;
 };
+export type IPFSResponse = {
+  name: string;
+  ipfs_hash: string;
+  size: number; 
+};
\ No newline at end of file
diff --git a/backend/src/drep/drep.controller.ts b/backend/src/drep/drep.controller.ts
index 66d7b32ef..5d6f86b85 100644
--- a/backend/src/drep/drep.controller.ts
+++ b/backend/src/drep/drep.controller.ts
@@ -7,6 +7,7 @@ import {
   ParseIntPipe,
   Post,
   Query,
+  Res,
   Search,
   UploadedFile,
   UseInterceptors,
@@ -17,6 +18,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
 import { DrepService } from './drep.service';
 import { VoterService } from 'src/voter/voter.service';
 import { Delegation, StakeKeys } from 'src/common/types';
+import { Response } from 'express';
 
 @Controller('dreps')
 export class DrepController {
@@ -117,8 +119,8 @@ export class DrepController {
     return this.drepService.getMetadataFromExternalLink(metadataUrl);
   }
   @Get(':drepId/metadata/:hash')
-  getMetadata(@Param('drepId') drepId: number, @Param('hash') hash: string) {
-    return this.drepService.getMetadata(drepId, hash);
+  getMetadata(@Param('drepId') drepId: number, @Param('hash') hash: string,  @Res() res: Response,) {
+    return this.drepService.getMetadata(drepId, hash, res);
   }
   @Post('metadata/validate')
   validateMetadata(@Body() metadataBody: ValidateMetadataDTO) {
@@ -134,7 +136,6 @@ export class DrepController {
   ) {
     return this.drepService.saveMetadata(metadata, hash, drepId, name);
   }
-
   @Get(':voterId/stats')
   getStats(@Param('voterId') voterId: string) {
     return this.drepService.getStats(voterId);
diff --git a/backend/src/drep/drep.service.ts b/backend/src/drep/drep.service.ts
index 50f83a0c2..45027298d 100644
--- a/backend/src/drep/drep.service.ts
+++ b/backend/src/drep/drep.service.ts
@@ -1,10 +1,16 @@
-import { Injectable, Logger, NotFoundException } from '@nestjs/common';
+import {
+  HttpException,
+  HttpStatus,
+  Injectable,
+  Logger,
+  NotFoundException,
+} from '@nestjs/common';
 import { createDrepDto, ValidateMetadataDTO } from 'src/dto';
 import { faker } from '@faker-js/faker';
 import * as blake from 'blakejs';
 import { HttpService } from '@nestjs/axios';
 import { AttachmentService } from 'src/attachment/attachment.service';
-import { Observable } from 'rxjs';
+import { lastValueFrom, Observable } from 'rxjs';
 import { AxiosResponse } from 'axios';
 import { InjectDataSource } from '@nestjs/typeorm';
 import { DataSource } from 'typeorm';
@@ -14,6 +20,7 @@ import { ReactionsService } from 'src/reactions/reactions.service';
 import { CommentsService } from 'src/comments/comments.service';
 import {
   Delegation,
+  IPFSResponse,
   LoggerMessage,
   MetadataStandard,
   MetadataValidationStatus,
@@ -32,6 +39,8 @@ import { parseMetadata } from 'src/common/parseMetadata';
 import { Metadata } from 'src/entities/metadata.entity';
 import { getEpochParams } from 'src/queries/getEpochParams';
 import { getDRepDelegatorsHistory } from 'src/queries/drepDelegatorsHistory';
+import { JsonLd } from 'jsonld/jsonld-spec';
+import { Response } from 'express';
 
 @Injectable()
 export class DrepService {
@@ -913,7 +922,7 @@ export class DrepService {
       .getRepository('Drep')
       .update(drepId, updatedDrep);
   }
-  async getMetadata(drepId: number, hash: string) {
+  async getMetadata(drepId: number, hash: string, res: Response) {
     if (!drepId || !hash) throw new Error('Inadequate parameters');
     const foundMetadata = await this.voltaireService
       .getRepository('Metadata')
@@ -921,8 +930,9 @@ export class DrepService {
       .where('metadata.drep = :drepId', { drepId })
       .andWhere('metadata.hash = :hash', { hash })
       .getOne();
-
-    return foundMetadata ? foundMetadata?.content : 'Not found';
+    if (!foundMetadata) throw new NotFoundException('Metadata not found');
+    const cid = foundMetadata.content;
+    return await this.getMetadataFromIPFS(cid, res);
   }
   async getMetadataFromExternalLink(metadataUrl: string) {
     if (!metadataUrl) throw new Error('Inadequate parameters');
@@ -993,21 +1003,14 @@ export class DrepService {
       .andWhere('metadata.hash = :hash', { hash: hash })
       .getOne();
     if (existingMetadata) {
-      const updateMetadata = {
-        name: fileName + '.jsonld',
-        hash: hash,
-        content: metadata,
-        drep: drepId,
-      };
-
-      await metadataRepo.update(existingMetadata.id, updateMetadata);
       return existingMetadata;
     }
-
+    // Create a new metadata record in IPFS
+    const { ipfs_hash } = await this.saveMetadataToIPFS(metadata);
     const newMetadata = {
       name: fileName + '.jsonld',
       hash: hash,
-      content: metadata,
+      content: ipfs_hash,
       drep: drepId,
     };
 
@@ -1015,7 +1018,34 @@ export class DrepService {
     const res = (await metadataRepo.save(createdMetadata)) as Metadata;
     return res;
   }
-
+  async saveMetadataToIPFS(metadata: JsonLd): Promise<IPFSResponse> {
+    try {
+      //save to IPFS via blockfrost
+      const metadataStr = JSON.stringify(metadata);
+      const binary = Buffer.from(metadataStr);
+
+      // Prepare the FormData
+      const formData = new FormData();
+      formData.append('file', binary as any);
+      const res = await this.attachmentService.uploadAttachmentToIPFS(formData);
+      return res;
+    } catch (error) {
+      console.error(error);
+      throw error;
+    }
+  }
+  async getMetadataFromIPFS(hash: string, res: Response): Promise<JsonLd> {
+    try {
+      const response = await this.attachmentService.getAttachmentFromIPFS(
+        hash,
+        res,
+      );
+      return response;
+    } catch (error) {
+      console.error(error);
+      throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+  }
   async getStats(drepVoterId: string) {
     const drepDelegatorsCountResult = await this.cexplorerService.manager.query(
       getDRepDelegatorsCountQuery,
@@ -1075,12 +1105,10 @@ export class DrepService {
     );
     const addrIds = addrIdsResult.map((row) => row.addr_id);
 
-    const drepDelegations = await this.cexplorerService.manager.query(getDRepDelegatorsHistory(addrIds), [
-      drepHashId,
-      drepVoterId,
-      beforeDate,
-      tillDate,
-    ]);
+    const drepDelegations = await this.cexplorerService.manager.query(
+      getDRepDelegatorsHistory(addrIds),
+      [drepHashId, drepVoterId, beforeDate, tillDate],
+    );
     return drepDelegations;
   }
 }
diff --git a/backend/src/entities/metadata.entity.ts b/backend/src/entities/metadata.entity.ts
index 1df68ee0d..53933b849 100644
--- a/backend/src/entities/metadata.entity.ts
+++ b/backend/src/entities/metadata.entity.ts
@@ -9,8 +9,8 @@ export class Metadata extends BaseEntity {
   @Column({ nullable: false })
   hash: string;
 
-  @Column({ type: 'json', nullable: false })
-  content: Record<string, any>;
+  @Column({ nullable: false })
+  content: string;
 
   @ManyToOne(() => Drep, (drep) => drep.id)
   drep: Drep;
diff --git a/frontend/src/components/molecules/DrepTimeline.tsx b/frontend/src/components/molecules/DrepTimeline.tsx
index c09c887f7..432178521 100644
--- a/frontend/src/components/molecules/DrepTimeline.tsx
+++ b/frontend/src/components/molecules/DrepTimeline.tsx
@@ -241,7 +241,7 @@ const DrepTimeline = ({
       <div
         id="scrollableDiv"
         style={{
-          height: 1000,
+          height: 1600,
           overflow: 'auto',
           display: 'flex',
           flexDirection: 'column',
-- 
GitLab