Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3dad0aaf | ||
|
|
7ad9d755eb | ||
|
|
7bdf81d95e | ||
|
|
c5dbc5c855 | ||
|
|
5e28d7dfb0 | ||
|
|
71c273c4ef | ||
|
|
2f681bbcba | ||
|
|
03ddc13911 | ||
|
|
2f3065dca2 | ||
|
|
32f4157c42 | ||
|
|
dc902313a6 | ||
|
|
35e736062a | ||
|
|
e64073499d | ||
|
|
019a0c4d73 | ||
|
|
129953f7b3 | ||
|
|
6f36284fbd | ||
|
|
f4f11dbe6a | ||
|
|
07a08ca35a | ||
|
|
eb82d10140 | ||
|
|
9eab06738b | ||
|
|
fc65053978 | ||
|
|
fae92e5483 | ||
|
|
2340657020 |
+11
-50
@@ -1,57 +1,18 @@
|
||||
APP_VERSION=0.7.3
|
||||
APP_VERSION=0.1.0
|
||||
|
||||
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||
NEXT_PUBLIC_SG721_CODE_ID=2595
|
||||
NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID=2596
|
||||
NEXT_PUBLIC_OPEN_EDITION_SG721_CODE_ID=2595
|
||||
NEXT_PUBLIC_OPEN_EDITION_SG721_UPDATABLE_CODE_ID=2596
|
||||
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=2600
|
||||
NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID=2601
|
||||
NEXT_PUBLIC_BASE_MINTER_CODE_ID=2598
|
||||
NEXT_PUBLIC_OPEN_EDITION_MINTER_CODE_ID=2579
|
||||
NEXT_PUBLIC_WHITELIST_CODE_ID=3
|
||||
NEXT_PUBLIC_MINTER_CODE_ID=2
|
||||
NEXT_PUBLIC_SG721_CODE_ID=1
|
||||
|
||||
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars18h7ugh8eaug7wr0w4yjw0ls5s937z35pnkg935ucsek2y9xl3gaqqk4jtx"
|
||||
NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1h65nms9gwg4vdktyqj84tu50gwlm34e0eczl5w2ezllxuzfxy9esa9qlt0"
|
||||
NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS="stars1hvu2ghqkcnvhtj2fc6wuazxt4dqcftslp2rwkkkcxy269a35a9pq60ug2q"
|
||||
NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS=
|
||||
|
||||
# NEXT_PUBLIC_VENDING_IBC_ATOM_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_ATOM_UPDATABLE_FACTORY_FLEX_ADDRESS=
|
||||
|
||||
# NEXT_PUBLIC_VENDING_IBC_USDC_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS=
|
||||
|
||||
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1a45hcxty3spnmm2f0papl8v4dk5ew29s4syhn4efte8u5haex99qlkrtnx"
|
||||
NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS="stars100xegx2syry4tclkmejjwxk4nfqahvcqhm9qxut5wxuzhj5d9qfsh5nmym"
|
||||
|
||||
NEXT_PUBLIC_OPEN_EDITION_FACTORY_ADDRESS="stars1sqweqcxlf2f7qhf27gn5naqusk5q52fkzewmy63c4sglvle3s7ls6k828e"
|
||||
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS="stars1fk5dkzcylam8mcpqrn8y9spauvc3d4navtaqurcc49dc3p9f8d3qdkvymx"
|
||||
|
||||
# NEXT_PUBLIC_OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS=
|
||||
|
||||
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
|
||||
NEXT_PUBLIC_WHITELIST_CODE_ID=2602
|
||||
NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=2603
|
||||
NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336
|
||||
NEXT_PUBLIC_BADGE_HUB_ADDRESS="stars1dacun0xn7z73qzdcmq27q3xn6xuprg8e2ugj364784al2v27tklqynhuqa"
|
||||
NEXT_PUBLIC_BADGE_NFT_CODE_ID=1337
|
||||
NEXT_PUBLIC_BADGE_NFT_ADDRESS="stars1vlw4y54dyzt3zg7phj8yey9fg4zj49czknssngwmgrnwymyktztstalg7t"
|
||||
NEXT_PUBLIC_SPLITS_CODE_ID=1905
|
||||
NEXT_PUBLIC_CW4_GROUP_CODE_ID=1904
|
||||
|
||||
NEXT_PUBLIC_API_URL=https://nft-api.elgafar-1.stargaze-apis.com
|
||||
NEXT_PUBLIC_API_URL=https://
|
||||
NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze
|
||||
NEXT_PUBLIC_NETWORK=testnet
|
||||
NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev
|
||||
NEXT_PUBLIC_BADGES_URL=https://badges.publicawesome.dev
|
||||
NEXT_PUBLIC_WEBSITE_URL=https://
|
||||
NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL="https://..."
|
||||
|
||||
NEXT_PUBLIC_S3_BUCKET= # TODO
|
||||
NEXT_PUBLIC_S3_ENDPOINT= # TODO
|
||||
NEXT_PUBLIC_S3_KEY= # TODO
|
||||
NEXT_PUBLIC_S3_REGION= # TODO
|
||||
NEXT_PUBLIC_S3_SECRET= # TODO
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @MightOfOaks @name-user1 @Ninjatosba
|
||||
* @MightOfOaks @Ninjatosba
|
||||
|
||||
@@ -1,60 +1,16 @@
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { SG721_NAME_ADDRESS } from 'utils/constants'
|
||||
import { csvToArray } from 'utils/csvToArray'
|
||||
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
|
||||
import { isValidAccountsFile } from 'utils/isValidAccountsFile'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
|
||||
interface AirdropUploadProps {
|
||||
onChange: (data: AirdropAllocation[]) => void
|
||||
}
|
||||
|
||||
export const AirdropUpload = ({ onChange }: AirdropUploadProps) => {
|
||||
const wallet = useWallet()
|
||||
const [resolvedAllocationData, setResolvedAllocationData] = useState<AirdropAllocation[]>([])
|
||||
|
||||
const resolveAllocationData = async (allocationData: AirdropAllocation[]) => {
|
||||
if (!allocationData.length) return []
|
||||
await new Promise((resolve) => {
|
||||
let i = 0
|
||||
allocationData.map(async (data) => {
|
||||
if (!wallet.client) throw new Error('Wallet not connected')
|
||||
await wallet.client
|
||||
.queryContractRaw(
|
||||
SG721_NAME_ADDRESS,
|
||||
toUtf8(
|
||||
Buffer.from(
|
||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(
|
||||
data.address.trim().substring(0, data.address.lastIndexOf('.stars')),
|
||||
).toString('hex')}`,
|
||||
'hex',
|
||||
).toString(),
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
||||
if (tokenUri && isValidAddress(tokenUri))
|
||||
resolvedAllocationData.push({ address: tokenUri, amount: data.amount, tokenId: data.tokenId })
|
||||
else toast.error(`Resolved address is empty or invalid for the name: ${data.address}`)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
toast.error(`Error resolving address for the name: ${data.address}`)
|
||||
})
|
||||
|
||||
i++
|
||||
if (i === allocationData.length) resolve(resolvedAllocationData)
|
||||
})
|
||||
})
|
||||
return resolvedAllocationData
|
||||
}
|
||||
|
||||
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setResolvedAllocationData([])
|
||||
if (!event.target.files) return toast.error('Error opening file')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!event.target.files[0]?.name.endsWith('.csv')) {
|
||||
@@ -62,38 +18,18 @@ export const AirdropUpload = ({ onChange }: AirdropUploadProps) => {
|
||||
return onChange([])
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e: ProgressEvent<FileReader>) => {
|
||||
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||
try {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
const accountsData = csvToArray(e.target.result.toString())
|
||||
console.log(accountsData)
|
||||
if (!isValidAccountsFile(accountsData)) {
|
||||
event.target.value = ''
|
||||
return onChange([])
|
||||
}
|
||||
await resolveAllocationData(accountsData.filter((data) => data.address.trim().endsWith('.stars'))).finally(
|
||||
() => {
|
||||
return onChange(
|
||||
accountsData
|
||||
.filter((data) => data.address.startsWith('stars') && !data.address.endsWith('.stars'))
|
||||
.map((data) => ({
|
||||
address: data.address.trim(),
|
||||
amount: data.amount,
|
||||
tokenId: data.tokenId,
|
||||
}))
|
||||
.concat(
|
||||
resolvedAllocationData.map((data) => ({
|
||||
address: data.address,
|
||||
amount: data.amount,
|
||||
tokenId: data.tokenId,
|
||||
})),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
return onChange(accountsData)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
toast.error(error.message)
|
||||
}
|
||||
}
|
||||
reader.readAsText(event.target.files[0])
|
||||
|
||||
@@ -21,7 +21,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
|
||||
tempArray.push(
|
||||
<video
|
||||
key={assetFile.name}
|
||||
className={clsx('absolute px-1 my-1 max-h-24 thumbnail')}
|
||||
className="absolute px-1 my-1 max-h-24 thumbnail"
|
||||
id="video"
|
||||
muted
|
||||
onMouseEnter={(e) => {
|
||||
@@ -50,9 +50,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
|
||||
return assetFilesArray.slice((page - 1) * ITEM_NUMBER, page * ITEM_NUMBER).map((assetSource, index) => (
|
||||
<button
|
||||
key={assetSource.name}
|
||||
className={clsx(
|
||||
'relative p-0 w-[100px] h-[100px] bg-transparent hover:bg-transparent border-0 btn modal-button',
|
||||
)}
|
||||
className="relative p-0 w-[100px] h-[100px] bg-transparent hover:bg-transparent border-0 btn modal-button"
|
||||
onClick={() => {
|
||||
updateMetadataFileIndex((page - 1) * ITEM_NUMBER + index)
|
||||
}}
|
||||
@@ -71,15 +69,9 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
|
||||
>
|
||||
{(page - 1) * 12 + (index + 1)}
|
||||
</div>
|
||||
|
||||
{getAssetType(assetSource.name) === 'audio' && (
|
||||
<div className="flex absolute flex-col items-center mt-4 ml-2">
|
||||
<img
|
||||
key={`audio-${index}`}
|
||||
alt="audio_icon"
|
||||
className={clsx('mb-2 ml-1 w-6 h-6 thumbnail')}
|
||||
src="/audio.png"
|
||||
/>
|
||||
<img key={`audio-${index}`} alt="audio_icon" className="mb-2 ml-1 w-6 h-6 thumbnail" src="/audio.png" />
|
||||
<span className="flex self-center ">{assetSource.name}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -91,23 +83,11 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
|
||||
<img
|
||||
key={`image-${index}`}
|
||||
alt="asset"
|
||||
className={clsx('px-1 my-1 max-h-24 thumbnail')}
|
||||
className="px-1 my-1 max-h-24 thumbnail"
|
||||
src={URL.createObjectURL(assetSource)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{getAssetType(assetSource.name) === 'html' && (
|
||||
<div className="flex absolute flex-col items-center mt-4 ml-2">
|
||||
<img
|
||||
key={`html-${index}`}
|
||||
alt="html_icon"
|
||||
className={clsx('mb-2 ml-1 w-10 h-10 thumbnail')}
|
||||
src="/html.png"
|
||||
/>
|
||||
<span className="flex self-center">{assetSource.name.toLowerCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
</button>
|
||||
))
|
||||
@@ -136,7 +116,6 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mt-2 w-[400px] h-[300px]">{renderImages()}</div>
|
||||
|
||||
<div className="mt-5 btn-group">
|
||||
<button className="text-white bg-plumbus-light btn" onClick={multiplePrevPage} type="button">
|
||||
««
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import React, { useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { useWallet } from '../contexts/wallet'
|
||||
import { SG721_NAME_ADDRESS } from '../utils/constants'
|
||||
import { isValidAddress } from '../utils/isValidAddress'
|
||||
|
||||
interface BadgeAirdropListUploadProps {
|
||||
onChange: (data: string[]) => void
|
||||
}
|
||||
|
||||
export const BadgeAirdropListUpload = ({ onChange }: BadgeAirdropListUploadProps) => {
|
||||
const wallet = useWallet()
|
||||
const [resolvedAddresses, setResolvedAddresses] = useState<string[]>([])
|
||||
|
||||
const resolveAddresses = async (names: string[]) => {
|
||||
await new Promise((resolve) => {
|
||||
let i = 0
|
||||
names.map(async (name) => {
|
||||
if (!wallet.client) throw new Error('Wallet not connected')
|
||||
await wallet.client
|
||||
.queryContractRaw(
|
||||
SG721_NAME_ADDRESS,
|
||||
toUtf8(
|
||||
Buffer.from(
|
||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
|
||||
'hex',
|
||||
).toString(),
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
||||
if (tokenUri && isValidAddress(tokenUri)) resolvedAddresses.push(tokenUri)
|
||||
else toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
toast.error(`Error resolving address for the name: ${name}.stars`)
|
||||
})
|
||||
|
||||
i++
|
||||
if (i === names.length) resolve(resolvedAddresses)
|
||||
})
|
||||
})
|
||||
return resolvedAddresses
|
||||
}
|
||||
|
||||
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setResolvedAddresses([])
|
||||
if (!event.target.files) return toast.error('Error opening file')
|
||||
if (event.target.files.length !== 1) {
|
||||
toast.error('No file selected')
|
||||
return onChange([])
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (event.target.files[0]?.type !== 'text/plain') {
|
||||
toast.error('Invalid file type')
|
||||
return onChange([])
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e: ProgressEvent<FileReader>) => {
|
||||
const text = e.target?.result?.toString()
|
||||
let newline = '\n'
|
||||
if (text?.includes('\r')) newline = '\r'
|
||||
if (text?.includes('\r\n')) newline = '\r\n'
|
||||
|
||||
const cleanText = text?.toLowerCase().replace(/,/g, '').replace(/"/g, '').replace(/'/g, '').replace(/ /g, '')
|
||||
const data = cleanText?.split(newline)
|
||||
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 printableData = data?.map((item) => item.replace(regex, ''))
|
||||
const names = printableData?.filter((address) => address !== '' && address.endsWith('.stars'))
|
||||
const strippedNames = names?.map((name) => name.split('.')[0])
|
||||
console.log(names)
|
||||
if (strippedNames?.length) {
|
||||
await toast
|
||||
.promise(resolveAddresses(strippedNames), {
|
||||
loading: 'Resolving addresses...',
|
||||
success: 'Address resolution finalized.',
|
||||
error: 'Address resolution failed!',
|
||||
})
|
||||
.then((addresses) => {
|
||||
console.log(addresses)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
return onChange([
|
||||
...new Set(
|
||||
printableData
|
||||
?.filter((address) => address !== '' && isValidAddress(address) && address.startsWith('stars'))
|
||||
.concat(resolvedAddresses) || [],
|
||||
),
|
||||
])
|
||||
}
|
||||
reader.readAsText(event.target.files[0])
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept=".txt"
|
||||
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="badge-airdrop-list-file"
|
||||
multiple={false}
|
||||
onChange={onFileChange}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from './Button'
|
||||
|
||||
export interface BadgeConfirmationModalProps {
|
||||
confirm: () => void
|
||||
}
|
||||
export const BadgeConfirmationModal = (props: BadgeConfirmationModalProps) => {
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
return (
|
||||
<div>
|
||||
<input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" />
|
||||
<label className="cursor-pointer modal" htmlFor="my-modal-2">
|
||||
<label
|
||||
className="absolute top-[25%] bottom-5 left-1/3 max-w-[600px] max-h-[400px] border-2 no-scrollbar modal-box"
|
||||
htmlFor="temp"
|
||||
>
|
||||
{/* <Alert type="warning"></Alert> */}
|
||||
<div className="text-xl font-bold">
|
||||
<div className="text-sm font-thin">
|
||||
You represent and warrant that you have, or have obtained, all rights, licenses, consents, permissions,
|
||||
power and/or authority necessary to grant the rights granted herein for any content that you create,
|
||||
submit, post, promote, or display on or through the Service. You represent and warrant that such contain
|
||||
material subject to copyright, trademark, publicity rights, or other intellectual property rights, unless
|
||||
you have necessary permission or are otherwise legally entitled to post the material and to grant Stargaze
|
||||
Parties the license described above, and that the content does not violate any laws.
|
||||
</div>
|
||||
<br />
|
||||
<div className="flex flex-row pb-4">
|
||||
<label className="flex flex-col space-y-1" htmlFor="terms">
|
||||
<span className="text-sm font-light text-white">I agree with the terms above.</span>
|
||||
</label>
|
||||
<input
|
||||
checked={isChecked}
|
||||
className="p-2 mb-1 ml-2"
|
||||
id="terms"
|
||||
name="terms"
|
||||
onClick={() => setIsChecked(!isChecked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
Are you sure to proceed with creating a new badge?
|
||||
</div>
|
||||
<div className="flex justify-end w-full">
|
||||
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">
|
||||
<label
|
||||
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 rounded border-0 btn modal-button"
|
||||
htmlFor="my-modal-2"
|
||||
>
|
||||
Go Back
|
||||
</label>
|
||||
</Button>
|
||||
<Button className="px-0 mt-4 mb-4 max-h-12" isDisabled={!isChecked} onClick={props.confirm}>
|
||||
<label
|
||||
className="w-full h-full text-white bg-plumbus hover:bg-plumbus-light border-0 btn modal-button"
|
||||
htmlFor="my-modal-2"
|
||||
>
|
||||
Confirm
|
||||
</label>
|
||||
</Button>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export const BadgeLoadingModal = () => {
|
||||
return (
|
||||
<div
|
||||
className="flex overflow-hidden fixed top-0 right-0 bottom-0 left-0 z-50 flex-col justify-center items-center w-full h-screen bg-gray-900 opacity-80"
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
<img alt="Pixel Logo" className="mb-5 w-[50px] h-[50px] animate-spin" src="/icon.svg" />
|
||||
<p className="w-1/3 font-bold text-center text-white">Uploading the image for badge creation, please wait...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,59 +1,33 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from './Button'
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
confirm: () => void
|
||||
}
|
||||
export const ConfirmationModal = (props: ConfirmationModalProps) => {
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
return (
|
||||
<div>
|
||||
<input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" />
|
||||
<label className="cursor-pointer modal" htmlFor="my-modal-2">
|
||||
<label
|
||||
className="absolute top-[25%] bottom-5 left-1/3 max-w-[600px] max-h-[400px] border-2 no-scrollbar modal-box"
|
||||
className="absolute top-[40%] bottom-5 left-1/3 max-w-xl max-h-40 border-2 no-scrollbar modal-box"
|
||||
htmlFor="temp"
|
||||
>
|
||||
{/* <Alert type="warning"></Alert> */}
|
||||
<div className="text-xl font-bold">
|
||||
<div className="text-sm font-thin">
|
||||
You represent and warrant that you have, or have obtained, all rights, licenses, consents, permissions,
|
||||
power and/or authority necessary to grant the rights granted herein for any content that you create,
|
||||
submit, post, promote, or display on or through the Service. You represent and warrant that such contain
|
||||
material subject to copyright, trademark, publicity rights, or other intellectual property rights, unless
|
||||
you have necessary permission or are otherwise legally entitled to post the material and to grant Stargaze
|
||||
Parties the license described above, and that the content does not violate any laws.
|
||||
</div>
|
||||
<br />
|
||||
<div className="flex flex-row pb-4">
|
||||
<label className="flex flex-col space-y-1" htmlFor="terms">
|
||||
<span className="text-sm font-light text-white">I agree with the terms above.</span>
|
||||
</label>
|
||||
<input
|
||||
checked={isChecked}
|
||||
className="p-2 mb-1 ml-2"
|
||||
id="terms"
|
||||
name="terms"
|
||||
onClick={() => setIsChecked(!isChecked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
Are you sure to proceed with the specified assets, metadata and parameters?
|
||||
Are you sure to create a collection with the specified assets, metadata and parameters?
|
||||
</div>
|
||||
<div className="flex justify-end w-full">
|
||||
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">
|
||||
<label
|
||||
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 rounded border-0 btn modal-button"
|
||||
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 border-0 btn modal-button"
|
||||
htmlFor="my-modal-2"
|
||||
>
|
||||
Go Back
|
||||
</label>
|
||||
</Button>
|
||||
<Button className="px-0 mt-4 mb-4 max-h-12" isDisabled={!isChecked} onClick={props.confirm}>
|
||||
<Button className="px-0 mt-4 mb-4 max-h-12" onClick={props.confirm}>
|
||||
<label
|
||||
className="w-full h-full text-white bg-plumbus hover:bg-plumbus-light border-0 btn modal-button"
|
||||
className="w-full h-full text-white bg-plumbus-light hover:bg-plumbus-light border-0 btn modal-button"
|
||||
htmlFor="my-modal-2"
|
||||
>
|
||||
Confirm
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import { Button } from './Button'
|
||||
|
||||
export interface IncomeDashboardDisclaimerProps {
|
||||
creatorAddress: string
|
||||
}
|
||||
export const IncomeDashboardDisclaimer = (props: IncomeDashboardDisclaimerProps) => {
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
const checkBoxRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleCheckBox = () => {
|
||||
checkBoxRef.current?.click()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input className="modal-toggle" defaultChecked={false} id="my-modal-1" ref={checkBoxRef} type="checkbox" />
|
||||
<label className="cursor-pointer modal" htmlFor="my-modal-1">
|
||||
<label
|
||||
className="absolute top-[25%] bottom-5 left-1/3 max-w-[600px] max-h-[450px] border-2 no-scrollbar modal-box"
|
||||
htmlFor="temp"
|
||||
>
|
||||
{/* <Alert type="warning"></Alert> */}
|
||||
<div className="text-xl font-bold">
|
||||
<div className="text-sm font-thin">
|
||||
The tool provided on this website is for informational purposes only and does not constitute tax, legal or
|
||||
financial advice. The information provided by the tool is not intended to be used for tax planning, tax
|
||||
avoidance, promoting, marketing or related purposes. Users should consult their own tax, legal or
|
||||
financial advisors prior to acting on any information provided by the tool. By clicking accept below, you
|
||||
agree that neither Stargaze Foundation or Public Awesome, LLC or any of its directors, officers,
|
||||
employees, or advisors shall be responsible for any errors, omissions, or inaccuracies in the information
|
||||
provided by the tool, and shall not be liable for any damages, losses, or expenses arising out of or in
|
||||
connection with the use of the tool. Furthermore, you agree to indemnify Stargaze Foundation, Public
|
||||
Awesome, LLC and any of its directors, officers, employees and advisors against any claims, suits, or
|
||||
actions related to your use of the tool.
|
||||
</div>
|
||||
<br />
|
||||
<div className="flex flex-row pb-4">
|
||||
<label className="flex flex-col space-y-1" htmlFor="terms">
|
||||
<span className="text-sm font-light text-white">I agree with the terms above.</span>
|
||||
</label>
|
||||
<input
|
||||
checked={isChecked}
|
||||
className="p-2 mb-1 ml-2"
|
||||
id="terms"
|
||||
name="terms"
|
||||
onClick={() => setIsChecked(!isChecked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
Are you sure to proceed to the Creator Revenue Dashboard?
|
||||
</div>
|
||||
<div className="flex justify-end w-full">
|
||||
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700">
|
||||
<label
|
||||
className="w-full h-full text-white bg-gray-600 hover:bg-gray-700 rounded border-0 btn modal-button"
|
||||
htmlFor="my-modal-1"
|
||||
>
|
||||
Go Back
|
||||
</label>
|
||||
</Button>
|
||||
<a
|
||||
className="my-4"
|
||||
href={
|
||||
isChecked
|
||||
? `https://metabase.constellations.zone/public/dashboard/4d751721-51ab-46ff-ad27-075ec8d47a17?creator_address=${props.creatorAddress}&chart_granularity_(day%252Fweek%252Fmonth)=week`
|
||||
: undefined
|
||||
}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<Button className="px-5 w-full h-full" isDisabled={!isChecked} onClick={() => handleCheckBox()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+15
-97
@@ -1,6 +1,11 @@
|
||||
import type { LinkTabProps } from './LinkTab'
|
||||
|
||||
export const sg721LinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
description: `Create a new SG721 contract`,
|
||||
href: '/contracts/sg721/instantiate',
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries with your SG721 contract`,
|
||||
@@ -18,67 +23,26 @@ export const sg721LinkTabs: LinkTabProps[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export const vendingMinterLinkTabs: LinkTabProps[] = [
|
||||
export const minterLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
description: `Initialize a new Vending Minter contract`,
|
||||
href: '/contracts/vendingMinter/instantiate',
|
||||
description: `Initialize a new Minter contract`,
|
||||
href: '/contracts/minter/instantiate',
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries for your Vending Minter contract`,
|
||||
href: '/contracts/vendingMinter/query',
|
||||
description: `Dispatch queries with your Minter contract`,
|
||||
href: '/contracts/minter/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Vending Minter contract actions`,
|
||||
href: '/contracts/vendingMinter/execute',
|
||||
description: `Execute Minter contract actions`,
|
||||
href: '/contracts/minter/execute',
|
||||
},
|
||||
{
|
||||
title: 'Migrate',
|
||||
description: `Migrate Vending Minter contract`,
|
||||
href: '/contracts/vendingMinter/migrate',
|
||||
},
|
||||
]
|
||||
|
||||
export const openEditionMinterLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries for your Open Edition Minter contract`,
|
||||
href: '/contracts/openEditionMinter/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Open Edition Minter contract actions`,
|
||||
href: '/contracts/openEditionMinter/execute',
|
||||
},
|
||||
{
|
||||
title: 'Migrate',
|
||||
description: `Migrate Open Edition Minter contract`,
|
||||
href: '/contracts/openEditionMinter/migrate',
|
||||
},
|
||||
]
|
||||
|
||||
export const baseMinterLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
description: `Initialize a new Base Minter contract`,
|
||||
href: '/contracts/baseMinter/instantiate',
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries for your Base Minter contract`,
|
||||
href: '/contracts/baseMinter/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Base Minter contract actions`,
|
||||
href: '/contracts/baseMinter/execute',
|
||||
},
|
||||
{
|
||||
title: 'Migrate',
|
||||
description: `Migrate Base Minter contract`,
|
||||
href: '/contracts/baseMinter/migrate',
|
||||
description: `Migrate Minter contract`,
|
||||
href: '/contracts/minter/migrate',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -90,7 +54,7 @@ export const whitelistLinkTabs: LinkTabProps[] = [
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries for your Whitelist contract`,
|
||||
description: `Dispatch queries with your Whitelist contract`,
|
||||
href: '/contracts/whitelist/query',
|
||||
},
|
||||
{
|
||||
@@ -99,49 +63,3 @@ export const whitelistLinkTabs: LinkTabProps[] = [
|
||||
href: '/contracts/whitelist/execute',
|
||||
},
|
||||
]
|
||||
|
||||
export const badgeHubLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
description: `Initialize a new Badge Hub contract`,
|
||||
href: '/contracts/badgeHub/instantiate',
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries for your Badge Hub contract`,
|
||||
href: '/contracts/badgeHub/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Badge Hub contract actions`,
|
||||
href: '/contracts/badgeHub/execute',
|
||||
},
|
||||
{
|
||||
title: 'Migrate',
|
||||
description: `Migrate Badge Hub contract`,
|
||||
href: '/contracts/badgeHub/migrate',
|
||||
},
|
||||
]
|
||||
|
||||
export const splitsLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
description: `Initialize a new Splits contract`,
|
||||
href: '/contracts/splits/instantiate',
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries for your Splits contract`,
|
||||
href: '/contracts/splits/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Splits contract actions`,
|
||||
href: '/contracts/splits/execute',
|
||||
},
|
||||
{
|
||||
title: 'Migrate',
|
||||
description: `Migrate Splits contract`,
|
||||
href: '/contracts/splits/migrate',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { useLogStore } from 'contexts/log'
|
||||
import { useRef, useState } from 'react'
|
||||
import { FaCopy, FaEraser } from 'react-icons/fa'
|
||||
import { copy } from 'utils/clipboard'
|
||||
|
||||
import type { LogItem } from '../contexts/log'
|
||||
import { removeLogItem, setLogItemList } from '../contexts/log'
|
||||
import { Button } from './Button'
|
||||
import { Tooltip } from './Tooltip'
|
||||
|
||||
export interface LogModalProps {
|
||||
tempLogItem?: LogItem
|
||||
}
|
||||
export const LogModal = (props: LogModalProps) => {
|
||||
const logs = useLogStore()
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
|
||||
const checkBoxRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleCheckBox = () => {
|
||||
checkBoxRef.current?.click()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input className="modal-toggle" defaultChecked={false} id="my-modal-8" ref={checkBoxRef} type="checkbox" />
|
||||
<label className="cursor-pointer modal" htmlFor="my-modal-8">
|
||||
<label
|
||||
className={`absolute top-[15%] bottom-5 left-[21.5%] lg:max-w-[70%] ${
|
||||
logs.itemList.length > 4 ? 'max-h-[750px]' : 'max-h-[500px]'
|
||||
} border-2 no-scrollbar modal-box`}
|
||||
htmlFor="temp"
|
||||
>
|
||||
<div className="text-xl font-bold">
|
||||
<table className="table w-full h-1/2">
|
||||
<thead className="sticky inset-x-0 top-0 bg-blue-400/20 backdrop-blur-sm">
|
||||
<tr>
|
||||
<th className="text-lg font-bold bg-transparent">#</th>
|
||||
<th className="text-lg font-bold bg-transparent">Type</th>
|
||||
<th className="text-lg font-bold bg-transparent">Message</th>
|
||||
<th className="text-lg font-bold bg-transparent">
|
||||
Time (UTC +{-new Date().getTimezoneOffset() / 60}){' '}
|
||||
</th>
|
||||
<th className="bg-transparent" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.itemList.length > 0 &&
|
||||
logs.itemList.map((logItem, index) => (
|
||||
<tr key={logItem.id} className="p-0 border-b-2 border-teal-200/10 border-collapse">
|
||||
<td className="ml-8 w-[5%] font-mono text-base font-bold bg-transparent">{index + 1}</td>
|
||||
<td
|
||||
className={`w-[5%] font-mono text-base font-bold bg-transparent ${
|
||||
logItem.type === 'Error' ? 'text-red-400' : ''
|
||||
}`}
|
||||
>
|
||||
{logItem.type || 'Info'}
|
||||
</td>
|
||||
<td className="w-[70%] text-sm font-bold bg-transparent">
|
||||
<Tooltip backgroundColor="bg-transparent" label="" placement="bottom">
|
||||
<button
|
||||
className="group flex overflow-auto space-x-2 max-w-xl font-mono text-base text-white/80 hover:underline no-scrollbar"
|
||||
onClick={() => void copy(logItem.message)}
|
||||
type="button"
|
||||
>
|
||||
<span>{logItem.message}</span>
|
||||
<FaCopy className="opacity-0 group-hover:opacity-100" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className="w-[20%] font-mono text-base bg-transparent">
|
||||
{logItem.timestamp ? new Date(logItem.timestamp).toLocaleString() : ''}
|
||||
</td>
|
||||
<th className="bg-transparent">
|
||||
<div className="flex items-center space-x-8">
|
||||
<Button
|
||||
className="bg-clip-text border-blue-200"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
removeLogItem(logItem.id)
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<FaEraser />
|
||||
</Button>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
//line break
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex justify-start ml-4 w-full">
|
||||
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700">
|
||||
<label
|
||||
className="w-full h-full text-white bg-gray-600 hover:bg-gray-700 rounded border-0 btn modal-button"
|
||||
htmlFor="my-modal-8"
|
||||
>
|
||||
Go Back
|
||||
</label>
|
||||
</Button>
|
||||
<Button
|
||||
className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700"
|
||||
onClick={() => {
|
||||
window.localStorage.setItem('error_list', '')
|
||||
setLogItemList([])
|
||||
}}
|
||||
>
|
||||
<label
|
||||
className="w-full h-full text-white bg-blue-400 hover:bg-blue-500 rounded border-0 btn modal-button"
|
||||
htmlFor="my-modal-8"
|
||||
>
|
||||
Clear
|
||||
</label>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700"
|
||||
onClick={() => {
|
||||
const csv = logs.itemList
|
||||
.map((logItem) => {
|
||||
return `${logItem.type as string},${logItem.message},${
|
||||
logItem.timestamp ? new Date(logItem.timestamp).toUTCString().replace(',', '') : ''
|
||||
}`
|
||||
})
|
||||
.join('\n')
|
||||
const blob = new Blob([csv], { type: 'text/csv' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('hidden', '')
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', 'studio_logs.csv')
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}}
|
||||
>
|
||||
<label
|
||||
className="w-full h-full text-white bg-stargaze hover:bg-stargaze/80 rounded border-0 btn modal-button"
|
||||
htmlFor="my-modal-8"
|
||||
>
|
||||
Download
|
||||
</label>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
|
||||
export interface MetadataFormGroupProps {
|
||||
@@ -13,7 +13,6 @@ export interface MetadataFormGroupProps {
|
||||
|
||||
export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
|
||||
const { title, subtitle, relatedAsset, children } = props
|
||||
const [htmlContents, setHtmlContents] = useState<string>('')
|
||||
|
||||
const videoPreview = useMemo(
|
||||
() => (
|
||||
@@ -41,17 +40,6 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
|
||||
[relatedAsset],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (getAssetType(relatedAsset?.name as string) !== 'html') return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (typeof e.target?.result === 'string') {
|
||||
setHtmlContents(e.target.result)
|
||||
}
|
||||
}
|
||||
reader.readAsText(new Blob([relatedAsset as File]))
|
||||
}, [relatedAsset])
|
||||
|
||||
return (
|
||||
<div className="flex p-4 pt-0 space-x-4 w-full">
|
||||
<div className="flex flex-col w-1/3">
|
||||
@@ -66,9 +54,6 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
|
||||
{getAssetType(relatedAsset.name) === 'image' && (
|
||||
<img alt="preview" src={URL.createObjectURL(relatedAsset)} />
|
||||
)}
|
||||
{getAssetType(relatedAsset.name) === 'html' && (
|
||||
<iframe allowFullScreen height="420px" srcDoc={htmlContents} title="Preview" width="100%" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { TextInput } from './forms/FormInput'
|
||||
import { useInputState } from './forms/FormInput.hooks'
|
||||
import { MetadataAttributes } from './forms/MetadataAttributes'
|
||||
|
||||
export interface MetadataInputProps {
|
||||
selectedAssetFile: File
|
||||
selectedMetadataFile: File
|
||||
updateMetadataToUpload: (metadataFile: File) => void
|
||||
}
|
||||
|
||||
export const MetadataInput = (props: MetadataInputProps) => {
|
||||
const emptyMetadataFile = new File(
|
||||
[JSON.stringify({})],
|
||||
`${props.selectedAssetFile?.name.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))}.json`,
|
||||
{ type: 'application/json' },
|
||||
)
|
||||
|
||||
const [metadata, setMetadata] = useState<any>(null)
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
placeholder: 'Token name',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
id: 'description',
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
placeholder: 'Token description',
|
||||
})
|
||||
|
||||
const externalUrlState = useInputState({
|
||||
id: 'externalUrl',
|
||||
name: 'externalUrl',
|
||||
title: 'External URL',
|
||||
placeholder: 'https://',
|
||||
})
|
||||
|
||||
const youtubeUrlState = useInputState({
|
||||
id: 'youtubeUrl',
|
||||
name: 'youtubeUrl',
|
||||
title: 'Youtube URL',
|
||||
placeholder: 'https://',
|
||||
})
|
||||
|
||||
const attributesState = useMetadataAttributesState()
|
||||
|
||||
let parsedMetadata: any
|
||||
const parseMetadata = async (metadataFile: File) => {
|
||||
console.log(metadataFile.size)
|
||||
console.log(`Parsing metadataFile...`)
|
||||
if (metadataFile) {
|
||||
attributesState.reset()
|
||||
try {
|
||||
parsedMetadata = JSON.parse(await metadataFile.text())
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return
|
||||
}
|
||||
console.log('Parsed metadata: ', parsedMetadata)
|
||||
|
||||
if (!parsedMetadata.attributes || parsedMetadata.attributes?.length === 0) {
|
||||
attributesState.reset()
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
} else {
|
||||
attributesState.reset()
|
||||
for (let i = 0; i < parsedMetadata.attributes?.length; i++) {
|
||||
attributesState.add({
|
||||
trait_type: parsedMetadata.attributes[i].trait_type,
|
||||
value: parsedMetadata.attributes[i].value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!parsedMetadata.name) {
|
||||
nameState.onChange('')
|
||||
} else {
|
||||
nameState.onChange(parsedMetadata.name)
|
||||
}
|
||||
if (!parsedMetadata.description) {
|
||||
descriptionState.onChange('')
|
||||
} else {
|
||||
descriptionState.onChange(parsedMetadata.description)
|
||||
}
|
||||
if (!parsedMetadata.external_url) {
|
||||
externalUrlState.onChange('')
|
||||
} else {
|
||||
externalUrlState.onChange(parsedMetadata.external_url)
|
||||
}
|
||||
if (!parsedMetadata.youtube_url) {
|
||||
youtubeUrlState.onChange('')
|
||||
} else {
|
||||
youtubeUrlState.onChange(parsedMetadata.youtube_url)
|
||||
}
|
||||
setMetadata(parsedMetadata)
|
||||
} else {
|
||||
attributesState.reset()
|
||||
nameState.onChange('')
|
||||
descriptionState.onChange('')
|
||||
externalUrlState.onChange('')
|
||||
youtubeUrlState.onChange('')
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
setMetadata(null)
|
||||
props.updateMetadataToUpload(emptyMetadataFile)
|
||||
}
|
||||
}
|
||||
|
||||
const generateUpdatedMetadata = () => {
|
||||
metadata.attributes = Object.values(attributesState)[1]
|
||||
metadata.attributes = metadata.attributes.filter((attribute: { trait_type: string }) => attribute.trait_type !== '')
|
||||
|
||||
if (metadata.attributes.length === 0) delete metadata.attributes
|
||||
if (nameState.value === '') delete metadata.name
|
||||
else metadata.name = nameState.value
|
||||
if (descriptionState.value === '') delete metadata.description
|
||||
else metadata.description = descriptionState.value
|
||||
if (externalUrlState.value === '') delete metadata.external_url
|
||||
else metadata.external_url = externalUrlState.value
|
||||
if (youtubeUrlState.value === '') delete metadata.youtube_url
|
||||
else metadata.youtube_url = youtubeUrlState.value
|
||||
|
||||
const metadataFileBlob = new Blob([JSON.stringify(metadata)], {
|
||||
type: 'application/json',
|
||||
})
|
||||
|
||||
const editedMetadataFile = new File(
|
||||
[metadataFileBlob],
|
||||
props.selectedMetadataFile?.name
|
||||
? props.selectedMetadataFile?.name
|
||||
: `${props.selectedAssetFile?.name.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))}.json`,
|
||||
{ type: 'application/json' },
|
||||
)
|
||||
props.updateMetadataToUpload(editedMetadataFile)
|
||||
//console.log(editedMetadataFile)
|
||||
//console.log(`${props.assetFile?.name.substring(0, props.assetFile?.name.lastIndexOf('.'))}.json`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(props.selectedMetadataFile?.name)
|
||||
if (props.selectedMetadataFile) void parseMetadata(props.selectedMetadataFile)
|
||||
else void parseMetadata(emptyMetadataFile)
|
||||
}, [props.selectedMetadataFile?.name])
|
||||
|
||||
const nameStateMemo = useMemo(() => nameState, [nameState.value])
|
||||
const descriptionStateMemo = useMemo(() => descriptionState, [descriptionState.value])
|
||||
const externalUrlStateMemo = useMemo(() => externalUrlState, [externalUrlState.value])
|
||||
const youtubeUrlStateMemo = useMemo(() => youtubeUrlState, [youtubeUrlState.value])
|
||||
const attributesStateMemo = useMemo(() => attributesState, [attributesState.entries])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Update metadata')
|
||||
if (metadata) generateUpdatedMetadata()
|
||||
console.log(metadata)
|
||||
}, [
|
||||
nameStateMemo.value,
|
||||
descriptionStateMemo.value,
|
||||
externalUrlStateMemo.value,
|
||||
youtubeUrlStateMemo.value,
|
||||
attributesStateMemo.entries,
|
||||
])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-2 mt-4 mr-4 ml-8 w-full max-w-6xl max-h-full no-scrollbar">
|
||||
<div className="mr-4">
|
||||
<div className="mb-7 text-xl font-bold underline underline-offset-4">NFT Metadata</div>
|
||||
<TextInput {...nameState} />
|
||||
<TextInput className="mt-2" {...descriptionState} />
|
||||
<TextInput className="mt-2" {...externalUrlState} />
|
||||
<TextInput className="mt-2" {...youtubeUrlState} />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<MetadataAttributes
|
||||
attributes={attributesState.entries}
|
||||
onAdd={attributesState.add}
|
||||
onChange={attributesState.update}
|
||||
onRemove={attributesState.remove}
|
||||
title="Attributes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { Alert } from './Alert'
|
||||
import { Button } from './Button'
|
||||
import { Conditional } from './Conditional'
|
||||
import { TextInput } from './forms/FormInput'
|
||||
import { useInputState } from './forms/FormInput.hooks'
|
||||
import { MetadataAttributes } from './forms/MetadataAttributes'
|
||||
@@ -66,13 +64,6 @@ export const MetadataModal = (props: MetadataModalProps) => {
|
||||
}
|
||||
|
||||
setMetadata(parsedMetadata)
|
||||
} else {
|
||||
attributesState.reset()
|
||||
nameState.onChange('')
|
||||
descriptionState.onChange('')
|
||||
externalUrlState.onChange('')
|
||||
youtubeUrlState.onChange('')
|
||||
setMetadata(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +102,9 @@ export const MetadataModal = (props: MetadataModalProps) => {
|
||||
const attributesState = useMetadataAttributesState()
|
||||
|
||||
const generateUpdatedMetadata = () => {
|
||||
console.log(`Current parsed data: ${parsedMetadata}`)
|
||||
console.log('Updating...')
|
||||
|
||||
metadata.attributes = Object.values(attributesState)[1]
|
||||
metadata.attributes = metadata.attributes.filter((attribute: { trait_type: string }) => attribute.trait_type !== '')
|
||||
|
||||
@@ -119,7 +113,7 @@ export const MetadataModal = (props: MetadataModalProps) => {
|
||||
if (descriptionState.value === '') delete metadata.description
|
||||
else metadata.description = descriptionState.value
|
||||
if (externalUrlState.value === '') delete metadata.external_url
|
||||
else metadata.external_url = externalUrlState.value
|
||||
else metadata.externalUrl = externalUrlState.value
|
||||
if (youtubeUrlState.value === '') delete metadata.youtube_url
|
||||
else metadata.youtube_url = youtubeUrlState.value
|
||||
|
||||
@@ -130,6 +124,7 @@ export const MetadataModal = (props: MetadataModalProps) => {
|
||||
const editedMetadataFile = new File([metadataFileBlob], metadataFile.name, { type: 'application/json' })
|
||||
props.updateMetadata(editedMetadataFile)
|
||||
toast.success('Metadata updated successfully.')
|
||||
console.log(editedMetadataFile)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -149,42 +144,21 @@ export const MetadataModal = (props: MetadataModalProps) => {
|
||||
subtitle={`Asset filename: ${props.assetFile?.name}`}
|
||||
title="Update Metadata"
|
||||
>
|
||||
<TextInput
|
||||
{...nameState}
|
||||
disabled={!props.metadataFile}
|
||||
onChange={(e) => nameState.onChange(e.target.value)}
|
||||
<TextInput {...nameState} onChange={(e) => nameState.onChange(e.target.value)} />
|
||||
<TextInput {...descriptionState} onChange={(e) => descriptionState.onChange(e.target.value)} />
|
||||
<TextInput {...externalUrlState} onChange={(e) => externalUrlState.onChange(e.target.value)} />
|
||||
<TextInput {...youtubeUrlState} onChange={(e) => youtubeUrlState.onChange(e.target.value)} />
|
||||
<MetadataAttributes
|
||||
attributes={attributesState.entries}
|
||||
onAdd={attributesState.add}
|
||||
onChange={attributesState.update}
|
||||
onRemove={attributesState.remove}
|
||||
subtitle="Enter trait types and values"
|
||||
title="Attributes"
|
||||
/>
|
||||
<TextInput
|
||||
{...descriptionState}
|
||||
disabled={!props.metadataFile}
|
||||
onChange={(e) => descriptionState.onChange(e.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
{...externalUrlState}
|
||||
disabled={!props.metadataFile}
|
||||
onChange={(e) => externalUrlState.onChange(e.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
{...youtubeUrlState}
|
||||
disabled={!props.metadataFile}
|
||||
onChange={(e) => youtubeUrlState.onChange(e.target.value)}
|
||||
/>
|
||||
<Conditional test={props.metadataFile !== null}>
|
||||
<MetadataAttributes
|
||||
attributes={attributesState.entries}
|
||||
onAdd={attributesState.add}
|
||||
onChange={attributesState.update}
|
||||
onRemove={attributesState.remove}
|
||||
subtitle="Enter trait types and values"
|
||||
title="Attributes"
|
||||
/>
|
||||
</Conditional>
|
||||
<Button isDisabled={!props.metadataFile} onClick={generateUpdatedMetadata}>
|
||||
Update Metadata
|
||||
</Button>
|
||||
<Conditional test={Boolean(!props.metadataFile)}>
|
||||
<Alert type="info">No metadata file to preview. Please select metadata files.</Alert>
|
||||
</Conditional>
|
||||
</MetadataFormGroup>
|
||||
</label>
|
||||
</label>
|
||||
|
||||
+36
-222
@@ -1,246 +1,60 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { setLogItemList, useLogStore } from 'contexts/log'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
// import BrandText from 'public/brand/brand-text.svg'
|
||||
import { footerLinks, socialsLinks } from 'utils/links'
|
||||
|
||||
import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS, NETWORK, OPEN_EDITION_FACTORY_ADDRESS } from '../utils/constants'
|
||||
import { Conditional } from './Conditional'
|
||||
import { IncomeDashboardDisclaimer } from './IncomeDashboardDisclaimer'
|
||||
import { LogModal } from './LogModal'
|
||||
import { SidebarLayout } from './SidebarLayout'
|
||||
import { WalletLoader } from './WalletLoader'
|
||||
|
||||
const routes = [
|
||||
{ text: 'Collections', href: `/collections/`, isChild: false },
|
||||
{ text: 'Create a Collection', href: `/collections/create/`, isChild: true },
|
||||
{ text: 'My Collections', href: `/collections/myCollections/`, isChild: true },
|
||||
{ text: 'Collection Actions', href: `/collections/actions/`, isChild: true },
|
||||
{ text: 'Contract Dashboards', href: `/contracts/`, isChild: false },
|
||||
{ text: 'Minter Contract', href: `/contracts/minter/`, isChild: true },
|
||||
{ text: 'SG721 Contract', href: `/contracts/sg721/`, isChild: true },
|
||||
{ text: 'Whitelist Contract', href: `/contracts/whitelist/`, isChild: true },
|
||||
]
|
||||
|
||||
export const Sidebar = () => {
|
||||
const router = useRouter()
|
||||
const wallet = useWallet()
|
||||
const logs = useLogStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (logs.itemList.length === 0) return
|
||||
console.log('Stringified log item list: ', JSON.stringify(logs.itemList))
|
||||
window.localStorage.setItem('logs', JSON.stringify(logs.itemList))
|
||||
}, [logs])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(window.localStorage.getItem('logs'))
|
||||
setLogItemList(JSON.parse(window.localStorage.getItem('logs') || '[]'))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SidebarLayout>
|
||||
{/* Stargaze brand as home button */}
|
||||
<Anchor href="/" onContextMenu={(e) => [e.preventDefault(), router.push('/brand')]}>
|
||||
<img alt="Brand Text" className="ml-6 w-3/4" src="/studio-logo.png" />
|
||||
<img alt="Brand Text" className="w-full" src="/stargaze_logo_800.svg" />
|
||||
</Anchor>
|
||||
|
||||
{/* wallet button */}
|
||||
<WalletLoader />
|
||||
{/* main navigation routes */}
|
||||
|
||||
<div className="absolute top-[20%] left-[5%] mt-2">
|
||||
<ul className="group p-2 w-full bg-transparent menu rounded-box">
|
||||
<li tabIndex={0}>
|
||||
<div
|
||||
className={clsx(
|
||||
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
|
||||
'hover:bg-white/5 transition-colors',
|
||||
router.asPath.includes('/collections/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
>
|
||||
<Link href="/collections/" passHref>
|
||||
Collections
|
||||
</Link>
|
||||
</div>
|
||||
<ul className="z-50 p-2 bg-base-200">
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/collections/create') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/collections/create/">Create a Collection</Link>
|
||||
</li>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/collections/myCollections/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/collections/myCollections/">My Collections</Link>
|
||||
</li>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/collections/actions/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/collections/actions/">Collection Actions</Link>
|
||||
</li>
|
||||
<Conditional test={NETWORK === 'mainnet'}>
|
||||
<li className={clsx('text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded')} tabIndex={-1}>
|
||||
<label
|
||||
className="w-full h-full text-lg font-bold text-gray hover:text-white normal-case bg-clip-text bg-transparent border-none animate-none btn modal-button"
|
||||
htmlFor="my-modal-1"
|
||||
>
|
||||
Revenue Dashboard
|
||||
</label>
|
||||
</li>
|
||||
</Conditional>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<Conditional test={BADGE_HUB_ADDRESS !== undefined}>
|
||||
<ul className="group p-2 w-full bg-transparent menu rounded-box">
|
||||
<li tabIndex={0}>
|
||||
<span
|
||||
className={clsx(
|
||||
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
|
||||
'hover:bg-white/5 transition-colors',
|
||||
router.asPath.includes('/badges/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
>
|
||||
<Link href="/badges/"> Badges </Link>
|
||||
</span>
|
||||
<ul className="z-50 p-2 rounded-box bg-base-200">
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/badges/create/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/badges/create/">Create a Badge</Link>
|
||||
</li>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/badges/myBadges/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/badges/myBadges/">My Badges</Link>
|
||||
</li>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/badges/actions/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/badges/actions/">Badge Actions</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</Conditional>
|
||||
<ul className="group p-2 w-full bg-transparent menu rounded-box">
|
||||
<li tabIndex={0}>
|
||||
<span
|
||||
className={clsx(
|
||||
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
|
||||
'hover:bg-white/5 transition-colors',
|
||||
router.asPath.includes('/contracts/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
>
|
||||
<Link href="/contracts/"> Contract Dashboards </Link>
|
||||
</span>
|
||||
<ul className="z-50 p-2 bg-base-200">
|
||||
<Conditional test={BASE_FACTORY_ADDRESS !== undefined}>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/baseMinter/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/baseMinter/">Base Minter Contract</Link>
|
||||
</li>
|
||||
</Conditional>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/vendingMinter/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/vendingMinter/">Vending Minter Contract</Link>
|
||||
</li>
|
||||
<Conditional test={OPEN_EDITION_FACTORY_ADDRESS !== undefined}>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/openEditionMinter/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/openEditionMinter/">Open Edition Minter Contract</Link>
|
||||
</li>
|
||||
</Conditional>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/sg721/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/sg721/">SG721 Contract</Link>
|
||||
</li>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/whitelist/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/whitelist/">Whitelist Contract</Link>
|
||||
</li>
|
||||
<Conditional test={BADGE_HUB_ADDRESS !== undefined}>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/badgeHub/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/badgeHub/">Badge Hub Contract</Link>
|
||||
</li>
|
||||
</Conditional>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/splits/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/splits/">Splits Contract</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<IncomeDashboardDisclaimer creatorAddress={wallet.address ? wallet.address : ''} />
|
||||
<LogModal />
|
||||
{routes.map(({ text, href, isChild }) => (
|
||||
<Anchor
|
||||
key={href}
|
||||
className={clsx(
|
||||
'px-4 -mx-5 font-extrabold uppercase rounded-lg', // styling
|
||||
'hover:bg-white/5 transition-colors', // hover styling
|
||||
{ 'py-0 ml-2 text-sm font-bold': isChild },
|
||||
{
|
||||
'text-gray hover:text-white':
|
||||
router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1) !== href && isChild,
|
||||
},
|
||||
{ 'text-plumbus': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1) === href && isChild }, // active route styling
|
||||
// { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
{text}
|
||||
</Anchor>
|
||||
))}
|
||||
|
||||
<div className="flex-grow" />
|
||||
{logs.itemList.length > 0 && (
|
||||
<label
|
||||
className="w-[65%] h-[4px] text-lg font-bold text-white normal-case bg-blue-500 hover:bg-blue-600 border-none animate-none btn modal-button"
|
||||
htmlFor="my-modal-8"
|
||||
>
|
||||
View Logs
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* Stargaze network status */}
|
||||
<div className="text-sm capitalize">Network: {wallet.network}</div>
|
||||
|
||||
@@ -258,9 +72,9 @@ export const Sidebar = () => {
|
||||
{/* footer attribution */}
|
||||
<div className="text-xs text-white/50">
|
||||
Stargaze Studio {process.env.APP_VERSION} <br />
|
||||
Powered by{' '}
|
||||
<Anchor className="text-plumbus hover:underline" href="https://stargaze.zone">
|
||||
Stargaze
|
||||
Made by{' '}
|
||||
<Anchor className="text-plumbus hover:underline" href="https://deuslabs.fi">
|
||||
deus labs
|
||||
</Anchor>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const SidebarLayout = ({ children }: SidebarLayoutProps) => {
|
||||
{/* fixed component */}
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-x-visible fixed top-0 left-0 min-w-[250px] max-w-[250px] no-scrollbar',
|
||||
'overflow-auto fixed top-0 left-0 min-w-[250px] max-w-[250px] no-scrollbar',
|
||||
'border-r-[1px] border-r-plumbus-light',
|
||||
{ 'translate-x-[-230px]': !isOpen },
|
||||
)}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
|
||||
export interface SingleAssetPreviewProps {
|
||||
subtitle: ReactNode
|
||||
relatedAsset?: File
|
||||
updateMetadataFileIndex?: (index: number) => void
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const SingleAssetPreview = (props: SingleAssetPreviewProps) => {
|
||||
const { subtitle, relatedAsset, updateMetadataFileIndex, children } = props
|
||||
const [htmlContents, setHtmlContents] = useState<string>('')
|
||||
|
||||
const videoPreview = useMemo(
|
||||
() => (
|
||||
<video
|
||||
controls
|
||||
id="video"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={relatedAsset ? URL.createObjectURL(relatedAsset) : ''}
|
||||
/>
|
||||
),
|
||||
[relatedAsset],
|
||||
)
|
||||
|
||||
const audioPreview = useMemo(
|
||||
() => (
|
||||
<audio
|
||||
controls
|
||||
id="audio"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={relatedAsset ? URL.createObjectURL(relatedAsset) : ''}
|
||||
/>
|
||||
),
|
||||
[relatedAsset],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (getAssetType(relatedAsset?.name as string) !== 'html') return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (typeof e.target?.result === 'string') {
|
||||
setHtmlContents(e.target.result)
|
||||
}
|
||||
}
|
||||
reader.readAsText(new Blob([relatedAsset as File]))
|
||||
}, [relatedAsset])
|
||||
|
||||
return (
|
||||
<div className="flex p-4 pt-0 mt-11 ml-24 space-x-4 w-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<label className="flex flex-col space-y-1">
|
||||
<div>
|
||||
{/* {subtitle && <span className="text-sm text-white/50">{subtitle}</span>} */}
|
||||
{relatedAsset && (
|
||||
<div className="flex flex-row items-center mt-2 mr-4 border-2 border-dashed">
|
||||
{getAssetType(relatedAsset.name) === 'audio' && audioPreview}
|
||||
{getAssetType(relatedAsset.name) === 'video' && videoPreview}
|
||||
{getAssetType(relatedAsset.name) === 'image' && (
|
||||
<img alt="preview" src={URL.createObjectURL(relatedAsset)} />
|
||||
)}
|
||||
{getAssetType(relatedAsset.name) === 'html' && (
|
||||
<iframe allowFullScreen height="300px" srcDoc={htmlContents} title="Preview" width="100%" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-4 w-2/3">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import { usePopper } from 'react-popper'
|
||||
export interface TooltipProps extends ComponentProps<'div'> {
|
||||
label: ReactNode
|
||||
children: ReactElement
|
||||
placement?: 'top' | 'bottom' | 'left' | 'right'
|
||||
backgroundColor?: string
|
||||
}
|
||||
|
||||
export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
||||
@@ -16,7 +14,7 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: props.placement ? props.placement : 'top',
|
||||
placement: 'top',
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -34,11 +32,7 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
||||
<div
|
||||
{...props}
|
||||
{...attributes.popper}
|
||||
className={clsx(
|
||||
'py-1 px-2 m-1 text-sm rounded shadow-md',
|
||||
props.backgroundColor ? props.backgroundColor : 'bg-slate-900',
|
||||
props.className,
|
||||
)}
|
||||
className={clsx('py-1 px-2 m-1 text-sm bg-black/80 rounded shadow-md', props.className)}
|
||||
ref={setPopperElement}
|
||||
style={{ ...styles.popper, ...props.style }}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const WalletLoader = () => {
|
||||
const displayName = useWalletStore((store) => store.name || getShortAddress(store.address))
|
||||
|
||||
return (
|
||||
<Popover className="mt-4 mb-2">
|
||||
<Popover className="my-8">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<div className="grid -mx-4">
|
||||
@@ -44,7 +44,7 @@ export const WalletLoader = () => {
|
||||
>
|
||||
<Popover.Panel
|
||||
className={clsx(
|
||||
'absolute inset-x-4 z-50 mt-2',
|
||||
'absolute inset-x-4 mt-2',
|
||||
'bg-stone-800/80 rounded shadow-lg shadow-black/90 backdrop-blur-sm',
|
||||
'flex flex-col items-stretch text-sm divide-y divide-white/10',
|
||||
)}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import React, { useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { SG721_NAME_ADDRESS } from 'utils/constants'
|
||||
import { csvToFlexList } from 'utils/csvToFlexList'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { isValidFlexListFile } from 'utils/isValidFlexListFile'
|
||||
|
||||
export interface WhitelistFlexMember {
|
||||
address: string
|
||||
mint_count: number
|
||||
}
|
||||
|
||||
interface WhitelistFlexUploadProps {
|
||||
onChange: (data: WhitelistFlexMember[]) => void
|
||||
}
|
||||
|
||||
export const WhitelistFlexUpload = ({ onChange }: WhitelistFlexUploadProps) => {
|
||||
const wallet = useWallet()
|
||||
const [resolvedMemberData, setResolvedMemberData] = useState<WhitelistFlexMember[]>([])
|
||||
|
||||
const resolveMemberData = async (memberData: WhitelistFlexMember[]) => {
|
||||
if (!memberData.length) return []
|
||||
await new Promise((resolve) => {
|
||||
let i = 0
|
||||
memberData.map(async (data) => {
|
||||
if (!wallet.client) throw new Error('Wallet not connected')
|
||||
await wallet.client
|
||||
.queryContractRaw(
|
||||
SG721_NAME_ADDRESS,
|
||||
toUtf8(
|
||||
Buffer.from(
|
||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(
|
||||
data.address.trim().substring(0, data.address.lastIndexOf('.stars')),
|
||||
).toString('hex')}`,
|
||||
'hex',
|
||||
).toString(),
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
||||
if (tokenUri && isValidAddress(tokenUri))
|
||||
resolvedMemberData.push({ address: tokenUri, mint_count: Number(data.mint_count) })
|
||||
else toast.error(`Resolved address is empty or invalid for the name: ${data.address}`)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
toast.error(`Error resolving address for the name: ${data.address}`)
|
||||
})
|
||||
|
||||
i++
|
||||
if (i === memberData.length) resolve(resolvedMemberData)
|
||||
})
|
||||
})
|
||||
return resolvedMemberData
|
||||
}
|
||||
|
||||
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setResolvedMemberData([])
|
||||
if (!event.target.files) return toast.error('Error opening file')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!event.target.files[0]?.name.endsWith('.csv')) {
|
||||
toast.error('Please select a .csv file!')
|
||||
return onChange([])
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e: ProgressEvent<FileReader>) => {
|
||||
try {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
const memberData = csvToFlexList(e.target.result.toString())
|
||||
console.log(memberData)
|
||||
if (!isValidFlexListFile(memberData)) {
|
||||
event.target.value = ''
|
||||
return onChange([])
|
||||
}
|
||||
await resolveMemberData(memberData.filter((data) => data.address.trim().endsWith('.stars'))).finally(() => {
|
||||
return onChange(
|
||||
memberData
|
||||
.filter((data) => data.address.startsWith('stars') && !data.address.endsWith('.stars'))
|
||||
.map((data) => ({
|
||||
address: data.address.trim(),
|
||||
mint_count: Number(data.mint_count),
|
||||
}))
|
||||
.concat(resolvedMemberData),
|
||||
)
|
||||
})
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
}
|
||||
reader.readAsText(event.target.files[0])
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept=".csv"
|
||||
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="whitelist-flex-file"
|
||||
onChange={onFileChange}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,104 +1,24 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { useWallet } from '../contexts/wallet'
|
||||
import { SG721_NAME_ADDRESS } from '../utils/constants'
|
||||
import { isValidAddress } from '../utils/isValidAddress'
|
||||
|
||||
interface WhitelistUploadProps {
|
||||
onChange: (data: string[]) => void
|
||||
}
|
||||
|
||||
export const WhitelistUpload = ({ onChange }: WhitelistUploadProps) => {
|
||||
const wallet = useWallet()
|
||||
const [resolvedAddresses, setResolvedAddresses] = useState<string[]>([])
|
||||
|
||||
const resolveAddresses = async (names: string[]) => {
|
||||
await new Promise((resolve) => {
|
||||
let i = 0
|
||||
names.map(async (name) => {
|
||||
if (!wallet.client) throw new Error('Wallet not connected')
|
||||
await wallet.client
|
||||
.queryContractRaw(
|
||||
SG721_NAME_ADDRESS,
|
||||
toUtf8(
|
||||
Buffer.from(
|
||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
|
||||
'hex',
|
||||
).toString(),
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
||||
if (tokenUri && isValidAddress(tokenUri)) resolvedAddresses.push(tokenUri)
|
||||
else toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
toast.error(`Error resolving address for the name: ${name}.stars`)
|
||||
})
|
||||
|
||||
i++
|
||||
if (i === names.length) resolve(resolvedAddresses)
|
||||
})
|
||||
})
|
||||
return resolvedAddresses
|
||||
}
|
||||
|
||||
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setResolvedAddresses([])
|
||||
if (!event.target.files) return toast.error('Error opening file')
|
||||
if (event.target.files.length !== 1) {
|
||||
toast.error('No file selected')
|
||||
return onChange([])
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (event.target.files[0]?.type !== 'text/plain') {
|
||||
toast.error('Invalid file type')
|
||||
return onChange([])
|
||||
}
|
||||
if (event.target.files[0].type !== 'text/plain') return toast.error('Invalid file type')
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e: ProgressEvent<FileReader>) => {
|
||||
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||
const text = e.target?.result?.toString()
|
||||
let newline = '\n'
|
||||
if (text?.includes('\r')) newline = '\r'
|
||||
if (text?.includes('\r\n')) newline = '\r\n'
|
||||
const data = text?.split(newline)
|
||||
|
||||
const cleanText = text?.toLowerCase().replace(/,/g, '').replace(/"/g, '').replace(/'/g, '').replace(/ /g, '')
|
||||
const data = cleanText?.split(newline)
|
||||
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 printableData = data?.map((item) => item.replace(regex, ''))
|
||||
const names = printableData?.filter((address) => address !== '' && address.endsWith('.stars'))
|
||||
const strippedNames = names?.map((name) => name.split('.')[0])
|
||||
console.log(names)
|
||||
if (strippedNames?.length) {
|
||||
await toast
|
||||
.promise(resolveAddresses(strippedNames), {
|
||||
loading: 'Resolving addresses...',
|
||||
success: 'Address resolution finalized.',
|
||||
error: 'Address resolution failed!',
|
||||
})
|
||||
.then((addresses) => {
|
||||
console.log(addresses)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
return onChange([
|
||||
...new Set(
|
||||
printableData
|
||||
?.filter((address) => address !== '' && isValidAddress(address) && address.startsWith('stars'))
|
||||
.concat(resolvedAddresses) || [],
|
||||
),
|
||||
])
|
||||
return onChange([...new Set(data?.filter((address) => address !== '') || [])])
|
||||
}
|
||||
reader.readAsText(event.target.files[0])
|
||||
}
|
||||
@@ -117,7 +37,7 @@ export const WhitelistUpload = ({ onChange }: WhitelistUploadProps) => {
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="whitelist-file"
|
||||
multiple={false}
|
||||
multiple
|
||||
onChange={onFileChange}
|
||||
type="file"
|
||||
/>
|
||||
|
||||
@@ -1,635 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
// import { AirdropUpload } from 'components/AirdropUpload'
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import { Alert } from 'components/Alert'
|
||||
import type { DispatchExecuteArgs } from 'components/badges/actions/actions'
|
||||
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions'
|
||||
import { ActionsCombobox } from 'components/badges/actions/Combobox'
|
||||
import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks'
|
||||
import { Button } from 'components/Button'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { AddressList } from 'components/forms/AddressList'
|
||||
import { useAddressListState } from 'components/forms/AddressList.hooks'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { MetadataAttributes } from 'components/forms/MetadataAttributes'
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { WhitelistUpload } from 'components/WhitelistUpload'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
|
||||
import sizeof from 'object-sizeof'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { FaArrowRight } from 'react-icons/fa'
|
||||
import { useMutation } from 'react-query'
|
||||
import * as secp256k1 from 'secp256k1'
|
||||
import { generateKeyPairs, sha256 } from 'utils/hash'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
|
||||
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
||||
|
||||
interface BadgeActionsProps {
|
||||
badgeHubContractAddress: string
|
||||
badgeId: number
|
||||
badgeHubMessages: BadgeHubInstance | undefined
|
||||
mintRule: MintRule
|
||||
}
|
||||
|
||||
type TransferrableType = true | false | undefined
|
||||
|
||||
export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeActionsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [airdropAllocationArray, setAirdropAllocationArray] = useState<string[]>([])
|
||||
const [badge, setBadge] = useState<Badge>()
|
||||
const [transferrable, setTransferrable] = useState<TransferrableType>(undefined)
|
||||
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
||||
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
||||
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
||||
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
|
||||
const [signature, setSignature] = useState<string>('')
|
||||
const [ownerList, setOwnerList] = useState<string[]>([])
|
||||
const [numberOfKeys, setNumberOfKeys] = useState(0)
|
||||
|
||||
const actionComboboxState = useActionsComboboxState()
|
||||
const type = actionComboboxState.value?.id
|
||||
|
||||
const maxSupplyState = useNumberInputState({
|
||||
id: 'max-supply',
|
||||
name: 'max-supply',
|
||||
title: 'Max Supply',
|
||||
subtitle: 'Maximum number of badges that can be minted',
|
||||
})
|
||||
|
||||
// Metadata related fields
|
||||
const managerState = useInputState({
|
||||
id: 'manager-address',
|
||||
name: 'manager',
|
||||
title: 'Manager',
|
||||
subtitle: 'Badge Hub Manager',
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'metadata-name',
|
||||
name: 'metadata-name',
|
||||
title: 'Name',
|
||||
subtitle: 'Name of the badge',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
id: 'metadata-description',
|
||||
name: 'metadata-description',
|
||||
title: 'Description',
|
||||
subtitle: 'Description of the badge',
|
||||
})
|
||||
|
||||
const imageState = useInputState({
|
||||
id: 'metadata-image',
|
||||
name: 'metadata-image',
|
||||
title: 'Image',
|
||||
subtitle: 'Badge Image URL',
|
||||
})
|
||||
|
||||
const imageDataState = useInputState({
|
||||
id: 'metadata-image-data',
|
||||
name: 'metadata-image-data',
|
||||
title: 'Image Data',
|
||||
subtitle: 'Raw SVG image data',
|
||||
})
|
||||
|
||||
const externalUrlState = useInputState({
|
||||
id: 'metadata-external-url',
|
||||
name: 'metadata-external-url',
|
||||
title: 'External URL',
|
||||
subtitle: 'External URL for the badge',
|
||||
})
|
||||
|
||||
const attributesState = useMetadataAttributesState()
|
||||
|
||||
const backgroundColorState = useInputState({
|
||||
id: 'metadata-background-color',
|
||||
name: 'metadata-background-color',
|
||||
title: 'Background Color',
|
||||
subtitle: 'Background color of the badge',
|
||||
})
|
||||
|
||||
const animationUrlState = useInputState({
|
||||
id: 'metadata-animation-url',
|
||||
name: 'metadata-animation-url',
|
||||
title: 'Animation URL',
|
||||
subtitle: 'Animation URL for the badge',
|
||||
})
|
||||
|
||||
const youtubeUrlState = useInputState({
|
||||
id: 'metadata-youtube-url',
|
||||
name: 'metadata-youtube-url',
|
||||
title: 'YouTube URL',
|
||||
subtitle: 'YouTube URL for the badge',
|
||||
})
|
||||
// Rules related fields
|
||||
const keyState = useInputState({
|
||||
id: 'key',
|
||||
name: 'key',
|
||||
title: 'Key',
|
||||
subtitle: 'The key generated for the badge',
|
||||
})
|
||||
|
||||
const ownerState = useInputState({
|
||||
id: 'owner-address',
|
||||
name: 'owner',
|
||||
title: 'Owner',
|
||||
subtitle: 'The owner of the badge',
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const ownerListState = useAddressListState()
|
||||
|
||||
const pubKeyState = useInputState({
|
||||
id: 'pubKey',
|
||||
name: 'pubKey',
|
||||
title: 'Public Key',
|
||||
subtitle:
|
||||
type === 'mint_by_keys'
|
||||
? 'The whitelisted public key authorized to mint a badge'
|
||||
: 'The public key to check whether it can be used to mint a badge',
|
||||
})
|
||||
|
||||
const privateKeyState = useInputState({
|
||||
id: 'privateKey',
|
||||
name: 'privateKey',
|
||||
title: 'Private Key',
|
||||
subtitle:
|
||||
type === 'mint_by_keys'
|
||||
? 'The corresponding private key for the whitelisted public key'
|
||||
: 'The private key that was generated during badge creation',
|
||||
})
|
||||
|
||||
const nftState = useInputState({
|
||||
id: 'nft-address',
|
||||
name: 'nft-address',
|
||||
title: 'NFT Contract Address',
|
||||
subtitle: 'The NFT Contract Address for the badge',
|
||||
})
|
||||
|
||||
const limitState = useNumberInputState({
|
||||
id: 'limit',
|
||||
name: 'limit',
|
||||
title: 'Limit',
|
||||
subtitle: 'Number of keys/owners to execute the action for (0 for all)',
|
||||
})
|
||||
|
||||
const showMetadataField = isEitherType(type, ['edit_badge'])
|
||||
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'mint_by_minter'])
|
||||
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key'])
|
||||
const showAirdropFileField = isEitherType(type, ['airdrop_by_key'])
|
||||
const showOwnerList = isEitherType(type, ['mint_by_minter'])
|
||||
const showPubKeyField = isEitherType(type, ['mint_by_keys'])
|
||||
const showLimitState = isEitherType(type, ['purge_keys', 'purge_owners'])
|
||||
|
||||
const payload: DispatchExecuteArgs = {
|
||||
badge: {
|
||||
manager: badge?.manager || managerState.value,
|
||||
metadata: {
|
||||
name: nameState.value || undefined,
|
||||
description: descriptionState.value || undefined,
|
||||
image: imageState.value || undefined,
|
||||
image_data: imageDataState.value || undefined,
|
||||
external_url: externalUrlState.value || undefined,
|
||||
attributes:
|
||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
||||
? attributesState.values
|
||||
.map((attr) => ({
|
||||
trait_type: attr.trait_type,
|
||||
value: attr.value,
|
||||
}))
|
||||
.filter((attr) => attr.trait_type && attr.value)
|
||||
: undefined,
|
||||
background_color: backgroundColorState.value || undefined,
|
||||
animation_url: animationUrlState.value || undefined,
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable: transferrable === true,
|
||||
rule: {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
},
|
||||
metadata: {
|
||||
name: nameState.value || undefined,
|
||||
description: descriptionState.value || undefined,
|
||||
image: imageState.value || undefined,
|
||||
image_data: imageDataState.value || undefined,
|
||||
external_url: externalUrlState.value || undefined,
|
||||
attributes:
|
||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
||||
? attributesState.values
|
||||
.map((attr) => ({
|
||||
trait_type: attr.trait_type,
|
||||
value: attr.value,
|
||||
}))
|
||||
.filter((attr) => attr.trait_type && attr.value)
|
||||
: undefined,
|
||||
background_color: backgroundColorState.value || undefined,
|
||||
animation_url: animationUrlState.value || undefined,
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
id: badgeId,
|
||||
editFee,
|
||||
owner: resolvedOwnerAddress,
|
||||
pubkey: pubKeyState.value,
|
||||
signature,
|
||||
keys: keyPairs.map((keyPair) => keyPair.publicKey),
|
||||
limit: limitState.value || undefined,
|
||||
owners: [
|
||||
...new Set(
|
||||
ownerListState.values
|
||||
.map((a) => a.address.trim())
|
||||
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
|
||||
.concat(ownerList),
|
||||
),
|
||||
],
|
||||
recipients: airdropAllocationArray,
|
||||
privateKey: privateKeyState.value,
|
||||
nft: nftState.value,
|
||||
badgeHubMessages,
|
||||
badgeHubContract: badgeHubContractAddress,
|
||||
txSigner: wallet.address,
|
||||
type,
|
||||
}
|
||||
|
||||
const resolveOwnerAddress = async () => {
|
||||
await resolveAddress(ownerState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setResolvedOwnerAddress(resolvedAddress)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
void resolveOwnerAddress()
|
||||
}, [ownerState.value])
|
||||
|
||||
const resolveManagerAddress = async () => {
|
||||
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setBadge({
|
||||
manager: resolvedAddress,
|
||||
metadata: {
|
||||
name: nameState.value || undefined,
|
||||
description: descriptionState.value || undefined,
|
||||
image: imageState.value || undefined,
|
||||
image_data: imageDataState.value || undefined,
|
||||
external_url: externalUrlState.value || undefined,
|
||||
attributes:
|
||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
||||
? attributesState.values
|
||||
.map((attr) => ({
|
||||
trait_type: attr.trait_type,
|
||||
value: attr.value,
|
||||
}))
|
||||
.filter((attr) => attr.trait_type && attr.value)
|
||||
: undefined,
|
||||
background_color: backgroundColorState.value || undefined,
|
||||
animation_url: animationUrlState.value || undefined,
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable: transferrable === true,
|
||||
rule: {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void resolveManagerAddress()
|
||||
}, [managerState.value])
|
||||
|
||||
useEffect(() => {
|
||||
setBadge({
|
||||
manager: managerState.value,
|
||||
metadata: {
|
||||
name: nameState.value || undefined,
|
||||
description: descriptionState.value || undefined,
|
||||
image: imageState.value || undefined,
|
||||
image_data: imageDataState.value || undefined,
|
||||
external_url: externalUrlState.value || undefined,
|
||||
attributes:
|
||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
||||
? attributesState.values
|
||||
.map((attr) => ({
|
||||
trait_type: attr.trait_type,
|
||||
value: attr.value,
|
||||
}))
|
||||
.filter((attr) => attr.trait_type && attr.value)
|
||||
: undefined,
|
||||
background_color: backgroundColorState.value || undefined,
|
||||
animation_url: animationUrlState.value || undefined,
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable: transferrable === true,
|
||||
rule: {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
})
|
||||
}, [
|
||||
nameState.value,
|
||||
descriptionState.value,
|
||||
imageState.value,
|
||||
imageDataState.value,
|
||||
externalUrlState.value,
|
||||
attributesState.values,
|
||||
backgroundColorState.value,
|
||||
animationUrlState.value,
|
||||
youtubeUrlState.value,
|
||||
transferrable,
|
||||
keyState.value,
|
||||
timestamp,
|
||||
maxSupplyState.value,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (attributesState.values.length === 0)
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void dispatchEditBadgeMessage().catch((err) => {
|
||||
toast.error(String(err), { style: { maxWidth: 'none' } })
|
||||
})
|
||||
}, [triggerDispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
|
||||
handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value)
|
||||
}, [privateKeyState.value, resolvedOwnerAddress])
|
||||
|
||||
useEffect(() => {
|
||||
if (numberOfKeys > 0) {
|
||||
setKeyPairs(generateKeyPairs(numberOfKeys))
|
||||
}
|
||||
}, [numberOfKeys])
|
||||
|
||||
const handleDownloadKeys = () => {
|
||||
const element = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
|
||||
element.href = URL.createObjectURL(file)
|
||||
element.download = `badge-${badgeId.toString()}-keys.json`
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
}
|
||||
|
||||
const { isLoading, mutate } = useMutation(
|
||||
async (event: FormEvent) => {
|
||||
if (!wallet.client) {
|
||||
throw new Error('Please connect your wallet.')
|
||||
}
|
||||
event.preventDefault()
|
||||
if (!type) {
|
||||
throw new Error('Please select an action.')
|
||||
}
|
||||
if (badgeHubContractAddress === '') {
|
||||
throw new Error('Please enter the Badge Hub contract addresses.')
|
||||
}
|
||||
|
||||
if (type === 'mint_by_key' && privateKeyState.value.length !== 64) {
|
||||
throw new Error('Please enter a valid private key.')
|
||||
}
|
||||
|
||||
if (wallet.client && type === 'edit_badge') {
|
||||
const feeRateRaw = await wallet.client.queryContractRaw(
|
||||
badgeHubContractAddress,
|
||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
||||
|
||||
await toast
|
||||
.promise(
|
||||
wallet.client.queryContractSmart(badgeHubContractAddress, {
|
||||
badge: { id: badgeId },
|
||||
}),
|
||||
{
|
||||
error: `Edit Fee calculation failed!`,
|
||||
loading: 'Calculating Edit Fee...',
|
||||
success: (currentBadge) => {
|
||||
console.log('Current badge: ', currentBadge)
|
||||
return `Current metadata is ${
|
||||
Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes))
|
||||
} bytes in size.`
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((currentBadge) => {
|
||||
// TODO - Go over the calculation
|
||||
const currentBadgeMetadataSize =
|
||||
Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes) * 2)
|
||||
console.log('Current badge metadata size: ', currentBadgeMetadataSize)
|
||||
const newBadgeMetadataSize =
|
||||
Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) * 2
|
||||
console.log('New badge metadata size: ', newBadgeMetadataSize)
|
||||
if (newBadgeMetadataSize > currentBadgeMetadataSize) {
|
||||
const calculatedFee = ((newBadgeMetadataSize - currentBadgeMetadataSize) * Number(feeRate.metadata)) / 2
|
||||
setEditFee(calculatedFee)
|
||||
setTriggerDispatch(!triggerDispatch)
|
||||
} else {
|
||||
setEditFee(undefined)
|
||||
setTriggerDispatch(!triggerDispatch)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7))
|
||||
})
|
||||
} else {
|
||||
const txHash = await toast.promise(dispatchExecute(payload), {
|
||||
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
|
||||
loading: 'Executing message...',
|
||||
success: (tx) => `Transaction ${tx} success!`,
|
||||
})
|
||||
if (txHash) {
|
||||
setLastTx(txHash)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(String(error), { style: { maxWidth: 'none' } })
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const dispatchEditBadgeMessage = async () => {
|
||||
if (type) {
|
||||
const txHash = await toast.promise(dispatchExecute(payload), {
|
||||
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
|
||||
loading: 'Executing message...',
|
||||
success: (tx) => `Transaction ${tx} success!`,
|
||||
})
|
||||
if (txHash) {
|
||||
setLastTx(txHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const airdropFileOnChange = (data: string[]) => {
|
||||
console.log(data)
|
||||
setAirdropAllocationArray(data)
|
||||
}
|
||||
|
||||
const handleGenerateSignature = (id: number, owner: string, privateKey: string) => {
|
||||
try {
|
||||
const message = `claim badge ${id} for user ${owner}`
|
||||
|
||||
const privKey = Buffer.from(privateKey, 'hex')
|
||||
// const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true))
|
||||
const msgBytes = Buffer.from(message, 'utf8')
|
||||
const msgHashBytes = sha256(msgBytes)
|
||||
const signedMessage = secp256k1.ecdsaSign(msgHashBytes, privKey)
|
||||
setSignature(Buffer.from(signedMessage.signature).toString('hex'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
toast.error('Error generating signature.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="grid grid-cols-2 mt-4">
|
||||
<div className="mr-2">
|
||||
<ActionsCombobox mintRule={mintRule} {...actionComboboxState} />
|
||||
{showMetadataField && (
|
||||
<div className="p-4 mt-2 rounded-md border-2 border-gray-800">
|
||||
<span className="text-gray-400">Metadata</span>
|
||||
<TextInput className="mt-2" {...nameState} />
|
||||
<TextInput className="mt-2" {...descriptionState} />
|
||||
<TextInput className="mt-2" {...imageState} />
|
||||
<TextInput className="mt-2" {...imageDataState} />
|
||||
<TextInput className="mt-2" {...externalUrlState} />
|
||||
<div className="mt-2">
|
||||
<MetadataAttributes
|
||||
attributes={attributesState.entries}
|
||||
onAdd={attributesState.add}
|
||||
onChange={attributesState.update}
|
||||
onRemove={attributesState.remove}
|
||||
title="Traits"
|
||||
/>
|
||||
</div>
|
||||
<TextInput className="mt-2" {...backgroundColorState} />
|
||||
<TextInput className="mt-2" {...animationUrlState} />
|
||||
<TextInput className="mt-2" {...youtubeUrlState} />
|
||||
</div>
|
||||
)}
|
||||
{showOwnerField && (
|
||||
<AddressInput
|
||||
className="mt-2"
|
||||
{...ownerState}
|
||||
subtitle="The address that the badge will be minted to"
|
||||
title="Owner"
|
||||
/>
|
||||
)}
|
||||
{showPubKeyField && <TextInput className="mt-2" {...pubKeyState} />}
|
||||
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
|
||||
{showLimitState && <NumberInput className="mt-2" {...limitState} />}
|
||||
<Conditional test={isEitherType(type, ['purge_owners', 'purge_keys'])}>
|
||||
<Alert className="mt-4" type="info">
|
||||
This action is only available if the badge with the specified id is either minted out or expired.
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={type === 'add_keys'}>
|
||||
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
|
||||
<div className="grid grid-cols-2 gap-24">
|
||||
<div className="flex flex-col ml-4">
|
||||
<span className="font-bold">Number of Keys</span>
|
||||
<span className="text-sm text-white/80">
|
||||
The number of public keys to be whitelisted for minting badges
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
|
||||
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
|
||||
required
|
||||
type="number"
|
||||
value={numberOfKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
|
||||
<Alert type="info">
|
||||
<div className="pt-2">
|
||||
<span className="mt-2">
|
||||
Make sure to download the whitelisted public keys together with their private key counterparts.
|
||||
</span>
|
||||
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
|
||||
Download Key Pairs
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={showOwnerList}>
|
||||
<div className="mt-4">
|
||||
<AddressList
|
||||
entries={ownerListState.entries}
|
||||
isRequired
|
||||
onAdd={ownerListState.add}
|
||||
onChange={ownerListState.update}
|
||||
onRemove={ownerListState.remove}
|
||||
subtitle="Enter the owner addresses"
|
||||
title="Addresses"
|
||||
/>
|
||||
<Alert className="mt-8" type="info">
|
||||
You may optionally choose a text file of additional owner addresses.
|
||||
</Alert>
|
||||
<WhitelistUpload onChange={setOwnerList} />
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
{showAirdropFileField && (
|
||||
<FormGroup
|
||||
subtitle="TXT file that contains the addresses to airdrop a badge for"
|
||||
title="Badge Airdrop List File"
|
||||
>
|
||||
<BadgeAirdropListUpload onChange={airdropFileOnChange} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-6">
|
||||
<div className="relative mb-2">
|
||||
<Button
|
||||
className="absolute top-0 right-0"
|
||||
isLoading={isLoading}
|
||||
onClick={mutate}
|
||||
rightIcon={<FaArrowRight />}
|
||||
>
|
||||
Execute
|
||||
</Button>
|
||||
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
|
||||
<TransactionHash hash={lastTx} />
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormControl subtitle="View current message to be sent" title="Payload Preview">
|
||||
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { ActionListItem } from './actions'
|
||||
|
||||
export const useActionsComboboxState = () => {
|
||||
const [value, setValue] = useState<ActionListItem | null>(null)
|
||||
return { value, onChange: (item: ActionListItem) => setValue(item) }
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
||||
import type { ActionListItem } from './actions'
|
||||
import { BY_KEY_ACTION_LIST, BY_KEYS_ACTION_LIST, BY_MINTER_ACTION_LIST } from './actions'
|
||||
|
||||
export interface ActionsComboboxProps {
|
||||
value: ActionListItem | null
|
||||
onChange: (item: ActionListItem) => void
|
||||
mintRule?: MintRule
|
||||
}
|
||||
|
||||
export const ActionsCombobox = ({ value, onChange, mintRule }: ActionsComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
const [ACTION_LIST, SET_ACTION_LIST] = useState<ActionListItem[]>(BY_KEY_ACTION_LIST)
|
||||
|
||||
useEffect(() => {
|
||||
if (mintRule === 'by_keys') {
|
||||
SET_ACTION_LIST(BY_KEYS_ACTION_LIST)
|
||||
} else if (mintRule === 'by_minter') {
|
||||
SET_ACTION_LIST(BY_MINTER_ACTION_LIST)
|
||||
} else {
|
||||
SET_ACTION_LIST(BY_KEY_ACTION_LIST)
|
||||
}
|
||||
}, [mintRule])
|
||||
|
||||
const filtered =
|
||||
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={FormControl}
|
||||
htmlId="action"
|
||||
labelAs={Combobox.Label}
|
||||
onChange={onChange}
|
||||
subtitle="Badge actions"
|
||||
title=""
|
||||
value={value}
|
||||
>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className={clsx(
|
||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
displayValue={(val?: ActionListItem) => val?.name ?? ''}
|
||||
id="message-type"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Select action"
|
||||
/>
|
||||
|
||||
<Combobox.Button
|
||||
className={clsx(
|
||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||
)}
|
||||
>
|
||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||
<Combobox.Options
|
||||
className={clsx(
|
||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||
'divide-y divide-stone-500/50',
|
||||
)}
|
||||
>
|
||||
{filtered.length < 1 && (
|
||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||
Action not found
|
||||
</span>
|
||||
)}
|
||||
{filtered.map((entry) => (
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
<span className="font-bold">{entry.name}</span>
|
||||
<span className="max-w-md text-sm">{entry.description}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
{value && (
|
||||
<div className="flex space-x-2 text-white/50">
|
||||
<div className="mt-1">
|
||||
<FaInfoCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm">{value.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import type { Badge, BadgeHubInstance, Metadata } from 'contracts/badgeHub'
|
||||
import { useBadgeHubContract } from 'contracts/badgeHub'
|
||||
|
||||
export type ActionType = typeof ACTION_TYPES[number]
|
||||
|
||||
export const ACTION_TYPES = [
|
||||
'create_badge',
|
||||
'edit_badge',
|
||||
'add_keys',
|
||||
'purge_keys',
|
||||
'purge_owners',
|
||||
'mint_by_minter',
|
||||
'mint_by_key',
|
||||
'airdrop_by_key',
|
||||
'mint_by_keys',
|
||||
'set_nft',
|
||||
] as const
|
||||
|
||||
export interface ActionListItem {
|
||||
id: ActionType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const BY_KEY_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'edit_badge',
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_key',
|
||||
name: 'Mint by Key',
|
||||
description: `Mint a badge to a specified address`,
|
||||
},
|
||||
{
|
||||
id: 'airdrop_by_key',
|
||||
name: 'Airdrop by Key',
|
||||
description: `Airdrop badges to a list of specified addresses`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
]
|
||||
|
||||
export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'edit_badge',
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_keys',
|
||||
name: 'Mint by Keys',
|
||||
description: `Mint a new badge with a whitelisted private key`,
|
||||
},
|
||||
{
|
||||
id: 'add_keys',
|
||||
name: 'Add Keys',
|
||||
description: `Add keys to the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_keys',
|
||||
name: 'Purge Keys',
|
||||
description: `Purge keys from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
]
|
||||
|
||||
export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'edit_badge',
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_minter',
|
||||
name: 'Mint by Minter',
|
||||
description: `Mint a new badge to specified owner addresses`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
type: ActionType
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
type Select<T extends ActionType> = T
|
||||
|
||||
/** @see {@link BadgeHubInstance}*/
|
||||
export type DispatchExecuteArgs = {
|
||||
badgeHubContract: string
|
||||
badgeHubMessages?: BadgeHubInstance
|
||||
txSigner: string
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'create_badge'>; badge: Badge }
|
||||
| { type: Select<'edit_badge'>; id: number; metadata: Metadata; editFee?: number }
|
||||
| { type: Select<'add_keys'>; id: number; keys: string[] }
|
||||
| { type: Select<'purge_keys'>; id: number; limit?: number }
|
||||
| { type: Select<'purge_owners'>; id: number; limit?: number }
|
||||
| { type: Select<'mint_by_minter'>; id: number; owners: string[] }
|
||||
| { type: Select<'mint_by_key'>; id: number; owner: string; signature: string }
|
||||
| { type: Select<'airdrop_by_key'>; id: number; recipients: string[]; privateKey: string }
|
||||
| { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string }
|
||||
| { type: Select<'set_nft'>; nft: string }
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { badgeHubMessages, txSigner } = args
|
||||
if (!badgeHubMessages) {
|
||||
throw new Error('Cannot execute actions')
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'create_badge': {
|
||||
return badgeHubMessages.createBadge(txSigner, args.badge)
|
||||
}
|
||||
case 'edit_badge': {
|
||||
return badgeHubMessages.editBadge(txSigner, args.id, args.metadata, args.editFee)
|
||||
}
|
||||
case 'add_keys': {
|
||||
return badgeHubMessages.addKeys(txSigner, args.id, args.keys)
|
||||
}
|
||||
case 'purge_keys': {
|
||||
return badgeHubMessages.purgeKeys(txSigner, args.id, args.limit)
|
||||
}
|
||||
case 'purge_owners': {
|
||||
return badgeHubMessages.purgeOwners(txSigner, args.id, args.limit)
|
||||
}
|
||||
case 'mint_by_minter': {
|
||||
return badgeHubMessages.mintByMinter(txSigner, args.id, args.owners)
|
||||
}
|
||||
case 'mint_by_key': {
|
||||
return badgeHubMessages.mintByKey(txSigner, args.id, args.owner, args.signature)
|
||||
}
|
||||
case 'airdrop_by_key': {
|
||||
return badgeHubMessages.airdropByKey(txSigner, args.id, args.recipients, args.privateKey)
|
||||
}
|
||||
case 'mint_by_keys': {
|
||||
return badgeHubMessages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature)
|
||||
}
|
||||
case 'set_nft': {
|
||||
return badgeHubMessages.setNft(txSigner, args.nft)
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown action')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: badgeHubMessages } = useBadgeHubContract()
|
||||
|
||||
const { badgeHubContract } = args
|
||||
switch (args.type) {
|
||||
case 'create_badge': {
|
||||
return badgeHubMessages(badgeHubContract)?.createBadge(args.badge)
|
||||
}
|
||||
case 'edit_badge': {
|
||||
return badgeHubMessages(badgeHubContract)?.editBadge(args.id, args.metadata)
|
||||
}
|
||||
case 'add_keys': {
|
||||
return badgeHubMessages(badgeHubContract)?.addKeys(args.id, args.keys)
|
||||
}
|
||||
case 'purge_keys': {
|
||||
return badgeHubMessages(badgeHubContract)?.purgeKeys(args.id, args.limit)
|
||||
}
|
||||
case 'purge_owners': {
|
||||
return badgeHubMessages(badgeHubContract)?.purgeOwners(args.id, args.limit)
|
||||
}
|
||||
case 'mint_by_minter': {
|
||||
return badgeHubMessages(badgeHubContract)?.mintByMinter(args.id, args.owners)
|
||||
}
|
||||
case 'mint_by_key': {
|
||||
return badgeHubMessages(badgeHubContract)?.mintByKey(args.id, args.owner, args.signature)
|
||||
}
|
||||
case 'airdrop_by_key': {
|
||||
return badgeHubMessages(badgeHubContract)?.airdropByKey(args.id, args.recipients, args.privateKey)
|
||||
}
|
||||
case 'mint_by_keys': {
|
||||
return badgeHubMessages(badgeHubContract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature)
|
||||
}
|
||||
case 'set_nft': {
|
||||
return badgeHubMessages(badgeHubContract)?.setNft(args.nft)
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isEitherType = <T extends ActionType>(type: unknown, arr: T[]): type is T => {
|
||||
return arr.some((val) => type === val)
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Trait } from 'contracts/badgeHub'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { BADGE_HUB_ADDRESS } from 'utils/constants'
|
||||
|
||||
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
||||
import { MetadataAttributes } from '../../forms/MetadataAttributes'
|
||||
import { Tooltip } from '../../Tooltip'
|
||||
import type { MintRule, UploadMethod } from './ImageUploadDetails'
|
||||
|
||||
interface BadgeDetailsProps {
|
||||
onChange: (data: BadgeDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod | undefined
|
||||
mintRule: MintRule
|
||||
metadataSize: number
|
||||
}
|
||||
|
||||
export interface BadgeDetailsDataProps {
|
||||
manager: string
|
||||
name?: string
|
||||
description?: string
|
||||
attributes?: Trait[]
|
||||
expiry?: number
|
||||
transferrable: boolean
|
||||
max_supply?: number
|
||||
image_data?: string
|
||||
external_url?: string
|
||||
background_color?: string
|
||||
animation_url?: string
|
||||
youtube_url?: string
|
||||
}
|
||||
|
||||
export const BadgeDetails = ({ metadataSize, onChange }: BadgeDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [transferrable, setTransferrable] = useState<boolean>(false)
|
||||
const [metadataFile, setMetadataFile] = useState<File>()
|
||||
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
|
||||
|
||||
const metadataFileRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const managerState = useInputState({
|
||||
id: 'manager-address',
|
||||
name: 'manager',
|
||||
title: 'Manager',
|
||||
subtitle: 'Badge Hub Manager',
|
||||
defaultValue: wallet.address ? wallet.address : '',
|
||||
})
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
placeholder: 'My Awesome Collection',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
id: 'description',
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
placeholder: 'My Awesome Collection Description',
|
||||
})
|
||||
|
||||
const imageDataState = useInputState({
|
||||
id: 'metadata-image-data',
|
||||
name: 'metadata-image-data',
|
||||
title: 'Image Data',
|
||||
subtitle: 'Raw SVG image data',
|
||||
})
|
||||
|
||||
const externalUrlState = useInputState({
|
||||
id: 'metadata-external-url',
|
||||
name: 'metadata-external-url',
|
||||
title: 'External URL',
|
||||
subtitle: 'External URL for the badge',
|
||||
})
|
||||
|
||||
const attributesState = useMetadataAttributesState()
|
||||
|
||||
const maxSupplyState = useNumberInputState({
|
||||
id: 'max-supply',
|
||||
name: 'max-supply',
|
||||
title: 'Max Supply',
|
||||
subtitle: 'Maximum number of badges that can be minted',
|
||||
})
|
||||
|
||||
const backgroundColorState = useInputState({
|
||||
id: 'metadata-background-color',
|
||||
name: 'metadata-background-color',
|
||||
title: 'Background Color',
|
||||
subtitle: 'Background color of the badge',
|
||||
})
|
||||
|
||||
const animationUrlState = useInputState({
|
||||
id: 'metadata-animation-url',
|
||||
name: 'metadata-animation-url',
|
||||
title: 'Animation URL',
|
||||
subtitle: 'Animation URL for the badge',
|
||||
})
|
||||
|
||||
const youtubeUrlState = useInputState({
|
||||
id: 'metadata-youtube-url',
|
||||
name: 'metadata-youtube-url',
|
||||
title: 'YouTube URL',
|
||||
subtitle: 'YouTube URL for the badge',
|
||||
})
|
||||
|
||||
const parseMetadata = async () => {
|
||||
try {
|
||||
let parsedMetadata: any
|
||||
if (metadataFile) {
|
||||
attributesState.reset()
|
||||
parsedMetadata = JSON.parse(await metadataFile.text())
|
||||
|
||||
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
|
||||
attributesState.add({
|
||||
trait_type: parsedMetadata.attributes[i].trait_type,
|
||||
value: parsedMetadata.attributes[i].value,
|
||||
})
|
||||
}
|
||||
}
|
||||
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
|
||||
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
|
||||
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
|
||||
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
|
||||
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
|
||||
backgroundColorState.onChange(parsedMetadata.background_color ? parsedMetadata.background_color : '')
|
||||
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
|
||||
} else {
|
||||
attributesState.reset()
|
||||
nameState.onChange('')
|
||||
descriptionState.onChange('')
|
||||
externalUrlState.onChange('')
|
||||
youtubeUrlState.onChange('')
|
||||
animationUrlState.onChange('')
|
||||
backgroundColorState.onChange('')
|
||||
imageDataState.onChange('')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error parsing metadata file: Invalid JSON format.')
|
||||
if (metadataFileRef.current) metadataFileRef.current.value = ''
|
||||
setMetadataFile(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMetadataFile(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: 'application/json' })
|
||||
}
|
||||
// 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.')
|
||||
setMetadataFile(selectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void parseMetadata()
|
||||
if (!metadataFile)
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
}, [metadataFile])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: BadgeDetailsDataProps = {
|
||||
manager: managerState.value,
|
||||
name: nameState.value || undefined,
|
||||
description: descriptionState.value || undefined,
|
||||
attributes:
|
||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
||||
? attributesState.values
|
||||
.map((attr) => ({
|
||||
trait_type: attr.trait_type,
|
||||
value: attr.value,
|
||||
}))
|
||||
.filter((attr) => attr.trait_type && attr.value)
|
||||
: undefined,
|
||||
expiry: timestamp ? timestamp.getTime() / 1000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
transferrable,
|
||||
image_data: imageDataState.value || undefined,
|
||||
external_url: externalUrlState.value || undefined,
|
||||
background_color: backgroundColorState.value || undefined,
|
||||
animation_url: animationUrlState.value || undefined,
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
managerState.value,
|
||||
nameState.value,
|
||||
descriptionState.value,
|
||||
timestamp,
|
||||
maxSupplyState.value,
|
||||
transferrable,
|
||||
imageDataState.value,
|
||||
externalUrlState.value,
|
||||
attributesState.values,
|
||||
backgroundColorState.value,
|
||||
animationUrlState.value,
|
||||
youtubeUrlState.value,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const retrieveFeeRate = async () => {
|
||||
try {
|
||||
if (wallet.client) {
|
||||
const feeRateRaw = await wallet.client.queryContractRaw(
|
||||
BADGE_HUB_ADDRESS,
|
||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
console.log('Fee Rate Raw: ', feeRateRaw)
|
||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
||||
setMetadataFeeRate(Number(feeRate.metadata))
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error retrieving metadata fee rate.')
|
||||
setMetadataFeeRate(0)
|
||||
console.log('Error retrieving fee rate: ', error)
|
||||
}
|
||||
}
|
||||
void retrieveFeeRate()
|
||||
}, [wallet.client])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={clsx('grid grid-cols-2 ml-4 max-w-5xl')}>
|
||||
<div className={clsx('mt-2')}>
|
||||
<AddressInput {...managerState} isRequired />
|
||||
<TextInput className="mt-2" {...nameState} />
|
||||
<TextInput className="mt-2" {...descriptionState} />
|
||||
<NumberInput className="mt-2" {...maxSupplyState} />
|
||||
<TextInput className="mt-2" {...externalUrlState} />
|
||||
|
||||
<FormControl className="mt-2" htmlId="expiry-date" subtitle="Badge minting expiry date" title="Expiry Date">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="mt-2 w-1/3 form-control">
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<span className="mr-4 font-bold">Transferrable</span>
|
||||
<input
|
||||
checked={transferrable}
|
||||
className={`toggle ${transferrable ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setTransferrable(!transferrable)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Conditional test={managerState.value !== ''}>
|
||||
<Tooltip
|
||||
backgroundColor="bg-stargaze"
|
||||
className="bg-yellow-600"
|
||||
label="This is only an estimate. Be sure to check the final amount before signing the transaction."
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="grid grid-cols-2 ml-12 w-full">
|
||||
<div className="mt-4 font-bold">Fee Estimate:</div>
|
||||
<span className="mt-4">{(metadataSize * Number(metadataFeeRate)) / 1000000} stars</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('ml-10')}>
|
||||
<div>
|
||||
<MetadataAttributes
|
||||
attributes={attributesState.entries}
|
||||
onAdd={attributesState.add}
|
||||
onChange={attributesState.update}
|
||||
onRemove={attributesState.remove}
|
||||
title="Traits"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label="A metadata file can be selected to automatically fill in the related fields."
|
||||
placement="bottom"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFile"
|
||||
>
|
||||
Metadata File Selection (optional)
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="application/json"
|
||||
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="metadataFile"
|
||||
onChange={selectMetadata}
|
||||
ref={metadataFileRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import type { UploadServiceType } from 'services/upload'
|
||||
|
||||
export type UploadMethod = 'new' | 'existing'
|
||||
export type MintRule = 'by_key' | 'by_minter' | 'by_keys' | 'not_resolved'
|
||||
|
||||
interface ImageUploadDetailsProps {
|
||||
onChange: (value: ImageUploadDetailsDataProps) => void
|
||||
mintRule: MintRule
|
||||
}
|
||||
|
||||
export interface ImageUploadDetailsDataProps {
|
||||
assetFile: File | undefined
|
||||
uploadService: UploadServiceType
|
||||
nftStorageApiKey?: string
|
||||
pinataApiKey?: string
|
||||
pinataSecretKey?: string
|
||||
uploadMethod: UploadMethod
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsProps) => {
|
||||
const [assetFile, setAssetFile] = useState<File>()
|
||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
||||
|
||||
const assetFileRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nftStorageApiKeyState = useInputState({
|
||||
id: 'nft-storage-api-key',
|
||||
name: 'nftStorageApiKey',
|
||||
title: 'NFT.Storage API Key',
|
||||
placeholder: 'Enter NFT.Storage API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataApiKeyState = useInputState({
|
||||
id: 'pinata-api-key',
|
||||
name: 'pinataApiKey',
|
||||
title: 'Pinata API Key',
|
||||
placeholder: 'Enter Pinata API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataSecretKeyState = useInputState({
|
||||
id: 'pinata-secret-key',
|
||||
name: 'pinataSecretKey',
|
||||
title: 'Pinata Secret Key',
|
||||
placeholder: 'Enter Pinata Secret Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const imageUrlState = useInputState({
|
||||
id: 'imageUrl',
|
||||
name: 'imageUrl',
|
||||
title: 'Image URL',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setAssetFile(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/jpg' })
|
||||
}
|
||||
// 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.')
|
||||
setAssetFile(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
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: ImageUploadDetailsDataProps = {
|
||||
assetFile,
|
||||
uploadService,
|
||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
||||
pinataApiKey: pinataApiKeyState.value,
|
||||
pinataSecretKey: pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
imageUrl: imageUrlState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(regex, '')
|
||||
.trim(),
|
||||
}
|
||||
onChange(data)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
}, [
|
||||
assetFile,
|
||||
uploadService,
|
||||
nftStorageApiKeyState.value,
|
||||
pinataApiKeyState.value,
|
||||
pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
imageUrlState.value,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (assetFileRef.current) assetFileRef.current.value = ''
|
||||
setAssetFile(undefined)
|
||||
imageUrlState.onChange('')
|
||||
}, [uploadMethod, mintRule])
|
||||
|
||||
return (
|
||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
||||
<div className="flex justify-center">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'new'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio2"
|
||||
name="inlineRadioOptions2"
|
||||
onClick={() => {
|
||||
setUploadMethod('new')
|
||||
}}
|
||||
type="radio"
|
||||
value="New"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio2"
|
||||
>
|
||||
Upload New Image
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'existing'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio1"
|
||||
name="inlineRadioOptions1"
|
||||
onClick={() => {
|
||||
setUploadMethod('existing')
|
||||
}}
|
||||
type="radio"
|
||||
value="Existing"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio1"
|
||||
>
|
||||
Use an existing Image URL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 py-5 pb-4">
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<div className="ml-3 flex-column">
|
||||
<p className="mb-5 ml-5">
|
||||
Though the Badge Hub contract allows for off-chain image storage, it is recommended to use a decentralized
|
||||
storage solution, such as IPFS. <br /> You may head over to{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
||||
NFT.Storage
|
||||
</Anchor>{' '}
|
||||
or{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
||||
Pinata
|
||||
</Anchor>{' '}
|
||||
and upload your image manually to get an image URL for your badge.
|
||||
</p>
|
||||
<div className="flex flex-row w-full">
|
||||
<TextInput {...imageUrlState} className="mt-2 ml-6 w-full max-w-2xl" />
|
||||
<Conditional test={imageUrlState.value !== ''}>
|
||||
<div className="mt-2 ml-4 w-1/4 border-2 border-dashed">
|
||||
<img
|
||||
alt="badge-preview"
|
||||
className="w-full"
|
||||
src={imageUrlState.value.replace('IPFS://', 'ipfs://').replace(/,/g, '').replace(/"/g, '').trim()}
|
||||
/>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={uploadMethod === 'new'}>
|
||||
<div>
|
||||
<div className="flex flex-col items-center px-8 w-full">
|
||||
<div className="flex justify-items-start mb-5 w-full font-bold">
|
||||
<div className="form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'nft-storage'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio3"
|
||||
name="inlineRadioOptions3"
|
||||
onClick={() => {
|
||||
setUploadService('nft-storage')
|
||||
}}
|
||||
type="radio"
|
||||
value="nft-storage"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio3"
|
||||
>
|
||||
Upload using NFT.Storage
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="ml-2 form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'pinata'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio4"
|
||||
name="inlineRadioOptions4"
|
||||
onClick={() => {
|
||||
setUploadService('pinata')
|
||||
}}
|
||||
type="radio"
|
||||
value="pinata"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio4"
|
||||
>
|
||||
Upload using Pinata
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full">
|
||||
<Conditional test={uploadService === 'nft-storage'}>
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
<Conditional test={uploadService === 'pinata'}>
|
||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
||||
<div className="w-[20px]" />
|
||||
<TextInput {...pinataSecretKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="grid grid-cols-2">
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFile"
|
||||
>
|
||||
Image Selection
|
||||
</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="assetFile"
|
||||
onChange={selectAsset}
|
||||
ref={assetFileRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={assetFile !== undefined}>
|
||||
<SingleAssetPreview
|
||||
relatedAsset={assetFile}
|
||||
subtitle={`Asset filename: ${assetFile?.name as string}`}
|
||||
/>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { QueryListItem } from './query'
|
||||
|
||||
export const useQueryComboboxState = () => {
|
||||
const [value, setValue] = useState<QueryListItem | null>(null)
|
||||
return { value, onChange: (item: QueryListItem) => setValue(item) }
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
||||
import type { QueryListItem } from './query'
|
||||
import { BY_KEY_QUERY_LIST, BY_KEYS_QUERY_LIST, BY_MINTER_QUERY_LIST } from './query'
|
||||
|
||||
export interface QueryComboboxProps {
|
||||
value: QueryListItem | null
|
||||
onChange: (item: QueryListItem) => void
|
||||
mintRule?: MintRule
|
||||
}
|
||||
|
||||
export const QueryCombobox = ({ value, onChange, mintRule }: QueryComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
const [QUERY_LIST, SET_QUERY_LIST] = useState<QueryListItem[]>(BY_KEY_QUERY_LIST)
|
||||
|
||||
useEffect(() => {
|
||||
if (mintRule === 'by_keys') {
|
||||
SET_QUERY_LIST(BY_KEYS_QUERY_LIST)
|
||||
} else if (mintRule === 'by_minter') {
|
||||
SET_QUERY_LIST(BY_MINTER_QUERY_LIST)
|
||||
} else {
|
||||
SET_QUERY_LIST(BY_KEY_QUERY_LIST)
|
||||
}
|
||||
}, [mintRule])
|
||||
|
||||
const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={FormControl}
|
||||
htmlId="query"
|
||||
labelAs={Combobox.Label}
|
||||
onChange={onChange}
|
||||
subtitle="Badge queries"
|
||||
title=""
|
||||
value={value}
|
||||
>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className={clsx(
|
||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
displayValue={(val?: QueryListItem) => val?.name ?? ''}
|
||||
id="message-type"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Select query"
|
||||
/>
|
||||
|
||||
<Combobox.Button
|
||||
className={clsx(
|
||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||
)}
|
||||
>
|
||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||
<Combobox.Options
|
||||
className={clsx(
|
||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||
'divide-y divide-stone-500/50',
|
||||
)}
|
||||
>
|
||||
{filtered.length < 1 && (
|
||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||
Query not found
|
||||
</span>
|
||||
)}
|
||||
{filtered.map((entry) => (
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
<span className="font-bold">{entry.name}</span>
|
||||
<span className="max-w-md text-sm">{entry.description}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
{value && (
|
||||
<div className="flex space-x-2 text-white/50">
|
||||
<div className="mt-1">
|
||||
<FaInfoCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm">{value.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import { QueryCombobox } from 'components/badges/queries/Combobox'
|
||||
import { useQueryComboboxState } from 'components/badges/queries/Combobox.hooks'
|
||||
import { dispatchQuery } from 'components/badges/queries/query'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { NumberInput, TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import type { BadgeHubInstance } from 'contracts/badgeHub'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
import { useWallet } from '../../../contexts/wallet'
|
||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
||||
|
||||
interface BadgeQueriesProps {
|
||||
badgeHubContractAddress: string
|
||||
badgeId: number
|
||||
badgeHubMessages: BadgeHubInstance | undefined
|
||||
mintRule: MintRule
|
||||
}
|
||||
export const BadgeQueries = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeQueriesProps) => {
|
||||
const wallet = useWallet()
|
||||
|
||||
const comboboxState = useQueryComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
|
||||
const pubkeyState = useInputState({
|
||||
id: 'pubkey',
|
||||
name: 'pubkey',
|
||||
title: 'Public Key',
|
||||
subtitle: 'The public key to check whether it can be used to mint a badge',
|
||||
})
|
||||
|
||||
const startAfterNumberState = useNumberInputState({
|
||||
id: 'start-after-number',
|
||||
name: 'start-after-number',
|
||||
title: 'Start After (optional)',
|
||||
subtitle: 'The id to start the pagination after',
|
||||
})
|
||||
|
||||
const startAfterStringState = useInputState({
|
||||
id: 'start-after-string',
|
||||
name: 'start-after-string',
|
||||
title: 'Start After (optional)',
|
||||
subtitle: 'The public key to start the pagination after',
|
||||
})
|
||||
|
||||
const paginationLimitState = useNumberInputState({
|
||||
id: 'pagination-limit',
|
||||
name: 'pagination-limit',
|
||||
title: 'Pagination Limit (optional)',
|
||||
subtitle: 'The number of items to return (max: 30)',
|
||||
defaultValue: 5,
|
||||
})
|
||||
|
||||
const { data: response } = useQuery(
|
||||
[
|
||||
badgeHubMessages,
|
||||
type,
|
||||
badgeId,
|
||||
pubkeyState.value,
|
||||
startAfterNumberState.value,
|
||||
startAfterStringState.value,
|
||||
paginationLimitState.value,
|
||||
] as const,
|
||||
async ({ queryKey }) => {
|
||||
const [_badgeHubMessages, _type, _badgeId, _pubKey, _startAfterNumber, _startAfterString, _limit] = queryKey
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const result = await dispatchQuery({
|
||||
badgeHubMessages: _badgeHubMessages,
|
||||
id: _badgeId,
|
||||
startAfterNumber: _startAfterNumber,
|
||||
startAfterString: _startAfterString,
|
||||
limit: _limit,
|
||||
type: _type,
|
||||
pubkey: _pubKey,
|
||||
})
|
||||
return result
|
||||
},
|
||||
{
|
||||
placeholderData: null,
|
||||
onError: (error: any) => {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
},
|
||||
enabled: Boolean(badgeHubContractAddress && type && badgeId),
|
||||
retry: false,
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 mt-4">
|
||||
<div className="mr-2 space-y-8">
|
||||
<QueryCombobox mintRule={mintRule} {...comboboxState} />
|
||||
<Conditional test={type === 'getKey'}>
|
||||
<TextInput {...pubkeyState} />
|
||||
</Conditional>
|
||||
<Conditional test={type === 'getBadges'}>
|
||||
<NumberInput {...startAfterNumberState} />
|
||||
</Conditional>
|
||||
<Conditional test={type === 'getBadges' || type === 'getKeys'}>
|
||||
<NumberInput {...paginationLimitState} />
|
||||
</Conditional>
|
||||
<Conditional test={type === 'getKeys'}>
|
||||
<TextInput {...startAfterStringState} />
|
||||
</Conditional>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<FormControl title="Query Response">
|
||||
<JsonPreview content={response || {}} isCopyable />
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
import type { BadgeHubInstance } from 'contracts/badgeHub'
|
||||
|
||||
export type QueryType = typeof QUERY_TYPES[number]
|
||||
|
||||
export const QUERY_TYPES = ['config', 'getBadge', 'getBadges', 'getKey', 'getKeys'] as const
|
||||
|
||||
export interface QueryListItem {
|
||||
id: QueryType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const BY_KEY_QUERY_LIST: QueryListItem[] = [
|
||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
||||
]
|
||||
export const BY_KEYS_QUERY_LIST: QueryListItem[] = [
|
||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
||||
{ id: 'getKey', name: 'Query Key', description: "Query a key by ID to see if it's whitelisted" },
|
||||
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
|
||||
]
|
||||
export const BY_MINTER_QUERY_LIST: QueryListItem[] = [
|
||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
type: QueryType
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
type Select<T extends QueryType> = T
|
||||
|
||||
export type DispatchQueryArgs = {
|
||||
badgeHubMessages?: BadgeHubInstance
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'config'> }
|
||||
| { type: Select<'getBadge'>; id: number }
|
||||
| { type: Select<'getBadges'>; startAfterNumber: number; limit: number }
|
||||
| { type: Select<'getKey'>; id: number; pubkey: string }
|
||||
| { type: Select<'getKeys'>; id: number; startAfterString: string; limit: number }
|
||||
)
|
||||
|
||||
export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
||||
const { badgeHubMessages } = args
|
||||
if (!badgeHubMessages) {
|
||||
throw new Error('Cannot perform a query')
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'config': {
|
||||
return badgeHubMessages?.getConfig()
|
||||
}
|
||||
case 'getBadge': {
|
||||
return badgeHubMessages?.getBadge(args.id)
|
||||
}
|
||||
case 'getBadges': {
|
||||
return badgeHubMessages?.getBadges(args.startAfterNumber, args.limit)
|
||||
}
|
||||
case 'getKey': {
|
||||
return badgeHubMessages?.getKey(args.id, args.pubkey)
|
||||
}
|
||||
case 'getKeys': {
|
||||
return badgeHubMessages?.getKeys(args.id, args.startAfterString, args.limit)
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown action')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,65 +12,44 @@ import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
|
||||
import type { MinterInstance } from 'contracts/minter'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { FaArrowRight } from 'react-icons/fa'
|
||||
import { useMutation } from 'react-query'
|
||||
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import type { CollectionInfo } from '../../../contracts/sg721/contract'
|
||||
import { TextInput } from '../../forms/FormInput'
|
||||
import type { MinterType, Sg721Type } from './Combobox'
|
||||
|
||||
interface CollectionActionsProps {
|
||||
minterContractAddress: string
|
||||
sg721ContractAddress: string
|
||||
sg721Messages: SG721Instance | undefined
|
||||
vendingMinterMessages: VendingMinterInstance | undefined
|
||||
baseMinterMessages: BaseMinterInstance | undefined
|
||||
openEditionMinterMessages: OpenEditionMinterInstance | undefined
|
||||
minterType: MinterType
|
||||
sg721Type: Sg721Type
|
||||
minterMessages: MinterInstance | undefined
|
||||
}
|
||||
|
||||
type ExplicitContentType = true | false | undefined
|
||||
|
||||
export const CollectionActions = ({
|
||||
sg721ContractAddress,
|
||||
sg721Messages,
|
||||
minterContractAddress,
|
||||
vendingMinterMessages,
|
||||
baseMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
minterType,
|
||||
sg721Type,
|
||||
minterMessages,
|
||||
}: CollectionActionsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([])
|
||||
const [airdropArray, setAirdropArray] = useState<string[]>([])
|
||||
const [collectionInfo, setCollectionInfo] = useState<CollectionInfo>()
|
||||
const [explicitContent, setExplicitContent] = useState<ExplicitContentType>(undefined)
|
||||
const [resolvedRecipientAddress, setResolvedRecipientAddress] = useState<string>('')
|
||||
const [jsonExtensions, setJsonExtensions] = useState<boolean>(false)
|
||||
|
||||
const actionComboboxState = useActionsComboboxState()
|
||||
const type = actionComboboxState.value?.id
|
||||
|
||||
const limitState = useNumberInputState({
|
||||
id: 'per-address-limit',
|
||||
id: 'per-address-limi',
|
||||
name: 'perAddressLimit',
|
||||
title: 'Per Address Limit',
|
||||
subtitle: 'Enter the per address limit',
|
||||
@@ -128,33 +107,6 @@ export const CollectionActions = ({
|
||||
subtitle: 'Address of the whitelist contract',
|
||||
})
|
||||
|
||||
const priceState = useNumberInputState({
|
||||
id: 'update-mint-price',
|
||||
name: 'updateMintPrice',
|
||||
title: type === 'update_discount_price' ? 'Discount Price' : 'Update Mint Price',
|
||||
subtitle: type === 'update_discount_price' ? 'New discount price in STARS' : 'New minting price in STARS',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
id: 'collection-description',
|
||||
name: 'description',
|
||||
title: 'Collection Description',
|
||||
})
|
||||
|
||||
const imageState = useInputState({
|
||||
id: 'collection-cover-image',
|
||||
name: 'cover_image',
|
||||
title: 'Collection Cover Image',
|
||||
subtitle: 'URL for collection cover image.',
|
||||
})
|
||||
|
||||
const externalLinkState = useInputState({
|
||||
id: 'collection-ext-link',
|
||||
name: 'external_link',
|
||||
title: 'External Link',
|
||||
subtitle: 'External URL for the collection.',
|
||||
})
|
||||
|
||||
const royaltyPaymentAddressState = useInputState({
|
||||
id: 'royalty-payment-address',
|
||||
name: 'royaltyPaymentAddress',
|
||||
@@ -168,123 +120,47 @@ export const CollectionActions = ({
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '5%',
|
||||
placeholder: '8%',
|
||||
})
|
||||
|
||||
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
|
||||
const showWhitelistField = type === 'set_whitelist'
|
||||
const showDateField = isEitherType(type, ['update_start_time', 'update_start_trading_time'])
|
||||
const showEndDateField = type === 'update_end_time'
|
||||
const showDateField = type === 'update_start_time'
|
||||
const showLimitField = type === 'update_per_address_limit'
|
||||
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata'])
|
||||
const showNumberOfTokensField = isEitherType(type, ['batch_mint', 'batch_mint_open_edition'])
|
||||
const showTokenIdListField = isEitherType(type, [
|
||||
'batch_burn',
|
||||
'batch_transfer',
|
||||
'batch_mint_for',
|
||||
'batch_update_token_metadata',
|
||||
])
|
||||
const showRecipientField = isEitherType(type, [
|
||||
'transfer',
|
||||
'mint_to',
|
||||
'mint_to_open_edition',
|
||||
'mint_for',
|
||||
'batch_mint',
|
||||
'batch_mint_open_edition',
|
||||
'batch_transfer',
|
||||
'batch_mint_for',
|
||||
])
|
||||
const showAirdropFileField = isEitherType(type, ['airdrop', 'airdrop_open_edition', 'airdrop_specific'])
|
||||
const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
|
||||
const showDescriptionField = type === 'update_collection_info'
|
||||
const showImageField = type === 'update_collection_info'
|
||||
const showExternalLinkField = type === 'update_collection_info'
|
||||
const showRoyaltyRelatedFields = type === 'update_collection_info'
|
||||
const showExplicitContentField = type === 'update_collection_info'
|
||||
const showNumberOfTokensField = type === 'batch_mint'
|
||||
const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer', 'batch_update_token_metadata'])
|
||||
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer'])
|
||||
const showAirdropFileField = type === 'airdrop'
|
||||
const showRoyaltyInfoFields = type === 'update_royalty_info'
|
||||
const showTokenUriField = type === 'update_token_metadata'
|
||||
const showBaseUriField = type === 'batch_update_token_metadata'
|
||||
|
||||
const payload: DispatchExecuteArgs = {
|
||||
whitelist: whitelistState.value,
|
||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
|
||||
limit: limitState.value,
|
||||
minterContract: minterContractAddress,
|
||||
sg721Contract: sg721ContractAddress,
|
||||
tokenId: tokenIdState.value,
|
||||
tokenIds: tokenIdListState.value,
|
||||
tokenUri: tokenURIState.value.trim().endsWith('/')
|
||||
? tokenURIState.value.trim().slice(0, -1)
|
||||
: tokenURIState.value.trim(),
|
||||
batchNumber: batchNumberState.value,
|
||||
vendingMinterMessages,
|
||||
baseMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
minterMessages,
|
||||
sg721Messages,
|
||||
recipient: resolvedRecipientAddress,
|
||||
recipient: recipientState.value,
|
||||
recipients: airdropArray,
|
||||
tokenRecipients: airdropAllocationArray,
|
||||
txSigner: wallet.address,
|
||||
type,
|
||||
price: priceState.value.toString(),
|
||||
royaltyInfo: {
|
||||
payment_address: royaltyPaymentAddressState.value,
|
||||
share_bps: Number(royaltyShareState.value),
|
||||
},
|
||||
baseUri: baseURIState.value.trim().endsWith('/')
|
||||
? baseURIState.value.trim().slice(0, -1)
|
||||
: baseURIState.value.trim(),
|
||||
collectionInfo,
|
||||
jsonExtensions,
|
||||
tokenUri: tokenURIState.value.trim().endsWith('/')
|
||||
? tokenURIState.value.trim().slice(0, -1)
|
||||
: tokenURIState.value.trim(),
|
||||
type,
|
||||
}
|
||||
const resolveRecipientAddress = async () => {
|
||||
await resolveAddress(recipientState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setResolvedRecipientAddress(resolvedAddress)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
void resolveRecipientAddress()
|
||||
}, [recipientState.value])
|
||||
|
||||
const resolveRoyaltyPaymentAddress = async () => {
|
||||
await resolveAddress(royaltyPaymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setCollectionInfo({
|
||||
description: descriptionState.value || undefined,
|
||||
image: imageState.value || undefined,
|
||||
explicit_content: explicitContent,
|
||||
external_link: externalLinkState.value || undefined,
|
||||
royalty_info:
|
||||
royaltyPaymentAddressState.value && royaltyShareState.value
|
||||
? {
|
||||
payment_address: resolvedAddress,
|
||||
share: (Number(royaltyShareState.value) / 100).toString(),
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void resolveRoyaltyPaymentAddress()
|
||||
}, [royaltyPaymentAddressState.value])
|
||||
|
||||
useEffect(() => {
|
||||
setCollectionInfo({
|
||||
description: descriptionState.value || undefined,
|
||||
image: imageState.value || undefined,
|
||||
explicit_content: explicitContent,
|
||||
external_link: externalLinkState.value || undefined,
|
||||
royalty_info:
|
||||
royaltyPaymentAddressState.value && royaltyShareState.value
|
||||
? {
|
||||
payment_address: royaltyPaymentAddressState.value.trim(),
|
||||
share: (Number(royaltyShareState.value) / 100).toString(),
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}, [
|
||||
descriptionState.value,
|
||||
imageState.value,
|
||||
explicitContent,
|
||||
externalLinkState.value,
|
||||
royaltyPaymentAddressState.value,
|
||||
royaltyShareState.value,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const addresses: string[] = []
|
||||
@@ -311,71 +187,15 @@ export const CollectionActions = ({
|
||||
throw new Error('Please enter minter and sg721 contract addresses!')
|
||||
}
|
||||
|
||||
if (wallet.client && type === 'update_mint_price') {
|
||||
const contractConfig = wallet.client.queryContractSmart(minterContractAddress, {
|
||||
config: {},
|
||||
})
|
||||
await toast
|
||||
.promise(
|
||||
wallet.client.queryContractSmart(minterContractAddress, {
|
||||
mint_price: {},
|
||||
}),
|
||||
{
|
||||
error: `Querying mint price failed!`,
|
||||
loading: 'Querying current mint price...',
|
||||
success: (price) => {
|
||||
console.log('Current mint price: ', price)
|
||||
return `Current mint price is ${Number(price.public_price.amount) / 1000000} STARS`
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(async (price) => {
|
||||
if (Number(price.public_price.amount) / 1000000 <= priceState.value) {
|
||||
await contractConfig
|
||||
.then((config) => {
|
||||
console.log(config.start_time, Date.now() * 1000000)
|
||||
if (Number(config.start_time) < Date.now() * 1000000) {
|
||||
throw new Error(
|
||||
`Minting has already started on ${new Date(
|
||||
Number(config.start_time) / 1000000,
|
||||
).toLocaleString()}. Updated mint price cannot be higher than the current price of ${
|
||||
Number(price.public_price.amount) / 1000000
|
||||
} STARS`,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7))
|
||||
})
|
||||
} else {
|
||||
await contractConfig.then(async (config) => {
|
||||
const factoryParameters = await wallet.client?.queryContractSmart(config.factory, {
|
||||
params: {},
|
||||
})
|
||||
if (
|
||||
factoryParameters.params.min_mint_price.amount &&
|
||||
priceState.value < Number(factoryParameters.params.min_mint_price.amount) / 1000000
|
||||
) {
|
||||
throw new Error(
|
||||
`Updated mint price cannot be lower than the minimum mint price of ${
|
||||
Number(factoryParameters.params.min_mint_price.amount) / 1000000
|
||||
} STARS`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'update_collection_info' &&
|
||||
type === 'update_royalty_info' &&
|
||||
(royaltyShareState.value ? !royaltyPaymentAddressState.value : royaltyPaymentAddressState.value)
|
||||
) {
|
||||
throw new Error('Royalty payment address and share percentage are both required')
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'update_collection_info' &&
|
||||
type === 'update_royalty_info' &&
|
||||
royaltyPaymentAddressState.value &&
|
||||
!royaltyPaymentAddressState.value.trim().endsWith('.stars')
|
||||
) {
|
||||
@@ -392,8 +212,8 @@ export const CollectionActions = ({
|
||||
if (contractInfoResponse !== undefined) {
|
||||
const contractInfo = JSON.parse(new TextDecoder().decode(contractInfoResponse as Uint8Array))
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (contractInfo && !contractInfo.contract.includes('splits'))
|
||||
throw new Error('The provided royalty payment address does not belong to a splits contract.')
|
||||
if (contractInfo && (contractInfo.contract.includes('minter') || contractInfo.contract.includes('sg721')))
|
||||
throw new Error('The provided royalty payment address does not belong to a compatible contract.')
|
||||
else console.log(contractInfo)
|
||||
}
|
||||
}
|
||||
@@ -401,6 +221,7 @@ export const CollectionActions = ({
|
||||
const txHash = await toast.promise(dispatchExecute(payload), {
|
||||
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
|
||||
loading: 'Executing message...',
|
||||
|
||||
success: (tx) => `Transaction ${tx} success!`,
|
||||
})
|
||||
if (txHash) {
|
||||
@@ -416,120 +237,37 @@ export const CollectionActions = ({
|
||||
|
||||
const airdropFileOnChange = (data: AirdropAllocation[]) => {
|
||||
setAirdropAllocationArray(data)
|
||||
console.log(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="grid grid-cols-2 mt-4">
|
||||
<div className="mr-2">
|
||||
<ActionsCombobox minterType={minterType} sg721Type={sg721Type} {...actionComboboxState} />
|
||||
<ActionsCombobox {...actionComboboxState} />
|
||||
{showRecipientField && <AddressInput {...recipientState} />}
|
||||
{showTokenUriField && <TextInput className="mt-2" {...tokenURIState} />}
|
||||
{showWhitelistField && <AddressInput {...whitelistState} />}
|
||||
{showLimitField && <NumberInput {...limitState} />}
|
||||
{showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />}
|
||||
{showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />}
|
||||
{showTokenIdField && <NumberInput {...tokenIdState} />}
|
||||
{showTokenUriField && <TextInput className="mt-2" {...tokenURIState} />}
|
||||
{showTokenIdListField && <TextInput {...tokenIdListState} />}
|
||||
{showBaseUriField && <TextInput className="mt-2" {...baseURIState} />}
|
||||
{showNumberOfTokensField && <NumberInput className="mt-2" {...batchNumberState} />}
|
||||
{showPriceField && <NumberInput className="mt-2" {...priceState} />}
|
||||
{showDescriptionField && <TextInput className="my-2" {...descriptionState} />}
|
||||
{showImageField && <TextInput className="mb-2" {...imageState} />}
|
||||
{showExternalLinkField && <TextInput className="mb-2" {...externalLinkState} />}
|
||||
{showRoyaltyRelatedFields && (
|
||||
<div className="p-2 my-4 rounded border-2 border-gray-500/50">
|
||||
<TextInput className="mb-2" {...royaltyPaymentAddressState} />
|
||||
<NumberInput className="mb-2" {...royaltyShareState} />
|
||||
</div>
|
||||
)}
|
||||
{showExplicitContentField && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div>
|
||||
<div className="flex">
|
||||
<span className="mt-1 text-sm first-letter:capitalize">
|
||||
Does the collection contain explicit content?
|
||||
</span>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={explicitContent === true}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio1"
|
||||
name="explicitRadioOptions1"
|
||||
onClick={() => {
|
||||
setExplicitContent(true)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio1"
|
||||
>
|
||||
YES
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={explicitContent === false}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio2"
|
||||
name="explicitRadioOptions2"
|
||||
onClick={() => {
|
||||
setExplicitContent(false)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio2"
|
||||
>
|
||||
NO
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showNumberOfTokensField && <NumberInput {...batchNumberState} />}
|
||||
{showRoyaltyInfoFields && <TextInput className="mt-2" {...royaltyPaymentAddressState} />}
|
||||
{showRoyaltyInfoFields && <NumberInput className="mt-2" {...royaltyShareState} />}
|
||||
{showAirdropFileField && (
|
||||
<FormGroup
|
||||
subtitle={`CSV file that contains the airdrop addresses and the ${
|
||||
type === 'airdrop' ? 'amount of tokens' : 'token ID'
|
||||
} allocated for each address. Should start with the following header row: ${
|
||||
type === 'airdrop' ? 'address,amount' : 'address,tokenId'
|
||||
}`}
|
||||
subtitle="CSV file that contains the airdrop addresses and the amount of tokens allocated for each address. Should start with the following header row: address,amount"
|
||||
title="Airdrop File"
|
||||
>
|
||||
<AirdropUpload onChange={airdropFileOnChange} />
|
||||
</FormGroup>
|
||||
)}
|
||||
<Conditional test={showDateField}>
|
||||
<FormControl className="mt-2" htmlId="start-date" title="Start Time">
|
||||
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
</Conditional>
|
||||
<Conditional test={showEndDateField}>
|
||||
<FormControl className="mt-2" htmlId="end-date" title="End Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setEndTimestamp(date)} value={endTimestamp} />
|
||||
</FormControl>
|
||||
</Conditional>
|
||||
<Conditional test={showBaseUriField}>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
className="ml-7"
|
||||
label="Please toggle this on if the IPFS folder contains files with .json extensions."
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="mt-2 w-3/4 form-control">
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<span className="mr-4 font-bold">Metadata files with .json extensions?</span>
|
||||
<input
|
||||
checked={jsonExtensions}
|
||||
className={`toggle ${jsonExtensions ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setJsonExtensions(!jsonExtensions)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Conditional>
|
||||
</div>
|
||||
<div className="-mt-6">
|
||||
<div className="relative mb-2">
|
||||
|
||||
@@ -2,38 +2,19 @@ import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
import type { ActionListItem } from './actions'
|
||||
import { BASE_ACTION_LIST, OPEN_EDITION_ACTION_LIST, SG721_UPDATABLE_ACTION_LIST, VENDING_ACTION_LIST } from './actions'
|
||||
|
||||
export type MinterType = 'base' | 'vending' | 'openEdition'
|
||||
export type Sg721Type = 'updatable' | 'base'
|
||||
import { ACTION_LIST } from './actions'
|
||||
|
||||
export interface ActionsComboboxProps {
|
||||
value: ActionListItem | null
|
||||
onChange: (item: ActionListItem) => void
|
||||
minterType?: MinterType
|
||||
sg721Type?: Sg721Type
|
||||
}
|
||||
|
||||
export const ActionsCombobox = ({ value, onChange, minterType, sg721Type }: ActionsComboboxProps) => {
|
||||
export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
const [ACTION_LIST, SET_ACTION_LIST] = useState<ActionListItem[]>(VENDING_ACTION_LIST)
|
||||
|
||||
useEffect(() => {
|
||||
if (minterType === 'base') {
|
||||
if (sg721Type === 'updatable') SET_ACTION_LIST(BASE_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||
else SET_ACTION_LIST(BASE_ACTION_LIST)
|
||||
} else if (minterType === 'vending') {
|
||||
if (sg721Type === 'updatable') SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||
else SET_ACTION_LIST(VENDING_ACTION_LIST)
|
||||
} else if (minterType === 'openEdition') {
|
||||
if (sg721Type === 'updatable') SET_ACTION_LIST(OPEN_EDITION_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||
else SET_ACTION_LIST(OPEN_EDITION_ACTION_LIST)
|
||||
} else SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||
}, [minterType, sg721Type])
|
||||
|
||||
const filtered =
|
||||
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
@@ -87,7 +68,7 @@ export const ActionsCombobox = ({ value, onChange, minterType, sg721Type }: Acti
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
|
||||
@@ -1,48 +1,28 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
import { useBaseMinterContract } from 'contracts/baseMinter'
|
||||
import { useOpenEditionMinterContract } from 'contracts/openEditionMinter'
|
||||
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
|
||||
import type { MinterInstance } from 'contracts/minter'
|
||||
import { useMinterContract } from 'contracts/minter'
|
||||
import type { RoyaltyInfo, SG721Instance } from 'contracts/sg721'
|
||||
import { useSG721Contract } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
import { useVendingMinterContract } from 'contracts/vendingMinter'
|
||||
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
|
||||
|
||||
import type { BaseMinterInstance } from '../../../contracts/baseMinter/contract'
|
||||
import type { OpenEditionMinterInstance } from '../../../contracts/openEditionMinter/contract'
|
||||
|
||||
export type ActionType = typeof ACTION_TYPES[number]
|
||||
|
||||
export const ACTION_TYPES = [
|
||||
'mint_token_uri',
|
||||
'update_mint_price',
|
||||
'update_discount_price',
|
||||
'remove_discount_price',
|
||||
'mint_to',
|
||||
'mint_to_open_edition',
|
||||
'mint_for',
|
||||
'batch_mint',
|
||||
'batch_mint_open_edition',
|
||||
'set_whitelist',
|
||||
'update_start_time',
|
||||
'update_end_time',
|
||||
'update_start_trading_time',
|
||||
'update_royalty_info',
|
||||
'update_per_address_limit',
|
||||
'update_collection_info',
|
||||
'freeze_collection_info',
|
||||
'withdraw',
|
||||
'transfer',
|
||||
'batch_transfer',
|
||||
'burn',
|
||||
'batch_burn',
|
||||
'batch_mint_for',
|
||||
'shuffle',
|
||||
'airdrop',
|
||||
'airdrop_open_edition',
|
||||
'airdrop_specific',
|
||||
'burn_remaining',
|
||||
'update_token_metadata',
|
||||
'batch_update_token_metadata',
|
||||
'freeze_token_metadata',
|
||||
'enable_updatable',
|
||||
] as const
|
||||
|
||||
export interface ActionListItem {
|
||||
@@ -51,84 +31,21 @@ export interface ActionListItem {
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const BASE_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'mint_token_uri',
|
||||
name: 'Add New Token',
|
||||
description: `Mint a new token and add it to the collection`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_trading_time',
|
||||
name: 'Update Trading Start Time',
|
||||
description: `Update start time for trading`,
|
||||
},
|
||||
{
|
||||
id: 'update_collection_info',
|
||||
name: 'Update Collection Info',
|
||||
description: `Update Collection Info`,
|
||||
},
|
||||
{
|
||||
id: 'freeze_collection_info',
|
||||
name: 'Freeze Collection Info',
|
||||
description: `Freeze collection info to prevent further updates`,
|
||||
},
|
||||
{
|
||||
id: 'transfer',
|
||||
name: 'Transfer Tokens',
|
||||
description: `Transfer tokens from one address to another`,
|
||||
},
|
||||
{
|
||||
id: 'batch_transfer',
|
||||
name: 'Batch Transfer Tokens',
|
||||
description: `Transfer a list of tokens to a recipient`,
|
||||
},
|
||||
{
|
||||
id: 'burn',
|
||||
name: 'Burn Token',
|
||||
description: `Burn a specified token from the collection`,
|
||||
},
|
||||
{
|
||||
id: 'batch_burn',
|
||||
name: 'Batch Burn Tokens',
|
||||
description: `Burn a list of tokens from the collection`,
|
||||
},
|
||||
]
|
||||
|
||||
export const VENDING_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'update_mint_price',
|
||||
name: 'Update Mint Price',
|
||||
description: `Update mint price`,
|
||||
},
|
||||
{
|
||||
id: 'update_discount_price',
|
||||
name: 'Update Discount Price',
|
||||
description: `Update discount price`,
|
||||
},
|
||||
{
|
||||
id: 'remove_discount_price',
|
||||
name: 'Remove Discount Price',
|
||||
description: `Remove discount price`,
|
||||
},
|
||||
export const ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'mint_to',
|
||||
name: 'Mint To',
|
||||
description: `Mint a token to a user`,
|
||||
},
|
||||
{
|
||||
id: 'batch_mint',
|
||||
name: 'Batch Mint To',
|
||||
description: `Mint multiple tokens to a user`,
|
||||
},
|
||||
{
|
||||
id: 'mint_for',
|
||||
name: 'Mint For',
|
||||
description: `Mint a token for a user with the given token ID`,
|
||||
description: `Mint a token for a user with given token ID`,
|
||||
},
|
||||
{
|
||||
id: 'batch_mint_for',
|
||||
name: 'Batch Mint For',
|
||||
description: `Mint a specific range of tokens from the collection to a specific address`,
|
||||
id: 'batch_mint',
|
||||
name: 'Batch Mint',
|
||||
description: `Mint multiple tokens to a user with given token amount`,
|
||||
},
|
||||
{
|
||||
id: 'set_whitelist',
|
||||
@@ -136,14 +53,14 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
||||
description: `Set whitelist contract address`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_time',
|
||||
name: 'Update Minting Start Time',
|
||||
description: `Update start time for minting`,
|
||||
id: 'update_royalty_info',
|
||||
name: 'Update Royalty Info',
|
||||
description: `Update royalty payment details`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_trading_time',
|
||||
name: 'Update Trading Start Time',
|
||||
description: `Update start time for trading`,
|
||||
id: 'update_start_time',
|
||||
name: 'Update Start Time',
|
||||
description: `Update start time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_per_address_limit',
|
||||
@@ -151,14 +68,24 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
||||
description: `Update token per address limit`,
|
||||
},
|
||||
{
|
||||
id: 'update_collection_info',
|
||||
name: 'Update Collection Info',
|
||||
description: `Update Collection Info`,
|
||||
id: 'update_token_metadata',
|
||||
name: 'Update Token Metadata',
|
||||
description: `Update the metadata URI for a token`,
|
||||
},
|
||||
{
|
||||
id: 'freeze_collection_info',
|
||||
name: 'Freeze Collection Info',
|
||||
description: `Freeze collection info to prevent further updates`,
|
||||
id: 'batch_update_token_metadata',
|
||||
name: 'Batch Update Token Metadata',
|
||||
description: `Update the metadata URI for a range of tokens`,
|
||||
},
|
||||
{
|
||||
id: 'freeze_token_metadata',
|
||||
name: 'Freeze Token Metadata',
|
||||
description: `Render the metadata for tokens no longer updatable`,
|
||||
},
|
||||
{
|
||||
id: 'withdraw',
|
||||
name: 'Withdraw Tokens',
|
||||
description: `Withdraw tokens from the contract`,
|
||||
},
|
||||
{
|
||||
id: 'transfer',
|
||||
@@ -190,112 +117,6 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Airdrop Tokens',
|
||||
description: 'Airdrop tokens to given addresses',
|
||||
},
|
||||
{
|
||||
id: 'airdrop_specific',
|
||||
name: 'Airdrop Specific Tokens',
|
||||
description: 'Airdrop specific tokens to given addresses',
|
||||
},
|
||||
{
|
||||
id: 'burn_remaining',
|
||||
name: 'Burn Remaining Tokens',
|
||||
description: 'Burn remaining tokens',
|
||||
},
|
||||
]
|
||||
|
||||
export const OPEN_EDITION_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'update_mint_price',
|
||||
name: 'Update Mint Price',
|
||||
description: `Update mint price`,
|
||||
},
|
||||
{
|
||||
id: 'mint_to_open_edition',
|
||||
name: 'Mint To',
|
||||
description: `Mint a token to a user`,
|
||||
},
|
||||
{
|
||||
id: 'batch_mint_open_edition',
|
||||
name: 'Batch Mint To',
|
||||
description: `Mint multiple tokens to a user`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_time',
|
||||
name: 'Update Minting Start Time',
|
||||
description: `Update the start time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_end_time',
|
||||
name: 'Update Minting End Time',
|
||||
description: `Update the end time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_trading_time',
|
||||
name: 'Update Trading Start Time',
|
||||
description: `Update start time for trading`,
|
||||
},
|
||||
{
|
||||
id: 'update_per_address_limit',
|
||||
name: 'Update Tokens Per Address Limit',
|
||||
description: `Update token per address limit`,
|
||||
},
|
||||
{
|
||||
id: 'update_collection_info',
|
||||
name: 'Update Collection Info',
|
||||
description: `Update Collection Info`,
|
||||
},
|
||||
{
|
||||
id: 'freeze_collection_info',
|
||||
name: 'Freeze Collection Info',
|
||||
description: `Freeze collection info to prevent further updates`,
|
||||
},
|
||||
{
|
||||
id: 'transfer',
|
||||
name: 'Transfer Tokens',
|
||||
description: `Transfer tokens from one address to another`,
|
||||
},
|
||||
{
|
||||
id: 'batch_transfer',
|
||||
name: 'Batch Transfer Tokens',
|
||||
description: `Transfer a list of tokens to a recipient`,
|
||||
},
|
||||
{
|
||||
id: 'burn',
|
||||
name: 'Burn Token',
|
||||
description: `Burn a specified token from the collection`,
|
||||
},
|
||||
{
|
||||
id: 'batch_burn',
|
||||
name: 'Batch Burn Tokens',
|
||||
description: `Burn a list of tokens from the collection`,
|
||||
},
|
||||
{
|
||||
id: 'airdrop_open_edition',
|
||||
name: 'Airdrop Tokens',
|
||||
description: 'Airdrop tokens to given addresses',
|
||||
},
|
||||
]
|
||||
|
||||
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'update_token_metadata',
|
||||
name: 'Update Token Metadata',
|
||||
description: `Update the metadata URI for a token`,
|
||||
},
|
||||
{
|
||||
id: 'batch_update_token_metadata',
|
||||
name: 'Batch Update Token Metadata',
|
||||
description: `Update the metadata URI for a range of tokens`,
|
||||
},
|
||||
{
|
||||
id: 'freeze_token_metadata',
|
||||
name: 'Freeze Token Metadata',
|
||||
description: `Render the metadata for tokens no longer updatable`,
|
||||
},
|
||||
{
|
||||
id: 'enable_updatable',
|
||||
name: 'Enable Updatable',
|
||||
description: `Render a collection updatable following a migration`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
@@ -303,101 +124,65 @@ export interface DispatchExecuteProps {
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
/** @see {@link VendingMinterInstance}{@link BaseMinterInstance} */
|
||||
export interface DispatchExecuteArgs {
|
||||
type Select<T extends ActionType> = T
|
||||
|
||||
/** @see {@link MinterInstance} */
|
||||
export type DispatchExecuteArgs = {
|
||||
minterContract: string
|
||||
sg721Contract: string
|
||||
vendingMinterMessages?: VendingMinterInstance
|
||||
baseMinterMessages?: BaseMinterInstance
|
||||
openEditionMinterMessages?: OpenEditionMinterInstance
|
||||
minterMessages?: MinterInstance
|
||||
sg721Messages?: SG721Instance
|
||||
txSigner: string
|
||||
type: string | undefined
|
||||
tokenUri: string
|
||||
price: string
|
||||
recipient: string
|
||||
tokenId: number
|
||||
batchNumber: number
|
||||
whitelist: string
|
||||
startTime: string | undefined
|
||||
endTime: string | undefined
|
||||
limit: number
|
||||
tokenIds: string
|
||||
recipients: string[]
|
||||
tokenRecipients: AirdropAllocation[]
|
||||
collectionInfo: CollectionInfo | undefined
|
||||
baseUri: string
|
||||
jsonExtensions: boolean
|
||||
}
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'mint_to'>; recipient: string }
|
||||
| { type: Select<'mint_for'>; recipient: string; tokenId: number }
|
||||
| { type: Select<'batch_mint'>; recipient: string; batchNumber: number }
|
||||
| { type: Select<'set_whitelist'>; whitelist: string }
|
||||
| { type: Select<'update_start_time'>; startTime: string }
|
||||
| { type: Select<'update_per_address_limit'>; limit: number }
|
||||
| { type: Select<'shuffle'> }
|
||||
| { type: Select<'withdraw'> }
|
||||
| { type: Select<'transfer'>; recipient: string; tokenId: number }
|
||||
| { type: Select<'batch_transfer'>; recipient: string; tokenIds: string }
|
||||
| { type: Select<'burn'>; tokenId: number }
|
||||
| { type: Select<'batch_burn'>; tokenIds: string }
|
||||
| { type: Select<'update_royalty_info'>; royaltyInfo: RoyaltyInfo }
|
||||
| { type: Select<'airdrop'>; recipients: string[] }
|
||||
| { type: Select<'update_token_metadata'>; tokenId: number; tokenUri: string }
|
||||
| { type: Select<'batch_update_token_metadata'>; tokenIds: string; baseUri: string }
|
||||
| { type: Select<'freeze_token_metadata'> }
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { vendingMinterMessages, baseMinterMessages, openEditionMinterMessages, sg721Messages, txSigner } = args
|
||||
if (!vendingMinterMessages || !baseMinterMessages || !openEditionMinterMessages || !sg721Messages) {
|
||||
const { minterMessages, sg721Messages, txSigner } = args
|
||||
if (!minterMessages || !sg721Messages) {
|
||||
throw new Error('Cannot execute actions')
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'mint_token_uri': {
|
||||
return baseMinterMessages.mint(txSigner, args.tokenUri)
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return vendingMinterMessages.updateMintPrice(txSigner, args.price)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return vendingMinterMessages.updateDiscountPrice(txSigner, args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return vendingMinterMessages.removeDiscountPrice(txSigner)
|
||||
}
|
||||
case 'mint_to': {
|
||||
return vendingMinterMessages.mintTo(txSigner, args.recipient)
|
||||
}
|
||||
case 'mint_to_open_edition': {
|
||||
return openEditionMinterMessages.mintTo(txSigner, args.recipient)
|
||||
return minterMessages.mintTo(txSigner, args.recipient)
|
||||
}
|
||||
case 'mint_for': {
|
||||
return vendingMinterMessages.mintFor(txSigner, args.recipient, args.tokenId)
|
||||
return minterMessages.mintFor(txSigner, args.recipient, args.tokenId)
|
||||
}
|
||||
case 'batch_mint': {
|
||||
return vendingMinterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
|
||||
}
|
||||
case 'batch_mint_open_edition': {
|
||||
return openEditionMinterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
|
||||
return minterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
|
||||
}
|
||||
case 'set_whitelist': {
|
||||
return vendingMinterMessages.setWhitelist(txSigner, args.whitelist)
|
||||
return minterMessages.setWhitelist(txSigner, args.whitelist)
|
||||
}
|
||||
case 'update_start_time': {
|
||||
return vendingMinterMessages.updateStartTime(txSigner, args.startTime as string)
|
||||
}
|
||||
case 'update_end_time': {
|
||||
return openEditionMinterMessages.updateEndTime(txSigner, args.endTime as string)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return vendingMinterMessages.updateStartTradingTime(txSigner, args.startTime)
|
||||
return minterMessages.updateStartTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return vendingMinterMessages.updatePerAddressLimit(txSigner, args.limit)
|
||||
}
|
||||
case 'update_collection_info': {
|
||||
return sg721Messages.updateCollectionInfo(args.collectionInfo as CollectionInfo)
|
||||
}
|
||||
case 'freeze_collection_info': {
|
||||
return sg721Messages.freezeCollectionInfo()
|
||||
}
|
||||
case 'update_token_metadata': {
|
||||
return sg721Messages.updateTokenMetadata(args.tokenId.toString(), args.tokenUri)
|
||||
}
|
||||
case 'batch_update_token_metadata': {
|
||||
return sg721Messages.batchUpdateTokenMetadata(args.tokenIds, args.baseUri, args.jsonExtensions)
|
||||
}
|
||||
case 'freeze_token_metadata': {
|
||||
return sg721Messages.freezeTokenMetadata()
|
||||
}
|
||||
case 'enable_updatable': {
|
||||
return sg721Messages.enableUpdatable()
|
||||
return minterMessages.updatePerAddressLimit(txSigner, args.limit)
|
||||
}
|
||||
case 'shuffle': {
|
||||
return vendingMinterMessages.shuffle(txSigner)
|
||||
return minterMessages.shuffle(txSigner)
|
||||
}
|
||||
case 'withdraw': {
|
||||
return minterMessages.withdraw(txSigner)
|
||||
}
|
||||
case 'transfer': {
|
||||
return sg721Messages.transferNft(args.recipient, args.tokenId.toString())
|
||||
@@ -408,23 +193,23 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
case 'burn': {
|
||||
return sg721Messages.burn(args.tokenId.toString())
|
||||
}
|
||||
case 'update_royalty_info': {
|
||||
return sg721Messages.updateRoyaltyInfo(args.royaltyInfo)
|
||||
}
|
||||
case 'batch_burn': {
|
||||
return sg721Messages.batchBurn(args.tokenIds)
|
||||
}
|
||||
case 'batch_mint_for': {
|
||||
return vendingMinterMessages.batchMintFor(txSigner, args.recipient, args.tokenIds)
|
||||
}
|
||||
case 'airdrop': {
|
||||
return vendingMinterMessages.airdrop(txSigner, args.recipients)
|
||||
return minterMessages.airdrop(txSigner, args.recipients)
|
||||
}
|
||||
case 'airdrop_open_edition': {
|
||||
return openEditionMinterMessages.airdrop(txSigner, args.recipients)
|
||||
case 'update_token_metadata': {
|
||||
return sg721Messages.updateTokenMetadata(args.tokenId.toString(), args.tokenUri)
|
||||
}
|
||||
case 'airdrop_specific': {
|
||||
return vendingMinterMessages.airdropSpecificTokens(txSigner, args.tokenRecipients)
|
||||
case 'batch_update_token_metadata': {
|
||||
return sg721Messages.batchUpdateTokenMetadata(args.tokenIds, args.baseUri)
|
||||
}
|
||||
case 'burn_remaining': {
|
||||
return vendingMinterMessages.burnRemaining(txSigner)
|
||||
case 'freeze_token_metadata': {
|
||||
return sg721Messages.freezeTokenMetadata()
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown action')
|
||||
@@ -434,78 +219,44 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: vendingMinterMessages } = useVendingMinterContract()
|
||||
const { messages: minterMessages } = useMinterContract()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: sg721Messages } = useSG721Contract()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: baseMinterMessages } = useBaseMinterContract()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: openEditionMinterMessages } = useOpenEditionMinterContract()
|
||||
const { minterContract, sg721Contract } = args
|
||||
switch (args.type) {
|
||||
case 'mint_token_uri': {
|
||||
return baseMinterMessages(minterContract)?.mint(args.tokenUri)
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return vendingMinterMessages(minterContract)?.updateMintPrice(args.price)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return vendingMinterMessages(minterContract)?.updateDiscountPrice(args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return vendingMinterMessages(minterContract)?.removeDiscountPrice()
|
||||
}
|
||||
case 'mint_to': {
|
||||
return vendingMinterMessages(minterContract)?.mintTo(args.recipient)
|
||||
}
|
||||
case 'mint_to_open_edition': {
|
||||
return openEditionMinterMessages(minterContract)?.mintTo(args.recipient)
|
||||
return minterMessages(minterContract)?.mintTo(args.recipient)
|
||||
}
|
||||
case 'mint_for': {
|
||||
return vendingMinterMessages(minterContract)?.mintFor(args.recipient, args.tokenId)
|
||||
return minterMessages(minterContract)?.mintFor(args.recipient, args.tokenId)
|
||||
}
|
||||
case 'batch_mint': {
|
||||
return vendingMinterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
|
||||
}
|
||||
case 'batch_mint_open_edition': {
|
||||
return openEditionMinterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
|
||||
return minterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
|
||||
}
|
||||
case 'set_whitelist': {
|
||||
return vendingMinterMessages(minterContract)?.setWhitelist(args.whitelist)
|
||||
return minterMessages(minterContract)?.setWhitelist(args.whitelist)
|
||||
}
|
||||
case 'update_start_time': {
|
||||
return vendingMinterMessages(minterContract)?.updateStartTime(args.startTime as string)
|
||||
}
|
||||
case 'update_end_time': {
|
||||
return openEditionMinterMessages(minterContract)?.updateEndTime(args.endTime as string)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return vendingMinterMessages(minterContract)?.updateStartTradingTime(args.startTime as string)
|
||||
return minterMessages(minterContract)?.updateStartTime(args.startTime)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return vendingMinterMessages(minterContract)?.updatePerAddressLimit(args.limit)
|
||||
return minterMessages(minterContract)?.updatePerAddressLimit(args.limit)
|
||||
}
|
||||
case 'update_collection_info': {
|
||||
return sg721Messages(sg721Contract)?.updateCollectionInfo(args.collectionInfo as CollectionInfo)
|
||||
case 'shuffle': {
|
||||
return minterMessages(minterContract)?.shuffle()
|
||||
}
|
||||
case 'freeze_collection_info': {
|
||||
return sg721Messages(sg721Contract)?.freezeCollectionInfo()
|
||||
case 'withdraw': {
|
||||
return minterMessages(minterContract)?.withdraw()
|
||||
}
|
||||
case 'update_token_metadata': {
|
||||
return sg721Messages(sg721Contract)?.updateTokenMetadata(args.tokenId.toString(), args.tokenUri)
|
||||
}
|
||||
case 'batch_update_token_metadata': {
|
||||
return sg721Messages(sg721Contract)?.batchUpdateTokenMetadata(args.tokenIds, args.baseUri, args.jsonExtensions)
|
||||
return sg721Messages(sg721Contract)?.batchUpdateTokenMetadata(args.tokenIds, args.baseUri)
|
||||
}
|
||||
case 'freeze_token_metadata': {
|
||||
return sg721Messages(sg721Contract)?.freezeTokenMetadata()
|
||||
}
|
||||
case 'enable_updatable': {
|
||||
return sg721Messages(sg721Contract)?.enableUpdatable()
|
||||
}
|
||||
case 'shuffle': {
|
||||
return vendingMinterMessages(minterContract)?.shuffle()
|
||||
}
|
||||
case 'transfer': {
|
||||
return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString())
|
||||
}
|
||||
@@ -515,23 +266,14 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
case 'burn': {
|
||||
return sg721Messages(sg721Contract)?.burn(args.tokenId.toString())
|
||||
}
|
||||
case 'update_royalty_info': {
|
||||
return sg721Messages(sg721Contract)?.updateRoyaltyInfo(args.royaltyInfo)
|
||||
}
|
||||
case 'batch_burn': {
|
||||
return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds)
|
||||
}
|
||||
case 'batch_mint_for': {
|
||||
return vendingMinterMessages(minterContract)?.batchMintFor(args.recipient, args.tokenIds)
|
||||
}
|
||||
case 'airdrop': {
|
||||
return vendingMinterMessages(minterContract)?.airdrop(args.recipients)
|
||||
}
|
||||
case 'airdrop_open_edition': {
|
||||
return openEditionMinterMessages(minterContract)?.airdrop(args.recipients)
|
||||
}
|
||||
case 'airdrop_specific': {
|
||||
return vendingMinterMessages(minterContract)?.airdropSpecificTokens(args.tokenRecipients)
|
||||
}
|
||||
case 'burn_remaining': {
|
||||
return vendingMinterMessages(minterContract)?.burnRemaining()
|
||||
return minterMessages(minterContract)?.airdrop(args.recipients)
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import axios from 'axios'
|
||||
import clsx from 'clsx'
|
||||
import { Alert } from 'components/Alert'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { API_URL } from 'utils/constants'
|
||||
|
||||
import { useDebounce } from '../../../utils/debounce'
|
||||
import { TextInput } from '../../forms/FormInput'
|
||||
import type { MinterType } from '../actions/Combobox'
|
||||
|
||||
export type BaseMinterAcquisitionMethod = 'existing' | 'new'
|
||||
|
||||
export interface MinterInfo {
|
||||
name: string
|
||||
minter: string
|
||||
contractAddress: string
|
||||
}
|
||||
|
||||
interface BaseMinterDetailsProps {
|
||||
onChange: (data: BaseMinterDetailsDataProps) => void
|
||||
minterType: MinterType
|
||||
}
|
||||
|
||||
export interface BaseMinterDetailsDataProps {
|
||||
baseMinterAcquisitionMethod: BaseMinterAcquisitionMethod
|
||||
existingBaseMinter: string | undefined
|
||||
selectedCollectionAddress: string | undefined
|
||||
collectionTokenCount: number | undefined
|
||||
}
|
||||
|
||||
export const BaseMinterDetails = ({ onChange, minterType }: BaseMinterDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [myBaseMinterContracts, setMyBaseMinterContracts] = useState<MinterInfo[]>([])
|
||||
const [baseMinterAcquisitionMethod, setBaseMinterAcquisitionMethod] = useState<BaseMinterAcquisitionMethod>('new')
|
||||
const [selectedCollectionAddress, setSelectedCollectionAddress] = useState<string | undefined>(undefined)
|
||||
const [collectionTokenCount, setCollectionTokenCount] = useState<number | undefined>(undefined)
|
||||
|
||||
const existingBaseMinterState = useInputState({
|
||||
id: 'existingMinter',
|
||||
name: 'existingMinter',
|
||||
title: 'Existing Base Minter Contract Address',
|
||||
subtitle: '',
|
||||
placeholder: 'stars1...',
|
||||
})
|
||||
|
||||
const fetchMinterContracts = async (): Promise<MinterInfo[]> => {
|
||||
const contracts: MinterInfo[] = await axios
|
||||
.get(`${API_URL}/api/v1beta/collections/${wallet.address}`)
|
||||
.then((response) => {
|
||||
const collectionData = response.data
|
||||
const minterContracts = collectionData.map((collection: any) => {
|
||||
return { name: collection.name, minter: collection.minter, contractAddress: collection.contractAddress }
|
||||
})
|
||||
return minterContracts
|
||||
})
|
||||
.catch(console.error)
|
||||
console.log(contracts)
|
||||
return contracts
|
||||
}
|
||||
|
||||
async function getMinterContractType(minterContractAddress: string) {
|
||||
if (wallet.client && minterContractAddress.length > 0) {
|
||||
const client = wallet.client
|
||||
const data = await client.queryContractRaw(
|
||||
minterContractAddress,
|
||||
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
const contractType: string = JSON.parse(new TextDecoder().decode(data as Uint8Array)).contract
|
||||
return contractType
|
||||
}
|
||||
}
|
||||
|
||||
const filterBaseMinterContracts = async () => {
|
||||
await fetchMinterContracts()
|
||||
.then((minterContracts) =>
|
||||
minterContracts.map(async (minterContract: any) => {
|
||||
await getMinterContractType(minterContract.minter)
|
||||
.then((contractType) => {
|
||||
if (contractType?.includes('sg-base-minter')) {
|
||||
setMyBaseMinterContracts((prevState) => [...prevState, minterContract])
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
console.log('Unable to retrieve contract type')
|
||||
})
|
||||
}),
|
||||
)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
console.log('Unable to fetch base minter contracts')
|
||||
})
|
||||
}
|
||||
|
||||
const debouncedMyBaseMinterContracts = useDebounce(myBaseMinterContracts, 500)
|
||||
|
||||
const renderBaseMinterContracts = useCallback(() => {
|
||||
return debouncedMyBaseMinterContracts.map((baseMinterContract, index) => {
|
||||
return (
|
||||
<option key={index} className="mt-2 text-lg bg-[#1A1A1A]">
|
||||
{`${baseMinterContract.name} - ${baseMinterContract.minter}`}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}, [debouncedMyBaseMinterContracts])
|
||||
|
||||
const debouncedWalletAddress = useDebounce(wallet.address, 300)
|
||||
|
||||
const debouncedExistingBaseMinterContract = useDebounce(existingBaseMinterState.value, 300)
|
||||
|
||||
const displayToast = async () => {
|
||||
await toast.promise(filterBaseMinterContracts(), {
|
||||
loading: 'Retrieving previous 1/1 collections...',
|
||||
success: 'Collection retrieval finalized.',
|
||||
error: 'Unable to retrieve any 1/1 collections.',
|
||||
})
|
||||
}
|
||||
|
||||
const fetchSg721Address = async () => {
|
||||
if (debouncedExistingBaseMinterContract.length === 0) return
|
||||
await wallet.client
|
||||
?.queryContractSmart(debouncedExistingBaseMinterContract, {
|
||||
config: {},
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response.collection_address)
|
||||
setSelectedCollectionAddress(response.collection_address)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
console.log('Unable to retrieve collection address')
|
||||
})
|
||||
}
|
||||
|
||||
const fetchCollectionTokenCount = async () => {
|
||||
if (selectedCollectionAddress === undefined) return
|
||||
await wallet.client
|
||||
?.queryContractSmart(selectedCollectionAddress, {
|
||||
num_tokens: {},
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response)
|
||||
setCollectionTokenCount(Number(response.count))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
console.log('Unable to retrieve collection token count')
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedWalletAddress && baseMinterAcquisitionMethod === 'existing') {
|
||||
setMyBaseMinterContracts([])
|
||||
existingBaseMinterState.onChange('')
|
||||
void displayToast()
|
||||
} else if (baseMinterAcquisitionMethod === 'new' || !wallet.initialized) {
|
||||
setMyBaseMinterContracts([])
|
||||
existingBaseMinterState.onChange('')
|
||||
}
|
||||
}, [debouncedWalletAddress, baseMinterAcquisitionMethod])
|
||||
|
||||
useEffect(() => {
|
||||
if (baseMinterAcquisitionMethod === 'existing') {
|
||||
void fetchSg721Address()
|
||||
}
|
||||
}, [debouncedExistingBaseMinterContract])
|
||||
|
||||
useEffect(() => {
|
||||
if (baseMinterAcquisitionMethod === 'existing') {
|
||||
void fetchCollectionTokenCount()
|
||||
}
|
||||
}, [selectedCollectionAddress])
|
||||
|
||||
useEffect(() => {
|
||||
const data: BaseMinterDetailsDataProps = {
|
||||
baseMinterAcquisitionMethod,
|
||||
existingBaseMinter: existingBaseMinterState.value,
|
||||
selectedCollectionAddress,
|
||||
collectionTokenCount,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
existingBaseMinterState.value,
|
||||
baseMinterAcquisitionMethod,
|
||||
wallet.initialized,
|
||||
selectedCollectionAddress,
|
||||
collectionTokenCount,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="mx-10 mb-4 rounded border-2 border-white/20">
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={baseMinterAcquisitionMethod === 'new'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio5"
|
||||
name="inlineRadioOptions5"
|
||||
onClick={() => {
|
||||
setBaseMinterAcquisitionMethod('new')
|
||||
}}
|
||||
type="radio"
|
||||
value="New"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio5"
|
||||
>
|
||||
Create a New 1/1 Collection
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={baseMinterAcquisitionMethod === 'existing'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio6"
|
||||
name="inlineRadioOptions6"
|
||||
onClick={() => {
|
||||
setBaseMinterAcquisitionMethod('existing')
|
||||
}}
|
||||
type="radio"
|
||||
value="Existing"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio6"
|
||||
>
|
||||
Add a New Token to an Existing 1/1 Collection
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{baseMinterAcquisitionMethod === 'existing' && (
|
||||
<div>
|
||||
<div className={clsx('my-4 mx-10')}>
|
||||
<Conditional test={myBaseMinterContracts.length !== 0}>
|
||||
<select
|
||||
className="mt-4 w-full max-w-3xl text-base bg-white/10 select select-bordered"
|
||||
onChange={(e) => {
|
||||
existingBaseMinterState.onChange(e.target.value.slice(e.target.value.indexOf('stars1')))
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<option className="mt-2 text-lg bg-[#1A1A1A]" disabled selected>
|
||||
Select one of your existing 1/1 collections
|
||||
</option>
|
||||
{renderBaseMinterContracts()}
|
||||
</select>
|
||||
</Conditional>
|
||||
<Conditional test={myBaseMinterContracts.length === 0}>
|
||||
<div className="flex flex-col">
|
||||
<Conditional test={wallet.initialized}>
|
||||
<Alert className="my-2 w-[90%]" type="info">
|
||||
No previous 1/1 collections were found. You may create a new 1/1 collection or fill in the minter
|
||||
contract address manually.
|
||||
</Alert>
|
||||
<TextInput
|
||||
className="w-3/5"
|
||||
defaultValue={existingBaseMinterState.value}
|
||||
{...existingBaseMinterState}
|
||||
isRequired
|
||||
/>
|
||||
</Conditional>
|
||||
<Conditional test={!wallet.initialized}>
|
||||
<Alert className="my-2 w-[90%]" type="warning">
|
||||
Please connect your wallet first.
|
||||
</Alert>
|
||||
</Conditional>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,28 +4,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { SG721_UPDATABLE_CODE_ID } from 'utils/constants'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import { TextInput } from '../../forms/FormInput'
|
||||
import type { MinterType } from '../actions/Combobox'
|
||||
import type { UploadMethod } from './UploadDetails'
|
||||
|
||||
interface CollectionDetailsProps {
|
||||
onChange: (data: CollectionDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod
|
||||
coverImageUrl: string
|
||||
minterType: MinterType
|
||||
}
|
||||
|
||||
export interface CollectionDetailsDataProps {
|
||||
@@ -34,18 +26,10 @@ export interface CollectionDetailsDataProps {
|
||||
symbol: string
|
||||
imageFile: File[]
|
||||
externalLink?: string
|
||||
startTradingTime?: string
|
||||
explicit: boolean
|
||||
updatable: boolean
|
||||
}
|
||||
|
||||
export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minterType }: CollectionDetailsProps) => {
|
||||
export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: CollectionDetailsProps) => {
|
||||
const [coverImage, setCoverImage] = useState<File | null>(null)
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
||||
const [explicit, setExplicit] = useState<boolean>(false)
|
||||
const [updatable, setUpdatable] = useState<boolean>(false)
|
||||
|
||||
const initialRender = useRef(true)
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
@@ -82,28 +66,15 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
|
||||
description: descriptionState.value,
|
||||
symbol: symbolState.value,
|
||||
imageFile: coverImage ? [coverImage] : [],
|
||||
externalLink: externalLinkState.value || undefined,
|
||||
startTradingTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
explicit,
|
||||
updatable,
|
||||
externalLink: externalLinkState.value,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
toast.error(error.message)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
nameState.value,
|
||||
descriptionState.value,
|
||||
symbolState.value,
|
||||
externalLinkState.value,
|
||||
coverImage,
|
||||
timestamp,
|
||||
explicit,
|
||||
updatable,
|
||||
])
|
||||
}, [nameState.value, descriptionState.value, coverImage, externalLinkState.value])
|
||||
|
||||
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files === null) return toast.error('Error selecting cover image')
|
||||
@@ -121,133 +92,18 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
|
||||
reader.readAsArrayBuffer(event.target.files[0])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRender.current) {
|
||||
initialRender.current = false
|
||||
} else if (updatable) {
|
||||
toast.success('Token metadata will be updatable upon collection creation.', {
|
||||
style: { maxWidth: 'none' },
|
||||
icon: '✅📝',
|
||||
})
|
||||
} else {
|
||||
toast.error('Token metadata will not be updatable upon collection creation.', {
|
||||
style: { maxWidth: 'none' },
|
||||
icon: '⛔🔏',
|
||||
})
|
||||
}
|
||||
}, [updatable])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormGroup subtitle="Information about your collection" title="Collection Details">
|
||||
<div className={clsx(minterType === 'base' ? 'grid grid-cols-2 -ml-16 max-w-5xl' : '')}>
|
||||
<div className={clsx(minterType === 'base' ? 'ml-0' : '')}>
|
||||
<TextInput {...nameState} isRequired />
|
||||
<TextInput className="mt-2" {...descriptionState} isRequired />
|
||||
<TextInput className="mt-2" {...symbolState} isRequired />
|
||||
</div>
|
||||
<div className={clsx(minterType === 'base' ? 'ml-10' : '')}>
|
||||
<TextInput className={clsx(minterType === 'base' ? 'mt-0' : 'mt-2')} {...externalLinkState} />
|
||||
{/* Currently trading starts immediately for 1/1 Collections */}
|
||||
<Conditional test={minterType !== 'base'}>
|
||||
<FormControl
|
||||
className={clsx(minterType === 'base' ? 'mt-10' : 'mt-2')}
|
||||
htmlId="timestamp"
|
||||
subtitle="Trading start time offset will be set as 2 weeks by default."
|
||||
title="Trading Start Time (optional)"
|
||||
>
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
</Conditional>
|
||||
<Conditional test={minterType === 'base'}>
|
||||
<div
|
||||
className={clsx(minterType === 'base' ? 'flex flex-col -ml-6 space-y-2' : 'flex flex-col space-y-2')}
|
||||
>
|
||||
<div>
|
||||
<div className="flex mt-9 ml-6">
|
||||
<span className="mt-1 ml-[2px] text-sm first-letter:capitalize">
|
||||
Does the collection contain explicit content?
|
||||
</span>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={explicit}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio1"
|
||||
name="explicitRadioOptions1"
|
||||
onClick={() => {
|
||||
setExplicit(true)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio1"
|
||||
>
|
||||
YES
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={!explicit}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio2"
|
||||
name="explicitRadioOptions2"
|
||||
onClick={() => {
|
||||
setExplicit(false)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio2"
|
||||
>
|
||||
NO
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={SG721_UPDATABLE_CODE_ID > 0}>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label={
|
||||
<div className="grid grid-flow-row">
|
||||
<span>
|
||||
ℹ️ When enabled, the metadata for tokens can be updated after the collection is created until
|
||||
the collection is frozen by the creator.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<div className={clsx('flex flex-col mt-11 space-y-2 w-full form-control')}>
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<span className="mr-4 font-bold">Updatable Token Metadata</span>
|
||||
<input
|
||||
checked={updatable}
|
||||
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setUpdatable(!updatable)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
<TextInput {...nameState} isRequired />
|
||||
<TextInput {...descriptionState} isRequired />
|
||||
<TextInput {...symbolState} isRequired />
|
||||
|
||||
<FormControl
|
||||
className={clsx(minterType === 'base' ? '-ml-16' : '')}
|
||||
isRequired={uploadMethod === 'new'}
|
||||
title="Cover Image"
|
||||
>
|
||||
<FormControl isRequired={uploadMethod === 'new'} title="Cover Image">
|
||||
{uploadMethod === 'new' && (
|
||||
<input
|
||||
accept="image/*"
|
||||
className={clsx(
|
||||
minterType === 'base' ? 'w-1/2' : 'w-full',
|
||||
'p-[13px] rounded border-2 border-white/20 border-dashed cursor-pointer h-18',
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
@@ -266,9 +122,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
|
||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
||||
<img
|
||||
alt="no-preview-available"
|
||||
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
|
||||
coverImageUrl.lastIndexOf('ipfs://') + 7,
|
||||
)}`}
|
||||
src={`https://ipfs.io/ipfs/${coverImageUrl.substring(coverImageUrl.lastIndexOf('ipfs://') + 7)}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -281,85 +135,8 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
|
||||
<span className="italic font-light ">Waiting for cover image URL to be specified.</span>
|
||||
)}
|
||||
</FormControl>
|
||||
<Conditional test={minterType !== 'base'}>
|
||||
<div className={clsx(minterType === 'base' ? 'flex flex-col -ml-6 space-y-2' : 'flex flex-col space-y-2')}>
|
||||
<div>
|
||||
<div className="flex mt-4">
|
||||
<span className="mt-1 ml-[2px] text-sm first-letter:capitalize">
|
||||
Does the collection contain explicit content?
|
||||
</span>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={explicit}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio1"
|
||||
name="explicitRadioOptions1"
|
||||
onClick={() => {
|
||||
setExplicit(true)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio1"
|
||||
>
|
||||
YES
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={!explicit}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio2"
|
||||
name="explicitRadioOptions2"
|
||||
onClick={() => {
|
||||
setExplicit(false)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio2"
|
||||
>
|
||||
NO
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={SG721_UPDATABLE_CODE_ID > 0}>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label={
|
||||
<div className="grid grid-flow-row">
|
||||
<span>
|
||||
ℹ️ When enabled, the metadata for tokens can be updated after the collection is created until the
|
||||
collection is frozen by the creator.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
minterType === 'base'
|
||||
? 'flex flex-col -ml-16 space-y-2 w-1/2 form-control'
|
||||
: 'flex flex-col space-y-2 w-3/4 form-control',
|
||||
)}
|
||||
>
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<span className="mr-4 font-bold">Updatable Token Metadata</span>
|
||||
<input
|
||||
checked={updatable}
|
||||
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setUpdatable(!updatable)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
|
||||
<TextInput {...externalLinkState} />
|
||||
</FormGroup>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,49 +1,27 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { vendingMinterList } from 'config/minter'
|
||||
import type { TokenInfo } from 'config/token'
|
||||
import { stars, tokensList } from 'config/token'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import { useWallet } from '../../../contexts/wallet'
|
||||
import { NumberInput, TextInput } from '../../forms/FormInput'
|
||||
import { NumberInput } from '../../forms/FormInput'
|
||||
import type { UploadMethod } from './UploadDetails'
|
||||
|
||||
interface MintingDetailsProps {
|
||||
onChange: (data: MintingDetailsDataProps) => void
|
||||
numberOfTokens: number | undefined
|
||||
uploadMethod: UploadMethod
|
||||
minimumMintPrice: number
|
||||
mintingTokenFromFactory?: TokenInfo
|
||||
}
|
||||
|
||||
export type TokenType = 'fungible' | 'non-fungible'
|
||||
export interface MintingDetailsDataProps {
|
||||
numTokens: number
|
||||
unitPrice: string
|
||||
perAddressLimit: number
|
||||
startTime: string
|
||||
paymentAddress?: string
|
||||
selectedMintToken?: TokenInfo
|
||||
selectesTokenType?: TokenType
|
||||
}
|
||||
|
||||
export const MintingDetails = ({
|
||||
onChange,
|
||||
numberOfTokens,
|
||||
uploadMethod,
|
||||
minimumMintPrice,
|
||||
mintingTokenFromFactory,
|
||||
}: MintingDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
|
||||
export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: MintingDetailsProps) => {
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
||||
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
|
||||
|
||||
const numberOfTokensState = useNumberInputState({
|
||||
id: 'numberoftokens',
|
||||
@@ -57,9 +35,7 @@ export const MintingDetails = ({
|
||||
id: 'unitPrice',
|
||||
name: 'unitPrice',
|
||||
title: 'Unit Price',
|
||||
subtitle: `Price of each token (min. ${minimumMintPrice} ${
|
||||
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
|
||||
})`,
|
||||
subtitle: 'Price of each token (min. 50 STARS)',
|
||||
placeholder: '50',
|
||||
})
|
||||
|
||||
@@ -71,49 +47,17 @@ export const MintingDetails = ({
|
||||
placeholder: '1',
|
||||
})
|
||||
|
||||
const paymentAddressState = useInputState({
|
||||
id: 'payment-address',
|
||||
name: 'paymentAddress',
|
||||
title: 'Payment Address (optional)',
|
||||
subtitle: 'Address to receive minting revenues (defaults to current wallet address)',
|
||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||
})
|
||||
|
||||
const resolvePaymentAddress = async () => {
|
||||
await resolveAddress(paymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
paymentAddressState.onChange(resolvedAddress)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void resolvePaymentAddress()
|
||||
}, [paymentAddressState.value])
|
||||
|
||||
useEffect(() => {
|
||||
if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens)
|
||||
const data: MintingDetailsDataProps = {
|
||||
numTokens: numberOfTokensState.value,
|
||||
unitPrice: unitPriceState.value
|
||||
? (Number(unitPriceState.value) * 1_000_000).toString()
|
||||
: unitPriceState.value === 0
|
||||
? '0'
|
||||
: '',
|
||||
unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : '',
|
||||
perAddressLimit: perAddressLimitState.value,
|
||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
paymentAddress: paymentAddressState.value.trim(),
|
||||
selectedMintToken,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
numberOfTokens,
|
||||
numberOfTokensState.value,
|
||||
unitPriceState.value,
|
||||
perAddressLimitState.value,
|
||||
timestamp,
|
||||
paymentAddressState.value,
|
||||
selectedMintToken,
|
||||
])
|
||||
}, [numberOfTokens, numberOfTokensState.value, unitPriceState.value, perAddressLimitState.value, timestamp])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -124,28 +68,12 @@ export const MintingDetails = ({
|
||||
isRequired
|
||||
value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value}
|
||||
/>
|
||||
<div className="flex flex-row items-center">
|
||||
<NumberInput {...unitPriceState} isRequired />
|
||||
<select
|
||||
className="py-[9px] px-4 mt-14 ml-2 placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
|
||||
onChange={(e) => setSelectedMintToken(tokensList.find((t) => t.displayName === e.target.value))}
|
||||
>
|
||||
{vendingMinterList
|
||||
.filter((minter) => minter.factoryAddress !== undefined && minter.updatable === false)
|
||||
.map((minter) => (
|
||||
<option key={minter.id} className="bg-black" value={minter.supportedToken.displayName}>
|
||||
{minter.supportedToken.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<NumberInput {...unitPriceState} isRequired />
|
||||
<NumberInput {...perAddressLimitState} isRequired />
|
||||
<FormControl htmlId="timestamp" isRequired subtitle="Minting start time (local)" title="Start Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<TextInput className="p-4 mt-5" {...paymentAddressState} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { resolveAddress } from '../../../utils/resolveAddress'
|
||||
import { NumberInput, TextInput } from '../../forms/FormInput'
|
||||
|
||||
interface RoyaltyDetailsProps {
|
||||
@@ -20,7 +18,6 @@ export interface RoyaltyDetailsDataProps {
|
||||
type RoyaltyState = 'none' | 'new'
|
||||
|
||||
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
|
||||
|
||||
const royaltyPaymentAddressState = useInputState({
|
||||
@@ -36,27 +33,16 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '5%',
|
||||
placeholder: '8%',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
void resolveAddress(
|
||||
royaltyPaymentAddressState.value
|
||||
.toLowerCase()
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, ''),
|
||||
wallet,
|
||||
).then((royaltyPaymentAddress) => {
|
||||
royaltyPaymentAddressState.onChange(royaltyPaymentAddress)
|
||||
const data: RoyaltyDetailsDataProps = {
|
||||
royaltyType: royaltyState,
|
||||
paymentAddress: royaltyPaymentAddressState.value,
|
||||
share: Number(royaltyShareState.value),
|
||||
}
|
||||
onChange(data)
|
||||
})
|
||||
const data: RoyaltyDetailsDataProps = {
|
||||
royaltyType: royaltyState,
|
||||
paymentAddress: royaltyPaymentAddressState.value,
|
||||
share: Number(royaltyShareState.value),
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value])
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
// eslint-disable-next-line eslint-comments/disable-enable-pair
|
||||
/* eslint-disable @typescript-eslint/no-loop-func */
|
||||
import clsx from 'clsx'
|
||||
import { Alert } from 'components/Alert'
|
||||
@@ -10,27 +7,17 @@ import { AssetsPreview } from 'components/AssetsPreview'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { MetadataInput } from 'components/MetadataInput'
|
||||
import { MetadataModal } from 'components/MetadataModal'
|
||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import type { UploadServiceType } from 'services/upload'
|
||||
import { uid } from 'utils/random'
|
||||
import { naturalCompare } from 'utils/sort'
|
||||
|
||||
import type { MinterType } from '../actions/Combobox'
|
||||
import type { BaseMinterAcquisitionMethod } from './BaseMinterDetails'
|
||||
|
||||
export type UploadMethod = 'new' | 'existing'
|
||||
|
||||
interface UploadDetailsProps {
|
||||
onChange: (value: UploadDetailsDataProps) => void
|
||||
minterType: MinterType
|
||||
baseMinterAcquisitionMethod?: BaseMinterAcquisitionMethod
|
||||
}
|
||||
|
||||
export interface UploadDetailsDataProps {
|
||||
@@ -43,10 +30,9 @@ export interface UploadDetailsDataProps {
|
||||
uploadMethod: UploadMethod
|
||||
baseTokenURI?: string
|
||||
imageUrl?: string
|
||||
baseMinterMetadataFile?: File
|
||||
}
|
||||
|
||||
export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMethod }: UploadDetailsProps) => {
|
||||
export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
|
||||
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
|
||||
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
|
||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
||||
@@ -54,11 +40,6 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
|
||||
const [refreshMetadata, setRefreshMetadata] = useState(false)
|
||||
|
||||
const [baseMinterMetadataFile, setBaseMinterMetadataFile] = useState<File | undefined>()
|
||||
|
||||
const assetFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nftStorageApiKeyState = useInputState({
|
||||
id: 'nft-storage-api-key',
|
||||
name: 'nftStorageApiKey',
|
||||
@@ -84,7 +65,7 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
const baseTokenUriState = useInputState({
|
||||
id: 'baseTokenUri',
|
||||
name: 'baseTokenUri',
|
||||
title: minterType === 'vending' ? 'Base Token URI' : 'Token URI',
|
||||
title: 'Base Token URI',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
@@ -101,24 +82,16 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
setAssetFilesArray([])
|
||||
setMetadataFilesArray([])
|
||||
if (event.target.files === null) return
|
||||
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])
|
||||
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.')
|
||||
addLogItem({
|
||||
id: uid(),
|
||||
message: 'The file names should be in numerical order starting from 1.',
|
||||
type: 'Error',
|
||||
timestamp: new Date(),
|
||||
})
|
||||
//clear the input
|
||||
event.target.value = ''
|
||||
return
|
||||
}
|
||||
//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])
|
||||
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.')
|
||||
//clear the input
|
||||
event.target.value = ''
|
||||
return
|
||||
}
|
||||
}
|
||||
let loadedFileCount = 0
|
||||
@@ -146,30 +119,20 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMetadataFilesArray([])
|
||||
if (event.target.files === null) return toast.error('No files selected.')
|
||||
if (
|
||||
(minterType === 'vending' || (minterType === 'base' && assetFilesArray.length > 1)) &&
|
||||
event.target.files.length !== assetFilesArray.length
|
||||
) {
|
||||
if (event.target.files.length !== assetFilesArray.length) {
|
||||
event.target.value = ''
|
||||
return toast.error('The number of metadata files should be equal to the number of asset files.')
|
||||
}
|
||||
if (minterType === 'vending' || (minterType === 'base' && assetFilesArray.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])
|
||||
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.')
|
||||
addLogItem({
|
||||
id: uid(),
|
||||
message: 'The file names should be in numerical order starting from 1.',
|
||||
type: 'Error',
|
||||
timestamp: new Date(),
|
||||
})
|
||||
event.target.value = ''
|
||||
return
|
||||
}
|
||||
//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])
|
||||
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.')
|
||||
//clear the input
|
||||
event.target.value = ''
|
||||
return
|
||||
}
|
||||
}
|
||||
let loadedFileCount = 0
|
||||
@@ -177,24 +140,11 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
let reader: FileReader
|
||||
for (let i = 0; i < event.target.files.length; i++) {
|
||||
reader = new FileReader()
|
||||
reader.onload = async (e) => {
|
||||
reader.onload = (e) => {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
if (!event.target.files) return toast.error('No files selected.')
|
||||
const metadataFile = new File([e.target.result], event.target.files[i].name, { type: 'application/json' })
|
||||
files.push(metadataFile)
|
||||
try {
|
||||
const parsedMetadata = JSON.parse(await metadataFile.text())
|
||||
if (!parsedMetadata || typeof parsedMetadata !== 'object') {
|
||||
event.target.value = ''
|
||||
setMetadataFilesArray([])
|
||||
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
event.target.value = ''
|
||||
setMetadataFilesArray([])
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
|
||||
}
|
||||
}
|
||||
reader.readAsText(event.target.files[i], 'utf8')
|
||||
reader.onloadend = () => {
|
||||
@@ -212,18 +162,12 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
setRefreshMetadata((prev) => !prev)
|
||||
}
|
||||
|
||||
const updateMetadataFileArray = (updatedMetadataFile: File) => {
|
||||
const updateMetadataFileArray = async (updatedMetadataFile: File) => {
|
||||
metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile
|
||||
console.log('Updated Metadata File:')
|
||||
console.log(JSON.parse(await metadataFilesArray[metadataFileArrayIndex]?.text()))
|
||||
}
|
||||
|
||||
const updateBaseMinterMetadataFile = (updatedMetadataFile: File) => {
|
||||
setBaseMinterMetadataFile(updatedMetadataFile)
|
||||
console.log('Updated Base Minter Metadata File:')
|
||||
console.log(baseMinterMetadataFile)
|
||||
}
|
||||
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
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: UploadDetailsDataProps = {
|
||||
@@ -234,26 +178,12 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
pinataApiKey: pinataApiKeyState.value,
|
||||
pinataSecretKey: pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
baseTokenURI: baseTokenUriState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(regex, '')
|
||||
.trim(),
|
||||
imageUrl: coverImageUrlState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(regex, '')
|
||||
.trim(),
|
||||
baseMinterMetadataFile,
|
||||
baseTokenURI: baseTokenUriState.value,
|
||||
imageUrl: coverImageUrlState.value,
|
||||
}
|
||||
onChange(data)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
toast.error(error.message)
|
||||
}
|
||||
}, [
|
||||
assetFilesArray,
|
||||
@@ -265,21 +195,17 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
uploadMethod,
|
||||
baseTokenUriState.value,
|
||||
coverImageUrlState.value,
|
||||
refreshMetadata,
|
||||
baseMinterMetadataFile,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
|
||||
setMetadataFilesArray([])
|
||||
if (assetFilesRef.current) assetFilesRef.current.value = ''
|
||||
setAssetFilesArray([])
|
||||
setMetadataFilesArray([])
|
||||
baseTokenUriState.onChange('')
|
||||
coverImageUrlState.onChange('')
|
||||
}, [uploadMethod, minterType, baseMinterAcquisitionMethod])
|
||||
}, [uploadMethod])
|
||||
|
||||
return (
|
||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
||||
<div className="justify-items-start mt-5 mb-3 rounded border border-2 border-white/20 flex-column">
|
||||
<div className="flex justify-center">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
@@ -297,7 +223,7 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio2"
|
||||
>
|
||||
{minterType === 'base' ? 'Upload assets & metadata' : 'Upload assets & metadata'}
|
||||
Upload assets & metadata
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
@@ -316,13 +242,13 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio1"
|
||||
>
|
||||
{minterType === 'base' ? 'Use an existing Token URI' : 'Use an existing base URI'}
|
||||
Use an existing base URI
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 py-5 pb-8">
|
||||
<Conditional test={uploadMethod === 'existing' && minterType === 'vending'}>
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<div className="ml-3 flex-column">
|
||||
<p className="mb-5 ml-5">
|
||||
Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a
|
||||
@@ -337,53 +263,11 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
and upload your assets & metadata manually to get a base URI for your collection.
|
||||
</p>
|
||||
<div>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
className="mb-2 ml-20"
|
||||
label="The base token URI that points to the IPFS folder containing the metadata files."
|
||||
placement="top"
|
||||
>
|
||||
<TextInput {...baseTokenUriState} className="ml-4 w-1/2" />
|
||||
</Tooltip>
|
||||
<TextInput {...baseTokenUriState} className="w-1/2" />
|
||||
</div>
|
||||
<Conditional test={minterType !== 'base'}>
|
||||
<div>
|
||||
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</Conditional>
|
||||
<Conditional test={uploadMethod === 'existing' && minterType === 'base'}>
|
||||
<div className="ml-3 flex-column">
|
||||
<p className="mb-5 ml-5">
|
||||
Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a
|
||||
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
||||
NFT.Storage
|
||||
</Anchor>{' '}
|
||||
or{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
||||
Pinata
|
||||
</Anchor>{' '}
|
||||
and upload your asset & metadata manually to get a URI for your token before minting.
|
||||
</p>
|
||||
<div>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
className="mb-2 ml-4"
|
||||
label="The token URI that points directly to the metadata file stored on IPFS."
|
||||
placement="top"
|
||||
>
|
||||
<TextInput {...baseTokenUriState} className="ml-4 w-1/2" />
|
||||
</Tooltip>
|
||||
<TextInput {...coverImageUrlState} className="mt-2 w-1/2" />
|
||||
</div>
|
||||
<Conditional
|
||||
test={minterType !== 'base' || (minterType === 'base' && baseMinterAcquisitionMethod === 'new')}
|
||||
>
|
||||
<div>
|
||||
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</Conditional>
|
||||
<Conditional test={uploadMethod === 'new'}>
|
||||
@@ -472,7 +356,7 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="image/*, audio/*, video/*, .html"
|
||||
accept="image/*, audio/*, video/*"
|
||||
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',
|
||||
@@ -480,7 +364,6 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
id="assetFiles"
|
||||
multiple
|
||||
onChange={selectAssets}
|
||||
ref={assetFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
@@ -492,11 +375,7 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="metadataFiles"
|
||||
>
|
||||
{minterType === 'vending'
|
||||
? 'Metadata Selection'
|
||||
: assetFilesArray.length === 1
|
||||
? 'Metadata Selection (optional)'
|
||||
: 'Metadata Selection'}
|
||||
Metadata Selection
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -513,48 +392,24 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
|
||||
id="metadataFiles"
|
||||
multiple
|
||||
onChange={selectMetadata}
|
||||
ref={metadataFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Conditional test={assetFilesArray.length >= 1}>
|
||||
<MetadataModal
|
||||
assetFile={assetFilesArray[metadataFileArrayIndex]}
|
||||
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
|
||||
refresher={refreshMetadata}
|
||||
updateMetadata={updateMetadataFileArray}
|
||||
/>
|
||||
</Conditional>
|
||||
|
||||
<MetadataModal
|
||||
assetFile={assetFilesArray[metadataFileArrayIndex]}
|
||||
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
|
||||
refresher={refreshMetadata}
|
||||
updateMetadata={updateMetadataFileArray}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Conditional test={assetFilesArray.length > 0 && minterType === 'vending'}>
|
||||
<Conditional test={assetFilesArray.length > 0}>
|
||||
<AssetsPreview assetFilesArray={assetFilesArray} updateMetadataFileIndex={updateMetadataFileIndex} />
|
||||
</Conditional>
|
||||
<Conditional test={assetFilesArray.length > 0 && minterType === 'base'}>
|
||||
<Conditional test={assetFilesArray.length === 1}>
|
||||
<SingleAssetPreview
|
||||
relatedAsset={assetFilesArray[0]}
|
||||
subtitle={`Asset filename: ${assetFilesArray[0]?.name}`}
|
||||
updateMetadataFileIndex={updateMetadataFileIndex}
|
||||
/>
|
||||
</Conditional>
|
||||
<Conditional test={assetFilesArray.length > 1}>
|
||||
<AssetsPreview
|
||||
assetFilesArray={assetFilesArray}
|
||||
updateMetadataFileIndex={updateMetadataFileIndex}
|
||||
/>
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
</div>
|
||||
<Conditional test={minterType === 'base' && assetFilesArray.length === 1}>
|
||||
<MetadataInput
|
||||
selectedAssetFile={assetFilesArray[0]}
|
||||
selectedMetadataFile={metadataFilesArray[0]}
|
||||
updateMetadataToUpload={updateBaseMinterMetadataFile}
|
||||
/>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { AddressList } from 'components/forms/AddressList'
|
||||
import { useAddressListState } from 'components/forms/AddressList.hooks'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
|
||||
import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
|
||||
import type { TokenInfo } from 'config/token'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
|
||||
import { Conditional } from '../../Conditional'
|
||||
import { AddressInput, NumberInput } from '../../forms/FormInput'
|
||||
@@ -19,35 +11,26 @@ import { WhitelistUpload } from '../../WhitelistUpload'
|
||||
|
||||
interface WhitelistDetailsProps {
|
||||
onChange: (data: WhitelistDetailsDataProps) => void
|
||||
mintingTokenFromFactory?: TokenInfo
|
||||
}
|
||||
|
||||
export interface WhitelistDetailsDataProps {
|
||||
whitelistState: WhitelistState
|
||||
whitelistType: WhitelistType
|
||||
whitelistType: WhitelistState
|
||||
contractAddress?: string
|
||||
members?: string[] | WhitelistFlexMember[]
|
||||
members?: string[]
|
||||
unitPrice?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
perAddressLimit?: number
|
||||
memberLimit?: number
|
||||
admins?: string[]
|
||||
adminsMutable?: boolean
|
||||
}
|
||||
|
||||
type WhitelistState = 'none' | 'existing' | 'new'
|
||||
|
||||
type WhitelistType = 'standard' | 'flex'
|
||||
|
||||
export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: WhitelistDetailsProps) => {
|
||||
export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
|
||||
const [whitelistState, setWhitelistState] = useState<WhitelistState>('none')
|
||||
const [whitelistType, setWhitelistType] = useState<WhitelistType>('standard')
|
||||
const [startDate, setStartDate] = useState<Date | undefined>(undefined)
|
||||
const [endDate, setEndDate] = useState<Date | undefined>(undefined)
|
||||
const [whitelistStandardArray, setWhitelistStandardArray] = useState<string[]>([])
|
||||
const [whitelistFlexArray, setWhitelistFlexArray] = useState<WhitelistFlexMember[]>([])
|
||||
const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
|
||||
const [whitelistArray, setWhitelistArray] = useState<string[]>([])
|
||||
|
||||
const whitelistAddressState = useInputState({
|
||||
id: 'whitelist-address',
|
||||
@@ -56,13 +39,11 @@ export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: Whitelis
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const unitPriceState = useNumberInputState({
|
||||
const uniPriceState = useNumberInputState({
|
||||
id: 'unit-price',
|
||||
name: 'unitPrice',
|
||||
title: 'Unit Price',
|
||||
subtitle: `Token price for whitelisted addresses \n (min. 0 ${
|
||||
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
|
||||
})`,
|
||||
subtitle: 'Token price for whitelisted addresses \n (min. 25 STARS)',
|
||||
placeholder: '25',
|
||||
})
|
||||
|
||||
@@ -82,64 +63,32 @@ export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: Whitelis
|
||||
placeholder: '5',
|
||||
})
|
||||
|
||||
const addressListState = useAddressListState()
|
||||
|
||||
const whitelistFileOnChange = (data: string[]) => {
|
||||
setWhitelistStandardArray(data)
|
||||
setWhitelistArray(data)
|
||||
}
|
||||
|
||||
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
|
||||
setWhitelistFlexArray(whitelistData)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setWhitelistStandardArray([])
|
||||
setWhitelistFlexArray([])
|
||||
}, [whitelistType])
|
||||
|
||||
useEffect(() => {
|
||||
const data: WhitelistDetailsDataProps = {
|
||||
whitelistState,
|
||||
whitelistType,
|
||||
contractAddress: whitelistAddressState.value
|
||||
.toLowerCase()
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, ''),
|
||||
members: whitelistType === 'standard' ? whitelistStandardArray : whitelistFlexArray,
|
||||
unitPrice: unitPriceState.value
|
||||
? (Number(unitPriceState.value) * 1_000_000).toString()
|
||||
: unitPriceState.value === 0
|
||||
? '0'
|
||||
: undefined,
|
||||
whitelistType: whitelistState,
|
||||
contractAddress: whitelistAddressState.value,
|
||||
members: whitelistArray,
|
||||
unitPrice: uniPriceState.value ? (Number(uniPriceState.value) * 1_000_000).toString() : '',
|
||||
startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '',
|
||||
endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '',
|
||||
perAddressLimit: perAddressLimitState.value,
|
||||
memberLimit: memberLimitState.value,
|
||||
admins: [
|
||||
...new Set(
|
||||
addressListState.values
|
||||
.map((a) => a.address.trim())
|
||||
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')),
|
||||
),
|
||||
],
|
||||
adminsMutable,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
whitelistAddressState.value,
|
||||
unitPriceState.value,
|
||||
uniPriceState.value,
|
||||
memberLimitState.value,
|
||||
perAddressLimitState.value,
|
||||
startDate,
|
||||
endDate,
|
||||
whitelistStandardArray,
|
||||
whitelistFlexArray,
|
||||
whitelistArray,
|
||||
whitelistState,
|
||||
addressListState.values,
|
||||
adminsMutable,
|
||||
])
|
||||
|
||||
return (
|
||||
@@ -153,7 +102,6 @@ export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: Whitelis
|
||||
name="whitelistRadioOptions1"
|
||||
onClick={() => {
|
||||
setWhitelistState('none')
|
||||
setWhitelistType('standard')
|
||||
}}
|
||||
type="radio"
|
||||
value="None"
|
||||
@@ -210,54 +158,11 @@ export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: Whitelis
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={whitelistState === 'new'}>
|
||||
<div className="flex justify-between mb-5 ml-6 max-w-[300px] text-lg font-bold">
|
||||
<div className="form-check form-check-inline">
|
||||
<input
|
||||
checked={whitelistType === 'standard'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio7"
|
||||
name="inlineRadioOptions7"
|
||||
onClick={() => {
|
||||
setWhitelistType('standard')
|
||||
}}
|
||||
type="radio"
|
||||
value="nft-storage"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio7"
|
||||
>
|
||||
Standard Whitelist
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-check form-check-inline">
|
||||
<input
|
||||
checked={whitelistType === 'flex'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio8"
|
||||
name="inlineRadioOptions8"
|
||||
onClick={() => {
|
||||
setWhitelistType('flex')
|
||||
}}
|
||||
type="radio"
|
||||
value="flex"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio8"
|
||||
>
|
||||
Whitelist Flex
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2">
|
||||
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
|
||||
<NumberInput isRequired {...unitPriceState} />
|
||||
<NumberInput isRequired {...uniPriceState} />
|
||||
<NumberInput isRequired {...memberLimitState} />
|
||||
<Conditional test={whitelistType === 'standard'}>
|
||||
<NumberInput isRequired {...perAddressLimitState} />
|
||||
</Conditional>
|
||||
<NumberInput isRequired {...perAddressLimitState} />
|
||||
<FormControl
|
||||
htmlId="start-date"
|
||||
isRequired
|
||||
@@ -276,46 +181,11 @@ export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: Whitelis
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<div>
|
||||
<div className="mt-2 ml-3 w-[65%] form-control">
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<span className="mr-4 font-bold">Mutable Administrator Addresses</span>
|
||||
<input
|
||||
checked={adminsMutable}
|
||||
className={`toggle ${adminsMutable ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setAdminsMutable(!adminsMutable)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="my-4 ml-4">
|
||||
<AddressList
|
||||
entries={addressListState.entries}
|
||||
isRequired
|
||||
onAdd={addressListState.add}
|
||||
onChange={addressListState.update}
|
||||
onRemove={addressListState.remove}
|
||||
subtitle="The list of administrator addresses"
|
||||
title="Administrator Addresses"
|
||||
/>
|
||||
</div>
|
||||
<Conditional test={whitelistType === 'standard'}>
|
||||
<FormGroup subtitle="TXT file that contains the whitelisted addresses" title="Whitelist File">
|
||||
<WhitelistUpload onChange={whitelistFileOnChange} />
|
||||
</FormGroup>
|
||||
<Conditional test={whitelistStandardArray.length > 0}>
|
||||
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
<Conditional test={whitelistType === 'flex'}>
|
||||
<FormGroup
|
||||
subtitle="CSV file that contains the whitelisted addresses and their corresponding mint counts"
|
||||
title="Whitelist File"
|
||||
>
|
||||
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
|
||||
</FormGroup>
|
||||
<Conditional test={whitelistFlexArray.length > 0}>
|
||||
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
|
||||
</Conditional>
|
||||
<FormGroup subtitle="TXT file that contains the whitelisted addresses" title="Whitelist File">
|
||||
<WhitelistUpload onChange={whitelistFileOnChange} />
|
||||
</FormGroup>
|
||||
<Conditional test={whitelistArray.length > 0}>
|
||||
<JsonPreview content={whitelistArray} initialState title="File Contents" />
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,32 +2,19 @@ import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
import type { MinterType } from '../actions/Combobox'
|
||||
import type { QueryListItem } from './query'
|
||||
import { BASE_QUERY_LIST, OPEN_EDITION_QUERY_LIST, VENDING_QUERY_LIST } from './query'
|
||||
import { QUERY_LIST } from './query'
|
||||
|
||||
export interface QueryComboboxProps {
|
||||
value: QueryListItem | null
|
||||
onChange: (item: QueryListItem) => void
|
||||
minterType?: MinterType
|
||||
}
|
||||
|
||||
export const QueryCombobox = ({ value, onChange, minterType }: QueryComboboxProps) => {
|
||||
export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
const [QUERY_LIST, SET_QUERY_LIST] = useState<QueryListItem[]>(VENDING_QUERY_LIST)
|
||||
|
||||
useEffect(() => {
|
||||
if (minterType === 'base') {
|
||||
SET_QUERY_LIST(BASE_QUERY_LIST)
|
||||
} else if (minterType === 'openEdition') {
|
||||
SET_QUERY_LIST(OPEN_EDITION_QUERY_LIST)
|
||||
} else {
|
||||
SET_QUERY_LIST(VENDING_QUERY_LIST)
|
||||
}
|
||||
}, [minterType])
|
||||
|
||||
const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
@@ -80,7 +67,7 @@ export const QueryCombobox = ({ value, onChange, minterType }: QueryComboboxProp
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
|
||||
@@ -5,37 +5,23 @@ import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput, TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
|
||||
import type { MinterInstance } from 'contracts/minter'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
import { useWallet } from '../../../contexts/wallet'
|
||||
import { resolveAddress } from '../../../utils/resolveAddress'
|
||||
import type { MinterType } from '../actions/Combobox'
|
||||
|
||||
interface CollectionQueriesProps {
|
||||
minterContractAddress: string
|
||||
sg721ContractAddress: string
|
||||
sg721Messages: SG721Instance | undefined
|
||||
vendingMinterMessages: VendingMinterInstance | undefined
|
||||
baseMinterMessages: BaseMinterInstance | undefined
|
||||
openEditionMinterMessages: OpenEditionMinterInstance | undefined
|
||||
minterType: MinterType
|
||||
minterMessages: MinterInstance | undefined
|
||||
}
|
||||
export const CollectionQueries = ({
|
||||
sg721ContractAddress,
|
||||
sg721Messages,
|
||||
minterContractAddress,
|
||||
vendingMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
baseMinterMessages,
|
||||
minterType,
|
||||
minterMessages,
|
||||
}: CollectionQueriesProps) => {
|
||||
const wallet = useWallet()
|
||||
|
||||
const comboboxState = useQueryComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
|
||||
@@ -57,50 +43,25 @@ export const CollectionQueries = ({
|
||||
const address = addressState.value
|
||||
|
||||
const showTokenIdField = type === 'token_info'
|
||||
const showAddressField = type === 'tokens_minted_to_user' || type === 'tokens'
|
||||
const showAddressField = type === 'tokens_minted_to_user'
|
||||
|
||||
const { data: response } = useQuery(
|
||||
[
|
||||
sg721Messages,
|
||||
baseMinterMessages,
|
||||
vendingMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
type,
|
||||
tokenId,
|
||||
address,
|
||||
] as const,
|
||||
[sg721Messages, minterMessages, type, tokenId, address] as const,
|
||||
async ({ queryKey }) => {
|
||||
const [
|
||||
_sg721Messages,
|
||||
_baseMinterMessages_,
|
||||
_vendingMinterMessages,
|
||||
_openEditionMinterMessages,
|
||||
_type,
|
||||
_tokenId,
|
||||
_address,
|
||||
] = queryKey
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const res = await resolveAddress(_address, wallet).then(async (resolvedAddress) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const result = await dispatchQuery({
|
||||
tokenId: _tokenId,
|
||||
vendingMinterMessages: _vendingMinterMessages,
|
||||
baseMinterMessages: _baseMinterMessages_,
|
||||
openEditionMinterMessages: _openEditionMinterMessages,
|
||||
sg721Messages: _sg721Messages,
|
||||
address: resolvedAddress,
|
||||
type: _type,
|
||||
})
|
||||
return result
|
||||
const [_sg721Messages, _minterMessages, _type, _tokenId, _address] = queryKey
|
||||
const result = await dispatchQuery({
|
||||
tokenId: _tokenId,
|
||||
minterMessages: _minterMessages,
|
||||
sg721Messages: _sg721Messages,
|
||||
address: _address,
|
||||
type: _type,
|
||||
})
|
||||
return res
|
||||
return result
|
||||
},
|
||||
{
|
||||
placeholderData: null,
|
||||
onError: (error: any) => {
|
||||
if (addressState.value.length > 12 && !addressState.value.includes('.')) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
toast.error(error.message)
|
||||
},
|
||||
enabled: Boolean(sg721ContractAddress && minterContractAddress && type),
|
||||
retry: false,
|
||||
@@ -110,7 +71,7 @@ export const CollectionQueries = ({
|
||||
return (
|
||||
<div className="grid grid-cols-2 mt-4">
|
||||
<div className="mr-2 space-y-8">
|
||||
<QueryCombobox minterType={minterType} {...comboboxState} />
|
||||
<QueryCombobox {...comboboxState} />
|
||||
{showAddressField && <AddressInput {...addressState} />}
|
||||
{showTokenIdField && <TextInput {...tokenIdState} />}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter/contract'
|
||||
import type { MinterInstance } from 'contracts/minter'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
|
||||
export type QueryType = typeof QUERY_TYPES[number]
|
||||
|
||||
@@ -10,12 +8,8 @@ export const QUERY_TYPES = [
|
||||
'mint_price',
|
||||
'num_tokens',
|
||||
'tokens_minted_to_user',
|
||||
'total_mint_count',
|
||||
'tokens',
|
||||
// 'token_owners',
|
||||
'token_info',
|
||||
'config',
|
||||
'status',
|
||||
] as const
|
||||
|
||||
export interface QueryListItem {
|
||||
@@ -24,7 +18,7 @@ export interface QueryListItem {
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const VENDING_QUERY_LIST: QueryListItem[] = [
|
||||
export const QUERY_LIST: QueryListItem[] = [
|
||||
{
|
||||
id: 'collection_info',
|
||||
name: 'Collection Info',
|
||||
@@ -55,85 +49,6 @@ export const VENDING_QUERY_LIST: QueryListItem[] = [
|
||||
name: 'Token Info',
|
||||
description: `Get information about a token in the collection.`,
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
name: 'Minter Config',
|
||||
description: `Query Minter Config`,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
name: 'Minter Status',
|
||||
description: `Query Minter Status`,
|
||||
},
|
||||
]
|
||||
export const BASE_QUERY_LIST: QueryListItem[] = [
|
||||
{
|
||||
id: 'collection_info',
|
||||
name: 'Collection Info',
|
||||
description: `Get information about the collection.`,
|
||||
},
|
||||
{
|
||||
id: 'tokens',
|
||||
name: 'Tokens Minted to User',
|
||||
description: `Get the number of tokens minted in the collection to a user.`,
|
||||
},
|
||||
{
|
||||
id: 'token_info',
|
||||
name: 'Token Info',
|
||||
description: `Get information about a token in the collection.`,
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
name: 'Minter Config',
|
||||
description: `Query Minter Config`,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
name: 'Minter Status',
|
||||
description: `Query Minter Status`,
|
||||
},
|
||||
]
|
||||
export const OPEN_EDITION_QUERY_LIST: QueryListItem[] = [
|
||||
{
|
||||
id: 'collection_info',
|
||||
name: 'Collection Info',
|
||||
description: `Get information about the collection.`,
|
||||
},
|
||||
{
|
||||
id: 'mint_price',
|
||||
name: 'Mint Price',
|
||||
description: `Get the price of minting a token.`,
|
||||
},
|
||||
{
|
||||
id: 'tokens_minted_to_user',
|
||||
name: 'Tokens Minted to User',
|
||||
description: `Get the number of tokens minted in the collection to a user.`,
|
||||
},
|
||||
{
|
||||
id: 'total_mint_count',
|
||||
name: 'Total Mint Count',
|
||||
description: `Get the total number of tokens minted for the collection.`,
|
||||
},
|
||||
// {
|
||||
// id: 'token_owners',
|
||||
// name: 'Token Owners',
|
||||
// description: `Get the list of users who own tokens in the collection.`,
|
||||
// },
|
||||
{
|
||||
id: 'token_info',
|
||||
name: 'Token Info',
|
||||
description: `Get information about a token in the collection.`,
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
name: 'Minter Config',
|
||||
description: `Query Minter Config`,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
name: 'Minter Status',
|
||||
description: `Query Minter Status`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
@@ -144,9 +59,7 @@ export interface DispatchExecuteProps {
|
||||
type Select<T extends QueryType> = T
|
||||
|
||||
export type DispatchQueryArgs = {
|
||||
baseMinterMessages?: BaseMinterInstance
|
||||
vendingMinterMessages?: VendingMinterInstance
|
||||
openEditionMinterMessages?: OpenEditionMinterInstance
|
||||
minterMessages?: MinterInstance
|
||||
sg721Messages?: SG721Instance
|
||||
} & (
|
||||
| { type: undefined }
|
||||
@@ -154,17 +67,13 @@ export type DispatchQueryArgs = {
|
||||
| { type: Select<'mint_price'> }
|
||||
| { type: Select<'num_tokens'> }
|
||||
| { type: Select<'tokens_minted_to_user'>; address: string }
|
||||
| { type: Select<'total_mint_count'> }
|
||||
| { type: Select<'tokens'>; address: string }
|
||||
// | { type: Select<'token_owners'> }
|
||||
| { type: Select<'token_info'>; tokenId: string }
|
||||
| { type: Select<'config'> }
|
||||
| { type: Select<'status'> }
|
||||
)
|
||||
|
||||
export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
||||
const { baseMinterMessages, vendingMinterMessages, openEditionMinterMessages, sg721Messages } = args
|
||||
if (!baseMinterMessages || !vendingMinterMessages || !openEditionMinterMessages || !sg721Messages) {
|
||||
const { minterMessages, sg721Messages } = args
|
||||
if (!minterMessages || !sg721Messages) {
|
||||
throw new Error('Cannot execute actions')
|
||||
}
|
||||
switch (args.type) {
|
||||
@@ -172,33 +81,21 @@ export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
||||
return sg721Messages.collectionInfo()
|
||||
}
|
||||
case 'mint_price': {
|
||||
return vendingMinterMessages.getMintPrice()
|
||||
return minterMessages.getMintPrice()
|
||||
}
|
||||
case 'num_tokens': {
|
||||
return vendingMinterMessages.getMintableNumTokens()
|
||||
return minterMessages.getMintableNumTokens()
|
||||
}
|
||||
case 'tokens_minted_to_user': {
|
||||
return vendingMinterMessages.getMintCount(args.address)
|
||||
}
|
||||
case 'total_mint_count': {
|
||||
return openEditionMinterMessages.getTotalMintCount()
|
||||
}
|
||||
case 'tokens': {
|
||||
return sg721Messages.tokens(args.address)
|
||||
return minterMessages.getMintCount(args.address)
|
||||
}
|
||||
// case 'token_owners': {
|
||||
// return vendingMinterMessages.updateStartTime(txSigner, args.startTime)
|
||||
// return minterMessages.updateStartTime(txSigner, args.startTime)
|
||||
// }
|
||||
case 'token_info': {
|
||||
if (!args.tokenId) return
|
||||
return sg721Messages.allNftInfo(args.tokenId)
|
||||
}
|
||||
case 'config': {
|
||||
return baseMinterMessages.getConfig()
|
||||
}
|
||||
case 'status': {
|
||||
return baseMinterMessages.getStatus()
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown action')
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/badgeHub/messages/execute'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
export interface ExecuteComboboxProps {
|
||||
value: ExecuteListItem | null
|
||||
onChange: (item: ExecuteListItem) => void
|
||||
}
|
||||
|
||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filtered =
|
||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={FormControl}
|
||||
htmlId="message-type"
|
||||
labelAs={Combobox.Label}
|
||||
onChange={onChange}
|
||||
subtitle="Contract execute message type"
|
||||
title="Message Type"
|
||||
value={value}
|
||||
>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className={clsx(
|
||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
||||
id="message-type"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Select message type"
|
||||
/>
|
||||
|
||||
<Combobox.Button
|
||||
className={clsx(
|
||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||
)}
|
||||
>
|
||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||
<Combobox.Options
|
||||
className={clsx(
|
||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||
'divide-y divide-stone-500/50',
|
||||
)}
|
||||
>
|
||||
{filtered.length < 1 && (
|
||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||
Message type not found.
|
||||
</span>
|
||||
)}
|
||||
{filtered.map((entry) => (
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
<span className="font-bold">{entry.name}</span>
|
||||
<span className="max-w-md text-sm">{entry.description}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
{value && (
|
||||
<div className="flex space-x-2 text-white/50">
|
||||
<div className="mt-1">
|
||||
<FaInfoCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm">{value.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ExecuteListItem } from 'contracts/baseMinter/messages/execute'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/baseMinter/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/baseMinter/messages/execute'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
export interface ExecuteComboboxProps {
|
||||
value: ExecuteListItem | null
|
||||
onChange: (item: ExecuteListItem) => void
|
||||
}
|
||||
|
||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filtered =
|
||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={FormControl}
|
||||
htmlId="message-type"
|
||||
labelAs={Combobox.Label}
|
||||
onChange={onChange}
|
||||
subtitle="Contract execute message type"
|
||||
title="Message Type"
|
||||
value={value}
|
||||
>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className={clsx(
|
||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
||||
id="message-type"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Select message type"
|
||||
/>
|
||||
|
||||
<Combobox.Button
|
||||
className={clsx(
|
||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||
)}
|
||||
>
|
||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||
<Combobox.Options
|
||||
className={clsx(
|
||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||
'divide-y divide-stone-500/50',
|
||||
)}
|
||||
>
|
||||
{filtered.length < 1 && (
|
||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||
Message type not found.
|
||||
</span>
|
||||
)}
|
||||
{filtered.map((entry) => (
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
<span className="font-bold">{entry.name}</span>
|
||||
<span className="max-w-md text-sm">{entry.description}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
{value && (
|
||||
<div className="flex space-x-2 text-white/50">
|
||||
<div className="mt-1">
|
||||
<FaInfoCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm">{value.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { ExecuteListItem } from 'contracts/splits/messages/execute'
|
||||
import type { ExecuteListItem } from 'contracts/minter/messages/execute'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/splits/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/splits/messages/execute'
|
||||
import type { ExecuteListItem } from 'contracts/minter/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/minter/messages/execute'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
@@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ExecuteListItem } from 'contracts/openEditionMinter/messages/execute'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/openEditionMinter/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/openEditionMinter/messages/execute'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
export interface ExecuteComboboxProps {
|
||||
value: ExecuteListItem | null
|
||||
onChange: (item: ExecuteListItem) => void
|
||||
}
|
||||
|
||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filtered =
|
||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={FormControl}
|
||||
htmlId="message-type"
|
||||
labelAs={Combobox.Label}
|
||||
onChange={onChange}
|
||||
subtitle="Contract execute message type"
|
||||
title="Message Type"
|
||||
value={value}
|
||||
>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className={clsx(
|
||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
||||
id="message-type"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Select message type"
|
||||
/>
|
||||
|
||||
<Combobox.Button
|
||||
className={clsx(
|
||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||
)}
|
||||
>
|
||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||
<Combobox.Options
|
||||
className={clsx(
|
||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||
'divide-y divide-stone-500/50',
|
||||
)}
|
||||
>
|
||||
{filtered.length < 1 && (
|
||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||
Message type not found.
|
||||
</span>
|
||||
)}
|
||||
{filtered.map((entry) => (
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
<span className="font-bold">{entry.name}</span>
|
||||
<span className="max-w-md text-sm">{entry.description}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
{value && (
|
||||
<div className="flex space-x-2 text-white/50">
|
||||
<div className="mt-1">
|
||||
<FaInfoCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm">{value.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ExecuteListItem } from 'contracts/vendingMinter/messages/execute'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/vendingMinter/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/vendingMinter/messages/execute'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
export interface ExecuteComboboxProps {
|
||||
value: ExecuteListItem | null
|
||||
onChange: (item: ExecuteListItem) => void
|
||||
}
|
||||
|
||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filtered =
|
||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={FormControl}
|
||||
htmlId="message-type"
|
||||
labelAs={Combobox.Label}
|
||||
onChange={onChange}
|
||||
subtitle="Contract execute message type"
|
||||
title="Message Type"
|
||||
value={value}
|
||||
>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className={clsx(
|
||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
||||
id="message-type"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Select message type"
|
||||
/>
|
||||
|
||||
<Combobox.Button
|
||||
className={clsx(
|
||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||
)}
|
||||
>
|
||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||
<Combobox.Options
|
||||
className={clsx(
|
||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||
'divide-y divide-stone-500/50',
|
||||
)}
|
||||
>
|
||||
{filtered.length < 1 && (
|
||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||
Message type not found.
|
||||
</span>
|
||||
)}
|
||||
{filtered.map((entry) => (
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
<span className="font-bold">{entry.name}</span>
|
||||
<span className="max-w-md text-sm">{entry.description}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
{value && (
|
||||
<div className="flex space-x-2 text-white/50">
|
||||
<div className="mt-1">
|
||||
<FaInfoCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm">{value.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-plumbus-70': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
|
||||
@@ -27,9 +27,5 @@ export function useAddressListState() {
|
||||
})
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setRecord({})
|
||||
}
|
||||
|
||||
return { entries, values, add, update, remove, reset }
|
||||
return { entries, values, add, update, remove }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput } from 'components/forms/FormInput'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useEffect, useId, useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||
import { SG721_NAME_ADDRESS } from 'utils/constants'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
|
||||
import { useInputState } from './FormInput.hooks'
|
||||
|
||||
@@ -51,7 +46,6 @@ export interface AddressProps {
|
||||
}
|
||||
|
||||
export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps) {
|
||||
const wallet = useWallet()
|
||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||
|
||||
const htmlId = useId()
|
||||
@@ -62,40 +56,10 @@ export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps)
|
||||
title: ``,
|
||||
})
|
||||
|
||||
const resolveAddress = async (name: string) => {
|
||||
if (!wallet.client) throw new Error('Wallet not connected')
|
||||
await wallet.client
|
||||
.queryContractRaw(
|
||||
SG721_NAME_ADDRESS,
|
||||
toUtf8(
|
||||
Buffer.from(
|
||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
|
||||
'hex',
|
||||
).toString(),
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
||||
if (tokenUri && isValidAddress(tokenUri)) onChange(id, { address: tokenUri })
|
||||
else {
|
||||
toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
|
||||
onChange(id, { address: '' })
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(`Error resolving address for the name: ${name}.stars`)
|
||||
console.error(err)
|
||||
onChange(id, { address: '' })
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
if (addressState.value.endsWith('.stars')) {
|
||||
void resolveAddress(addressState.value.split('.')[0])
|
||||
} else {
|
||||
onChange(id, {
|
||||
address: addressState.value,
|
||||
})
|
||||
}
|
||||
onChange(id, {
|
||||
address: addressState.value,
|
||||
})
|
||||
}, [addressState.value, id])
|
||||
|
||||
return (
|
||||
@@ -103,7 +67,7 @@ export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps)
|
||||
<AddressInput {...addressState} />
|
||||
<div className="flex justify-end items-end pb-2 w-8">
|
||||
<button
|
||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
||||
className="flex justify-center items-center p-2 bg-plumbus-80 hover:bg-plumbus-60 rounded-full"
|
||||
onClick={() => (isLast ? onAdd() : onRemove(id))}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
export function useFlexMemberAttributesState() {
|
||||
const [record, setRecord] = useState<Record<string, WhitelistFlexMember>>(() => ({}))
|
||||
|
||||
const entries = useMemo(() => Object.entries(record), [record])
|
||||
const values = useMemo(() => Object.values(record), [record])
|
||||
|
||||
function add(attribute: WhitelistFlexMember = { address: '', mint_count: 0 }) {
|
||||
setRecord((prev) => ({ ...prev, [uid()]: attribute }))
|
||||
}
|
||||
|
||||
function update(key: string, attribute = record[key]) {
|
||||
setRecord((prev) => ({ ...prev, [key]: attribute }))
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
return setRecord((prev) => {
|
||||
const latest = { ...prev }
|
||||
delete latest[key]
|
||||
return latest
|
||||
})
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setRecord({})
|
||||
}
|
||||
|
||||
return { entries, values, add, update, remove, reset }
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useEffect, useId, useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||
import { SG721_NAME_ADDRESS } from 'utils/constants'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
|
||||
import { useInputState, useNumberInputState } from './FormInput.hooks'
|
||||
|
||||
export interface FlexMemberAttributesProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
isRequired?: boolean
|
||||
attributes: [string, WhitelistFlexMember][]
|
||||
onAdd: () => void
|
||||
onChange: (key: string, attribute: WhitelistFlexMember) => void
|
||||
onRemove: (key: string) => void
|
||||
}
|
||||
|
||||
export function FlexMemberAttributes(props: FlexMemberAttributesProps) {
|
||||
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
|
||||
|
||||
return (
|
||||
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||
{attributes.map(([id], i) => (
|
||||
<FlexMemberAttribute
|
||||
key={`ma-${id}`}
|
||||
defaultAttribute={attributes[i][1]}
|
||||
id={id}
|
||||
isLast={i === attributes.length - 1}
|
||||
onAdd={onAdd}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export interface MemberAttributeProps {
|
||||
id: string
|
||||
isLast: boolean
|
||||
onAdd: FlexMemberAttributesProps['onAdd']
|
||||
onChange: FlexMemberAttributesProps['onChange']
|
||||
onRemove: FlexMemberAttributesProps['onRemove']
|
||||
defaultAttribute: WhitelistFlexMember
|
||||
}
|
||||
|
||||
export function FlexMemberAttribute({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: MemberAttributeProps) {
|
||||
const wallet = useWallet()
|
||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||
|
||||
const htmlId = useId()
|
||||
|
||||
const addressState = useInputState({
|
||||
id: `ma-address-${htmlId}`,
|
||||
name: `ma-address-${htmlId}`,
|
||||
title: `Address`,
|
||||
defaultValue: defaultAttribute.address,
|
||||
})
|
||||
|
||||
const mintCountState = useNumberInputState({
|
||||
id: `mint-count-${htmlId}`,
|
||||
name: `mint-count-${htmlId}`,
|
||||
title: `Mint Count`,
|
||||
defaultValue: defaultAttribute.mint_count,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onChange(id, { address: addressState.value, mint_count: mintCountState.value })
|
||||
}, [addressState.value, mintCountState.value, id])
|
||||
|
||||
const resolveAddress = async (name: string) => {
|
||||
if (!wallet.client) throw new Error('Wallet not connected')
|
||||
await wallet.client
|
||||
.queryContractRaw(
|
||||
SG721_NAME_ADDRESS,
|
||||
toUtf8(
|
||||
Buffer.from(
|
||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
|
||||
'hex',
|
||||
).toString(),
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
||||
if (tokenUri && isValidAddress(tokenUri)) onChange(id, { address: tokenUri, mint_count: mintCountState.value })
|
||||
else {
|
||||
toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
|
||||
onChange(id, { address: '', mint_count: mintCountState.value })
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(`Error resolving address for the name: ${name}.stars`)
|
||||
console.error(err)
|
||||
onChange(id, { address: '', mint_count: mintCountState.value })
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
if (addressState.value.endsWith('.stars')) {
|
||||
void resolveAddress(addressState.value.split('.')[0])
|
||||
} else {
|
||||
onChange(id, {
|
||||
address: addressState.value,
|
||||
mint_count: mintCountState.value,
|
||||
})
|
||||
}
|
||||
}, [addressState.value, id])
|
||||
|
||||
return (
|
||||
<div className="grid relative md:grid-cols-[50%_43%_7%] lg:grid-cols-[65%_28%_7%] 2xl:grid-cols-[70%_23%_7%] 2xl:space-x-2">
|
||||
<AddressInput {...addressState} />
|
||||
<NumberInput className="ml-2" {...mintCountState} />
|
||||
|
||||
<div className="flex justify-end items-end pb-2 w-8">
|
||||
<button
|
||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
isLast ? onAdd() : onRemove(id)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -69,26 +69,6 @@ export const TextInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
//
|
||||
)
|
||||
|
||||
export const CheckBoxInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
function CheckBoxInput(props, ref) {
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex flex-col space-y-1" htmlFor="explicit">
|
||||
<span className="font-bold first-letter:capitalize">Explicit Content</span>
|
||||
</label>
|
||||
<input
|
||||
className="placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
|
||||
id="explicit"
|
||||
name="explicit"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
//
|
||||
)
|
||||
|
||||
export const UrlInput = forwardRef<HTMLInputElement, FormInputProps>(
|
||||
function UrlInput(props, ref) {
|
||||
return <FormInput {...props} ref={ref} type="url" />
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import type { Attribute } from './MemberAttributes'
|
||||
|
||||
export function useMemberAttributesState() {
|
||||
const [record, setRecord] = useState<Record<string, Attribute>>(() => ({}))
|
||||
|
||||
const entries = useMemo(() => Object.entries(record), [record])
|
||||
const values = useMemo(() => Object.values(record), [record])
|
||||
|
||||
function add(attribute: Attribute = { address: '', weight: 0 }) {
|
||||
setRecord((prev) => ({ ...prev, [uid()]: attribute }))
|
||||
}
|
||||
|
||||
function update(key: string, attribute = record[key]) {
|
||||
setRecord((prev) => ({ ...prev, [key]: attribute }))
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
return setRecord((prev) => {
|
||||
const latest = { ...prev }
|
||||
delete latest[key]
|
||||
return latest
|
||||
})
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setRecord({})
|
||||
}
|
||||
|
||||
return { entries, values, add, update, remove, reset }
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||
import { useEffect, useId, useMemo } from 'react'
|
||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||
|
||||
import { useInputState, useNumberInputState } from './FormInput.hooks'
|
||||
|
||||
export interface Attribute {
|
||||
address: string
|
||||
weight: number
|
||||
}
|
||||
|
||||
export interface MemberAttributesProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
isRequired?: boolean
|
||||
attributes: [string, Attribute][]
|
||||
onAdd: () => void
|
||||
onChange: (key: string, attribute: Attribute) => void
|
||||
onRemove: (key: string) => void
|
||||
}
|
||||
|
||||
export function MemberAttributes(props: MemberAttributesProps) {
|
||||
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
|
||||
|
||||
const calculateMemberPercent = (id: string) => {
|
||||
const total = attributes.reduce((acc, [, { weight }]) => acc + (weight ? weight : 0), 0)
|
||||
// return attributes.map(([id, { weight }]) => [id, weight / total])
|
||||
const memberWeight = attributes.find(([memberId]) => memberId === id)?.[1].weight
|
||||
return memberWeight ? memberWeight / total : 0
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||
{attributes.map(([id], i) => (
|
||||
<MemberAttribute
|
||||
key={`ma-${id}`}
|
||||
defaultAttribute={attributes[i][1]}
|
||||
id={id}
|
||||
isLast={i === attributes.length - 1}
|
||||
onAdd={onAdd}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
percentage={(Number(calculateMemberPercent(id)) * 100).toFixed(2)}
|
||||
/>
|
||||
))}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export interface MemberAttributeProps {
|
||||
id: string
|
||||
isLast: boolean
|
||||
onAdd: MemberAttributesProps['onAdd']
|
||||
onChange: MemberAttributesProps['onChange']
|
||||
onRemove: MemberAttributesProps['onRemove']
|
||||
defaultAttribute: Attribute
|
||||
percentage?: string
|
||||
}
|
||||
|
||||
export function MemberAttribute({
|
||||
id,
|
||||
isLast,
|
||||
onAdd,
|
||||
onChange,
|
||||
onRemove,
|
||||
defaultAttribute,
|
||||
percentage,
|
||||
}: MemberAttributeProps) {
|
||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||
|
||||
const htmlId = useId()
|
||||
|
||||
const addressState = useInputState({
|
||||
id: `ma-address-${htmlId}`,
|
||||
name: `ma-address-${htmlId}`,
|
||||
title: `Address`,
|
||||
defaultValue: defaultAttribute.address,
|
||||
})
|
||||
|
||||
const weightState = useNumberInputState({
|
||||
id: `ma-weight-${htmlId}`,
|
||||
name: `ma-weight-${htmlId}`,
|
||||
title: `Weight ${percentage ? `: ${percentage}%` : ''}`,
|
||||
defaultValue: defaultAttribute.weight,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onChange(id, { address: addressState.value, weight: weightState.value })
|
||||
}, [addressState.value, weightState.value, id])
|
||||
|
||||
return (
|
||||
<div className="grid relative md:grid-cols-[50%_43%_7%] lg:grid-cols-[65%_28%_7%] 2xl:grid-cols-[70%_23%_7%] 2xl:space-x-2">
|
||||
<AddressInput {...addressState} />
|
||||
<NumberInput {...weightState} />
|
||||
|
||||
<div className="flex justify-end items-end pb-2 w-8">
|
||||
<button
|
||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
isLast ? onAdd() : onRemove(id)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -73,13 +73,12 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
|
||||
}, [traitTypeState.value, traitValueState.value, id])
|
||||
|
||||
return (
|
||||
<div className="grid relative xl:grid-cols-[6fr_6fr_1fr] xl:-space-x-8 2xl:space-x-2">
|
||||
<TraitTypeInput className="lg:w-4/5 2xl:w-full" {...traitTypeState} />
|
||||
<TraitValueInput className="lg:w-4/5 xl:pr-2 xl:w-full" {...traitValueState} />
|
||||
|
||||
<div className="grid relative grid-cols-[1fr_1fr_auto] space-x-2">
|
||||
<TraitTypeInput {...traitTypeState} />
|
||||
<TraitValueInput {...traitValueState} />
|
||||
<div className="flex justify-end items-end pb-2 w-8">
|
||||
<button
|
||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
||||
className="flex justify-center items-center p-2 bg-plumbus-80 hover:bg-plumbus-60 rounded-full"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
isLast ? onAdd() : onRemove(id)
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS, SG721_OPEN_EDITION_UPDATABLE_CODE_ID } from 'utils/constants'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import { TextInput } from '../forms/FormInput'
|
||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
||||
import type { MetadataStorageMethod } from './OpenEditionMinterCreator'
|
||||
|
||||
interface CollectionDetailsProps {
|
||||
onChange: (data: CollectionDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod
|
||||
coverImageUrl: string
|
||||
metadataStorageMethod: MetadataStorageMethod
|
||||
}
|
||||
|
||||
export interface CollectionDetailsDataProps {
|
||||
name: string
|
||||
description: string
|
||||
symbol: string
|
||||
imageFile: File[]
|
||||
externalLink?: string
|
||||
startTradingTime?: string
|
||||
explicit: boolean
|
||||
updatable: boolean
|
||||
}
|
||||
|
||||
export const CollectionDetails = ({
|
||||
onChange,
|
||||
uploadMethod,
|
||||
metadataStorageMethod,
|
||||
coverImageUrl,
|
||||
}: CollectionDetailsProps) => {
|
||||
const [coverImage, setCoverImage] = useState<File | null>(null)
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
||||
const [explicit, setExplicit] = useState<boolean>(false)
|
||||
const [updatable, setUpdatable] = useState<boolean>(false)
|
||||
|
||||
const initialRender = useRef(true)
|
||||
|
||||
const coverImageInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
placeholder: 'My Awesome Collection',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
id: 'description',
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
placeholder: 'My Awesome Collection Description',
|
||||
})
|
||||
|
||||
const symbolState = useInputState({
|
||||
id: 'symbol',
|
||||
name: 'symbol',
|
||||
title: 'Symbol',
|
||||
placeholder: 'SYMBOL',
|
||||
})
|
||||
|
||||
const externalLinkState = useInputState({
|
||||
id: 'external-link',
|
||||
name: 'externalLink',
|
||||
title: 'External Link (optional)',
|
||||
placeholder: 'https://my-collection...',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: CollectionDetailsDataProps = {
|
||||
name: nameState.value,
|
||||
description: descriptionState.value,
|
||||
symbol: symbolState.value,
|
||||
imageFile: coverImage ? [coverImage] : [],
|
||||
externalLink: externalLinkState.value || undefined,
|
||||
startTradingTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
explicit,
|
||||
updatable,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
nameState.value,
|
||||
descriptionState.value,
|
||||
symbolState.value,
|
||||
externalLinkState.value,
|
||||
coverImage,
|
||||
timestamp,
|
||||
explicit,
|
||||
updatable,
|
||||
])
|
||||
|
||||
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files === null) return toast.error('Error selecting cover image')
|
||||
if (event.target.files.length === 0) {
|
||||
setCoverImage(null)
|
||||
return toast.error('No files selected.')
|
||||
}
|
||||
const 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 imageFile = new File([e.target.result], event.target.files[0].name, { type: 'image/jpg' })
|
||||
setCoverImage(imageFile)
|
||||
}
|
||||
reader.readAsArrayBuffer(event.target.files[0])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCoverImage(null)
|
||||
// empty the element so that the same file can be selected again
|
||||
if (coverImageInputRef.current) coverImageInputRef.current.value = ''
|
||||
}, [metadataStorageMethod])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRender.current) {
|
||||
initialRender.current = false
|
||||
} else if (updatable) {
|
||||
toast.success('Token metadata will be updatable upon collection creation.', {
|
||||
style: { maxWidth: 'none' },
|
||||
icon: '✅📝',
|
||||
})
|
||||
} else {
|
||||
toast.error('Token metadata will not be updatable upon collection creation.', {
|
||||
style: { maxWidth: 'none' },
|
||||
icon: '⛔🔏',
|
||||
})
|
||||
}
|
||||
}, [updatable])
|
||||
|
||||
const videoPreview = useMemo(() => {
|
||||
if (uploadMethod === 'new' && coverImage) {
|
||||
return (
|
||||
<video
|
||||
controls
|
||||
id="video"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={URL.createObjectURL(coverImage)}
|
||||
/>
|
||||
)
|
||||
} else if (uploadMethod === 'existing' && coverImageUrl && coverImageUrl.includes('ipfs://')) {
|
||||
return (
|
||||
<video
|
||||
controls
|
||||
id="video"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
|
||||
coverImageUrl.lastIndexOf('ipfs://') + 7,
|
||||
)}`}
|
||||
/>
|
||||
)
|
||||
} else if (uploadMethod === 'existing' && coverImageUrl && !coverImageUrl.includes('ipfs://')) {
|
||||
return (
|
||||
<video
|
||||
controls
|
||||
id="video"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={coverImageUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}, [coverImage, coverImageUrl, uploadMethod])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormGroup subtitle="Information about your collection" title="Collection Details">
|
||||
<div className={clsx('')}>
|
||||
<div className="">
|
||||
<TextInput {...nameState} isRequired />
|
||||
<TextInput className="mt-2" {...descriptionState} isRequired />
|
||||
<TextInput className="mt-2" {...symbolState} isRequired />
|
||||
</div>
|
||||
<div className={clsx('')}>
|
||||
<TextInput className={clsx('mt-2')} {...externalLinkState} />
|
||||
{/* Currently trading starts immediately for 1/1 Collections */}
|
||||
|
||||
<FormControl
|
||||
className={clsx('mt-2')}
|
||||
htmlId="timestamp"
|
||||
subtitle="Trading start time offset will be set as 2 weeks by default."
|
||||
title="Trading Start Time (optional)"
|
||||
>
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormControl className={clsx('')} isRequired={uploadMethod === 'new'} title="Cover Image">
|
||||
{uploadMethod === 'new' && (
|
||||
<input
|
||||
accept="image/*, video/*"
|
||||
className={clsx(
|
||||
'w-full',
|
||||
'p-[13px] rounded border-2 border-white/20 border-dashed cursor-pointer h-18',
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="cover-image"
|
||||
onChange={selectCoverImage}
|
||||
ref={coverImageInputRef}
|
||||
type="file"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Conditional
|
||||
test={coverImage !== null && uploadMethod === 'new' && getAssetType(coverImage.name) === 'image'}
|
||||
>
|
||||
{coverImage !== null && (
|
||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
||||
<img alt="no-preview-available" src={URL.createObjectURL(coverImage)} />
|
||||
</div>
|
||||
)}
|
||||
</Conditional>
|
||||
<Conditional
|
||||
test={coverImage !== null && uploadMethod === 'new' && getAssetType(coverImage.name) === 'video'}
|
||||
>
|
||||
{coverImage !== null && videoPreview}
|
||||
</Conditional>
|
||||
|
||||
{uploadMethod === 'existing' && coverImageUrl?.includes('ipfs://') && (
|
||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
||||
<Conditional test={getAssetType(coverImageUrl) !== 'video'}>
|
||||
<img
|
||||
alt="no-preview-available"
|
||||
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
|
||||
coverImageUrl.lastIndexOf('ipfs://') + 7,
|
||||
)}`}
|
||||
/>
|
||||
</Conditional>
|
||||
<Conditional test={getAssetType(coverImageUrl) === 'video'}>{videoPreview}</Conditional>
|
||||
</div>
|
||||
)}
|
||||
{uploadMethod === 'existing' && coverImageUrl && !coverImageUrl?.includes('ipfs://') && (
|
||||
<div>
|
||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
||||
<Conditional test={getAssetType(coverImageUrl) !== 'video'}>
|
||||
<img alt="no-preview-available" src={coverImageUrl} />
|
||||
</Conditional>
|
||||
<Conditional test={getAssetType(coverImageUrl) === 'video'}>{videoPreview}</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{uploadMethod === 'existing' && !coverImageUrl && (
|
||||
<span className="italic font-light ">Waiting for cover image URL to be specified.</span>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<div className={clsx('flex flex-col space-y-2')}>
|
||||
<div>
|
||||
<div className="flex mt-4">
|
||||
<span className="mt-1 ml-[2px] text-sm first-letter:capitalize">
|
||||
Does the collection contain explicit content?
|
||||
</span>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={explicit}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio1"
|
||||
name="explicitRadioOptions1"
|
||||
onClick={() => {
|
||||
setExplicit(true)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio1"
|
||||
>
|
||||
YES
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={!explicit}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio2"
|
||||
name="explicitRadioOptions2"
|
||||
onClick={() => {
|
||||
setExplicit(false)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio2"
|
||||
>
|
||||
NO
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional
|
||||
test={SG721_OPEN_EDITION_UPDATABLE_CODE_ID > 0 && OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS !== undefined}
|
||||
>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label={
|
||||
<div className="grid grid-flow-row">
|
||||
<span>
|
||||
ℹ️ When enabled, the metadata for tokens can be updated after the collection is created until the
|
||||
collection is frozen by the creator.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<div className={clsx('flex flex-col space-y-2 w-3/4 form-control')}>
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<span className="mr-4 font-bold">Updatable Token Metadata</span>
|
||||
<input
|
||||
checked={updatable}
|
||||
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setUpdatable(!updatable)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Conditional>
|
||||
</FormGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
||||
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 { getAssetType } from 'utils/getAssetType'
|
||||
|
||||
export type UploadMethod = 'new' | 'existing'
|
||||
|
||||
interface ImageUploadDetailsProps {
|
||||
onChange: (value: ImageUploadDetailsDataProps) => void
|
||||
}
|
||||
|
||||
export interface ImageUploadDetailsDataProps {
|
||||
assetFile: File | undefined
|
||||
uploadService: UploadServiceType
|
||||
nftStorageApiKey?: string
|
||||
pinataApiKey?: string
|
||||
pinataSecretKey?: string
|
||||
uploadMethod: UploadMethod
|
||||
imageUrl?: string
|
||||
coverImageUrl?: string
|
||||
}
|
||||
|
||||
export const ImageUploadDetails = ({ onChange }: ImageUploadDetailsProps) => {
|
||||
const [assetFile, setAssetFile] = useState<File>()
|
||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
||||
|
||||
const assetFileRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nftStorageApiKeyState = useInputState({
|
||||
id: 'nft-storage-api-key',
|
||||
name: 'nftStorageApiKey',
|
||||
title: 'NFT.Storage API Key',
|
||||
placeholder: 'Enter NFT.Storage API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataApiKeyState = useInputState({
|
||||
id: 'pinata-api-key',
|
||||
name: 'pinataApiKey',
|
||||
title: 'Pinata API Key',
|
||||
placeholder: 'Enter Pinata API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataSecretKeyState = useInputState({
|
||||
id: 'pinata-secret-key',
|
||||
name: 'pinataSecretKey',
|
||||
title: 'Pinata Secret Key',
|
||||
placeholder: 'Enter Pinata Secret Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const imageUrlState = useInputState({
|
||||
id: 'imageUrl',
|
||||
name: 'imageUrl',
|
||||
title: 'Asset URL',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const coverImageUrlState = useInputState({
|
||||
id: 'coverImageUrl',
|
||||
name: 'coverImageUrl',
|
||||
title: 'Cover Image URL',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setAssetFile(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/jpg' })
|
||||
}
|
||||
// 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.')
|
||||
setAssetFile(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
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: ImageUploadDetailsDataProps = {
|
||||
assetFile,
|
||||
uploadService,
|
||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
||||
pinataApiKey: pinataApiKeyState.value,
|
||||
pinataSecretKey: pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
imageUrl: imageUrlState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(regex, '')
|
||||
.trim(),
|
||||
coverImageUrl: coverImageUrlState.value.trim(),
|
||||
}
|
||||
onChange(data)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
}, [
|
||||
assetFile,
|
||||
uploadService,
|
||||
nftStorageApiKeyState.value,
|
||||
pinataApiKeyState.value,
|
||||
pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
imageUrlState.value,
|
||||
coverImageUrlState.value,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (assetFileRef.current) assetFileRef.current.value = ''
|
||||
setAssetFile(undefined)
|
||||
imageUrlState.onChange('')
|
||||
}, [uploadMethod])
|
||||
|
||||
const previewUrl = imageUrlState.value.toLowerCase().trim().startsWith('ipfs://')
|
||||
? `https://ipfs-gw.stargaze-apis.com/ipfs/${imageUrlState.value.substring(7)}`
|
||||
: imageUrlState.value
|
||||
|
||||
const audioPreview = useMemo(
|
||||
() => (
|
||||
<audio
|
||||
controls
|
||||
id="audio"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={imageUrlState.value ? previewUrl : ''}
|
||||
/>
|
||||
),
|
||||
[imageUrlState.value],
|
||||
)
|
||||
|
||||
const videoPreview = useMemo(
|
||||
() => (
|
||||
<video
|
||||
controls
|
||||
id="video"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={imageUrlState.value ? previewUrl : ''}
|
||||
/>
|
||||
),
|
||||
[imageUrlState.value],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
||||
<div className="flex justify-center">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'new'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio2"
|
||||
name="inlineRadioOptions2"
|
||||
onClick={() => {
|
||||
setUploadMethod('new')
|
||||
}}
|
||||
type="radio"
|
||||
value="New"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio2"
|
||||
>
|
||||
Upload New Asset
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'existing'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio1"
|
||||
name="inlineRadioOptions1"
|
||||
onClick={() => {
|
||||
setUploadMethod('existing')
|
||||
}}
|
||||
type="radio"
|
||||
value="Existing"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio1"
|
||||
>
|
||||
Use an existing Asset URL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 py-5 pb-4">
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<div className="ml-3 flex-column">
|
||||
<p className="mb-5 ml-5">
|
||||
Though the Open Edition contracts allow for off-chain asset storage, it is recommended to use a
|
||||
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
||||
NFT.Storage
|
||||
</Anchor>{' '}
|
||||
or{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
||||
Pinata
|
||||
</Anchor>{' '}
|
||||
and upload your asset manually to get an asset URL for your NFT.
|
||||
</p>
|
||||
<div className="flex flex-row w-full">
|
||||
<div className="flex flex-col w-3/5">
|
||||
<TextInput {...imageUrlState} className="mt-2 ml-6" />
|
||||
<TextInput {...coverImageUrlState} className="mt-4 ml-6" />
|
||||
</div>
|
||||
<Conditional test={imageUrlState.value !== ''}>
|
||||
<div className="flex mt-2 ml-8 w-1/3 border-2 border-dashed">
|
||||
{getAssetType(imageUrlState.value) === 'audio' && audioPreview}
|
||||
{getAssetType(imageUrlState.value) === 'video' && videoPreview}
|
||||
{getAssetType(imageUrlState.value) === 'image' && <img alt="asset-preview" src={previewUrl} />}
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={uploadMethod === 'new'}>
|
||||
<div>
|
||||
<div className="flex flex-col items-center px-8 w-full">
|
||||
<div className="flex justify-items-start mb-5 w-full font-bold">
|
||||
<div className="form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'nft-storage'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio3"
|
||||
name="inlineRadioOptions3"
|
||||
onClick={() => {
|
||||
setUploadService('nft-storage')
|
||||
}}
|
||||
type="radio"
|
||||
value="nft-storage"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio3"
|
||||
>
|
||||
Upload using NFT.Storage
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="ml-2 form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'pinata'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio4"
|
||||
name="inlineRadioOptions4"
|
||||
onClick={() => {
|
||||
setUploadService('pinata')
|
||||
}}
|
||||
type="radio"
|
||||
value="pinata"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio4"
|
||||
>
|
||||
Upload using Pinata
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full">
|
||||
<Conditional test={uploadService === 'nft-storage'}>
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
<Conditional test={uploadService === 'pinata'}>
|
||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
||||
<div className="w-[20px]" />
|
||||
<TextInput {...pinataSecretKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="grid grid-cols-2">
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFile"
|
||||
>
|
||||
Asset Selection
|
||||
</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/*, audio/*, video/*, .html"
|
||||
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="assetFile"
|
||||
onChange={selectAsset}
|
||||
ref={assetFileRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={assetFile !== undefined}>
|
||||
<SingleAssetPreview
|
||||
relatedAsset={assetFile}
|
||||
subtitle={`Asset filename: ${assetFile?.name as string}`}
|
||||
/>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { openEditionMinterList } from 'config/minter'
|
||||
import type { TokenInfo } from 'config/token'
|
||||
import { stars, tokensList } from 'config/token'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import { useWallet } from '../../contexts/wallet'
|
||||
import { NumberInput, TextInput } from '../forms/FormInput'
|
||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
||||
|
||||
interface MintingDetailsProps {
|
||||
onChange: (data: MintingDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod
|
||||
minimumMintPrice: number
|
||||
mintTokenFromFactory?: TokenInfo | undefined
|
||||
}
|
||||
|
||||
export interface MintingDetailsDataProps {
|
||||
unitPrice: string
|
||||
perAddressLimit: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
paymentAddress?: string
|
||||
selectedMintToken?: TokenInfo
|
||||
}
|
||||
|
||||
export const MintingDetails = ({
|
||||
onChange,
|
||||
uploadMethod,
|
||||
minimumMintPrice,
|
||||
mintTokenFromFactory,
|
||||
}: MintingDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
||||
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>()
|
||||
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
|
||||
|
||||
const unitPriceState = useNumberInputState({
|
||||
id: 'unitPrice',
|
||||
name: 'unitPrice',
|
||||
title: 'Mint Price',
|
||||
subtitle: `Price of each token (min. ${minimumMintPrice} ${
|
||||
mintTokenFromFactory ? mintTokenFromFactory.displayName : 'STARS'
|
||||
})`,
|
||||
placeholder: '50',
|
||||
})
|
||||
|
||||
const perAddressLimitState = useNumberInputState({
|
||||
id: 'peraddresslimit',
|
||||
name: 'peraddresslimit',
|
||||
title: 'Per Address Limit',
|
||||
subtitle: '',
|
||||
placeholder: '1',
|
||||
})
|
||||
|
||||
const paymentAddressState = useInputState({
|
||||
id: 'payment-address',
|
||||
name: 'paymentAddress',
|
||||
title: 'Payment Address (optional)',
|
||||
subtitle: 'Address to receive minting revenues (defaults to current wallet address)',
|
||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||
})
|
||||
|
||||
const resolvePaymentAddress = async () => {
|
||||
await resolveAddress(paymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
paymentAddressState.onChange(resolvedAddress)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void resolvePaymentAddress()
|
||||
}, [paymentAddressState.value])
|
||||
|
||||
useEffect(() => {
|
||||
const data: MintingDetailsDataProps = {
|
||||
unitPrice: unitPriceState.value
|
||||
? (Number(unitPriceState.value) * 1_000_000).toString()
|
||||
: unitPriceState.value === 0
|
||||
? '0'
|
||||
: '',
|
||||
perAddressLimit: perAddressLimitState.value,
|
||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
|
||||
paymentAddress: paymentAddressState.value.trim(),
|
||||
selectedMintToken,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
unitPriceState.value,
|
||||
perAddressLimitState.value,
|
||||
timestamp,
|
||||
endTimestamp,
|
||||
paymentAddressState.value,
|
||||
selectedMintToken,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="border-l-[1px] border-gray-500 border-opacity-20">
|
||||
<FormGroup subtitle="Information about your minting settings" title="Minting Details">
|
||||
<div className="flex flex-row items-center">
|
||||
<NumberInput {...unitPriceState} isRequired />
|
||||
<select
|
||||
className="py-[9px] px-4 mt-14 ml-4 placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
|
||||
onChange={(e) => setSelectedMintToken(tokensList.find((t) => t.displayName === e.target.value))}
|
||||
>
|
||||
{openEditionMinterList
|
||||
.filter((minter) => minter.factoryAddress !== undefined && minter.updatable === false)
|
||||
.map((minter) => (
|
||||
<option key={minter.id} className="bg-black" value={minter.supportedToken.displayName}>
|
||||
{minter.supportedToken.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<NumberInput {...perAddressLimitState} isRequired />
|
||||
<FormControl htmlId="timestamp" isRequired subtitle="Minting start time (local)" title="Start Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
<FormControl htmlId="endTimestamp" isRequired subtitle="Minting end time (local)" title="End Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setEndTimestamp(date)} value={endTimestamp} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<TextInput className="pr-4 pl-4 mt-3" {...paymentAddressState} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
/* eslint-disable @typescript-eslint/no-loop-func */
|
||||
import clsx from 'clsx'
|
||||
import { Alert } from 'components/Alert'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { MetadataInput } from 'components/MetadataInput'
|
||||
import { MetadataModal } from 'components/MetadataModal'
|
||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import type { UploadServiceType } from 'services/upload'
|
||||
import { uid } from 'utils/random'
|
||||
import { naturalCompare } from 'utils/sort'
|
||||
|
||||
import type { MetadataStorageMethod } from './OpenEditionMinterCreator'
|
||||
|
||||
export type UploadMethod = 'new' | 'existing'
|
||||
|
||||
interface OffChainMetadataUploadDetailsProps {
|
||||
onChange: (value: OffChainMetadataUploadDetailsDataProps) => void
|
||||
metadataStorageMethod?: MetadataStorageMethod
|
||||
}
|
||||
|
||||
export interface OffChainMetadataUploadDetailsDataProps {
|
||||
assetFiles: File[]
|
||||
metadataFiles: File[]
|
||||
uploadService: UploadServiceType
|
||||
nftStorageApiKey?: string
|
||||
pinataApiKey?: string
|
||||
pinataSecretKey?: string
|
||||
uploadMethod: UploadMethod
|
||||
tokenURI?: string
|
||||
imageUrl?: string
|
||||
openEditionMinterMetadataFile?: File
|
||||
}
|
||||
|
||||
export const OffChainMetadataUploadDetails = ({
|
||||
onChange,
|
||||
metadataStorageMethod,
|
||||
}: OffChainMetadataUploadDetailsProps) => {
|
||||
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
|
||||
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
|
||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
||||
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
|
||||
const [refreshMetadata, setRefreshMetadata] = useState(false)
|
||||
|
||||
const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>()
|
||||
|
||||
const assetFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nftStorageApiKeyState = useInputState({
|
||||
id: 'nft-storage-api-key',
|
||||
name: 'nftStorageApiKey',
|
||||
title: 'NFT.Storage API Key',
|
||||
placeholder: 'Enter NFT.Storage API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataApiKeyState = useInputState({
|
||||
id: 'pinata-api-key',
|
||||
name: 'pinataApiKey',
|
||||
title: 'Pinata API Key',
|
||||
placeholder: 'Enter Pinata API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataSecretKeyState = useInputState({
|
||||
id: 'pinata-secret-key',
|
||||
name: 'pinataSecretKey',
|
||||
title: 'Pinata Secret Key',
|
||||
placeholder: 'Enter Pinata Secret Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const tokenUriState = useInputState({
|
||||
id: 'tokenUri',
|
||||
name: 'tokenUri',
|
||||
title: 'Token URI',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const coverImageUrlState = useInputState({
|
||||
id: 'coverImageUrl',
|
||||
name: 'coverImageUrl',
|
||||
title: 'Cover Image URL',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setAssetFilesArray([])
|
||||
setMetadataFilesArray([])
|
||||
if (event.target.files === null) 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 assetFile = new File([e.target.result], event.target.files[i].name, { type: 'image/jpg' })
|
||||
files.push(assetFile)
|
||||
}
|
||||
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) {
|
||||
setAssetFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMetadataFilesArray([])
|
||||
if (event.target.files === null) return toast.error('No files selected.')
|
||||
|
||||
let loadedFileCount = 0
|
||||
const files: File[] = []
|
||||
let reader: FileReader
|
||||
for (let i = 0; i < event.target.files.length; i++) {
|
||||
reader = new FileReader()
|
||||
reader.onload = async (e) => {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
if (!event.target.files) return toast.error('No files selected.')
|
||||
const metadataFile = new File([e.target.result], event.target.files[i].name, { type: 'application/json' })
|
||||
files.push(metadataFile)
|
||||
try {
|
||||
const parsedMetadata = JSON.parse(await metadataFile.text())
|
||||
if (!parsedMetadata || typeof parsedMetadata !== 'object') {
|
||||
event.target.value = ''
|
||||
setMetadataFilesArray([])
|
||||
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
event.target.value = ''
|
||||
setMetadataFilesArray([])
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
|
||||
}
|
||||
}
|
||||
reader.readAsText(event.target.files[i], 'utf8')
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
loadedFileCount++
|
||||
if (loadedFileCount === event.target.files.length) {
|
||||
setMetadataFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateMetadataFileIndex = (index: number) => {
|
||||
setMetadataFileArrayIndex(index)
|
||||
setRefreshMetadata((prev) => !prev)
|
||||
}
|
||||
|
||||
const updateMetadataFileArray = (updatedMetadataFile: File) => {
|
||||
metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile
|
||||
}
|
||||
|
||||
const updateOpenEditionMinterMetadataFile = (updatedMetadataFile: File) => {
|
||||
setOpenEditionMinterMetadataFile(updatedMetadataFile)
|
||||
console.log('Updated Open Edition Minter Metadata File:')
|
||||
console.log(openEditionMinterMetadataFile)
|
||||
}
|
||||
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
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: OffChainMetadataUploadDetailsDataProps = {
|
||||
assetFiles: assetFilesArray,
|
||||
metadataFiles: metadataFilesArray,
|
||||
uploadService,
|
||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
||||
pinataApiKey: pinataApiKeyState.value,
|
||||
pinataSecretKey: pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
tokenURI: tokenUriState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(regex, '')
|
||||
.trim(),
|
||||
imageUrl: coverImageUrlState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(regex, '')
|
||||
.trim(),
|
||||
openEditionMinterMetadataFile,
|
||||
}
|
||||
onChange(data)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
}
|
||||
}, [
|
||||
assetFilesArray,
|
||||
metadataFilesArray,
|
||||
uploadService,
|
||||
nftStorageApiKeyState.value,
|
||||
pinataApiKeyState.value,
|
||||
pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
tokenUriState.value,
|
||||
coverImageUrlState.value,
|
||||
refreshMetadata,
|
||||
openEditionMinterMetadataFile,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
|
||||
setMetadataFilesArray([])
|
||||
if (assetFilesRef.current) assetFilesRef.current.value = ''
|
||||
setAssetFilesArray([])
|
||||
tokenUriState.onChange('')
|
||||
coverImageUrlState.onChange('')
|
||||
}, [uploadMethod, metadataStorageMethod])
|
||||
|
||||
return (
|
||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
||||
<div className="flex justify-center">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'new'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio2"
|
||||
name="inlineRadioOptions2"
|
||||
onClick={() => {
|
||||
setUploadMethod('new')
|
||||
}}
|
||||
type="radio"
|
||||
value="New"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio2"
|
||||
>
|
||||
Upload asset & metadata
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'existing'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio1"
|
||||
name="inlineRadioOptions1"
|
||||
onClick={() => {
|
||||
setUploadMethod('existing')
|
||||
}}
|
||||
type="radio"
|
||||
value="Existing"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio1"
|
||||
>
|
||||
Use an existing Token URI
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 py-5 pb-8">
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<div className="ml-3 flex-column">
|
||||
<p className="mb-5 ml-5">
|
||||
Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a
|
||||
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
||||
NFT.Storage
|
||||
</Anchor>{' '}
|
||||
or{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
||||
Pinata
|
||||
</Anchor>{' '}
|
||||
and upload your asset & metadata manually to get a URI for your token before minting.
|
||||
</p>
|
||||
<div>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
className="mb-2 ml-4"
|
||||
label="The token URI that points directly to the metadata file stored on IPFS."
|
||||
placement="top"
|
||||
>
|
||||
<TextInput {...tokenUriState} className="ml-4 w-1/2" />
|
||||
</Tooltip>
|
||||
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
<Conditional test={uploadMethod === 'new'}>
|
||||
<div>
|
||||
<div className="flex flex-col items-center px-8 w-full">
|
||||
<div className="flex justify-items-start mb-5 w-full font-bold">
|
||||
<div className="form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'nft-storage'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio3"
|
||||
name="inlineRadioOptions3"
|
||||
onClick={() => {
|
||||
setUploadService('nft-storage')
|
||||
}}
|
||||
type="radio"
|
||||
value="nft-storage"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio3"
|
||||
>
|
||||
Upload using NFT.Storage
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="ml-2 form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'pinata'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio4"
|
||||
name="inlineRadioOptions4"
|
||||
onClick={() => {
|
||||
setUploadService('pinata')
|
||||
}}
|
||||
type="radio"
|
||||
value="pinata"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio4"
|
||||
>
|
||||
Upload using Pinata
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full">
|
||||
<Conditional test={uploadService === 'nft-storage'}>
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
<Conditional test={uploadService === 'pinata'}>
|
||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
||||
<div className="w-[20px]" />
|
||||
<TextInput {...pinataSecretKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="w-full">
|
||||
<Conditional
|
||||
test={
|
||||
assetFilesArray.length > 0 &&
|
||||
metadataFilesArray.length > 0 &&
|
||||
assetFilesArray.length !== metadataFilesArray.length
|
||||
}
|
||||
>
|
||||
<Alert className="mt-4 ml-8 w-3/4" type="warning">
|
||||
The number of assets and metadata files should match.
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFiles"
|
||||
>
|
||||
Asset Selection
|
||||
</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/*, audio/*, video/*, .html"
|
||||
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="assetFiles"
|
||||
onChange={selectAssets}
|
||||
ref={assetFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{assetFilesArray.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="metadataFiles"
|
||||
>
|
||||
Metadata 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="application/json"
|
||||
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="metadataFiles"
|
||||
onChange={selectMetadata}
|
||||
ref={metadataFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Conditional test={assetFilesArray.length >= 1}>
|
||||
<MetadataModal
|
||||
assetFile={assetFilesArray[metadataFileArrayIndex]}
|
||||
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
|
||||
refresher={refreshMetadata}
|
||||
updateMetadata={updateMetadataFileArray}
|
||||
/>
|
||||
</Conditional>
|
||||
</div>
|
||||
<SingleAssetPreview
|
||||
relatedAsset={assetFilesArray[0]}
|
||||
subtitle={`Asset filename: ${assetFilesArray[0]?.name}`}
|
||||
updateMetadataFileIndex={updateMetadataFileIndex}
|
||||
/>
|
||||
</div>
|
||||
<MetadataInput
|
||||
selectedAssetFile={assetFilesArray[0]}
|
||||
selectedMetadataFile={metadataFilesArray[0]}
|
||||
updateMetadataToUpload={updateOpenEditionMinterMetadataFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Trait } from 'contracts/badgeHub'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { TextInput } from '../forms/FormInput'
|
||||
import { MetadataAttributes } from '../forms/MetadataAttributes'
|
||||
import { Tooltip } from '../Tooltip'
|
||||
import type { UploadMethod } from './ImageUploadDetails'
|
||||
|
||||
interface OnChainMetadataInputDetailsProps {
|
||||
onChange: (data: OnChainMetadataInputDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod | undefined
|
||||
}
|
||||
|
||||
export interface OnChainMetadataInputDetailsDataProps {
|
||||
name?: string
|
||||
description?: string
|
||||
attributes?: Trait[]
|
||||
image_data?: string
|
||||
external_url?: string
|
||||
background_color?: string
|
||||
animation_url?: string
|
||||
youtube_url?: string
|
||||
}
|
||||
|
||||
export const OnChainMetadataInputDetails = ({ onChange, uploadMethod }: OnChainMetadataInputDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [metadataFile, setMetadataFile] = useState<File>()
|
||||
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
|
||||
|
||||
const metadataFileRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
placeholder: 'My Awesome Collection',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
id: 'description',
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
placeholder: 'My Awesome Collection Description',
|
||||
})
|
||||
|
||||
const imageDataState = useInputState({
|
||||
id: 'metadata-image-data',
|
||||
name: 'metadata-image-data',
|
||||
title: 'Image Data',
|
||||
subtitle: 'Raw SVG image data',
|
||||
})
|
||||
|
||||
const externalUrlState = useInputState({
|
||||
id: 'metadata-external-url',
|
||||
name: 'metadata-external-url',
|
||||
title: 'External URL',
|
||||
subtitle: 'External URL for the token',
|
||||
placeholder: 'https://',
|
||||
})
|
||||
|
||||
const attributesState = useMetadataAttributesState()
|
||||
|
||||
const animationUrlState = useInputState({
|
||||
id: 'metadata-animation-url',
|
||||
name: 'metadata-animation-url',
|
||||
title: 'Animation URL',
|
||||
subtitle: 'Animation URL for the token',
|
||||
placeholder: 'https://',
|
||||
})
|
||||
|
||||
const youtubeUrlState = useInputState({
|
||||
id: 'metadata-youtube-url',
|
||||
name: 'metadata-youtube-url',
|
||||
title: 'YouTube URL',
|
||||
subtitle: 'YouTube URL for the token',
|
||||
placeholder: 'https://',
|
||||
})
|
||||
|
||||
const parseMetadata = async () => {
|
||||
try {
|
||||
let parsedMetadata: any
|
||||
if (metadataFile) {
|
||||
attributesState.reset()
|
||||
parsedMetadata = JSON.parse(await metadataFile.text())
|
||||
|
||||
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
|
||||
attributesState.add({
|
||||
trait_type: parsedMetadata.attributes[i].trait_type,
|
||||
value: parsedMetadata.attributes[i].value,
|
||||
})
|
||||
}
|
||||
}
|
||||
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
|
||||
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
|
||||
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
|
||||
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
|
||||
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
|
||||
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
|
||||
} else {
|
||||
attributesState.reset()
|
||||
nameState.onChange('')
|
||||
descriptionState.onChange('')
|
||||
externalUrlState.onChange('')
|
||||
youtubeUrlState.onChange('')
|
||||
animationUrlState.onChange('')
|
||||
imageDataState.onChange('')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error parsing metadata file: Invalid JSON format.')
|
||||
if (metadataFileRef.current) metadataFileRef.current.value = ''
|
||||
setMetadataFile(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMetadataFile(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: 'application/json' })
|
||||
}
|
||||
// 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.')
|
||||
setMetadataFile(selectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void parseMetadata()
|
||||
if (!metadataFile)
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
}, [metadataFile])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: OnChainMetadataInputDetailsDataProps = {
|
||||
name: nameState.value || undefined,
|
||||
description: descriptionState.value || undefined,
|
||||
attributes:
|
||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
||||
? attributesState.values
|
||||
.map((attr) => ({
|
||||
trait_type: attr.trait_type,
|
||||
value: attr.value,
|
||||
}))
|
||||
.filter((attr) => attr.trait_type && attr.value)
|
||||
: undefined,
|
||||
image_data: imageDataState.value || undefined,
|
||||
external_url: externalUrlState.value || undefined,
|
||||
animation_url: animationUrlState.value.trim() || undefined,
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
nameState.value,
|
||||
descriptionState.value,
|
||||
timestamp,
|
||||
imageDataState.value,
|
||||
externalUrlState.value,
|
||||
attributesState.values,
|
||||
animationUrlState.value,
|
||||
youtubeUrlState.value,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="py-3 px-8 rounded border-2 border-white/20">
|
||||
<span className="ml-4 text-xl font-bold underline underline-offset-4">NFT Metadata</span>
|
||||
<div className={clsx('grid grid-cols-2 mt-4 mb-2 ml-4 max-w-6xl')}>
|
||||
<div className={clsx('mt-6')}>
|
||||
<TextInput className="mt-2" {...nameState} />
|
||||
<TextInput className="mt-2" {...descriptionState} />
|
||||
<TextInput className="mt-2" {...externalUrlState} />
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<TextInput className="mt-2" {...animationUrlState} />
|
||||
</Conditional>
|
||||
<TextInput className="mt-2" {...youtubeUrlState} />
|
||||
</div>
|
||||
<div className={clsx('ml-10')}>
|
||||
<div>
|
||||
<MetadataAttributes
|
||||
attributes={attributesState.entries}
|
||||
onAdd={attributesState.add}
|
||||
onChange={attributesState.update}
|
||||
onRemove={attributesState.remove}
|
||||
title="Traits"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label="A metadata file can be selected to automatically fill in the related fields."
|
||||
placement="bottom"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFile"
|
||||
>
|
||||
Metadata File Selection (optional)
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="application/json"
|
||||
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="metadataFile"
|
||||
onChange={selectMetadata}
|
||||
ref={metadataFileRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,752 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import { coin } from '@cosmjs/proto-signing'
|
||||
import clsx from 'clsx'
|
||||
import { Button } from 'components/Button'
|
||||
import type { MinterType } from 'components/collections/actions/Combobox'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ConfirmationModal } from 'components/ConfirmationModal'
|
||||
import { LoadingModal } from 'components/LoadingModal'
|
||||
import { openEditionMinterList } from 'config/minter'
|
||||
import type { TokenInfo } from 'config/token'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { DispatchExecuteArgs as OpenEditionFactoryDispatchExecuteArgs } from 'contracts/openEditionFactory/messages/execute'
|
||||
import { dispatchExecute as openEditionFactoryDispatchExecute } from 'contracts/openEditionFactory/messages/execute'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { upload } from 'services/upload'
|
||||
import {
|
||||
OPEN_EDITION_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
||||
SG721_OPEN_EDITION_CODE_ID,
|
||||
SG721_OPEN_EDITION_UPDATABLE_CODE_ID,
|
||||
} from 'utils/constants'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { checkTokenUri } from 'utils/isValidTokenUri'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import { type CollectionDetailsDataProps, CollectionDetails } from './CollectionDetails'
|
||||
import type { ImageUploadDetailsDataProps } from './ImageUploadDetails'
|
||||
import { ImageUploadDetails } from './ImageUploadDetails'
|
||||
import type { MintingDetailsDataProps } from './MintingDetails'
|
||||
import { MintingDetails } from './MintingDetails'
|
||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
||||
import {
|
||||
type OffChainMetadataUploadDetailsDataProps,
|
||||
OffChainMetadataUploadDetails,
|
||||
} from './OffChainMetadataUploadDetails'
|
||||
import type { OnChainMetadataInputDetailsDataProps } from './OnChainMetadataInputDetails'
|
||||
import { OnChainMetadataInputDetails } from './OnChainMetadataInputDetails'
|
||||
import { type RoyaltyDetailsDataProps, RoyaltyDetails } from './RoyaltyDetails'
|
||||
|
||||
export type MetadataStorageMethod = 'off-chain' | 'on-chain'
|
||||
|
||||
export interface OpenEditionMinterDetailsDataProps {
|
||||
imageUploadDetails?: ImageUploadDetailsDataProps
|
||||
collectionDetails?: CollectionDetailsDataProps
|
||||
royaltyDetails?: RoyaltyDetailsDataProps
|
||||
onChainMetadataInputDetails?: OnChainMetadataInputDetailsDataProps
|
||||
offChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps
|
||||
mintingDetails?: MintingDetailsDataProps
|
||||
metadataStorageMethod?: MetadataStorageMethod
|
||||
}
|
||||
|
||||
interface OpenEditionMinterCreatorProps {
|
||||
onChange: (data: OpenEditionMinterCreatorDataProps) => void
|
||||
onDetailsChange: (data: OpenEditionMinterDetailsDataProps) => void
|
||||
openEditionMinterUpdatableCreationFee?: string
|
||||
openEditionMinterCreationFee?: string
|
||||
minimumMintPrice?: string
|
||||
minimumUpdatableMintPrice?: string
|
||||
minterType?: MinterType
|
||||
mintTokenFromFactory?: TokenInfo | undefined
|
||||
}
|
||||
|
||||
export interface OpenEditionMinterCreatorDataProps {
|
||||
metadataStorageMethod: MetadataStorageMethod
|
||||
openEditionMinterContractAddress: string | null
|
||||
sg721ContractAddress: string | null
|
||||
transactionHash: string | null
|
||||
}
|
||||
|
||||
export const OpenEditionMinterCreator = ({
|
||||
onChange,
|
||||
onDetailsChange,
|
||||
openEditionMinterCreationFee,
|
||||
openEditionMinterUpdatableCreationFee,
|
||||
minimumMintPrice,
|
||||
minimumUpdatableMintPrice,
|
||||
minterType,
|
||||
mintTokenFromFactory,
|
||||
}: OpenEditionMinterCreatorProps) => {
|
||||
const wallet = useWallet()
|
||||
const { openEditionMinter: openEditionMinterContract, openEditionFactory: openEditionFactoryContract } =
|
||||
useContracts()
|
||||
|
||||
const [metadataStorageMethod, setMetadataStorageMethod] = useState<MetadataStorageMethod>('off-chain')
|
||||
const [imageUploadDetails, setImageUploadDetails] = useState<ImageUploadDetailsDataProps | null>(null)
|
||||
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
|
||||
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
|
||||
const [onChainMetadataInputDetails, setOnChainMetadataInputDetails] =
|
||||
useState<OnChainMetadataInputDetailsDataProps | null>(null)
|
||||
const [offChainMetadataUploadDetails, setOffChainMetadataUploadDetails] =
|
||||
useState<OffChainMetadataUploadDetailsDataProps | null>(null)
|
||||
const [mintingDetails, setMintingDetails] = useState<MintingDetailsDataProps | null>(null)
|
||||
|
||||
const [creationInProgress, setCreationInProgress] = useState(false)
|
||||
const [readyToCreate, setReadyToCreate] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [tokenUri, setTokenUri] = useState<string | null>(null)
|
||||
const [tokenImageUri, setTokenImageUri] = useState<string | null>(null)
|
||||
const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
|
||||
const [openEditionMinterContractAddress, setOpenEditionMinterContractAddress] = useState<string | null>(null)
|
||||
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
|
||||
const [transactionHash, setTransactionHash] = useState<string | null>(null)
|
||||
|
||||
const factoryAddressForSelectedDenom =
|
||||
openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === false)
|
||||
?.factoryAddress || OPEN_EDITION_FACTORY_ADDRESS
|
||||
const updatableFactoryAddressForSelectedDenom =
|
||||
openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === true)
|
||||
?.factoryAddress || OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS
|
||||
|
||||
const openEditionFactoryMessages = useMemo(
|
||||
() =>
|
||||
openEditionFactoryContract?.use(
|
||||
collectionDetails?.updatable ? updatableFactoryAddressForSelectedDenom : factoryAddressForSelectedDenom,
|
||||
),
|
||||
[openEditionFactoryContract, wallet.address],
|
||||
)
|
||||
|
||||
const performOpenEditionMinterChecks = () => {
|
||||
try {
|
||||
setReadyToCreate(false)
|
||||
checkCollectionDetails()
|
||||
checkMintingDetails()
|
||||
void checkUploadDetails()
|
||||
.then(() => {
|
||||
void checkRoyaltyDetails()
|
||||
.then(() => {
|
||||
void checkwalletBalance()
|
||||
.then(() => {
|
||||
setReadyToCreate(true)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(`Error in Wallet Balance: ${error.message}`, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setReadyToCreate(false)
|
||||
})
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(`Error in Royalty Details: ${error.message}`, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setReadyToCreate(false)
|
||||
})
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(`Error in Upload Details: ${error.message}`, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setReadyToCreate(false)
|
||||
})
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setUploading(false)
|
||||
setReadyToCreate(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkUploadDetails = async () => {
|
||||
if (!wallet.initialized) throw new Error('Wallet not connected.')
|
||||
if (
|
||||
(metadataStorageMethod === 'off-chain' && !offChainMetadataUploadDetails) ||
|
||||
(metadataStorageMethod === 'on-chain' && !imageUploadDetails)
|
||||
) {
|
||||
throw new Error('Please select assets and metadata')
|
||||
}
|
||||
|
||||
if (
|
||||
metadataStorageMethod === 'off-chain' &&
|
||||
offChainMetadataUploadDetails?.uploadMethod === 'new' &&
|
||||
offChainMetadataUploadDetails.assetFiles.length === 0
|
||||
) {
|
||||
throw new Error('Please select the asset file')
|
||||
}
|
||||
if (
|
||||
metadataStorageMethod === 'on-chain' &&
|
||||
imageUploadDetails?.uploadMethod === 'new' &&
|
||||
imageUploadDetails.assetFile === undefined
|
||||
) {
|
||||
throw new Error('Please select the asset file')
|
||||
}
|
||||
if (metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'new') {
|
||||
if (
|
||||
offChainMetadataUploadDetails.uploadService === 'nft-storage' &&
|
||||
offChainMetadataUploadDetails.nftStorageApiKey === ''
|
||||
) {
|
||||
throw new Error('Please enter a valid NFT.Storage API key')
|
||||
} else if (
|
||||
offChainMetadataUploadDetails.uploadService === 'pinata' &&
|
||||
(offChainMetadataUploadDetails.pinataApiKey === '' || offChainMetadataUploadDetails.pinataSecretKey === '')
|
||||
) {
|
||||
throw new Error('Please enter valid Pinata API and secret keys')
|
||||
}
|
||||
}
|
||||
if (metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'new') {
|
||||
if (imageUploadDetails.uploadService === 'nft-storage' && imageUploadDetails.nftStorageApiKey === '') {
|
||||
throw new Error('Please enter a valid NFT.Storage API key')
|
||||
} else if (
|
||||
imageUploadDetails.uploadService === 'pinata' &&
|
||||
(imageUploadDetails.pinataApiKey === '' || imageUploadDetails.pinataSecretKey === '')
|
||||
) {
|
||||
throw new Error('Please enter valid Pinata API and secret keys')
|
||||
}
|
||||
}
|
||||
if (metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'existing') {
|
||||
if (
|
||||
offChainMetadataUploadDetails.tokenURI === '' ||
|
||||
!(offChainMetadataUploadDetails.tokenURI as string).includes('ipfs://')
|
||||
) {
|
||||
throw new Error('Please enter a valid token URI')
|
||||
}
|
||||
if (
|
||||
offChainMetadataUploadDetails.imageUrl === '' ||
|
||||
!(offChainMetadataUploadDetails.imageUrl as string).includes('ipfs://')
|
||||
) {
|
||||
throw new Error('Please enter a valid image URI')
|
||||
}
|
||||
}
|
||||
if (metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'existing') {
|
||||
if (imageUploadDetails.imageUrl === '' || !(imageUploadDetails.imageUrl as string).includes('ipfs://')) {
|
||||
throw new Error('Please enter a valid asset URI')
|
||||
}
|
||||
if (
|
||||
imageUploadDetails.coverImageUrl === '' ||
|
||||
!(imageUploadDetails.coverImageUrl as string).includes('ipfs://')
|
||||
) {
|
||||
throw new Error('Please enter a valid cover image URL')
|
||||
}
|
||||
}
|
||||
if (offChainMetadataUploadDetails?.uploadMethod === 'existing') {
|
||||
await checkTokenUri(offChainMetadataUploadDetails.tokenURI as string)
|
||||
}
|
||||
}
|
||||
|
||||
const checkCollectionDetails = () => {
|
||||
if (!collectionDetails) throw new Error('Please fill out the collection details')
|
||||
if (collectionDetails.name === '') throw new Error('Collection name is required')
|
||||
if (collectionDetails.description === '') throw new Error('Collection description is required')
|
||||
if (collectionDetails.symbol === '') throw new Error('Collection symbol is required')
|
||||
if (collectionDetails.description.length > 512)
|
||||
throw new Error('Collection description cannot exceed 512 characters')
|
||||
if (
|
||||
metadataStorageMethod === 'off-chain' &&
|
||||
offChainMetadataUploadDetails?.uploadMethod === 'new' &&
|
||||
collectionDetails.imageFile.length === 0
|
||||
)
|
||||
throw new Error('Collection cover image is required')
|
||||
if (
|
||||
metadataStorageMethod === 'on-chain' &&
|
||||
imageUploadDetails?.uploadMethod === 'new' &&
|
||||
collectionDetails.imageFile.length === 0
|
||||
)
|
||||
throw new Error('Collection cover image is required')
|
||||
if (
|
||||
collectionDetails.startTradingTime &&
|
||||
Number(collectionDetails.startTradingTime) < new Date().getTime() * 1000000
|
||||
)
|
||||
throw new Error('Invalid trading start time')
|
||||
if (
|
||||
collectionDetails.startTradingTime &&
|
||||
Number(collectionDetails.startTradingTime) < Number(mintingDetails?.startTime)
|
||||
)
|
||||
throw new Error('Trading start time must be after minting start time')
|
||||
if (collectionDetails.externalLink) {
|
||||
try {
|
||||
const url = new URL(collectionDetails.externalLink)
|
||||
} catch (e: any) {
|
||||
throw new Error(`Invalid external link: Make sure to include the protocol (e.g. https://)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkMintingDetails = () => {
|
||||
if (!mintingDetails) throw new Error('Please fill out the minting details')
|
||||
if (mintingDetails.unitPrice === '') throw new Error('Mint price is required')
|
||||
if (collectionDetails?.updatable) {
|
||||
if (Number(mintingDetails.unitPrice) < Number(minimumUpdatableMintPrice))
|
||||
throw new Error(
|
||||
`Invalid mint price: The minimum mint price is ${Number(minimumUpdatableMintPrice) / 1000000} ${
|
||||
mintTokenFromFactory?.displayName
|
||||
}`,
|
||||
)
|
||||
} else if (Number(mintingDetails.unitPrice) < Number(minimumMintPrice))
|
||||
throw new Error(
|
||||
`Invalid mint price: The minimum mint price is ${Number(minimumMintPrice) / 1000000} ${
|
||||
mintTokenFromFactory?.displayName
|
||||
}`,
|
||||
)
|
||||
if (!mintingDetails.perAddressLimit || mintingDetails.perAddressLimit < 1 || mintingDetails.perAddressLimit > 50)
|
||||
throw new Error('Invalid limit for tokens per address')
|
||||
if (mintingDetails.startTime === '') throw new Error('Start time is required')
|
||||
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
|
||||
if (
|
||||
mintingDetails.paymentAddress &&
|
||||
(!isValidAddress(mintingDetails.paymentAddress) || !mintingDetails.paymentAddress.startsWith('stars1'))
|
||||
)
|
||||
throw new Error('Invalid payment address')
|
||||
}
|
||||
|
||||
const checkRoyaltyDetails = async () => {
|
||||
if (!royaltyDetails) throw new Error('Please fill out the royalty details')
|
||||
if (royaltyDetails.royaltyType === 'new') {
|
||||
if (royaltyDetails.share === 0) throw new Error('Royalty share percentage is required')
|
||||
if (royaltyDetails.share > 100 || royaltyDetails.share < 0) throw new Error('Invalid royalty share percentage')
|
||||
if (royaltyDetails.paymentAddress === '') throw new Error('Royalty payment address is required')
|
||||
if (!isValidAddress(royaltyDetails.paymentAddress.trim())) {
|
||||
if (royaltyDetails.paymentAddress.trim().endsWith('.stars')) {
|
||||
throw new Error('Royalty payment address could not be resolved')
|
||||
}
|
||||
throw new Error('Invalid royalty payment address')
|
||||
}
|
||||
const contractInfoResponse = await wallet.client
|
||||
?.queryContractRaw(
|
||||
royaltyDetails.paymentAddress.trim(),
|
||||
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (e.message.includes('bech32')) throw new Error('Invalid royalty payment address.')
|
||||
console.log(e.message)
|
||||
})
|
||||
if (contractInfoResponse !== undefined) {
|
||||
const contractInfo = JSON.parse(new TextDecoder().decode(contractInfoResponse as Uint8Array))
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (contractInfo && !contractInfo.contract.includes('splits'))
|
||||
throw new Error('The provided royalty payment address does not belong to a splits contract.')
|
||||
else console.log(contractInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkwalletBalance = async () => {
|
||||
if (!wallet.initialized) throw new Error('Wallet not connected.')
|
||||
const amountNeeded = collectionDetails?.updatable
|
||||
? Number(openEditionMinterUpdatableCreationFee)
|
||||
: Number(openEditionMinterCreationFee)
|
||||
await wallet.client?.getBalance(wallet.address, 'ustars').then((balance) => {
|
||||
if (amountNeeded >= Number(balance.amount))
|
||||
throw new Error(
|
||||
`Insufficient wallet balance to instantiate the required contracts. Needed amount: ${(
|
||||
amountNeeded / 1000000
|
||||
).toString()} STARS`,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const createOpenEditionMinter = async () => {
|
||||
try {
|
||||
setCreationInProgress(true)
|
||||
setTokenUri(null)
|
||||
setCoverImageUrl(null)
|
||||
setTokenImageUri(null)
|
||||
setOpenEditionMinterContractAddress(null)
|
||||
setSg721ContractAddress(null)
|
||||
setTransactionHash(null)
|
||||
if (metadataStorageMethod === 'off-chain') {
|
||||
if (offChainMetadataUploadDetails?.uploadMethod === 'new') {
|
||||
setUploading(true)
|
||||
const metadataUri = await uploadForOffChainStorage()
|
||||
const coverImageUri = await upload(
|
||||
collectionDetails?.imageFile as File[],
|
||||
offChainMetadataUploadDetails.uploadService,
|
||||
'cover',
|
||||
offChainMetadataUploadDetails.nftStorageApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
const metadataUriWithBase = `ipfs://${metadataUri}/${(
|
||||
offChainMetadataUploadDetails.openEditionMinterMetadataFile as File
|
||||
).name.substring(
|
||||
0,
|
||||
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.lastIndexOf('.'),
|
||||
)}`
|
||||
const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}`
|
||||
|
||||
setTokenUri(metadataUriWithBase)
|
||||
setCoverImageUrl(coverImageUriWithBase)
|
||||
setUploading(false)
|
||||
await instantiateOpenEditionMinter(metadataUriWithBase, coverImageUriWithBase)
|
||||
} else {
|
||||
setTokenUri(offChainMetadataUploadDetails?.tokenURI as string)
|
||||
setCoverImageUrl(offChainMetadataUploadDetails?.imageUrl as string)
|
||||
await instantiateOpenEditionMinter(
|
||||
offChainMetadataUploadDetails?.tokenURI as string,
|
||||
offChainMetadataUploadDetails?.imageUrl as string,
|
||||
)
|
||||
}
|
||||
} else if (metadataStorageMethod === 'on-chain') {
|
||||
if (imageUploadDetails?.uploadMethod === 'new') {
|
||||
setUploading(true)
|
||||
const imageUri = await upload(
|
||||
[imageUploadDetails.assetFile as File],
|
||||
imageUploadDetails.uploadService,
|
||||
'cover',
|
||||
imageUploadDetails.nftStorageApiKey as string,
|
||||
imageUploadDetails.pinataApiKey as string,
|
||||
imageUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
const imageUriWithBase = `ipfs://${imageUri}/${(imageUploadDetails.assetFile as File).name}`
|
||||
setTokenImageUri(imageUriWithBase)
|
||||
|
||||
const coverImageUri = await upload(
|
||||
collectionDetails?.imageFile as File[],
|
||||
imageUploadDetails.uploadService,
|
||||
'cover',
|
||||
imageUploadDetails.nftStorageApiKey as string,
|
||||
imageUploadDetails.pinataApiKey as string,
|
||||
imageUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}`
|
||||
setCoverImageUrl(coverImageUriWithBase)
|
||||
|
||||
setUploading(false)
|
||||
await instantiateOpenEditionMinter(imageUriWithBase, coverImageUriWithBase)
|
||||
} else if (imageUploadDetails?.uploadMethod === 'existing') {
|
||||
setTokenImageUri(imageUploadDetails.imageUrl as string)
|
||||
setCoverImageUrl(imageUploadDetails.coverImageUrl as string)
|
||||
await instantiateOpenEditionMinter(
|
||||
imageUploadDetails.imageUrl as string,
|
||||
imageUploadDetails.coverImageUrl as string,
|
||||
)
|
||||
}
|
||||
}
|
||||
setCreationInProgress(false)
|
||||
setReadyToCreate(false)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' }, duration: 10000 })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setReadyToCreate(false)
|
||||
setCreationInProgress(false)
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadForOffChainStorage = async (): Promise<string> => {
|
||||
if (!offChainMetadataUploadDetails) throw new Error('Please select the asset and fill in the metadata')
|
||||
return new Promise((resolve, reject) => {
|
||||
upload(
|
||||
offChainMetadataUploadDetails.assetFiles,
|
||||
offChainMetadataUploadDetails.uploadService,
|
||||
'assets',
|
||||
offChainMetadataUploadDetails.nftStorageApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
.then((assetUri: string) => {
|
||||
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'
|
||||
) {
|
||||
data.animation_url = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}`
|
||||
}
|
||||
if (getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) !== 'html')
|
||||
data.image = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}`
|
||||
|
||||
const metadataFileBlob = new Blob([JSON.stringify(data)], {
|
||||
type: 'application/json',
|
||||
})
|
||||
|
||||
console.log('Name: ', (offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name)
|
||||
const updatedMetadataFile = new File(
|
||||
[metadataFileBlob],
|
||||
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.substring(
|
||||
0,
|
||||
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.lastIndexOf('.'),
|
||||
),
|
||||
{
|
||||
type: 'application/json',
|
||||
},
|
||||
)
|
||||
|
||||
fileArray.push(updatedMetadataFile)
|
||||
}
|
||||
reader.onloadend = () => {
|
||||
upload(
|
||||
fileArray,
|
||||
offChainMetadataUploadDetails.uploadService,
|
||||
'metadata',
|
||||
offChainMetadataUploadDetails.nftStorageApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
}
|
||||
console.log('File: ', offChainMetadataUploadDetails.openEditionMinterMetadataFile)
|
||||
reader.readAsText(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File, 'utf8')
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
const instantiateOpenEditionMinter = async (uri: string, coverImageUri: 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')
|
||||
|
||||
let royaltyInfo = null
|
||||
if (royaltyDetails?.royaltyType === 'new') {
|
||||
royaltyInfo = {
|
||||
payment_address: royaltyDetails.paymentAddress.trim(),
|
||||
share: (Number(royaltyDetails.share) / 100).toString(),
|
||||
}
|
||||
}
|
||||
|
||||
const msg = {
|
||||
create_minter: {
|
||||
init_msg: {
|
||||
nft_data: {
|
||||
nft_data_type: metadataStorageMethod === 'off-chain' ? 'off_chain_metadata' : 'on_chain_metadata',
|
||||
token_uri: metadataStorageMethod === 'off-chain' ? uri : null,
|
||||
extension:
|
||||
metadataStorageMethod === 'on-chain'
|
||||
? {
|
||||
image: uri,
|
||||
name: onChainMetadataInputDetails?.name,
|
||||
description: onChainMetadataInputDetails?.description,
|
||||
attributes: onChainMetadataInputDetails?.attributes,
|
||||
external_url: onChainMetadataInputDetails?.external_url,
|
||||
animation_url:
|
||||
imageUploadDetails?.uploadMethod === 'existing'
|
||||
? onChainMetadataInputDetails?.animation_url
|
||||
: getAssetType(imageUploadDetails?.assetFile?.name as string) === 'video'
|
||||
? uri
|
||||
: undefined,
|
||||
youtube_url: onChainMetadataInputDetails?.youtube_url,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
start_time: mintingDetails?.startTime,
|
||||
end_time: mintingDetails?.endTime,
|
||||
mint_price: {
|
||||
amount: Number(mintingDetails?.unitPrice).toString(),
|
||||
denom: (mintTokenFromFactory?.denom as string) || 'ustars',
|
||||
},
|
||||
per_address_limit: mintingDetails?.perAddressLimit,
|
||||
payment_address: mintingDetails?.paymentAddress || null,
|
||||
},
|
||||
collection_params: {
|
||||
code_id: collectionDetails?.updatable ? SG721_OPEN_EDITION_UPDATABLE_CODE_ID : SG721_OPEN_EDITION_CODE_ID,
|
||||
name: collectionDetails?.name,
|
||||
symbol: collectionDetails?.symbol,
|
||||
info: {
|
||||
creator: wallet.address,
|
||||
description: collectionDetails?.description,
|
||||
image: coverImageUri,
|
||||
explicit_content: collectionDetails?.explicit || false,
|
||||
royalty_info: royaltyInfo,
|
||||
start_trading_time: collectionDetails?.startTradingTime || null,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
console.log('msg: ', msg)
|
||||
console.log('Using factory address: ', factoryAddressForSelectedDenom)
|
||||
const payload: OpenEditionFactoryDispatchExecuteArgs = {
|
||||
contract: collectionDetails?.updatable ? updatableFactoryAddressForSelectedDenom : factoryAddressForSelectedDenom,
|
||||
messages: openEditionFactoryMessages,
|
||||
txSigner: wallet.address,
|
||||
msg,
|
||||
funds: [
|
||||
coin(
|
||||
collectionDetails?.updatable
|
||||
? (openEditionMinterUpdatableCreationFee as string)
|
||||
: (openEditionMinterCreationFee as string),
|
||||
'ustars',
|
||||
),
|
||||
],
|
||||
updatable: collectionDetails?.updatable,
|
||||
}
|
||||
await openEditionFactoryDispatchExecute(payload)
|
||||
.then((data) => {
|
||||
setTransactionHash(data.transactionHash)
|
||||
setOpenEditionMinterContractAddress(data.openEditionMinterAddress)
|
||||
setSg721ContractAddress(data.sg721Address)
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setUploading(false)
|
||||
setCreationInProgress(false)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
if (minterType !== 'openEdition') {
|
||||
setTransactionHash(null)
|
||||
setOpenEditionMinterContractAddress(null)
|
||||
setSg721ContractAddress(null)
|
||||
setCreationInProgress(false)
|
||||
setUploading(false)
|
||||
}
|
||||
}, [minterType])
|
||||
|
||||
useEffect(() => {
|
||||
const data: OpenEditionMinterCreatorDataProps = {
|
||||
metadataStorageMethod,
|
||||
openEditionMinterContractAddress,
|
||||
sg721ContractAddress,
|
||||
transactionHash,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [metadataStorageMethod, openEditionMinterContractAddress, sg721ContractAddress, transactionHash])
|
||||
|
||||
useEffect(() => {
|
||||
const data: OpenEditionMinterDetailsDataProps = {
|
||||
imageUploadDetails: imageUploadDetails ? imageUploadDetails : undefined,
|
||||
collectionDetails: collectionDetails ? collectionDetails : undefined,
|
||||
royaltyDetails: royaltyDetails ? royaltyDetails : undefined,
|
||||
onChainMetadataInputDetails: onChainMetadataInputDetails ? onChainMetadataInputDetails : undefined,
|
||||
offChainMetadataUploadDetails: offChainMetadataUploadDetails ? offChainMetadataUploadDetails : undefined,
|
||||
mintingDetails: mintingDetails ? mintingDetails : undefined,
|
||||
metadataStorageMethod,
|
||||
}
|
||||
onDetailsChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
imageUploadDetails,
|
||||
collectionDetails,
|
||||
royaltyDetails,
|
||||
onChainMetadataInputDetails,
|
||||
offChainMetadataUploadDetails,
|
||||
mintingDetails,
|
||||
])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* TODO: Cancel once we're able to index on-chain metadata */}
|
||||
<Conditional test={false}>
|
||||
<div className="mx-10 mb-4 rounded border-2 border-white/20">
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={metadataStorageMethod === 'off-chain'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio9"
|
||||
name="inlineRadioOptions9"
|
||||
onClick={() => {
|
||||
setMetadataStorageMethod('off-chain')
|
||||
}}
|
||||
type="radio"
|
||||
value="Off Chain"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio9"
|
||||
>
|
||||
Off-Chain Metadata
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={metadataStorageMethod === 'on-chain'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio10"
|
||||
name="inlineRadioOptions10"
|
||||
onClick={() => {
|
||||
setMetadataStorageMethod('on-chain')
|
||||
}}
|
||||
type="radio"
|
||||
value="On Chain"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio10"
|
||||
>
|
||||
On-Chain Metadata
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
<div className={clsx('my-4 mx-10')}>
|
||||
<Conditional test={metadataStorageMethod === 'off-chain'}>
|
||||
<div>
|
||||
<OffChainMetadataUploadDetails onChange={setOffChainMetadataUploadDetails} />
|
||||
</div>
|
||||
</Conditional>
|
||||
<Conditional test={metadataStorageMethod === 'on-chain'}>
|
||||
<div>
|
||||
<ImageUploadDetails onChange={setImageUploadDetails} />
|
||||
<OnChainMetadataInputDetails
|
||||
onChange={setOnChainMetadataInputDetails}
|
||||
uploadMethod={imageUploadDetails?.uploadMethod}
|
||||
/>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
<div className="flex justify-between py-3 px-8 mx-10 rounded border-2 border-white/20 grid-col-2">
|
||||
<CollectionDetails
|
||||
coverImageUrl={
|
||||
metadataStorageMethod === 'off-chain'
|
||||
? (offChainMetadataUploadDetails?.imageUrl as string)
|
||||
: (imageUploadDetails?.coverImageUrl as string)
|
||||
}
|
||||
metadataStorageMethod={metadataStorageMethod}
|
||||
onChange={setCollectionDetails}
|
||||
uploadMethod={
|
||||
metadataStorageMethod === 'off-chain'
|
||||
? (offChainMetadataUploadDetails?.uploadMethod as UploadMethod)
|
||||
: (imageUploadDetails?.uploadMethod as UploadMethod)
|
||||
}
|
||||
/>
|
||||
<MintingDetails
|
||||
minimumMintPrice={
|
||||
collectionDetails?.updatable
|
||||
? Number(minimumUpdatableMintPrice) / 1000000
|
||||
: Number(minimumMintPrice) / 1000000
|
||||
}
|
||||
mintTokenFromFactory={mintTokenFromFactory}
|
||||
onChange={setMintingDetails}
|
||||
uploadMethod={offChainMetadataUploadDetails?.uploadMethod as UploadMethod}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-6">
|
||||
<RoyaltyDetails onChange={setRoyaltyDetails} />
|
||||
</div>
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
className="relative justify-center p-2 mr-12 mb-6 max-h-12 text-white bg-plumbus hover:bg-plumbus-light border-0"
|
||||
isLoading={creationInProgress}
|
||||
onClick={performOpenEditionMinterChecks}
|
||||
variant="solid"
|
||||
>
|
||||
Create Collection
|
||||
</Button>
|
||||
</div>
|
||||
<Conditional test={uploading}>
|
||||
<LoadingModal />
|
||||
</Conditional>
|
||||
<Conditional test={readyToCreate}>
|
||||
<ConfirmationModal confirm={createOpenEditionMinter} />
|
||||
</Conditional>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import { NumberInput, TextInput } from '../forms/FormInput'
|
||||
|
||||
interface RoyaltyDetailsProps {
|
||||
onChange: (data: RoyaltyDetailsDataProps) => void
|
||||
}
|
||||
|
||||
export interface RoyaltyDetailsDataProps {
|
||||
royaltyType: RoyaltyState
|
||||
paymentAddress: string
|
||||
share: number
|
||||
}
|
||||
|
||||
type RoyaltyState = 'none' | 'new'
|
||||
|
||||
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
|
||||
|
||||
const royaltyPaymentAddressState = useInputState({
|
||||
id: 'royalty-payment-address',
|
||||
name: 'royaltyPaymentAddress',
|
||||
title: 'Payment Address',
|
||||
subtitle: 'Address to receive royalties',
|
||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||
})
|
||||
|
||||
const royaltyShareState = useInputState({
|
||||
id: 'royalty-share',
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '5%',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
void resolveAddress(
|
||||
royaltyPaymentAddressState.value
|
||||
.toLowerCase()
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, ''),
|
||||
wallet,
|
||||
).then((royaltyPaymentAddress) => {
|
||||
royaltyPaymentAddressState.onChange(royaltyPaymentAddress)
|
||||
const data: RoyaltyDetailsDataProps = {
|
||||
royaltyType: royaltyState,
|
||||
paymentAddress: royaltyPaymentAddressState.value,
|
||||
share: Number(royaltyShareState.value),
|
||||
}
|
||||
onChange(data)
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value])
|
||||
|
||||
return (
|
||||
<div className="py-3 px-8 mx-10 rounded border-2 border-white/20">
|
||||
<div className="flex justify-center">
|
||||
<div className="ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={royaltyState === 'none'}
|
||||
className="peer sr-only"
|
||||
id="royaltyRadio1"
|
||||
name="royaltyRadioOptions1"
|
||||
onClick={() => {
|
||||
setRoyaltyState('none')
|
||||
}}
|
||||
type="radio"
|
||||
value="None"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="royaltyRadio1"
|
||||
>
|
||||
No royalty
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={royaltyState === 'new'}
|
||||
className="peer sr-only"
|
||||
id="royaltyRadio2"
|
||||
name="royaltyRadioOptions2"
|
||||
onClick={() => {
|
||||
setRoyaltyState('new')
|
||||
}}
|
||||
type="radio"
|
||||
value="Existing"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="royaltyRadio2"
|
||||
>
|
||||
Configure royalty details
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={royaltyState === 'new'}>
|
||||
<FormGroup subtitle="Information about royalty" title="Royalty Details">
|
||||
<TextInput {...royaltyPaymentAddressState} isRequired />
|
||||
<NumberInput {...royaltyShareState} isRequired />
|
||||
</FormGroup>
|
||||
</Conditional>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"path": "/assets/",
|
||||
"appName": "StargazeStudio",
|
||||
"appShortName": "StargazeStudio",
|
||||
"appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT collection in no time.",
|
||||
"appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own NFT collection in no time.",
|
||||
"developerName": "StargazeStudio",
|
||||
"developerURL": "https://",
|
||||
"background": "#FFC27D",
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
import {
|
||||
OPEN_EDITION_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
|
||||
VENDING_FACTORY_ADDRESS,
|
||||
VENDING_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_FACTORY_UPDATABLE_ADDRESS,
|
||||
VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS,
|
||||
VENDING_IBC_ATOM_FACTORY_ADDRESS,
|
||||
VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS,
|
||||
VENDING_IBC_ATOM_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_USDC_FACTORY_ADDRESS,
|
||||
VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
|
||||
VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
} from 'utils/constants'
|
||||
|
||||
import type { TokenInfo } from './token'
|
||||
import { ibcAtom, ibcFrnz, ibcUsdc, stars } from './token'
|
||||
|
||||
export interface MinterInfo {
|
||||
id: string
|
||||
factoryAddress: string
|
||||
supportedToken: TokenInfo
|
||||
updatable?: boolean
|
||||
flexible?: boolean
|
||||
}
|
||||
|
||||
export const openEditionStarsMinter: MinterInfo = {
|
||||
id: 'open-edition-stars-minter',
|
||||
factoryAddress: OPEN_EDITION_FACTORY_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableStarsMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-stars-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: true,
|
||||
}
|
||||
|
||||
export const openEditionIbcAtomMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-atom-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcAtomMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-atom-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: true,
|
||||
}
|
||||
|
||||
export const openEditionIbcUsdcMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-usdc-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcUsdcMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-usdc-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: true,
|
||||
}
|
||||
|
||||
export const openEditionIbcFrnzMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-frnz-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
|
||||
supportedToken: ibcFrnz,
|
||||
updatable: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcFrnzMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-frnz-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS,
|
||||
supportedToken: ibcFrnz,
|
||||
updatable: true,
|
||||
}
|
||||
|
||||
export const openEditionMinterList = [
|
||||
openEditionStarsMinter,
|
||||
openEditionUpdatableStarsMinter,
|
||||
openEditionUpdatableIbcAtomMinter,
|
||||
openEditionIbcAtomMinter,
|
||||
openEditionIbcFrnzMinter,
|
||||
openEditionUpdatableIbcFrnzMinter,
|
||||
openEditionIbcUsdcMinter,
|
||||
openEditionUpdatableIbcUsdcMinter,
|
||||
]
|
||||
|
||||
export const vendingStarsMinter: MinterInfo = {
|
||||
id: 'vending-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableStarsMinter: MinterInfo = {
|
||||
id: 'vending-updatable-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_UPDATABLE_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const vendingIbcAtomMinter: MinterInfo = {
|
||||
id: 'vending-ibc-atom-minter',
|
||||
factoryAddress: VENDING_IBC_ATOM_FACTORY_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableIbcAtomMinter: MinterInfo = {
|
||||
id: 'vending-updatable-ibc-atom-minter',
|
||||
factoryAddress: VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const vendingIbcUsdcMinter: MinterInfo = {
|
||||
id: 'vending-ibc-usdc-minter',
|
||||
factoryAddress: VENDING_IBC_USDC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableIbcUsdcMinter: MinterInfo = {
|
||||
id: 'vending-updatable-ibc-usdc-minter',
|
||||
factoryAddress: VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const vendingMinterList = [
|
||||
vendingStarsMinter,
|
||||
vendingUpdatableStarsMinter,
|
||||
vendingIbcAtomMinter,
|
||||
vendingUpdatableIbcAtomMinter,
|
||||
vendingIbcUsdcMinter,
|
||||
vendingUpdatableIbcUsdcMinter,
|
||||
]
|
||||
|
||||
export const flexibleVendingStarsMinter: MinterInfo = {
|
||||
id: 'flexible-vending-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingUpdatableStarsMinter: MinterInfo = {
|
||||
id: 'flexible-vending-updatable-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: true,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingIbcAtomMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-atom-minter',
|
||||
factoryAddress: VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingUpdatableIbcAtomMinter: MinterInfo = {
|
||||
id: 'flexible-vending-updatable-ibc-atom-minter',
|
||||
factoryAddress: VENDING_IBC_ATOM_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: true,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingIbcUsdcMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-usdc-minter',
|
||||
factoryAddress: VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingUpdatableIbcUsdcMinter: MinterInfo = {
|
||||
id: 'flexible-vending-updatable-ibc-usdc-minter',
|
||||
factoryAddress: VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: true,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingMinterList = [
|
||||
flexibleVendingStarsMinter,
|
||||
flexibleVendingUpdatableStarsMinter,
|
||||
flexibleVendingIbcAtomMinter,
|
||||
flexibleVendingUpdatableIbcAtomMinter,
|
||||
flexibleVendingIbcUsdcMinter,
|
||||
flexibleVendingUpdatableIbcUsdcMinter,
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
export interface TokenInfo {
|
||||
id: string
|
||||
denom: string
|
||||
displayName: string
|
||||
decimalPlaces: number
|
||||
imageURL?: string
|
||||
symbol?: string
|
||||
}
|
||||
|
||||
export const stars: TokenInfo = {
|
||||
id: 'stars',
|
||||
denom: 'ustars',
|
||||
displayName: 'STARS',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const ibcAtom: TokenInfo = {
|
||||
id: 'ibc-atom',
|
||||
denom: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2',
|
||||
displayName: 'ATOM',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const ibcUsdc: TokenInfo = {
|
||||
id: 'ibc-usdc',
|
||||
denom: 'ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858',
|
||||
displayName: 'USDC',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const ibcFrnz: TokenInfo = {
|
||||
id: 'ibc-frnz',
|
||||
denom: 'ibc/7FA7EC64490E3BDE5A1A28CBE73CC0AD22522794957BC891C46321E3A6074DB9',
|
||||
displayName: 'FRNZ',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const tokensList = [stars, ibcAtom, ibcUsdc, ibcFrnz]
|
||||
+7
-48
@@ -1,17 +1,7 @@
|
||||
import type { UseBadgeHubContractProps } from 'contracts/badgeHub'
|
||||
import { useBadgeHubContract } from 'contracts/badgeHub'
|
||||
import type { UseBaseFactoryContractProps } from 'contracts/baseFactory'
|
||||
import { useBaseFactoryContract } from 'contracts/baseFactory'
|
||||
import type { UseBaseMinterContractProps } from 'contracts/baseMinter'
|
||||
import { useBaseMinterContract } from 'contracts/baseMinter'
|
||||
import { type UseOpenEditionFactoryContractProps, useOpenEditionFactoryContract } from 'contracts/openEditionFactory'
|
||||
import { type UseOpenEditionMinterContractProps, useOpenEditionMinterContract } from 'contracts/openEditionMinter'
|
||||
import type { UseMinterContractProps } from 'contracts/minter'
|
||||
import { useMinterContract } from 'contracts/minter'
|
||||
import type { UseSG721ContractProps } from 'contracts/sg721'
|
||||
import { useSG721Contract } from 'contracts/sg721'
|
||||
import type { UseVendingFactoryContractProps } from 'contracts/vendingFactory'
|
||||
import { useVendingFactoryContract } from 'contracts/vendingFactory'
|
||||
import type { UseVendingMinterContractProps } from 'contracts/vendingMinter'
|
||||
import { useVendingMinterContract } from 'contracts/vendingMinter'
|
||||
import type { UseWhiteListContractProps } from 'contracts/whitelist'
|
||||
import { useWhiteListContract } from 'contracts/whitelist'
|
||||
import type { ReactNode, VFC } from 'react'
|
||||
@@ -19,23 +9,13 @@ import { Fragment, useEffect } from 'react'
|
||||
import type { State } from 'zustand'
|
||||
import create from 'zustand'
|
||||
|
||||
import type { UseSplitsContractProps } from '../contracts/splits/useContract'
|
||||
import { useSplitsContract } from '../contracts/splits/useContract'
|
||||
|
||||
/**
|
||||
* Contracts store type definitions
|
||||
*/
|
||||
export interface ContractsStore extends State {
|
||||
sg721: UseSG721ContractProps | null
|
||||
vendingMinter: UseVendingMinterContractProps | null
|
||||
baseMinter: UseBaseMinterContractProps | null
|
||||
openEditionMinter: UseOpenEditionMinterContractProps | null
|
||||
minter: UseMinterContractProps | null
|
||||
whitelist: UseWhiteListContractProps | null
|
||||
vendingFactory: UseVendingFactoryContractProps | null
|
||||
baseFactory: UseBaseFactoryContractProps | null
|
||||
openEditionFactory: UseOpenEditionFactoryContractProps | null
|
||||
badgeHub: UseBadgeHubContractProps | null
|
||||
splits: UseSplitsContractProps | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,15 +23,8 @@ export interface ContractsStore extends State {
|
||||
*/
|
||||
export const defaultValues: ContractsStore = {
|
||||
sg721: null,
|
||||
vendingMinter: null,
|
||||
baseMinter: null,
|
||||
openEditionMinter: null,
|
||||
minter: null,
|
||||
whitelist: null,
|
||||
vendingFactory: null,
|
||||
baseFactory: null,
|
||||
openEditionFactory: null,
|
||||
badgeHub: null,
|
||||
splits: null,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,30 +49,16 @@ export const ContractsProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const ContractsSubscription: VFC = () => {
|
||||
const sg721 = useSG721Contract()
|
||||
const vendingMinter = useVendingMinterContract()
|
||||
const baseMinter = useBaseMinterContract()
|
||||
const openEditionMinter = useOpenEditionMinterContract()
|
||||
const minter = useMinterContract()
|
||||
const whitelist = useWhiteListContract()
|
||||
const vendingFactory = useVendingFactoryContract()
|
||||
const baseFactory = useBaseFactoryContract()
|
||||
const openEditionFactory = useOpenEditionFactoryContract()
|
||||
const badgeHub = useBadgeHubContract()
|
||||
const splits = useSplitsContract()
|
||||
|
||||
useEffect(() => {
|
||||
useContracts.setState({
|
||||
sg721,
|
||||
vendingMinter,
|
||||
baseMinter,
|
||||
openEditionMinter,
|
||||
minter,
|
||||
whitelist,
|
||||
vendingFactory,
|
||||
baseFactory,
|
||||
openEditionFactory,
|
||||
badgeHub,
|
||||
splits,
|
||||
})
|
||||
}, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub, splits])
|
||||
}, [sg721, minter, whitelist])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import create from 'zustand'
|
||||
|
||||
export interface LogItem {
|
||||
id: string
|
||||
message: string
|
||||
type?: string
|
||||
timestamp?: Date
|
||||
code?: number
|
||||
source?: string
|
||||
connectedWallet?: string
|
||||
}
|
||||
|
||||
export const useLogStore = create(() => ({
|
||||
itemList: [] as LogItem[],
|
||||
}))
|
||||
|
||||
export const setLogItemList = (list: LogItem[]) => {
|
||||
useLogStore.setState({ itemList: list })
|
||||
}
|
||||
|
||||
export const addLogItem = (item: LogItem) => {
|
||||
useLogStore.setState((prev) => ({ itemList: [...prev.itemList, item] }))
|
||||
}
|
||||
|
||||
export const removeLogItem = (id: string) => {
|
||||
useLogStore.setState((prev) => ({ itemList: prev.itemList.filter((item) => item.id !== id) }))
|
||||
}
|
||||
+2
-2
@@ -83,7 +83,7 @@ export const useWalletStore = create(
|
||||
init(signer)
|
||||
if (walletChange) set({ initializing: false })
|
||||
} catch (err: any) {
|
||||
toast.error(err?.message, { style: { maxWidth: 'none' } })
|
||||
toast.error(err?.message)
|
||||
set({ initializing: false })
|
||||
}
|
||||
},
|
||||
@@ -113,7 +113,7 @@ export const useWalletStore = create(
|
||||
const client = (await createQueryClient()) as SigningCosmWasmClient
|
||||
set({ client })
|
||||
} catch (err: any) {
|
||||
toast.error(err?.message, { style: { maxWidth: 'none' } })
|
||||
toast.error(err?.message)
|
||||
set({ initializing: false })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,729 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable camelcase */
|
||||
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import { coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
|
||||
import sizeof from 'object-sizeof'
|
||||
|
||||
import { generateSignature } from '../../utils/hash'
|
||||
|
||||
export interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface MigrateResponse {
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
by_key?: string
|
||||
by_minter?: string
|
||||
by_keys?: string
|
||||
}
|
||||
|
||||
export interface Trait {
|
||||
display_type?: string
|
||||
trait_type: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
name?: string
|
||||
image?: string
|
||||
image_data?: string
|
||||
external_url?: string
|
||||
description?: string
|
||||
attributes?: Trait[]
|
||||
background_color?: string
|
||||
animation_url?: string
|
||||
youtube_url?: string
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
manager: string
|
||||
metadata: Metadata
|
||||
transferrable: boolean
|
||||
rule: Rule | string
|
||||
expiry?: number
|
||||
max_supply?: number
|
||||
}
|
||||
|
||||
export interface BadgeHubInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
//Query
|
||||
getConfig: () => Promise<any>
|
||||
getBadge: (id: number) => Promise<any>
|
||||
getBadges: (start_after?: number, limit?: number) => Promise<any>
|
||||
getKey: (id: number, pubkey: string) => Promise<any>
|
||||
getKeys: (id: number, start_after?: string, limit?: number) => Promise<any>
|
||||
|
||||
//Execute
|
||||
createBadge: (senderAddress: string, badge: Badge) => Promise<string>
|
||||
editBadge: (senderAddress: string, id: number, metadata: Metadata, editFee?: number) => Promise<string>
|
||||
addKeys: (senderAddress: string, id: number, keys: string[]) => Promise<string>
|
||||
purgeKeys: (senderAddress: string, id: number, limit?: number) => Promise<string>
|
||||
purgeOwners: (senderAddress: string, id: number, limit?: number) => Promise<string>
|
||||
mintByMinter: (senderAddress: string, id: number, owners: string[]) => Promise<string>
|
||||
mintByKey: (senderAddress: string, id: number, owner: string, signature: string) => Promise<string>
|
||||
airdropByKey: (senderAddress: string, id: number, recipients: string[], privateKey: string) => Promise<string>
|
||||
mintByKeys: (senderAddress: string, id: number, owner: string, pubkey: string, signature: string) => Promise<string>
|
||||
setNft: (senderAddress: string, nft: string) => Promise<string>
|
||||
}
|
||||
|
||||
export interface BadgeHubMessages {
|
||||
createBadge: (badge: Badge) => CreateBadgeMessage
|
||||
editBadge: (id: number, metadata: Metadata, editFee?: number) => EditBadgeMessage
|
||||
addKeys: (id: number, keys: string[]) => AddKeysMessage
|
||||
purgeKeys: (id: number, limit?: number) => PurgeKeysMessage
|
||||
purgeOwners: (id: number, limit?: number) => PurgeOwnersMessage
|
||||
mintByMinter: (id: number, owners: string[]) => MintByMinterMessage
|
||||
mintByKey: (id: number, owner: string, signature: string) => MintByKeyMessage
|
||||
airdropByKey: (id: number, recipients: string[], privateKey: string) => CustomMessage
|
||||
mintByKeys: (id: number, owner: string, pubkey: string, signature: string) => MintByKeysMessage
|
||||
setNft: (nft: string) => SetNftMessage
|
||||
}
|
||||
|
||||
export interface CreateBadgeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
create_badge: {
|
||||
manager: string
|
||||
metadata: Metadata
|
||||
transferrable: boolean
|
||||
rule: Rule | string
|
||||
expiry?: number
|
||||
max_supply?: number
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface EditBadgeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
edit_badge: {
|
||||
id: number
|
||||
metadata: Metadata
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface AddKeysMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
add_keys: {
|
||||
id: number
|
||||
keys: string[]
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface PurgeKeysMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
purge_keys: {
|
||||
id: number
|
||||
limit?: number
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface PurgeOwnersMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
purge_owners: {
|
||||
id: number
|
||||
limit?: number
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintByMinterMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint_by_minter: {
|
||||
id: number
|
||||
owners: string[]
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintByKeyMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint_by_key: {
|
||||
id: number
|
||||
owner: string
|
||||
signature: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface CustomMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>[]
|
||||
funds: Coin[]
|
||||
}
|
||||
export interface MintByKeysMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint_by_keys: {
|
||||
id: number
|
||||
owner: string
|
||||
pubkey: string
|
||||
signature: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface SetNftMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
set_nft: {
|
||||
nft: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface BadgeHubContract {
|
||||
instantiate: (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
funds?: Coin[],
|
||||
) => Promise<InstantiateResponse>
|
||||
|
||||
migrate: (
|
||||
senderAddress: string,
|
||||
contractAddress: string,
|
||||
codeId: number,
|
||||
migrateMsg: Record<string, unknown>,
|
||||
) => Promise<MigrateResponse>
|
||||
|
||||
use: (contractAddress: string) => BadgeHubInstance
|
||||
|
||||
messages: (contractAddress: string) => BadgeHubMessages
|
||||
}
|
||||
|
||||
export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): BadgeHubContract => {
|
||||
const use = (contractAddress: string): BadgeHubInstance => {
|
||||
//Query
|
||||
const getConfig = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
config: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getBadge = async (id: number): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
badge: { id },
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getBadges = async (start_after?: number, limit?: number): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
badges: { start_after, limit },
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getKey = async (id: number, pubkey: string): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
key: { id, pubkey },
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getKeys = async (id: number, start_after?: string, limit?: number): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
keys: { id, start_after, limit },
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
//Execute
|
||||
const createBadge = async (senderAddress: string, badge: Badge): Promise<string> => {
|
||||
const feeRateRaw = await client.queryContractRaw(
|
||||
contractAddress,
|
||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
console.log('Fee Rate Raw: ', feeRateRaw)
|
||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
||||
console.log('Fee Rate:', feeRate)
|
||||
|
||||
console.log('badge size: ', sizeof(badge))
|
||||
console.log('metadata size', sizeof(badge.metadata))
|
||||
console.log('size of attributes ', sizeof(badge.metadata.attributes))
|
||||
|
||||
console.log('Total: ', Number(sizeof(badge)) + Number(sizeof(badge.metadata.attributes)))
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
create_badge: {
|
||||
manager: badge.manager,
|
||||
metadata: badge.metadata,
|
||||
transferrable: badge.transferrable,
|
||||
rule: badge.rule,
|
||||
expiry: badge.expiry,
|
||||
max_supply: badge.max_supply,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
[
|
||||
coin(
|
||||
(Number(sizeof(badge)) + Number(sizeof(badge.metadata.attributes))) * Number(feeRate.metadata),
|
||||
'ustars',
|
||||
),
|
||||
],
|
||||
//[coin(1, 'ustars')],
|
||||
)
|
||||
|
||||
const events = res.logs
|
||||
.map((log) => log.events)
|
||||
.flat()
|
||||
.find(
|
||||
(event) =>
|
||||
event.attributes.findIndex((attr) => attr.key === 'action' && attr.value === 'badges/hub/create_badge') > 0,
|
||||
)!
|
||||
const id = Number(events.attributes.find((attr) => attr.key === 'id')!.value)
|
||||
|
||||
return res.transactionHash.concat(`:${id}`)
|
||||
}
|
||||
|
||||
const editBadge = async (
|
||||
senderAddress: string,
|
||||
id: number,
|
||||
metadata: Metadata,
|
||||
editFee?: number,
|
||||
): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
edit_badge: {
|
||||
id,
|
||||
metadata,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
editFee ? [coin(editFee, 'ustars')] : [],
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const addKeys = async (senderAddress: string, id: number, keys: string[]): Promise<string> => {
|
||||
const feeRateRaw = await client.queryContractRaw(
|
||||
contractAddress,
|
||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
console.log('Fee Rate Raw: ', feeRateRaw)
|
||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
||||
console.log('Fee Rate:', feeRate)
|
||||
|
||||
console.log('keys size: ', sizeof(keys))
|
||||
console.log(keys)
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
add_keys: {
|
||||
id,
|
||||
keys,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
[coin(Math.ceil((Number(sizeof(keys)) * 1.1 * Number(feeRate.key)) / 2), 'ustars')],
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const purgeKeys = async (senderAddress: string, id: number, limit?: number): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
purge_keys: {
|
||||
id,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const purgeOwners = async (senderAddress: string, id: number, limit?: number): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
purge_owners: {
|
||||
id,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const mintByMinter = async (senderAddress: string, id: number, owners: string[]): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
mint_by_minter: {
|
||||
id,
|
||||
owners,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const mintByKey = async (senderAddress: string, id: number, owner: string, signature: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
mint_by_key: {
|
||||
id,
|
||||
owner,
|
||||
signature,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const airdropByKey = async (
|
||||
senderAddress: string,
|
||||
id: number,
|
||||
recipients: string[],
|
||||
privateKey: string,
|
||||
): Promise<string> => {
|
||||
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
|
||||
for (let i = 0; i < recipients.length; i++) {
|
||||
const msg = {
|
||||
mint_by_key: { id, owner: recipients[i], signature: generateSignature(id, recipients[i], privateKey) },
|
||||
}
|
||||
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', 'airdrop_by_key')
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const mintByKeys = async (
|
||||
senderAddress: string,
|
||||
id: number,
|
||||
owner: string,
|
||||
pubkey: string,
|
||||
signature: string,
|
||||
): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
mint_by_keys: {
|
||||
id,
|
||||
owner,
|
||||
pubkey,
|
||||
signature,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const setNft = async (senderAddress: string, nft: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
set_nft: {
|
||||
nft,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
getConfig,
|
||||
getBadge,
|
||||
getBadges,
|
||||
getKey,
|
||||
getKeys,
|
||||
createBadge,
|
||||
editBadge,
|
||||
addKeys,
|
||||
purgeKeys,
|
||||
purgeOwners,
|
||||
mintByMinter,
|
||||
mintByKey,
|
||||
airdropByKey,
|
||||
mintByKeys,
|
||||
setNft,
|
||||
}
|
||||
}
|
||||
|
||||
const migrate = async (
|
||||
senderAddress: string,
|
||||
contractAddress: string,
|
||||
codeId: number,
|
||||
migrateMsg: Record<string, unknown>,
|
||||
): Promise<MigrateResponse> => {
|
||||
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
|
||||
return {
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
const instantiate = async (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
): Promise<InstantiateResponse> => {
|
||||
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto')
|
||||
|
||||
return {
|
||||
contractAddress: result.contractAddress,
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = (contractAddress: string) => {
|
||||
const createBadge = (badge: Badge): CreateBadgeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
create_badge: {
|
||||
manager: badge.manager,
|
||||
metadata: badge.metadata,
|
||||
transferrable: badge.transferrable,
|
||||
rule: badge.rule,
|
||||
expiry: badge.expiry,
|
||||
max_supply: badge.max_supply,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const editBadge = (id: number, metadata: Metadata, editFee?: number): EditBadgeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
edit_badge: {
|
||||
id,
|
||||
metadata,
|
||||
},
|
||||
},
|
||||
funds: editFee ? [coin(editFee, 'ustars')] : [],
|
||||
}
|
||||
}
|
||||
|
||||
const addKeys = (id: number, keys: string[]): AddKeysMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
add_keys: {
|
||||
id,
|
||||
keys,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const purgeKeys = (id: number, limit?: number): PurgeKeysMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
purge_keys: {
|
||||
id,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const purgeOwners = (id: number, limit?: number): PurgeOwnersMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
purge_owners: {
|
||||
id,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const mintByMinter = (id: number, owners: string[]): MintByMinterMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint_by_minter: {
|
||||
id,
|
||||
owners,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const mintByKey = (id: number, owner: string, signature: string): MintByKeyMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint_by_key: {
|
||||
id,
|
||||
owner,
|
||||
signature,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const airdropByKey = (id: number, recipients: string[], privateKey: string): CustomMessage => {
|
||||
const msg: Record<string, unknown>[] = []
|
||||
for (let i = 0; i < recipients.length; i++) {
|
||||
const signature = generateSignature(id, recipients[i], privateKey)
|
||||
msg.push({
|
||||
mint_by_key: { id, owner: recipients[i], signature },
|
||||
})
|
||||
}
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const mintByKeys = (id: number, owner: string, pubkey: string, signature: string): MintByKeysMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint_by_keys: {
|
||||
id,
|
||||
owner,
|
||||
pubkey,
|
||||
signature,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const setNft = (nft: string): SetNftMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
set_nft: {
|
||||
nft,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createBadge,
|
||||
editBadge,
|
||||
addKeys,
|
||||
purgeKeys,
|
||||
purgeOwners,
|
||||
mintByMinter,
|
||||
mintByKey,
|
||||
airdropByKey,
|
||||
mintByKeys,
|
||||
setNft,
|
||||
}
|
||||
}
|
||||
|
||||
return { use, instantiate, migrate, messages }
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
import type { Badge, BadgeHubInstance, Metadata } from '../index'
|
||||
import { useBadgeHubContract } from '../index'
|
||||
|
||||
export type ExecuteType = typeof EXECUTE_TYPES[number]
|
||||
|
||||
export const EXECUTE_TYPES = [
|
||||
'create_badge',
|
||||
'edit_badge',
|
||||
'add_keys',
|
||||
'purge_keys',
|
||||
'purge_owners',
|
||||
'mint_by_minter',
|
||||
'mint_by_key',
|
||||
'mint_by_keys',
|
||||
'set_nft',
|
||||
] as const
|
||||
|
||||
export interface ExecuteListItem {
|
||||
id: ExecuteType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
{
|
||||
id: 'create_badge',
|
||||
name: 'Create Badge',
|
||||
description: `Create a new badge with the specified mint rule and metadata`,
|
||||
},
|
||||
{
|
||||
id: 'edit_badge',
|
||||
name: 'Edit Badge',
|
||||
description: ` Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'add_keys',
|
||||
name: 'Add Keys',
|
||||
description: `Add keys to the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_keys',
|
||||
name: 'Purge Keys',
|
||||
description: `Purge keys from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_minter',
|
||||
name: 'Mint by Minter',
|
||||
description: `Mint a new token by the minter with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_key',
|
||||
name: 'Mint by Key',
|
||||
description: `Mint a new token by the key with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_keys',
|
||||
name: 'Mint by Keys',
|
||||
description: `Mint a new token by the keys with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'set_nft',
|
||||
name: 'Set NFT',
|
||||
description: `Set the Badge NFT contract address for the Badge Hub contract`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
type: ExecuteType
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
type Select<T extends ExecuteType> = T
|
||||
|
||||
/** @see {@link BadgeHubInstance} */
|
||||
export type DispatchExecuteArgs = {
|
||||
contract: string
|
||||
messages?: BadgeHubInstance
|
||||
txSigner: string
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'create_badge'>; badge: Badge }
|
||||
| { type: Select<'edit_badge'>; id: number; metadata: Metadata; editFee?: number }
|
||||
| { type: Select<'add_keys'>; id: number; keys: string[] }
|
||||
| { type: Select<'purge_keys'>; id: number; limit?: number }
|
||||
| { type: Select<'purge_owners'>; id: number; limit?: number }
|
||||
| { type: Select<'mint_by_minter'>; id: number; owners: string[] }
|
||||
| { type: Select<'mint_by_key'>; id: number; owner: string; signature: string }
|
||||
| { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string }
|
||||
| { type: Select<'set_nft'>; nft: string }
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { messages, txSigner } = args
|
||||
if (!messages) {
|
||||
throw new Error('cannot dispatch execute, messages is not defined')
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'create_badge': {
|
||||
return messages.createBadge(txSigner, args.badge)
|
||||
}
|
||||
case 'edit_badge': {
|
||||
return messages.editBadge(txSigner, args.id, args.metadata, args.editFee)
|
||||
}
|
||||
case 'add_keys': {
|
||||
return messages.addKeys(txSigner, args.id, args.keys)
|
||||
}
|
||||
case 'purge_keys': {
|
||||
return messages.purgeKeys(txSigner, args.id, args.limit)
|
||||
}
|
||||
case 'purge_owners': {
|
||||
return messages.purgeOwners(txSigner, args.id, args.limit)
|
||||
}
|
||||
case 'mint_by_minter': {
|
||||
return messages.mintByMinter(txSigner, args.id, args.owners)
|
||||
}
|
||||
case 'mint_by_key': {
|
||||
return messages.mintByKey(txSigner, args.id, args.owner, args.signature)
|
||||
}
|
||||
case 'mint_by_keys': {
|
||||
return messages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature)
|
||||
}
|
||||
case 'set_nft': {
|
||||
return messages.setNft(txSigner, args.nft)
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown execute type')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages } = useBadgeHubContract()
|
||||
const { contract } = args
|
||||
switch (args.type) {
|
||||
case 'create_badge': {
|
||||
return messages(contract)?.createBadge(args.badge)
|
||||
}
|
||||
case 'edit_badge': {
|
||||
return messages(contract)?.editBadge(args.id, args.metadata)
|
||||
}
|
||||
case 'add_keys': {
|
||||
return messages(contract)?.addKeys(args.id, args.keys)
|
||||
}
|
||||
case 'purge_keys': {
|
||||
return messages(contract)?.purgeKeys(args.id, args.limit)
|
||||
}
|
||||
case 'purge_owners': {
|
||||
return messages(contract)?.purgeOwners(args.id, args.limit)
|
||||
}
|
||||
case 'mint_by_minter': {
|
||||
return messages(contract)?.mintByMinter(args.id, args.owners)
|
||||
}
|
||||
case 'mint_by_key': {
|
||||
return messages(contract)?.mintByKey(args.id, args.owner, args.signature)
|
||||
}
|
||||
case 'mint_by_keys': {
|
||||
return messages(contract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature)
|
||||
}
|
||||
case 'set_nft': {
|
||||
return messages(contract)?.setNft(args.nft)
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
|
||||
return arr.some((val) => type === val)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { BadgeHubInstance } from '../contract'
|
||||
|
||||
export type QueryType = typeof QUERY_TYPES[number]
|
||||
|
||||
export const QUERY_TYPES = ['config', 'getBadge', 'getBadges', 'getKey', 'getKeys'] as const
|
||||
|
||||
export interface QueryListItem {
|
||||
id: QueryType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const QUERY_LIST: QueryListItem[] = [
|
||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
||||
{ id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' },
|
||||
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
|
||||
]
|
||||
|
||||
export interface DispatchQueryProps {
|
||||
id: number
|
||||
pubkey: string
|
||||
messages: BadgeHubInstance | undefined
|
||||
type: QueryType
|
||||
startAfterNumber: number
|
||||
startAfterString: string
|
||||
limit: number
|
||||
}
|
||||
|
||||
export const dispatchQuery = (props: DispatchQueryProps) => {
|
||||
const { id, pubkey, messages, type, startAfterNumber, startAfterString, limit } = props
|
||||
switch (type) {
|
||||
case 'config': {
|
||||
return messages?.getConfig()
|
||||
}
|
||||
case 'getBadge': {
|
||||
return messages?.getBadge(id)
|
||||
}
|
||||
case 'getBadges': {
|
||||
return messages?.getBadges(startAfterNumber, limit)
|
||||
}
|
||||
case 'getKey': {
|
||||
return messages?.getKey(id, pubkey)
|
||||
}
|
||||
case 'getKeys': {
|
||||
return messages?.getKeys(id, startAfterString, limit)
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown query type')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import { BASE_FACTORY_ADDRESS } from 'utils/constants'
|
||||
|
||||
import { BASE_FACTORY_UPDATABLE_ADDRESS } from '../../utils/constants'
|
||||
|
||||
export interface CreateBaseMinterResponse {
|
||||
readonly baseMinterAddress: string
|
||||
readonly sg721Address: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface BaseFactoryInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
//Query
|
||||
getParams: () => Promise<any>
|
||||
//Execute
|
||||
createBaseMinter: (
|
||||
senderAddress: string,
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
) => Promise<CreateBaseMinterResponse>
|
||||
}
|
||||
|
||||
export interface BaseFactoryMessages {
|
||||
createBaseMinter: (msg: Record<string, unknown>, funds: Coin[], updatable?: boolean) => CreateBaseMinterMessage
|
||||
}
|
||||
|
||||
export interface CreateBaseMinterMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface BaseFactoryContract {
|
||||
use: (contractAddress: string) => BaseFactoryInstance
|
||||
|
||||
messages: (contractAddress: string) => BaseFactoryMessages
|
||||
}
|
||||
|
||||
export const baseFactory = (client: SigningCosmWasmClient, txSigner: string): BaseFactoryContract => {
|
||||
const use = (contractAddress: string): BaseFactoryInstance => {
|
||||
//Query
|
||||
const getParams = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
params: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
//Execute
|
||||
const createBaseMinter = async (
|
||||
senderAddress: string,
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
): Promise<CreateBaseMinterResponse> => {
|
||||
const result = await client.execute(
|
||||
senderAddress,
|
||||
updatable ? BASE_FACTORY_UPDATABLE_ADDRESS : BASE_FACTORY_ADDRESS,
|
||||
msg,
|
||||
'auto',
|
||||
'',
|
||||
funds,
|
||||
)
|
||||
|
||||
return {
|
||||
baseMinterAddress: result.logs[0].events[5].attributes[0].value,
|
||||
sg721Address: result.logs[0].events[5].attributes[2].value,
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
getParams,
|
||||
createBaseMinter,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = (contractAddress: string) => {
|
||||
const createBaseMinter = (
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
): CreateBaseMinterMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createBaseMinter,
|
||||
}
|
||||
}
|
||||
|
||||
return { use, messages }
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './contract'
|
||||
export * from './useContract'
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
|
||||
import type { BaseFactoryInstance } from '../index'
|
||||
import { useBaseFactoryContract } from '../index'
|
||||
|
||||
/** @see {@link VendingFactoryInstance} */
|
||||
export interface DispatchExecuteArgs {
|
||||
contract: string
|
||||
messages?: BaseFactoryInstance
|
||||
txSigner: string
|
||||
msg: Record<string, unknown>
|
||||
funds: Coin[]
|
||||
updatable?: boolean
|
||||
}
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { messages, txSigner } = args
|
||||
if (!messages) {
|
||||
throw new Error('cannot dispatch execute, messages is not defined')
|
||||
}
|
||||
return messages.createBaseMinter(txSigner, args.msg, args.funds, args.updatable)
|
||||
}
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages } = useBaseFactoryContract()
|
||||
const { contract } = args
|
||||
return messages(contract)?.createBaseMinter(args.msg, args.funds, args.updatable)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { BaseFactoryContract, BaseFactoryInstance, BaseFactoryMessages } from './contract'
|
||||
import { baseFactory as initContract } from './contract'
|
||||
|
||||
export interface UseBaseFactoryContractProps {
|
||||
use: (customAddress: string) => BaseFactoryInstance | undefined
|
||||
updateContractAddress: (contractAddress: string) => void
|
||||
getContractAddress: () => string | undefined
|
||||
messages: (contractAddress: string) => BaseFactoryMessages | undefined
|
||||
}
|
||||
|
||||
export function useBaseFactoryContract(): UseBaseFactoryContractProps {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [address, setAddress] = useState<string>('')
|
||||
const [baseFactory, setBaseFactory] = useState<BaseFactoryContract>()
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(localStorage.getItem('contract_address') || '')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const BaseFactoryBaseContract = initContract(wallet.getClient(), wallet.address)
|
||||
setBaseFactory(BaseFactoryBaseContract)
|
||||
}, [wallet])
|
||||
|
||||
const updateContractAddress = (contractAddress: string) => {
|
||||
setAddress(contractAddress)
|
||||
}
|
||||
|
||||
const use = useCallback(
|
||||
(customAddress = ''): BaseFactoryInstance | undefined => {
|
||||
return baseFactory?.use(address || customAddress)
|
||||
},
|
||||
[baseFactory, address],
|
||||
)
|
||||
|
||||
const getContractAddress = (): string | undefined => {
|
||||
return address
|
||||
}
|
||||
|
||||
const messages = useCallback(
|
||||
(customAddress = ''): BaseFactoryMessages | undefined => {
|
||||
return baseFactory?.messages(address || customAddress)
|
||||
},
|
||||
[baseFactory, address],
|
||||
)
|
||||
|
||||
return {
|
||||
use,
|
||||
updateContractAddress,
|
||||
getContractAddress,
|
||||
messages,
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import { coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types'
|
||||
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface MigrateResponse {
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface RoyaltyInfo {
|
||||
payment_address: string
|
||||
share: string
|
||||
}
|
||||
|
||||
export interface BaseMinterInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
//Query
|
||||
getConfig: () => Promise<any>
|
||||
getStatus: () => Promise<any>
|
||||
|
||||
//Execute
|
||||
mint: (senderAddress: string, tokenUri: string) => Promise<string>
|
||||
updateStartTradingTime: (senderAddress: string, time?: Timestamp) => Promise<string>
|
||||
batchMint: (senderAddress: string, recipient: string, batchCount: number) => Promise<string>
|
||||
}
|
||||
|
||||
export interface BaseMinterMessages {
|
||||
mint: (tokenUri: string) => MintMessage
|
||||
updateStartTradingTime: (time: Timestamp) => UpdateStartTradingTimeMessage
|
||||
batchMint: (recipient: string, batchNumber: number) => CustomMessage
|
||||
}
|
||||
|
||||
export interface MintMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint: {
|
||||
token_uri: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateStartTradingTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_start_trading_time: string
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface CustomMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>[]
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintPriceMessage {
|
||||
public_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
airdrop_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
whitelist_price?: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
current_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface BaseMinterContract {
|
||||
instantiate: (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
funds?: Coin[],
|
||||
) => Promise<InstantiateResponse>
|
||||
|
||||
migrate: (
|
||||
senderAddress: string,
|
||||
contractAddress: string,
|
||||
codeId: number,
|
||||
migrateMsg: Record<string, unknown>,
|
||||
) => Promise<MigrateResponse>
|
||||
|
||||
use: (contractAddress: string) => BaseMinterInstance
|
||||
|
||||
messages: (contractAddress: string) => BaseMinterMessages
|
||||
}
|
||||
|
||||
export const baseMinter = (client: SigningCosmWasmClient, txSigner: string): BaseMinterContract => {
|
||||
const use = (contractAddress: string): BaseMinterInstance => {
|
||||
//Query
|
||||
const getFactoryParameters = async (factoryAddress: string): Promise<any> => {
|
||||
const res = await client.queryContractSmart(factoryAddress, { params: {} })
|
||||
return res
|
||||
}
|
||||
|
||||
const getConfig = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
config: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getStatus = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
status: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
//Execute
|
||||
const mint = async (senderAddress: string, tokenUri: string): Promise<string> => {
|
||||
const txHash = await getConfig().then(async (response) => {
|
||||
const factoryParameters = await toast.promise(getFactoryParameters(response.config?.factory), {
|
||||
loading: 'Querying Factory Parameters...',
|
||||
error: 'Querying Factory Parameters failed!',
|
||||
success: 'Query successful! Minting...',
|
||||
})
|
||||
console.log(factoryParameters.params.mint_fee_bps)
|
||||
|
||||
const price = response.config?.mint_price.amount
|
||||
if (!price) {
|
||||
throw new Error(
|
||||
'Unable to retrieve a valid mint price. It may be that the given contract address does not belong to a Base Minter contract.',
|
||||
)
|
||||
}
|
||||
console.log((Number(price) * Number(factoryParameters.params.mint_fee_bps)) / 100)
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
mint: { token_uri: tokenUri },
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
[coin((Number(price) * Number(factoryParameters.params.mint_fee_bps)) / 100 / 100, 'ustars')],
|
||||
)
|
||||
return res.transactionHash
|
||||
})
|
||||
return txHash
|
||||
}
|
||||
|
||||
const updateStartTradingTime = async (senderAddress: string, time?: Timestamp): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_start_trading_time: time || null,
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const batchMint = async (senderAddress: string, baseUri: string, batchCount: number): Promise<string> => {
|
||||
const txHash = await getConfig().then(async (response) => {
|
||||
const factoryParameters = await toast.promise(getFactoryParameters(response?.config?.factory), {
|
||||
loading: 'Querying Factory Parameters...',
|
||||
error: 'Querying Factory Parameters failed!',
|
||||
success: 'Query successful! Minting...',
|
||||
})
|
||||
console.log(factoryParameters.params.mint_fee_bps)
|
||||
|
||||
const price = response.config?.mint_price.amount
|
||||
if (!price) {
|
||||
throw new Error(
|
||||
'Unable to retrieve a valid mint price. It may be that the given contract address does not belong to a Base Minter contract.',
|
||||
)
|
||||
}
|
||||
console.log((Number(price) * Number(factoryParameters.params.mint_fee_bps)) / 100)
|
||||
|
||||
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
const msg = {
|
||||
mint: { token_uri: `${baseUri}/${i + 1}` },
|
||||
}
|
||||
const executeContractMsg: MsgExecuteContractEncodeObject = {
|
||||
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
|
||||
value: MsgExecuteContract.fromPartial({
|
||||
sender: senderAddress,
|
||||
contract: contractAddress,
|
||||
msg: toUtf8(JSON.stringify(msg)),
|
||||
funds: [coin((Number(price) * Number(factoryParameters.params.mint_fee_bps)) / 100 / 100, 'ustars')],
|
||||
}),
|
||||
}
|
||||
|
||||
executeContractMsgs.push(executeContractMsg)
|
||||
}
|
||||
|
||||
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'batch mint')
|
||||
|
||||
return res.transactionHash
|
||||
})
|
||||
return txHash
|
||||
}
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
getConfig,
|
||||
getStatus,
|
||||
mint,
|
||||
updateStartTradingTime,
|
||||
batchMint,
|
||||
}
|
||||
}
|
||||
|
||||
const migrate = async (
|
||||
senderAddress: string,
|
||||
contractAddress: string,
|
||||
codeId: number,
|
||||
migrateMsg: Record<string, unknown>,
|
||||
): Promise<MigrateResponse> => {
|
||||
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
|
||||
return {
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
const instantiate = async (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
): Promise<InstantiateResponse> => {
|
||||
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
|
||||
funds: [coin('250000000', 'ustars')],
|
||||
})
|
||||
return {
|
||||
contractAddress: result.contractAddress,
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = (contractAddress: string) => {
|
||||
const mint = (tokenUri: string): MintMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint: { token_uri: tokenUri },
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateStartTradingTime = (startTime: string): UpdateStartTradingTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_start_trading_time: startTime,
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const batchMint = (baseUri: string, batchCount: number): CustomMessage => {
|
||||
const msg: Record<string, unknown>[] = []
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
msg.push({ mint: { token_uri: `${baseUri}/${i + 1}` } })
|
||||
}
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mint,
|
||||
updateStartTradingTime,
|
||||
batchMint,
|
||||
}
|
||||
}
|
||||
|
||||
return { use, instantiate, migrate, messages }
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './contract'
|
||||
export * from './useContract'
|
||||
@@ -1,82 +0,0 @@
|
||||
import type { BaseMinterInstance } from '../index'
|
||||
import { useBaseMinterContract } from '../index'
|
||||
|
||||
export type ExecuteType = typeof EXECUTE_TYPES[number]
|
||||
|
||||
export const EXECUTE_TYPES = ['mint', 'update_start_trading_time'] as const
|
||||
|
||||
export interface ExecuteListItem {
|
||||
id: ExecuteType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
{
|
||||
id: 'mint',
|
||||
name: 'Mint',
|
||||
description: `Mint a token with the given token URI`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_trading_time',
|
||||
name: 'Update Start Trading Time',
|
||||
description: `Update start trading time for minting`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
type: ExecuteType
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
type Select<T extends ExecuteType> = T
|
||||
|
||||
/** @see {@link BaseMinterInstance} */
|
||||
export type DispatchExecuteArgs = {
|
||||
contract: string
|
||||
messages?: BaseMinterInstance
|
||||
txSigner: string
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'mint'>; tokenUri: string }
|
||||
| { type: Select<'update_start_trading_time'>; startTime?: string }
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { messages, txSigner } = args
|
||||
if (!messages) {
|
||||
throw new Error('cannot dispatch execute, messages is not defined')
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return messages.mint(txSigner, args.tokenUri)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return messages.updateStartTradingTime(txSigner, args.startTime)
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown execute type')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages } = useBaseMinterContract()
|
||||
const { contract } = args
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return messages(contract)?.mint(args.tokenUri)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return messages(contract)?.updateStartTradingTime(args.startTime as string)
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
|
||||
return arr.some((val) => type === val)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { BaseMinterInstance } from '../contract'
|
||||
|
||||
export type QueryType = typeof QUERY_TYPES[number]
|
||||
|
||||
export const QUERY_TYPES = ['config', 'status'] as const
|
||||
|
||||
export interface QueryListItem {
|
||||
id: QueryType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const QUERY_LIST: QueryListItem[] = [
|
||||
{ id: 'config', name: 'Config', description: 'Query current contract config' },
|
||||
{ id: 'status', name: 'Status', description: 'Query current contract status' },
|
||||
]
|
||||
|
||||
export interface DispatchQueryProps {
|
||||
address: string
|
||||
messages: BaseMinterInstance | undefined
|
||||
type: QueryType
|
||||
}
|
||||
|
||||
export const dispatchQuery = (props: DispatchQueryProps) => {
|
||||
const { address, messages, type } = props
|
||||
switch (type) {
|
||||
case 'config': {
|
||||
return messages?.getConfig()
|
||||
}
|
||||
case 'status': {
|
||||
return messages?.getStatus()
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown query type')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { BaseMinterContract, BaseMinterInstance, BaseMinterMessages, MigrateResponse } from './contract'
|
||||
import { baseMinter as initContract } from './contract'
|
||||
|
||||
interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface UseBaseMinterContractProps {
|
||||
instantiate: (
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
funds?: Coin[],
|
||||
) => Promise<InstantiateResponse>
|
||||
migrate: (contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>) => Promise<MigrateResponse>
|
||||
use: (customAddress: string) => BaseMinterInstance | undefined
|
||||
updateContractAddress: (contractAddress: string) => void
|
||||
getContractAddress: () => string | undefined
|
||||
messages: (contractAddress: string) => BaseMinterMessages | undefined
|
||||
}
|
||||
|
||||
export function useBaseMinterContract(): UseBaseMinterContractProps {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [address, setAddress] = useState<string>('')
|
||||
const [baseMinter, setBaseMinter] = useState<BaseMinterContract>()
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(localStorage.getItem('contract_address') || '')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const BaseMinterBaseContract = initContract(wallet.getClient(), wallet.address)
|
||||
setBaseMinter(BaseMinterBaseContract)
|
||||
}, [wallet])
|
||||
|
||||
const updateContractAddress = (contractAddress: string) => {
|
||||
setAddress(contractAddress)
|
||||
}
|
||||
|
||||
const instantiate = useCallback(
|
||||
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!baseMinter) {
|
||||
reject(new Error('Contract is not initialized.'))
|
||||
return
|
||||
}
|
||||
baseMinter.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
|
||||
})
|
||||
},
|
||||
[baseMinter, wallet],
|
||||
)
|
||||
|
||||
const migrate = useCallback(
|
||||
(contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>): Promise<MigrateResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!baseMinter) {
|
||||
reject(new Error('Contract is not initialized.'))
|
||||
return
|
||||
}
|
||||
console.log(wallet.address, contractAddress, codeId)
|
||||
baseMinter.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
|
||||
})
|
||||
},
|
||||
[baseMinter, wallet],
|
||||
)
|
||||
|
||||
const use = useCallback(
|
||||
(customAddress = ''): BaseMinterInstance | undefined => {
|
||||
return baseMinter?.use(address || customAddress)
|
||||
},
|
||||
[baseMinter, address],
|
||||
)
|
||||
|
||||
const getContractAddress = (): string | undefined => {
|
||||
return address
|
||||
}
|
||||
|
||||
const messages = useCallback(
|
||||
(customAddress = ''): BaseMinterMessages | undefined => {
|
||||
return baseMinter?.messages(address || customAddress)
|
||||
},
|
||||
[baseMinter, address],
|
||||
)
|
||||
|
||||
return {
|
||||
instantiate,
|
||||
use,
|
||||
updateContractAddress,
|
||||
getContractAddress,
|
||||
messages,
|
||||
migrate,
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types'
|
||||
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
|
||||
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
|
||||
|
||||
export interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
@@ -18,12 +17,12 @@ export interface MigrateResponse {
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface RoyaltyInfo {
|
||||
export interface RoyalityInfo {
|
||||
payment_address: string
|
||||
share: string
|
||||
}
|
||||
|
||||
export interface VendingMinterInstance {
|
||||
export interface MinterInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
//Query
|
||||
@@ -34,83 +33,40 @@ export interface VendingMinterInstance {
|
||||
getMintCount: (address: string) => Promise<any>
|
||||
|
||||
//Execute
|
||||
mint: (senderAddress: string) => Promise<string>
|
||||
purge: (senderAddress: string) => Promise<string>
|
||||
updateMintPrice: (senderAddress: string, price: string) => Promise<string>
|
||||
mint: (senderAddress: string, price: string) => Promise<string>
|
||||
setWhitelist: (senderAddress: string, whitelist: string) => Promise<string>
|
||||
updateStartTime: (senderAddress: string, time: Timestamp) => Promise<string>
|
||||
updateStartTradingTime: (senderAddress: string, time?: Timestamp) => Promise<string>
|
||||
updatePerAddressLimit: (senderAddress: string, perAddressLimit: number) => Promise<string>
|
||||
mintTo: (senderAddress: string, recipient: string) => Promise<string>
|
||||
mintFor: (senderAddress: string, recipient: string, tokenId: number) => Promise<string>
|
||||
batchMintFor: (senderAddress: string, recipient: string, tokenIds: string) => Promise<string>
|
||||
batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise<string>
|
||||
shuffle: (senderAddress: string) => Promise<string>
|
||||
withdraw: (senderAddress: string) => Promise<string>
|
||||
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
|
||||
airdropSpecificTokens: (senderAddress: string, tokenRecipients: AirdropAllocation[]) => Promise<string>
|
||||
burnRemaining: (senderAddress: string) => Promise<string>
|
||||
updateDiscountPrice: (senderAddress: string, price: string) => Promise<string>
|
||||
removeDiscountPrice: (senderAddress: string) => Promise<string>
|
||||
}
|
||||
|
||||
export interface VendingMinterMessages {
|
||||
mint: () => MintMessage
|
||||
purge: () => PurgeMessage
|
||||
updateMintPrice: (price: string) => UpdateMintPriceMessage
|
||||
export interface MinterMessages {
|
||||
mint: (price: string) => MintMessage
|
||||
setWhitelist: (whitelist: string) => SetWhitelistMessage
|
||||
updateStartTime: (time: Timestamp) => UpdateStartTimeMessage
|
||||
updateStartTradingTime: (time: Timestamp) => UpdateStartTradingTimeMessage
|
||||
updateStartTime: (time: Timestamp) => UpdateStarTimeMessage
|
||||
updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage
|
||||
mintTo: (recipient: string) => MintToMessage
|
||||
mintFor: (recipient: string, tokenId: number) => MintForMessage
|
||||
batchMintFor: (recipient: string, tokenIds: string) => BatchMintForMessage
|
||||
batchMint: (recipient: string, batchNumber: number) => CustomMessage
|
||||
shuffle: () => ShuffleMessage
|
||||
withdraw: () => WithdrawMessage
|
||||
airdrop: (recipients: string[]) => CustomMessage
|
||||
airdropSpecificTokens: (recipients: AirdropAllocation[]) => CustomMessage
|
||||
burnRemaining: () => BurnRemainingMessage
|
||||
updateDiscountPrice: (price: string) => UpdateDiscountPriceMessage
|
||||
removeDiscountPrice: () => RemoveDiscountPriceMessage
|
||||
}
|
||||
|
||||
export interface MintMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface PurgeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
purge: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateMintPriceMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_mint_price: {
|
||||
price: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateDiscountPriceMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_discount_price: {
|
||||
price: string
|
||||
}
|
||||
update_discount_price: Record<string, unknown>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
@@ -124,6 +80,15 @@ export interface RemoveDiscountPriceMessage {
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface SetWhitelistMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
@@ -135,7 +100,7 @@ export interface SetWhitelistMessage {
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateStartTimeMessage {
|
||||
export interface UpdateStarTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
@@ -144,15 +109,6 @@ export interface UpdateStartTimeMessage {
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateStartTradingTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_start_trading_time: string
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdatePerAddressLimitMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
@@ -186,12 +142,6 @@ export interface MintForMessage {
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
export interface BatchMintForMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>[]
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface CustomMessage {
|
||||
sender: string
|
||||
@@ -218,35 +168,7 @@ export interface WithdrawMessage {
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface BurnRemainingMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
burn_remaining: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintPriceMessage {
|
||||
public_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
airdrop_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
whitelist_price?: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
current_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface VendingMinterContract {
|
||||
export interface MinterContract {
|
||||
instantiate: (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
@@ -263,13 +185,13 @@ export interface VendingMinterContract {
|
||||
migrateMsg: Record<string, unknown>,
|
||||
) => Promise<MigrateResponse>
|
||||
|
||||
use: (contractAddress: string) => VendingMinterInstance
|
||||
use: (contractAddress: string) => MinterInstance
|
||||
|
||||
messages: (contractAddress: string) => VendingMinterMessages
|
||||
messages: (contractAddress: string) => MinterMessages
|
||||
}
|
||||
|
||||
export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string): VendingMinterContract => {
|
||||
const use = (contractAddress: string): VendingMinterInstance => {
|
||||
export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterContract => {
|
||||
const use = (contractAddress: string): MinterInstance => {
|
||||
//Query
|
||||
const getConfig = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
@@ -292,7 +214,7 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
return res
|
||||
}
|
||||
|
||||
const getMintPrice = async (): Promise<MintPriceMessage> => {
|
||||
const getMintPrice = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
mint_price: {},
|
||||
})
|
||||
@@ -307,8 +229,7 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
}
|
||||
|
||||
//Execute
|
||||
const mint = async (senderAddress: string): Promise<string> => {
|
||||
const price = (await getMintPrice()).public_price.amount
|
||||
const mint = async (senderAddress: string, price: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
@@ -323,66 +244,6 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const purge = async (senderAddress: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
purge: {},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateMintPrice = async (senderAddress: string, price: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_mint_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateDiscountPrice = async (senderAddress: string, price: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_discount_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const removeDiscountPrice = async (senderAddress: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
remove_discount_price: {},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const setWhitelist = async (senderAddress: string, whitelist: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
@@ -402,21 +263,7 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_start_time: time,
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateStartTradingTime = async (senderAddress: string, time?: Timestamp): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_start_trading_time: time || null,
|
||||
update_start_time: { time },
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
@@ -467,49 +314,6 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const batchMintFor = async (senderAddress: string, recipient: string, tokenIds: string): Promise<string> => {
|
||||
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
|
||||
if (tokenIds.includes(':')) {
|
||||
const [start, end] = tokenIds.split(':').map(Number)
|
||||
for (let i = start; i <= end; i++) {
|
||||
const msg = {
|
||||
mint_for: { token_id: i, recipient },
|
||||
}
|
||||
const executeContractMsg: MsgExecuteContractEncodeObject = {
|
||||
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
|
||||
value: MsgExecuteContract.fromPartial({
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: toUtf8(JSON.stringify(msg)),
|
||||
}),
|
||||
}
|
||||
|
||||
executeContractMsgs.push(executeContractMsg)
|
||||
}
|
||||
} else {
|
||||
const tokenNumbers = tokenIds.split(',').map(Number)
|
||||
for (let i = 0; i < tokenNumbers.length; i++) {
|
||||
const msg = {
|
||||
mint_for: { token_id: tokenNumbers[i], recipient },
|
||||
}
|
||||
const executeContractMsg: MsgExecuteContractEncodeObject = {
|
||||
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
|
||||
value: MsgExecuteContract.fromPartial({
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: toUtf8(JSON.stringify(msg)),
|
||||
}),
|
||||
}
|
||||
|
||||
executeContractMsgs.push(executeContractMsg)
|
||||
}
|
||||
}
|
||||
|
||||
const res = await client.signAndBroadcast(txSigner, executeContractMsgs, 'auto', 'batch mint for')
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const batchMint = async (senderAddress: string, recipient: string, batchNumber: number): Promise<string> => {
|
||||
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
|
||||
for (let i = 0; i < batchNumber; i++) {
|
||||
@@ -556,29 +360,6 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const airdropSpecificTokens = async (senderAddress: string, recipients: AirdropAllocation[]): Promise<string> => {
|
||||
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
|
||||
for (let i = 0; i < recipients.length; i++) {
|
||||
const msg = {
|
||||
mint_for: { recipient: recipients[i].address, token_id: Number(recipients[i].tokenId) },
|
||||
}
|
||||
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', 'airdrop_specific_tokens')
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const shuffle = async (senderAddress: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
@@ -588,7 +369,6 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
[coin(500000000, 'ustars')],
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
@@ -608,12 +388,14 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const burnRemaining = async (senderAddress: string): Promise<string> => {
|
||||
const updateDiscountPrice = async (senderAddress: string, price: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
burn_remaining: {},
|
||||
update_discount_price: {
|
||||
price,
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
@@ -622,6 +404,19 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const removeDiscountPrice = async (senderAddress: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
remove_discount_price: {},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
return {
|
||||
contractAddress,
|
||||
getConfig,
|
||||
@@ -630,23 +425,17 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
getMintPrice,
|
||||
getMintCount,
|
||||
mint,
|
||||
purge,
|
||||
updateMintPrice,
|
||||
updateDiscountPrice,
|
||||
removeDiscountPrice,
|
||||
setWhitelist,
|
||||
updateStartTime,
|
||||
updateStartTradingTime,
|
||||
updatePerAddressLimit,
|
||||
mintTo,
|
||||
mintFor,
|
||||
batchMintFor,
|
||||
batchMint,
|
||||
airdrop,
|
||||
airdropSpecificTokens,
|
||||
shuffle,
|
||||
withdraw,
|
||||
burnRemaining,
|
||||
updateDiscountPrice,
|
||||
removeDiscountPrice,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,7 +459,7 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
label: string,
|
||||
): Promise<InstantiateResponse> => {
|
||||
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
|
||||
funds: [coin('3000000000', 'ustars')],
|
||||
funds: [coin('1000000000', 'ustars')],
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -681,62 +470,14 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
}
|
||||
|
||||
const messages = (contractAddress: string) => {
|
||||
const mint = (): MintMessage => {
|
||||
const mint = (price: string): MintMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint: {},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const purge = (): PurgeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
purge: {},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateMintPrice = (price: string): UpdateMintPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_mint_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateDiscountPrice = (price: string): UpdateDiscountPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_discount_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const removeDiscountPrice = (): RemoveDiscountPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
remove_discount_price: {},
|
||||
},
|
||||
funds: [],
|
||||
funds: [coin(price, 'ustars')],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,7 +494,7 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
}
|
||||
}
|
||||
|
||||
const updateStartTime = (startTime: string): UpdateStartTimeMessage => {
|
||||
const updateStartTime = (startTime: string): UpdateStarTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
@@ -764,17 +505,6 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
}
|
||||
}
|
||||
|
||||
const updateStartTradingTime = (startTime: string): UpdateStartTradingTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_start_trading_time: startTime,
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updatePerAddressLimit = (limit: number): UpdatePerAddressLimitMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
@@ -815,30 +545,6 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
}
|
||||
}
|
||||
|
||||
const batchMintFor = (recipient: string, tokenIds: string): BatchMintForMessage => {
|
||||
const msg: Record<string, unknown>[] = []
|
||||
if (tokenIds.includes(':')) {
|
||||
const [start, end] = tokenIds.split(':').map(Number)
|
||||
for (let i = start; i <= end; i++) {
|
||||
msg.push({
|
||||
mint_for: { token_id: i.toString(), recipient },
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const tokenNumbers = tokenIds.split(',').map(Number)
|
||||
for (let i = 0; i < tokenNumbers.length; i++) {
|
||||
msg.push({ mint_for: { token_id: tokenNumbers[i].toString(), recipient } })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const batchMint = (recipient: string, batchNumber: number): CustomMessage => {
|
||||
const msg: Record<string, unknown>[] = []
|
||||
for (let i = 0; i < batchNumber; i++) {
|
||||
@@ -865,19 +571,6 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
}
|
||||
}
|
||||
|
||||
const airdropSpecificTokens = (recipients: AirdropAllocation[]): CustomMessage => {
|
||||
const msg: Record<string, unknown>[] = []
|
||||
for (let i = 0; i < recipients.length; i++) {
|
||||
msg.push({ mint_for: { recipient: recipients[i].address, token_id: recipients[i].tokenId } })
|
||||
}
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const shuffle = (): ShuffleMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
@@ -899,13 +592,24 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const burnRemaining = (): BurnRemainingMessage => {
|
||||
const updateDiscountPrice = (price: string): UpdateDiscountPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
burn_remaining: {},
|
||||
update_discount_price: {
|
||||
price,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
const removeDiscountPrice = (): RemoveDiscountPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
remove_discount_price: {},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
@@ -913,25 +617,19 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
|
||||
return {
|
||||
mint,
|
||||
purge,
|
||||
updateMintPrice,
|
||||
updateDiscountPrice,
|
||||
removeDiscountPrice,
|
||||
setWhitelist,
|
||||
updateStartTime,
|
||||
updateStartTradingTime,
|
||||
updatePerAddressLimit,
|
||||
mintTo,
|
||||
mintFor,
|
||||
batchMintFor,
|
||||
batchMint,
|
||||
airdrop,
|
||||
airdropSpecificTokens,
|
||||
shuffle,
|
||||
withdraw,
|
||||
burnRemaining,
|
||||
updateDiscountPrice,
|
||||
removeDiscountPrice,
|
||||
}
|
||||
}
|
||||
|
||||
return { use, instantiate, migrate, messages }
|
||||
return { use, instantiate, messages, migrate }
|
||||
}
|
||||
+39
-83
@@ -1,22 +1,19 @@
|
||||
import type { VendingMinterInstance } from '../index'
|
||||
import { useVendingMinterContract } from '../index'
|
||||
import type { MinterInstance } from '../index'
|
||||
import { useMinterContract } from '../index'
|
||||
|
||||
export type ExecuteType = typeof EXECUTE_TYPES[number]
|
||||
|
||||
export const EXECUTE_TYPES = [
|
||||
'mint',
|
||||
'purge',
|
||||
'update_mint_price',
|
||||
'update_discount_price',
|
||||
'remove_discount_price',
|
||||
'set_whitelist',
|
||||
'update_start_time',
|
||||
'update_start_trading_time',
|
||||
'update_per_address_limit',
|
||||
'mint_to',
|
||||
'mint_for',
|
||||
'shuffle',
|
||||
'burn_remaining',
|
||||
'withdraw',
|
||||
'update_discount_price',
|
||||
'remove_discount_price',
|
||||
] as const
|
||||
|
||||
export interface ExecuteListItem {
|
||||
@@ -29,27 +26,7 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
{
|
||||
id: 'mint',
|
||||
name: 'Mint',
|
||||
description: `Mint a new token`,
|
||||
},
|
||||
{
|
||||
id: 'purge',
|
||||
name: 'Purge',
|
||||
description: `Purge`,
|
||||
},
|
||||
{
|
||||
id: 'update_mint_price',
|
||||
name: 'Update Mint Price',
|
||||
description: `Update mint price`,
|
||||
},
|
||||
{
|
||||
id: 'update_discount_price',
|
||||
name: 'Update Discount Price',
|
||||
description: `Update discount price`,
|
||||
},
|
||||
{
|
||||
id: 'remove_discount_price',
|
||||
name: 'Remove Discount Price',
|
||||
description: `Remove discount price`,
|
||||
description: `Mint new tokens for a given address`,
|
||||
},
|
||||
{
|
||||
id: 'set_whitelist',
|
||||
@@ -61,11 +38,6 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
name: 'Update Start Time',
|
||||
description: `Update start time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_trading_time',
|
||||
name: 'Update Start Trading Time',
|
||||
description: `Update start trading time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_per_address_limit',
|
||||
name: 'Update Per Address Limit',
|
||||
@@ -87,9 +59,14 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
description: `Shuffle the token IDs`,
|
||||
},
|
||||
{
|
||||
id: 'burn_remaining',
|
||||
name: 'Burn Remaining',
|
||||
description: `Burn remaining tokens`,
|
||||
id: 'update_discount_price',
|
||||
name: 'Update Discount Price',
|
||||
description: `Updates discount price`,
|
||||
},
|
||||
{
|
||||
id: 'remove_discount_price',
|
||||
name: 'Remove Discount Price',
|
||||
description: `Removes discount price`,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -100,26 +77,23 @@ export interface DispatchExecuteProps {
|
||||
|
||||
type Select<T extends ExecuteType> = T
|
||||
|
||||
/** @see {@link VendingMinterInstance} */
|
||||
/** @see {@link MinterInstance} */
|
||||
export type DispatchExecuteArgs = {
|
||||
contract: string
|
||||
messages?: VendingMinterInstance
|
||||
messages?: MinterInstance
|
||||
txSigner: string
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'mint'> }
|
||||
| { type: Select<'purge'> }
|
||||
| { type: Select<'update_mint_price'>; price: string }
|
||||
| { type: Select<'update_discount_price'>; price: string }
|
||||
| { type: Select<'remove_discount_price'> }
|
||||
| { type: Select<'mint'>; price: string }
|
||||
| { type: Select<'set_whitelist'>; whitelist: string }
|
||||
| { type: Select<'update_start_time'>; startTime: string }
|
||||
| { type: Select<'update_start_trading_time'>; startTime?: string }
|
||||
| { type: Select<'update_per_address_limit'>; limit: number }
|
||||
| { type: Select<'mint_to'>; recipient: string }
|
||||
| { type: Select<'mint_for'>; recipient: string; tokenId: number }
|
||||
| { type: Select<'shuffle'> }
|
||||
| { type: Select<'burn_remaining'> }
|
||||
| { type: Select<'withdraw'> }
|
||||
| { type: Select<'update_discount_price'>; price: string }
|
||||
| { type: Select<'remove_discount_price'> }
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
@@ -129,19 +103,7 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return messages.mint(txSigner)
|
||||
}
|
||||
case 'purge': {
|
||||
return messages.purge(txSigner)
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return messages.updateMintPrice(txSigner, args.price)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return messages.updateDiscountPrice(txSigner, args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return messages.removeDiscountPrice(txSigner)
|
||||
return messages.mint(txSigner, args.price === '' ? '0' : args.price)
|
||||
}
|
||||
case 'set_whitelist': {
|
||||
return messages.setWhitelist(txSigner, args.whitelist)
|
||||
@@ -149,9 +111,6 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
case 'update_start_time': {
|
||||
return messages.updateStartTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return messages.updateStartTradingTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return messages.updatePerAddressLimit(txSigner, args.limit)
|
||||
}
|
||||
@@ -164,8 +123,14 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
case 'shuffle': {
|
||||
return messages.shuffle(txSigner)
|
||||
}
|
||||
case 'burn_remaining': {
|
||||
return messages.burnRemaining(txSigner)
|
||||
case 'withdraw': {
|
||||
return messages.withdraw(txSigner)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return messages.updateDiscountPrice(txSigner, args.price === '' ? '0' : args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return messages.removeDiscountPrice(txSigner)
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown execute type')
|
||||
@@ -175,23 +140,11 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages } = useVendingMinterContract()
|
||||
const { messages } = useMinterContract()
|
||||
const { contract } = args
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return messages(contract)?.mint()
|
||||
}
|
||||
case 'purge': {
|
||||
return messages(contract)?.purge()
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return messages(contract)?.updateMintPrice(args.price)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return messages(contract)?.updateDiscountPrice(args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return messages(contract)?.removeDiscountPrice()
|
||||
return messages(contract)?.mint(args.price === '' ? '0' : args.price)
|
||||
}
|
||||
case 'set_whitelist': {
|
||||
return messages(contract)?.setWhitelist(args.whitelist)
|
||||
@@ -199,9 +152,6 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
case 'update_start_time': {
|
||||
return messages(contract)?.updateStartTime(args.startTime)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return messages(contract)?.updateStartTradingTime(args.startTime as string)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return messages(contract)?.updatePerAddressLimit(args.limit)
|
||||
}
|
||||
@@ -214,8 +164,14 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
case 'shuffle': {
|
||||
return messages(contract)?.shuffle()
|
||||
}
|
||||
case 'burn_remaining': {
|
||||
return messages(contract)?.burnRemaining()
|
||||
case 'withdraw': {
|
||||
return messages(contract)?.withdraw()
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return messages(contract)?.updateDiscountPrice(args.price === '' ? '0' : args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return messages(contract)?.removeDiscountPrice()
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { VendingMinterInstance } from '../contract'
|
||||
import type { MinterInstance } from '../contract'
|
||||
|
||||
export type QueryType = typeof QUERY_TYPES[number]
|
||||
|
||||
@@ -24,7 +24,7 @@ export const QUERY_LIST: QueryListItem[] = [
|
||||
|
||||
export interface DispatchQueryProps {
|
||||
address: string
|
||||
messages: VendingMinterInstance | undefined
|
||||
messages: MinterInstance | undefined
|
||||
type: QueryType
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { logs } from '@cosmjs/stargate'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { BadgeHubContract, BadgeHubInstance, BadgeHubMessages, MigrateResponse } from './contract'
|
||||
import { badgeHub as initContract } from './contract'
|
||||
import type { MigrateResponse, MinterContract, MinterInstance, MinterMessages } from './contract'
|
||||
import { minter as initContract } from './contract'
|
||||
|
||||
/*export interface InstantiateResponse {
|
||||
/** The address of the newly instantiated contract *-/
|
||||
@@ -24,7 +24,7 @@ interface InstantiateResponse {
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface UseBadgeHubContractProps {
|
||||
export interface UseMinterContractProps {
|
||||
instantiate: (
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
@@ -33,25 +33,25 @@ export interface UseBadgeHubContractProps {
|
||||
funds?: Coin[],
|
||||
) => Promise<InstantiateResponse>
|
||||
migrate: (contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>) => Promise<MigrateResponse>
|
||||
use: (customAddress: string) => BadgeHubInstance | undefined
|
||||
use: (customAddress: string) => MinterInstance | undefined
|
||||
updateContractAddress: (contractAddress: string) => void
|
||||
getContractAddress: () => string | undefined
|
||||
messages: (contractAddress: string) => BadgeHubMessages | undefined
|
||||
messages: (contractAddress: string) => MinterMessages | undefined
|
||||
}
|
||||
|
||||
export function useBadgeHubContract(): UseBadgeHubContractProps {
|
||||
export function useMinterContract(): UseMinterContractProps {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [address, setAddress] = useState<string>('')
|
||||
const [badgeHub, setBadgeHub] = useState<BadgeHubContract>()
|
||||
const [minter, setMinter] = useState<MinterContract>()
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(localStorage.getItem('contract_address') || '')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const BadgeHubBaseContract = initContract(wallet.getClient(), wallet.address)
|
||||
setBadgeHub(BadgeHubBaseContract)
|
||||
const MinterBaseContract = initContract(wallet.getClient(), wallet.address)
|
||||
setMinter(MinterBaseContract)
|
||||
}, [wallet])
|
||||
|
||||
const updateContractAddress = (contractAddress: string) => {
|
||||
@@ -61,35 +61,35 @@ export function useBadgeHubContract(): UseBadgeHubContractProps {
|
||||
const instantiate = useCallback(
|
||||
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!badgeHub) {
|
||||
if (!minter) {
|
||||
reject(new Error('Contract is not initialized.'))
|
||||
return
|
||||
}
|
||||
badgeHub.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
|
||||
minter.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
|
||||
})
|
||||
},
|
||||
[badgeHub, wallet],
|
||||
[minter, wallet],
|
||||
)
|
||||
|
||||
const migrate = useCallback(
|
||||
(contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>): Promise<MigrateResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!badgeHub) {
|
||||
if (!minter) {
|
||||
reject(new Error('Contract is not initialized.'))
|
||||
return
|
||||
}
|
||||
console.log(wallet.address, contractAddress, codeId)
|
||||
badgeHub.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
|
||||
minter.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
|
||||
})
|
||||
},
|
||||
[badgeHub, wallet],
|
||||
[minter, wallet],
|
||||
)
|
||||
|
||||
const use = useCallback(
|
||||
(customAddress = ''): BadgeHubInstance | undefined => {
|
||||
return badgeHub?.use(address || customAddress)
|
||||
(customAddress = ''): MinterInstance | undefined => {
|
||||
return minter?.use(address || customAddress)
|
||||
},
|
||||
[badgeHub, address],
|
||||
[minter, address],
|
||||
)
|
||||
|
||||
const getContractAddress = (): string | undefined => {
|
||||
@@ -97,10 +97,10 @@ export function useBadgeHubContract(): UseBadgeHubContractProps {
|
||||
}
|
||||
|
||||
const messages = useCallback(
|
||||
(customAddress = ''): BadgeHubMessages | undefined => {
|
||||
return badgeHub?.messages(address || customAddress)
|
||||
(customAddress = ''): MinterMessages | undefined => {
|
||||
return minter?.messages(address || customAddress)
|
||||
},
|
||||
[badgeHub, address],
|
||||
[minter, address],
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -1,97 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
|
||||
export interface CreateOpenEditionMinterResponse {
|
||||
readonly openEditionMinterAddress: string
|
||||
readonly sg721Address: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface OpenEditionFactoryInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
//Query
|
||||
|
||||
//Execute
|
||||
createOpenEditionMinter: (
|
||||
senderAddress: string,
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
selectedFactoryAddress?: string,
|
||||
) => Promise<CreateOpenEditionMinterResponse>
|
||||
}
|
||||
|
||||
export interface OpenEditionFactoryMessages {
|
||||
createOpenEditionMinter: (
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
) => CreateOpenEditionMinterMessage
|
||||
}
|
||||
|
||||
export interface CreateOpenEditionMinterMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface OpenEditionFactoryContract {
|
||||
use: (contractAddress: string) => OpenEditionFactoryInstance
|
||||
|
||||
messages: (contractAddress: string) => OpenEditionFactoryMessages
|
||||
}
|
||||
|
||||
export const openEditionFactory = (client: SigningCosmWasmClient, txSigner: string): OpenEditionFactoryContract => {
|
||||
const use = (contractAddress: string): OpenEditionFactoryInstance => {
|
||||
//Query
|
||||
|
||||
//Execute
|
||||
const createOpenEditionMinter = async (
|
||||
senderAddress: string,
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
): Promise<CreateOpenEditionMinterResponse> => {
|
||||
console.log('Contract Address: ', contractAddress)
|
||||
const result = await client.execute(senderAddress, contractAddress, msg, 'auto', '', funds)
|
||||
|
||||
return {
|
||||
openEditionMinterAddress: result.logs[0].events[5].attributes[0].value,
|
||||
sg721Address: result.logs[0].events[5].attributes[2].value,
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
createOpenEditionMinter,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = (contractAddress: string) => {
|
||||
const createOpenEditionMinter = (
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
): CreateOpenEditionMinterMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createOpenEditionMinter,
|
||||
}
|
||||
}
|
||||
|
||||
return { use, messages }
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './contract'
|
||||
export * from './useContract'
|
||||
@@ -1,31 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
|
||||
import type { OpenEditionFactoryInstance } from '../index'
|
||||
import { useOpenEditionFactoryContract } from '../index'
|
||||
|
||||
/** @see {@link OpenEditionFactoryInstance} */
|
||||
export interface DispatchExecuteArgs {
|
||||
contract: string
|
||||
messages?: OpenEditionFactoryInstance
|
||||
txSigner: string
|
||||
msg: Record<string, unknown>
|
||||
funds: Coin[]
|
||||
updatable?: boolean
|
||||
}
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { messages, txSigner } = args
|
||||
if (!messages) {
|
||||
throw new Error('cannot dispatch execute, messages is not defined')
|
||||
}
|
||||
return messages.createOpenEditionMinter(txSigner, args.msg, args.funds, args.updatable)
|
||||
}
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages } = useOpenEditionFactoryContract()
|
||||
const { contract } = args
|
||||
return messages(contract)?.createOpenEditionMinter(args.msg, args.funds, args.updatable)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { OpenEditionFactoryContract, OpenEditionFactoryInstance, OpenEditionFactoryMessages } from './contract'
|
||||
import { openEditionFactory as initContract } from './contract'
|
||||
|
||||
/*export interface InstantiateResponse {
|
||||
/** The address of the newly instantiated contract *-/
|
||||
readonly contractAddress: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
/** Block height in which the transaction is included *-/
|
||||
readonly height: number
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex *-/
|
||||
readonly transactionHash: string
|
||||
readonly gasWanted: number
|
||||
readonly gasUsed: number
|
||||
}*/
|
||||
|
||||
interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface UseOpenEditionFactoryContractProps {
|
||||
use: (customAddress: string) => OpenEditionFactoryInstance | undefined
|
||||
updateContractAddress: (contractAddress: string) => void
|
||||
getContractAddress: () => string | undefined
|
||||
messages: (contractAddress: string) => OpenEditionFactoryMessages | undefined
|
||||
}
|
||||
|
||||
export function useOpenEditionFactoryContract(): UseOpenEditionFactoryContractProps {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [address, setAddress] = useState<string>('')
|
||||
const [openEditionFactory, setOpenEditionFactory] = useState<OpenEditionFactoryContract>()
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(localStorage.getItem('contract_address') || '')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const OpenEditionFactoryBaseContract = initContract(wallet.getClient(), wallet.address)
|
||||
setOpenEditionFactory(OpenEditionFactoryBaseContract)
|
||||
}, [wallet])
|
||||
|
||||
const updateContractAddress = (contractAddress: string) => {
|
||||
setAddress(contractAddress)
|
||||
}
|
||||
|
||||
const use = useCallback(
|
||||
(customAddress = ''): OpenEditionFactoryInstance | undefined => {
|
||||
return openEditionFactory?.use(address || customAddress)
|
||||
},
|
||||
[openEditionFactory, address],
|
||||
)
|
||||
|
||||
const getContractAddress = (): string | undefined => {
|
||||
return address
|
||||
}
|
||||
|
||||
const messages = useCallback(
|
||||
(customAddress = ''): OpenEditionFactoryMessages | undefined => {
|
||||
return openEditionFactory?.messages(address || customAddress)
|
||||
},
|
||||
[openEditionFactory, address],
|
||||
)
|
||||
|
||||
return {
|
||||
use,
|
||||
updateContractAddress,
|
||||
getContractAddress,
|
||||
messages,
|
||||
}
|
||||
}
|
||||
@@ -1,651 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import { coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types'
|
||||
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface MigrateResponse {
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface RoyaltyInfo {
|
||||
payment_address: string
|
||||
share: string
|
||||
}
|
||||
|
||||
export interface OpenEditionMinterInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
//Query
|
||||
getConfig: () => Promise<any>
|
||||
getStartTime: () => Promise<any>
|
||||
getEndTime: () => Promise<any>
|
||||
getMintPrice: () => Promise<any>
|
||||
getMintCount: (address: string) => Promise<any>
|
||||
getTotalMintCount: () => Promise<any>
|
||||
getStatus: () => Promise<any>
|
||||
|
||||
//Execute
|
||||
mint: (senderAddress: string) => Promise<string>
|
||||
purge: (senderAddress: string) => Promise<string>
|
||||
updateMintPrice: (senderAddress: string, price: string) => Promise<string>
|
||||
updateStartTime: (senderAddress: string, time: Timestamp) => Promise<string>
|
||||
updateEndTime: (senderAddress: string, time: Timestamp) => Promise<string>
|
||||
updateStartTradingTime: (senderAddress: string, time?: Timestamp) => Promise<string>
|
||||
updatePerAddressLimit: (senderAddress: string, perAddressLimit: number) => Promise<string>
|
||||
mintTo: (senderAddress: string, recipient: string) => Promise<string>
|
||||
batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise<string>
|
||||
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
|
||||
}
|
||||
|
||||
export interface OpenEditionMinterMessages {
|
||||
mint: () => MintMessage
|
||||
purge: () => PurgeMessage
|
||||
updateMintPrice: (price: string) => UpdateMintPriceMessage
|
||||
updateStartTime: (time: Timestamp) => UpdateStartTimeMessage
|
||||
updateEndTime: (time: Timestamp) => UpdateEndTimeMessage
|
||||
updateStartTradingTime: (time: Timestamp) => UpdateStartTradingTimeMessage
|
||||
updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage
|
||||
mintTo: (recipient: string) => MintToMessage
|
||||
batchMint: (recipient: string, batchNumber: number) => CustomMessage
|
||||
airdrop: (recipients: string[]) => CustomMessage
|
||||
}
|
||||
|
||||
export interface MintMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface PurgeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
purge: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateStartTradingTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_start_trading_time: string
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateStartTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_start_time: string
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateEndTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_end_time: string
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateMintPriceMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_mint_price: {
|
||||
price: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdatePerAddressLimitMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_per_address_limit: {
|
||||
per_address_limit: number
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintToMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint_to: {
|
||||
recipient: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface CustomMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>[]
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintPriceMessage {
|
||||
public_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
airdrop_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
current_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface OpenEditionMinterContract {
|
||||
instantiate: (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
funds?: Coin[],
|
||||
) => Promise<InstantiateResponse>
|
||||
|
||||
migrate: (
|
||||
senderAddress: string,
|
||||
contractAddress: string,
|
||||
codeId: number,
|
||||
migrateMsg: Record<string, unknown>,
|
||||
) => Promise<MigrateResponse>
|
||||
|
||||
use: (contractAddress: string) => OpenEditionMinterInstance
|
||||
|
||||
messages: (contractAddress: string) => OpenEditionMinterMessages
|
||||
}
|
||||
|
||||
export const openEditionMinter = (client: SigningCosmWasmClient, txSigner: string): OpenEditionMinterContract => {
|
||||
const use = (contractAddress: string): OpenEditionMinterInstance => {
|
||||
//Query
|
||||
const getFactoryParameters = async (factoryAddress: string): Promise<any> => {
|
||||
const res = await client.queryContractSmart(factoryAddress, { params: {} })
|
||||
return res
|
||||
}
|
||||
|
||||
const getConfig = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
config: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getStartTime = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
start_time: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getEndTime = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
end_time: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getMintPrice = async (): Promise<MintPriceMessage> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
mint_price: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getMintCount = async (address: string): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
mint_count: { address },
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getTotalMintCount = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
total_mint_count: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getStatus = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
status: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
//Execute
|
||||
const mint = async (senderAddress: string): Promise<string> => {
|
||||
const price = (await getMintPrice()).public_price.amount
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
mint: {},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
[coin(price, 'ustars')],
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const purge = async (senderAddress: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
purge: {},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateStartTradingTime = async (senderAddress: string, time?: Timestamp): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_start_trading_time: time || null,
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateStartTime = async (senderAddress: string, time: Timestamp): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_start_time: time,
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateEndTime = async (senderAddress: string, time: Timestamp): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_end_time: time,
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateMintPrice = async (senderAddress: string, price: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_mint_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updatePerAddressLimit = async (senderAddress: string, perAddressLimit: number): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_per_address_limit: { per_address_limit: perAddressLimit },
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const mintTo = async (senderAddress: string, recipient: string): Promise<string> => {
|
||||
const txHash = await getConfig().then(async (response) => {
|
||||
const factoryParameters = await toast.promise(getFactoryParameters(response.factory), {
|
||||
loading: 'Querying Factory Parameters...',
|
||||
error: 'Querying Factory Parameters failed!',
|
||||
success: 'Query successful! Minting...',
|
||||
})
|
||||
console.log(factoryParameters?.params?.extension?.airdrop_mint_fee_bps)
|
||||
|
||||
const price = factoryParameters?.params?.extension?.airdrop_mint_price.amount
|
||||
const denom = factoryParameters?.params?.extension?.airdrop_mint_price.denom || 'ustars'
|
||||
if (!price) {
|
||||
throw new Error(
|
||||
'Unable to retrieve a valid airdrop mint price. It may be that the given contract address does not belong to an Open Edition Factory.',
|
||||
)
|
||||
}
|
||||
const airdropFee = Number(price) * Number(factoryParameters.params.extension?.airdrop_mint_fee_bps)
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
mint_to: { recipient },
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
airdropFee > 0 ? [coin(airdropFee / 100 / 100, denom as string)] : [],
|
||||
)
|
||||
return res.transactionHash
|
||||
})
|
||||
return txHash
|
||||
}
|
||||
|
||||
const batchMint = async (senderAddress: string, recipient: string, batchNumber: number): Promise<string> => {
|
||||
const txHash = await getConfig().then(async (response) => {
|
||||
const factoryParameters = await toast.promise(getFactoryParameters(response?.factory), {
|
||||
loading: 'Querying Factory Parameters...',
|
||||
error: 'Querying Factory Parameters failed!',
|
||||
success: 'Query successful! Minting...',
|
||||
})
|
||||
|
||||
const price = factoryParameters?.params?.extension?.airdrop_mint_price.amount
|
||||
const denom = factoryParameters?.params?.extension?.airdrop_mint_price.denom || 'ustars'
|
||||
if (!price) {
|
||||
throw new Error(
|
||||
'Unable to retrieve a valid airdrop mint price. It may be that the given contract address does not belong to a Open Edition Factory.',
|
||||
)
|
||||
}
|
||||
const airdropFee = Number(price) * Number(factoryParameters.params.extension?.airdrop_mint_fee_bps)
|
||||
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
|
||||
for (let i = 0; i < batchNumber; i++) {
|
||||
const msg = {
|
||||
mint_to: { recipient },
|
||||
}
|
||||
const executeContractMsg: MsgExecuteContractEncodeObject = {
|
||||
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
|
||||
value: MsgExecuteContract.fromPartial({
|
||||
sender: senderAddress,
|
||||
contract: contractAddress,
|
||||
msg: toUtf8(JSON.stringify(msg)),
|
||||
funds: airdropFee > 0 ? [coin(airdropFee / 100 / 100, denom as string)] : [],
|
||||
}),
|
||||
}
|
||||
|
||||
executeContractMsgs.push(executeContractMsg)
|
||||
}
|
||||
|
||||
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'batch mint')
|
||||
|
||||
return res.transactionHash
|
||||
})
|
||||
return txHash
|
||||
}
|
||||
|
||||
const airdrop = async (senderAddress: string, recipients: string[]): Promise<string> => {
|
||||
const txHash = await getConfig().then(async (response) => {
|
||||
const factoryParameters = await toast.promise(getFactoryParameters(response?.factory), {
|
||||
loading: 'Querying Factory Parameters...',
|
||||
error: 'Querying Factory Parameters failed!',
|
||||
success: 'Query successful! Minting...',
|
||||
})
|
||||
|
||||
const price = factoryParameters?.params?.extension?.airdrop_mint_price.amount
|
||||
const denom = factoryParameters?.params?.extension?.airdrop_mint_price.denom || 'ustars'
|
||||
if (!price) {
|
||||
throw new Error(
|
||||
'Unable to retrieve a valid airdrop mint price. It may be that the given contract address does not belong to a Open Edition Factory.',
|
||||
)
|
||||
}
|
||||
const airdropFee = Number(price) * Number(factoryParameters.params.extension?.airdrop_mint_fee_bps)
|
||||
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
|
||||
for (let i = 0; i < recipients.length; i++) {
|
||||
const msg = {
|
||||
mint_to: { recipient: recipients[i] },
|
||||
}
|
||||
const executeContractMsg: MsgExecuteContractEncodeObject = {
|
||||
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
|
||||
value: MsgExecuteContract.fromPartial({
|
||||
sender: senderAddress,
|
||||
contract: contractAddress,
|
||||
msg: toUtf8(JSON.stringify(msg)),
|
||||
funds: airdropFee > 0 ? [coin(airdropFee / 100 / 100, denom as string)] : [],
|
||||
}),
|
||||
}
|
||||
|
||||
executeContractMsgs.push(executeContractMsg)
|
||||
}
|
||||
|
||||
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop')
|
||||
|
||||
return res.transactionHash
|
||||
})
|
||||
return txHash
|
||||
}
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
getConfig,
|
||||
getStartTime,
|
||||
getEndTime,
|
||||
getMintPrice,
|
||||
getMintCount,
|
||||
getTotalMintCount,
|
||||
getStatus,
|
||||
mint,
|
||||
purge,
|
||||
updateStartTradingTime,
|
||||
updateStartTime,
|
||||
updateEndTime,
|
||||
updateMintPrice,
|
||||
updatePerAddressLimit,
|
||||
mintTo,
|
||||
batchMint,
|
||||
airdrop,
|
||||
}
|
||||
}
|
||||
|
||||
const migrate = async (
|
||||
senderAddress: string,
|
||||
contractAddress: string,
|
||||
codeId: number,
|
||||
migrateMsg: Record<string, unknown>,
|
||||
): Promise<MigrateResponse> => {
|
||||
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
|
||||
return {
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
const instantiate = async (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
): Promise<InstantiateResponse> => {
|
||||
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
|
||||
funds: [coin('1000000000', 'ustars')],
|
||||
})
|
||||
|
||||
return {
|
||||
contractAddress: result.contractAddress,
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = (contractAddress: string) => {
|
||||
const mint = (): MintMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint: {},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const purge = (): PurgeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
purge: {},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateStartTradingTime = (startTime: string): UpdateStartTradingTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_start_trading_time: startTime,
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateStartTime = (startTime: string): UpdateStartTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_start_time: startTime,
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateEndTime = (endTime: string): UpdateEndTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_end_time: endTime,
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateMintPrice = (price: string): UpdateMintPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_mint_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updatePerAddressLimit = (limit: number): UpdatePerAddressLimitMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_per_address_limit: {
|
||||
per_address_limit: limit,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const mintTo = (recipient: string): MintToMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint_to: {
|
||||
recipient,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const batchMint = (recipient: string, batchNumber: number): CustomMessage => {
|
||||
const msg: Record<string, unknown>[] = []
|
||||
for (let i = 0; i < batchNumber; i++) {
|
||||
msg.push({ mint_to: { recipient } })
|
||||
}
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const airdrop = (recipients: string[]): CustomMessage => {
|
||||
const msg: Record<string, unknown>[] = []
|
||||
for (let i = 0; i < recipients.length; i++) {
|
||||
msg.push({ mint_to: { recipient: recipients[i] } })
|
||||
}
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mint,
|
||||
purge,
|
||||
updateStartTradingTime,
|
||||
updateStartTime,
|
||||
updateEndTime,
|
||||
updateMintPrice,
|
||||
updatePerAddressLimit,
|
||||
mintTo,
|
||||
batchMint,
|
||||
airdrop,
|
||||
}
|
||||
}
|
||||
|
||||
return { use, instantiate, migrate, messages }
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './contract'
|
||||
export * from './useContract'
|
||||
@@ -1,163 +0,0 @@
|
||||
import type { OpenEditionMinterInstance } from '../index'
|
||||
import { useOpenEditionMinterContract } from '../index'
|
||||
|
||||
export type ExecuteType = typeof EXECUTE_TYPES[number]
|
||||
|
||||
export const EXECUTE_TYPES = [
|
||||
'mint',
|
||||
'update_start_time',
|
||||
'update_end_time',
|
||||
'update_mint_price',
|
||||
'update_start_trading_time',
|
||||
'update_per_address_limit',
|
||||
'mint_to',
|
||||
'purge',
|
||||
] as const
|
||||
|
||||
export interface ExecuteListItem {
|
||||
id: ExecuteType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
{
|
||||
id: 'mint',
|
||||
name: 'Mint',
|
||||
description: `Mint a new token`,
|
||||
},
|
||||
{
|
||||
id: 'update_mint_price',
|
||||
name: 'Update Mint Price',
|
||||
description: `Update the mint price per token`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_time',
|
||||
name: 'Update Start Time',
|
||||
description: `Update the start time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_end_time',
|
||||
name: 'Update End Time',
|
||||
description: `Update the end time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_trading_time',
|
||||
name: 'Update Start Trading Time',
|
||||
description: `Update start trading time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_per_address_limit',
|
||||
name: 'Update Per Address Limit',
|
||||
description: `Update token per address limit`,
|
||||
},
|
||||
{
|
||||
id: 'mint_to',
|
||||
name: 'Mint To',
|
||||
description: `Mint tokens to a given address`,
|
||||
},
|
||||
{
|
||||
id: 'purge',
|
||||
name: 'Purge',
|
||||
description: `Purge`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
type: ExecuteType
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
type Select<T extends ExecuteType> = T
|
||||
|
||||
/** @see {@link OpenEditionMinterInstance} */
|
||||
export type DispatchExecuteArgs = {
|
||||
contract: string
|
||||
messages?: OpenEditionMinterInstance
|
||||
txSigner: string
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'mint'> }
|
||||
| { type: Select<'purge'> }
|
||||
| { type: Select<'update_start_time'>; startTime: string }
|
||||
| { type: Select<'update_end_time'>; endTime: string }
|
||||
| { type: Select<'update_mint_price'>; price: string }
|
||||
| { type: Select<'update_start_trading_time'>; startTime?: string }
|
||||
| { type: Select<'update_per_address_limit'>; limit: number }
|
||||
| { type: Select<'mint_to'>; recipient: string }
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { messages, txSigner } = args
|
||||
if (!messages) {
|
||||
throw new Error('cannot dispatch execute, messages is not defined')
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return messages.mint(txSigner)
|
||||
}
|
||||
case 'purge': {
|
||||
return messages.purge(txSigner)
|
||||
}
|
||||
case 'update_start_time': {
|
||||
return messages.updateStartTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_end_time': {
|
||||
return messages.updateEndTime(txSigner, args.endTime)
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return messages.updateMintPrice(txSigner, args.price)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return messages.updateStartTradingTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return messages.updatePerAddressLimit(txSigner, args.limit)
|
||||
}
|
||||
case 'mint_to': {
|
||||
return messages.mintTo(txSigner, args.recipient)
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown execute type')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages } = useOpenEditionMinterContract()
|
||||
const { contract } = args
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return messages(contract)?.mint()
|
||||
}
|
||||
case 'purge': {
|
||||
return messages(contract)?.purge()
|
||||
}
|
||||
case 'update_start_time': {
|
||||
return messages(contract)?.updateStartTime(args.startTime)
|
||||
}
|
||||
case 'update_end_time': {
|
||||
return messages(contract)?.updateEndTime(args.endTime)
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return messages(contract)?.updateMintPrice(args.price)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return messages(contract)?.updateStartTradingTime(args.startTime as string)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return messages(contract)?.updatePerAddressLimit(args.limit)
|
||||
}
|
||||
case 'mint_to': {
|
||||
return messages(contract)?.mintTo(args.recipient)
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
|
||||
return arr.some((val) => type === val)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user