Compare commits
19 Commits
develop
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad9d755eb | ||
|
|
7bdf81d95e | ||
|
|
5e28d7dfb0 | ||
|
|
71c273c4ef | ||
|
|
2f681bbcba | ||
|
|
03ddc13911 | ||
|
|
32f4157c42 | ||
|
|
dc902313a6 | ||
|
|
35e736062a | ||
|
|
e64073499d | ||
|
|
019a0c4d73 | ||
|
|
129953f7b3 | ||
|
|
6f36284fbd | ||
|
|
f4f11dbe6a | ||
|
|
07a08ca35a | ||
|
|
eb82d10140 | ||
|
|
fc65053978 | ||
|
|
fae92e5483 | ||
|
|
2340657020 |
131
.env.example
131
.env.example
@ -1,129 +1,18 @@
|
||||
APP_VERSION=0.8.7
|
||||
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_STRDST_SG721_CODE_ID=2595
|
||||
NEXT_PUBLIC_BASE_FACTORY_SG721_CODE_ID=2595
|
||||
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_FEATURED_VENDING_FACTORY_ADDRESS="stars14pd96yk3t6gq9l6uyrkg0n5dr09n8rt5y9v3at8x4wl4lrkxhlzq4trqmh"
|
||||
NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1h65nms9gwg4vdktyqj84tu50gwlm34e0eczl5w2ezllxuzfxy9esa9qlt0"
|
||||
NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS="stars1hvu2ghqkcnvhtj2fc6wuazxt4dqcftslp2rwkkkcxy269a35a9pq60ug2q"
|
||||
NEXT_PUBLIC_VENDING_FACTORY_MERKLE_TREE_ADDRESS="stars167tudcsr9n2y9ljgk4cwxhs0cvkfkk0hh6c3dzngsz7m5s9jmqnsdgr3jy"
|
||||
NEXT_PUBLIC_FEATURED_VENDING_FACTORY_MERKLE_TREE_ADDRESS="stars167tudcsr9n2y9ljgk4cwxhs0cvkfkk0hh6c3dzngsz7m5s9jmqnsdgr3jy"
|
||||
NEXT_PUBLIC_FEATURED_VENDING_FACTORY_FLEX_ADDRESS="stars1udlmmnmmnnqamh36hy6d7azn3ycv23yymkmg6558ntalvyt2pz7s8lhgcd"
|
||||
# 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_FEATURED_VENDING_IBC_USDC_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS=
|
||||
# NEXT_PUBLIC_FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS=
|
||||
|
||||
# NEXT_PUBLIC_VENDING_IBC_USK_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USK_UPDATABLE_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USK_FACTORY_FLEX_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_USK_UPDATABLE_FACTORY_FLEX_ADDRESS=
|
||||
|
||||
# NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS=
|
||||
# NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS=
|
||||
# NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS=
|
||||
# NEXT_PUBLIC_VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS=
|
||||
|
||||
NEXT_PUBLIC_VENDING_NATIVE_STARDUST_FACTORY_ADDRESS="stars1mxwf2hjcjvqnlw0v3j7m0u34975qesp325wzrgz0ht7vr8ys2zmsenjutf"
|
||||
NEXT_PUBLIC_VENDING_NATIVE_STARDUST_UPDATABLE_FACTORY_ADDRESS="stars18gjczf88jd4z3a3megwj9g5c9famu654csxfnnq59mkqeszuzy4ssdgr46"
|
||||
NEXT_PUBLIC_VENDING_NATIVE_STRDST_FLEX_FACTORY_ADDRESS="stars1eluqmr6x78ehl4plrln6khxc0qrspfhc7rt3whmr59escpve0r4swcacjh"
|
||||
|
||||
# NEXT_PUBLIC_VENDING_NATIVE_BRNCH_FACTORY_ADDRESS=""
|
||||
# NEXT_PUBLIC_VENDING_NATIVE_BRNCH_UPDATABLE_FACTORY_ADDRESS=""
|
||||
# NEXT_PUBLIC_VENDING_NATIVE_BRNCH_FLEX_FACTORY_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_FACTORY_FLEX_ADDRESS="stars1nc59ddaa8xcx9mu8jladza82dznhxrta3njal3xylkqlsfqa7g4s9s5q02"
|
||||
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS="stars1fk5dkzcylam8mcpqrn8y9spauvc3d4navtaqurcc49dc3p9f8d3qdkvymx"
|
||||
|
||||
NEXT_PUBLIC_VENDING_IBC_KUJI_FACTORY_ADDRESS="stars1yyje87e0h9mqg34kp3x75yesa78ve4glc3dstdrn6nscw3zjfanqkj95f0"
|
||||
NEXT_PUBLIC_VENDING_IBC_KUJI_FACTORY_FLEX_ADDRESS="stars1jralxqalpw9nf3kdc0s222z3mk343wry60cjaze9xadgfn2te4usf92e9r"
|
||||
|
||||
NEXT_PUBLIC_VENDING_IBC_HUAHUA_FACTORY_ADDRESS="stars16luw6rxgr6as9s7eu5auvnk5tnzszjrs34etsw9fmk25yqjfq09qq9gzl4"
|
||||
NEXT_PUBLIC_VENDING_IBC_HUAHUA_FACTORY_FLEX_ADDRESS="stars1d97h6nfgwqr8eynzdcrsm3p0n6rduvkrcqdjhm5z7heavtgnqg4sgy2yew"
|
||||
|
||||
NEXT_PUBLIC_VENDING_IBC_CRBRUS_FACTORY_ADDRESS="stars1z0upxsyxhrvygrsd2t69majd6wl8qw4h8ff2fp27z3nn93m73pwsu4hpdh"
|
||||
NEXT_PUBLIC_VENDING_IBC_CRBRUS_FACTORY_FLEX_ADDRESS="stars1halhp674yxwgn3p4gpkl8790h07vkm0vjm4vj7y8ql499e3zydzqurt5m3"
|
||||
|
||||
# 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="stars152a40mmd3k2kk90add606vrqxcvzdp29qrjx4pjv33cjl6svksfscrrtuk"
|
||||
# NEXT_PUBLIC_OPEN_EDITION_IBC_USDC_FACTORY_FLEX_ADDRESS="stars10sz9mup3a548l34k83q5w59nrklrnvv2gdsdkr2xref4zl5j3d4q0efamx"
|
||||
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS=
|
||||
# NEXT_PUBLIC_OPEN_EDITION_IBC_TIA_FACTORY_ADDRESS="stars1vza7k890fkejxz3mqwau0u2m89k9y76w94vvxe4d42ya9862ryfq0damns"
|
||||
# NEXT_PUBLIC_OPEN_EDITION_IBC_TIA_FACTORY_FLEX_ADDRESS="stars1jgn0ntt5tut93yn756rrqa60794qdsrn6dwhl8vhfx0yxgpr44qsfzhmrt"
|
||||
# NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_TIA_FACTORY_ADDRESS=
|
||||
NEXT_PUBLIC_OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS="stars1vzffawsjhvspstu5lvtzz2x5n7zh07hnw09c9dfxcj78un05rcms5n3q3e"
|
||||
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS="stars1tc09vlgdg8rqyapcxwm9qdq8naj4gym9px4ntue9cs0kse5rvess0nee3a"
|
||||
|
||||
NEXT_PUBLIC_OPEN_EDITION_NATIVE_STRDST_FACTORY_ADDRESS="stars10sw8fvwtetndy3ctpcvee8yq7t6qp49m5yahm5gf8qz3qt3hzvcq5c2m0s"
|
||||
NEXT_PUBLIC_OPEN_EDITION_NATIVE_BRNCH_FACTORY_ADDRESS="stars1uxdqnu9ysd9q8kd43c52ufy9azfxyuvyt5nnyk4p2gtag30zre3q0cg30z"
|
||||
|
||||
NEXT_PUBLIC_OPEN_EDITION_IBC_USK_FACTORY_ADDRESS="stars1vxf9u6a4d5ty00k59zthv7mnpzlrfhqnf4ds0y0eake7lepuamnqymyf3t"
|
||||
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS="stars1njhkyyv0l8dmq528w67t8dxyg5a3h0hvusk6pfvpm52pspd9gq9s3zmdez"
|
||||
|
||||
NEXT_PUBLIC_OPEN_EDITION_IBC_KUJI_FACTORY_ADDRESS="stars1yjvfy6fpm4nxl0afm6e8lnx96e6v49e3fxsymsdxxtu0pdeshrxq702zaz"
|
||||
|
||||
NEXT_PUBLIC_OPEN_EDITION_IBC_HUAHUA_FACTORY_ADDRESS="stars1grxlqatna07y8f3tzu2l9lmt82uj8gzzshxnz2ruwn6yljpyucnq059rmn"
|
||||
|
||||
# NEXT_PUBLIC_OPEN_EDITION_IBC_CRBRUS_FACTORY_ADDRESS=""
|
||||
|
||||
NEXT_PUBLIC_OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS="stars1tjzlz2e8pkucgytkjct5drt7x0dysnepqv3nmvxn0fzk2hfv73zsneevyt"
|
||||
|
||||
NEXT_PUBLIC_OPEN_EDITION_IBC_NBTC_FACTORY_ADDRESS="stars1cd4gykxfq4nc4yx8uzn8yr3ggu86r57chhxme4y7q2jag53cw75qgs96u8"
|
||||
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_NBTC_FACTORY_ADDRESS="stars1d57xe77mvcg5q337umf4qz49vumfn6w3wss0t7u8ra6s3cyvezsqyaeejn"
|
||||
|
||||
NEXT_PUBLIC_VENDING_IBC_NBTC_FACTORY_ADDRESS="stars1e6t6lp052er2gu3rwjnf434vgh59ydkfg8dm589fxlx593afqmuqh75a0s"
|
||||
NEXT_PUBLIC_VENDING_IBC_NBTC_UPDATABLE_FACTORY_ADDRESS="stars1k6ee8qgwvumguqnqqrvsnwluwk0rp994nkcgdemk0tj3ecc5kk8su2tcr4"
|
||||
|
||||
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
|
||||
NEXT_PUBLIC_ROYALTY_REGISTRY_ADDRESS="stars1crgx0f70fzksa57hq87wtl8f04h0qyk5la0hk0fu8dyhl67ju80qaxzr5z"
|
||||
NEXT_PUBLIC_INFINITY_SWAP_PROTOCOL_ADDRESS="stars136yp6fl9h66m0cwv8weu4w4aawveuz40992ty0atj5ecjd8z0thqv9xpy5"
|
||||
NEXT_PUBLIC_WHITELIST_CODE_ID=4008
|
||||
NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=4009
|
||||
NEXT_PUBLIC_WHITELIST_MERKLE_TREE_CODE_ID=3911
|
||||
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=4010
|
||||
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_WHITELIST_MERKLE_TREE_API_URL="https://..."
|
||||
NEXT_PUBLIC_NFT_STORAGE_DEFAULT_API_KEY="..."
|
||||
|
||||
NEXT_PUBLIC_MEILISEARCH_HOST="https://search.publicawesome.dev"
|
||||
NEXT_PUBLIC_MEILISEARCH_API_KEY= "..."
|
||||
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
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -1 +1 @@
|
||||
* @MightOfOaks @name-user1 @Ninjatosba
|
||||
* @MightOfOaks @Ninjatosba
|
||||
|
||||
45
.github/workflows/publish.yaml
vendored
45
.github/workflows/publish.yaml
vendored
@ -1,45 +0,0 @@
|
||||
name: Publish ApplicationRecord to Registry
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '*'
|
||||
|
||||
env:
|
||||
CERC_REGISTRY_USER_KEY: ${{ secrets.CICD_LACONIC_USER_KEY }}
|
||||
CERC_REGISTRY_BOND_ID: ${{ secrets.CICD_LACONIC_BOND_ID }}
|
||||
|
||||
jobs:
|
||||
cns_publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Clone project repository"
|
||||
uses: actions/checkout@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18 # though you need version 14 with geojson
|
||||
# - name: "Install exiftool"
|
||||
# run: |
|
||||
# apt-get update -y
|
||||
# apt-get upgrade -y
|
||||
# apt-get install exiftool -y
|
||||
#- name: "Exiftool Version"
|
||||
# run: |
|
||||
# exiftool -ver
|
||||
- name: "Install Yarn"
|
||||
run: npm install -g yarn
|
||||
- name: "Install registry CLI"
|
||||
run: |
|
||||
npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/
|
||||
yarn global add @cerc-io/laconic-registry-cli
|
||||
- name: "Install jq"
|
||||
uses: dcarbone/install-jq-action@v2.1.0
|
||||
- name: "Publish App Record"
|
||||
run: scripts/publish-app-record.sh
|
||||
#- name: "Create Metadata Record"
|
||||
# run: scripts/create-metadata-record.sh
|
||||
- name: "Request Deployment"
|
||||
run: scripts/request-app-deployment.sh
|
||||
@ -1,62 +1,16 @@
|
||||
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 { 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'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
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.isWalletConnected) throw new Error('Wallet not connected')
|
||||
await (
|
||||
await wallet.getCosmWasmClient()
|
||||
)
|
||||
.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')) {
|
||||
@ -64,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,26 +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"
|
||||
/>
|
||||
<span className="flex self-center ">{assetSource.name}</span>
|
||||
</div>
|
||||
)}
|
||||
{getAssetType(assetSource.name) === 'document' && (
|
||||
<div className="flex absolute flex-col items-center mt-4 ml-2">
|
||||
<img
|
||||
key={`document-${index}`}
|
||||
alt="document_icon"
|
||||
className={clsx('mb-2 ml-1 w-6 h-6 thumbnail')}
|
||||
src="/pdf.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>
|
||||
)}
|
||||
@ -102,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>
|
||||
))
|
||||
@ -147,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,128 +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 'utils/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.isWalletConnected) throw new Error('Wallet not connected')
|
||||
await (
|
||||
await wallet.getCosmWasmClient()
|
||||
)
|
||||
.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,69 +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-[23%] bottom-5 left-1/3 max-w-[600px] max-h-[410px] 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. Stargaze.zone
|
||||
reserves the right to exercise its discretion in concealing user-generated content, should such content be
|
||||
determined to have a detrimental impact on the brand.
|
||||
</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,57 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
|
||||
/* eslint-disable jsx-a11y/img-redundant-alt */
|
||||
import { truncateAddress } from 'utils/wallet'
|
||||
|
||||
export interface ClickableCollection {
|
||||
contractAddress: string
|
||||
name: string
|
||||
media: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function CollectionsTable({ collections }: { collections: ClickableCollection[] }) {
|
||||
return (
|
||||
<table className="w-full divide-y divide-zinc-800 table-fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-3.5 pr-3 pl-4 text-sm text-left sm:pl-0 text-infinity-blue" scope="col">
|
||||
Name
|
||||
</th>
|
||||
<th className="py-3.5 px-3 text-sm text-left text-infinity-blue" scope="col">
|
||||
Address
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className=" bg-black">
|
||||
{collections
|
||||
? collections?.map((collection) => (
|
||||
<tr
|
||||
key={collection.contractAddress}
|
||||
className="hover:bg-zinc-900 cursor-pointer"
|
||||
onClick={collection.onClick}
|
||||
>
|
||||
<td className="py-2 pr-3 pl-4 whitespace-nowrap sm:pl-0">
|
||||
<div className="flex items-center">
|
||||
<div className="shrink-0 w-11 h-11">
|
||||
<img alt="Collection Image" src={collection.media} />
|
||||
</div>
|
||||
<div className="ml-4 font-medium text-white truncate">{collection.name}</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-5 px-3 text-zinc-400 whitespace-nowrap">
|
||||
<div className="text-left text-white">
|
||||
{collection.contractAddress?.startsWith('stars')
|
||||
? truncateAddress(collection.contractAddress)
|
||||
: collection.contractAddress}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
@ -1,61 +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-[23%] bottom-5 left-1/3 max-w-[600px] max-h-[440px] 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. Stargaze.zone
|
||||
reserves the right to exercise its discretion in concealing user-generated content, should such content be
|
||||
determined to have a detrimental impact on the brand.
|
||||
</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
|
||||
|
||||
@ -8,7 +8,7 @@ export function FaviconsMetaTags() {
|
||||
<link href="/assets/manifest.webmanifest" rel="manifest" />
|
||||
<meta content="yes" name="mobile-web-app-capable" />
|
||||
<meta content="#F0827D" name="theme-color" />
|
||||
<meta content="Stargaze Studio" name="application-name" />
|
||||
<meta content="StargazeStudio" name="application-name" />
|
||||
<link href="/assets/apple-touch-icon-57x57.png" rel="apple-touch-icon" sizes="57x57" />
|
||||
<link href="/assets/apple-touch-icon-60x60.png" rel="apple-touch-icon" sizes="60x60" />
|
||||
<link href="/assets/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72" />
|
||||
@ -22,7 +22,7 @@ export function FaviconsMetaTags() {
|
||||
<link href="/assets/apple-touch-icon-1024x1024.png" rel="apple-touch-icon" sizes="1024x1024" />
|
||||
<meta content="yes" name="apple-mobile-web-app-capable" />
|
||||
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style" />
|
||||
<meta content="Stargaze Studio" name="apple-mobile-web-app-title" />
|
||||
<meta content="StargazeStudio" name="apple-mobile-web-app-title" />
|
||||
<link
|
||||
href="/assets/apple-touch-startup-image-640x1136.png"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-implicit-coercion */
|
||||
/* eslint-disable import/no-default-export */
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface FieldsetBaseType {
|
||||
/**
|
||||
* The input's required id, used to link the label and input, as well as the error message.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* Error message to show input validation.
|
||||
*/
|
||||
error?: string
|
||||
/**
|
||||
* Success message to show input validation.
|
||||
*/
|
||||
success?: string
|
||||
/**
|
||||
* Label to describe the input.
|
||||
*/
|
||||
label?: string | ReactNode
|
||||
/**
|
||||
* Hint to show optional fields or a hint to the user of what to enter in the input.
|
||||
*/
|
||||
hint?: string
|
||||
}
|
||||
|
||||
type FieldsetType = FieldsetBaseType & {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Fieldset
|
||||
* @description A fieldset component, used to share markup for labels, hints, and errors for Input components.
|
||||
*
|
||||
* @example
|
||||
* <Fieldset error={error} hint={hint} id={id} label={label}>
|
||||
* <input id={id} {...props} />
|
||||
* </Fieldset>
|
||||
*/
|
||||
export default function Fieldset({ label, hint, id, children, error, success }: FieldsetType) {
|
||||
return (
|
||||
<div>
|
||||
{!!label && (
|
||||
<div className="flex justify-between mb-1">
|
||||
<label className="block w-full text-sm font-medium text-zinc-700 dark:text-zinc-300" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
{typeof hint === 'string' && (
|
||||
<span className="text-sm text-zinc-500 dark:text-zinc-400" id={`${id}-optional`}>
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
||||
{error && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-zinc-600" id={`${id}-error`}>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-zinc-500" id={`${id}-success`}>
|
||||
{success}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -1,173 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable import/no-default-export */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable jsx-a11y/autocomplete-valid */
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
import type { PropsOf } from '@headlessui/react/dist/types'
|
||||
import type { ReactNode } from 'react'
|
||||
import { forwardRef } from 'react'
|
||||
import { classNames } from 'utils/css'
|
||||
|
||||
import type { FieldsetBaseType } from './Fieldset'
|
||||
import Fieldset from './Fieldset'
|
||||
import type { TrailingSelectProps } from './TrailingSelect'
|
||||
import TrailingSelect from './TrailingSelect'
|
||||
|
||||
/**
|
||||
* Shared styles for all input components.
|
||||
*/
|
||||
export const inputClassNames = {
|
||||
base: [
|
||||
'block w-full rounded-lg bg-white shadow-sm dark:bg-zinc-900 sm:text-sm',
|
||||
'text-white placeholder:text-zinc-500 focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-primary-500 focus:ring-0 focus:ring-offset-0',
|
||||
],
|
||||
valid: 'border-zinc-300 focus:border-zinc-300 dark:border-zinc-800 dark:focus:border-zinc-800',
|
||||
invalid: '!text-red-500 !border-red-500 focus:!border-red-500',
|
||||
success: 'text-green border-green focus:border-green',
|
||||
}
|
||||
|
||||
type InputProps = Omit<PropsOf<'input'> & FieldsetBaseType, 'className'> & {
|
||||
directory?: 'true'
|
||||
mozdirectory?: 'true'
|
||||
webkitdirectory?: 'true'
|
||||
leadingAddon?: string
|
||||
trailingAddon?: string
|
||||
trailingAddonIcon?: ReactNode
|
||||
trailingSelectProps?: TrailingSelectProps
|
||||
autoCompleteOff?: boolean
|
||||
preventAutoCapitalizeFirstLetter?: boolean
|
||||
className?: string
|
||||
icon?: JSX.Element
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Input
|
||||
* @description A standard input component, defaults to the text type.
|
||||
*
|
||||
* @example
|
||||
* // Standard input
|
||||
* <Input id="first-name" name="first-name" />
|
||||
*
|
||||
* @example
|
||||
* // Input component with label, placeholder and type email
|
||||
* <Input id="email" name="email" type="email" autoComplete="email" label="Email" placeholder="name@email.com" />
|
||||
*
|
||||
* @example
|
||||
* // Input component with label and leading and trailing addons
|
||||
* <Input
|
||||
* id="input-label-leading-trailing"
|
||||
* label="Bid"
|
||||
* placeholder="0.00"
|
||||
* leadingAddon="$"
|
||||
* trailingAddon="USD"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Input component with label and trailing select
|
||||
* const [trailingSelectValue, trailingSelectValueSet] = useState('USD');
|
||||
*
|
||||
* <Input
|
||||
* id="input-label-trailing-select"
|
||||
* label="Bid"
|
||||
* placeholder="0.00"
|
||||
* trailingSelectProps={{
|
||||
* id: 'currency',
|
||||
* label: 'Currency',
|
||||
* value: trailingSelectValue,
|
||||
* onChange: (event) => trailingSelectValueSet(event.target.value),
|
||||
* options: ['USD', 'CAD', 'EUR'],
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
error,
|
||||
success,
|
||||
hint,
|
||||
label,
|
||||
leadingAddon,
|
||||
trailingAddon,
|
||||
trailingAddonIcon,
|
||||
trailingSelectProps,
|
||||
id,
|
||||
className,
|
||||
type = 'text',
|
||||
autoCompleteOff = false,
|
||||
preventAutoCapitalizeFirstLetter,
|
||||
icon,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const cachedClassNames = classNames(
|
||||
...inputClassNames.base,
|
||||
className,
|
||||
error ? inputClassNames.invalid : inputClassNames.valid,
|
||||
success ? inputClassNames.success : inputClassNames.valid,
|
||||
leadingAddon && 'pl-7',
|
||||
trailingAddon && 'pr-12',
|
||||
trailingSelectProps && 'pr-16',
|
||||
icon && 'pl-10',
|
||||
)
|
||||
|
||||
const describedBy = [
|
||||
...(error ? [`${id}-error`] : []),
|
||||
...(success ? [`${id}-success`] : []),
|
||||
...(typeof hint === 'string' ? [`${id}-optional`] : []),
|
||||
...(typeof trailingAddon === 'string' ? [`${id}-addon`] : []),
|
||||
].join(' ')
|
||||
|
||||
return (
|
||||
<Fieldset error={error} hint={hint} id={id} label={label} success={success}>
|
||||
<div className="relative rounded-md shadow-sm">
|
||||
{leadingAddon && (
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm">{leadingAddon}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{icon && (
|
||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
||||
<span className="pr-10 text-zinc-500 dark:text-zinc-400 sm:text-sm">{icon}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={error ? 'true' : undefined}
|
||||
autoCapitalize={`${preventAutoCapitalizeFirstLetter ?? 'off'}`}
|
||||
autoComplete={`${autoCompleteOff ? 'off' : 'on'}`}
|
||||
className={cachedClassNames}
|
||||
id={id}
|
||||
ref={ref}
|
||||
type={type}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
{!trailingAddon && trailingSelectProps && <TrailingSelect {...trailingSelectProps} />}
|
||||
|
||||
{trailingAddon && (
|
||||
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm" id={`${id}-addon`}>
|
||||
{trailingAddon}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{trailingAddonIcon && (
|
||||
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
|
||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm" id={`${id}-addonicon`}>
|
||||
{trailingAddonIcon}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fieldset>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export default Input
|
||||
@ -66,7 +66,7 @@ export const JsonPreview = ({
|
||||
</div>
|
||||
{show && (
|
||||
<div className="overflow-auto p-2 font-mono text-sm">
|
||||
<pre>{content ? JSON.stringify(content, null, 2).trim() : '{}'}</pre>
|
||||
<pre>{JSON.stringify(content, null, 2).trim()}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -47,9 +47,9 @@ export const Layout = ({ children, metadata = {} }: LayoutProps) => {
|
||||
<FaDesktop size={48} />
|
||||
<h1 className="text-2xl font-bold">Unsupported Viewport</h1>
|
||||
<p>
|
||||
Stargaze Studio is best viewed on the big screen.
|
||||
StargazeStudio is best viewed on the big screen.
|
||||
<br />
|
||||
Please open Stargaze Studio on your tablet or desktop browser.
|
||||
Please open StargazeStudio on your tablet or desktop browser.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export interface LinkTabProps {
|
||||
title: string
|
||||
@ -12,10 +11,6 @@ export interface LinkTabProps {
|
||||
export const LinkTab = (props: LinkTabProps) => {
|
||||
const { title, description, href, isActive } = props
|
||||
|
||||
// get contract address from the router
|
||||
const router = useRouter()
|
||||
const { contractAddress } = router.query
|
||||
|
||||
return (
|
||||
<Anchor
|
||||
className={clsx(
|
||||
@ -24,7 +19,7 @@ export const LinkTab = (props: LinkTabProps) => {
|
||||
isActive ? 'border-plumbus' : 'border-transparent',
|
||||
isActive ? 'bg-plumbus/5 hover:bg-plumbus/10' : 'hover:bg-white/5',
|
||||
)}
|
||||
href={href + (contractAddress ? `?contractAddress=${contractAddress as string}` : '')}
|
||||
href={href}
|
||||
>
|
||||
<h4 className="font-bold">{title}</h4>
|
||||
<span className="text-sm text-white/80 line-clamp-2">{description}</span>
|
||||
|
||||
@ -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,88 +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',
|
||||
},
|
||||
]
|
||||
|
||||
export const royaltyRegistryLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries for your Royalty Registry contract`,
|
||||
href: '/contracts/royaltyRegistry/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Royalty Registry contract actions`,
|
||||
href: '/contracts/royaltyRegistry/execute',
|
||||
},
|
||||
]
|
||||
|
||||
export const authzLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Grant',
|
||||
description: `Grant authorizations to a given address`,
|
||||
href: '/authz/grant',
|
||||
},
|
||||
{
|
||||
title: 'Revoke',
|
||||
description: `Revoke already granted authorizations`,
|
||||
href: '/authz/revoke',
|
||||
},
|
||||
]
|
||||
|
||||
export const snapshotLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Collection Holders',
|
||||
description: `Take a snapshot of collection holders`,
|
||||
href: '/snapshots/holders',
|
||||
},
|
||||
{
|
||||
title: 'Chain Snapshots',
|
||||
description: `Export a list of users fulfilling a given condition`,
|
||||
href: '/snapshots/chain',
|
||||
},
|
||||
]
|
||||
|
||||
@ -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,8 +1,7 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
import clsx from 'clsx'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
|
||||
export interface MetadataFormGroupProps {
|
||||
@ -14,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(
|
||||
() => (
|
||||
@ -42,27 +40,6 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
|
||||
[relatedAsset],
|
||||
)
|
||||
|
||||
const documentPreview = useMemo(
|
||||
() => (
|
||||
<div className="flex flex-col items-center mt-4 ml-2">
|
||||
<img key="document-key" alt="document_icon" className={clsx('mb-2 ml-2 w-24 h-24 thumbnail')} src="/pdf.png" />
|
||||
<span className="flex self-center ">{relatedAsset?.name}</span>
|
||||
</div>
|
||||
),
|
||||
[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">
|
||||
@ -71,20 +48,12 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
|
||||
{subtitle && <span className="text-sm text-white/50">{subtitle}</span>}
|
||||
<div>
|
||||
{relatedAsset && (
|
||||
<div
|
||||
className={`flex flex-row items-center mt-2 mr-4 ${
|
||||
getAssetType(relatedAsset.name) === 'document' ? '' : `border-2 border-dashed`
|
||||
}`}
|
||||
>
|
||||
<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) === 'document' && documentPreview}
|
||||
{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,237 +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
|
||||
onChange?: (metadata: any) => void
|
||||
importedMetadata?: any
|
||||
}
|
||||
|
||||
export const MetadataInput = (props: MetadataInputProps) => {
|
||||
const emptyMetadataFile = new File(
|
||||
[JSON.stringify({})],
|
||||
`${props.selectedAssetFile?.name
|
||||
.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))
|
||||
.replaceAll('#', '')}.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.replaceAll('\\n', '\n')
|
||||
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.replaceAll('#', '')
|
||||
: `${props.selectedAssetFile?.name
|
||||
.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))
|
||||
.replaceAll('#', '')}.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 if (!props.importedMetadata) {
|
||||
void parseMetadata(emptyMetadataFile)
|
||||
}
|
||||
}, [props.selectedMetadataFile?.name, props.importedMetadata])
|
||||
|
||||
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()
|
||||
if (props.onChange) props.onChange(metadata)
|
||||
}
|
||||
console.log(metadata)
|
||||
}, [
|
||||
nameStateMemo.value,
|
||||
descriptionStateMemo.value,
|
||||
externalUrlStateMemo.value,
|
||||
youtubeUrlStateMemo.value,
|
||||
attributesStateMemo.entries,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (props.importedMetadata) {
|
||||
void parseMetadata(emptyMetadataFile).then(() => {
|
||||
console.log('Imported metadata: ', props.importedMetadata)
|
||||
nameState.onChange(props.importedMetadata.name || '')
|
||||
descriptionState.onChange(props.importedMetadata.description || '')
|
||||
externalUrlState.onChange(props.importedMetadata.external_url || '')
|
||||
youtubeUrlState.onChange(props.importedMetadata.youtube_url || '')
|
||||
if (props.importedMetadata?.attributes && props.importedMetadata?.attributes?.length > 0) {
|
||||
attributesState.reset()
|
||||
props.importedMetadata?.attributes?.forEach((attribute: { trait_type: string; value: string }) => {
|
||||
attributesState.add({
|
||||
trait_type: attribute.trait_type,
|
||||
value: attribute.value,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
attributesState.reset()
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [props.importedMetadata])
|
||||
|
||||
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
|
||||
|
||||
@ -127,11 +121,10 @@ export const MetadataModal = (props: MetadataModalProps) => {
|
||||
type: 'application/json',
|
||||
})
|
||||
|
||||
const editedMetadataFile = new File([metadataFileBlob], metadataFile.name.replaceAll('#', ''), {
|
||||
type: 'application/json',
|
||||
})
|
||||
const editedMetadataFile = new File([metadataFileBlob], metadataFile.name, { type: 'application/json' })
|
||||
props.updateMetadata(editedMetadataFile)
|
||||
toast.success('Metadata updated successfully.')
|
||||
console.log(editedMetadataFile)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -151,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>
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
|
||||
import Input from 'components/Input'
|
||||
import useSearch from 'hooks/useSearch'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useDebounce } from 'utils/debounce'
|
||||
|
||||
import { CollectionsTable } from './CollectionsTable'
|
||||
|
||||
export function SelectCollection({ selectCollection }: { selectCollection: (collectionAddress: string) => void }) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [isInputFocused, setInputFocus] = useState(false)
|
||||
|
||||
const debouncedQuery = useDebounce<string>(search, 200)
|
||||
const debouncedIsInputFocused = useDebounce<boolean>(isInputFocused, 200)
|
||||
const collectionsQuery = useSearch(debouncedQuery, ['collections'], 5)
|
||||
const collectionsResults = useMemo(() => {
|
||||
return collectionsQuery.data?.find((searchResult) => searchResult.indexUid === 'collections')
|
||||
}, [collectionsQuery.data])
|
||||
|
||||
const clickableCollections = useMemo(() => {
|
||||
return (
|
||||
collectionsResults?.hits.map((hit) => ({
|
||||
contractAddress: hit.id,
|
||||
name: hit.name,
|
||||
media: hit.thumbnail_url || hit.image_url,
|
||||
onClick: () => {
|
||||
selectCollection(hit.id)
|
||||
setSearch(hit.name)
|
||||
},
|
||||
})) ?? []
|
||||
)
|
||||
}, [collectionsResults, selectCollection, setSearch])
|
||||
|
||||
const handleInputFocus = () => {
|
||||
setInputFocus(true)
|
||||
}
|
||||
|
||||
const handleInputBlur = () => {
|
||||
setInputFocus(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4 space-y-4 w-3/4 h-full bg-black rounded-md border-2 border-gray-600 border-solid md:p-6">
|
||||
<p className="text-base font-bold text-white text-start">Select the NFT collection to take a snapshot for</p>
|
||||
<Input
|
||||
className="py-2 w-full text-black dark:text-white rounded-sm md:w-72"
|
||||
icon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
|
||||
id="collection-search"
|
||||
onBlur={handleInputBlur}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onFocus={handleInputFocus}
|
||||
placeholder="Search Collections..."
|
||||
value={search}
|
||||
/>
|
||||
|
||||
{debouncedIsInputFocused && (
|
||||
<div className="overflow-auto w-full">
|
||||
<CollectionsTable collections={clickableCollections} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
import type { Timezone } from 'contexts/globalSettings'
|
||||
import { useGlobalSettings } from 'contexts/globalSettings'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import { setTimezone } from '../contexts/globalSettings'
|
||||
import { Button } from './Button'
|
||||
|
||||
export interface SettingsModalProps {
|
||||
timezone?: Timezone
|
||||
}
|
||||
export const SettingsModal = (props: SettingsModalProps) => {
|
||||
const globalSettings = useGlobalSettings()
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
|
||||
const checkBoxRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input className="modal-toggle" defaultChecked={false} id="my-modal-9" ref={checkBoxRef} type="checkbox" />
|
||||
<label className="cursor-pointer modal" htmlFor="my-modal-9">
|
||||
<label
|
||||
className={`absolute top-[42%] bottom-5 left-[260px] max-w-[450px] max-h-[250px]
|
||||
border-[1px] no-scrollbar modal-box`}
|
||||
htmlFor="temp"
|
||||
>
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-2xl font-bold underline underline-offset-2">Settings</h1>
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="flex-row mt-2 w-full form-control">
|
||||
<h1 className="mt-[5px] text-lg font-bold">Time & Date: </h1>
|
||||
<label className="justify-start ml-6 cursor-pointer label">
|
||||
<span className="mr-2 font-bold">Local</span>
|
||||
<input
|
||||
checked={globalSettings.timezone === 'Local'}
|
||||
className={`${globalSettings.timezone === 'Local' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
||||
onClick={() => {
|
||||
setTimezone('Local' as Timezone)
|
||||
window.localStorage.setItem('timezone', 'Local')
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
<label className="justify-start ml-4 cursor-pointer label">
|
||||
<span className="mr-2 font-bold">UTC</span>
|
||||
<input
|
||||
checked={globalSettings.timezone === 'UTC'}
|
||||
className={`${globalSettings.timezone === 'UTC' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
||||
onClick={() => {
|
||||
setTimezone('UTC' as Timezone)
|
||||
window.localStorage.setItem('timezone', 'UTC')
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-[40%] max-h-12 bg-blue-500 hover:bg-blue-600"
|
||||
isWide
|
||||
onClick={() => {
|
||||
setTimezone('UTC' as Timezone)
|
||||
window.localStorage.setItem('timezone', 'UTC')
|
||||
}}
|
||||
>
|
||||
Use Defaults
|
||||
</Button>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,379 +1,84 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import type { Timezone } from 'contexts/globalSettings'
|
||||
import { setTimezone } from 'contexts/globalSettings'
|
||||
import { setLogItemList, useLogStore } from 'contexts/log'
|
||||
import Link from 'next/link'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FaCog } from 'react-icons/fa'
|
||||
// import BrandText from 'public/brand/brand-text.svg'
|
||||
import { footerLinks, socialsLinks } from 'utils/links'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
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 { SettingsModal } from './SettingsModal'
|
||||
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()
|
||||
const [isTallWindow, setIsTallWindow] = useState(false)
|
||||
|
||||
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') || '[]'))
|
||||
setTimezone(
|
||||
(window.localStorage.getItem('timezone') as Timezone)
|
||||
? (window.localStorage.getItem('timezone') as Timezone)
|
||||
: 'UTC',
|
||||
)
|
||||
}, [])
|
||||
|
||||
const handleResize = () => {
|
||||
setIsTallWindow(window.innerHeight > 768)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleResize()
|
||||
window.addEventListener('resize', handleResize)
|
||||
// return () => {
|
||||
// window.removeEventListener('resize', handleResize)
|
||||
// }
|
||||
}, [])
|
||||
|
||||
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={clsx('absolute left-[5%] mt-2', isTallWindow ? 'top-[20%]' : 'top-[30%]')}>
|
||||
<ul className="group py-1 px-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>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/snapshots') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/snapshots">Snapshots</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 py-1 px-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('/tokenfactory') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
>
|
||||
<Link href="/tokenfactory/">Tokens</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('/tokenfactory/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/tokenfactory/">Token Factory</Link>
|
||||
</li>
|
||||
<li
|
||||
className={clsx(
|
||||
'disabled',
|
||||
'text-lg font-bold hover:text-white',
|
||||
router.asPath.includes('/airdrop-tokens/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/">Airdrop Tokens</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="group py-1 px-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>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/royaltyRegistry/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/royaltyRegistry/">Royalty Registry</Link>
|
||||
</li>
|
||||
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/upload/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/upload/">Upload Contract</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="group py-1 px-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('/authz/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
>
|
||||
<Link href="/authz/"> Authz </Link>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<IncomeDashboardDisclaimer creatorAddress={wallet.address ? wallet.address : ''} />
|
||||
<LogModal />
|
||||
<SettingsModal />
|
||||
{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" />
|
||||
{isTallWindow && (
|
||||
<div className="flex-row w-full h-full">
|
||||
<label
|
||||
className="absolute mb-8 w-[25%] text-lg font-bold text-white normal-case bg-zinc-500 hover:bg-zinc-600 border-none animate-none btn modal-button"
|
||||
htmlFor="my-modal-9"
|
||||
>
|
||||
<FaCog className="justify-center align-bottom" size={20} />
|
||||
</label>
|
||||
|
||||
<label
|
||||
className="ml-16 w-[65%] 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>
|
||||
</div>
|
||||
)}
|
||||
{/* Stargaze network status */}
|
||||
{isTallWindow && <div className="text-sm capitalize">Network: {wallet.chain.pretty_name}</div>}
|
||||
<div className="text-sm capitalize">Network: {wallet.network}</div>
|
||||
|
||||
{/* footer reference links */}
|
||||
<ul className="text-sm list-disc list-inside">
|
||||
{isTallWindow &&
|
||||
footerLinks.map(({ href, text }) => (
|
||||
<li key={href}>
|
||||
<Anchor className="hover:text-plumbus hover:underline" href={href}>
|
||||
{text}
|
||||
</Anchor>
|
||||
</li>
|
||||
))}
|
||||
{footerLinks.map(({ href, text }) => (
|
||||
<li key={href}>
|
||||
<Anchor className="hover:text-plumbus hover:underline" href={href}>
|
||||
{text}
|
||||
</Anchor>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* footer social links */}
|
||||
|
||||
<div className="flex gap-x-6 items-center text-white/75">
|
||||
{socialsLinks.map(({ Icon, href, text }) => (
|
||||
<Anchor key={href} className="hover:text-plumbus" href={href}>
|
||||
|
||||
@ -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,95 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
import clsx from 'clsx'
|
||||
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],
|
||||
)
|
||||
|
||||
const documentPreview = useMemo(
|
||||
() => (
|
||||
<div className="flex flex-col items-center mt-4 ml-2">
|
||||
<img key="document-key" alt="document_icon" className={clsx('mb-2 ml-1 w-20 h-20 thumbnail')} src="/pdf.png" />
|
||||
<span className="flex self-center ">{relatedAsset?.name}</span>
|
||||
</div>
|
||||
),
|
||||
[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 ${
|
||||
getAssetType(relatedAsset.name) === 'document' ? '' : `border-2 border-dashed`
|
||||
}`}
|
||||
>
|
||||
{getAssetType(relatedAsset.name) === 'audio' && audioPreview}
|
||||
{getAssetType(relatedAsset.name) === 'video' && videoPreview}
|
||||
{getAssetType(relatedAsset.name) === 'document' && documentPreview}
|
||||
{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 }}
|
||||
>
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable import/no-default-export */
|
||||
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { classNames } from 'utils/css'
|
||||
|
||||
export interface TrailingSelectProps {
|
||||
id: string
|
||||
label: string
|
||||
options: string[]
|
||||
value: string
|
||||
onChange: (event: ChangeEvent<HTMLSelectElement>) => void
|
||||
}
|
||||
|
||||
export default function TrailingSelect({ id, label, value, onChange, options }: TrailingSelectProps) {
|
||||
const cachedClassNames = classNames(
|
||||
'h-full rounded-md border-transparent bg-transparent py-0 pl-2 pr-7 text-zinc-500 dark:text-zinc-400 sm:text-sm',
|
||||
'focus:border-transparent focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-primary-500 focus:ring-0 focus:ring-offset-0',
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex absolute inset-y-0 right-0 items-center">
|
||||
<label className="sr-only" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<select className={cachedClassNames} id={id} name={id} onChange={onChange} value={value}>
|
||||
{/* TODO - Option values in a select are supposed to be unique, remove this comment during PR review */}
|
||||
{options.map((opt) => (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,63 +1,36 @@
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { tokensList } from 'config/token'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import { useWallet, useWalletStore } from 'contexts/wallet'
|
||||
import { Fragment } from 'react'
|
||||
import { FaCopy, FaPowerOff, FaRedo } from 'react-icons/fa'
|
||||
import { copy } from 'utils/clipboard'
|
||||
import { convertDenomToReadable } from 'utils/convertDenomToReadable'
|
||||
import { getShortAddress } from 'utils/getShortAddress'
|
||||
import { truncateMiddle } from 'utils/text'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
import { WalletButton } from './WalletButton'
|
||||
import { WalletPanelButton } from './WalletPanelButton'
|
||||
|
||||
export const WalletLoader = () => {
|
||||
const {
|
||||
address = '',
|
||||
username,
|
||||
connect,
|
||||
disconnect,
|
||||
isWalletConnecting,
|
||||
isWalletConnected,
|
||||
getStargateClient,
|
||||
} = useWallet()
|
||||
const { address, balance, connect, disconnect, initializing: isLoading, initialized: isReady } = useWallet()
|
||||
|
||||
// Once wallet connects, load balances.
|
||||
const [balances, setBalances] = useState<readonly Coin[] | undefined>()
|
||||
useEffect(() => {
|
||||
if (!isWalletConnected) {
|
||||
setBalances(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const loadBalances = async () => {
|
||||
const client = await getStargateClient()
|
||||
setBalances(await client.getAllBalances(address))
|
||||
}
|
||||
|
||||
loadBalances().catch(console.error)
|
||||
}, [isWalletConnected, getStargateClient, address])
|
||||
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">
|
||||
{isWalletConnected ? (
|
||||
<Popover.Button as={WalletButton} className="w-full">
|
||||
{username || address}
|
||||
</Popover.Button>
|
||||
) : (
|
||||
<WalletButton
|
||||
className="w-full"
|
||||
isLoading={isWalletConnecting}
|
||||
onClick={() => void connect().catch(console.error)}
|
||||
>
|
||||
{!isReady && (
|
||||
<WalletButton className="w-full" isLoading={isLoading} onClick={() => void connect()}>
|
||||
Connect Wallet
|
||||
</WalletButton>
|
||||
)}
|
||||
|
||||
{isReady && (
|
||||
<Popover.Button as={WalletButton} className="w-full" isLoading={isLoading}>
|
||||
{displayName}
|
||||
</Popover.Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
@ -71,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',
|
||||
)}
|
||||
@ -81,12 +54,9 @@ export const WalletLoader = () => {
|
||||
{getShortAddress(address)}
|
||||
</span>
|
||||
<div className="font-bold">Your Balances</div>
|
||||
{balances?.map((val) => (
|
||||
{balance.map((val) => (
|
||||
<span key={`balance-${val.denom}`}>
|
||||
{convertDenomToReadable(val.amount)}{' '}
|
||||
{tokensList.find((t) => t.denom === val.denom)?.displayName
|
||||
? tokensList.find((t) => t.denom === val.denom)?.displayName
|
||||
: truncateMiddle(val.denom ? val.denom : '', 28)}
|
||||
{convertDenomToReadable(val.amount)} {val.denom.slice(1, val.denom.length)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
// Styles required for @cosmos-kit/react modal
|
||||
import '@interchain-ui/react/styles'
|
||||
|
||||
import { GasPrice } from '@cosmjs/stargate'
|
||||
import { wallets as keplrExtensionWallets } from '@cosmos-kit/keplr-extension'
|
||||
import { wallets as leapExtensionWallets } from '@cosmos-kit/leap-extension'
|
||||
import { ChainProvider } from '@cosmos-kit/react'
|
||||
import { assets, chains } from 'chain-registry'
|
||||
import { getConfig } from 'config'
|
||||
import type { ReactNode } from 'react'
|
||||
import { NETWORK } from 'utils/constants'
|
||||
|
||||
export const WalletProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { gasPrice, feeToken } = getConfig(NETWORK)
|
||||
return (
|
||||
<ChainProvider
|
||||
assetLists={assets}
|
||||
chains={chains}
|
||||
endpointOptions={{
|
||||
endpoints: {
|
||||
stargaze: {
|
||||
rpc: ['https://rpc.stargaze-apis.com/'],
|
||||
rest: ['https://rest.stargaze-apis.com/'],
|
||||
},
|
||||
stargazetestnet: {
|
||||
rpc: ['https://rpc.elgafar-1.stargaze-apis.com/'],
|
||||
rest: ['https://rest.elgafar-1.stargaze-apis.com/'],
|
||||
},
|
||||
},
|
||||
isLazy: true,
|
||||
}}
|
||||
sessionOptions={{
|
||||
duration: 1000 * 60 * 60 * 12, // 12 hours
|
||||
}}
|
||||
signerOptions={{
|
||||
signingCosmwasm: () => ({
|
||||
gasPrice: GasPrice.fromString(`${gasPrice}${feeToken}`),
|
||||
}),
|
||||
signingStargate: () => ({
|
||||
gasPrice: GasPrice.fromString(`${gasPrice}${feeToken}`),
|
||||
}),
|
||||
}}
|
||||
wallets={[...keplrExtensionWallets, ...leapExtensionWallets]}
|
||||
>
|
||||
{children}
|
||||
</ChainProvider>
|
||||
)
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
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'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
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.isWalletConnected) throw new Error('Wallet not connected')
|
||||
await (
|
||||
await wallet.getCosmWasmClient()
|
||||
)
|
||||
.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,106 +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 'utils/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.isWalletConnected) throw new Error('Wallet not connected')
|
||||
await (
|
||||
await wallet.getCosmWasmClient()
|
||||
)
|
||||
.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: ', 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])
|
||||
}
|
||||
@ -119,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,639 +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 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 { useWallet } from 'utils/wallet'
|
||||
|
||||
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.isWalletConnected) {
|
||||
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 (type === 'edit_badge') {
|
||||
const feeRateRaw = await (
|
||||
await wallet.getCosmWasmClient()
|
||||
).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(
|
||||
(
|
||||
await wallet.getCosmWasmClient()
|
||||
).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,382 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
|
||||
/* 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 { useGlobalSettings } from 'contexts/globalSettings'
|
||||
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 { useWallet } from 'utils/wallet'
|
||||
|
||||
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, uploadMethod }: BadgeDetailsProps) => {
|
||||
const { address = '', isWalletConnected, getCosmWasmClient } = useWallet()
|
||||
const { timezone } = useGlobalSettings()
|
||||
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: 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.replaceAll('#', ''), {
|
||||
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(() => {
|
||||
animationUrlState.onChange('')
|
||||
}, [uploadMethod])
|
||||
|
||||
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 (isWalletConnected) {
|
||||
const feeRateRaw = await (
|
||||
await getCosmWasmClient()
|
||||
).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()
|
||||
}, [isWalletConnected, getCosmWasmClient])
|
||||
|
||||
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} />
|
||||
{uploadMethod === 'existing' ? <TextInput className="mt-2" {...animationUrlState} /> : null}
|
||||
<TextInput className="mt-2" {...externalUrlState} />
|
||||
|
||||
<FormControl
|
||||
className="mt-2"
|
||||
htmlId="expiry-date"
|
||||
subtitle={`Badge minting expiry date ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
title="Expiry Date"
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
setTimestamp(
|
||||
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? timestamp
|
||||
: timestamp
|
||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</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,352 +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 { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
|
||||
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 [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
|
||||
|
||||
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.replaceAll('#', ''), { 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])
|
||||
|
||||
useEffect(() => {
|
||||
if (useDefaultApiKey) {
|
||||
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
|
||||
} else {
|
||||
nftStorageApiKeyState.onChange('')
|
||||
}
|
||||
}, [useDefaultApiKey])
|
||||
|
||||
const videoPreview = useMemo(
|
||||
() => (
|
||||
<video
|
||||
className="ml-4"
|
||||
controls
|
||||
id="video"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={
|
||||
imageUrlState.value ? imageUrlState.value.replace('ipfs://', 'https://ipfs-gw.stargaze-apis.com/ipfs/') : ''
|
||||
}
|
||||
/>
|
||||
),
|
||||
[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 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 !== ''}>
|
||||
{getAssetType(imageUrlState.value) === 'image' && (
|
||||
<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>
|
||||
)}
|
||||
{getAssetType(imageUrlState.value) === 'video' && videoPreview}
|
||||
</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'}>
|
||||
<div className="flex-col w-full">
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
|
||||
<div className="flex-row mt-2 w-full form-control">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="mr-2 font-bold">Use Default API Key</span>
|
||||
<input
|
||||
checked={useDefaultApiKey}
|
||||
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
||||
onClick={() => {
|
||||
setUseDefaultApiKey(!useDefaultApiKey)
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</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/*, 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',
|
||||
)}
|
||||
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,113 +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 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 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. Please connect your wallet first.')
|
||||
}
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,5 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import { AirdropUpload } from 'components/AirdropUpload'
|
||||
import { Alert } from 'components/Alert'
|
||||
import { Button } from 'components/Button'
|
||||
import type { DispatchExecuteArgs } from 'components/collections/actions/actions'
|
||||
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/collections/actions/actions'
|
||||
@ -16,72 +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 { useGlobalSettings } from 'contexts/globalSettings'
|
||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
|
||||
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
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 { ROYALTY_REGISTRY_ADDRESS } from 'utils/constants'
|
||||
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
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
|
||||
royaltyRegistryMessages: RoyaltyRegistryInstance | undefined
|
||||
minterType: MinterType
|
||||
sg721Type: Sg721Type
|
||||
minterMessages: MinterInstance | undefined
|
||||
}
|
||||
|
||||
type ExplicitContentType = true | false | undefined
|
||||
|
||||
export const CollectionActions = ({
|
||||
sg721ContractAddress,
|
||||
sg721Messages,
|
||||
minterContractAddress,
|
||||
vendingMinterMessages,
|
||||
baseMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
royaltyRegistryMessages,
|
||||
minterType,
|
||||
sg721Type,
|
||||
minterMessages,
|
||||
}: CollectionActionsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
const { timezone } = useGlobalSettings()
|
||||
|
||||
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 [decrement, setDecrement] = 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',
|
||||
@ -116,13 +84,6 @@ export const CollectionActions = ({
|
||||
subtitle: 'Address of the recipient',
|
||||
})
|
||||
|
||||
const creatorState = useInputState({
|
||||
id: 'creator-address',
|
||||
name: 'creator',
|
||||
title: 'Creator Address',
|
||||
subtitle: 'Address of the creator',
|
||||
})
|
||||
|
||||
const tokenURIState = useInputState({
|
||||
id: 'token-uri',
|
||||
name: 'tokenURI',
|
||||
@ -146,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' : 'New minting price',
|
||||
})
|
||||
|
||||
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',
|
||||
@ -184,161 +118,49 @@ export const CollectionActions = ({
|
||||
const royaltyShareState = useInputState({
|
||||
id: 'royalty-share',
|
||||
name: 'royaltyShare',
|
||||
title: type !== 'update_royalties_for_infinity_swap' ? 'Share Percentage' : 'Share Delta',
|
||||
subtitle:
|
||||
type !== 'update_royalties_for_infinity_swap'
|
||||
? 'Percentage of royalties to be paid'
|
||||
: 'Change in share percentage',
|
||||
placeholder: isEitherType(type, ['set_royalties_for_infinity_swap', 'update_royalties_for_infinity_swap'])
|
||||
? '0.5%'
|
||||
: '5%',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
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',
|
||||
'batch_transfer_multi_address',
|
||||
])
|
||||
const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
|
||||
const showDescriptionField = type === 'update_collection_info'
|
||||
const showCreatorField = type === 'update_collection_info'
|
||||
const showImageField = type === 'update_collection_info'
|
||||
const showExternalLinkField = type === 'update_collection_info'
|
||||
const showRoyaltyRelatedFields =
|
||||
type === 'update_collection_info' ||
|
||||
type === 'set_royalties_for_infinity_swap' ||
|
||||
type === 'update_royalties_for_infinity_swap'
|
||||
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,
|
||||
royaltyRegistryContract: ROYALTY_REGISTRY_ADDRESS,
|
||||
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,
|
||||
royaltyRegistryMessages,
|
||||
recipient: resolvedRecipientAddress,
|
||||
recipient: recipientState.value,
|
||||
recipients: airdropArray,
|
||||
tokenRecipients: airdropAllocationArray,
|
||||
txSigner: wallet.address || '',
|
||||
type,
|
||||
price: priceState.value.toString(),
|
||||
txSigner: wallet.address,
|
||||
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,
|
||||
decrement,
|
||||
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.replaceAll('\\n', '\n') || 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])
|
||||
|
||||
const resolveCreatorAddress = async () => {
|
||||
await resolveAddress(creatorState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
creatorState.onChange(resolvedAddress)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void resolveCreatorAddress()
|
||||
}, [creatorState.value])
|
||||
|
||||
useEffect(() => {
|
||||
setCollectionInfo({
|
||||
description: descriptionState.value.replaceAll('\\n', '\n') || 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,
|
||||
creator: creatorState.value || undefined,
|
||||
})
|
||||
}, [
|
||||
descriptionState.value,
|
||||
imageState.value,
|
||||
explicitContent,
|
||||
externalLinkState.value,
|
||||
royaltyPaymentAddressState.value,
|
||||
royaltyShareState.value,
|
||||
creatorState.value,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (isEitherType(type, ['set_royalties_for_infinity_swap']) && Number(royaltyShareState.value) > 5) {
|
||||
royaltyShareState.onChange('5')
|
||||
toast.error('Royalty share cannot be greater than 5% for Infinity Swap')
|
||||
}
|
||||
}, [royaltyShareState.value])
|
||||
|
||||
useEffect(() => {
|
||||
const addresses: string[] = []
|
||||
@ -358,90 +180,27 @@ export const CollectionActions = ({
|
||||
const { isLoading, mutate } = useMutation(
|
||||
async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!wallet.isWalletConnected) {
|
||||
throw new Error('Please connect your wallet first.')
|
||||
}
|
||||
if (!type) {
|
||||
throw new Error('Please select an action.')
|
||||
throw new Error('Please select an action!')
|
||||
}
|
||||
if (minterContractAddress === '' && sg721ContractAddress === '') {
|
||||
throw new Error('Please enter minter and sg721 contract addresses!')
|
||||
}
|
||||
|
||||
if (wallet.isWalletConnected && type === 'update_mint_price') {
|
||||
const contractConfig = (await wallet.getCosmWasmClient()).queryContractSmart(minterContractAddress, {
|
||||
config: {},
|
||||
})
|
||||
await toast
|
||||
.promise(
|
||||
(
|
||||
await wallet.getCosmWasmClient()
|
||||
).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 (
|
||||
await wallet.getCosmWasmClient()
|
||||
).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')
|
||||
) {
|
||||
const contractInfoResponse = await (await wallet.getCosmWasmClient())
|
||||
.queryContractRaw(
|
||||
const contractInfoResponse = await wallet.client
|
||||
?.queryContractRaw(
|
||||
royaltyPaymentAddressState.value.trim(),
|
||||
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
@ -459,30 +218,10 @@ export const CollectionActions = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'update_collection_info' && creatorState.value) {
|
||||
const resolvedCreatorAddress = await resolveAddress(creatorState.value.trim(), wallet)
|
||||
const contractInfoResponse = await (await wallet.getCosmWasmClient())
|
||||
.queryContractRaw(
|
||||
resolvedCreatorAddress,
|
||||
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 creator 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('dao'))
|
||||
throw new Error('The provided creator address does not belong to a compatible contract.')
|
||||
else console.log(contractInfo)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -498,226 +237,37 @@ export const CollectionActions = ({
|
||||
|
||||
const airdropFileOnChange = (data: AirdropAllocation[]) => {
|
||||
setAirdropAllocationArray(data)
|
||||
}
|
||||
|
||||
const downloadSampleAirdropTokensFile = () => {
|
||||
const csvData =
|
||||
'address,amount\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,3\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,1\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,2'
|
||||
const blob = new Blob([csvData], { type: 'text/csv' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', 'airdrop_tokens.csv')
|
||||
a.click()
|
||||
}
|
||||
|
||||
const downloadSampleAirdropSpecificTokensFile = () => {
|
||||
const csvData =
|
||||
'address,tokenId\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,214\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,683\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,102'
|
||||
const blob = new Blob([csvData], { type: 'text/csv' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', 'airdrop_specific_tokens.csv')
|
||||
a.click()
|
||||
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} />}
|
||||
{showCreatorField && <AddressInput className="mt-2" {...creatorState} />}
|
||||
{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} />
|
||||
<Conditional test={type === 'update_royalties_for_infinity_swap'}>
|
||||
<div className="flex flex-row space-y-2 w-1/4">
|
||||
<div className={clsx('flex flex-col space-y-2 w-full form-control')}>
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<div className="flex flex-col">
|
||||
<span className="mr-4 font-bold">Increment</span>
|
||||
</div>
|
||||
<input
|
||||
checked={decrement}
|
||||
className={`toggle ${decrement ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setDecrement(!decrement)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<span className="mx-4 font-bold">Decrement</span>
|
||||
</div>
|
||||
</Conditional>
|
||||
</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 && (
|
||||
<div>
|
||||
<FormGroup
|
||||
subtitle={`CSV file that contains the ${
|
||||
type === 'batch_transfer_multi_address' ? '' : 'airdrop'
|
||||
} addresses and the ${
|
||||
type === 'airdrop' || type === 'airdrop_open_edition' ? 'amount of tokens' : 'token ID'
|
||||
} allocated for each address. Should start with the following header row: ${
|
||||
type === 'airdrop' || type === 'airdrop_open_edition' ? 'address,amount' : 'address,tokenId'
|
||||
}`}
|
||||
title={`${type === 'batch_transfer_multi_address' ? 'Multi-Recipient Transfer File' : 'Airdrop File'}`}
|
||||
>
|
||||
<AirdropUpload onChange={airdropFileOnChange} />
|
||||
</FormGroup>
|
||||
<Button
|
||||
className="ml-4 text-sm"
|
||||
onClick={
|
||||
type === 'airdrop' || type === 'airdrop_open_edition'
|
||||
? downloadSampleAirdropTokensFile
|
||||
: downloadSampleAirdropSpecificTokensFile
|
||||
}
|
||||
>
|
||||
Download Sample File
|
||||
</Button>
|
||||
</div>
|
||||
<FormGroup
|
||||
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 ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
setTimestamp(
|
||||
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? timestamp
|
||||
: timestamp
|
||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<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 ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
setEndTimestamp(
|
||||
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? endTimestamp
|
||||
: endTimestamp
|
||||
? new Date(endTimestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
<Conditional test={type === 'update_collection_info'}>
|
||||
<Alert className="mt-2 text-sm" type="info">
|
||||
Please note that you are only required to fill in the fields you want to update.
|
||||
</Alert>
|
||||
</Conditional>
|
||||
<Conditional test={type === 'update_discount_price'}>
|
||||
<Alert className="mt-2 text-sm" type="warning">
|
||||
Please note that discount price can only be updated every 24 hours and be removed 12 hours after its last
|
||||
update.
|
||||
</Alert>
|
||||
</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,54 +1,28 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
import { useBaseMinterContract } from 'contracts/baseMinter'
|
||||
import { useOpenEditionMinterContract } from 'contracts/openEditionMinter'
|
||||
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
|
||||
import { useRoyaltyRegistryContract } from 'contracts/royaltyRegistry'
|
||||
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 { INFINITY_SWAP_PROTOCOL_ADDRESS } from 'utils/constants'
|
||||
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',
|
||||
'set_royalties_for_infinity_swap',
|
||||
'update_royalties_for_infinity_swap',
|
||||
'withdraw',
|
||||
'transfer',
|
||||
'batch_transfer',
|
||||
'batch_transfer_multi_address',
|
||||
'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 {
|
||||
@ -57,99 +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: 'set_royalties_for_infinity_swap',
|
||||
name: 'Set Royalty Details for Infinity Swap',
|
||||
description: `Set royalty details for Infinity Swap`,
|
||||
},
|
||||
{
|
||||
id: 'update_royalties_for_infinity_swap',
|
||||
name: 'Update Royalty Details for Infinity Swap',
|
||||
description: `Update royalty details for Infinity Swap`,
|
||||
},
|
||||
{
|
||||
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: 'batch_transfer_multi_address',
|
||||
name: 'Transfer Tokens to Multiple Recipients',
|
||||
description: `Transfer a list of tokens to multiple addresses`,
|
||||
},
|
||||
{
|
||||
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',
|
||||
@ -157,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',
|
||||
@ -172,24 +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: 'set_royalties_for_infinity_swap',
|
||||
name: 'Set Royalty Details for Infinity Swap',
|
||||
description: `Set royalty details for Infinity Swap`,
|
||||
id: 'freeze_token_metadata',
|
||||
name: 'Freeze Token Metadata',
|
||||
description: `Render the metadata for tokens no longer updatable`,
|
||||
},
|
||||
{
|
||||
id: 'update_royalties_for_infinity_swap',
|
||||
name: 'Update Royalty Details for Infinity Swap',
|
||||
description: `Update royalty details for Infinity Swap`,
|
||||
id: 'withdraw',
|
||||
name: 'Withdraw Tokens',
|
||||
description: `Withdraw tokens from the contract`,
|
||||
},
|
||||
{
|
||||
id: 'transfer',
|
||||
@ -201,11 +97,6 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Batch Transfer Tokens',
|
||||
description: `Transfer a list of tokens to a recipient`,
|
||||
},
|
||||
{
|
||||
id: 'batch_transfer_multi_address',
|
||||
name: 'Transfer Tokens to Multiple Recipients',
|
||||
description: `Transfer a list of tokens to multiple addresses`,
|
||||
},
|
||||
{
|
||||
id: 'burn',
|
||||
name: 'Burn Token',
|
||||
@ -226,127 +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: 'set_royalties_for_infinity_swap',
|
||||
name: 'Set Royalty Details for Infinity Swap',
|
||||
description: `Set royalty details for Infinity Swap`,
|
||||
},
|
||||
{
|
||||
id: 'update_royalties_for_infinity_swap',
|
||||
name: 'Update Royalty Details for Infinity Swap',
|
||||
description: `Update royalty details for Infinity Swap`,
|
||||
},
|
||||
{
|
||||
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: 'batch_transfer_multi_address',
|
||||
name: 'Transfer Tokens to Multiple Recipients',
|
||||
description: `Transfer a list of tokens to multiple addresses`,
|
||||
},
|
||||
{
|
||||
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 {
|
||||
@ -354,134 +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
|
||||
royaltyRegistryContract: string
|
||||
vendingMinterMessages?: VendingMinterInstance
|
||||
baseMinterMessages?: BaseMinterInstance
|
||||
openEditionMinterMessages?: OpenEditionMinterInstance
|
||||
minterMessages?: MinterInstance
|
||||
sg721Messages?: SG721Instance
|
||||
royaltyRegistryMessages?: RoyaltyRegistryInstance
|
||||
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
|
||||
decrement: 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,
|
||||
royaltyRegistryMessages,
|
||||
txSigner,
|
||||
} = args
|
||||
if (
|
||||
!vendingMinterMessages ||
|
||||
!baseMinterMessages ||
|
||||
!openEditionMinterMessages ||
|
||||
!sg721Messages ||
|
||||
!royaltyRegistryMessages
|
||||
) {
|
||||
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 'set_royalties_for_infinity_swap': {
|
||||
return royaltyRegistryMessages.setCollectionRoyaltyProtocol(
|
||||
args.sg721Contract,
|
||||
INFINITY_SWAP_PROTOCOL_ADDRESS,
|
||||
args.collectionInfo?.royalty_info?.payment_address as string,
|
||||
Number(args.collectionInfo?.royalty_info?.share) * 100,
|
||||
)
|
||||
}
|
||||
case 'update_royalties_for_infinity_swap': {
|
||||
return royaltyRegistryMessages.updateCollectionRoyaltyProtocol(
|
||||
args.sg721Contract,
|
||||
INFINITY_SWAP_PROTOCOL_ADDRESS,
|
||||
args.collectionInfo?.royalty_info?.payment_address as string,
|
||||
Number(args.collectionInfo?.royalty_info?.share) * 100,
|
||||
args.decrement,
|
||||
)
|
||||
case 'withdraw': {
|
||||
return minterMessages.withdraw(txSigner)
|
||||
}
|
||||
case 'transfer': {
|
||||
return sg721Messages.transferNft(args.recipient, args.tokenId.toString())
|
||||
@ -489,29 +190,26 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
case 'batch_transfer': {
|
||||
return sg721Messages.batchTransfer(args.recipient, args.tokenIds)
|
||||
}
|
||||
case 'batch_transfer_multi_address': {
|
||||
return sg721Messages.batchTransferMultiAddress(txSigner, args.tokenRecipients)
|
||||
}
|
||||
case 'burn': {
|
||||
return sg721Messages.burn(args.tokenId.toString())
|
||||
}
|
||||
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')
|
||||
@ -521,127 +219,61 @@ 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()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: royaltyRegistryMessages } = useRoyaltyRegistryContract()
|
||||
|
||||
const { minterContract, sg721Contract, royaltyRegistryContract } = args
|
||||
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 'set_royalties_for_infinity_swap': {
|
||||
return royaltyRegistryMessages(royaltyRegistryContract)?.setCollectionRoyaltyProtocol(
|
||||
args.sg721Contract,
|
||||
INFINITY_SWAP_PROTOCOL_ADDRESS,
|
||||
args.collectionInfo?.royalty_info?.payment_address as string,
|
||||
Number(args.collectionInfo?.royalty_info?.share) * 100,
|
||||
)
|
||||
}
|
||||
case 'update_royalties_for_infinity_swap': {
|
||||
return royaltyRegistryMessages(royaltyRegistryContract)?.updateCollectionRoyaltyProtocol(
|
||||
args.sg721Contract,
|
||||
INFINITY_SWAP_PROTOCOL_ADDRESS,
|
||||
args.collectionInfo?.royalty_info?.payment_address as string,
|
||||
Number(args.collectionInfo?.royalty_info?.share) * 100,
|
||||
args.decrement,
|
||||
)
|
||||
}
|
||||
case 'transfer': {
|
||||
return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString())
|
||||
}
|
||||
case 'batch_transfer': {
|
||||
return sg721Messages(sg721Contract)?.batchTransfer(args.recipient, args.tokenIds)
|
||||
}
|
||||
case 'batch_transfer_multi_address': {
|
||||
return sg721Messages(sg721Contract)?.batchTransferMultiAddress(args.tokenRecipients)
|
||||
}
|
||||
case 'burn': {
|
||||
return sg721Messages(sg721Contract)?.burn(args.tokenId.toString())
|
||||
}
|
||||
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,301 +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 React, { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { API_URL } from 'utils/constants'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
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
|
||||
importedBaseMinterDetails?: BaseMinterDetailsDataProps
|
||||
}
|
||||
|
||||
export interface BaseMinterDetailsDataProps {
|
||||
baseMinterAcquisitionMethod: BaseMinterAcquisitionMethod
|
||||
existingBaseMinter: string | undefined
|
||||
selectedCollectionAddress: string | undefined
|
||||
collectionTokenCount: number | undefined
|
||||
}
|
||||
|
||||
export const BaseMinterDetails = ({ onChange, minterType, importedBaseMinterDetails }: 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.isWalletConnected && minterContractAddress.length > 0) {
|
||||
const client = await wallet.getCosmWasmClient()
|
||||
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 (
|
||||
await wallet.getCosmWasmClient()
|
||||
)
|
||||
.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 (
|
||||
await wallet.getCosmWasmClient()
|
||||
)
|
||||
.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.isWalletConnected) {
|
||||
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.isWalletConnected,
|
||||
selectedCollectionAddress,
|
||||
collectionTokenCount,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedBaseMinterDetails) {
|
||||
setBaseMinterAcquisitionMethod(importedBaseMinterDetails.baseMinterAcquisitionMethod)
|
||||
existingBaseMinterState.onChange(
|
||||
importedBaseMinterDetails.existingBaseMinter ? importedBaseMinterDetails.existingBaseMinter : '',
|
||||
)
|
||||
}
|
||||
}, [importedBaseMinterDetails])
|
||||
|
||||
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.isWalletConnected}>
|
||||
<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.isWalletConnected}>
|
||||
<Alert className="my-2 w-[90%]" type="warning">
|
||||
Please connect your wallet first.
|
||||
</Alert>
|
||||
</Conditional>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,34 +1,23 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* 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 { useGlobalSettings } from 'contexts/globalSettings'
|
||||
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
|
||||
importedCollectionDetails?: CollectionDetailsDataProps
|
||||
}
|
||||
|
||||
export interface CollectionDetailsDataProps {
|
||||
@ -37,25 +26,10 @@ export interface CollectionDetailsDataProps {
|
||||
symbol: string
|
||||
imageFile: File[]
|
||||
externalLink?: string
|
||||
startTradingTime?: string
|
||||
explicit: boolean
|
||||
updatable: boolean
|
||||
}
|
||||
|
||||
export const CollectionDetails = ({
|
||||
onChange,
|
||||
uploadMethod,
|
||||
coverImageUrl,
|
||||
minterType,
|
||||
importedCollectionDetails,
|
||||
}: 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 { timezone } = useGlobalSettings()
|
||||
|
||||
const initialRender = useRef(true)
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
@ -92,45 +66,15 @@ export const CollectionDetails = ({
|
||||
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,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedCollectionDetails) {
|
||||
nameState.onChange(importedCollectionDetails.name)
|
||||
descriptionState.onChange(importedCollectionDetails.description)
|
||||
symbolState.onChange(importedCollectionDetails.symbol)
|
||||
externalLinkState.onChange(importedCollectionDetails.externalLink || '')
|
||||
setTimestamp(
|
||||
importedCollectionDetails.startTradingTime
|
||||
? new Date(parseInt(importedCollectionDetails.startTradingTime) / 1_000_000)
|
||||
: undefined,
|
||||
)
|
||||
setExplicit(importedCollectionDetails.explicit)
|
||||
setUpdatable(importedCollectionDetails.updatable)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importedCollectionDetails])
|
||||
}, [nameState.value, descriptionState.value, coverImage, externalLinkState.value])
|
||||
|
||||
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files === null) return toast.error('Error selecting cover image')
|
||||
@ -142,166 +86,24 @@ export const CollectionDetails = ({
|
||||
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.replaceAll('#', ''), {
|
||||
type: 'image/jpg',
|
||||
})
|
||||
const imageFile = new File([e.target.result], event.target.files[0].name, { type: 'image/jpg' })
|
||||
setCoverImage(imageFile)
|
||||
}
|
||||
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 | ${timezone === 'Local' ? 'local)' : 'UTC)'}`}
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local'
|
||||
? new Date()
|
||||
: new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setTimestamp(
|
||||
timezone === 'Local'
|
||||
? date
|
||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setTimestamp(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? timestamp
|
||||
: timestamp
|
||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</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={false && 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">
|
||||
<div className="flex flex-col">
|
||||
<span className="mr-4 font-bold">Updatable Token Metadata</span>
|
||||
<span className="mr-4">(Price: 2000 STARS)</span>
|
||||
</div>
|
||||
<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',
|
||||
)}
|
||||
@ -320,9 +122,7 @@ export const CollectionDetails = ({
|
||||
<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>
|
||||
)}
|
||||
@ -335,88 +135,8 @@ export const CollectionDetails = ({
|
||||
<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={false && 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-full form-control',
|
||||
)}
|
||||
>
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<div className="flex flex-col">
|
||||
<span className="mr-4 font-bold">Updatable Token Metadata</span>
|
||||
<span className="mr-4">(Price: 2000 STARS)</span>
|
||||
</div>
|
||||
<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,30 +1,16 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* 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 { useGlobalSettings } from 'contexts/globalSettings'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
import { useWallet } from 'utils/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
|
||||
importedMintingDetails?: MintingDetailsDataProps
|
||||
isPresale: boolean
|
||||
whitelistStartDate?: string
|
||||
}
|
||||
|
||||
export interface MintingDetailsDataProps {
|
||||
@ -32,24 +18,10 @@ export interface MintingDetailsDataProps {
|
||||
unitPrice: string
|
||||
perAddressLimit: number
|
||||
startTime: string
|
||||
paymentAddress?: string
|
||||
selectedMintToken?: TokenInfo
|
||||
}
|
||||
|
||||
export const MintingDetails = ({
|
||||
onChange,
|
||||
numberOfTokens,
|
||||
uploadMethod,
|
||||
minimumMintPrice,
|
||||
mintingTokenFromFactory,
|
||||
importedMintingDetails,
|
||||
isPresale,
|
||||
whitelistStartDate,
|
||||
}: MintingDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const { timezone } = useGlobalSettings()
|
||||
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',
|
||||
@ -63,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',
|
||||
})
|
||||
|
||||
@ -77,68 +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,
|
||||
}
|
||||
console.log('Timestamp:', timestamp?.getTime())
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
numberOfTokens,
|
||||
numberOfTokensState.value,
|
||||
unitPriceState.value,
|
||||
perAddressLimitState.value,
|
||||
timestamp,
|
||||
paymentAddressState.value,
|
||||
selectedMintToken,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedMintingDetails) {
|
||||
numberOfTokensState.onChange(importedMintingDetails.numTokens)
|
||||
unitPriceState.onChange(Number(importedMintingDetails.unitPrice) / 1_000_000)
|
||||
perAddressLimitState.onChange(importedMintingDetails.perAddressLimit)
|
||||
setTimestamp(new Date(Number(importedMintingDetails.startTime) / 1_000_000))
|
||||
paymentAddressState.onChange(importedMintingDetails.paymentAddress ? importedMintingDetails.paymentAddress : '')
|
||||
setSelectedMintToken(tokensList.find((token) => token.id === importedMintingDetails.selectedMintToken?.id))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importedMintingDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (isPresale) {
|
||||
setTimestamp(whitelistStartDate ? new Date(Number(whitelistStartDate) / 1_000_000) : undefined)
|
||||
}
|
||||
}, [whitelistStartDate, isPresale])
|
||||
}, [numberOfTokens, numberOfTokensState.value, unitPriceState.value, perAddressLimitState.value, timestamp])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -149,58 +68,12 @@ export const MintingDetails = ({
|
||||
isRequired
|
||||
value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value}
|
||||
/>
|
||||
<div className="flex flex-row items-end">
|
||||
<NumberInput {...unitPriceState} isRequired />
|
||||
<select
|
||||
className="py-[9px] px-4 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))}
|
||||
value={selectedMintToken?.displayName}
|
||||
>
|
||||
{vendingMinterList
|
||||
.filter(
|
||||
(minter) =>
|
||||
minter.factoryAddress !== undefined && minter.updatable === false && minter.featured === 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 ${isPresale ? '(is dictated by whitelist start time)' : ''} ${
|
||||
timezone === 'Local' ? '(local)' : '(UTC)'
|
||||
}`}
|
||||
title="Start Time"
|
||||
>
|
||||
<InputDateTime
|
||||
disabled={isPresale}
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setTimestamp(
|
||||
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setTimestamp(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? timestamp
|
||||
: timestamp
|
||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,14 +2,11 @@ import { Conditional } from 'components/Conditional'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
import { resolveAddress } from '../../../utils/resolveAddress'
|
||||
import { NumberInput, TextInput } from '../../forms/FormInput'
|
||||
|
||||
interface RoyaltyDetailsProps {
|
||||
onChange: (data: RoyaltyDetailsDataProps) => void
|
||||
importedRoyaltyDetails?: RoyaltyDetailsDataProps
|
||||
}
|
||||
|
||||
export interface RoyaltyDetailsDataProps {
|
||||
@ -20,8 +17,7 @@ export interface RoyaltyDetailsDataProps {
|
||||
|
||||
type RoyaltyState = 'none' | 'new'
|
||||
|
||||
export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
||||
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
|
||||
|
||||
const royaltyPaymentAddressState = useInputState({
|
||||
@ -37,39 +33,19 @@ export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDeta
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedRoyaltyDetails) {
|
||||
setRoyaltyState(importedRoyaltyDetails.royaltyType)
|
||||
royaltyPaymentAddressState.onChange(importedRoyaltyDetails.paymentAddress)
|
||||
royaltyShareState.onChange(importedRoyaltyDetails.share.toString())
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importedRoyaltyDetails])
|
||||
|
||||
return (
|
||||
<div className="py-3 px-8 rounded border-2 border-white/20">
|
||||
<div className="flex justify-center">
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable array-callback-return */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
// 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'
|
||||
@ -11,38 +7,22 @@ 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 { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
|
||||
import type { AssetType } from 'utils/getAssetType'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
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
|
||||
importedUploadDetails?: UploadDetailsDataProps
|
||||
}
|
||||
|
||||
export interface UploadDetailsDataProps {
|
||||
assetFiles: File[]
|
||||
metadataFiles: File[]
|
||||
thumbnailFiles?: File[]
|
||||
thumbnailCompatibleAssetFileNames?: string[]
|
||||
uploadService: UploadServiceType
|
||||
nftStorageApiKey?: string
|
||||
pinataApiKey?: string
|
||||
@ -50,30 +30,15 @@ export interface UploadDetailsDataProps {
|
||||
uploadMethod: UploadMethod
|
||||
baseTokenURI?: string
|
||||
imageUrl?: string
|
||||
baseMinterMetadataFile?: File
|
||||
}
|
||||
|
||||
export const UploadDetails = ({
|
||||
onChange,
|
||||
minterType,
|
||||
baseMinterAcquisitionMethod,
|
||||
importedUploadDetails,
|
||||
}: UploadDetailsProps) => {
|
||||
export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
|
||||
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
|
||||
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
|
||||
const [thumbnailCompatibleAssetFileNames, setThumbnailCompatibleAssetFileNames] = useState<string[]>([])
|
||||
const [thumbnailFilesArray, setThumbnailFilesArray] = 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 [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
|
||||
|
||||
const [baseMinterMetadataFile, setBaseMinterMetadataFile] = useState<File | undefined>()
|
||||
|
||||
const assetFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
const thumbnailFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nftStorageApiKeyState = useInputState({
|
||||
id: 'nft-storage-api-key',
|
||||
@ -100,7 +65,7 @@ export const UploadDetails = ({
|
||||
const baseTokenUriState = useInputState({
|
||||
id: 'baseTokenUri',
|
||||
name: 'baseTokenUri',
|
||||
title: minterType === 'vending' ? 'Base Token URI' : 'Token URI',
|
||||
title: 'Base Token URI',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
@ -116,78 +81,19 @@ export const UploadDetails = ({
|
||||
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setAssetFilesArray([])
|
||||
setMetadataFilesArray([])
|
||||
setThumbnailFilesArray([])
|
||||
setThumbnailCompatibleAssetFileNames([])
|
||||
if (event.target.files === null) return
|
||||
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html', 'document']
|
||||
const thumbnailCompatibleFileNamesList: string[] = []
|
||||
if (minterType === 'vending') {
|
||||
//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])
|
||||
sortedFiles.map((file) => {
|
||||
if (thumbnailCompatibleAssetTypes.includes(getAssetType(file.name))) {
|
||||
thumbnailCompatibleFileNamesList.push(file.name.split('.')[0])
|
||||
}
|
||||
})
|
||||
setThumbnailCompatibleAssetFileNames(thumbnailCompatibleFileNamesList)
|
||||
console.log('Thumbnail Compatible Files: ', thumbnailCompatibleFileNamesList)
|
||||
for (let i = 0; i < sortedFileNames.length; i++) {
|
||||
if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) {
|
||||
toast.error('The file names should be in numerical order starting from 1.')
|
||||
setThumbnailCompatibleAssetFileNames([])
|
||||
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
|
||||
}
|
||||
} else if (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])
|
||||
sortedFiles.map((file) => {
|
||||
if (thumbnailCompatibleAssetTypes.includes(getAssetType(file.name))) {
|
||||
thumbnailCompatibleFileNamesList.push(file.name.split('.')[0])
|
||||
}
|
||||
})
|
||||
setThumbnailCompatibleAssetFileNames(thumbnailCompatibleFileNamesList)
|
||||
console.log('Thumbnail Compatible Files: ', thumbnailCompatibleFileNamesList)
|
||||
|
||||
for (let i = 0; i < sortedFileNames.length - 1; i++) {
|
||||
if (
|
||||
isNaN(Number(sortedFileNames[i])) ||
|
||||
isNaN(Number(sortedFileNames[i + 1])) ||
|
||||
parseInt(sortedFileNames[i]) !== parseInt(sortedFileNames[i + 1]) - 1
|
||||
) {
|
||||
toast.error('The file names should be in numerical order.')
|
||||
setThumbnailCompatibleAssetFileNames([])
|
||||
addLogItem({
|
||||
id: uid(),
|
||||
message: 'The file names should be in numerical order.',
|
||||
type: 'Error',
|
||||
timestamp: new Date(),
|
||||
})
|
||||
//clear the input
|
||||
event.target.value = ''
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if (minterType === 'base' && event.target.files.length === 1) {
|
||||
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
|
||||
thumbnailCompatibleFileNamesList.push(event.target.files[0].name.split('.')[0])
|
||||
}
|
||||
setThumbnailCompatibleAssetFileNames(thumbnailCompatibleFileNamesList)
|
||||
console.log('Thumbnail Compatible Files: ', thumbnailCompatibleFileNamesList)
|
||||
}
|
||||
|
||||
let loadedFileCount = 0
|
||||
const files: File[] = []
|
||||
let reader: FileReader
|
||||
@ -196,9 +102,7 @@ export const UploadDetails = ({
|
||||
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.replaceAll('#', ''), {
|
||||
type: 'image/jpg',
|
||||
})
|
||||
const assetFile = new File([e.target.result], event.target.files[i].name, { type: 'image/jpg' })
|
||||
files.push(assetFile)
|
||||
}
|
||||
reader.readAsArrayBuffer(event.target.files[i])
|
||||
@ -215,68 +119,20 @@ export const UploadDetails = ({
|
||||
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.')
|
||||
}
|
||||
// compare the first file name for asset and metadata files
|
||||
if (
|
||||
minterType === 'base' &&
|
||||
assetFilesArray.length > 1 &&
|
||||
event.target.files[0].name.split('.')[0] !== assetFilesArray[0].name.split('.')[0]
|
||||
) {
|
||||
event.target.value = ''
|
||||
toast.error('The metadata file names should match the asset file names.')
|
||||
addLogItem({
|
||||
id: uid(),
|
||||
message: 'The metadata file names should match the asset file names.',
|
||||
type: 'Error',
|
||||
timestamp: new Date(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (minterType === 'vending') {
|
||||
//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
|
||||
}
|
||||
}
|
||||
} else if (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 - 1; i++) {
|
||||
if (
|
||||
isNaN(Number(sortedFileNames[i])) ||
|
||||
isNaN(Number(sortedFileNames[i + 1])) ||
|
||||
parseInt(sortedFileNames[i]) !== parseInt(sortedFileNames[i + 1]) - 1
|
||||
) {
|
||||
toast.error('The file names should be in numerical order.')
|
||||
addLogItem({
|
||||
id: uid(),
|
||||
message: 'The file names should be in numerical order.',
|
||||
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
|
||||
@ -284,26 +140,11 @@ export const UploadDetails = ({
|
||||
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.replaceAll('#', ''), {
|
||||
type: 'application/json',
|
||||
})
|
||||
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 = () => {
|
||||
@ -321,64 +162,10 @@ export const UploadDetails = ({
|
||||
setRefreshMetadata((prev) => !prev)
|
||||
}
|
||||
|
||||
const updateMetadataFileArray = (updatedMetadataFile: File) => {
|
||||
const updateMetadataFileArray = async (updatedMetadataFile: File) => {
|
||||
metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const selectThumbnails = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setThumbnailFilesArray([])
|
||||
if (event.target.files === null) return
|
||||
|
||||
// if (minterType === 'vending' || (minterType === 'base' && thumbnailCompatibleAssetFileNames.length > 1)) {
|
||||
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
|
||||
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
|
||||
// make sure the sorted file names match thumbnail compatible asset file names
|
||||
for (let i = 0; i < thumbnailCompatibleAssetFileNames.length; i++) {
|
||||
if (minterType === 'base' && assetFilesArray.length === 1) break
|
||||
if (sortedFileNames[i] !== thumbnailCompatibleAssetFileNames[i]) {
|
||||
toast.error('The thumbnail file names should match the thumbnail compatible asset file names.')
|
||||
addLogItem({
|
||||
id: uid(),
|
||||
message: 'The thumbnail file names should match the thumbnail compatible asset file names.',
|
||||
type: 'Error',
|
||||
timestamp: new Date(),
|
||||
})
|
||||
//clear the input
|
||||
event.target.value = ''
|
||||
return
|
||||
}
|
||||
}
|
||||
// }
|
||||
let loadedFileCount = 0
|
||||
const files: File[] = []
|
||||
let reader: FileReader
|
||||
for (let i = 0; i < event.target.files.length; i++) {
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
if (!event.target.files) return toast.error('No files selected.')
|
||||
const thumbnailFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), {
|
||||
type: 'image/jpg',
|
||||
})
|
||||
files.push(thumbnailFile)
|
||||
}
|
||||
reader.readAsArrayBuffer(event.target.files[i])
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
loadedFileCount++
|
||||
if (loadedFileCount === event.target.files.length) {
|
||||
setThumbnailFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('Updated Metadata File:')
|
||||
console.log(JSON.parse(await metadataFilesArray[metadataFileArrayIndex]?.text()))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -386,39 +173,21 @@ export const UploadDetails = ({
|
||||
const data: UploadDetailsDataProps = {
|
||||
assetFiles: assetFilesArray,
|
||||
metadataFiles: metadataFilesArray,
|
||||
thumbnailFiles: thumbnailFilesArray,
|
||||
thumbnailCompatibleAssetFileNames,
|
||||
uploadService,
|
||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
||||
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,
|
||||
metadataFilesArray,
|
||||
thumbnailFilesArray,
|
||||
thumbnailCompatibleAssetFileNames,
|
||||
uploadService,
|
||||
nftStorageApiKeyState.value,
|
||||
pinataApiKeyState.value,
|
||||
@ -426,53 +195,17 @@ export const UploadDetails = ({
|
||||
uploadMethod,
|
||||
baseTokenUriState.value,
|
||||
coverImageUrlState.value,
|
||||
refreshMetadata,
|
||||
baseMinterMetadataFile,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
|
||||
setMetadataFilesArray([])
|
||||
if (assetFilesRef.current) assetFilesRef.current.value = ''
|
||||
setAssetFilesArray([])
|
||||
if (thumbnailFilesRef.current) thumbnailFilesRef.current.value = ''
|
||||
setThumbnailFilesArray([])
|
||||
setThumbnailCompatibleAssetFileNames([])
|
||||
if (!importedUploadDetails || minterType === 'base') {
|
||||
baseTokenUriState.onChange('')
|
||||
coverImageUrlState.onChange('')
|
||||
}
|
||||
}, [uploadMethod, minterType, baseMinterAcquisitionMethod])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedUploadDetails) {
|
||||
if (importedUploadDetails.uploadMethod === 'new') {
|
||||
setUploadMethod('new')
|
||||
setUploadService(importedUploadDetails.uploadService)
|
||||
nftStorageApiKeyState.onChange(importedUploadDetails.nftStorageApiKey || '')
|
||||
pinataApiKeyState.onChange(importedUploadDetails.pinataApiKey || '')
|
||||
pinataSecretKeyState.onChange(importedUploadDetails.pinataSecretKey || '')
|
||||
baseTokenUriState.onChange(importedUploadDetails.baseTokenURI || '')
|
||||
coverImageUrlState.onChange(importedUploadDetails.imageUrl || '')
|
||||
} else if (importedUploadDetails.uploadMethod === 'existing') {
|
||||
setUploadMethod('existing')
|
||||
baseTokenUriState.onChange(importedUploadDetails.baseTokenURI || '')
|
||||
coverImageUrlState.onChange(importedUploadDetails.imageUrl || '')
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importedUploadDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (useDefaultApiKey) {
|
||||
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
|
||||
} else {
|
||||
nftStorageApiKeyState.onChange('')
|
||||
}
|
||||
}, [useDefaultApiKey])
|
||||
setMetadataFilesArray([])
|
||||
baseTokenUriState.onChange('')
|
||||
coverImageUrlState.onChange('')
|
||||
}, [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
|
||||
@ -490,7 +223,7 @@ export const UploadDetails = ({
|
||||
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">
|
||||
@ -509,13 +242,13 @@ export const UploadDetails = ({
|
||||
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
|
||||
@ -530,53 +263,11 @@ export const UploadDetails = ({
|
||||
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'}>
|
||||
@ -626,22 +317,7 @@ export const UploadDetails = ({
|
||||
|
||||
<div className="flex w-full">
|
||||
<Conditional test={uploadService === 'nft-storage'}>
|
||||
<div className="flex-col w-full">
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
|
||||
<div className="flex-row mt-2 w-full form-control">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="mr-2 font-bold">Use Default API Key</span>
|
||||
<input
|
||||
checked={useDefaultApiKey}
|
||||
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
||||
onClick={() => {
|
||||
setUseDefaultApiKey(!useDefaultApiKey)
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
<Conditional test={uploadService === 'pinata'}>
|
||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
||||
@ -680,7 +356,7 @@ export const UploadDetails = ({
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="image/*, audio/*, video/*, .html, .pdf"
|
||||
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',
|
||||
@ -688,7 +364,6 @@ export const UploadDetails = ({
|
||||
id="assetFiles"
|
||||
multiple
|
||||
onChange={selectAssets}
|
||||
ref={assetFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
@ -700,11 +375,7 @@ export const UploadDetails = ({
|
||||
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(
|
||||
@ -721,80 +392,24 @@ export const UploadDetails = ({
|
||||
id="metadataFiles"
|
||||
multiple
|
||||
onChange={selectMetadata}
|
||||
ref={metadataFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{thumbnailCompatibleAssetFileNames.length > 0 && (
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="thumbnailFiles"
|
||||
>
|
||||
{thumbnailCompatibleAssetFileNames.length > 1
|
||||
? 'Thumbnail Selection for Compatible Assets (optional)'
|
||||
: 'Thumbnail Selection (optional)'}
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="image/*"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="thumbnailFiles"
|
||||
multiple
|
||||
onChange={selectThumbnails}
|
||||
ref={thumbnailFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Conditional test={assetFilesArray.length >= 1}>
|
||||
<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,20 +1,8 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Button } from 'components/Button'
|
||||
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 { useGlobalSettings } from 'contexts/globalSettings'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
import { Conditional } from '../../Conditional'
|
||||
import { AddressInput, NumberInput } from '../../forms/FormInput'
|
||||
@ -23,44 +11,26 @@ import { WhitelistUpload } from '../../WhitelistUpload'
|
||||
|
||||
interface WhitelistDetailsProps {
|
||||
onChange: (data: WhitelistDetailsDataProps) => void
|
||||
mintingTokenFromFactory?: TokenInfo
|
||||
importedWhitelistDetails?: WhitelistDetailsDataProps
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
export type WhitelistType = 'standard' | 'flex' | 'merkletree'
|
||||
|
||||
export const WhitelistDetails = ({
|
||||
onChange,
|
||||
mintingTokenFromFactory,
|
||||
importedWhitelistDetails,
|
||||
}: WhitelistDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const { timezone } = useGlobalSettings()
|
||||
|
||||
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 [whitelistMerkleTreeArray, setWhitelistMerkleTreeArray] = useState<string[]>([])
|
||||
const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
|
||||
const [whitelistArray, setWhitelistArray] = useState<string[]>([])
|
||||
|
||||
const whitelistAddressState = useInputState({
|
||||
id: 'whitelist-address',
|
||||
@ -69,13 +39,11 @@ export const WhitelistDetails = ({
|
||||
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',
|
||||
})
|
||||
|
||||
@ -95,161 +63,34 @@ export const WhitelistDetails = ({
|
||||
placeholder: '5',
|
||||
})
|
||||
|
||||
const addressListState = useAddressListState()
|
||||
|
||||
const whitelistFileOnChange = (data: string[]) => {
|
||||
if (whitelistType === 'standard') setWhitelistStandardArray(data)
|
||||
if (whitelistType === 'merkletree') setWhitelistMerkleTreeArray(data)
|
||||
setWhitelistArray(data)
|
||||
}
|
||||
|
||||
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
|
||||
setWhitelistFlexArray(whitelistData)
|
||||
}
|
||||
|
||||
const downloadSampleWhitelistFlexFile = () => {
|
||||
const csvData =
|
||||
'address,mint_count\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,3\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,1\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,2'
|
||||
const blob = new Blob([csvData], { type: 'text/csv' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', 'sample_whitelist_flex.csv')
|
||||
a.click()
|
||||
}
|
||||
|
||||
const downloadSampleWhitelistFile = () => {
|
||||
const txtData =
|
||||
'stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3'
|
||||
const blob = new Blob([txtData], { type: 'text/txt' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', 'sample_whitelist.txt')
|
||||
a.click()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!importedWhitelistDetails) {
|
||||
setWhitelistStandardArray([])
|
||||
setWhitelistFlexArray([])
|
||||
setWhitelistMerkleTreeArray([])
|
||||
}
|
||||
}, [whitelistType])
|
||||
|
||||
useEffect(() => {
|
||||
const data: WhitelistDetailsDataProps = {
|
||||
whitelistState,
|
||||
whitelistType,
|
||||
contractAddress: whitelistAddressState.value
|
||||
.toLowerCase()
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, ''),
|
||||
members:
|
||||
whitelistType === 'standard'
|
||||
? whitelistStandardArray
|
||||
: whitelistType === 'merkletree'
|
||||
? whitelistMerkleTreeArray
|
||||
: 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,
|
||||
whitelistMerkleTreeArray,
|
||||
whitelistArray,
|
||||
whitelistState,
|
||||
whitelistType,
|
||||
addressListState.values,
|
||||
adminsMutable,
|
||||
])
|
||||
|
||||
// make the necessary changes with respect to imported whitelist details
|
||||
useEffect(() => {
|
||||
if (importedWhitelistDetails) {
|
||||
setWhitelistState(importedWhitelistDetails.whitelistState)
|
||||
setWhitelistType(importedWhitelistDetails.whitelistType)
|
||||
whitelistAddressState.onChange(
|
||||
importedWhitelistDetails.contractAddress ? importedWhitelistDetails.contractAddress : '',
|
||||
)
|
||||
unitPriceState.onChange(
|
||||
importedWhitelistDetails.unitPrice ? Number(importedWhitelistDetails.unitPrice) / 1000000 : 0,
|
||||
)
|
||||
memberLimitState.onChange(importedWhitelistDetails.memberLimit ? importedWhitelistDetails.memberLimit : 0)
|
||||
perAddressLimitState.onChange(
|
||||
importedWhitelistDetails.perAddressLimit ? importedWhitelistDetails.perAddressLimit : 0,
|
||||
)
|
||||
setStartDate(
|
||||
importedWhitelistDetails.startTime
|
||||
? new Date(Number(importedWhitelistDetails.startTime) / 1_000_000)
|
||||
: undefined,
|
||||
)
|
||||
setEndDate(
|
||||
importedWhitelistDetails.endTime ? new Date(Number(importedWhitelistDetails.endTime) / 1_000_000) : undefined,
|
||||
)
|
||||
setAdminsMutable(importedWhitelistDetails.adminsMutable ? importedWhitelistDetails.adminsMutable : true)
|
||||
importedWhitelistDetails.admins?.forEach((admin) => {
|
||||
addressListState.reset()
|
||||
addressListState.add({ address: admin })
|
||||
})
|
||||
if (importedWhitelistDetails.whitelistType === 'standard') {
|
||||
setWhitelistStandardArray([])
|
||||
importedWhitelistDetails.members?.forEach((member) => {
|
||||
setWhitelistStandardArray((standardArray) => [...standardArray, member as string])
|
||||
})
|
||||
} else if (importedWhitelistDetails.whitelistType === 'merkletree') {
|
||||
setWhitelistMerkleTreeArray([])
|
||||
// importedWhitelistDetails.members?.forEach((member) => {
|
||||
// setWhitelistMerkleTreeArray((merkleTreeArray) => [...merkleTreeArray, member as string])
|
||||
// })
|
||||
} else if (importedWhitelistDetails.whitelistType === 'flex') {
|
||||
setWhitelistFlexArray([])
|
||||
importedWhitelistDetails.members?.forEach((member) => {
|
||||
setWhitelistFlexArray((flexArray) => [
|
||||
...flexArray,
|
||||
{
|
||||
address: (member as WhitelistFlexMember).address,
|
||||
mint_count: (member as WhitelistFlexMember).mint_count,
|
||||
},
|
||||
])
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importedWhitelistDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (whitelistState === 'new' && wallet.address) {
|
||||
addressListState.reset()
|
||||
addressListState.add({ address: wallet.address })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [whitelistState, wallet.address])
|
||||
|
||||
return (
|
||||
<div className="py-3 px-8 rounded border-2 border-white/20">
|
||||
<div className="flex justify-center">
|
||||
@ -261,7 +102,6 @@ export const WhitelistDetails = ({
|
||||
name="whitelistRadioOptions1"
|
||||
onClick={() => {
|
||||
setWhitelistState('none')
|
||||
setWhitelistType('standard')
|
||||
}}
|
||||
type="radio"
|
||||
value="None"
|
||||
@ -318,207 +158,34 @@ export const WhitelistDetails = ({
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={whitelistState === 'new'}>
|
||||
<div className="flex justify-between mb-5 ml-6 max-w-[500px] 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="standard"
|
||||
/>
|
||||
<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 className="form-check form-check-inline">
|
||||
<input
|
||||
checked={whitelistType === 'merkletree'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio9"
|
||||
name="inlineRadioOptions9"
|
||||
onClick={() => {
|
||||
setWhitelistType('merkletree')
|
||||
}}
|
||||
type="radio"
|
||||
value="merkletree"
|
||||
/>
|
||||
<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="inlineRadio9"
|
||||
>
|
||||
Whitelist Merkle Tree
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2">
|
||||
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
|
||||
<NumberInput isRequired {...unitPriceState} />
|
||||
<Conditional test={whitelistType !== 'merkletree'}>
|
||||
<NumberInput isRequired {...memberLimitState} />
|
||||
</Conditional>
|
||||
<Conditional test={whitelistType === 'standard' || whitelistType === 'merkletree'}>
|
||||
<NumberInput isRequired {...perAddressLimitState} />
|
||||
</Conditional>
|
||||
<NumberInput isRequired {...uniPriceState} />
|
||||
<NumberInput isRequired {...memberLimitState} />
|
||||
<NumberInput isRequired {...perAddressLimitState} />
|
||||
<FormControl
|
||||
htmlId="start-date"
|
||||
isRequired
|
||||
subtitle="Start time for minting tokens to whitelisted addresses"
|
||||
title={`Whitelist Start Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
title="Start Time"
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setStartDate(
|
||||
timezone === 'Local'
|
||||
? date
|
||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setStartDate(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? startDate
|
||||
: startDate
|
||||
? new Date(startDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setStartDate(date)} value={startDate} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
htmlId="end-date"
|
||||
isRequired
|
||||
subtitle="Whitelist End Time dictates when public sales will start"
|
||||
title={`Whitelist End Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
subtitle="End time for minting tokens to whitelisted addresses"
|
||||
title="End Time"
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setEndDate(
|
||||
timezone === 'Local'
|
||||
? date
|
||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setEndDate(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? endDate
|
||||
: endDate
|
||||
? new Date(endDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setEndDate(date)} value={endDate} />
|
||||
</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}
|
||||
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={
|
||||
<div>
|
||||
<span>TXT file that contains the whitelisted addresses</span>
|
||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
|
||||
Download Sample File
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
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={
|
||||
<div>
|
||||
<span>CSV file that contains the whitelisted addresses and corresponding mint counts</span>
|
||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFlexFile}>
|
||||
Download Sample File
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title="Whitelist File"
|
||||
>
|
||||
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
|
||||
</FormGroup>
|
||||
<Conditional test={whitelistFlexArray.length > 0}>
|
||||
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
<Conditional test={whitelistType === 'merkletree'}>
|
||||
<FormGroup
|
||||
subtitle={
|
||||
<div>
|
||||
<span>TXT file that contains the whitelisted addresses</span>
|
||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
|
||||
Download Sample File
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title="Whitelist File"
|
||||
>
|
||||
<WhitelistUpload onChange={whitelistFileOnChange} />
|
||||
</FormGroup>
|
||||
<Conditional test={whitelistStandardArray.length > 0}>
|
||||
<JsonPreview content={whitelistStandardArray} initialState 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,41 +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 { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
|
||||
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 'utils/wallet'
|
||||
|
||||
import { resolveAddress } from '../../../utils/resolveAddress'
|
||||
import type { MinterType } from '../actions/Combobox'
|
||||
|
||||
interface CollectionQueriesProps {
|
||||
minterContractAddress: string
|
||||
sg721ContractAddress: string
|
||||
royaltyRegistryContractAddress: string
|
||||
sg721Messages: SG721Instance | undefined
|
||||
vendingMinterMessages: VendingMinterInstance | undefined
|
||||
baseMinterMessages: BaseMinterInstance | undefined
|
||||
openEditionMinterMessages: OpenEditionMinterInstance | undefined
|
||||
royaltyRegistryMessages: RoyaltyRegistryInstance | undefined
|
||||
minterType: MinterType
|
||||
minterMessages: MinterInstance | undefined
|
||||
}
|
||||
export const CollectionQueries = ({
|
||||
sg721ContractAddress,
|
||||
sg721Messages,
|
||||
minterContractAddress,
|
||||
vendingMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
baseMinterMessages,
|
||||
minterType,
|
||||
royaltyRegistryMessages,
|
||||
minterMessages,
|
||||
}: CollectionQueriesProps) => {
|
||||
const wallet = useWallet()
|
||||
|
||||
const comboboxState = useQueryComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
|
||||
@ -61,60 +43,27 @@ 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,
|
||||
royaltyRegistryMessages,
|
||||
type,
|
||||
tokenId,
|
||||
address,
|
||||
sg721ContractAddress,
|
||||
] as const,
|
||||
[sg721Messages, minterMessages, type, tokenId, address] as const,
|
||||
async ({ queryKey }) => {
|
||||
const [
|
||||
_sg721Messages,
|
||||
_baseMinterMessages_,
|
||||
_vendingMinterMessages,
|
||||
_openEditionMinterMessages,
|
||||
_royaltyRegistryMessages,
|
||||
_type,
|
||||
_tokenId,
|
||||
_address,
|
||||
_sg721ContractAddress,
|
||||
] = 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,
|
||||
royaltyRegistryMessages: _royaltyRegistryMessages,
|
||||
address: resolvedAddress,
|
||||
type: _type,
|
||||
sg721ContractAddress: _sg721ContractAddress,
|
||||
})
|
||||
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(type && type === 'infinity_swap_royalties' && sg721ContractAddress) ||
|
||||
Boolean(sg721ContractAddress && minterContractAddress && type),
|
||||
enabled: Boolean(sg721ContractAddress && minterContractAddress && type),
|
||||
retry: false,
|
||||
},
|
||||
)
|
||||
@ -122,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,9 +1,5 @@
|
||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter/contract'
|
||||
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
|
||||
import type { MinterInstance } from 'contracts/minter'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
import { INFINITY_SWAP_PROTOCOL_ADDRESS } from 'utils/constants'
|
||||
|
||||
export type QueryType = typeof QUERY_TYPES[number]
|
||||
|
||||
@ -12,13 +8,8 @@ export const QUERY_TYPES = [
|
||||
'mint_price',
|
||||
'num_tokens',
|
||||
'tokens_minted_to_user',
|
||||
'total_mint_count',
|
||||
'tokens',
|
||||
// 'token_owners',
|
||||
'infinity_swap_royalties',
|
||||
'token_info',
|
||||
'config',
|
||||
'status',
|
||||
] as const
|
||||
|
||||
export interface QueryListItem {
|
||||
@ -27,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',
|
||||
@ -38,11 +29,6 @@ export const VENDING_QUERY_LIST: QueryListItem[] = [
|
||||
name: 'Mint Price',
|
||||
description: `Get the price of minting a token.`,
|
||||
},
|
||||
{
|
||||
id: 'infinity_swap_royalties',
|
||||
name: 'Infinity Swap Royalty Details',
|
||||
description: `Get the collection's royalty details for Infinity Swap`,
|
||||
},
|
||||
{
|
||||
id: 'num_tokens',
|
||||
name: 'Mintable Number of Tokens',
|
||||
@ -63,95 +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: 'infinity_swap_royalties',
|
||||
name: 'Infinity Swap Royalty Details',
|
||||
description: `Get the collection's royalty details for Infinity Swap`,
|
||||
},
|
||||
{
|
||||
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: 'infinity_swap_royalties',
|
||||
name: 'Infinity Swap Royalty Details',
|
||||
description: `Get the collection's royalty details for Infinity Swap`,
|
||||
},
|
||||
{
|
||||
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 {
|
||||
@ -162,36 +59,21 @@ export interface DispatchExecuteProps {
|
||||
type Select<T extends QueryType> = T
|
||||
|
||||
export type DispatchQueryArgs = {
|
||||
baseMinterMessages?: BaseMinterInstance
|
||||
vendingMinterMessages?: VendingMinterInstance
|
||||
openEditionMinterMessages?: OpenEditionMinterInstance
|
||||
minterMessages?: MinterInstance
|
||||
sg721Messages?: SG721Instance
|
||||
royaltyRegistryMessages?: RoyaltyRegistryInstance
|
||||
sg721ContractAddress?: string
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'collection_info'> }
|
||||
| { 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<'infinity_swap_royalties'> }
|
||||
// | { 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,
|
||||
royaltyRegistryMessages,
|
||||
} = args
|
||||
if (!baseMinterMessages || !vendingMinterMessages || !openEditionMinterMessages || !sg721Messages) {
|
||||
const { minterMessages, sg721Messages } = args
|
||||
if (!minterMessages || !sg721Messages) {
|
||||
throw new Error('Cannot execute actions')
|
||||
}
|
||||
switch (args.type) {
|
||||
@ -199,39 +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 'infinity_swap_royalties': {
|
||||
return royaltyRegistryMessages?.collectionRoyaltyProtocol(
|
||||
args.sg721ContractAddress as string,
|
||||
INFINITY_SWAP_PROTOCOL_ADDRESS,
|
||||
)
|
||||
}
|
||||
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,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 = () => {
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import type { ExecuteListItem } from 'contracts/royaltyRegistry/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/royaltyRegistry/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/royaltyRegistry/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>
|
||||
)
|
||||
}
|
||||
@ -1,11 +1,8 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/whitelist/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/whitelist/messages/execute'
|
||||
import { EXECUTE_LIST as WL_MERKLE_TREE_EXECUTE_LIST } from 'contracts/whitelistMerkleTree/messages/execute'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
@ -13,20 +10,13 @@ import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
export interface ExecuteComboboxProps {
|
||||
value: ExecuteListItem | null
|
||||
onChange: (item: ExecuteListItem) => void
|
||||
whitelistType?: 'standard' | 'flex' | 'merkletree'
|
||||
}
|
||||
|
||||
export const ExecuteCombobox = ({ value, onChange, whitelistType }: ExecuteComboboxProps) => {
|
||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filtered =
|
||||
whitelistType !== 'merkletree'
|
||||
? search === ''
|
||||
? EXECUTE_LIST
|
||||
: matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
: search === ''
|
||||
? WL_MERKLE_TREE_EXECUTE_LIST
|
||||
: matchSorter(WL_MERKLE_TREE_EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -77,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange, whitelistType }: ExecuteCombo
|
||||
<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 { 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 { useWallet } from 'utils/wallet'
|
||||
|
||||
import { useInputState } from './FormInput.hooks'
|
||||
|
||||
@ -31,7 +26,6 @@ export function AddressList(props: AddressListProps) {
|
||||
{entries.map(([id], i) => (
|
||||
<Address
|
||||
key={`ib-${id}`}
|
||||
defaultValue={entries[i][1]}
|
||||
id={id}
|
||||
isLast={i === entries.length - 1}
|
||||
onAdd={onAdd}
|
||||
@ -49,11 +43,9 @@ export interface AddressProps {
|
||||
onAdd: AddressListProps['onAdd']
|
||||
onChange: AddressListProps['onChange']
|
||||
onRemove: AddressListProps['onRemove']
|
||||
defaultValue?: Address
|
||||
}
|
||||
|
||||
export function Address({ id, isLast, onAdd, onChange, onRemove, defaultValue }: AddressProps) {
|
||||
const wallet = useWallet()
|
||||
export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps) {
|
||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||
|
||||
const htmlId = useId()
|
||||
@ -62,45 +54,12 @@ export function Address({ id, isLast, onAdd, onChange, onRemove, defaultValue }:
|
||||
id: `ib-address-${htmlId}`,
|
||||
name: `ib-address-${htmlId}`,
|
||||
title: ``,
|
||||
defaultValue: defaultValue?.address,
|
||||
})
|
||||
|
||||
const resolveAddress = async (name: string) => {
|
||||
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
|
||||
await (
|
||||
await wallet.getCosmWasmClient()
|
||||
)
|
||||
.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 (
|
||||
@ -108,7 +67,7 @@ export function Address({ id, isLast, onAdd, onChange, onRemove, defaultValue }:
|
||||
<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,33 +0,0 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import type { DenomUnit } from './DenomUnits'
|
||||
|
||||
export function useDenomUnitsState() {
|
||||
const [record, setRecord] = useState<Record<string, DenomUnit>>(() => ({}))
|
||||
|
||||
const entries = useMemo(() => Object.entries(record), [record])
|
||||
const values = useMemo(() => Object.values(record), [record])
|
||||
|
||||
function add(attribute: DenomUnit = { denom: '', exponent: 0, aliases: '' }) {
|
||||
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,106 +0,0 @@
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { NumberInput, TextInput } from 'components/forms/FormInput'
|
||||
import { useEffect, useId, useMemo } from 'react'
|
||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
import { useInputState, useNumberInputState } from './FormInput.hooks'
|
||||
|
||||
export interface DenomUnit {
|
||||
denom: string
|
||||
exponent: number
|
||||
aliases: string
|
||||
}
|
||||
|
||||
export interface DenomUnitsProps {
|
||||
title: string
|
||||
subtitle?: string
|
||||
isRequired?: boolean
|
||||
attributes: [string, DenomUnit][]
|
||||
onAdd: () => void
|
||||
onChange: (key: string, attribute: DenomUnit) => void
|
||||
onRemove: (key: string) => void
|
||||
}
|
||||
|
||||
export function DenomUnits(props: DenomUnitsProps) {
|
||||
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
|
||||
|
||||
return (
|
||||
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||
{attributes.map(([id], i) => (
|
||||
<DenomUnit
|
||||
key={`ma-${id}`}
|
||||
defaultAttribute={attributes[i][1]}
|
||||
id={id}
|
||||
isLast={i === attributes.length - 1}
|
||||
onAdd={onAdd}
|
||||
onChange={onChange}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export interface DenomUnitProps {
|
||||
id: string
|
||||
isLast: boolean
|
||||
onAdd: DenomUnitsProps['onAdd']
|
||||
onChange: DenomUnitsProps['onChange']
|
||||
onRemove: DenomUnitsProps['onRemove']
|
||||
defaultAttribute: DenomUnit
|
||||
}
|
||||
|
||||
export function DenomUnit({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: DenomUnitProps) {
|
||||
const wallet = useWallet()
|
||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
||||
|
||||
const htmlId = useId()
|
||||
|
||||
const denomState = useInputState({
|
||||
id: `ma-denom-${htmlId}`,
|
||||
name: `ma-denom-${htmlId}`,
|
||||
title: `Denom`,
|
||||
defaultValue: defaultAttribute.denom,
|
||||
})
|
||||
|
||||
const exponentState = useNumberInputState({
|
||||
id: `mint-exponent-${htmlId}`,
|
||||
name: `mint-exponent-${htmlId}`,
|
||||
title: `Exponent`,
|
||||
defaultValue: defaultAttribute.exponent,
|
||||
})
|
||||
|
||||
const aliasesState = useInputState({
|
||||
id: `ma-aliases-${htmlId}`,
|
||||
name: `ma-aliases-${htmlId}`,
|
||||
title: `Aliases`,
|
||||
defaultValue: defaultAttribute.aliases,
|
||||
placeholder: 'Comma separated aliases',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onChange(id, { denom: denomState.value, exponent: exponentState.value, aliases: aliasesState.value })
|
||||
}, [id, denomState.value, exponentState.value, aliasesState.value])
|
||||
|
||||
return (
|
||||
<div className="grid relative md:grid-cols-[40%_18%_35_7%] lg:grid-cols-[55%_13%_25%_7%] 2xl:grid-cols-[55%_13%_25%_7%] 2xl:space-x-2">
|
||||
<TextInput {...denomState} />
|
||||
<NumberInput className="ml-2" {...exponentState} />
|
||||
<TextInput className="ml-2" {...aliasesState} />
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@ -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,135 +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 { 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 { useWallet } from 'utils/wallet'
|
||||
|
||||
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.isWalletConnected) throw new Error('Wallet not connected')
|
||||
await (
|
||||
await wallet.getCosmWasmClient()
|
||||
)
|
||||
.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,400 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* 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 { useGlobalSettings } from 'contexts/globalSettings'
|
||||
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
|
||||
importedCollectionDetails?: CollectionDetailsDataProps
|
||||
}
|
||||
|
||||
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,
|
||||
importedCollectionDetails,
|
||||
}: 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 { timezone } = useGlobalSettings()
|
||||
|
||||
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.replaceAll('#', ''), {
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedCollectionDetails) {
|
||||
nameState.onChange(importedCollectionDetails.name)
|
||||
descriptionState.onChange(importedCollectionDetails.description)
|
||||
symbolState.onChange(importedCollectionDetails.symbol)
|
||||
//setCoverImage(importedCollectionDetails.imageFile[0] || null)
|
||||
externalLinkState.onChange(importedCollectionDetails.externalLink || '')
|
||||
setTimestamp(
|
||||
importedCollectionDetails.startTradingTime
|
||||
? new Date(parseInt(importedCollectionDetails.startTradingTime) / 1_000_000)
|
||||
: undefined,
|
||||
)
|
||||
setExplicit(importedCollectionDetails.explicit)
|
||||
setUpdatable(importedCollectionDetails.updatable)
|
||||
}
|
||||
}, [importedCollectionDetails])
|
||||
|
||||
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 1 week by default."
|
||||
title={`Trading Start Time (optional | ${timezone === 'Local' ? 'local)' : 'UTC)'}`}
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setTimestamp(
|
||||
timezone === 'Local'
|
||||
? date
|
||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setTimestamp(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? timestamp
|
||||
: timestamp
|
||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</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={
|
||||
false && 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-full form-control')}>
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<div className="flex flex-col">
|
||||
<span className="mr-4 font-bold">Updatable Token Metadata</span>
|
||||
<span className="mr-4">(Price: 2000 STARS)</span>
|
||||
</div>
|
||||
<input
|
||||
checked={updatable}
|
||||
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setUpdatable(!updatable)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Conditional>
|
||||
</FormGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,454 +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 { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
|
||||
import type { AssetType } from 'utils/getAssetType'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
|
||||
export type UploadMethod = 'new' | 'existing'
|
||||
|
||||
interface ImageUploadDetailsProps {
|
||||
onChange: (value: ImageUploadDetailsDataProps) => void
|
||||
importedImageUploadDetails?: ImageUploadDetailsDataProps
|
||||
}
|
||||
|
||||
export interface ImageUploadDetailsDataProps {
|
||||
assetFile: File | undefined
|
||||
thumbnailFile?: File | undefined
|
||||
isThumbnailCompatible?: boolean
|
||||
uploadService: UploadServiceType
|
||||
nftStorageApiKey?: string
|
||||
pinataApiKey?: string
|
||||
pinataSecretKey?: string
|
||||
uploadMethod: UploadMethod
|
||||
imageUrl?: string
|
||||
coverImageUrl?: string
|
||||
}
|
||||
|
||||
export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: ImageUploadDetailsProps) => {
|
||||
const [assetFile, setAssetFile] = useState<File>()
|
||||
const [thumbnailFile, setThumbnailFile] = useState<File>()
|
||||
const [isThumbnailCompatible, setIsThumbnailCompatible] = useState<boolean>(false)
|
||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
||||
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
|
||||
|
||||
const assetFileRef = useRef<HTMLInputElement | null>(null)
|
||||
const thumbnailFileRef = 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 thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html']
|
||||
|
||||
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setAssetFile(undefined)
|
||||
setThumbnailFile(undefined)
|
||||
setIsThumbnailCompatible(false)
|
||||
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.replaceAll('#', ''), { 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.')
|
||||
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
|
||||
setIsThumbnailCompatible(true)
|
||||
}
|
||||
setAssetFile(selectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
const selectThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setThumbnailFile(undefined)
|
||||
if (event.target.files === null) return
|
||||
|
||||
let selectedFile: File
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/*' })
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
||||
else return toast.error('No file selected.')
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
setThumbnailFile(selectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
const regex =
|
||||
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: ImageUploadDetailsDataProps = {
|
||||
assetFile,
|
||||
thumbnailFile,
|
||||
isThumbnailCompatible,
|
||||
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,
|
||||
thumbnailFile,
|
||||
isThumbnailCompatible,
|
||||
uploadService,
|
||||
nftStorageApiKeyState.value,
|
||||
pinataApiKeyState.value,
|
||||
pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
imageUrlState.value,
|
||||
coverImageUrlState.value,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (assetFileRef.current) assetFileRef.current.value = ''
|
||||
setAssetFile(undefined)
|
||||
setThumbnailFile(undefined)
|
||||
setIsThumbnailCompatible(false)
|
||||
imageUrlState.onChange('')
|
||||
}, [uploadMethod])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedImageUploadDetails) {
|
||||
setUploadMethod(importedImageUploadDetails.uploadMethod)
|
||||
setUploadService(importedImageUploadDetails.uploadService)
|
||||
nftStorageApiKeyState.onChange(importedImageUploadDetails.nftStorageApiKey || '')
|
||||
pinataApiKeyState.onChange(importedImageUploadDetails.pinataApiKey || '')
|
||||
pinataSecretKeyState.onChange(importedImageUploadDetails.pinataSecretKey || '')
|
||||
imageUrlState.onChange(importedImageUploadDetails.imageUrl || '')
|
||||
coverImageUrlState.onChange(importedImageUploadDetails.coverImageUrl || '')
|
||||
}
|
||||
}, [importedImageUploadDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (useDefaultApiKey) {
|
||||
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
|
||||
} else {
|
||||
nftStorageApiKeyState.onChange('')
|
||||
}
|
||||
}, [useDefaultApiKey])
|
||||
|
||||
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'}>
|
||||
<div className="flex-col w-full">
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
|
||||
<div className="flex-row mt-2 w-full form-control">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="mr-2 font-bold">Use Default API Key</span>
|
||||
<input
|
||||
checked={useDefaultApiKey}
|
||||
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
||||
onClick={() => {
|
||||
setUseDefaultApiKey(!useDefaultApiKey)
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</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, .pdf"
|
||||
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>
|
||||
<Conditional test={isThumbnailCompatible}>
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="thumbnailFile"
|
||||
>
|
||||
Thumbnail Selection (optional)
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="image/*"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="thumbnailFile"
|
||||
onChange={selectThumbnail}
|
||||
ref={thumbnailFileRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={assetFile !== undefined}>
|
||||
<SingleAssetPreview
|
||||
relatedAsset={assetFile}
|
||||
subtitle={`Asset filename: ${assetFile?.name as string}`}
|
||||
/>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,280 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Conditional } from 'components/Conditional'
|
||||
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 { useGlobalSettings } from 'contexts/globalSettings'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
import { NumberInput, TextInput } from '../forms/FormInput'
|
||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
||||
|
||||
export type LimitType = 'count_limited' | 'time_limited' | 'time_and_count_limited'
|
||||
|
||||
interface MintingDetailsProps {
|
||||
onChange: (data: MintingDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod
|
||||
minimumMintPrice: number
|
||||
mintTokenFromFactory?: TokenInfo | undefined
|
||||
importedMintingDetails?: MintingDetailsDataProps
|
||||
isPresale: boolean
|
||||
whitelistStartDate?: string
|
||||
}
|
||||
|
||||
export interface MintingDetailsDataProps {
|
||||
unitPrice: string
|
||||
perAddressLimit: number
|
||||
startTime: string
|
||||
endTime?: string
|
||||
tokenCountLimit?: number
|
||||
paymentAddress?: string
|
||||
selectedMintToken?: TokenInfo
|
||||
limitType: LimitType
|
||||
}
|
||||
|
||||
export const MintingDetails = ({
|
||||
onChange,
|
||||
uploadMethod,
|
||||
minimumMintPrice,
|
||||
mintTokenFromFactory,
|
||||
importedMintingDetails,
|
||||
isPresale,
|
||||
whitelistStartDate,
|
||||
}: MintingDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
||||
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>()
|
||||
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
|
||||
const [mintingDetailsImported, setMintingDetailsImported] = useState(false)
|
||||
const [limitType, setLimitType] = useState<LimitType>('time_limited')
|
||||
const { timezone } = useGlobalSettings()
|
||||
|
||||
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 tokenCountLimitState = useNumberInputState({
|
||||
id: 'tokencountlimit',
|
||||
name: 'tokencountlimit',
|
||||
title: 'Maximum Token Count',
|
||||
subtitle: 'Total number of mintable tokens',
|
||||
placeholder: '100',
|
||||
})
|
||||
|
||||
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(() => {
|
||||
if (!importedMintingDetails || (importedMintingDetails && mintingDetailsImported)) {
|
||||
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:
|
||||
limitType === 'time_limited' || limitType === 'time_and_count_limited'
|
||||
? endTimestamp
|
||||
? (endTimestamp.getTime() * 1_000_000).toString()
|
||||
: ''
|
||||
: undefined,
|
||||
paymentAddress: paymentAddressState.value.trim(),
|
||||
selectedMintToken,
|
||||
limitType,
|
||||
tokenCountLimit:
|
||||
limitType === 'count_limited' || limitType === 'time_and_count_limited'
|
||||
? tokenCountLimitState.value
|
||||
: undefined,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
unitPriceState.value,
|
||||
perAddressLimitState.value,
|
||||
timestamp,
|
||||
endTimestamp,
|
||||
paymentAddressState.value,
|
||||
selectedMintToken,
|
||||
tokenCountLimitState.value,
|
||||
limitType,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedMintingDetails) {
|
||||
console.log('Selected Token ID: ', importedMintingDetails.selectedMintToken?.id)
|
||||
unitPriceState.onChange(Number(importedMintingDetails.unitPrice) / 1000000)
|
||||
perAddressLimitState.onChange(importedMintingDetails.perAddressLimit)
|
||||
setLimitType(importedMintingDetails.limitType)
|
||||
tokenCountLimitState.onChange(importedMintingDetails.tokenCountLimit ? importedMintingDetails.tokenCountLimit : 0)
|
||||
setTimestamp(new Date(Number(importedMintingDetails.startTime) / 1_000_000))
|
||||
setEndTimestamp(new Date(Number(importedMintingDetails.endTime) / 1_000_000))
|
||||
paymentAddressState.onChange(importedMintingDetails.paymentAddress ? importedMintingDetails.paymentAddress : '')
|
||||
setSelectedMintToken(tokensList.find((token) => token.id === importedMintingDetails.selectedMintToken?.id))
|
||||
setMintingDetailsImported(true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importedMintingDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (isPresale) {
|
||||
setTimestamp(whitelistStartDate ? new Date(Number(whitelistStartDate) / 1_000_000) : undefined)
|
||||
}
|
||||
}, [whitelistStartDate, isPresale])
|
||||
|
||||
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-end">
|
||||
<NumberInput {...unitPriceState} isRequired />
|
||||
<select
|
||||
className="py-[9px] px-4 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))}
|
||||
value={selectedMintToken?.displayName}
|
||||
>
|
||||
{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 ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
title="Start Time"
|
||||
>
|
||||
<InputDateTime
|
||||
disabled={isPresale}
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setTimestamp(
|
||||
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setTimestamp(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? timestamp
|
||||
: timestamp
|
||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="flex-row mt-2 w-full form-control">
|
||||
<h1 className="mt-2 font-bold text-md">Limit Type: </h1>
|
||||
<label className="justify-start ml-6 cursor-pointer label">
|
||||
<span className="mr-2">Time</span>
|
||||
<input
|
||||
checked={limitType === 'time_limited' || limitType === 'time_and_count_limited'}
|
||||
className={`${limitType === 'time_limited' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
||||
onClick={() => {
|
||||
if (limitType === 'time_and_count_limited') setLimitType('count_limited' as LimitType)
|
||||
else if (limitType === 'count_limited') setLimitType('time_and_count_limited' as LimitType)
|
||||
else setLimitType('count_limited' as LimitType)
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
<label className="justify-start ml-4 cursor-pointer label">
|
||||
<span className="mr-2">Token Count</span>
|
||||
<input
|
||||
checked={limitType === 'count_limited' || limitType === 'time_and_count_limited'}
|
||||
className={`${limitType === 'count_limited' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
||||
onClick={() => {
|
||||
if (limitType === 'time_and_count_limited') setLimitType('time_limited' as LimitType)
|
||||
else if (limitType === 'time_limited') setLimitType('time_and_count_limited' as LimitType)
|
||||
else setLimitType('time_limited' as LimitType)
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Conditional test={limitType === 'time_limited' || limitType === 'time_and_count_limited'}>
|
||||
<FormControl
|
||||
htmlId="endTimestamp"
|
||||
isRequired
|
||||
subtitle={`Minting end time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
title="End Time"
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setEndTimestamp(
|
||||
timezone === 'Local'
|
||||
? date
|
||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setEndTimestamp(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? endTimestamp
|
||||
: endTimestamp
|
||||
? new Date(endTimestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</Conditional>
|
||||
<Conditional test={limitType === 'count_limited' || limitType === 'time_and_count_limited'}>
|
||||
<NumberInput {...tokenCountLimitState} isRequired />
|
||||
</Conditional>
|
||||
</FormGroup>
|
||||
<TextInput className="pr-4 pl-4 mt-3" {...paymentAddressState} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,579 +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 { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
|
||||
import type { AssetType } from 'utils/getAssetType'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
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
|
||||
importedOffChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps
|
||||
}
|
||||
|
||||
export interface OffChainMetadataUploadDetailsDataProps {
|
||||
assetFiles: File[]
|
||||
metadataFiles: File[]
|
||||
thumbnailFile?: File
|
||||
isThumbnailCompatible?: boolean
|
||||
uploadService: UploadServiceType
|
||||
nftStorageApiKey?: string
|
||||
pinataApiKey?: string
|
||||
pinataSecretKey?: string
|
||||
uploadMethod: UploadMethod
|
||||
tokenURI?: string
|
||||
imageUrl?: string
|
||||
openEditionMinterMetadataFile?: File
|
||||
exportedMetadata?: any
|
||||
}
|
||||
|
||||
export const OffChainMetadataUploadDetails = ({
|
||||
onChange,
|
||||
metadataStorageMethod,
|
||||
importedOffChainMetadataUploadDetails,
|
||||
}: OffChainMetadataUploadDetailsProps) => {
|
||||
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
|
||||
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
|
||||
const [thumbnailFile, setThumbnailFile] = useState<File>()
|
||||
const [isThumbnailCompatible, setIsThumbnailCompatible] = useState<boolean>(false)
|
||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
||||
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
|
||||
const [refreshMetadata, setRefreshMetadata] = useState(false)
|
||||
const [exportedMetadata, setExportedMetadata] = useState(undefined)
|
||||
const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>()
|
||||
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
|
||||
|
||||
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html', 'document']
|
||||
|
||||
const assetFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
const thumbnailFilesRef = 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([])
|
||||
setThumbnailFile(undefined)
|
||||
setIsThumbnailCompatible(false)
|
||||
if (event.target.files === null) return
|
||||
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
|
||||
setIsThumbnailCompatible(true)
|
||||
}
|
||||
let loadedFileCount = 0
|
||||
const files: File[] = []
|
||||
let reader: FileReader
|
||||
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.replaceAll('#', ''), {
|
||||
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.replaceAll('#', ''), {
|
||||
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 selectThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setThumbnailFile(undefined)
|
||||
if (event.target.files === null) return
|
||||
|
||||
let selectedFile: File
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/*' })
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
||||
else return toast.error('No file selected.')
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
setThumbnailFile(selectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
const updateMetadataFileIndex = (index: number) => {
|
||||
setMetadataFileArrayIndex(index)
|
||||
setRefreshMetadata((prev) => !prev)
|
||||
}
|
||||
|
||||
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,
|
||||
thumbnailFile,
|
||||
isThumbnailCompatible,
|
||||
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,
|
||||
exportedMetadata,
|
||||
}
|
||||
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,
|
||||
thumbnailFile,
|
||||
isThumbnailCompatible,
|
||||
uploadService,
|
||||
nftStorageApiKeyState.value,
|
||||
pinataApiKeyState.value,
|
||||
pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
tokenUriState.value,
|
||||
coverImageUrlState.value,
|
||||
refreshMetadata,
|
||||
openEditionMinterMetadataFile,
|
||||
exportedMetadata,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
|
||||
setMetadataFilesArray([])
|
||||
if (assetFilesRef.current) assetFilesRef.current.value = ''
|
||||
setAssetFilesArray([])
|
||||
setThumbnailFile(undefined)
|
||||
setIsThumbnailCompatible(false)
|
||||
if (!importedOffChainMetadataUploadDetails) {
|
||||
tokenUriState.onChange('')
|
||||
coverImageUrlState.onChange('')
|
||||
}
|
||||
}, [uploadMethod, metadataStorageMethod])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedOffChainMetadataUploadDetails) {
|
||||
setUploadService(importedOffChainMetadataUploadDetails.uploadService)
|
||||
nftStorageApiKeyState.onChange(importedOffChainMetadataUploadDetails.nftStorageApiKey || '')
|
||||
pinataApiKeyState.onChange(importedOffChainMetadataUploadDetails.pinataApiKey || '')
|
||||
pinataSecretKeyState.onChange(importedOffChainMetadataUploadDetails.pinataSecretKey || '')
|
||||
setUploadMethod(importedOffChainMetadataUploadDetails.uploadMethod)
|
||||
tokenUriState.onChange(importedOffChainMetadataUploadDetails.tokenURI || '')
|
||||
coverImageUrlState.onChange(importedOffChainMetadataUploadDetails.imageUrl || '')
|
||||
// setOpenEditionMinterMetadataFile(importedOffChainMetadataUploadDetails.openEditionMinterMetadataFile)
|
||||
}
|
||||
}, [importedOffChainMetadataUploadDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (useDefaultApiKey) {
|
||||
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
|
||||
} else {
|
||||
nftStorageApiKeyState.onChange('')
|
||||
}
|
||||
}, [useDefaultApiKey])
|
||||
|
||||
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'}>
|
||||
<div className="flex-col w-full">
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
|
||||
<div className="flex-row mt-2 w-full form-control">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="mr-2 font-bold">Use Default API Key</span>
|
||||
<input
|
||||
checked={useDefaultApiKey}
|
||||
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
||||
onClick={() => {
|
||||
setUseDefaultApiKey(!useDefaultApiKey)
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</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, .pdf"
|
||||
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>
|
||||
<Conditional test={isThumbnailCompatible}>
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="thumbnailFiles"
|
||||
>
|
||||
Thumbnail Selection (optional)
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="image/*"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="thumbnailFiles"
|
||||
onChange={selectThumbnail}
|
||||
ref={thumbnailFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
{assetFilesArray.length > 0 && (
|
||||
<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
|
||||
importedMetadata={importedOffChainMetadataUploadDetails?.exportedMetadata}
|
||||
onChange={setExportedMetadata}
|
||||
selectedAssetFile={assetFilesArray[0]}
|
||||
selectedMetadataFile={metadataFilesArray[0]}
|
||||
updateMetadataToUpload={updateOpenEditionMinterMetadataFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,285 +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 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
|
||||
importedOnChainMetadataInputDetails?: OnChainMetadataInputDetailsDataProps
|
||||
}
|
||||
|
||||
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,
|
||||
importedOnChainMetadataInputDetails,
|
||||
}: OnChainMetadataInputDetailsProps) => {
|
||||
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.replaceAll('#', ''), {
|
||||
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,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedOnChainMetadataInputDetails) {
|
||||
nameState.onChange(importedOnChainMetadataInputDetails.name || '')
|
||||
descriptionState.onChange(importedOnChainMetadataInputDetails.description || '')
|
||||
externalUrlState.onChange(importedOnChainMetadataInputDetails.external_url || '')
|
||||
youtubeUrlState.onChange(importedOnChainMetadataInputDetails.youtube_url || '')
|
||||
animationUrlState.onChange(importedOnChainMetadataInputDetails.animation_url || '')
|
||||
imageDataState.onChange(importedOnChainMetadataInputDetails.image_data || '')
|
||||
if (importedOnChainMetadataInputDetails.attributes) {
|
||||
attributesState.reset()
|
||||
importedOnChainMetadataInputDetails.attributes.forEach((attr) => {
|
||||
attributesState.add({
|
||||
trait_type: attr.trait_type,
|
||||
value: attr.value,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [importedOnChainMetadataInputDetails])
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,127 +0,0 @@
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
import { NumberInput, TextInput } from '../forms/FormInput'
|
||||
|
||||
interface RoyaltyDetailsProps {
|
||||
onChange: (data: RoyaltyDetailsDataProps) => void
|
||||
importedRoyaltyDetails?: RoyaltyDetailsDataProps
|
||||
}
|
||||
|
||||
export interface RoyaltyDetailsDataProps {
|
||||
royaltyType: RoyaltyState
|
||||
paymentAddress: string
|
||||
share: number
|
||||
}
|
||||
|
||||
type RoyaltyState = 'none' | 'new'
|
||||
|
||||
export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
|
||||
const [royaltyDetailsImported, setRoyaltyDetailsImported] = useState(false)
|
||||
|
||||
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(() => {
|
||||
if (!importedRoyaltyDetails || (importedRoyaltyDetails && royaltyDetailsImported)) {
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
if (importedRoyaltyDetails) {
|
||||
setRoyaltyState(importedRoyaltyDetails.royaltyType)
|
||||
royaltyPaymentAddressState.onChange(importedRoyaltyDetails.paymentAddress.toString())
|
||||
royaltyShareState.onChange(importedRoyaltyDetails.share.toString())
|
||||
setRoyaltyDetailsImported(true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importedRoyaltyDetails])
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,528 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Button } from 'components/Button'
|
||||
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 { useGlobalSettings } from 'contexts/globalSettings'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { useWallet } from 'utils/wallet'
|
||||
|
||||
import { Conditional } from '../Conditional'
|
||||
import { AddressInput, NumberInput } from '../forms/FormInput'
|
||||
import { JsonPreview } from '../JsonPreview'
|
||||
import { WhitelistUpload } from '../WhitelistUpload'
|
||||
|
||||
interface WhitelistDetailsProps {
|
||||
onChange: (data: WhitelistDetailsDataProps) => void
|
||||
mintingTokenFromFactory?: TokenInfo
|
||||
importedWhitelistDetails?: WhitelistDetailsDataProps
|
||||
}
|
||||
|
||||
export interface WhitelistDetailsDataProps {
|
||||
whitelistState: WhitelistState
|
||||
whitelistType: WhitelistType
|
||||
contractAddress?: string
|
||||
members?: string[] | WhitelistFlexMember[]
|
||||
unitPrice?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
perAddressLimit?: number
|
||||
memberLimit?: number
|
||||
admins?: string[]
|
||||
adminsMutable?: boolean
|
||||
}
|
||||
|
||||
type WhitelistState = 'none' | 'existing' | 'new'
|
||||
|
||||
export type WhitelistType = 'standard' | 'flex' | 'merkletree'
|
||||
|
||||
export const WhitelistDetails = ({
|
||||
onChange,
|
||||
mintingTokenFromFactory,
|
||||
importedWhitelistDetails,
|
||||
}: WhitelistDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const { timezone } = useGlobalSettings()
|
||||
|
||||
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 [whitelistMerkleTreeArray, setWhitelistMerkleTreeArray] = useState<string[]>([])
|
||||
const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
|
||||
|
||||
const whitelistAddressState = useInputState({
|
||||
id: 'whitelist-address',
|
||||
name: 'whitelistAddress',
|
||||
title: 'Whitelist Address',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const unitPriceState = useNumberInputState({
|
||||
id: 'unit-price',
|
||||
name: 'unitPrice',
|
||||
title: 'Unit Price',
|
||||
subtitle: `Token price for whitelisted addresses \n (min. 0 ${
|
||||
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
|
||||
})`,
|
||||
placeholder: '25',
|
||||
})
|
||||
|
||||
const memberLimitState = useNumberInputState({
|
||||
id: 'member-limit',
|
||||
name: 'memberLimit',
|
||||
title: 'Member Limit',
|
||||
subtitle: 'Maximum number of whitelisted addresses',
|
||||
placeholder: '1000',
|
||||
})
|
||||
|
||||
const perAddressLimitState = useNumberInputState({
|
||||
id: 'per-address-limit',
|
||||
name: 'perAddressLimit',
|
||||
title: 'Per Address Limit',
|
||||
subtitle: 'Maximum number of tokens per whitelisted address',
|
||||
placeholder: '5',
|
||||
})
|
||||
|
||||
const addressListState = useAddressListState()
|
||||
|
||||
const whitelistFileOnChange = (data: string[]) => {
|
||||
if (whitelistType === 'standard') setWhitelistStandardArray(data)
|
||||
if (whitelistType === 'merkletree') setWhitelistMerkleTreeArray(data)
|
||||
}
|
||||
|
||||
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
|
||||
setWhitelistFlexArray(whitelistData)
|
||||
}
|
||||
|
||||
const downloadSampleWhitelistFlexFile = () => {
|
||||
const csvData =
|
||||
'address,mint_count\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,3\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,1\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,2'
|
||||
const blob = new Blob([csvData], { type: 'text/csv' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', 'sample_whitelist_flex.csv')
|
||||
a.click()
|
||||
}
|
||||
|
||||
const downloadSampleWhitelistFile = () => {
|
||||
const txtData =
|
||||
'stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3'
|
||||
const blob = new Blob([txtData], { type: 'text/txt' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', 'sample_whitelist.txt')
|
||||
a.click()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!importedWhitelistDetails) {
|
||||
setWhitelistStandardArray([])
|
||||
setWhitelistFlexArray([])
|
||||
setWhitelistMerkleTreeArray([])
|
||||
}
|
||||
}, [whitelistType])
|
||||
|
||||
useEffect(() => {
|
||||
const data: WhitelistDetailsDataProps = {
|
||||
whitelistState,
|
||||
whitelistType,
|
||||
contractAddress: whitelistAddressState.value
|
||||
.toLowerCase()
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, ''),
|
||||
members:
|
||||
whitelistType === 'standard'
|
||||
? whitelistStandardArray
|
||||
: whitelistType === 'merkletree'
|
||||
? whitelistMerkleTreeArray
|
||||
: whitelistFlexArray,
|
||||
unitPrice: unitPriceState.value
|
||||
? (Number(unitPriceState.value) * 1_000_000).toString()
|
||||
: unitPriceState.value === 0
|
||||
? '0'
|
||||
: undefined,
|
||||
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,
|
||||
memberLimitState.value,
|
||||
perAddressLimitState.value,
|
||||
startDate,
|
||||
endDate,
|
||||
whitelistStandardArray,
|
||||
whitelistFlexArray,
|
||||
whitelistMerkleTreeArray,
|
||||
whitelistState,
|
||||
whitelistType,
|
||||
addressListState.values,
|
||||
adminsMutable,
|
||||
])
|
||||
|
||||
// make the necessary changes with respect to imported whitelist details
|
||||
useEffect(() => {
|
||||
if (importedWhitelistDetails) {
|
||||
setWhitelistState(importedWhitelistDetails.whitelistState)
|
||||
setWhitelistType(importedWhitelistDetails.whitelistType)
|
||||
whitelistAddressState.onChange(
|
||||
importedWhitelistDetails.contractAddress ? importedWhitelistDetails.contractAddress : '',
|
||||
)
|
||||
unitPriceState.onChange(
|
||||
importedWhitelistDetails.unitPrice ? Number(importedWhitelistDetails.unitPrice) / 1000000 : 0,
|
||||
)
|
||||
memberLimitState.onChange(importedWhitelistDetails.memberLimit ? importedWhitelistDetails.memberLimit : 0)
|
||||
perAddressLimitState.onChange(
|
||||
importedWhitelistDetails.perAddressLimit ? importedWhitelistDetails.perAddressLimit : 0,
|
||||
)
|
||||
setStartDate(
|
||||
importedWhitelistDetails.startTime
|
||||
? new Date(Number(importedWhitelistDetails.startTime) / 1_000_000)
|
||||
: undefined,
|
||||
)
|
||||
setEndDate(
|
||||
importedWhitelistDetails.endTime ? new Date(Number(importedWhitelistDetails.endTime) / 1_000_000) : undefined,
|
||||
)
|
||||
setAdminsMutable(importedWhitelistDetails.adminsMutable ? importedWhitelistDetails.adminsMutable : true)
|
||||
importedWhitelistDetails.admins?.forEach((admin) => {
|
||||
addressListState.reset()
|
||||
addressListState.add({ address: admin })
|
||||
})
|
||||
if (importedWhitelistDetails.whitelistType === 'standard') {
|
||||
setWhitelistStandardArray([])
|
||||
importedWhitelistDetails.members?.forEach((member) => {
|
||||
setWhitelistStandardArray((standardArray) => [...standardArray, member as string])
|
||||
})
|
||||
} else if (importedWhitelistDetails.whitelistType === 'merkletree') {
|
||||
setWhitelistMerkleTreeArray([])
|
||||
// importedWhitelistDetails.members?.forEach((member) => {
|
||||
// setWhitelistMerkleTreeArray((merkleTreeArray) => [...merkleTreeArray, member as string])
|
||||
// })
|
||||
} else if (importedWhitelistDetails.whitelistType === 'flex') {
|
||||
setWhitelistFlexArray([])
|
||||
importedWhitelistDetails.members?.forEach((member) => {
|
||||
setWhitelistFlexArray((flexArray) => [
|
||||
...flexArray,
|
||||
{
|
||||
address: (member as WhitelistFlexMember).address,
|
||||
mint_count: (member as WhitelistFlexMember).mint_count,
|
||||
},
|
||||
])
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importedWhitelistDetails])
|
||||
|
||||
useEffect(() => {
|
||||
if (whitelistState === 'new' && wallet.address) {
|
||||
addressListState.reset()
|
||||
addressListState.add({ address: wallet.address })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [whitelistState, wallet.address])
|
||||
|
||||
return (
|
||||
<div className="py-3 px-8 rounded border-2 border-white/20">
|
||||
<div className="flex justify-center">
|
||||
<div className="ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={whitelistState === 'none'}
|
||||
className="peer sr-only"
|
||||
id="whitelistRadio1"
|
||||
name="whitelistRadioOptions1"
|
||||
onClick={() => {
|
||||
setWhitelistState('none')
|
||||
setWhitelistType('standard')
|
||||
}}
|
||||
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="whitelistRadio1"
|
||||
>
|
||||
No whitelist
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={whitelistState === 'existing'}
|
||||
className="peer sr-only"
|
||||
id="whitelistRadio2"
|
||||
name="whitelistRadioOptions2"
|
||||
onClick={() => {
|
||||
setWhitelistState('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 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="whitelistRadio2"
|
||||
>
|
||||
Existing whitelist
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={whitelistState === 'new'}
|
||||
className="peer sr-only"
|
||||
id="whitelistRadio3"
|
||||
name="whitelistRadioOptions3"
|
||||
onClick={() => {
|
||||
setWhitelistState('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 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="whitelistRadio3"
|
||||
>
|
||||
New whitelist
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Conditional test={whitelistState === 'existing'}>
|
||||
<AddressInput {...whitelistAddressState} className="pb-5" isRequired />
|
||||
</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="standard"
|
||||
/>
|
||||
<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 className="form-check form-check-inline">
|
||||
<input
|
||||
checked={whitelistType === 'merkletree'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio9"
|
||||
name="inlineRadioOptions9"
|
||||
onClick={() => {
|
||||
setWhitelistType('merkletree')
|
||||
}}
|
||||
type="radio"
|
||||
value="merkletree"
|
||||
/>
|
||||
<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="inlineRadio9"
|
||||
>
|
||||
Whitelist Merkle Tree
|
||||
</label>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="grid grid-cols-2">
|
||||
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
|
||||
<NumberInput isRequired {...unitPriceState} />
|
||||
<Conditional test={whitelistType !== 'merkletree'}>
|
||||
<NumberInput isRequired {...memberLimitState} />
|
||||
</Conditional>
|
||||
<Conditional test={whitelistType === 'standard' || whitelistType === 'merkletree'}>
|
||||
<NumberInput isRequired {...perAddressLimitState} />
|
||||
</Conditional>
|
||||
<FormControl
|
||||
htmlId="start-date"
|
||||
isRequired
|
||||
subtitle="Start time for minting tokens to whitelisted addresses"
|
||||
title={`Whitelist Start Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setStartDate(
|
||||
timezone === 'Local'
|
||||
? date
|
||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setStartDate(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? startDate
|
||||
: startDate
|
||||
? new Date(startDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
htmlId="end-date"
|
||||
isRequired
|
||||
subtitle="Whitelist End Time dictates when public sales will start"
|
||||
title={`Whitelist End Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
||||
>
|
||||
<InputDateTime
|
||||
minDate={
|
||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
onChange={(date) =>
|
||||
date
|
||||
? setEndDate(
|
||||
timezone === 'Local'
|
||||
? date
|
||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
||||
)
|
||||
: setEndDate(undefined)
|
||||
}
|
||||
value={
|
||||
timezone === 'Local'
|
||||
? endDate
|
||||
: endDate
|
||||
? new Date(endDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</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}
|
||||
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={
|
||||
<div>
|
||||
<span>TXT file that contains the whitelisted addresses</span>
|
||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
|
||||
Download Sample File
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
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={
|
||||
<div>
|
||||
<span>CSV file that contains the whitelisted addresses and corresponding mint counts</span>
|
||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFlexFile}>
|
||||
Download Sample File
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title="Whitelist File"
|
||||
>
|
||||
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
|
||||
</FormGroup>
|
||||
<Conditional test={whitelistFlexArray.length > 0}>
|
||||
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
<Conditional test={whitelistType === 'merkletree'}>
|
||||
<FormGroup
|
||||
subtitle={
|
||||
<div>
|
||||
<span>TXT file that contains the whitelisted addresses</span>
|
||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
|
||||
Download Sample File
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
title="Whitelist File"
|
||||
>
|
||||
<WhitelistUpload onChange={whitelistFileOnChange} />
|
||||
</FormGroup>
|
||||
<Conditional test={whitelistStandardArray.length > 0}>
|
||||
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
324
config/authz.ts
324
config/authz.ts
@ -1,324 +0,0 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { GenericAuthorization } from 'cosmjs-types/cosmos/authz/v1beta1/authz'
|
||||
import { MsgGrant } from 'cosmjs-types/cosmos/authz/v1beta1/tx'
|
||||
import { SendAuthorization } from 'cosmjs-types/cosmos/bank/v1beta1/authz'
|
||||
import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'
|
||||
import type { AuthorizationType } from 'cosmjs-types/cosmos/staking/v1beta1/authz'
|
||||
import { StakeAuthorization, StakeAuthorization_Validators } from 'cosmjs-types/cosmos/staking/v1beta1/authz'
|
||||
import {
|
||||
AcceptedMessageKeysFilter,
|
||||
AllowAllMessagesFilter,
|
||||
CombinedLimit,
|
||||
ContractExecutionAuthorization,
|
||||
ContractMigrationAuthorization,
|
||||
MaxCallsLimit,
|
||||
MaxFundsLimit,
|
||||
} from 'cosmjs-types/cosmwasm/wasm/v1/authz'
|
||||
import type { AuthorizationMode, GenericAuthorizationType, GrantAuthorizationType } from 'pages/authz/grant'
|
||||
|
||||
export interface Msg {
|
||||
typeUrl: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export interface AuthzMessage {
|
||||
authzMode: AuthorizationMode
|
||||
authzType: GrantAuthorizationType
|
||||
displayName: string
|
||||
typeUrl: string
|
||||
genericAuthzType?: GenericAuthorizationType
|
||||
}
|
||||
|
||||
export const grantGenericStakeAuthorization: AuthzMessage = {
|
||||
authzMode: 'Grant',
|
||||
authzType: 'Generic',
|
||||
displayName: 'Stake',
|
||||
typeUrl: '/cosmos.staking.v1beta1.MsgDelegate',
|
||||
genericAuthzType: 'MsgDelegate',
|
||||
}
|
||||
|
||||
export const grantGenericSendAuthorization: AuthzMessage = {
|
||||
authzMode: 'Grant',
|
||||
authzType: 'Generic',
|
||||
displayName: 'Send',
|
||||
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
|
||||
genericAuthzType: 'MsgSend',
|
||||
}
|
||||
|
||||
export const authzMessages: AuthzMessage[] = [grantGenericStakeAuthorization, grantGenericSendAuthorization]
|
||||
|
||||
const msgAuthzGrantTypeUrl = '/cosmos.authz.v1beta1.MsgGrant'
|
||||
|
||||
export function AuthzSendGrantMsg(
|
||||
granter: string,
|
||||
grantee: string,
|
||||
denom: string,
|
||||
spendLimit: number,
|
||||
expiration: number,
|
||||
allowList?: string[],
|
||||
): Msg {
|
||||
const sendAuthValue = SendAuthorization.encode(
|
||||
SendAuthorization.fromPartial({
|
||||
spendLimit: [
|
||||
Coin.fromPartial({
|
||||
amount: String(spendLimit),
|
||||
denom,
|
||||
}),
|
||||
],
|
||||
// Needs cosmos-sdk >= 0.47
|
||||
// allowList,
|
||||
}),
|
||||
).finish()
|
||||
|
||||
const grantValue = MsgGrant.fromPartial({
|
||||
grant: {
|
||||
authorization: {
|
||||
typeUrl: '/cosmos.bank.v1beta1.SendAuthorization',
|
||||
value: sendAuthValue,
|
||||
},
|
||||
expiration: expiration ? { seconds: BigInt(expiration) } : undefined,
|
||||
},
|
||||
grantee,
|
||||
granter,
|
||||
})
|
||||
|
||||
return {
|
||||
typeUrl: msgAuthzGrantTypeUrl,
|
||||
value: grantValue,
|
||||
}
|
||||
}
|
||||
|
||||
export function AuthzExecuteContractGrantMsg(
|
||||
granter: string,
|
||||
grantee: string,
|
||||
contract: string,
|
||||
expiration: number,
|
||||
callsRemaining?: number,
|
||||
amounts?: Coin[],
|
||||
allowedMessages?: string[],
|
||||
): Msg {
|
||||
const sendAuthValue = ContractExecutionAuthorization.encode(
|
||||
ContractExecutionAuthorization.fromPartial({
|
||||
grants: [
|
||||
{
|
||||
contract,
|
||||
filter: {
|
||||
typeUrl: allowedMessages
|
||||
? '/cosmwasm.wasm.v1.AcceptedMessageKeysFilter'
|
||||
: '/cosmwasm.wasm.v1.AllowAllMessagesFilter',
|
||||
value: allowedMessages
|
||||
? AcceptedMessageKeysFilter.encode({ keys: allowedMessages }).finish()
|
||||
: AllowAllMessagesFilter.encode({}).finish(),
|
||||
},
|
||||
limit:
|
||||
callsRemaining || amounts
|
||||
? {
|
||||
typeUrl:
|
||||
callsRemaining && amounts
|
||||
? '/cosmwasm.wasm.v1.CombinedLimit'
|
||||
: callsRemaining
|
||||
? '/cosmwasm.wasm.v1.MaxCallsLimit'
|
||||
: '/cosmwasm.wasm.v1.MaxFundsLimit',
|
||||
value:
|
||||
callsRemaining && amounts
|
||||
? CombinedLimit.encode({
|
||||
callsRemaining: BigInt(callsRemaining),
|
||||
amounts,
|
||||
}).finish()
|
||||
: callsRemaining
|
||||
? MaxCallsLimit.encode({
|
||||
remaining: BigInt(callsRemaining),
|
||||
}).finish()
|
||||
: MaxFundsLimit.encode({
|
||||
amounts: amounts || [],
|
||||
}).finish(),
|
||||
}
|
||||
: {
|
||||
// limit: undefined is not accepted
|
||||
typeUrl: '/cosmwasm.wasm.v1.MaxCallsLimit',
|
||||
value: MaxCallsLimit.encode({
|
||||
remaining: BigInt(100000),
|
||||
}).finish(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).finish()
|
||||
|
||||
const grantValue = MsgGrant.fromPartial({
|
||||
grant: {
|
||||
authorization: {
|
||||
typeUrl: '/cosmwasm.wasm.v1.ContractExecutionAuthorization',
|
||||
value: sendAuthValue,
|
||||
},
|
||||
expiration: expiration ? { seconds: BigInt(expiration), nanos: 0 } : undefined,
|
||||
},
|
||||
grantee,
|
||||
granter,
|
||||
})
|
||||
|
||||
return {
|
||||
typeUrl: msgAuthzGrantTypeUrl,
|
||||
value: grantValue,
|
||||
}
|
||||
}
|
||||
|
||||
export function AuthzMigrateContractGrantMsg(
|
||||
granter: string,
|
||||
grantee: string,
|
||||
contract: string,
|
||||
expiration: number,
|
||||
callsRemaining?: number,
|
||||
amounts?: Coin[],
|
||||
allowedMessages?: string[],
|
||||
): Msg {
|
||||
const sendAuthValue = ContractMigrationAuthorization.encode(
|
||||
ContractMigrationAuthorization.fromPartial({
|
||||
grants: [
|
||||
{
|
||||
contract,
|
||||
filter: {
|
||||
typeUrl: allowedMessages
|
||||
? '/cosmwasm.wasm.v1.AcceptedMessageKeysFilter'
|
||||
: '/cosmwasm.wasm.v1.AllowAllMessagesFilter',
|
||||
value: allowedMessages
|
||||
? AcceptedMessageKeysFilter.encode({ keys: allowedMessages }).finish()
|
||||
: AllowAllMessagesFilter.encode({}).finish(),
|
||||
},
|
||||
limit:
|
||||
callsRemaining || amounts
|
||||
? {
|
||||
typeUrl:
|
||||
callsRemaining && amounts
|
||||
? '/cosmwasm.wasm.v1.CombinedLimit'
|
||||
: callsRemaining
|
||||
? '/cosmwasm.wasm.v1.MaxCallsLimit'
|
||||
: '/cosmwasm.wasm.v1.MaxFundsLimit',
|
||||
value:
|
||||
callsRemaining && amounts
|
||||
? CombinedLimit.encode({
|
||||
callsRemaining: BigInt(callsRemaining),
|
||||
amounts,
|
||||
}).finish()
|
||||
: callsRemaining
|
||||
? MaxCallsLimit.encode({
|
||||
remaining: BigInt(callsRemaining),
|
||||
}).finish()
|
||||
: MaxFundsLimit.encode({
|
||||
amounts: amounts || [],
|
||||
}).finish(),
|
||||
}
|
||||
: {
|
||||
// limit: undefined is not accepted
|
||||
typeUrl: '/cosmwasm.wasm.v1.MaxCallsLimit',
|
||||
value: MaxCallsLimit.encode({
|
||||
remaining: BigInt(100000),
|
||||
}).finish(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).finish()
|
||||
|
||||
const grantValue = MsgGrant.fromPartial({
|
||||
grant: {
|
||||
authorization: {
|
||||
typeUrl: '/cosmwasm.wasm.v1.ContractMigrationAuthorization',
|
||||
value: sendAuthValue,
|
||||
},
|
||||
expiration: expiration ? { seconds: BigInt(expiration), nanos: 0 } : undefined,
|
||||
},
|
||||
grantee,
|
||||
granter,
|
||||
})
|
||||
|
||||
return {
|
||||
typeUrl: msgAuthzGrantTypeUrl,
|
||||
value: grantValue,
|
||||
}
|
||||
}
|
||||
|
||||
export function AuthzGenericGrantMsg(granter: string, grantee: string, typeURL: string, expiration: number): Msg {
|
||||
return {
|
||||
typeUrl: msgAuthzGrantTypeUrl,
|
||||
value: {
|
||||
grant: {
|
||||
authorization: {
|
||||
typeUrl: '/cosmos.authz.v1beta1.GenericAuthorization',
|
||||
value: GenericAuthorization.encode(
|
||||
GenericAuthorization.fromPartial({
|
||||
msg: typeURL,
|
||||
}),
|
||||
).finish(),
|
||||
},
|
||||
expiration: expiration ? { seconds: expiration } : undefined,
|
||||
},
|
||||
grantee,
|
||||
granter,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function AuthzStakeGrantMsg({
|
||||
expiration,
|
||||
grantee,
|
||||
granter,
|
||||
allowList,
|
||||
denyList,
|
||||
maxTokens,
|
||||
denom,
|
||||
stakeAuthzType,
|
||||
}: {
|
||||
granter: string
|
||||
grantee: string
|
||||
expiration: number
|
||||
allowList?: string[]
|
||||
denyList?: string[]
|
||||
maxTokens?: string
|
||||
denom?: string
|
||||
stakeAuthzType: AuthorizationType
|
||||
}): Msg {
|
||||
const allow_list = StakeAuthorization_Validators.encode(
|
||||
StakeAuthorization_Validators.fromPartial({
|
||||
address: allowList,
|
||||
}),
|
||||
).finish()
|
||||
const deny_list = StakeAuthorization_Validators.encode(
|
||||
StakeAuthorization_Validators.fromPartial({
|
||||
address: denyList,
|
||||
}),
|
||||
).finish()
|
||||
const stakeAuthValue = StakeAuthorization.encode(
|
||||
StakeAuthorization.fromPartial({
|
||||
authorizationType: stakeAuthzType,
|
||||
allowList: allowList?.length ? StakeAuthorization_Validators.decode(allow_list) : undefined,
|
||||
denyList: denyList?.length ? StakeAuthorization_Validators.decode(deny_list) : undefined,
|
||||
maxTokens: maxTokens
|
||||
? Coin.fromPartial({
|
||||
amount: maxTokens,
|
||||
denom,
|
||||
})
|
||||
: undefined,
|
||||
}),
|
||||
).finish()
|
||||
const grantValue = MsgGrant.fromPartial({
|
||||
grant: {
|
||||
authorization: {
|
||||
typeUrl: '/cosmos.staking.v1beta1.StakeAuthorization',
|
||||
value: stakeAuthValue,
|
||||
},
|
||||
expiration: { seconds: BigInt(expiration) },
|
||||
},
|
||||
grantee,
|
||||
granter,
|
||||
})
|
||||
|
||||
return {
|
||||
typeUrl: msgAuthzGrantTypeUrl,
|
||||
value: grantValue,
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"path": "/assets/",
|
||||
"appName": "Stargaze Studio",
|
||||
"appShortName": "Stargaze Studio",
|
||||
"appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT collection in no time.",
|
||||
"developerName": "Stargaze Studio",
|
||||
"appName": "StargazeStudio",
|
||||
"appShortName": "StargazeStudio",
|
||||
"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",
|
||||
"theme_color": "#FFC27D",
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './app'
|
||||
export * from './keplr'
|
||||
export * from './network'
|
||||
|
||||
76
config/keplr.ts
Normal file
76
config/keplr.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type { ChainInfo } from '@keplr-wallet/types'
|
||||
|
||||
import type { AppConfig } from './app'
|
||||
|
||||
export interface KeplrCoin {
|
||||
readonly coinDenom: string
|
||||
readonly coinMinimalDenom: string
|
||||
readonly coinDecimals: number
|
||||
}
|
||||
|
||||
export interface KeplrConfig {
|
||||
readonly chainId: string
|
||||
readonly chainName: string
|
||||
readonly rpc: string
|
||||
readonly rest?: string
|
||||
readonly bech32Config: {
|
||||
readonly bech32PrefixAccAddr: string
|
||||
readonly bech32PrefixAccPub: string
|
||||
readonly bech32PrefixValAddr: string
|
||||
readonly bech32PrefixValPub: string
|
||||
readonly bech32PrefixConsAddr: string
|
||||
readonly bech32PrefixConsPub: string
|
||||
}
|
||||
readonly currencies: readonly KeplrCoin[]
|
||||
readonly feeCurrencies: readonly KeplrCoin[]
|
||||
readonly stakeCurrency: KeplrCoin
|
||||
readonly gasPriceStep: {
|
||||
readonly low: number
|
||||
readonly average: number
|
||||
readonly high: number
|
||||
}
|
||||
readonly bip44: { readonly coinType: number }
|
||||
readonly coinType: number
|
||||
}
|
||||
|
||||
export const keplrConfig = (config: AppConfig): ChainInfo => ({
|
||||
chainId: config.chainId,
|
||||
chainName: config.chainName,
|
||||
rpc: config.rpcUrl,
|
||||
rest: config.httpUrl!,
|
||||
bech32Config: {
|
||||
bech32PrefixAccAddr: `${config.addressPrefix}`,
|
||||
bech32PrefixAccPub: `${config.addressPrefix}pub`,
|
||||
bech32PrefixValAddr: `${config.addressPrefix}valoper`,
|
||||
bech32PrefixValPub: `${config.addressPrefix}valoperpub`,
|
||||
bech32PrefixConsAddr: `${config.addressPrefix}valcons`,
|
||||
bech32PrefixConsPub: `${config.addressPrefix}valconspub`,
|
||||
},
|
||||
currencies: [
|
||||
{
|
||||
coinDenom: config.coinMap[config.feeToken].denom,
|
||||
coinMinimalDenom: config.feeToken,
|
||||
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
|
||||
},
|
||||
],
|
||||
feeCurrencies: [
|
||||
{
|
||||
coinDenom: config.coinMap[config.feeToken].denom,
|
||||
coinMinimalDenom: config.feeToken,
|
||||
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
|
||||
},
|
||||
],
|
||||
stakeCurrency: {
|
||||
coinDenom: config.coinMap[config.stakingToken].denom,
|
||||
coinMinimalDenom: config.stakingToken,
|
||||
coinDecimals: config.coinMap[config.stakingToken].fractionalDigits,
|
||||
},
|
||||
gasPriceStep: {
|
||||
low: config.gasPrice / 2,
|
||||
average: config.gasPrice,
|
||||
high: config.gasPrice * 2,
|
||||
},
|
||||
bip44: { coinType: 118 },
|
||||
coinType: 118,
|
||||
features: ['ibc-transfer', 'cosmwasm', 'ibc-go'],
|
||||
})
|
||||
@ -6,6 +6,6 @@ export const meta = {
|
||||
domain: 'stargaze.tools',
|
||||
url: faviconsJson.developerURL,
|
||||
twitter: {
|
||||
username: '@StargazeZone',
|
||||
username: '@stargazestudio',
|
||||
},
|
||||
}
|
||||
|
||||
844
config/minter.ts
844
config/minter.ts
@ -1,844 +0,0 @@
|
||||
import {
|
||||
FEATURED_IBC_TIA_FACTORY_ADDRESS,
|
||||
FEATURED_IBC_USDC_FACTORY_ADDRESS,
|
||||
FEATURED_VENDING_FACTORY_ADDRESS,
|
||||
FEATURED_VENDING_FACTORY_FLEX_ADDRESS,
|
||||
FEATURED_VENDING_FACTORY_MERKLE_TREE_ADDRESS,
|
||||
FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
||||
FEATURED_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
|
||||
FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
||||
OPEN_EDITION_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_FACTORY_FLEX_ADDRESS,
|
||||
OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
||||
OPEN_EDITION_IBC_CRBRUS_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_KUJI_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_NBTC_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_TIA_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
||||
OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
||||
OPEN_EDITION_IBC_USK_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_NATIVE_BRNCH_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_NATIVE_STRDST_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_NBTC_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_IBC_TIA_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS,
|
||||
VENDING_FACTORY_ADDRESS,
|
||||
VENDING_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_FACTORY_MERKLE_TREE_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_CRBRUS_FACTORY_ADDRESS,
|
||||
VENDING_IBC_CRBRUS_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_KUJI_FACTORY_ADDRESS,
|
||||
VENDING_IBC_KUJI_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_NBTC_FACTORY_ADDRESS,
|
||||
VENDING_IBC_NBTC_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_NBTC_UPDATABLE_FACTORY_ADDRESS,
|
||||
VENDING_IBC_NBTC_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_TIA_FACTORY_ADDRESS,
|
||||
VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
|
||||
VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS,
|
||||
VENDING_IBC_TIA_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,
|
||||
VENDING_IBC_USK_FACTORY_ADDRESS,
|
||||
VENDING_IBC_USK_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_IBC_USK_UPDATABLE_FACTORY_ADDRESS,
|
||||
VENDING_IBC_USK_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
VENDING_NATIVE_BRNCH_FACTORY_ADDRESS,
|
||||
VENDING_NATIVE_BRNCH_FLEX_FACTORY_ADDRESS,
|
||||
VENDING_NATIVE_BRNCH_UPDATABLE_FACTORY_ADDRESS,
|
||||
VENDING_NATIVE_STARDUST_FACTORY_ADDRESS,
|
||||
VENDING_NATIVE_STARDUST_UPDATABLE_FACTORY_ADDRESS,
|
||||
VENDING_NATIVE_STRDST_FLEX_FACTORY_ADDRESS,
|
||||
} from 'utils/constants'
|
||||
|
||||
import type { TokenInfo } from './token'
|
||||
import {
|
||||
ibcAtom,
|
||||
ibcCrbrus,
|
||||
ibcFrnz,
|
||||
// ibcHuahua,
|
||||
ibcKuji,
|
||||
ibcNbtc,
|
||||
ibcTia,
|
||||
ibcUsdc,
|
||||
ibcUsk,
|
||||
nativeBrnch,
|
||||
nativeStardust,
|
||||
stars,
|
||||
} from './token'
|
||||
|
||||
export interface MinterInfo {
|
||||
id: string
|
||||
factoryAddress: string
|
||||
supportedToken: TokenInfo
|
||||
updatable?: boolean
|
||||
flexible?: boolean
|
||||
merkleTree?: boolean
|
||||
featured?: boolean
|
||||
}
|
||||
|
||||
export const openEditionStarsMinter: MinterInfo = {
|
||||
id: 'open-edition-stars-minter',
|
||||
factoryAddress: OPEN_EDITION_FACTORY_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableStarsMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-stars-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: true,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionIbcAtomMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-atom-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcAtomMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-atom-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: true,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionIbcUsdcMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-usdc-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionIbcTiaMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-tia-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_TIA_FACTORY_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionIbcNbtcMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-nbtc-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_NBTC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcNbtc,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcUsdcMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-usdc-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: true,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcTiaMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-tia-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_TIA_FACTORY_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: true,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcNbtcMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-nbtc-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_NBTC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcNbtc,
|
||||
updatable: true,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionIbcFrnzMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-frnz-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
|
||||
supportedToken: ibcFrnz,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcFrnzMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-frnz-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS,
|
||||
supportedToken: ibcFrnz,
|
||||
updatable: true,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionIbcUskMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-usk-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_USK_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsk,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionUpdatableIbcUskMinter: MinterInfo = {
|
||||
id: 'open-edition-updatable-ibc-usk-minter',
|
||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsk,
|
||||
updatable: true,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionIbcKujiMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-kuji-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_KUJI_FACTORY_ADDRESS,
|
||||
supportedToken: ibcKuji,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
// export const openEditionIbcHuahuaMinter: MinterInfo = {
|
||||
// id: 'open-edition-ibc-huahua-minter',
|
||||
// factoryAddress: OPEN_EDITION_IBC_HUAHUA_FACTORY_ADDRESS,
|
||||
// supportedToken: ibcHuahua,
|
||||
// updatable: false,
|
||||
// featured: false,
|
||||
// }
|
||||
|
||||
export const openEditionIbcCrbrusMinter: MinterInfo = {
|
||||
id: 'open-edition-ibc-crbrus-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_CRBRUS_FACTORY_ADDRESS,
|
||||
supportedToken: ibcCrbrus,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionNativeStrdstMinter: MinterInfo = {
|
||||
id: 'open-edition-native-strdst-minter',
|
||||
factoryAddress: OPEN_EDITION_NATIVE_STRDST_FACTORY_ADDRESS,
|
||||
supportedToken: nativeStardust,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionNativeBrnchMinter: MinterInfo = {
|
||||
id: 'open-edition-native-brnch-minter',
|
||||
factoryAddress: OPEN_EDITION_NATIVE_BRNCH_FACTORY_ADDRESS,
|
||||
supportedToken: nativeBrnch,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: false,
|
||||
}
|
||||
|
||||
export const openEditionMinterList = [
|
||||
openEditionStarsMinter,
|
||||
openEditionUpdatableStarsMinter,
|
||||
openEditionUpdatableIbcAtomMinter,
|
||||
openEditionIbcAtomMinter,
|
||||
openEditionIbcFrnzMinter,
|
||||
openEditionUpdatableIbcFrnzMinter,
|
||||
openEditionIbcUsdcMinter,
|
||||
openEditionUpdatableIbcUsdcMinter,
|
||||
openEditionIbcTiaMinter,
|
||||
openEditionUpdatableIbcTiaMinter,
|
||||
openEditionIbcNbtcMinter,
|
||||
openEditionUpdatableIbcNbtcMinter,
|
||||
openEditionIbcUskMinter,
|
||||
openEditionUpdatableIbcUskMinter,
|
||||
openEditionIbcKujiMinter,
|
||||
// openEditionIbcHuahuaMinter,
|
||||
openEditionIbcCrbrusMinter,
|
||||
openEditionNativeStrdstMinter,
|
||||
openEditionNativeBrnchMinter,
|
||||
]
|
||||
|
||||
export const flexibleOpenEditionStarsMinter: MinterInfo = {
|
||||
id: 'flexible-open-edition-stars-minter',
|
||||
factoryAddress: OPEN_EDITION_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleOpenEditionIbcAtomMinter: MinterInfo = {
|
||||
id: 'flexible-open-edition-ibc-atom-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleOpenEditionIbcUsdcMinter: MinterInfo = {
|
||||
id: 'flexible-open-edition-ibc-usdc-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleOpenEditionIbcTiaMinter: MinterInfo = {
|
||||
id: 'flexible-open-edition-ibc-tia-minter',
|
||||
factoryAddress: OPEN_EDITION_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: false,
|
||||
featured: false,
|
||||
flexible: true,
|
||||
}
|
||||
|
||||
export const flexibleOpenEditionMinterList = [
|
||||
flexibleOpenEditionStarsMinter,
|
||||
flexibleOpenEditionIbcAtomMinter,
|
||||
flexibleOpenEditionIbcUsdcMinter,
|
||||
flexibleOpenEditionIbcTiaMinter,
|
||||
]
|
||||
|
||||
export const vendingStarsMinter: MinterInfo = {
|
||||
id: 'vending-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingFeaturedStarsMinter: MinterInfo = {
|
||||
id: 'vending-stars-minter',
|
||||
factoryAddress: FEATURED_VENDING_FACTORY_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: true,
|
||||
}
|
||||
|
||||
export const vendingUpdatableStarsMinter: MinterInfo = {
|
||||
id: 'vending-updatable-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_UPDATABLE_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingIbcAtomMinter: MinterInfo = {
|
||||
id: 'vending-ibc-atom-minter',
|
||||
factoryAddress: VENDING_IBC_ATOM_FACTORY_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableIbcAtomMinter: MinterInfo = {
|
||||
id: 'vending-updatable-ibc-atom-minter',
|
||||
factoryAddress: VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingIbcUsdcMinter: MinterInfo = {
|
||||
id: 'vending-ibc-usdc-minter',
|
||||
factoryAddress: VENDING_IBC_USDC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingFeaturedIbcUsdcMinter: MinterInfo = {
|
||||
id: 'vending-featured-ibc-usdc-minter',
|
||||
factoryAddress: FEATURED_IBC_USDC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: true,
|
||||
}
|
||||
|
||||
export const vendingIbcTiaMinter: MinterInfo = {
|
||||
id: 'vending-ibc-tia-minter',
|
||||
factoryAddress: VENDING_IBC_TIA_FACTORY_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingFeaturedIbcTiaMinter: MinterInfo = {
|
||||
id: 'vending-featured-ibc-tia-minter',
|
||||
factoryAddress: FEATURED_IBC_TIA_FACTORY_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: true,
|
||||
}
|
||||
|
||||
export const vendingIbcNbtcMinter: MinterInfo = {
|
||||
id: 'vending-ibc-nbtc-minter',
|
||||
factoryAddress: VENDING_IBC_NBTC_FACTORY_ADDRESS,
|
||||
supportedToken: ibcNbtc,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableIbcUsdcMinter: MinterInfo = {
|
||||
id: 'vending-updatable-ibc-usdc-minter',
|
||||
factoryAddress: VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableIbcTiaMinter: MinterInfo = {
|
||||
id: 'vending-updatable-ibc-tia-minter',
|
||||
factoryAddress: VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableIbcNbtcMinter: MinterInfo = {
|
||||
id: 'vending-updatable-ibc-nbtc-minter',
|
||||
factoryAddress: VENDING_IBC_NBTC_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: ibcNbtc,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingIbcUskMinter: MinterInfo = {
|
||||
id: 'vending-ibc-usk-minter',
|
||||
factoryAddress: VENDING_IBC_USK_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsk,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableIbcUskMinter: MinterInfo = {
|
||||
id: 'vending-updatable-ibc-usk-minter',
|
||||
factoryAddress: VENDING_IBC_USK_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: ibcUsk,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingIbcKujiMinter: MinterInfo = {
|
||||
id: 'vending-ibc-kuji-minter',
|
||||
factoryAddress: VENDING_IBC_KUJI_FACTORY_ADDRESS,
|
||||
supportedToken: ibcKuji,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
// export const vendingIbcHuahuaMinter: MinterInfo = {
|
||||
// id: 'vending-ibc-huahua-minter',
|
||||
// factoryAddress: VENDING_IBC_HUAHUA_FACTORY_ADDRESS,
|
||||
// supportedToken: ibcHuahua,
|
||||
// updatable: false,
|
||||
// flexible: false,
|
||||
// merkleTree: false,
|
||||
// featured: false,
|
||||
// }
|
||||
|
||||
export const vendingIbcCrbrusMinter: MinterInfo = {
|
||||
id: 'vending-ibc-crbrus-minter',
|
||||
factoryAddress: VENDING_IBC_CRBRUS_FACTORY_ADDRESS,
|
||||
supportedToken: ibcCrbrus,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingNativeStardustMinter: MinterInfo = {
|
||||
id: 'vending-native-stardust-minter',
|
||||
factoryAddress: VENDING_NATIVE_STARDUST_FACTORY_ADDRESS,
|
||||
supportedToken: nativeStardust,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableNativeStardustMinter: MinterInfo = {
|
||||
id: 'vending-native-stardust-minter',
|
||||
factoryAddress: VENDING_NATIVE_STARDUST_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: nativeStardust,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingNativeBrnchMinter: MinterInfo = {
|
||||
id: 'vending-native-brnch-minter',
|
||||
factoryAddress: VENDING_NATIVE_BRNCH_FACTORY_ADDRESS,
|
||||
supportedToken: nativeBrnch,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingUpdatableNativeBrnchMinter: MinterInfo = {
|
||||
id: 'vending-native-brnch-minter',
|
||||
factoryAddress: VENDING_NATIVE_BRNCH_UPDATABLE_FACTORY_ADDRESS,
|
||||
supportedToken: nativeBrnch,
|
||||
updatable: true,
|
||||
flexible: false,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const vendingMinterList = [
|
||||
vendingStarsMinter,
|
||||
vendingFeaturedStarsMinter,
|
||||
vendingUpdatableStarsMinter,
|
||||
vendingIbcAtomMinter,
|
||||
vendingUpdatableIbcAtomMinter,
|
||||
vendingIbcUsdcMinter,
|
||||
vendingFeaturedIbcUsdcMinter,
|
||||
vendingUpdatableIbcUsdcMinter,
|
||||
vendingIbcTiaMinter,
|
||||
vendingFeaturedIbcTiaMinter,
|
||||
vendingUpdatableIbcTiaMinter,
|
||||
vendingIbcNbtcMinter,
|
||||
vendingUpdatableIbcNbtcMinter,
|
||||
vendingIbcUskMinter,
|
||||
vendingUpdatableIbcUskMinter,
|
||||
vendingIbcKujiMinter,
|
||||
// vendingIbcHuahuaMinter,
|
||||
vendingIbcCrbrusMinter,
|
||||
vendingNativeStardustMinter,
|
||||
vendingUpdatableNativeStardustMinter,
|
||||
vendingNativeBrnchMinter,
|
||||
vendingUpdatableNativeBrnchMinter,
|
||||
]
|
||||
|
||||
export const flexibleVendingStarsMinter: MinterInfo = {
|
||||
id: 'flexible-vending-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleFeaturedVendingStarsMinter: MinterInfo = {
|
||||
id: 'flexible-vending-stars-minter',
|
||||
factoryAddress: FEATURED_VENDING_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingUpdatableStarsMinter: MinterInfo = {
|
||||
id: 'flexible-vending-updatable-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: true,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingIbcAtomMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-atom-minter',
|
||||
factoryAddress: VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcAtom,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
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,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingIbcUsdcMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-usdc-minter',
|
||||
factoryAddress: VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleFeaturedVendingIbcUsdcMinter: MinterInfo = {
|
||||
id: 'flexible-featured-vending-ibc-usdc-minter',
|
||||
factoryAddress: FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcUsdc,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingIbcTiaMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-tia-minter',
|
||||
factoryAddress: VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleFeaturedVendingIbcTiaMinter: MinterInfo = {
|
||||
id: 'flexible-featured-vending-ibc-tia-minter',
|
||||
factoryAddress: FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: true,
|
||||
}
|
||||
|
||||
export const flexibleVendingIbcNbtcMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-nbtc-minter',
|
||||
factoryAddress: VENDING_IBC_NBTC_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcNbtc,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
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,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingUpdatableIbcTiaMinter: MinterInfo = {
|
||||
id: 'flexible-vending-updatable-ibc-tia-minter',
|
||||
factoryAddress: VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: true,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingUpdatableIbcNbtcMinter: MinterInfo = {
|
||||
id: 'flexible-vending-updatable-ibc-nbtc-minter',
|
||||
factoryAddress: VENDING_IBC_NBTC_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcNbtc,
|
||||
updatable: true,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingIbcUskMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-usk-minter',
|
||||
factoryAddress: VENDING_IBC_USK_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcUsk,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingUpdatableIbcUskMinter: MinterInfo = {
|
||||
id: 'flexible-vending-updatable-ibc-usk-minter',
|
||||
factoryAddress: VENDING_IBC_USK_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcUsk,
|
||||
updatable: true,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingIbcKujiMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-kuji-minter',
|
||||
factoryAddress: VENDING_IBC_KUJI_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcKuji,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
// export const flexibleVendingIbcHuahuaMinter: MinterInfo = {
|
||||
// id: 'flexible-vending-ibc-huahua-minter',
|
||||
// factoryAddress: VENDING_IBC_HUAHUA_FACTORY_FLEX_ADDRESS,
|
||||
// supportedToken: ibcHuahua,
|
||||
// updatable: false,
|
||||
// flexible: true,
|
||||
// merkleTree: false,
|
||||
// featured: false,
|
||||
// }
|
||||
|
||||
export const flexibleVendingIbcCrbrusMinter: MinterInfo = {
|
||||
id: 'flexible-vending-ibc-crbrus-minter',
|
||||
factoryAddress: VENDING_IBC_CRBRUS_FACTORY_FLEX_ADDRESS,
|
||||
supportedToken: ibcCrbrus,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingStrdstMinter: MinterInfo = {
|
||||
id: 'flexible-vending-native-strdst-minter',
|
||||
factoryAddress: VENDING_NATIVE_STRDST_FLEX_FACTORY_ADDRESS,
|
||||
supportedToken: nativeStardust,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingBrnchMinter: MinterInfo = {
|
||||
id: 'flexible-vending-native-brnch-minter',
|
||||
factoryAddress: VENDING_NATIVE_BRNCH_FLEX_FACTORY_ADDRESS,
|
||||
supportedToken: nativeBrnch,
|
||||
updatable: false,
|
||||
flexible: true,
|
||||
merkleTree: false,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const flexibleVendingMinterList = [
|
||||
flexibleVendingStarsMinter,
|
||||
flexibleFeaturedVendingStarsMinter,
|
||||
flexibleVendingUpdatableStarsMinter,
|
||||
flexibleVendingIbcAtomMinter,
|
||||
flexibleVendingUpdatableIbcAtomMinter,
|
||||
flexibleVendingIbcUsdcMinter,
|
||||
flexibleFeaturedVendingIbcUsdcMinter,
|
||||
flexibleVendingUpdatableIbcUsdcMinter,
|
||||
flexibleVendingIbcTiaMinter,
|
||||
flexibleFeaturedVendingIbcTiaMinter,
|
||||
flexibleVendingUpdatableIbcTiaMinter,
|
||||
flexibleVendingIbcNbtcMinter,
|
||||
flexibleVendingUpdatableIbcNbtcMinter,
|
||||
flexibleVendingIbcUskMinter,
|
||||
flexibleVendingUpdatableIbcUskMinter,
|
||||
flexibleVendingIbcKujiMinter,
|
||||
// flexibleVendingIbcHuahuaMinter,
|
||||
flexibleVendingIbcCrbrusMinter,
|
||||
flexibleVendingStrdstMinter,
|
||||
flexibleVendingBrnchMinter,
|
||||
]
|
||||
|
||||
export const merkleTreeVendingStarsMinter: MinterInfo = {
|
||||
id: 'merkletree-vending-stars-minter',
|
||||
factoryAddress: VENDING_FACTORY_MERKLE_TREE_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: true,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const merkleTreeVendingFeaturedStarsMinter: MinterInfo = {
|
||||
id: 'merkletree-vending-featured-stars-minter',
|
||||
factoryAddress: FEATURED_VENDING_FACTORY_MERKLE_TREE_ADDRESS,
|
||||
supportedToken: stars,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: true,
|
||||
featured: true,
|
||||
}
|
||||
|
||||
export const merkleTreeVendingIbcTiaMinter: MinterInfo = {
|
||||
id: 'merkletree-vending-ibc-tia-minter',
|
||||
factoryAddress: VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: true,
|
||||
featured: false,
|
||||
}
|
||||
|
||||
export const merkleTreeVendingFeaturedIbcTiaMinter: MinterInfo = {
|
||||
id: 'merkletree-vending-featured-ibc-tia-minter',
|
||||
factoryAddress: FEATURED_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
|
||||
supportedToken: ibcTia,
|
||||
updatable: false,
|
||||
flexible: false,
|
||||
merkleTree: true,
|
||||
featured: true,
|
||||
}
|
||||
|
||||
export const merkleTreeVendingMinterList = [
|
||||
merkleTreeVendingStarsMinter,
|
||||
merkleTreeVendingIbcTiaMinter,
|
||||
merkleTreeVendingFeaturedStarsMinter,
|
||||
merkleTreeVendingFeaturedIbcTiaMinter,
|
||||
]
|
||||
@ -5,7 +5,6 @@ export const mainnetConfig: AppConfig = {
|
||||
chainName: 'Stargaze',
|
||||
addressPrefix: 'stars',
|
||||
rpcUrl: 'https://rpc.stargaze-apis.com/',
|
||||
httpUrl: 'https://rest.stargaze-apis.com/',
|
||||
feeToken: 'ustars',
|
||||
stakingToken: 'ustars',
|
||||
coinMap: {
|
||||
|
||||
135
config/token.ts
135
config/token.ts
@ -1,135 +0,0 @@
|
||||
import { NETWORK } from 'utils/constants'
|
||||
|
||||
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:
|
||||
NETWORK === 'mainnet'
|
||||
? 'ibc/4A1C18CA7F50544760CF306189B810CE4C1CB156C7FC870143D401FE7280E591'
|
||||
: 'factory/stars1paqkeyluuw47pflgwwqaaj8y679zj96aatg5a7/uusdc',
|
||||
displayName: 'USDC',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
export const ibcUsk: TokenInfo = {
|
||||
id: 'ibc-usk',
|
||||
denom:
|
||||
NETWORK === 'mainnet'
|
||||
? 'ibc/938CEB62ABCBA6366AA369A8362E310B2A0B1A54835E4F3FF01D69D860959128'
|
||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uusk',
|
||||
displayName: 'USK',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const ibcKuji: TokenInfo = {
|
||||
id: 'ibc-kuji',
|
||||
denom:
|
||||
NETWORK === 'mainnet'
|
||||
? 'ibc/0E57658B71E9CC4BB0F6FE3E01712966713B49E6FD292E6B66E3F111B103D361'
|
||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/ukuji',
|
||||
displayName: 'KUJI',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const ibcFrnz: TokenInfo = {
|
||||
id: 'ibc-frnz',
|
||||
denom:
|
||||
NETWORK === 'mainnet'
|
||||
? 'ibc/9C40A8368C0E1CAA4144DBDEBBCE2E7A5CC2D128F0A9F785ECB71ECFF575114C'
|
||||
: 'factory/stars1paqkeyluuw47pflgwwqaaj8y679zj96aatg5a7/ufrienzies',
|
||||
displayName: 'FRNZ',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const ibcNbtc: TokenInfo = {
|
||||
id: 'ibc-nBTC',
|
||||
denom: NETWORK === 'mainnet' ? 'Not available' : 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/unbtc',
|
||||
displayName: 'nBTC',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
// export const ibcHuahua: TokenInfo = {
|
||||
// id: 'ibc-huahua',
|
||||
// denom:
|
||||
// NETWORK === 'mainnet'
|
||||
// ? 'ibc/CAD8A9F306CAAC55731C66930D6BEE539856DD12E59061C965E44D82AA26A0E7'
|
||||
// : 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uhuahua',
|
||||
// displayName: 'HUAHUA',
|
||||
// decimalPlaces: 6,
|
||||
// }
|
||||
|
||||
export const ibcCrbrus: TokenInfo = {
|
||||
id: 'ibc-crbrus',
|
||||
denom:
|
||||
NETWORK === 'mainnet'
|
||||
? 'ibc/71CEEB5CC09F75A3ACDC417108C14514351B6B2A540ACE9B37A80BF930845134'
|
||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uCRBRUS',
|
||||
displayName: 'CRBRUS',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const ibcTia: TokenInfo = {
|
||||
id: 'ibc-tia',
|
||||
denom:
|
||||
NETWORK === 'mainnet'
|
||||
? 'ibc/14D1406D84227FDF4B055EA5CB2298095BBCA3F3BC3EF583AE6DF36F0FB179C8'
|
||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/utia',
|
||||
displayName: 'TIA',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const nativeStardust: TokenInfo = {
|
||||
id: 'native-strdst',
|
||||
denom:
|
||||
NETWORK === 'mainnet'
|
||||
? 'factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust'
|
||||
: 'factory/stars18vxuarvh44wxltxqsyac36972nvaqc377sdh40/dust',
|
||||
displayName: 'STRDST',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const nativeBrnch: TokenInfo = {
|
||||
id: 'native-brnch',
|
||||
denom:
|
||||
NETWORK === 'mainnet'
|
||||
? 'factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/uBRNCH'
|
||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uBRNCH',
|
||||
displayName: 'BRNCH',
|
||||
decimalPlaces: 6,
|
||||
}
|
||||
|
||||
export const tokensList = [
|
||||
stars,
|
||||
ibcAtom,
|
||||
ibcUsdc,
|
||||
ibcUsk,
|
||||
ibcFrnz,
|
||||
ibcNbtc,
|
||||
ibcKuji,
|
||||
// ibcHuahua,
|
||||
ibcCrbrus,
|
||||
ibcTia,
|
||||
nativeStardust,
|
||||
nativeBrnch,
|
||||
]
|
||||
@ -1,4 +1,4 @@
|
||||
import { create } from 'zustand'
|
||||
import create from 'zustand'
|
||||
|
||||
export const useCollectionStore = create(() => ({
|
||||
name: 'Example',
|
||||
|
||||
@ -1,45 +1,21 @@
|
||||
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 { UseRoyaltyRegistryContractProps } from 'contracts/royaltyRegistry'
|
||||
import { useRoyaltyRegistryContract } from 'contracts/royaltyRegistry'
|
||||
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 UseWhiteListMerkleTreeContractProps, useWhiteListMerkleTreeContract } from 'contracts/whitelistMerkleTree'
|
||||
import type { ReactNode, VFC } from 'react'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
import { create } from 'zustand'
|
||||
|
||||
import type { UseSplitsContractProps } from '../contracts/splits/useContract'
|
||||
import { useSplitsContract } from '../contracts/splits/useContract'
|
||||
import type { State } from 'zustand'
|
||||
import create from 'zustand'
|
||||
|
||||
/**
|
||||
* Contracts store type definitions
|
||||
*/
|
||||
export interface ContractsStore {
|
||||
export interface ContractsStore extends State {
|
||||
sg721: UseSG721ContractProps | null
|
||||
vendingMinter: UseVendingMinterContractProps | null
|
||||
baseMinter: UseBaseMinterContractProps | null
|
||||
openEditionMinter: UseOpenEditionMinterContractProps | null
|
||||
minter: UseMinterContractProps | null
|
||||
whitelist: UseWhiteListContractProps | null
|
||||
whitelistMerkleTree: UseWhiteListMerkleTreeContractProps | null
|
||||
vendingFactory: UseVendingFactoryContractProps | null
|
||||
baseFactory: UseBaseFactoryContractProps | null
|
||||
openEditionFactory: UseOpenEditionFactoryContractProps | null
|
||||
badgeHub: UseBadgeHubContractProps | null
|
||||
splits: UseSplitsContractProps | null
|
||||
royaltyRegistry: UseRoyaltyRegistryContractProps | null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,17 +23,8 @@ export interface ContractsStore {
|
||||
*/
|
||||
export const defaultValues: ContractsStore = {
|
||||
sg721: null,
|
||||
vendingMinter: null,
|
||||
baseMinter: null,
|
||||
openEditionMinter: null,
|
||||
minter: null,
|
||||
whitelist: null,
|
||||
whitelistMerkleTree: null,
|
||||
vendingFactory: null,
|
||||
baseFactory: null,
|
||||
openEditionFactory: null,
|
||||
badgeHub: null,
|
||||
splits: null,
|
||||
royaltyRegistry: null,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,47 +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 whitelistMerkleTree = useWhiteListMerkleTreeContract()
|
||||
const vendingFactory = useVendingFactoryContract()
|
||||
const baseFactory = useBaseFactoryContract()
|
||||
const openEditionFactory = useOpenEditionFactoryContract()
|
||||
const badgeHub = useBadgeHubContract()
|
||||
const splits = useSplitsContract()
|
||||
const royaltyRegistry = useRoyaltyRegistryContract()
|
||||
|
||||
useEffect(() => {
|
||||
useContracts.setState({
|
||||
sg721,
|
||||
vendingMinter,
|
||||
baseMinter,
|
||||
openEditionMinter,
|
||||
minter,
|
||||
whitelist,
|
||||
whitelistMerkleTree,
|
||||
vendingFactory,
|
||||
baseFactory,
|
||||
openEditionFactory,
|
||||
badgeHub,
|
||||
splits,
|
||||
royaltyRegistry,
|
||||
})
|
||||
}, [
|
||||
sg721,
|
||||
vendingMinter,
|
||||
baseMinter,
|
||||
whitelist,
|
||||
whitelistMerkleTree,
|
||||
vendingFactory,
|
||||
baseFactory,
|
||||
badgeHub,
|
||||
splits,
|
||||
royaltyRegistry,
|
||||
openEditionMinter,
|
||||
openEditionFactory,
|
||||
])
|
||||
}, [sg721, minter, whitelist])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
export type Timezone = 'UTC' | 'Local'
|
||||
|
||||
export const useGlobalSettings = create(() => ({
|
||||
timezone: 'UTC' as Timezone,
|
||||
}))
|
||||
|
||||
export const setTimezone = (timezone: Timezone) => {
|
||||
useGlobalSettings.setState({ timezone })
|
||||
}
|
||||
@ -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) }))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user