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