Skip to content

Commit

Permalink
feat: mint and register ip asset when create chapter
Browse files Browse the repository at this point in the history
  • Loading branch information
harisato committed Oct 16, 2024
1 parent 6028d78 commit d90b725
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 5 deletions.
27 changes: 27 additions & 0 deletions src/modules/chapter/chapter.graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,4 +433,31 @@ export class ChapterGraphql {
headers
);
}

async queryMangaMainTitle(variables: any) {
const headers = {
'x-hasura-admin-secret': this.configService.get<string>(
'graphql.adminSecret'
),
};
const result = await this.graphqlSvc.query(
this.configService.get<string>('graphql.endpoint'),
'',
`query manga_by_pk($id: Int!) {
manga_by_pk(id: $id) {
manga_languages(where: {is_main_language: {_eq: true}}) {
title
}
}
}
`,
'manga_by_pk',
variables,
headers
);

return (
result.data?.manga_by_pk?.manga_languages[0]?.title || 'Punkga Manga'
);
}
}
10 changes: 9 additions & 1 deletion src/modules/chapter/chapter.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ import { ChapterGraphql } from './chapter.graphql';
import { ChapterService } from './chapter.service';
import { UploadChapterService } from './upload-chapter.service';
import { CreatorModule } from '../creator/creator.module';
import { StoryEventModule } from '../story-event/story-event.module';

@Module({
imports: [JwtModule, GraphqlModule, FilesModule, MangaModule, CreatorModule],
imports: [
JwtModule,
GraphqlModule,
FilesModule,
MangaModule,
CreatorModule,
StoryEventModule,
],
providers: [ChapterService, UploadChapterService, ChapterGraphql],
controllers: [ChapterController],
exports: [UploadChapterService, ChapterGraphql, ChapterService],
Expand Down
60 changes: 59 additions & 1 deletion src/modules/chapter/chapter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import { ViewProtectedChapterRequestDto } from './dto/view-chapter-request.dto';
import { UploadChapterService } from './upload-chapter.service';
import { mkdirp, writeFilesToFolder } from './utils';
import { CreatorService } from '../creator/creator.service';
import { StoryEventGraphql } from '../story-event/story-event.graphql';
import { IPFSService } from '../files/ipfs.service';
import { FilesService } from '../files/files.service';

@Injectable()
export class ChapterService {
Expand All @@ -39,7 +42,10 @@ export class ChapterService {
private chapterGraphql: ChapterGraphql,
private uploadChapterService: UploadChapterService,
private configSvc: ConfigService,
private creatorService: CreatorService
private creatorService: CreatorService,
private storyEventService: StoryEventGraphql,
private ipfsService: IPFSService,
private fileService: FilesService
) {}

async upload(data: UploadInputDto, file: Express.Multer.File) {
Expand Down Expand Up @@ -148,6 +154,55 @@ export class ChapterService {
chapterImages: chapter_images,
});

if (submission_id) {
// query submission
const submission = await this.storyEventService.getSubmissionDetail({
id: submission_id,
});

// upload chapter images to ipfs
const ipfsDisplayUrl = this.configSvc.get<string>('network.ipfsQuery');

const ipfsImageFolder = `/punkga-manga-${manga_id}-chapter-${chapterId}/images`;
const { cid: chapterImagesCid } =
await this.ipfsService.uploadLocalFolderToIpfs(
storageFolder,
ipfsImageFolder
);
const ipfsFolderUrl = `${ipfsDisplayUrl}/${chapterImagesCid}`;

// upload nft image to ipfs
const thumbnail = files.filter((f) => f.fieldname === 'thumbnail')[0];
const { cid: thumbnailCid } = await this.fileService.uploadFileToIpfs(
thumbnail.buffer,
thumbnail.originalname
);
const thumbnailIpfs = `${ipfsDisplayUrl}/${thumbnailCid}`;

// query manga main title
const mangaMainTitle = await this.chapterGraphql.queryMangaMainTitle({
id: manga_id,
});

const metadata = {
name: mangaMainTitle,
description: `Punkga Story Event Manga - ${mangaMainTitle}`,
attributes: [
{
chapter_images: ipfsFolderUrl,
},
],
image: thumbnailIpfs,
};
const { cid: metadataCID } =
await this.fileService.uploadMetadataToIpfs(
metadata,
`/metadata-${new Date().getTime()}`
);

// create job to mint and register ip_asset
}

// remove files
rimraf.sync(storageFolder);

Expand Down Expand Up @@ -195,6 +250,7 @@ export class ChapterService {
}
}
}

return result.data;
} catch (errors) {
return {
Expand Down Expand Up @@ -519,4 +575,6 @@ export class ChapterService {

return this.chapterGraphql.deactiveChapter(id);
}

private uploadChapterImagesToIpfs() {}
}
5 changes: 3 additions & 2 deletions src/modules/files/files.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { FilesService } from './files.service';
import { FileController } from './files.controller';
import { JwtModule } from '@nestjs/jwt';
import { IPFSService } from './ipfs.service';

@Module({
imports: [JwtModule],
providers: [FilesService],
providers: [FilesService, IPFSService],
controllers: [FileController],
exports: [FilesService],
exports: [FilesService, IPFSService],
})
export class FilesModule {}
137 changes: 137 additions & 0 deletions src/modules/files/ipfs.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { readFileSync, readdirSync } from 'fs';
import { create, IPFSHTTPClient } from 'ipfs-http-client';
import _, { omit } from 'lodash';
import { IMetadata } from '../collection/interfaces/metadata';
// import { IMetadata } from '../launchpad/interfaces/metadata';

@Injectable()
export class IPFSService implements OnModuleInit {
private readonly logger = new Logger(IPFSService.name);
private ipfsClient: IPFSHTTPClient;

constructor(private configService: ConfigService) {}

onModuleInit() {
const ipfsUrl = this.configService.get<string>('network.ipfsUrl');

this.ipfsClient = create({
url: ipfsUrl,
timeout: 60000,
});
}

async uploadImageToIpfs(file: Express.Multer.File) {
if (!file.mimetype.includes('image')) {
throw Error('file type is not valid');
}

const response = await this.ipfsClient.add(
{
path: file.originalname,
content: file.buffer,
},
{
wrapWithDirectory: true,
}
);

return `/ipfs/${response.cid.toString()}/${file.originalname}`;
}

async uploadLocalFolderToIpfs(
localFolderPath: string,
ipfsFolderPath: string,
chunkSize = 10
) {
const filenames = readdirSync(localFolderPath);
if (_.isEmpty(filenames))
throw new Error(`Folder ${localFolderPath} is empty`);

// await this.ipfsClient.files.rm(`/${ipfsFolderPath}`, { recursive: true });

await this.ipfsClient.files.mkdir(ipfsFolderPath, { parents: true });
for (let i = 0; i < filenames.length; i += chunkSize) {
const uploadPromisses = filenames
.slice(i, i + chunkSize)
.map((filename) => {
const ipfsFilePath = `${ipfsFolderPath}/${filename}`;
const content = readFileSync(`${localFolderPath}/${filename}`);
return this.ipfsClient.files.write(ipfsFilePath, content, {
create: true,
});
});
await Promise.all(uploadPromisses);
}

const folderCid = (
await this.ipfsClient.files.stat(ipfsFolderPath)
).cid.toString();

console.log(
`\nUploaded local folder (${localFolderPath}). CID: ${folderCid}`
);

return {
cid: folderCid,
filenames: filenames.sort((i1, i2) =>
i1.localeCompare(i2, undefined, {
sensitivity: 'variant',
numeric: true,
})
),
};
}

async uploadMetadataObjectsToIpfs(
objects: IMetadata[],
ipfsFolderPath: string,
chunkSize = 10
): Promise<string> {
await this.createFolderIfNotExist(ipfsFolderPath);

for (let i = 0; i < objects.length; i += chunkSize) {
const uploadPromisses = objects.slice(i, i + chunkSize).map((object) => {
const ipfsPath = `${ipfsFolderPath}/${object.token_id}`;
const content = JSON.stringify(omit(object, ['filename', 'token_id']));
return this.ipfsClient.files.write(ipfsPath, content, { create: true });
});

await Promise.all(uploadPromisses);
}

const folderCid = (
await this.ipfsClient.files.stat(ipfsFolderPath)
).cid.toString();

return folderCid;
}

async uploadMetadataContractToIpfs(object: any, ipfsFolderPath: string) {
await this.createFolderIfNotExist(ipfsFolderPath);
const ipfsPath = `${ipfsFolderPath}/metadata_contract`;
const content = JSON.stringify(object);
await this.ipfsClient.files.write(ipfsPath, content, { create: true });
const metadataContractCid = (
await this.ipfsClient.files.stat(ipfsPath)
).cid.toString();

return metadataContractCid;
}

private async createFolderIfNotExist(ipfsPath: string) {
const exists = await this.checkExist(ipfsPath);
if (!exists) return this.ipfsClient.files.mkdir(ipfsPath);
}

private async checkExist(ipfsPath: string) {
try {
const result = await this.ipfsClient.files.stat(ipfsPath);
if (result?.cid) return true;
return false;
} catch {
return false;
}
}
}
27 changes: 27 additions & 0 deletions src/modules/story-event/story-event.graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,4 +588,31 @@ export class StoryEventGraphql {
headers
);
}

async getSubmissionDetail(variables: any) {
const headers = {
'x-hasura-admin-secret': this.configSvc.get<string>(
'graphql.adminSecret'
),
};

const result = await this.graphqlSvc.query(
this.configSvc.get<string>('graphql.endpoint'),
'',
`query story_event_submission_by_pk($id: Int!) {
story_event_submission_by_pk(id: $id) {
id
data
name
status
type
}
}`,
'story_event_submission_by_pk',
variables,
headers
);

return result.data.story_event_submission_by_pk;
}
}
2 changes: 1 addition & 1 deletion src/modules/story-event/story-event.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ import { StoryEventConsumer } from './story-event.consumer';
],
providers: [StoryEventService, StoryEventGraphql, StoryEventConsumer],
controllers: [StoryEventController],
exports: [],
exports: [StoryEventService, StoryEventGraphql],
})
export class StoryEventModule {}
7 changes: 7 additions & 0 deletions src/utils/nft-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface IMetadata {
token_id: number;
name: string;
description: string;
attributes: any[];
image: string;
}

0 comments on commit d90b725

Please sign in to comment.