From df0c7a5f1f02a9f815b84ecd12072c3e80356923 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sat, 9 Sep 2023 15:30:02 +0300 Subject: [PATCH 01/10] File selection logic for thumbnails --- .../collections/creation/UploadDetails.tsx | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/components/collections/creation/UploadDetails.tsx b/components/collections/creation/UploadDetails.tsx index bb99e3b..4eb2de8 100644 --- a/components/collections/creation/UploadDetails.tsx +++ b/components/collections/creation/UploadDetails.tsx @@ -1,4 +1,5 @@ /* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable array-callback-return */ /* eslint-disable no-nested-ternary */ /* eslint-disable no-misleading-character-class */ /* eslint-disable no-control-regex */ @@ -19,6 +20,8 @@ import type { ChangeEvent } from 'react' import { useEffect, useRef, useState } from 'react' import { toast } from 'react-hot-toast' import type { UploadServiceType } from 'services/upload' +import type { AssetType } from 'utils/getAssetType' +import { getAssetType } from 'utils/getAssetType' import { uid } from 'utils/random' import { naturalCompare } from 'utils/sort' @@ -37,6 +40,7 @@ interface UploadDetailsProps { export interface UploadDetailsDataProps { assetFiles: File[] metadataFiles: File[] + thumbnailFiles?: File[] uploadService: UploadServiceType nftStorageApiKey?: string pinataApiKey?: string @@ -55,6 +59,8 @@ export const UploadDetails = ({ }: UploadDetailsProps) => { const [assetFilesArray, setAssetFilesArray] = useState([]) const [metadataFilesArray, setMetadataFilesArray] = useState([]) + const [thumbnailCompatibleAssetFileNames, setThumbnailCompatibleAssetFileNames] = useState([]) + const [thumbnailFilesArray, setThumbnailFilesArray] = useState([]) const [uploadMethod, setUploadMethod] = useState('new') const [uploadService, setUploadService] = useState('nft-storage') const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0) @@ -64,6 +70,7 @@ export const UploadDetails = ({ const assetFilesRef = useRef(null) const metadataFilesRef = useRef(null) + const thumbnailFilesRef = useRef(null) const nftStorageApiKeyState = useInputState({ id: 'nft-storage-api-key', @@ -112,6 +119,15 @@ export const UploadDetails = ({ const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) //check if the sorted file names are in numerical order const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) + const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html'] + const thumbnailCompatibleFileNamesList: string[] = [] + sortedFiles.map((file) => { + if (thumbnailCompatibleAssetTypes.includes(getAssetType(file.name))) { + thumbnailCompatibleFileNamesList.push(file.name.split('.')[0]) + } + }) + setThumbnailCompatibleAssetFileNames(thumbnailCompatibleFileNamesList) + console.log('Thumbnail Compatible Files: ', thumbnailCompatibleFileNamesList) for (let i = 0; i < sortedFileNames.length; i++) { if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) { toast.error('The file names should be in numerical order starting from 1.') @@ -230,6 +246,51 @@ export const UploadDetails = ({ const regex = /[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g + const selectThumbnails = (event: ChangeEvent) => { + setThumbnailFilesArray([]) + if (event.target.files === null) return + + if (minterType === 'vending' || (minterType === 'base' && thumbnailCompatibleAssetFileNames.length > 1)) { + const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) + const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) + // make sure the sorted file names match thumbnail compatible asset file names + for (let i = 0; i < sortedFileNames.length; i++) { + if (sortedFileNames[i] !== thumbnailCompatibleAssetFileNames[i]) { + toast.error('The thumbnail file names should match the thumbnail compatible asset file names.') + addLogItem({ + id: uid(), + message: 'The thumbnail file names should match the thumbnail compatible asset file names.', + type: 'Error', + timestamp: new Date(), + }) + //clear the input + event.target.value = '' + return + } + } + } + let loadedFileCount = 0 + const files: File[] = [] + let reader: FileReader + for (let i = 0; i < event.target.files.length; i++) { + reader = new FileReader() + reader.onload = (e) => { + if (!e.target?.result) return toast.error('Error parsing file.') + if (!event.target.files) return toast.error('No files selected.') + const thumbnailFile = new File([e.target.result], event.target.files[i].name, { type: 'image/jpg' }) + files.push(thumbnailFile) + } + reader.readAsArrayBuffer(event.target.files[i]) + reader.onloadend = () => { + if (!event.target.files) return toast.error('No file selected.') + loadedFileCount++ + if (loadedFileCount === event.target.files.length) { + setThumbnailFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name))) + } + } + } + } + useEffect(() => { try { const data: UploadDetailsDataProps = { From be2d644ec9ba40edbdeb424ab259a88f648b0994 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sat, 9 Sep 2023 20:39:05 +0300 Subject: [PATCH 02/10] Surface thumbnail compatible asset file names --- .../collections/creation/UploadDetails.tsx | 72 ++++++++++++++----- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/components/collections/creation/UploadDetails.tsx b/components/collections/creation/UploadDetails.tsx index 4eb2de8..276c09e 100644 --- a/components/collections/creation/UploadDetails.tsx +++ b/components/collections/creation/UploadDetails.tsx @@ -41,6 +41,7 @@ export interface UploadDetailsDataProps { assetFiles: File[] metadataFiles: File[] thumbnailFiles?: File[] + thumbnailCompatibleAssetFileNames?: string[] uploadService: UploadServiceType nftStorageApiKey?: string pinataApiKey?: string @@ -113,6 +114,7 @@ export const UploadDetails = ({ const selectAssets = (event: ChangeEvent) => { setAssetFilesArray([]) setMetadataFilesArray([]) + setThumbnailFilesArray([]) if (event.target.files === null) return if (minterType === 'vending' || (minterType === 'base' && event.target.files.length > 1)) { //sort the files @@ -250,25 +252,25 @@ export const UploadDetails = ({ setThumbnailFilesArray([]) if (event.target.files === null) return - if (minterType === 'vending' || (minterType === 'base' && thumbnailCompatibleAssetFileNames.length > 1)) { - const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) - const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) - // make sure the sorted file names match thumbnail compatible asset file names - for (let i = 0; i < sortedFileNames.length; i++) { - if (sortedFileNames[i] !== thumbnailCompatibleAssetFileNames[i]) { - toast.error('The thumbnail file names should match the thumbnail compatible asset file names.') - addLogItem({ - id: uid(), - message: 'The thumbnail file names should match the thumbnail compatible asset file names.', - type: 'Error', - timestamp: new Date(), - }) - //clear the input - event.target.value = '' - return - } + // if (minterType === 'vending' || (minterType === 'base' && thumbnailCompatibleAssetFileNames.length > 1)) { + const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) + const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) + // make sure the sorted file names match thumbnail compatible asset file names + for (let i = 0; i < sortedFileNames.length; i++) { + if (sortedFileNames[i] !== thumbnailCompatibleAssetFileNames[i]) { + toast.error('The thumbnail file names should match the thumbnail compatible asset file names.') + addLogItem({ + id: uid(), + message: 'The thumbnail file names should match the thumbnail compatible asset file names.', + type: 'Error', + timestamp: new Date(), + }) + //clear the input + event.target.value = '' + return } } + // } let loadedFileCount = 0 const files: File[] = [] let reader: FileReader @@ -296,6 +298,8 @@ export const UploadDetails = ({ const data: UploadDetailsDataProps = { assetFiles: assetFilesArray, metadataFiles: metadataFilesArray, + thumbnailFiles: thumbnailFilesArray, + thumbnailCompatibleAssetFileNames, uploadService, nftStorageApiKey: nftStorageApiKeyState.value, pinataApiKey: pinataApiKeyState.value, @@ -325,6 +329,8 @@ export const UploadDetails = ({ }, [ assetFilesArray, metadataFilesArray, + thumbnailFilesArray, + thumbnailCompatibleAssetFileNames, uploadService, nftStorageApiKeyState.value, pinataApiKeyState.value, @@ -607,6 +613,38 @@ export const UploadDetails = ({ )} + + {thumbnailCompatibleAssetFileNames.length > 0 && ( +
+ +
+ +
+
+ )} = 1}> Date: Sat, 9 Sep 2023 22:01:25 +0300 Subject: [PATCH 03/10] Update upload & metadata upload logic for standard & 1/1 collections --- .../collections/creation/UploadDetails.tsx | 18 ++++++-- pages/collections/create.tsx | 44 ++++++++++++++++--- services/upload/pinata.ts | 2 +- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/components/collections/creation/UploadDetails.tsx b/components/collections/creation/UploadDetails.tsx index 276c09e..a639031 100644 --- a/components/collections/creation/UploadDetails.tsx +++ b/components/collections/creation/UploadDetails.tsx @@ -115,14 +115,15 @@ export const UploadDetails = ({ setAssetFilesArray([]) setMetadataFilesArray([]) setThumbnailFilesArray([]) + setThumbnailCompatibleAssetFileNames([]) if (event.target.files === null) return + const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html'] + const thumbnailCompatibleFileNamesList: string[] = [] if (minterType === 'vending' || (minterType === 'base' && event.target.files.length > 1)) { //sort the files const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) //check if the sorted file names are in numerical order const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) - const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html'] - const thumbnailCompatibleFileNamesList: string[] = [] sortedFiles.map((file) => { if (thumbnailCompatibleAssetTypes.includes(getAssetType(file.name))) { thumbnailCompatibleFileNamesList.push(file.name.split('.')[0]) @@ -144,7 +145,14 @@ export const UploadDetails = ({ return } } + } else if (minterType === 'base' && event.target.files.length === 1) { + if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) { + thumbnailCompatibleFileNamesList.push(event.target.files[0].name.split('.')[0]) + } + setThumbnailCompatibleAssetFileNames(thumbnailCompatibleFileNamesList) + console.log('Thumbnail Compatible Files: ', thumbnailCompatibleFileNamesList) } + let loadedFileCount = 0 const files: File[] = [] let reader: FileReader @@ -256,7 +264,8 @@ export const UploadDetails = ({ const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) // make sure the sorted file names match thumbnail compatible asset file names - for (let i = 0; i < sortedFileNames.length; i++) { + for (let i = 0; i < thumbnailCompatibleAssetFileNames.length; i++) { + if (minterType === 'base' && assetFilesArray.length === 1) break if (sortedFileNames[i] !== thumbnailCompatibleAssetFileNames[i]) { toast.error('The thumbnail file names should match the thumbnail compatible asset file names.') addLogItem({ @@ -347,6 +356,9 @@ export const UploadDetails = ({ setMetadataFilesArray([]) if (assetFilesRef.current) assetFilesRef.current.value = '' setAssetFilesArray([]) + if (thumbnailFilesRef.current) thumbnailFilesRef.current.value = '' + setThumbnailFilesArray([]) + setThumbnailCompatibleAssetFileNames([]) if (!importedUploadDetails || minterType === 'base') { baseTokenUriState.onChange('') coverImageUrlState.onChange('') diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index 46a158d..b9eb557 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -734,7 +734,19 @@ const CollectionCreationPage: NextPage = () => { uploadDetails.pinataApiKey as string, uploadDetails.pinataSecretKey as string, ) - .then((assetUri: string) => { + .then(async (assetUri: string) => { + let thumbnailUri: string | undefined + if (uploadDetails.thumbnailFiles && uploadDetails.thumbnailFiles.length > 0) { + thumbnailUri = await upload( + uploadDetails.thumbnailFiles, + uploadDetails.uploadService, + 'thumbnail', + uploadDetails.nftStorageApiKey as string, + uploadDetails.pinataApiKey as string, + uploadDetails.pinataSecretKey as string, + ) + } + console.log('Thumbnail URI: ', thumbnailUri) if (minterType === 'vending' || (minterType === 'base' && uploadDetails.assetFiles.length > 1)) { const fileArray: File[] = [] let reader: FileReader @@ -751,9 +763,24 @@ const CollectionCreationPage: NextPage = () => { ) { data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}` } - if (getAssetType(uploadDetails.assetFiles[i].name) !== 'html') - data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}` - + data.image = `ipfs://${ + thumbnailUri && + (uploadDetails.thumbnailCompatibleAssetFileNames as string[]).includes( + uploadDetails.assetFiles[i].name.split('.')[0], + ) + ? thumbnailUri + : assetUri + }/${ + thumbnailUri && + (uploadDetails.thumbnailCompatibleAssetFileNames as string[]).includes( + uploadDetails.assetFiles[i].name.split('.')[0], + ) + ? uploadDetails.thumbnailFiles?.find( + (thumbnailFile) => + thumbnailFile.name.split('.')[0] === uploadDetails.assetFiles[i].name.split('.')[0], + )?.name + : uploadDetails.assetFiles[i].name + }` if (data.description) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call data.description = data.description.replaceAll('\\n', '\n') @@ -805,8 +832,13 @@ const CollectionCreationPage: NextPage = () => { ) { data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[0].name}` } - if (getAssetType(uploadDetails.assetFiles[0].name) !== 'html') - data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[0].name}` + data.image = `ipfs://${ + uploadDetails.thumbnailFiles && uploadDetails.thumbnailFiles.length > 0 ? thumbnailUri : assetUri + }/${ + uploadDetails.thumbnailFiles && uploadDetails.thumbnailFiles.length > 0 + ? uploadDetails.thumbnailFiles[0].name + : uploadDetails.assetFiles[0].name + }` const metadataFileBlob = new Blob([JSON.stringify(data)], { type: 'application/json', diff --git a/services/upload/pinata.ts b/services/upload/pinata.ts index 985de93..d024aab 100644 --- a/services/upload/pinata.ts +++ b/services/upload/pinata.ts @@ -4,7 +4,7 @@ import axios from 'axios' import { PINATA_ENDPOINT_URL } from 'utils/constants' -export type UploadFileType = 'assets' | 'metadata' | 'cover' +export type UploadFileType = 'assets' | 'metadata' | 'cover' | 'thumbnail' export const uploadToPinata = async ( fileArray: File[], From 26c39e8985d88c5e857596cfcddf64a360905439 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 10 Sep 2023 12:20:23 +0300 Subject: [PATCH 04/10] Enable thumbnail selection for OE/on-chain metadata --- components/openEdition/ImageUploadDetails.tsx | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/components/openEdition/ImageUploadDetails.tsx b/components/openEdition/ImageUploadDetails.tsx index f8748f2..f37f709 100644 --- a/components/openEdition/ImageUploadDetails.tsx +++ b/components/openEdition/ImageUploadDetails.tsx @@ -14,6 +14,7 @@ import type { ChangeEvent } from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'react-hot-toast' import type { UploadServiceType } from 'services/upload' +import type { AssetType } from 'utils/getAssetType' import { getAssetType } from 'utils/getAssetType' export type UploadMethod = 'new' | 'existing' @@ -25,6 +26,8 @@ interface ImageUploadDetailsProps { export interface ImageUploadDetailsDataProps { assetFile: File | undefined + thumbnailFile?: File | undefined + isThumbnailCompatible?: boolean uploadService: UploadServiceType nftStorageApiKey?: string pinataApiKey?: string @@ -36,10 +39,13 @@ export interface ImageUploadDetailsDataProps { export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: ImageUploadDetailsProps) => { const [assetFile, setAssetFile] = useState() + const [thumbnailFile, setThumbnailFile] = useState() + const [isThumbnailCompatible, setIsThumbnailCompatible] = useState(false) const [uploadMethod, setUploadMethod] = useState('new') const [uploadService, setUploadService] = useState('nft-storage') const assetFileRef = useRef(null) + const thumbnailFileRef = useRef(null) const nftStorageApiKeyState = useInputState({ id: 'nft-storage-api-key', @@ -79,8 +85,12 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima defaultValue: '', }) + const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html'] + const selectAsset = (event: ChangeEvent) => { setAssetFile(undefined) + setThumbnailFile(undefined) + setIsThumbnailCompatible(false) if (event.target.files === null) return let selectedFile: File @@ -95,10 +105,33 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima else return toast.error('No file selected.') reader.onloadend = () => { if (!event.target.files) return toast.error('No file selected.') + if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) { + setIsThumbnailCompatible(true) + } setAssetFile(selectedFile) } } + const selectThumbnail = (event: ChangeEvent) => { + setThumbnailFile(undefined) + if (event.target.files === null) return + + let selectedFile: File + const reader = new FileReader() + reader.onload = (e) => { + if (!event.target.files) return toast.error('No file selected.') + if (!e.target?.result) return toast.error('Error parsing file.') + selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'image/*' }) + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0]) + else return toast.error('No file selected.') + reader.onloadend = () => { + if (!event.target.files) return toast.error('No file selected.') + setThumbnailFile(selectedFile) + } + } + const regex = /[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g @@ -106,6 +139,8 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima try { const data: ImageUploadDetailsDataProps = { assetFile, + thumbnailFile, + isThumbnailCompatible, uploadService, nftStorageApiKey: nftStorageApiKeyState.value, pinataApiKey: pinataApiKeyState.value, @@ -126,6 +161,8 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima } }, [ assetFile, + thumbnailFile, + isThumbnailCompatible, uploadService, nftStorageApiKeyState.value, pinataApiKeyState.value, @@ -138,6 +175,8 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima useEffect(() => { if (assetFileRef.current) assetFileRef.current.value = '' setAssetFile(undefined) + setThumbnailFile(undefined) + setIsThumbnailCompatible(false) imageUrlState.onChange('') }, [uploadMethod]) @@ -344,6 +383,34 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima /> + +
+ +
+ +
+
+
From 75a2d4c08977360ebac092d42cba8b014639f6a4 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 10 Sep 2023 12:46:52 +0300 Subject: [PATCH 05/10] Update collection creation logic for OE/on-chain metadata --- .../collections/creation/UploadDetails.tsx | 1 + .../openEdition/OpenEditionMinterCreator.tsx | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/components/collections/creation/UploadDetails.tsx b/components/collections/creation/UploadDetails.tsx index a639031..8d78746 100644 --- a/components/collections/creation/UploadDetails.tsx +++ b/components/collections/creation/UploadDetails.tsx @@ -134,6 +134,7 @@ export const UploadDetails = ({ for (let i = 0; i < sortedFileNames.length; i++) { if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) { toast.error('The file names should be in numerical order starting from 1.') + setThumbnailCompatibleAssetFileNames([]) addLogItem({ id: uid(), message: 'The file names should be in numerical order starting from 1.', diff --git a/components/openEdition/OpenEditionMinterCreator.tsx b/components/openEdition/OpenEditionMinterCreator.tsx index 9f2e620..2375c07 100644 --- a/components/openEdition/OpenEditionMinterCreator.tsx +++ b/components/openEdition/OpenEditionMinterCreator.tsx @@ -28,6 +28,7 @@ import { SG721_OPEN_EDITION_CODE_ID, SG721_OPEN_EDITION_UPDATABLE_CODE_ID, } from 'utils/constants' +import type { AssetType } from 'utils/getAssetType' import { getAssetType } from 'utils/getAssetType' import { isValidAddress } from 'utils/isValidAddress' import { checkTokenUri } from 'utils/isValidTokenUri' @@ -116,6 +117,9 @@ export const OpenEditionMinterCreator = ({ const [openEditionMinterContractAddress, setOpenEditionMinterContractAddress] = useState(null) const [sg721ContractAddress, setSg721ContractAddress] = useState(null) const [transactionHash, setTransactionHash] = useState(null) + const [thumbnailImageUri, setThumbnailImageUri] = useState(undefined) + + const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html'] const factoryAddressForSelectedDenom = openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === false) @@ -436,8 +440,23 @@ export const OpenEditionMinterCreator = ({ const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}` setCoverImageUrl(coverImageUriWithBase) + let thumbnailUri: string | undefined + if (imageUploadDetails.isThumbnailCompatible && imageUploadDetails.thumbnailFile) + thumbnailUri = await upload( + [imageUploadDetails.thumbnailFile] as File[], + imageUploadDetails.uploadService, + 'thumbnail', + imageUploadDetails.nftStorageApiKey as string, + imageUploadDetails.pinataApiKey as string, + imageUploadDetails.pinataSecretKey as string, + ) + const thumbnailUriWithBase = thumbnailUri + ? `ipfs://${thumbnailUri}/${(imageUploadDetails.thumbnailFile as File).name}` + : undefined + setThumbnailImageUri(thumbnailUriWithBase) + setUploading(false) - await instantiateOpenEditionMinter(imageUriWithBase, coverImageUriWithBase) + await instantiateOpenEditionMinter(imageUriWithBase, coverImageUriWithBase, thumbnailUriWithBase) } else if (imageUploadDetails?.uploadMethod === 'existing') { setTokenImageUri(imageUploadDetails.imageUrl as string) setCoverImageUrl(imageUploadDetails.coverImageUrl as string) @@ -527,7 +546,7 @@ export const OpenEditionMinterCreator = ({ }) } - const instantiateOpenEditionMinter = async (uri: string, coverImageUri: string) => { + const instantiateOpenEditionMinter = async (uri: string, coverImageUri: string, thumbnailUri?: string) => { if (!wallet.initialized) throw new Error('Wallet not connected') if (!openEditionFactoryContract) throw new Error('Contract not found') if (!openEditionMinterContract) throw new Error('Contract not found') @@ -549,7 +568,10 @@ export const OpenEditionMinterCreator = ({ extension: metadataStorageMethod === 'on-chain' ? { - image: uri, + image: + imageUploadDetails?.isThumbnailCompatible && imageUploadDetails.thumbnailFile + ? thumbnailUri + : uri, name: onChainMetadataInputDetails?.name, description: onChainMetadataInputDetails?.description?.replaceAll('\\n', '\n'), attributes: onChainMetadataInputDetails?.attributes, @@ -557,7 +579,7 @@ export const OpenEditionMinterCreator = ({ animation_url: imageUploadDetails?.uploadMethod === 'existing' ? onChainMetadataInputDetails?.animation_url - : getAssetType(imageUploadDetails?.assetFile?.name as string) === 'video' + : imageUploadDetails?.isThumbnailCompatible ? uri : undefined, youtube_url: onChainMetadataInputDetails?.youtube_url, @@ -677,7 +699,7 @@ export const OpenEditionMinterCreator = ({ return (
{/* TODO: Cancel once we're able to index on-chain metadata */} - +
From c4f486f1f0d489d8a5f57e55335a954300e422cb Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 10 Sep 2023 13:42:37 +0300 Subject: [PATCH 06/10] Update collection creation logic for OE/off-chain metadata --- .../OffChainMetadataUploadDetails.tsx | 69 ++++++++++++++++++- .../openEdition/OpenEditionMinterCreator.tsx | 30 +++++--- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/components/openEdition/OffChainMetadataUploadDetails.tsx b/components/openEdition/OffChainMetadataUploadDetails.tsx index e317a8e..df1f81c 100644 --- a/components/openEdition/OffChainMetadataUploadDetails.tsx +++ b/components/openEdition/OffChainMetadataUploadDetails.tsx @@ -18,6 +18,8 @@ import type { ChangeEvent } from 'react' import { useEffect, useRef, useState } from 'react' import { toast } from 'react-hot-toast' import type { UploadServiceType } from 'services/upload' +import type { AssetType } from 'utils/getAssetType' +import { getAssetType } from 'utils/getAssetType' import { uid } from 'utils/random' import { naturalCompare } from 'utils/sort' @@ -34,6 +36,8 @@ interface OffChainMetadataUploadDetailsProps { export interface OffChainMetadataUploadDetailsDataProps { assetFiles: File[] metadataFiles: File[] + thumbnailFile?: File + isThumbnailCompatible?: boolean uploadService: UploadServiceType nftStorageApiKey?: string pinataApiKey?: string @@ -52,16 +56,20 @@ export const OffChainMetadataUploadDetails = ({ }: OffChainMetadataUploadDetailsProps) => { const [assetFilesArray, setAssetFilesArray] = useState([]) const [metadataFilesArray, setMetadataFilesArray] = useState([]) + const [thumbnailFile, setThumbnailFile] = useState() + const [isThumbnailCompatible, setIsThumbnailCompatible] = useState(false) const [uploadMethod, setUploadMethod] = useState('new') const [uploadService, setUploadService] = useState('nft-storage') const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0) const [refreshMetadata, setRefreshMetadata] = useState(false) const [exportedMetadata, setExportedMetadata] = useState(undefined) - const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState() + const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html'] + const assetFilesRef = useRef(null) const metadataFilesRef = useRef(null) + const thumbnailFilesRef = useRef(null) const nftStorageApiKeyState = useInputState({ id: 'nft-storage-api-key', @@ -104,7 +112,12 @@ export const OffChainMetadataUploadDetails = ({ const selectAssets = (event: ChangeEvent) => { setAssetFilesArray([]) setMetadataFilesArray([]) + setThumbnailFile(undefined) + setIsThumbnailCompatible(false) if (event.target.files === null) return + if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) { + setIsThumbnailCompatible(true) + } let loadedFileCount = 0 const files: File[] = [] let reader: FileReader @@ -166,6 +179,26 @@ export const OffChainMetadataUploadDetails = ({ } } + const selectThumbnail = (event: ChangeEvent) => { + setThumbnailFile(undefined) + if (event.target.files === null) return + + let selectedFile: File + const reader = new FileReader() + reader.onload = (e) => { + if (!event.target.files) return toast.error('No file selected.') + if (!e.target?.result) return toast.error('Error parsing file.') + selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'image/*' }) + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0]) + else return toast.error('No file selected.') + reader.onloadend = () => { + if (!event.target.files) return toast.error('No file selected.') + setThumbnailFile(selectedFile) + } + } + const updateMetadataFileIndex = (index: number) => { setMetadataFileArrayIndex(index) setRefreshMetadata((prev) => !prev) @@ -188,6 +221,8 @@ export const OffChainMetadataUploadDetails = ({ const data: OffChainMetadataUploadDetailsDataProps = { assetFiles: assetFilesArray, metadataFiles: metadataFilesArray, + thumbnailFile, + isThumbnailCompatible, uploadService, nftStorageApiKey: nftStorageApiKeyState.value, pinataApiKey: pinataApiKeyState.value, @@ -218,6 +253,8 @@ export const OffChainMetadataUploadDetails = ({ }, [ assetFilesArray, metadataFilesArray, + thumbnailFile, + isThumbnailCompatible, uploadService, nftStorageApiKeyState.value, pinataApiKeyState.value, @@ -235,6 +272,8 @@ export const OffChainMetadataUploadDetails = ({ setMetadataFilesArray([]) if (assetFilesRef.current) assetFilesRef.current.value = '' setAssetFilesArray([]) + setThumbnailFile(undefined) + setIsThumbnailCompatible(false) if (!importedOffChainMetadataUploadDetails) { tokenUriState.onChange('') coverImageUrlState.onChange('') @@ -423,6 +462,34 @@ export const OffChainMetadataUploadDetails = ({ />
+ +
+ +
+ +
+
+
{assetFilesArray.length > 0 && (
diff --git a/components/openEdition/OpenEditionMinterCreator.tsx b/components/openEdition/OpenEditionMinterCreator.tsx index 2375c07..a930af8 100644 --- a/components/openEdition/OpenEditionMinterCreator.tsx +++ b/components/openEdition/OpenEditionMinterCreator.tsx @@ -29,7 +29,6 @@ import { SG721_OPEN_EDITION_UPDATABLE_CODE_ID, } from 'utils/constants' import type { AssetType } from 'utils/getAssetType' -import { getAssetType } from 'utils/getAssetType' import { isValidAddress } from 'utils/isValidAddress' import { checkTokenUri } from 'utils/isValidTokenUri' import { uid } from 'utils/random' @@ -488,22 +487,35 @@ export const OpenEditionMinterCreator = ({ offChainMetadataUploadDetails.pinataApiKey as string, offChainMetadataUploadDetails.pinataSecretKey as string, ) - .then((assetUri: string) => { + .then(async (assetUri: string) => { + let thumbnailUri: string | undefined + if (offChainMetadataUploadDetails.isThumbnailCompatible && offChainMetadataUploadDetails.thumbnailFile) + thumbnailUri = await upload( + [offChainMetadataUploadDetails.thumbnailFile] as File[], + offChainMetadataUploadDetails.uploadService, + 'thumbnail', + offChainMetadataUploadDetails.nftStorageApiKey as string, + offChainMetadataUploadDetails.pinataApiKey as string, + offChainMetadataUploadDetails.pinataSecretKey as string, + ) + const thumbnailUriWithBase = thumbnailUri + ? `ipfs://${thumbnailUri}/${(offChainMetadataUploadDetails.thumbnailFile as File).name}` + : undefined + const fileArray: File[] = [] const reader: FileReader = new FileReader() reader.onload = (e) => { const data: any = JSON.parse(e.target?.result as string) - if ( - getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'audio' || - getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'video' || - getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'html' - ) { + if (offChainMetadataUploadDetails.isThumbnailCompatible) { data.animation_url = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}` } - if (getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) !== 'html') - data.image = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}` + + data.image = + offChainMetadataUploadDetails.isThumbnailCompatible && offChainMetadataUploadDetails.thumbnailFile + ? thumbnailUriWithBase + : `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}` if (data.description) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call From b65fd5d3c95d0d61cabc743dd5b933181fd2e876 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 10 Sep 2023 14:55:17 +0300 Subject: [PATCH 07/10] Bump Studio version --- .env.example | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index f2c1812..032ed53 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.7.5 +APP_VERSION=0.7.6 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=2595 diff --git a/package.json b/package.json index 18f9378..d331d18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.7.5", + "version": "0.7.6", "workspaces": [ "packages/*" ], From c455eafb7bd5acdef202b9a7d555822bd537c49d Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 13 Sep 2023 10:38:26 +0300 Subject: [PATCH 08/10] Add multi-address batch transfers to collection actions --- components/collections/actions/Action.tsx | 9 +++- components/collections/actions/actions.ts | 22 +++++++++ contracts/sg721/contract.ts | 56 +++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx index 85738bc..ef0bc99 100644 --- a/components/collections/actions/Action.tsx +++ b/components/collections/actions/Action.tsx @@ -198,7 +198,12 @@ export const CollectionActions = ({ 'batch_transfer', 'batch_mint_for', ]) - const showAirdropFileField = isEitherType(type, ['airdrop', 'airdrop_open_edition', 'airdrop_specific']) + const showAirdropFileField = isEitherType(type, [ + 'airdrop', + 'airdrop_open_edition', + 'airdrop_specific', + 'batch_transfer_multi_address', + ]) const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price']) const showDescriptionField = type === 'update_collection_info' const showImageField = type === 'update_collection_info' @@ -499,7 +504,7 @@ export const CollectionActions = ({ } allocated for each address. Should start with the following header row: ${ type === 'airdrop' ? 'address,amount' : 'address,tokenId' }`} - title="Airdrop File" + title={`${type === 'batch_transfer_multi_address' ? 'Multi-Recipient Transfer File' : 'Airdrop File'}`} > diff --git a/components/collections/actions/actions.ts b/components/collections/actions/actions.ts index 2d7c850..d3e69d6 100644 --- a/components/collections/actions/actions.ts +++ b/components/collections/actions/actions.ts @@ -31,6 +31,7 @@ export const ACTION_TYPES = [ 'freeze_collection_info', 'transfer', 'batch_transfer', + 'batch_transfer_multi_address', 'burn', 'batch_burn', 'batch_mint_for', @@ -82,6 +83,11 @@ export const BASE_ACTION_LIST: ActionListItem[] = [ name: 'Batch Transfer Tokens', description: `Transfer a list of tokens to a recipient`, }, + { + id: 'batch_transfer_multi_address', + name: 'Transfer Tokens to Multiple Recipients', + description: `Transfer a list of tokens to multiple addresses`, + }, { id: 'burn', name: 'Burn Token', @@ -170,6 +176,11 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [ name: 'Batch Transfer Tokens', description: `Transfer a list of tokens to a recipient`, }, + { + id: 'batch_transfer_multi_address', + name: 'Transfer Tokens to Multiple Recipients', + description: `Transfer a list of tokens to multiple addresses`, + }, { id: 'burn', name: 'Burn Token', @@ -258,6 +269,11 @@ export const OPEN_EDITION_ACTION_LIST: ActionListItem[] = [ name: 'Batch Transfer Tokens', description: `Transfer a list of tokens to a recipient`, }, + { + id: 'batch_transfer_multi_address', + name: 'Transfer Tokens to Multiple Recipients', + description: `Transfer a list of tokens to multiple addresses`, + }, { id: 'burn', name: 'Burn Token', @@ -405,6 +421,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { case 'batch_transfer': { return sg721Messages.batchTransfer(args.recipient, args.tokenIds) } + case 'batch_transfer_multi_address': { + return sg721Messages.batchTransferMultiAddress(txSigner, args.tokenRecipients) + } case 'burn': { return sg721Messages.burn(args.tokenId.toString()) } @@ -512,6 +531,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { case 'batch_transfer': { return sg721Messages(sg721Contract)?.batchTransfer(args.recipient, args.tokenIds) } + case 'batch_transfer_multi_address': { + return sg721Messages(sg721Contract)?.batchTransferMultiAddress(args.tokenRecipients) + } case 'burn': { return sg721Messages(sg721Contract)?.burn(args.tokenId.toString()) } diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index 6963d13..0027df4 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -3,6 +3,7 @@ import { toBase64, toUtf8 } from '@cosmjs/encoding' import type { Coin, logs } from '@cosmjs/stargate' import { coin } from '@cosmjs/stargate' import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' +import type { AirdropAllocation } from 'utils/isValidAccountsFile' import type { RoyaltyInfo } from '../vendingMinter/contract' @@ -86,6 +87,7 @@ export interface SG721Instance { burn: (tokenId: string) => Promise batchBurn: (tokenIds: string) => Promise batchTransfer: (recipient: string, tokenIds: string) => Promise + batchTransferMultiAddress: (senderAddress: string, tokenRecipients: AirdropAllocation[]) => Promise updateTokenMetadata: (tokenId: string, tokenURI: string) => Promise batchUpdateTokenMetadata: (tokenIds: string, tokenURI: string, jsonExtensions: boolean) => Promise freezeTokenMetadata: () => Promise @@ -103,6 +105,7 @@ export interface Sg721Messages { burn: (tokenId: string) => BurnMessage batchBurn: (tokenIds: string) => BatchBurnMessage batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage + batchTransferMultiAddress: (tokenRecipients: AirdropAllocation[]) => BatchTransferMultiAddressMessage updateCollectionInfo: (collectionInfo: CollectionInfo) => UpdateCollectionInfoMessage freezeCollectionInfo: () => FreezeCollectionInfoMessage updateTokenMetadata: (tokenId: string, tokenURI: string) => UpdateTokenMetadataMessage @@ -227,6 +230,13 @@ export interface BatchTransferMessage { funds: Coin[] } +export interface BatchTransferMultiAddressMessage { + sender: string + contract: string + msg: Record[] + funds: Coin[] +} + export interface UpdateTokenMetadataMessage { sender: string contract: string @@ -601,6 +611,37 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con return res.transactionHash } + const batchTransferMultiAddress = async ( + senderAddress: string, + recipients: AirdropAllocation[], + ): Promise => { + const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] + for (let i = 0; i < recipients.length; i++) { + const msg = { + transfer_nft: { recipient: recipients[i].address, token_id: recipients[i].tokenId as string }, + } + const executeContractMsg: MsgExecuteContractEncodeObject = { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: MsgExecuteContract.fromPartial({ + sender: senderAddress, + contract: contractAddress, + msg: toUtf8(JSON.stringify(msg)), + }), + } + + executeContractMsgs.push(executeContractMsg) + } + + const res = await client.signAndBroadcast( + senderAddress, + executeContractMsgs, + 'auto', + 'batch transfer to multiple recipients', + ) + + return res.transactionHash + } + // eslint-disable-next-line @typescript-eslint/no-shadow const updateCollectionInfo = async (collectionInfo: CollectionInfo): Promise => { const res = await client.execute( @@ -748,6 +789,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con burn, batchBurn, batchTransfer, + batchTransferMultiAddress, updateCollectionInfo, freezeCollectionInfo, updateTokenMetadata, @@ -950,6 +992,19 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con } } + const batchTransferMultiAddress = (recipients: AirdropAllocation[]): BatchTransferMultiAddressMessage => { + const msg: Record[] = [] + for (let i = 0; i < recipients.length; i++) { + msg.push({ transfer_nft: { recipient: recipients[i].address, token_id: recipients[i].tokenId } }) + } + return { + sender: txSigner, + contract: contractAddress, + msg, + funds: [], + } + } + const batchUpdateTokenMetadata = ( tokenIds: string, baseURI: string, @@ -1055,6 +1110,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con burn, batchBurn, batchTransfer, + batchTransferMultiAddress, updateCollectionInfo, freezeCollectionInfo, updateTokenMetadata, From dd33b6129cb90e088e62ad3026a4cfc65c3c1ded Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 13 Sep 2023 10:39:07 +0300 Subject: [PATCH 09/10] Bump Studio version --- .env.example | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 032ed53..54d70c4 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.7.6 +APP_VERSION=0.7.9 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=2595 diff --git a/package.json b/package.json index 622b929..85365c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.7.8", + "version": "0.7.9", "workspaces": [ "packages/*" ], From 88177fd4465a6fcd4aefc30c6f4f0e33385c03d8 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 13 Sep 2023 10:56:12 +0300 Subject: [PATCH 10/10] Update file selection CTO copy --- components/collections/actions/Action.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx index ef0bc99..a8b1b2e 100644 --- a/components/collections/actions/Action.tsx +++ b/components/collections/actions/Action.tsx @@ -499,7 +499,9 @@ export const CollectionActions = ({ )} {showAirdropFileField && (