Merge pull request #216 from public-awesome/develop

Sync dev > main
This commit is contained in:
Serkan Reis 2023-09-21 08:14:16 +03:00 committed by GitHub
commit 572968cf24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 424 additions and 27 deletions

View File

@ -1,4 +1,4 @@
APP_VERSION=0.7.5 APP_VERSION=0.7.9
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_SG721_CODE_ID=2595 NEXT_PUBLIC_SG721_CODE_ID=2595

View File

@ -198,7 +198,12 @@ export const CollectionActions = ({
'batch_transfer', 'batch_transfer',
'batch_mint_for', '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 showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
const showDescriptionField = type === 'update_collection_info' const showDescriptionField = type === 'update_collection_info'
const showImageField = type === 'update_collection_info' const showImageField = type === 'update_collection_info'
@ -494,12 +499,14 @@ export const CollectionActions = ({
)} )}
{showAirdropFileField && ( {showAirdropFileField && (
<FormGroup <FormGroup
subtitle={`CSV file that contains the airdrop addresses and the ${ subtitle={`CSV file that contains the ${
type === 'batch_transfer_multi_address' ? '' : 'airdrop'
} addresses and the ${
type === 'airdrop' ? 'amount of tokens' : 'token ID' type === 'airdrop' ? 'amount of tokens' : 'token ID'
} allocated for each address. Should start with the following header row: ${ } allocated for each address. Should start with the following header row: ${
type === 'airdrop' ? 'address,amount' : 'address,tokenId' type === 'airdrop' ? 'address,amount' : 'address,tokenId'
}`} }`}
title="Airdrop File" title={`${type === 'batch_transfer_multi_address' ? 'Multi-Recipient Transfer File' : 'Airdrop File'}`}
> >
<AirdropUpload onChange={airdropFileOnChange} /> <AirdropUpload onChange={airdropFileOnChange} />
</FormGroup> </FormGroup>

View File

@ -31,6 +31,7 @@ export const ACTION_TYPES = [
'freeze_collection_info', 'freeze_collection_info',
'transfer', 'transfer',
'batch_transfer', 'batch_transfer',
'batch_transfer_multi_address',
'burn', 'burn',
'batch_burn', 'batch_burn',
'batch_mint_for', 'batch_mint_for',
@ -82,6 +83,11 @@ export const BASE_ACTION_LIST: ActionListItem[] = [
name: 'Batch Transfer Tokens', name: 'Batch Transfer Tokens',
description: `Transfer a list of tokens to a recipient`, 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', id: 'burn',
name: 'Burn Token', name: 'Burn Token',
@ -170,6 +176,11 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
name: 'Batch Transfer Tokens', name: 'Batch Transfer Tokens',
description: `Transfer a list of tokens to a recipient`, 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', id: 'burn',
name: 'Burn Token', name: 'Burn Token',
@ -258,6 +269,11 @@ export const OPEN_EDITION_ACTION_LIST: ActionListItem[] = [
name: 'Batch Transfer Tokens', name: 'Batch Transfer Tokens',
description: `Transfer a list of tokens to a recipient`, 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', id: 'burn',
name: 'Burn Token', name: 'Burn Token',
@ -405,6 +421,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'batch_transfer': { case 'batch_transfer': {
return sg721Messages.batchTransfer(args.recipient, args.tokenIds) return sg721Messages.batchTransfer(args.recipient, args.tokenIds)
} }
case 'batch_transfer_multi_address': {
return sg721Messages.batchTransferMultiAddress(txSigner, args.tokenRecipients)
}
case 'burn': { case 'burn': {
return sg721Messages.burn(args.tokenId.toString()) return sg721Messages.burn(args.tokenId.toString())
} }
@ -512,6 +531,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'batch_transfer': { case 'batch_transfer': {
return sg721Messages(sg721Contract)?.batchTransfer(args.recipient, args.tokenIds) return sg721Messages(sg721Contract)?.batchTransfer(args.recipient, args.tokenIds)
} }
case 'batch_transfer_multi_address': {
return sg721Messages(sg721Contract)?.batchTransferMultiAddress(args.tokenRecipients)
}
case 'burn': { case 'burn': {
return sg721Messages(sg721Contract)?.burn(args.tokenId.toString()) return sg721Messages(sg721Contract)?.burn(args.tokenId.toString())
} }

View File

@ -1,4 +1,5 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable array-callback-return */
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
/* eslint-disable no-misleading-character-class */ /* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */ /* eslint-disable no-control-regex */
@ -19,6 +20,8 @@ import type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload' import type { UploadServiceType } from 'services/upload'
import type { AssetType } from 'utils/getAssetType'
import { getAssetType } from 'utils/getAssetType'
import { uid } from 'utils/random' import { uid } from 'utils/random'
import { naturalCompare } from 'utils/sort' import { naturalCompare } from 'utils/sort'
@ -37,6 +40,8 @@ interface UploadDetailsProps {
export interface UploadDetailsDataProps { export interface UploadDetailsDataProps {
assetFiles: File[] assetFiles: File[]
metadataFiles: File[] metadataFiles: File[]
thumbnailFiles?: File[]
thumbnailCompatibleAssetFileNames?: string[]
uploadService: UploadServiceType uploadService: UploadServiceType
nftStorageApiKey?: string nftStorageApiKey?: string
pinataApiKey?: string pinataApiKey?: string
@ -55,6 +60,8 @@ export const UploadDetails = ({
}: UploadDetailsProps) => { }: UploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([]) const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([]) const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [thumbnailCompatibleAssetFileNames, setThumbnailCompatibleAssetFileNames] = useState<string[]>([])
const [thumbnailFilesArray, setThumbnailFilesArray] = useState<File[]>([])
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new') const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage') const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0) const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
@ -64,6 +71,7 @@ export const UploadDetails = ({
const assetFilesRef = useRef<HTMLInputElement | null>(null) const assetFilesRef = useRef<HTMLInputElement | null>(null)
const metadataFilesRef = useRef<HTMLInputElement | null>(null) const metadataFilesRef = useRef<HTMLInputElement | null>(null)
const thumbnailFilesRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({ const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key', id: 'nft-storage-api-key',
@ -106,15 +114,27 @@ export const UploadDetails = ({
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => { const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFilesArray([]) setAssetFilesArray([])
setMetadataFilesArray([]) setMetadataFilesArray([])
setThumbnailFilesArray([])
setThumbnailCompatibleAssetFileNames([])
if (event.target.files === null) return 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)) { if (minterType === 'vending' || (minterType === 'base' && event.target.files.length > 1)) {
//sort the files //sort the files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) 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 //check if the sorted file names are in numerical order
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
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++) { for (let i = 0; i < sortedFileNames.length; i++) {
if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) { if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) {
toast.error('The file names should be in numerical order starting from 1.') toast.error('The file names should be in numerical order starting from 1.')
setThumbnailCompatibleAssetFileNames([])
addLogItem({ addLogItem({
id: uid(), id: uid(),
message: 'The file names should be in numerical order starting from 1.', message: 'The file names should be in numerical order starting from 1.',
@ -126,7 +146,14 @@ export const UploadDetails = ({
return 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 let loadedFileCount = 0
const files: File[] = [] const files: File[] = []
let reader: FileReader let reader: FileReader
@ -230,11 +257,59 @@ export const UploadDetails = ({
const regex = 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 /[\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<HTMLInputElement>) => {
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 < 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({
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(() => { useEffect(() => {
try { try {
const data: UploadDetailsDataProps = { const data: UploadDetailsDataProps = {
assetFiles: assetFilesArray, assetFiles: assetFilesArray,
metadataFiles: metadataFilesArray, metadataFiles: metadataFilesArray,
thumbnailFiles: thumbnailFilesArray,
thumbnailCompatibleAssetFileNames,
uploadService, uploadService,
nftStorageApiKey: nftStorageApiKeyState.value, nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value, pinataApiKey: pinataApiKeyState.value,
@ -264,6 +339,8 @@ export const UploadDetails = ({
}, [ }, [
assetFilesArray, assetFilesArray,
metadataFilesArray, metadataFilesArray,
thumbnailFilesArray,
thumbnailCompatibleAssetFileNames,
uploadService, uploadService,
nftStorageApiKeyState.value, nftStorageApiKeyState.value,
pinataApiKeyState.value, pinataApiKeyState.value,
@ -280,6 +357,9 @@ export const UploadDetails = ({
setMetadataFilesArray([]) setMetadataFilesArray([])
if (assetFilesRef.current) assetFilesRef.current.value = '' if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([]) setAssetFilesArray([])
if (thumbnailFilesRef.current) thumbnailFilesRef.current.value = ''
setThumbnailFilesArray([])
setThumbnailCompatibleAssetFileNames([])
if (!importedUploadDetails || minterType === 'base') { if (!importedUploadDetails || minterType === 'base') {
baseTokenUriState.onChange('') baseTokenUriState.onChange('')
coverImageUrlState.onChange('') coverImageUrlState.onChange('')
@ -546,6 +626,38 @@ export const UploadDetails = ({
</div> </div>
</div> </div>
)} )}
{thumbnailCompatibleAssetFileNames.length > 0 && (
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="thumbnailFiles"
>
{thumbnailCompatibleAssetFileNames.length > 1
? 'Thumbnail Selection for Compatible Assets (optional)'
: 'Thumbnail Selection (optional)'}
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="thumbnailFiles"
multiple
onChange={selectThumbnails}
ref={thumbnailFilesRef}
type="file"
/>
</div>
</div>
)}
<Conditional test={assetFilesArray.length >= 1}> <Conditional test={assetFilesArray.length >= 1}>
<MetadataModal <MetadataModal
assetFile={assetFilesArray[metadataFileArrayIndex]} assetFile={assetFilesArray[metadataFileArrayIndex]}

View File

@ -14,6 +14,7 @@ import type { ChangeEvent } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload' import type { UploadServiceType } from 'services/upload'
import type { AssetType } from 'utils/getAssetType'
import { getAssetType } from 'utils/getAssetType' import { getAssetType } from 'utils/getAssetType'
export type UploadMethod = 'new' | 'existing' export type UploadMethod = 'new' | 'existing'
@ -25,6 +26,8 @@ interface ImageUploadDetailsProps {
export interface ImageUploadDetailsDataProps { export interface ImageUploadDetailsDataProps {
assetFile: File | undefined assetFile: File | undefined
thumbnailFile?: File | undefined
isThumbnailCompatible?: boolean
uploadService: UploadServiceType uploadService: UploadServiceType
nftStorageApiKey?: string nftStorageApiKey?: string
pinataApiKey?: string pinataApiKey?: string
@ -36,10 +39,13 @@ export interface ImageUploadDetailsDataProps {
export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: ImageUploadDetailsProps) => { export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: ImageUploadDetailsProps) => {
const [assetFile, setAssetFile] = useState<File>() const [assetFile, setAssetFile] = useState<File>()
const [thumbnailFile, setThumbnailFile] = useState<File>()
const [isThumbnailCompatible, setIsThumbnailCompatible] = useState<boolean>(false)
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new') const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage') const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const assetFileRef = useRef<HTMLInputElement | null>(null) const assetFileRef = useRef<HTMLInputElement | null>(null)
const thumbnailFileRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({ const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key', id: 'nft-storage-api-key',
@ -79,8 +85,12 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima
defaultValue: '', defaultValue: '',
}) })
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html']
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => { const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFile(undefined) setAssetFile(undefined)
setThumbnailFile(undefined)
setIsThumbnailCompatible(false)
if (event.target.files === null) return if (event.target.files === null) return
let selectedFile: File let selectedFile: File
@ -95,10 +105,33 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima
else return toast.error('No file selected.') else return toast.error('No file selected.')
reader.onloadend = () => { reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.') if (!event.target.files) return toast.error('No file selected.')
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
setIsThumbnailCompatible(true)
}
setAssetFile(selectedFile) setAssetFile(selectedFile)
} }
} }
const selectThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
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 = 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 /[\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 { try {
const data: ImageUploadDetailsDataProps = { const data: ImageUploadDetailsDataProps = {
assetFile, assetFile,
thumbnailFile,
isThumbnailCompatible,
uploadService, uploadService,
nftStorageApiKey: nftStorageApiKeyState.value, nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value, pinataApiKey: pinataApiKeyState.value,
@ -126,6 +161,8 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima
} }
}, [ }, [
assetFile, assetFile,
thumbnailFile,
isThumbnailCompatible,
uploadService, uploadService,
nftStorageApiKeyState.value, nftStorageApiKeyState.value,
pinataApiKeyState.value, pinataApiKeyState.value,
@ -138,6 +175,8 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima
useEffect(() => { useEffect(() => {
if (assetFileRef.current) assetFileRef.current.value = '' if (assetFileRef.current) assetFileRef.current.value = ''
setAssetFile(undefined) setAssetFile(undefined)
setThumbnailFile(undefined)
setIsThumbnailCompatible(false)
imageUrlState.onChange('') imageUrlState.onChange('')
}, [uploadMethod]) }, [uploadMethod])
@ -344,6 +383,34 @@ export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: Ima
/> />
</div> </div>
</div> </div>
<Conditional test={isThumbnailCompatible}>
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="thumbnailFile"
>
Thumbnail Selection (optional)
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="thumbnailFile"
onChange={selectThumbnail}
ref={thumbnailFileRef}
type="file"
/>
</div>
</div>
</Conditional>
</div> </div>
</div> </div>
<Conditional test={assetFile !== undefined}> <Conditional test={assetFile !== undefined}>

View File

@ -18,6 +18,8 @@ import type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload' import type { UploadServiceType } from 'services/upload'
import type { AssetType } from 'utils/getAssetType'
import { getAssetType } from 'utils/getAssetType'
import { uid } from 'utils/random' import { uid } from 'utils/random'
import { naturalCompare } from 'utils/sort' import { naturalCompare } from 'utils/sort'
@ -34,6 +36,8 @@ interface OffChainMetadataUploadDetailsProps {
export interface OffChainMetadataUploadDetailsDataProps { export interface OffChainMetadataUploadDetailsDataProps {
assetFiles: File[] assetFiles: File[]
metadataFiles: File[] metadataFiles: File[]
thumbnailFile?: File
isThumbnailCompatible?: boolean
uploadService: UploadServiceType uploadService: UploadServiceType
nftStorageApiKey?: string nftStorageApiKey?: string
pinataApiKey?: string pinataApiKey?: string
@ -52,16 +56,20 @@ export const OffChainMetadataUploadDetails = ({
}: OffChainMetadataUploadDetailsProps) => { }: OffChainMetadataUploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([]) const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([]) const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [thumbnailFile, setThumbnailFile] = useState<File>()
const [isThumbnailCompatible, setIsThumbnailCompatible] = useState<boolean>(false)
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new') const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage') const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0) const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
const [refreshMetadata, setRefreshMetadata] = useState(false) const [refreshMetadata, setRefreshMetadata] = useState(false)
const [exportedMetadata, setExportedMetadata] = useState(undefined) const [exportedMetadata, setExportedMetadata] = useState(undefined)
const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>() const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>()
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html']
const assetFilesRef = useRef<HTMLInputElement | null>(null) const assetFilesRef = useRef<HTMLInputElement | null>(null)
const metadataFilesRef = useRef<HTMLInputElement | null>(null) const metadataFilesRef = useRef<HTMLInputElement | null>(null)
const thumbnailFilesRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({ const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key', id: 'nft-storage-api-key',
@ -104,7 +112,12 @@ export const OffChainMetadataUploadDetails = ({
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => { const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFilesArray([]) setAssetFilesArray([])
setMetadataFilesArray([]) setMetadataFilesArray([])
setThumbnailFile(undefined)
setIsThumbnailCompatible(false)
if (event.target.files === null) return if (event.target.files === null) return
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
setIsThumbnailCompatible(true)
}
let loadedFileCount = 0 let loadedFileCount = 0
const files: File[] = [] const files: File[] = []
let reader: FileReader let reader: FileReader
@ -166,6 +179,26 @@ export const OffChainMetadataUploadDetails = ({
} }
} }
const selectThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
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) => { const updateMetadataFileIndex = (index: number) => {
setMetadataFileArrayIndex(index) setMetadataFileArrayIndex(index)
setRefreshMetadata((prev) => !prev) setRefreshMetadata((prev) => !prev)
@ -188,6 +221,8 @@ export const OffChainMetadataUploadDetails = ({
const data: OffChainMetadataUploadDetailsDataProps = { const data: OffChainMetadataUploadDetailsDataProps = {
assetFiles: assetFilesArray, assetFiles: assetFilesArray,
metadataFiles: metadataFilesArray, metadataFiles: metadataFilesArray,
thumbnailFile,
isThumbnailCompatible,
uploadService, uploadService,
nftStorageApiKey: nftStorageApiKeyState.value, nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value, pinataApiKey: pinataApiKeyState.value,
@ -218,6 +253,8 @@ export const OffChainMetadataUploadDetails = ({
}, [ }, [
assetFilesArray, assetFilesArray,
metadataFilesArray, metadataFilesArray,
thumbnailFile,
isThumbnailCompatible,
uploadService, uploadService,
nftStorageApiKeyState.value, nftStorageApiKeyState.value,
pinataApiKeyState.value, pinataApiKeyState.value,
@ -235,6 +272,8 @@ export const OffChainMetadataUploadDetails = ({
setMetadataFilesArray([]) setMetadataFilesArray([])
if (assetFilesRef.current) assetFilesRef.current.value = '' if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([]) setAssetFilesArray([])
setThumbnailFile(undefined)
setIsThumbnailCompatible(false)
if (!importedOffChainMetadataUploadDetails) { if (!importedOffChainMetadataUploadDetails) {
tokenUriState.onChange('') tokenUriState.onChange('')
coverImageUrlState.onChange('') coverImageUrlState.onChange('')
@ -423,6 +462,34 @@ export const OffChainMetadataUploadDetails = ({
/> />
</div> </div>
</div> </div>
<Conditional test={isThumbnailCompatible}>
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="thumbnailFiles"
>
Thumbnail Selection (optional)
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="thumbnailFiles"
onChange={selectThumbnail}
ref={thumbnailFilesRef}
type="file"
/>
</div>
</div>
</Conditional>
{assetFilesArray.length > 0 && ( {assetFilesArray.length > 0 && (
<div> <div>

View File

@ -28,7 +28,7 @@ import {
SG721_OPEN_EDITION_CODE_ID, SG721_OPEN_EDITION_CODE_ID,
SG721_OPEN_EDITION_UPDATABLE_CODE_ID, SG721_OPEN_EDITION_UPDATABLE_CODE_ID,
} from 'utils/constants' } from 'utils/constants'
import { getAssetType } from 'utils/getAssetType' import type { AssetType } from 'utils/getAssetType'
import { isValidAddress } from 'utils/isValidAddress' import { isValidAddress } from 'utils/isValidAddress'
import { checkTokenUri } from 'utils/isValidTokenUri' import { checkTokenUri } from 'utils/isValidTokenUri'
import { uid } from 'utils/random' import { uid } from 'utils/random'
@ -116,6 +116,9 @@ export const OpenEditionMinterCreator = ({
const [openEditionMinterContractAddress, setOpenEditionMinterContractAddress] = useState<string | null>(null) const [openEditionMinterContractAddress, setOpenEditionMinterContractAddress] = useState<string | null>(null)
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null) const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
const [transactionHash, setTransactionHash] = useState<string | null>(null) const [transactionHash, setTransactionHash] = useState<string | null>(null)
const [thumbnailImageUri, setThumbnailImageUri] = useState<string | undefined>(undefined)
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html']
const factoryAddressForSelectedDenom = const factoryAddressForSelectedDenom =
openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === false) openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === false)
@ -436,8 +439,23 @@ export const OpenEditionMinterCreator = ({
const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}` const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}`
setCoverImageUrl(coverImageUriWithBase) 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) setUploading(false)
await instantiateOpenEditionMinter(imageUriWithBase, coverImageUriWithBase) await instantiateOpenEditionMinter(imageUriWithBase, coverImageUriWithBase, thumbnailUriWithBase)
} else if (imageUploadDetails?.uploadMethod === 'existing') { } else if (imageUploadDetails?.uploadMethod === 'existing') {
setTokenImageUri(imageUploadDetails.imageUrl as string) setTokenImageUri(imageUploadDetails.imageUrl as string)
setCoverImageUrl(imageUploadDetails.coverImageUrl as string) setCoverImageUrl(imageUploadDetails.coverImageUrl as string)
@ -469,22 +487,35 @@ export const OpenEditionMinterCreator = ({
offChainMetadataUploadDetails.pinataApiKey as string, offChainMetadataUploadDetails.pinataApiKey as string,
offChainMetadataUploadDetails.pinataSecretKey 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 fileArray: File[] = []
const reader: FileReader = new FileReader() const reader: FileReader = new FileReader()
reader.onload = (e) => { reader.onload = (e) => {
const data: any = JSON.parse(e.target?.result as string) const data: any = JSON.parse(e.target?.result as string)
if ( if (offChainMetadataUploadDetails.isThumbnailCompatible) {
getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'audio' ||
getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'video' ||
getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'html'
) {
data.animation_url = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}` 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) { if (data.description) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
@ -527,7 +558,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 (!wallet.initialized) throw new Error('Wallet not connected')
if (!openEditionFactoryContract) throw new Error('Contract not found') if (!openEditionFactoryContract) throw new Error('Contract not found')
if (!openEditionMinterContract) throw new Error('Contract not found') if (!openEditionMinterContract) throw new Error('Contract not found')
@ -549,7 +580,10 @@ export const OpenEditionMinterCreator = ({
extension: extension:
metadataStorageMethod === 'on-chain' metadataStorageMethod === 'on-chain'
? { ? {
image: uri, image:
imageUploadDetails?.isThumbnailCompatible && imageUploadDetails.thumbnailFile
? thumbnailUri
: uri,
name: onChainMetadataInputDetails?.name, name: onChainMetadataInputDetails?.name,
description: onChainMetadataInputDetails?.description?.replaceAll('\\n', '\n'), description: onChainMetadataInputDetails?.description?.replaceAll('\\n', '\n'),
attributes: onChainMetadataInputDetails?.attributes, attributes: onChainMetadataInputDetails?.attributes,
@ -557,7 +591,7 @@ export const OpenEditionMinterCreator = ({
animation_url: animation_url:
imageUploadDetails?.uploadMethod === 'existing' imageUploadDetails?.uploadMethod === 'existing'
? onChainMetadataInputDetails?.animation_url ? onChainMetadataInputDetails?.animation_url
: getAssetType(imageUploadDetails?.assetFile?.name as string) === 'video' : imageUploadDetails?.isThumbnailCompatible
? uri ? uri
: undefined, : undefined,
youtube_url: onChainMetadataInputDetails?.youtube_url, youtube_url: onChainMetadataInputDetails?.youtube_url,
@ -677,7 +711,7 @@ export const OpenEditionMinterCreator = ({
return ( return (
<div> <div>
{/* TODO: Cancel once we're able to index on-chain metadata */} {/* TODO: Cancel once we're able to index on-chain metadata */}
<Conditional test={false}> <Conditional test>
<div className="mx-10 mb-4 rounded border-2 border-white/20"> <div className="mx-10 mb-4 rounded border-2 border-white/20">
<div className="flex justify-center mb-2"> <div className="flex justify-center mb-2">
<div className="mt-3 ml-4 font-bold form-check form-check-inline"> <div className="mt-3 ml-4 font-bold form-check form-check-inline">

View File

@ -3,6 +3,7 @@ import { toBase64, toUtf8 } from '@cosmjs/encoding'
import type { Coin, logs } from '@cosmjs/stargate' import type { Coin, logs } from '@cosmjs/stargate'
import { coin } from '@cosmjs/stargate' import { coin } from '@cosmjs/stargate'
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
import type { RoyaltyInfo } from '../vendingMinter/contract' import type { RoyaltyInfo } from '../vendingMinter/contract'
@ -86,6 +87,7 @@ export interface SG721Instance {
burn: (tokenId: string) => Promise<string> burn: (tokenId: string) => Promise<string>
batchBurn: (tokenIds: string) => Promise<string> batchBurn: (tokenIds: string) => Promise<string>
batchTransfer: (recipient: string, tokenIds: string) => Promise<string> batchTransfer: (recipient: string, tokenIds: string) => Promise<string>
batchTransferMultiAddress: (senderAddress: string, tokenRecipients: AirdropAllocation[]) => Promise<string>
updateTokenMetadata: (tokenId: string, tokenURI: string) => Promise<string> updateTokenMetadata: (tokenId: string, tokenURI: string) => Promise<string>
batchUpdateTokenMetadata: (tokenIds: string, tokenURI: string, jsonExtensions: boolean) => Promise<string> batchUpdateTokenMetadata: (tokenIds: string, tokenURI: string, jsonExtensions: boolean) => Promise<string>
freezeTokenMetadata: () => Promise<string> freezeTokenMetadata: () => Promise<string>
@ -103,6 +105,7 @@ export interface Sg721Messages {
burn: (tokenId: string) => BurnMessage burn: (tokenId: string) => BurnMessage
batchBurn: (tokenIds: string) => BatchBurnMessage batchBurn: (tokenIds: string) => BatchBurnMessage
batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage
batchTransferMultiAddress: (tokenRecipients: AirdropAllocation[]) => BatchTransferMultiAddressMessage
updateCollectionInfo: (collectionInfo: CollectionInfo) => UpdateCollectionInfoMessage updateCollectionInfo: (collectionInfo: CollectionInfo) => UpdateCollectionInfoMessage
freezeCollectionInfo: () => FreezeCollectionInfoMessage freezeCollectionInfo: () => FreezeCollectionInfoMessage
updateTokenMetadata: (tokenId: string, tokenURI: string) => UpdateTokenMetadataMessage updateTokenMetadata: (tokenId: string, tokenURI: string) => UpdateTokenMetadataMessage
@ -227,6 +230,13 @@ export interface BatchTransferMessage {
funds: Coin[] funds: Coin[]
} }
export interface BatchTransferMultiAddressMessage {
sender: string
contract: string
msg: Record<string, unknown>[]
funds: Coin[]
}
export interface UpdateTokenMetadataMessage { export interface UpdateTokenMetadataMessage {
sender: string sender: string
contract: string contract: string
@ -601,6 +611,37 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
return res.transactionHash return res.transactionHash
} }
const batchTransferMultiAddress = async (
senderAddress: string,
recipients: AirdropAllocation[],
): Promise<string> => {
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 // eslint-disable-next-line @typescript-eslint/no-shadow
const updateCollectionInfo = async (collectionInfo: CollectionInfo): Promise<string> => { const updateCollectionInfo = async (collectionInfo: CollectionInfo): Promise<string> => {
const res = await client.execute( const res = await client.execute(
@ -748,6 +789,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
burn, burn,
batchBurn, batchBurn,
batchTransfer, batchTransfer,
batchTransferMultiAddress,
updateCollectionInfo, updateCollectionInfo,
freezeCollectionInfo, freezeCollectionInfo,
updateTokenMetadata, updateTokenMetadata,
@ -950,6 +992,19 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
} }
} }
const batchTransferMultiAddress = (recipients: AirdropAllocation[]): BatchTransferMultiAddressMessage => {
const msg: Record<string, unknown>[] = []
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 = ( const batchUpdateTokenMetadata = (
tokenIds: string, tokenIds: string,
baseURI: string, baseURI: string,
@ -1055,6 +1110,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con
burn, burn,
batchBurn, batchBurn,
batchTransfer, batchTransfer,
batchTransferMultiAddress,
updateCollectionInfo, updateCollectionInfo,
freezeCollectionInfo, freezeCollectionInfo,
updateTokenMetadata, updateTokenMetadata,

View File

@ -1,6 +1,6 @@
{ {
"name": "stargaze-studio", "name": "stargaze-studio",
"version": "0.7.8", "version": "0.7.9",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],

View File

@ -734,7 +734,19 @@ const CollectionCreationPage: NextPage = () => {
uploadDetails.pinataApiKey as string, uploadDetails.pinataApiKey as string,
uploadDetails.pinataSecretKey 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)) { if (minterType === 'vending' || (minterType === 'base' && uploadDetails.assetFiles.length > 1)) {
const fileArray: File[] = [] const fileArray: File[] = []
let reader: FileReader let reader: FileReader
@ -751,9 +763,24 @@ const CollectionCreationPage: NextPage = () => {
) { ) {
data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}` data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}`
} }
if (getAssetType(uploadDetails.assetFiles[i].name) !== 'html') data.image = `ipfs://${
data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}` 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) { if (data.description) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
data.description = data.description.replaceAll('\\n', '\n') data.description = data.description.replaceAll('\\n', '\n')
@ -805,8 +832,13 @@ const CollectionCreationPage: NextPage = () => {
) { ) {
data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[0].name}` data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[0].name}`
} }
if (getAssetType(uploadDetails.assetFiles[0].name) !== 'html') data.image = `ipfs://${
data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[0].name}` 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)], { const metadataFileBlob = new Blob([JSON.stringify(data)], {
type: 'application/json', type: 'application/json',

View File

@ -4,7 +4,7 @@
import axios from 'axios' import axios from 'axios'
import { PINATA_ENDPOINT_URL } from 'utils/constants' import { PINATA_ENDPOINT_URL } from 'utils/constants'
export type UploadFileType = 'assets' | 'metadata' | 'cover' export type UploadFileType = 'assets' | 'metadata' | 'cover' | 'thumbnail'
export const uploadToPinata = async ( export const uploadToPinata = async (
fileArray: File[], fileArray: File[],