From 4f47240b3bde8a5652500b71dd5317684e77e65b Mon Sep 17 00:00:00 2001 From: harisato Date: Thu, 17 Oct 2024 14:10:04 +0700 Subject: [PATCH] feat: create ipa when create chapter --- .../down.sql | 1 + .../up.sql | 1 + src/modules/chapter/chapter.graphql.ts | 29 ++++ src/modules/chapter/chapter.service.ts | 82 ++++++++-- .../story-event/story-event.consumer.ts | 154 +++++++++++++----- .../story-event/story-event.graphql.ts | 21 +++ .../story-event/story-event.service.ts | 43 +++-- 7 files changed, 262 insertions(+), 69 deletions(-) create mode 100644 hasura/migrations/punkga-pg/1729142618680_alter_table_public_story_manga_alter_column_story_ip_asset_id/down.sql create mode 100644 hasura/migrations/punkga-pg/1729142618680_alter_table_public_story_manga_alter_column_story_ip_asset_id/up.sql diff --git a/hasura/migrations/punkga-pg/1729142618680_alter_table_public_story_manga_alter_column_story_ip_asset_id/down.sql b/hasura/migrations/punkga-pg/1729142618680_alter_table_public_story_manga_alter_column_story_ip_asset_id/down.sql new file mode 100644 index 0000000..0d76e3b --- /dev/null +++ b/hasura/migrations/punkga-pg/1729142618680_alter_table_public_story_manga_alter_column_story_ip_asset_id/down.sql @@ -0,0 +1 @@ +alter table "public"."story_manga" alter column "story_ip_asset_id" set not null; diff --git a/hasura/migrations/punkga-pg/1729142618680_alter_table_public_story_manga_alter_column_story_ip_asset_id/up.sql b/hasura/migrations/punkga-pg/1729142618680_alter_table_public_story_manga_alter_column_story_ip_asset_id/up.sql new file mode 100644 index 0000000..de0545a --- /dev/null +++ b/hasura/migrations/punkga-pg/1729142618680_alter_table_public_story_manga_alter_column_story_ip_asset_id/up.sql @@ -0,0 +1 @@ +alter table "public"."story_manga" alter column "story_ip_asset_id" drop not null; diff --git a/src/modules/chapter/chapter.graphql.ts b/src/modules/chapter/chapter.graphql.ts index 3462793..993ff87 100644 --- a/src/modules/chapter/chapter.graphql.ts +++ b/src/modules/chapter/chapter.graphql.ts @@ -319,6 +319,35 @@ export class ChapterGraphql { } } + async queryUserAddressById(userId: string): Promise { + const headers = { + 'x-hasura-admin-secret': this.configService.get( + 'graphql.adminSecret' + ), + }; + + const result = await this.graphqlSvc.query( + this.configService.get('graphql.endpoint'), + '', + `query authorizer_users($id: bpchar!) { + authorizer_users(limit: 1, where: {id: {_eq: $id}}) { + active_wallet_address: active_evm_address + } + }`, + 'authorizer_users', + { + id: userId, + }, + headers + ); + + if (result.data.authorizer_users[0]?.active_wallet_address) { + return result.data.authorizer_users[0]?.active_wallet_address; + } else { + throw new NotFoundException('wallet address not found'); + } + } + async adminCreateChapter(variables: any) { const headers = { 'x-hasura-admin-secret': this.configService.get( diff --git a/src/modules/chapter/chapter.service.ts b/src/modules/chapter/chapter.service.ts index 2353b21..0fcc9f7 100644 --- a/src/modules/chapter/chapter.service.ts +++ b/src/modules/chapter/chapter.service.ts @@ -32,6 +32,9 @@ 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'; +import { StoryEventService } from '../story-event/story-event.service'; +import { SubmissionType } from '../story-event/story-event.enum'; +import { getBytes32FromIpfsHash } from '../story-event/utils'; @Injectable() export class ChapterService { @@ -43,7 +46,8 @@ export class ChapterService { private uploadChapterService: UploadChapterService, private configSvc: ConfigService, private creatorService: CreatorService, - private storyEventService: StoryEventGraphql, + private storyEventGrapqh: StoryEventGraphql, + private storyEventService: StoryEventService, private ipfsService: IPFSService, private fileService: FilesService ) {} @@ -134,7 +138,7 @@ export class ChapterService { chapter_type, pushlish_date, status, - submission_id, + story_submission_id: submission_id, thumbnail_url: newThumbnailUrl, }); @@ -155,21 +159,24 @@ export class ChapterService { }); if (submission_id) { - // query submission - const submission = await this.storyEventService.getSubmissionDetail({ - id: submission_id, - }); - // upload chapter images to ipfs const ipfsDisplayUrl = this.configSvc.get('network.ipfsQuery'); - const ipfsImageFolder = `/punkga-manga-${manga_id}-chapter-${chapterId}/images`; - const { cid: chapterImagesCid } = - await this.ipfsService.uploadLocalFolderToIpfs( - storageFolder, - ipfsImageFolder - ); - const ipfsFolderUrl = `${ipfsDisplayUrl}/${chapterImagesCid}`; + let viChapterImagesIpfsUrl; + let enChapterImagesIpfsUrl; + for (const chapter_language of chapter_images.chapter_languages) { + const ipfsImageFolder = `/punkga-manga-${manga_id}-chapter-${chapterId}/${chapter_language.language_id}/images`; + const { cid: chapterImagesCid } = + await this.ipfsService.uploadLocalFolderToIpfs( + `${storageFolder}/unzip/1`, + ipfsImageFolder + ); + const ipfsFolderUrl = `${ipfsDisplayUrl}/${chapterImagesCid}`; + if (chapter_language.language_id === 1) + enChapterImagesIpfsUrl = ipfsFolderUrl; + if (chapter_language.language_id === 2) + viChapterImagesIpfsUrl = ipfsFolderUrl; + } // upload nft image to ipfs const thumbnail = files.filter((f) => f.fieldname === 'thumbnail')[0]; @@ -189,7 +196,10 @@ export class ChapterService { description: `Punkga Story Event Manga - ${mangaMainTitle}`, attributes: [ { - chapter_images: ipfsFolderUrl, + chapter_images: { + vi: viChapterImagesIpfsUrl, + en: enChapterImagesIpfsUrl, + }, }, ], image: thumbnailIpfs, @@ -200,7 +210,47 @@ export class ChapterService { `/metadata-${new Date().getTime()}` ); + const insertStoryMangaResult = + await this.storyEventGrapqh.insertStoryManga({ + object: { + manga_id: manga_id, + ipfs_url: metadataCID, + }, + }); + if (insertStoryMangaResult.errors) return insertStoryMangaResult; + const storyMangaId = + insertStoryMangaResult.data.insert_story_manga_one.id; + // create job to mint and register ip_asset + // --- query submission + const submission = await this.storyEventGrapqh.getSubmissionDetail({ + id: submission_id, + }); + const character_ids = submission.data.manga_characters; + const queryStoryCharactersResult = + await this.storyEventGrapqh.queryStoryCharacters({ + story_character_ids: character_ids.map( + (character) => character.story_character_id + ), + }); + const ipAssetIds = queryStoryCharactersResult.data.story_character.map( + (character) => character.story_ip_asset.ip_asset_id + ); + + const userWalletAddress = + await this.chapterGraphql.queryUserAddressById(userId); + const jobData = { + name: mangaMainTitle, + user_id: userId, + metadata_ipfs: `${ipfsDisplayUrl}/${metadataCID}`, + // story_artwork_id: storyArtworkId, + submission_id: submission_id, + user_wallet_address: userWalletAddress, + ip_asset_ids: ipAssetIds, + metadata_hash: getBytes32FromIpfsHash(metadataCID), + }; + + await this.storyEventService.addEventJob(SubmissionType.Manga, jobData); } // remove files @@ -432,7 +482,7 @@ export class ChapterService { chapter_type, pushlish_date, status, - submission_id, + story_submission_id: submission_id, thumbnail_url: newThumbnailUrl !== '' ? newThumbnailUrl : thumbnail_url, }, diff --git a/src/modules/story-event/story-event.consumer.ts b/src/modules/story-event/story-event.consumer.ts index 7345fd9..415b28b 100644 --- a/src/modules/story-event/story-event.consumer.ts +++ b/src/modules/story-event/story-event.consumer.ts @@ -190,51 +190,46 @@ export class StoryEventConsumer implements OnModuleInit { } } - async createStoryMangaIpAsset(data: any) {} - - async createStoryArtworkIpAsset(data: any) { + async createStoryMangaIpAsset(data: any) { try { // mint nft & create ipa + const { ipAssetId, nftId, hash } = + await this.mintAndRegisterIpAndMakeDerivative(data); - const args = [ - [ - data.ip_asset_ids, - '0x8bb1ade72e21090fc891e1d4b88ac5e57b27cb31', - defaultPILTerms.licenseTermsIds, - defaultPILTerms.royaltyContext, - ], - [ - data.metadata_ipfs, - data.metadata_hash, - data.metadata_ipfs, - data.metadata_hash, - ], - data.user_wallet_address, - ]; - - const address = - `${this.storyChain.contracts.story_event_derivative_contract}` as any; - - const hash = await this.walletClient.writeContract({ - abi: storyEventDerivativeAbi, - address, - functionName: 'mintAndRegisterIpAndMakeDerivative', - args, - chain: iliad, - account: this.account, - }); - const txReceipt = (await this.publicClient.waitForTransactionReceipt({ - hash, - })) as any; - - const targetLogs = parseTxIpRegisteredEvent(txReceipt); + // // --- update story artwork set story_ip_id + // const updateStoryArtworkResult = + // await this.storyEventGraphql.updateStoryArtwork({ + // id: data.story_artwork_id, + // story_ip_asset_id: + // insertStoryIPAResult.data.insert_story_ip_asset_one.id, + // }); + // if (updateStoryArtworkResult.errors) { + // this.logger.error( + // `Update story artwork error: ${JSON.stringify( + // updateStoryArtworkResult + // )}` + // ); + // throw new InternalServerErrorException('Update story artwork failed '); + // } - let nftId = 0; - let ipAssetId = targetLogs[0].ipId; + this.logger.log( + `Create Artwork IP Asset Done: ipid ${ipAssetId} hash ${hash}` + ); + } catch (error) { + this.logger.error(error.message); + return { + errors: { + message: error.message, + }, + }; + } + } - if (txReceipt.logs[0].topics[3]) { - nftId = parseInt(txReceipt.logs[0].topics[3], 16); - } + async createStoryArtworkIpAsset(data: any) { + try { + // mint nft & create ipa + const { ipAssetId, nftId, hash } = + await this.mintAndRegisterIpAndMakeDerivative(data); // update offchain data // --- insert story ip asset @@ -294,4 +289,83 @@ export class StoryEventConsumer implements OnModuleInit { }; } } + + async mintAndRegisterIpAndMakeDerivative(data: any) { + const args = [ + [ + data.ip_asset_ids, + '0x8bb1ade72e21090fc891e1d4b88ac5e57b27cb31', + defaultPILTerms.licenseTermsIds, + defaultPILTerms.royaltyContext, + ], + [ + data.metadata_ipfs, + data.metadata_hash, + data.metadata_ipfs, + data.metadata_hash, + ], + data.user_wallet_address, + ]; + + const address = + `${this.storyChain.contracts.story_event_derivative_contract}` as any; + + const hash = await this.walletClient.writeContract({ + abi: storyEventDerivativeAbi, + address, + functionName: 'mintAndRegisterIpAndMakeDerivative', + args, + chain: iliad, + account: this.account, + }); + const txReceipt = (await this.publicClient.waitForTransactionReceipt({ + hash, + })) as any; + + const targetLogs = parseTxIpRegisteredEvent(txReceipt); + + let nftId = 0; + let ipAssetId = targetLogs[0].ipId; + + if (txReceipt.logs[0].topics[3]) { + nftId = parseInt(txReceipt.logs[0].topics[3], 16); + } + + // update offchain data + // --- insert story ip asset + const insertStoryIPAResult = await this.storyEventGraphql.insertStoryIPA({ + object: { + ip_asset_id: ipAssetId, + nft_contract_address: this.storyChain.contracts.story_event_contract, + nft_token_id: nftId.toString(), + tx_hash: hash, + user_id: data.user_id, + }, + }); + if (insertStoryIPAResult.errors) { + this.logger.error( + `Insert story IP Asset error: ${JSON.stringify(insertStoryIPAResult)}` + ); + throw new InternalServerErrorException('Insert story IP Asset failed '); + } + + // --- update submission set status = done + const updateSubmissionResult = + await this.storyEventGraphql.updateSubmission({ + id: data.submission_id, + status: SubmissionStatus.Approved, + }); + if (updateSubmissionResult.errors) { + this.logger.error( + `Update submission error: ${JSON.stringify(updateSubmissionResult)}` + ); + throw new InternalServerErrorException('Update submission failed '); + } + + return { + ipAssetId, + nftId, + hash, + }; + } } diff --git a/src/modules/story-event/story-event.graphql.ts b/src/modules/story-event/story-event.graphql.ts index e7c676a..432a686 100644 --- a/src/modules/story-event/story-event.graphql.ts +++ b/src/modules/story-event/story-event.graphql.ts @@ -497,6 +497,27 @@ export class StoryEventGraphql { ); } + async insertStoryManga(variables: any) { + const headers = { + 'x-hasura-admin-secret': this.configSvc.get( + 'graphql.adminSecret' + ), + }; + + return this.graphqlSvc.query( + this.configSvc.get('graphql.endpoint'), + '', + `mutation insert_story_manga($object: story_manga_insert_input = {}) { + insert_story_manga_one(object: $object) { + id + } + }`, + 'insert_story_manga', + variables, + headers + ); + } + async queryStoryCharacters(variables: any) { const headers = { 'x-hasura-admin-secret': this.configSvc.get( diff --git a/src/modules/story-event/story-event.service.ts b/src/modules/story-event/story-event.service.ts index b4a650c..42d1e7f 100644 --- a/src/modules/story-event/story-event.service.ts +++ b/src/modules/story-event/story-event.service.ts @@ -399,20 +399,21 @@ export class StoryEventService { metadata_hash: getBytes32FromIpfsHash(metadataCID), }; - await this.storyEventQueue.add( - 'event', - { - type: SubmissionType.Artwork, - data: jobData, - }, - { - removeOnComplete: true, - removeOnFail: 10, - attempts: 5, - backoff: 5000, - } - ); + // await this.storyEventQueue.add( + // 'event', + // { + // type: SubmissionType.Artwork, + // data: jobData, + // }, + // { + // removeOnComplete: true, + // removeOnFail: 10, + // attempts: 5, + // backoff: 5000, + // } + // ); + await this.addEventJob(SubmissionType.Artwork, jobData); // return return result; } catch (error) { @@ -512,4 +513,20 @@ export class StoryEventService { user_id: userId, }); } + + addEventJob(type: SubmissionType, jobData: any) { + return this.storyEventQueue.add( + 'event', + { + type, + data: jobData, + }, + { + removeOnComplete: true, + removeOnFail: 10, + attempts: 5, + backoff: 5000, + } + ); + } }