Compare commits

..

19 Commits

Author SHA1 Message Date
Serkan Reis
7ad9d755eb
Merge pull request #340 from public-awesome/update-royalty-address-checks
Update royalty address checks for v1 collections
2024-02-13 09:44:54 +02:00
Serkan Reis
7bdf81d95e Update royalty address checks for v1 collections 2024-02-13 10:39:13 +03:00
Serkan Reis
5e28d7dfb0
Merge pull request #154 from public-awesome/sg721-dashboard-migrate-update
Add default code id & info box to sg721 dashboard > migrate
2023-04-24 11:26:38 +03:00
Serkan Reis
71c273c4ef Display info box conditionally 2023-04-24 11:22:24 +03:00
Serkan Reis
2f681bbcba Fetch sg721-updatable code id from env variables 2023-04-24 11:09:55 +03:00
Serkan Reis
03ddc13911 Add default code id & info box to sg721 dashboard > migrate 2023-04-24 10:59:49 +03:00
Adnan Deniz corlu
32f4157c42
Merge pull request #149 from public-awesome/sg721-dashboard-migrate
Enable migration, royalty info & token metadata update for v1 collections
2023-04-11 18:12:23 +03:00
Serkan Reis
dc902313a6 Update codeowners 2023-04-11 18:09:20 +03:00
Serkan Reis
35e736062a Update bps calc 2023-04-11 18:06:39 +03:00
Serkan Reis
e64073499d Add update/batch_update/freeze_token_metadata() to Collection Actions 2023-04-11 17:29:23 +03:00
Serkan Reis
019a0c4d73 Add update/batch_update/freeze_token_metadata() among sg721 contract helpers 2023-04-11 16:52:30 +03:00
Serkan Reis
129953f7b3 Include Update Royalty Info on Collection Actions 2023-04-11 16:33:33 +03:00
Serkan Reis
6f36284fbd Add update_royalty_info() among sg721 contract helpers 2023-04-11 15:57:37 +03:00
Serkan Reis
f4f11dbe6a Implement SG721 dashboard > Migrate tab 2023-04-11 13:57:44 +03:00
Serkan Reis
07a08ca35a Update sg721 dashboard link tabs 2023-04-11 13:42:27 +03:00
Serkan Reis
eb82d10140 Update contract helpers for sg721 2023-04-11 13:40:29 +03:00
jhernandezb
fc65053978 bump version 2022-11-25 06:07:22 -06:00
jhernandezb
fae92e5483 add update and remove discount price 2022-11-24 23:59:17 -06:00
jhernandezb
2340657020 add migrate option 2022-11-24 23:32:44 -06:00
251 changed files with 2778 additions and 53075 deletions

1
.env
View File

@ -1 +0,0 @@
CERC_MAX_GENERATE_TIME=180

View File

@ -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_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_SG721_CODE_ID=2595 NEXT_PUBLIC_WHITELIST_CODE_ID=3
NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID=2596 NEXT_PUBLIC_MINTER_CODE_ID=2
NEXT_PUBLIC_STRDST_SG721_CODE_ID=2595 NEXT_PUBLIC_SG721_CODE_ID=1
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_VENDING_FACTORY_ADDRESS="stars18h7ugh8eaug7wr0w4yjw0ls5s937z35pnkg935ucsek2y9xl3gaqqk4jtx" NEXT_PUBLIC_API_URL=https://
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_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze
NEXT_PUBLIC_NETWORK=testnet NEXT_PUBLIC_NETWORK=testnet
NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev
NEXT_PUBLIC_BADGES_URL=https://badges.publicawesome.dev
NEXT_PUBLIC_WEBSITE_URL=https:// 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_S3_BUCKET= # TODO
NEXT_PUBLIC_MEILISEARCH_API_KEY= "..." NEXT_PUBLIC_S3_ENDPOINT= # TODO
NEXT_PUBLIC_S3_KEY= # TODO
NEXT_PUBLIC_S3_REGION= # TODO
NEXT_PUBLIC_S3_SECRET= # TODO

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @MightOfOaks @name-user1 @Ninjatosba * @MightOfOaks @Ninjatosba

View File

@ -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

View File

@ -1,62 +1,16 @@
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx' import clsx from 'clsx'
import React, { useState } from 'react' import React from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { SG721_NAME_ADDRESS } from 'utils/constants'
import { csvToArray } from 'utils/csvToArray' import { csvToArray } from 'utils/csvToArray'
import type { AirdropAllocation } from 'utils/isValidAccountsFile' import type { AirdropAllocation } from 'utils/isValidAccountsFile'
import { isValidAccountsFile } from 'utils/isValidAccountsFile' import { isValidAccountsFile } from 'utils/isValidAccountsFile'
import { isValidAddress } from 'utils/isValidAddress'
import { useWallet } from 'utils/wallet'
interface AirdropUploadProps { interface AirdropUploadProps {
onChange: (data: AirdropAllocation[]) => void onChange: (data: AirdropAllocation[]) => void
} }
export const AirdropUpload = ({ onChange }: AirdropUploadProps) => { 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>) => { const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setResolvedAllocationData([])
if (!event.target.files) return toast.error('Error opening file') if (!event.target.files) return toast.error('Error opening file')
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!event.target.files[0]?.name.endsWith('.csv')) { if (!event.target.files[0]?.name.endsWith('.csv')) {
@ -64,38 +18,18 @@ export const AirdropUpload = ({ onChange }: AirdropUploadProps) => {
return onChange([]) return onChange([])
} }
const reader = new FileReader() const reader = new FileReader()
reader.onload = async (e: ProgressEvent<FileReader>) => { reader.onload = (e: ProgressEvent<FileReader>) => {
try { try {
if (!e.target?.result) return toast.error('Error parsing file.') if (!e.target?.result) return toast.error('Error parsing file.')
// eslint-disable-next-line @typescript-eslint/no-base-to-string // eslint-disable-next-line @typescript-eslint/no-base-to-string
const accountsData = csvToArray(e.target.result.toString()) const accountsData = csvToArray(e.target.result.toString())
console.log(accountsData)
if (!isValidAccountsFile(accountsData)) { if (!isValidAccountsFile(accountsData)) {
event.target.value = '' event.target.value = ''
return onChange([]) return onChange([])
} }
await resolveAllocationData(accountsData.filter((data) => data.address.trim().endsWith('.stars'))).finally( return onChange(accountsData)
() => {
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,
})),
),
)
},
)
} catch (error: any) { } catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } }) toast.error(error.message)
} }
} }
reader.readAsText(event.target.files[0]) reader.readAsText(event.target.files[0])

View File

@ -21,7 +21,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
tempArray.push( tempArray.push(
<video <video
key={assetFile.name} 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" id="video"
muted muted
onMouseEnter={(e) => { 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) => ( return assetFilesArray.slice((page - 1) * ITEM_NUMBER, page * ITEM_NUMBER).map((assetSource, index) => (
<button <button
key={assetSource.name} key={assetSource.name}
className={clsx( className="relative p-0 w-[100px] h-[100px] bg-transparent hover:bg-transparent border-0 btn modal-button"
'relative p-0 w-[100px] h-[100px] bg-transparent hover:bg-transparent border-0 btn modal-button',
)}
onClick={() => { onClick={() => {
updateMetadataFileIndex((page - 1) * ITEM_NUMBER + index) updateMetadataFileIndex((page - 1) * ITEM_NUMBER + index)
}} }}
@ -71,26 +69,9 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
> >
{(page - 1) * 12 + (index + 1)} {(page - 1) * 12 + (index + 1)}
</div> </div>
{getAssetType(assetSource.name) === 'audio' && ( {getAssetType(assetSource.name) === 'audio' && (
<div className="flex absolute flex-col items-center mt-4 ml-2"> <div className="flex absolute flex-col items-center mt-4 ml-2">
<img <img key={`audio-${index}`} alt="audio_icon" className="mb-2 ml-1 w-6 h-6 thumbnail" src="/audio.png" />
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"
/>
<span className="flex self-center ">{assetSource.name}</span> <span className="flex self-center ">{assetSource.name}</span>
</div> </div>
)} )}
@ -102,23 +83,11 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
<img <img
key={`image-${index}`} key={`image-${index}`}
alt="asset" 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)} src={URL.createObjectURL(assetSource)}
/> />
</div> </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> </label>
</button> </button>
)) ))
@ -147,7 +116,6 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
return ( return (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="mt-2 w-[400px] h-[300px]">{renderImages()}</div> <div className="mt-2 w-[400px] h-[300px]">{renderImages()}</div>
<div className="mt-5 btn-group"> <div className="mt-5 btn-group">
<button className="text-white bg-plumbus-light btn" onClick={multiplePrevPage} type="button"> <button className="text-white bg-plumbus-light btn" onClick={multiplePrevPage} type="button">
«« ««

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -1,61 +1,33 @@
import { useState } from 'react'
import { Button } from './Button' import { Button } from './Button'
export interface ConfirmationModalProps { export interface ConfirmationModalProps {
confirm: () => void confirm: () => void
} }
export const ConfirmationModal = (props: ConfirmationModalProps) => { export const ConfirmationModal = (props: ConfirmationModalProps) => {
const [isChecked, setIsChecked] = useState(false)
return ( return (
<div> <div>
<input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" /> <input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-2"> <label className="cursor-pointer modal" htmlFor="my-modal-2">
<label <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" htmlFor="temp"
> >
{/* <Alert type="warning"></Alert> */} {/* <Alert type="warning"></Alert> */}
<div className="text-xl font-bold"> <div className="text-xl font-bold">
<div className="text-sm font-thin"> Are you sure to create a collection with the specified assets, metadata and parameters?
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?
</div> </div>
<div className="flex justify-end w-full"> <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"> <Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">
<label <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" htmlFor="my-modal-2"
> >
Go Back Go Back
</label> </label>
</Button> </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 <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" htmlFor="my-modal-2"
> >
Confirm Confirm

View File

@ -8,7 +8,7 @@ export function FaviconsMetaTags() {
<link href="/assets/manifest.webmanifest" rel="manifest" /> <link href="/assets/manifest.webmanifest" rel="manifest" />
<meta content="yes" name="mobile-web-app-capable" /> <meta content="yes" name="mobile-web-app-capable" />
<meta content="#F0827D" name="theme-color" /> <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-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-60x60.png" rel="apple-touch-icon" sizes="60x60" />
<link href="/assets/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72" /> <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" /> <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="yes" name="apple-mobile-web-app-capable" />
<meta content="black-translucent" name="apple-mobile-web-app-status-bar-style" /> <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 <link
href="/assets/apple-touch-startup-image-640x1136.png" 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)" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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

View File

@ -66,7 +66,7 @@ export const JsonPreview = ({
</div> </div>
{show && ( {show && (
<div className="overflow-auto p-2 font-mono text-sm"> <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>
)} )}
</div> </div>

View File

@ -47,9 +47,9 @@ export const Layout = ({ children, metadata = {} }: LayoutProps) => {
<FaDesktop size={48} /> <FaDesktop size={48} />
<h1 className="text-2xl font-bold">Unsupported Viewport</h1> <h1 className="text-2xl font-bold">Unsupported Viewport</h1>
<p> <p>
Stargaze Studio is best viewed on the big screen. StargazeStudio is best viewed on the big screen.
<br /> <br />
Please open Stargaze Studio on your tablet or desktop browser. Please open StargazeStudio on your tablet or desktop browser.
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,6 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import { Anchor } from 'components/Anchor' import { Anchor } from 'components/Anchor'
import { useRouter } from 'next/router'
export interface LinkTabProps { export interface LinkTabProps {
title: string title: string
@ -12,10 +11,6 @@ export interface LinkTabProps {
export const LinkTab = (props: LinkTabProps) => { export const LinkTab = (props: LinkTabProps) => {
const { title, description, href, isActive } = props const { title, description, href, isActive } = props
// get contract address from the router
const router = useRouter()
const { contractAddress } = router.query
return ( return (
<Anchor <Anchor
className={clsx( className={clsx(
@ -24,7 +19,7 @@ export const LinkTab = (props: LinkTabProps) => {
isActive ? 'border-plumbus' : 'border-transparent', isActive ? 'border-plumbus' : 'border-transparent',
isActive ? 'bg-plumbus/5 hover:bg-plumbus/10' : 'hover:bg-white/5', 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> <h4 className="font-bold">{title}</h4>
<span className="text-sm text-white/80 line-clamp-2">{description}</span> <span className="text-sm text-white/80 line-clamp-2">{description}</span>

View File

@ -1,6 +1,11 @@
import type { LinkTabProps } from './LinkTab' import type { LinkTabProps } from './LinkTab'
export const sg721LinkTabs: LinkTabProps[] = [ export const sg721LinkTabs: LinkTabProps[] = [
{
title: 'Instantiate',
description: `Create a new SG721 contract`,
href: '/contracts/sg721/instantiate',
},
{ {
title: 'Query', title: 'Query',
description: `Dispatch queries with your SG721 contract`, 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', title: 'Instantiate',
description: `Initialize a new Vending Minter contract`, description: `Initialize a new Minter contract`,
href: '/contracts/vendingMinter/instantiate', href: '/contracts/minter/instantiate',
}, },
{ {
title: 'Query', title: 'Query',
description: `Dispatch queries for your Vending Minter contract`, description: `Dispatch queries with your Minter contract`,
href: '/contracts/vendingMinter/query', href: '/contracts/minter/query',
}, },
{ {
title: 'Execute', title: 'Execute',
description: `Execute Vending Minter contract actions`, description: `Execute Minter contract actions`,
href: '/contracts/vendingMinter/execute', href: '/contracts/minter/execute',
}, },
{ {
title: 'Migrate', title: 'Migrate',
description: `Migrate Vending Minter contract`, description: `Migrate Minter contract`,
href: '/contracts/vendingMinter/migrate', href: '/contracts/minter/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',
}, },
] ]
@ -90,7 +54,7 @@ export const whitelistLinkTabs: LinkTabProps[] = [
}, },
{ {
title: 'Query', title: 'Query',
description: `Dispatch queries for your Whitelist contract`, description: `Dispatch queries with your Whitelist contract`,
href: '/contracts/whitelist/query', href: '/contracts/whitelist/query',
}, },
{ {
@ -99,88 +63,3 @@ export const whitelistLinkTabs: LinkTabProps[] = [
href: '/contracts/whitelist/execute', 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',
},
]

View File

@ -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>
)
}

View File

@ -1,8 +1,7 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/media-has-caption */ /* eslint-disable jsx-a11y/media-has-caption */
import clsx from 'clsx'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { useEffect, useMemo, useState } from 'react' import { useMemo } from 'react'
import { getAssetType } from 'utils/getAssetType' import { getAssetType } from 'utils/getAssetType'
export interface MetadataFormGroupProps { export interface MetadataFormGroupProps {
@ -14,7 +13,6 @@ export interface MetadataFormGroupProps {
export const MetadataFormGroup = (props: MetadataFormGroupProps) => { export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
const { title, subtitle, relatedAsset, children } = props const { title, subtitle, relatedAsset, children } = props
const [htmlContents, setHtmlContents] = useState<string>('')
const videoPreview = useMemo( const videoPreview = useMemo(
() => ( () => (
@ -42,27 +40,6 @@ export const MetadataFormGroup = (props: MetadataFormGroupProps) => {
[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-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 ( return (
<div className="flex p-4 pt-0 space-x-4 w-full"> <div className="flex p-4 pt-0 space-x-4 w-full">
<div className="flex flex-col w-1/3"> <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>} {subtitle && <span className="text-sm text-white/50">{subtitle}</span>}
<div> <div>
{relatedAsset && ( {relatedAsset && (
<div <div className="flex flex-row items-center mt-2 mr-4 border-2 border-dashed">
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) === 'audio' && audioPreview}
{getAssetType(relatedAsset.name) === 'video' && videoPreview} {getAssetType(relatedAsset.name) === 'video' && videoPreview}
{getAssetType(relatedAsset.name) === 'document' && documentPreview}
{getAssetType(relatedAsset.name) === 'image' && ( {getAssetType(relatedAsset.name) === 'image' && (
<img alt="preview" src={URL.createObjectURL(relatedAsset)} /> <img alt="preview" src={URL.createObjectURL(relatedAsset)} />
)} )}
{getAssetType(relatedAsset.name) === 'html' && (
<iframe allowFullScreen height="420px" srcDoc={htmlContents} title="Preview" width="100%" />
)}
</div> </div>
)} )}
</div> </div>

View File

@ -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>
)
}

View File

@ -1,14 +1,12 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { Alert } from './Alert'
import { Button } from './Button' import { Button } from './Button'
import { Conditional } from './Conditional'
import { TextInput } from './forms/FormInput' import { TextInput } from './forms/FormInput'
import { useInputState } from './forms/FormInput.hooks' import { useInputState } from './forms/FormInput.hooks'
import { MetadataAttributes } from './forms/MetadataAttributes' import { MetadataAttributes } from './forms/MetadataAttributes'
@ -66,13 +64,6 @@ export const MetadataModal = (props: MetadataModalProps) => {
} }
setMetadata(parsedMetadata) 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 attributesState = useMetadataAttributesState()
const generateUpdatedMetadata = () => { const generateUpdatedMetadata = () => {
console.log(`Current parsed data: ${parsedMetadata}`)
console.log('Updating...')
metadata.attributes = Object.values(attributesState)[1] metadata.attributes = Object.values(attributesState)[1]
metadata.attributes = metadata.attributes.filter((attribute: { trait_type: string }) => attribute.trait_type !== '') 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 if (descriptionState.value === '') delete metadata.description
else metadata.description = descriptionState.value else metadata.description = descriptionState.value
if (externalUrlState.value === '') delete metadata.external_url 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 if (youtubeUrlState.value === '') delete metadata.youtube_url
else metadata.youtube_url = youtubeUrlState.value else metadata.youtube_url = youtubeUrlState.value
@ -127,11 +121,10 @@ export const MetadataModal = (props: MetadataModalProps) => {
type: 'application/json', type: 'application/json',
}) })
const editedMetadataFile = new File([metadataFileBlob], metadataFile.name.replaceAll('#', ''), { const editedMetadataFile = new File([metadataFileBlob], metadataFile.name, { type: 'application/json' })
type: 'application/json',
})
props.updateMetadata(editedMetadataFile) props.updateMetadata(editedMetadataFile)
toast.success('Metadata updated successfully.') toast.success('Metadata updated successfully.')
console.log(editedMetadataFile)
} }
useEffect(() => { useEffect(() => {
@ -151,42 +144,21 @@ export const MetadataModal = (props: MetadataModalProps) => {
subtitle={`Asset filename: ${props.assetFile?.name}`} subtitle={`Asset filename: ${props.assetFile?.name}`}
title="Update Metadata" title="Update Metadata"
> >
<TextInput <TextInput {...nameState} onChange={(e) => nameState.onChange(e.target.value)} />
{...nameState} <TextInput {...descriptionState} onChange={(e) => descriptionState.onChange(e.target.value)} />
disabled={!props.metadataFile} <TextInput {...externalUrlState} onChange={(e) => externalUrlState.onChange(e.target.value)} />
onChange={(e) => nameState.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}> <Button isDisabled={!props.metadataFile} onClick={generateUpdatedMetadata}>
Update Metadata Update Metadata
</Button> </Button>
<Conditional test={Boolean(!props.metadataFile)}>
<Alert type="info">No metadata file to preview. Please select metadata files.</Alert>
</Conditional>
</MetadataFormGroup> </MetadataFormGroup>
</label> </label>
</label> </label>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -1,379 +1,84 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import clsx from 'clsx' import clsx from 'clsx'
import { Anchor } from 'components/Anchor' import { Anchor } from 'components/Anchor'
import type { Timezone } from 'contexts/globalSettings' import { useWallet } from 'contexts/wallet'
import { setTimezone } from 'contexts/globalSettings'
import { setLogItemList, useLogStore } from 'contexts/log'
import Link from 'next/link'
import { useRouter } from 'next/router' 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 BrandText from 'public/brand/brand-text.svg'
import { footerLinks, socialsLinks } from 'utils/links' 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 { SidebarLayout } from './SidebarLayout'
import { WalletLoader } from './WalletLoader' 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 = () => { export const Sidebar = () => {
const router = useRouter() const router = useRouter()
const wallet = useWallet() 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 ( return (
<SidebarLayout> <SidebarLayout>
{/* Stargaze brand as home button */} {/* Stargaze brand as home button */}
<Anchor href="/" onContextMenu={(e) => [e.preventDefault(), router.push('/brand')]}> <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> </Anchor>
{/* wallet button */} {/* wallet button */}
<WalletLoader /> <WalletLoader />
{/* main navigation routes */} {/* main navigation routes */}
{routes.map(({ text, href, isChild }) => (
<div className={clsx('absolute left-[5%] mt-2', isTallWindow ? 'top-[20%]' : 'top-[30%]')}> <Anchor
<ul className="group py-1 px-2 w-full bg-transparent menu rounded-box"> key={href}
<li tabIndex={0}> className={clsx(
<div 'px-4 -mx-5 font-extrabold uppercase rounded-lg', // styling
className={clsx( 'hover:bg-white/5 transition-colors', // hover styling
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps', { 'py-0 ml-2 text-sm font-bold': isChild },
'hover:bg-white/5 transition-colors', {
router.asPath.includes('/collections/') ? 'text-white' : 'text-gray', 'text-gray hover:text-white':
)} router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1) !== href && isChild,
> },
<Link href="/collections/" passHref> { 'text-plumbus': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1) === href && isChild }, // active route styling
Collections // { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling
</Link> )}
</div> href={href}
<ul className="z-50 p-2 bg-base-200"> >
<li {text}
className={clsx( </Anchor>
'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 />
<div className="flex-grow" /> <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 */} {/* 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 */} {/* footer reference links */}
<ul className="text-sm list-disc list-inside"> <ul className="text-sm list-disc list-inside">
{isTallWindow && {footerLinks.map(({ href, text }) => (
footerLinks.map(({ href, text }) => ( <li key={href}>
<li key={href}> <Anchor className="hover:text-plumbus hover:underline" href={href}>
<Anchor className="hover:text-plumbus hover:underline" href={href}> {text}
{text} </Anchor>
</Anchor> </li>
</li> ))}
))}
</ul> </ul>
{/* footer attribution */} {/* footer attribution */}
<div className="text-xs text-white/50"> <div className="text-xs text-white/50">
Stargaze Studio {process.env.APP_VERSION} <br /> Stargaze Studio {process.env.APP_VERSION} <br />
Powered by{' '} Made by{' '}
<Anchor className="text-plumbus hover:underline" href="https://stargaze.zone"> <Anchor className="text-plumbus hover:underline" href="https://deuslabs.fi">
Stargaze deus labs
</Anchor> </Anchor>
</div> </div>
{/* footer social links */} {/* footer social links */}
<div className="flex gap-x-6 items-center text-white/75"> <div className="flex gap-x-6 items-center text-white/75">
{socialsLinks.map(({ Icon, href, text }) => ( {socialsLinks.map(({ Icon, href, text }) => (
<Anchor key={href} className="hover:text-plumbus" href={href}> <Anchor key={href} className="hover:text-plumbus" href={href}>

View File

@ -15,7 +15,7 @@ export const SidebarLayout = ({ children }: SidebarLayoutProps) => {
{/* fixed component */} {/* fixed component */}
<div <div
className={clsx( 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', 'border-r-[1px] border-r-plumbus-light',
{ 'translate-x-[-230px]': !isOpen }, { 'translate-x-[-230px]': !isOpen },
)} )}

View File

@ -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>
)
}

View File

@ -6,8 +6,6 @@ import { usePopper } from 'react-popper'
export interface TooltipProps extends ComponentProps<'div'> { export interface TooltipProps extends ComponentProps<'div'> {
label: ReactNode label: ReactNode
children: ReactElement children: ReactElement
placement?: 'top' | 'bottom' | 'left' | 'right'
backgroundColor?: string
} }
export const Tooltip = ({ label, children, ...props }: TooltipProps) => { export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
@ -16,7 +14,7 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
const { styles, attributes } = usePopper(referenceElement, popperElement, { const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: props.placement ? props.placement : 'top', placement: 'top',
}) })
return ( return (
@ -34,11 +32,7 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
<div <div
{...props} {...props}
{...attributes.popper} {...attributes.popper}
className={clsx( className={clsx('py-1 px-2 m-1 text-sm bg-black/80 rounded shadow-md', props.className)}
'py-1 px-2 m-1 text-sm rounded shadow-md',
props.backgroundColor ? props.backgroundColor : 'bg-slate-900',
props.className,
)}
ref={setPopperElement} ref={setPopperElement}
style={{ ...styles.popper, ...props.style }} style={{ ...styles.popper, ...props.style }}
> >

View File

@ -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>
)
}

View File

@ -1,63 +1,36 @@
import type { Coin } from '@cosmjs/proto-signing'
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { tokensList } from 'config/token' import { useWallet, useWalletStore } from 'contexts/wallet'
import { Fragment, useEffect, useState } from 'react' import { Fragment } from 'react'
import { FaCopy, FaPowerOff, FaRedo } from 'react-icons/fa' import { FaCopy, FaPowerOff, FaRedo } from 'react-icons/fa'
import { copy } from 'utils/clipboard' import { copy } from 'utils/clipboard'
import { convertDenomToReadable } from 'utils/convertDenomToReadable' import { convertDenomToReadable } from 'utils/convertDenomToReadable'
import { getShortAddress } from 'utils/getShortAddress' import { getShortAddress } from 'utils/getShortAddress'
import { truncateMiddle } from 'utils/text'
import { useWallet } from 'utils/wallet'
import { WalletButton } from './WalletButton' import { WalletButton } from './WalletButton'
import { WalletPanelButton } from './WalletPanelButton' import { WalletPanelButton } from './WalletPanelButton'
export const WalletLoader = () => { export const WalletLoader = () => {
const { const { address, balance, connect, disconnect, initializing: isLoading, initialized: isReady } = useWallet()
address = '',
username,
connect,
disconnect,
isWalletConnecting,
isWalletConnected,
getStargateClient,
} = useWallet()
// Once wallet connects, load balances. const displayName = useWalletStore((store) => store.name || getShortAddress(store.address))
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])
return ( return (
<Popover className="mt-4 mb-2"> <Popover className="my-8">
{({ close }) => ( {({ close }) => (
<> <>
<div className="grid -mx-4"> <div className="grid -mx-4">
{isWalletConnected ? ( {!isReady && (
<Popover.Button as={WalletButton} className="w-full"> <WalletButton className="w-full" isLoading={isLoading} onClick={() => void connect()}>
{username || address}
</Popover.Button>
) : (
<WalletButton
className="w-full"
isLoading={isWalletConnecting}
onClick={() => void connect().catch(console.error)}
>
Connect Wallet Connect Wallet
</WalletButton> </WalletButton>
)} )}
{isReady && (
<Popover.Button as={WalletButton} className="w-full" isLoading={isLoading}>
{displayName}
</Popover.Button>
)}
</div> </div>
<Transition <Transition
@ -71,7 +44,7 @@ export const WalletLoader = () => {
> >
<Popover.Panel <Popover.Panel
className={clsx( 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', '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', 'flex flex-col items-stretch text-sm divide-y divide-white/10',
)} )}
@ -81,12 +54,9 @@ export const WalletLoader = () => {
{getShortAddress(address)} {getShortAddress(address)}
</span> </span>
<div className="font-bold">Your Balances</div> <div className="font-bold">Your Balances</div>
{balances?.map((val) => ( {balance.map((val) => (
<span key={`balance-${val.denom}`}> <span key={`balance-${val.denom}`}>
{convertDenomToReadable(val.amount)}{' '} {convertDenomToReadable(val.amount)} {val.denom.slice(1, val.denom.length)}
{tokensList.find((t) => t.denom === val.denom)?.displayName
? tokensList.find((t) => t.denom === val.denom)?.displayName
: truncateMiddle(val.denom ? val.denom : '', 28)}
</span> </span>
))} ))}
</div> </div>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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 clsx from 'clsx'
import React, { useState } from 'react' import React from 'react'
import { toast } from 'react-hot-toast' 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 { interface WhitelistUploadProps {
onChange: (data: string[]) => void onChange: (data: string[]) => void
} }
export const WhitelistUpload = ({ onChange }: WhitelistUploadProps) => { 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>) => { const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setResolvedAddresses([])
if (!event.target.files) return toast.error('Error opening file') if (!event.target.files) return toast.error('Error opening file')
if (event.target.files.length !== 1) { if (event.target.files[0].type !== 'text/plain') return toast.error('Invalid file type')
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() const reader = new FileReader()
reader.onload = async (e: ProgressEvent<FileReader>) => { reader.onload = (e: ProgressEvent<FileReader>) => {
const text = e.target?.result?.toString() const text = e.target?.result?.toString()
let newline = '\n' let newline = '\n'
if (text?.includes('\r')) newline = '\r' if (text?.includes('\r')) newline = '\r'
if (text?.includes('\r\n')) newline = '\r\n' 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, '') return onChange([...new Set(data?.filter((address) => address !== '') || [])])
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) || [],
),
])
} }
reader.readAsText(event.target.files[0]) 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', 'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)} )}
id="whitelist-file" id="whitelist-file"
multiple={false} multiple
onChange={onFileChange} onChange={onFileChange}
type="file" type="file"
/> />

View 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>
)
}

View File

@ -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) }
}

View File

@ -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>
)
}

View File

@ -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)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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) }
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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')
}
}
}

View File

@ -1,9 +1,5 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
import { toUtf8 } from '@cosmjs/encoding' import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import { AirdropUpload } from 'components/AirdropUpload' import { AirdropUpload } from 'components/AirdropUpload'
import { Alert } from 'components/Alert'
import { Button } from 'components/Button' import { Button } from 'components/Button'
import type { DispatchExecuteArgs } from 'components/collections/actions/actions' import type { DispatchExecuteArgs } from 'components/collections/actions/actions'
import { dispatchExecute, isEitherType, previewExecutePayload } 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 { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview' import { JsonPreview } from 'components/JsonPreview'
import { Tooltip } from 'components/Tooltip'
import { TransactionHash } from 'components/TransactionHash' import { TransactionHash } from 'components/TransactionHash'
import { useGlobalSettings } from 'contexts/globalSettings' import { useWallet } from 'contexts/wallet'
import type { BaseMinterInstance } from 'contracts/baseMinter' import type { MinterInstance } from 'contracts/minter'
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
import type { SG721Instance } from 'contracts/sg721' import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import type { FormEvent } from 'react' import type { FormEvent } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa' import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query' import { useMutation } from 'react-query'
import { ROYALTY_REGISTRY_ADDRESS } from 'utils/constants'
import type { AirdropAllocation } from 'utils/isValidAccountsFile' 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 { TextInput } from '../../forms/FormInput'
import type { MinterType, Sg721Type } from './Combobox'
interface CollectionActionsProps { interface CollectionActionsProps {
minterContractAddress: string minterContractAddress: string
sg721ContractAddress: string sg721ContractAddress: string
sg721Messages: SG721Instance | undefined sg721Messages: SG721Instance | undefined
vendingMinterMessages: VendingMinterInstance | undefined minterMessages: MinterInstance | undefined
baseMinterMessages: BaseMinterInstance | undefined
openEditionMinterMessages: OpenEditionMinterInstance | undefined
royaltyRegistryMessages: RoyaltyRegistryInstance | undefined
minterType: MinterType
sg721Type: Sg721Type
} }
type ExplicitContentType = true | false | undefined
export const CollectionActions = ({ export const CollectionActions = ({
sg721ContractAddress, sg721ContractAddress,
sg721Messages, sg721Messages,
minterContractAddress, minterContractAddress,
vendingMinterMessages, minterMessages,
baseMinterMessages,
openEditionMinterMessages,
royaltyRegistryMessages,
minterType,
sg721Type,
}: CollectionActionsProps) => { }: CollectionActionsProps) => {
const wallet = useWallet() const wallet = useWallet()
const [lastTx, setLastTx] = useState('') const [lastTx, setLastTx] = useState('')
const { timezone } = useGlobalSettings()
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined) const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>(undefined)
const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([]) const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([])
const [airdropArray, setAirdropArray] = useState<string[]>([]) 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 actionComboboxState = useActionsComboboxState()
const type = actionComboboxState.value?.id const type = actionComboboxState.value?.id
const limitState = useNumberInputState({ const limitState = useNumberInputState({
id: 'per-address-limit', id: 'per-address-limi',
name: 'perAddressLimit', name: 'perAddressLimit',
title: 'Per Address Limit', title: 'Per Address Limit',
subtitle: 'Enter the per address limit', subtitle: 'Enter the per address limit',
@ -116,13 +84,6 @@ export const CollectionActions = ({
subtitle: 'Address of the recipient', subtitle: 'Address of the recipient',
}) })
const creatorState = useInputState({
id: 'creator-address',
name: 'creator',
title: 'Creator Address',
subtitle: 'Address of the creator',
})
const tokenURIState = useInputState({ const tokenURIState = useInputState({
id: 'token-uri', id: 'token-uri',
name: 'tokenURI', name: 'tokenURI',
@ -146,33 +107,6 @@ export const CollectionActions = ({
subtitle: 'Address of the whitelist contract', 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({ const royaltyPaymentAddressState = useInputState({
id: 'royalty-payment-address', id: 'royalty-payment-address',
name: 'royaltyPaymentAddress', name: 'royaltyPaymentAddress',
@ -184,161 +118,49 @@ export const CollectionActions = ({
const royaltyShareState = useInputState({ const royaltyShareState = useInputState({
id: 'royalty-share', id: 'royalty-share',
name: 'royaltyShare', name: 'royaltyShare',
title: type !== 'update_royalties_for_infinity_swap' ? 'Share Percentage' : 'Share Delta', title: 'Share Percentage',
subtitle: subtitle: 'Percentage of royalties to be paid',
type !== 'update_royalties_for_infinity_swap' placeholder: '8%',
? '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%',
}) })
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
const showWhitelistField = type === 'set_whitelist' const showWhitelistField = type === 'set_whitelist'
const showDateField = isEitherType(type, ['update_start_time', 'update_start_trading_time']) const showDateField = type === 'update_start_time'
const showEndDateField = type === 'update_end_time'
const showLimitField = type === 'update_per_address_limit' const showLimitField = type === 'update_per_address_limit'
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata']) const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata'])
const showNumberOfTokensField = isEitherType(type, ['batch_mint', 'batch_mint_open_edition']) const showNumberOfTokensField = type === 'batch_mint'
const showTokenIdListField = isEitherType(type, [ const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer', 'batch_update_token_metadata'])
'batch_burn', const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer'])
'batch_transfer', const showAirdropFileField = type === 'airdrop'
'batch_mint_for', const showRoyaltyInfoFields = type === 'update_royalty_info'
'batch_update_token_metadata', const showTokenUriField = type === '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 showBaseUriField = type === 'batch_update_token_metadata' const showBaseUriField = type === 'batch_update_token_metadata'
const payload: DispatchExecuteArgs = { const payload: DispatchExecuteArgs = {
whitelist: whitelistState.value, whitelist: whitelistState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
limit: limitState.value, limit: limitState.value,
minterContract: minterContractAddress, minterContract: minterContractAddress,
sg721Contract: sg721ContractAddress, sg721Contract: sg721ContractAddress,
royaltyRegistryContract: ROYALTY_REGISTRY_ADDRESS,
tokenId: tokenIdState.value, tokenId: tokenIdState.value,
tokenIds: tokenIdListState.value, tokenIds: tokenIdListState.value,
tokenUri: tokenURIState.value.trim().endsWith('/')
? tokenURIState.value.trim().slice(0, -1)
: tokenURIState.value.trim(),
batchNumber: batchNumberState.value, batchNumber: batchNumberState.value,
vendingMinterMessages, minterMessages,
baseMinterMessages,
openEditionMinterMessages,
sg721Messages, sg721Messages,
royaltyRegistryMessages, recipient: recipientState.value,
recipient: resolvedRecipientAddress,
recipients: airdropArray, recipients: airdropArray,
tokenRecipients: airdropAllocationArray, txSigner: wallet.address,
txSigner: wallet.address || '', royaltyInfo: {
type, payment_address: royaltyPaymentAddressState.value,
price: priceState.value.toString(), share_bps: Number(royaltyShareState.value),
},
baseUri: baseURIState.value.trim().endsWith('/') baseUri: baseURIState.value.trim().endsWith('/')
? baseURIState.value.trim().slice(0, -1) ? baseURIState.value.trim().slice(0, -1)
: baseURIState.value.trim(), : baseURIState.value.trim(),
collectionInfo, tokenUri: tokenURIState.value.trim().endsWith('/')
jsonExtensions, ? tokenURIState.value.trim().slice(0, -1)
decrement, : 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(() => { useEffect(() => {
const addresses: string[] = [] const addresses: string[] = []
@ -358,90 +180,27 @@ export const CollectionActions = ({
const { isLoading, mutate } = useMutation( const { isLoading, mutate } = useMutation(
async (event: FormEvent) => { async (event: FormEvent) => {
event.preventDefault() event.preventDefault()
if (!wallet.isWalletConnected) {
throw new Error('Please connect your wallet first.')
}
if (!type) { if (!type) {
throw new Error('Please select an action.') throw new Error('Please select an action!')
} }
if (minterContractAddress === '' && sg721ContractAddress === '') { if (minterContractAddress === '' && sg721ContractAddress === '') {
throw new Error('Please enter minter and sg721 contract addresses!') 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 ( if (
type === 'update_collection_info' && type === 'update_royalty_info' &&
(royaltyShareState.value ? !royaltyPaymentAddressState.value : royaltyPaymentAddressState.value) (royaltyShareState.value ? !royaltyPaymentAddressState.value : royaltyPaymentAddressState.value)
) { ) {
throw new Error('Royalty payment address and share percentage are both required') throw new Error('Royalty payment address and share percentage are both required')
} }
if ( if (
type === 'update_collection_info' && type === 'update_royalty_info' &&
royaltyPaymentAddressState.value && royaltyPaymentAddressState.value &&
!royaltyPaymentAddressState.value.trim().endsWith('.stars') !royaltyPaymentAddressState.value.trim().endsWith('.stars')
) { ) {
const contractInfoResponse = await (await wallet.getCosmWasmClient()) const contractInfoResponse = await wallet.client
.queryContractRaw( ?.queryContractRaw(
royaltyPaymentAddressState.value.trim(), royaltyPaymentAddressState.value.trim(),
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()), 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), { const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...', loading: 'Executing message...',
success: (tx) => `Transaction ${tx} success!`, success: (tx) => `Transaction ${tx} success!`,
}) })
if (txHash) { if (txHash) {
@ -498,226 +237,37 @@ export const CollectionActions = ({
const airdropFileOnChange = (data: AirdropAllocation[]) => { const airdropFileOnChange = (data: AirdropAllocation[]) => {
setAirdropAllocationArray(data) setAirdropAllocationArray(data)
} console.log(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()
} }
return ( return (
<form> <form>
<div className="grid grid-cols-2 mt-4"> <div className="grid grid-cols-2 mt-4">
<div className="mr-2"> <div className="mr-2">
<ActionsCombobox minterType={minterType} sg721Type={sg721Type} {...actionComboboxState} /> <ActionsCombobox {...actionComboboxState} />
{showRecipientField && <AddressInput {...recipientState} />} {showRecipientField && <AddressInput {...recipientState} />}
{showTokenUriField && <TextInput className="mt-2" {...tokenURIState} />}
{showWhitelistField && <AddressInput {...whitelistState} />} {showWhitelistField && <AddressInput {...whitelistState} />}
{showLimitField && <NumberInput {...limitState} />} {showLimitField && <NumberInput {...limitState} />}
{showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />} {showTokenIdField && <NumberInput {...tokenIdState} />}
{showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />} {showTokenUriField && <TextInput className="mt-2" {...tokenURIState} />}
{showTokenIdListField && <TextInput {...tokenIdListState} />}
{showBaseUriField && <TextInput className="mt-2" {...baseURIState} />} {showBaseUriField && <TextInput className="mt-2" {...baseURIState} />}
{showNumberOfTokensField && <NumberInput className="mt-2" {...batchNumberState} />} {showNumberOfTokensField && <NumberInput {...batchNumberState} />}
{showPriceField && <NumberInput className="mt-2" {...priceState} />} {showRoyaltyInfoFields && <TextInput className="mt-2" {...royaltyPaymentAddressState} />}
{showCreatorField && <AddressInput className="mt-2" {...creatorState} />} {showRoyaltyInfoFields && <NumberInput className="mt-2" {...royaltyShareState} />}
{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>
)}
{showAirdropFileField && ( {showAirdropFileField && (
<div> <FormGroup
<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"
subtitle={`CSV file that contains the ${ title="Airdrop File"
type === 'batch_transfer_multi_address' ? '' : 'airdrop' >
} addresses and the ${ <AirdropUpload onChange={airdropFileOnChange} />
type === 'airdrop' || type === 'airdrop_open_edition' ? 'amount of tokens' : 'token ID' </FormGroup>
} 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>
)} )}
<Conditional test={showDateField}> <Conditional test={showDateField}>
<FormControl <FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
className="mt-2" <InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
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> </FormControl>
</Conditional> </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>
<div className="-mt-6"> <div className="-mt-6">
<div className="relative mb-2"> <div className="relative mb-2">

View File

@ -2,38 +2,19 @@ import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter' import { matchSorter } from 'match-sorter'
import { Fragment, useEffect, useState } from 'react' import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { ActionListItem } from './actions' import type { ActionListItem } from './actions'
import { BASE_ACTION_LIST, OPEN_EDITION_ACTION_LIST, SG721_UPDATABLE_ACTION_LIST, VENDING_ACTION_LIST } from './actions' import { ACTION_LIST } from './actions'
export type MinterType = 'base' | 'vending' | 'openEdition'
export type Sg721Type = 'updatable' | 'base'
export interface ActionsComboboxProps { export interface ActionsComboboxProps {
value: ActionListItem | null value: ActionListItem | null
onChange: (item: ActionListItem) => void 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 [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 = const filtered =
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] }) 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 <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => 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} value={entry}
> >

View File

@ -1,54 +1,28 @@
/* eslint-disable eslint-comments/disable-enable-pair */ import type { MinterInstance } from 'contracts/minter'
import { useBaseMinterContract } from 'contracts/baseMinter' import { useMinterContract } from 'contracts/minter'
import { useOpenEditionMinterContract } from 'contracts/openEditionMinter' import type { RoyaltyInfo, SG721Instance } from 'contracts/sg721'
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
import { useRoyaltyRegistryContract } from 'contracts/royaltyRegistry'
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
import { useSG721Contract } 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 type ActionType = typeof ACTION_TYPES[number]
export const ACTION_TYPES = [ export const ACTION_TYPES = [
'mint_token_uri',
'update_mint_price',
'update_discount_price',
'remove_discount_price',
'mint_to', 'mint_to',
'mint_to_open_edition',
'mint_for', 'mint_for',
'batch_mint', 'batch_mint',
'batch_mint_open_edition',
'set_whitelist', 'set_whitelist',
'update_start_time', 'update_start_time',
'update_end_time', 'update_royalty_info',
'update_start_trading_time',
'update_per_address_limit', 'update_per_address_limit',
'update_collection_info', 'withdraw',
'freeze_collection_info',
'set_royalties_for_infinity_swap',
'update_royalties_for_infinity_swap',
'transfer', 'transfer',
'batch_transfer', 'batch_transfer',
'batch_transfer_multi_address',
'burn', 'burn',
'batch_burn', 'batch_burn',
'batch_mint_for',
'shuffle', 'shuffle',
'airdrop', 'airdrop',
'airdrop_open_edition',
'airdrop_specific',
'burn_remaining',
'update_token_metadata', 'update_token_metadata',
'batch_update_token_metadata', 'batch_update_token_metadata',
'freeze_token_metadata', 'freeze_token_metadata',
'enable_updatable',
] as const ] as const
export interface ActionListItem { export interface ActionListItem {
@ -57,99 +31,21 @@ export interface ActionListItem {
description?: string description?: string
} }
export const BASE_ACTION_LIST: ActionListItem[] = [ export const 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`,
},
{ {
id: 'mint_to', id: 'mint_to',
name: 'Mint To', name: 'Mint To',
description: `Mint a token to a user`, description: `Mint a token to a user`,
}, },
{
id: 'batch_mint',
name: 'Batch Mint To',
description: `Mint multiple tokens to a user`,
},
{ {
id: 'mint_for', id: 'mint_for',
name: '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', id: 'batch_mint',
name: 'Batch Mint For', name: 'Batch Mint',
description: `Mint a specific range of tokens from the collection to a specific address`, description: `Mint multiple tokens to a user with given token amount`,
}, },
{ {
id: 'set_whitelist', id: 'set_whitelist',
@ -157,14 +53,14 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
description: `Set whitelist contract address`, description: `Set whitelist contract address`,
}, },
{ {
id: 'update_start_time', id: 'update_royalty_info',
name: 'Update Minting Start Time', name: 'Update Royalty Info',
description: `Update start time for minting`, description: `Update royalty payment details`,
}, },
{ {
id: 'update_start_trading_time', id: 'update_start_time',
name: 'Update Trading Start Time', name: 'Update Start Time',
description: `Update start time for trading`, description: `Update start time for minting`,
}, },
{ {
id: 'update_per_address_limit', id: 'update_per_address_limit',
@ -172,24 +68,24 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
description: `Update token per address limit`, description: `Update token per address limit`,
}, },
{ {
id: 'update_collection_info', id: 'update_token_metadata',
name: 'Update Collection Info', name: 'Update Token Metadata',
description: `Update Collection Info`, description: `Update the metadata URI for a token`,
}, },
{ {
id: 'freeze_collection_info', id: 'batch_update_token_metadata',
name: 'Freeze Collection Info', name: 'Batch Update Token Metadata',
description: `Freeze collection info to prevent further updates`, description: `Update the metadata URI for a range of tokens`,
}, },
{ {
id: 'set_royalties_for_infinity_swap', id: 'freeze_token_metadata',
name: 'Set Royalty Details for Infinity Swap', name: 'Freeze Token Metadata',
description: `Set royalty details for Infinity Swap`, description: `Render the metadata for tokens no longer updatable`,
}, },
{ {
id: 'update_royalties_for_infinity_swap', id: 'withdraw',
name: 'Update Royalty Details for Infinity Swap', name: 'Withdraw Tokens',
description: `Update royalty details for Infinity Swap`, description: `Withdraw tokens from the contract`,
}, },
{ {
id: 'transfer', id: 'transfer',
@ -201,11 +97,6 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
name: 'Batch Transfer Tokens', name: 'Batch Transfer Tokens',
description: `Transfer a list of tokens to a recipient`, description: `Transfer a list of tokens to a recipient`,
}, },
{
id: 'batch_transfer_multi_address',
name: 'Transfer Tokens to Multiple Recipients',
description: `Transfer a list of tokens to multiple addresses`,
},
{ {
id: 'burn', id: 'burn',
name: 'Burn Token', name: 'Burn Token',
@ -226,127 +117,6 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
name: 'Airdrop Tokens', name: 'Airdrop Tokens',
description: 'Airdrop tokens to given addresses', 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 { export interface DispatchExecuteProps {
@ -354,134 +124,65 @@ export interface DispatchExecuteProps {
[k: string]: unknown [k: string]: unknown
} }
/** @see {@link VendingMinterInstance}{@link BaseMinterInstance} */ type Select<T extends ActionType> = T
export interface DispatchExecuteArgs {
/** @see {@link MinterInstance} */
export type DispatchExecuteArgs = {
minterContract: string minterContract: string
sg721Contract: string sg721Contract: string
royaltyRegistryContract: string minterMessages?: MinterInstance
vendingMinterMessages?: VendingMinterInstance
baseMinterMessages?: BaseMinterInstance
openEditionMinterMessages?: OpenEditionMinterInstance
sg721Messages?: SG721Instance sg721Messages?: SG721Instance
royaltyRegistryMessages?: RoyaltyRegistryInstance
txSigner: string txSigner: string
type: string | undefined } & (
tokenUri: string | { type: undefined }
price: string | { type: Select<'mint_to'>; recipient: string }
recipient: string | { type: Select<'mint_for'>; recipient: string; tokenId: number }
tokenId: number | { type: Select<'batch_mint'>; recipient: string; batchNumber: number }
batchNumber: number | { type: Select<'set_whitelist'>; whitelist: string }
whitelist: string | { type: Select<'update_start_time'>; startTime: string }
startTime: string | undefined | { type: Select<'update_per_address_limit'>; limit: number }
endTime: string | undefined | { type: Select<'shuffle'> }
limit: number | { type: Select<'withdraw'> }
tokenIds: string | { type: Select<'transfer'>; recipient: string; tokenId: number }
recipients: string[] | { type: Select<'batch_transfer'>; recipient: string; tokenIds: string }
tokenRecipients: AirdropAllocation[] | { type: Select<'burn'>; tokenId: number }
collectionInfo: CollectionInfo | undefined | { type: Select<'batch_burn'>; tokenIds: string }
baseUri: string | { type: Select<'update_royalty_info'>; royaltyInfo: RoyaltyInfo }
jsonExtensions: boolean | { type: Select<'airdrop'>; recipients: string[] }
decrement: boolean | { 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) => { export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { const { minterMessages, sg721Messages, txSigner } = args
vendingMinterMessages, if (!minterMessages || !sg721Messages) {
baseMinterMessages,
openEditionMinterMessages,
sg721Messages,
royaltyRegistryMessages,
txSigner,
} = args
if (
!vendingMinterMessages ||
!baseMinterMessages ||
!openEditionMinterMessages ||
!sg721Messages ||
!royaltyRegistryMessages
) {
throw new Error('Cannot execute actions') throw new Error('Cannot execute actions')
} }
switch (args.type) { 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': { case 'mint_to': {
return vendingMinterMessages.mintTo(txSigner, args.recipient) return minterMessages.mintTo(txSigner, args.recipient)
}
case 'mint_to_open_edition': {
return openEditionMinterMessages.mintTo(txSigner, args.recipient)
} }
case 'mint_for': { case 'mint_for': {
return vendingMinterMessages.mintFor(txSigner, args.recipient, args.tokenId) return minterMessages.mintFor(txSigner, args.recipient, args.tokenId)
} }
case 'batch_mint': { case 'batch_mint': {
return vendingMinterMessages.batchMint(txSigner, args.recipient, args.batchNumber) return minterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
}
case 'batch_mint_open_edition': {
return openEditionMinterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
} }
case 'set_whitelist': { case 'set_whitelist': {
return vendingMinterMessages.setWhitelist(txSigner, args.whitelist) return minterMessages.setWhitelist(txSigner, args.whitelist)
} }
case 'update_start_time': { case 'update_start_time': {
return vendingMinterMessages.updateStartTime(txSigner, args.startTime as string) return minterMessages.updateStartTime(txSigner, args.startTime)
}
case 'update_end_time': {
return openEditionMinterMessages.updateEndTime(txSigner, args.endTime as string)
}
case 'update_start_trading_time': {
return vendingMinterMessages.updateStartTradingTime(txSigner, args.startTime)
} }
case 'update_per_address_limit': { case 'update_per_address_limit': {
return vendingMinterMessages.updatePerAddressLimit(txSigner, args.limit) return minterMessages.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()
} }
case 'shuffle': { case 'shuffle': {
return vendingMinterMessages.shuffle(txSigner) return minterMessages.shuffle(txSigner)
} }
case 'set_royalties_for_infinity_swap': { case 'withdraw': {
return royaltyRegistryMessages.setCollectionRoyaltyProtocol( return minterMessages.withdraw(txSigner)
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 'transfer': { case 'transfer': {
return sg721Messages.transferNft(args.recipient, args.tokenId.toString()) return sg721Messages.transferNft(args.recipient, args.tokenId.toString())
@ -489,29 +190,26 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'batch_transfer': { case 'batch_transfer': {
return sg721Messages.batchTransfer(args.recipient, args.tokenIds) return sg721Messages.batchTransfer(args.recipient, args.tokenIds)
} }
case 'batch_transfer_multi_address': {
return sg721Messages.batchTransferMultiAddress(txSigner, args.tokenRecipients)
}
case 'burn': { case 'burn': {
return sg721Messages.burn(args.tokenId.toString()) return sg721Messages.burn(args.tokenId.toString())
} }
case 'update_royalty_info': {
return sg721Messages.updateRoyaltyInfo(args.royaltyInfo)
}
case 'batch_burn': { case 'batch_burn': {
return sg721Messages.batchBurn(args.tokenIds) return sg721Messages.batchBurn(args.tokenIds)
} }
case 'batch_mint_for': {
return vendingMinterMessages.batchMintFor(txSigner, args.recipient, args.tokenIds)
}
case 'airdrop': { case 'airdrop': {
return vendingMinterMessages.airdrop(txSigner, args.recipients) return minterMessages.airdrop(txSigner, args.recipients)
} }
case 'airdrop_open_edition': { case 'update_token_metadata': {
return openEditionMinterMessages.airdrop(txSigner, args.recipients) return sg721Messages.updateTokenMetadata(args.tokenId.toString(), args.tokenUri)
} }
case 'airdrop_specific': { case 'batch_update_token_metadata': {
return vendingMinterMessages.airdropSpecificTokens(txSigner, args.tokenRecipients) return sg721Messages.batchUpdateTokenMetadata(args.tokenIds, args.baseUri)
} }
case 'burn_remaining': { case 'freeze_token_metadata': {
return vendingMinterMessages.burnRemaining(txSigner) return sg721Messages.freezeTokenMetadata()
} }
default: { default: {
throw new Error('Unknown action') throw new Error('Unknown action')
@ -521,127 +219,61 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
export const previewExecutePayload = (args: DispatchExecuteArgs) => { export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks // 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 // eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: sg721Messages } = useSG721Contract() const { messages: sg721Messages } = useSG721Contract()
// eslint-disable-next-line react-hooks/rules-of-hooks const { minterContract, sg721Contract } = args
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
switch (args.type) { 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': { case 'mint_to': {
return vendingMinterMessages(minterContract)?.mintTo(args.recipient) return minterMessages(minterContract)?.mintTo(args.recipient)
}
case 'mint_to_open_edition': {
return openEditionMinterMessages(minterContract)?.mintTo(args.recipient)
} }
case 'mint_for': { case 'mint_for': {
return vendingMinterMessages(minterContract)?.mintFor(args.recipient, args.tokenId) return minterMessages(minterContract)?.mintFor(args.recipient, args.tokenId)
} }
case 'batch_mint': { case 'batch_mint': {
return vendingMinterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber) return minterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
}
case 'batch_mint_open_edition': {
return openEditionMinterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
} }
case 'set_whitelist': { case 'set_whitelist': {
return vendingMinterMessages(minterContract)?.setWhitelist(args.whitelist) return minterMessages(minterContract)?.setWhitelist(args.whitelist)
} }
case 'update_start_time': { case 'update_start_time': {
return vendingMinterMessages(minterContract)?.updateStartTime(args.startTime as string) return minterMessages(minterContract)?.updateStartTime(args.startTime)
}
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)
} }
case 'update_per_address_limit': { case 'update_per_address_limit': {
return vendingMinterMessages(minterContract)?.updatePerAddressLimit(args.limit) return minterMessages(minterContract)?.updatePerAddressLimit(args.limit)
} }
case 'update_collection_info': { case 'shuffle': {
return sg721Messages(sg721Contract)?.updateCollectionInfo(args.collectionInfo as CollectionInfo) return minterMessages(minterContract)?.shuffle()
} }
case 'freeze_collection_info': { case 'withdraw': {
return sg721Messages(sg721Contract)?.freezeCollectionInfo() return minterMessages(minterContract)?.withdraw()
} }
case 'update_token_metadata': { case 'update_token_metadata': {
return sg721Messages(sg721Contract)?.updateTokenMetadata(args.tokenId.toString(), args.tokenUri) return sg721Messages(sg721Contract)?.updateTokenMetadata(args.tokenId.toString(), args.tokenUri)
} }
case 'batch_update_token_metadata': { 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': { case 'freeze_token_metadata': {
return sg721Messages(sg721Contract)?.freezeTokenMetadata() 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': { case 'transfer': {
return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString()) return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString())
} }
case 'batch_transfer': { case 'batch_transfer': {
return sg721Messages(sg721Contract)?.batchTransfer(args.recipient, args.tokenIds) return sg721Messages(sg721Contract)?.batchTransfer(args.recipient, args.tokenIds)
} }
case 'batch_transfer_multi_address': {
return sg721Messages(sg721Contract)?.batchTransferMultiAddress(args.tokenRecipients)
}
case 'burn': { case 'burn': {
return sg721Messages(sg721Contract)?.burn(args.tokenId.toString()) return sg721Messages(sg721Contract)?.burn(args.tokenId.toString())
} }
case 'update_royalty_info': {
return sg721Messages(sg721Contract)?.updateRoyaltyInfo(args.royaltyInfo)
}
case 'batch_burn': { case 'batch_burn': {
return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds) return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds)
} }
case 'batch_mint_for': {
return vendingMinterMessages(minterContract)?.batchMintFor(args.recipient, args.tokenIds)
}
case 'airdrop': { case 'airdrop': {
return vendingMinterMessages(minterContract)?.airdrop(args.recipients) return minterMessages(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()
} }
default: { default: {
return {} return {}

View File

@ -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>
)
}

View File

@ -1,34 +1,23 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import clsx from 'clsx' import clsx from 'clsx'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup' import { FormGroup } from 'components/FormGroup'
import { useInputState } from 'components/forms/FormInput.hooks' 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 type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast' 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 { TextInput } from '../../forms/FormInput'
import type { MinterType } from '../actions/Combobox'
import type { UploadMethod } from './UploadDetails' import type { UploadMethod } from './UploadDetails'
interface CollectionDetailsProps { interface CollectionDetailsProps {
onChange: (data: CollectionDetailsDataProps) => void onChange: (data: CollectionDetailsDataProps) => void
uploadMethod: UploadMethod uploadMethod: UploadMethod
coverImageUrl: string coverImageUrl: string
minterType: MinterType
importedCollectionDetails?: CollectionDetailsDataProps
} }
export interface CollectionDetailsDataProps { export interface CollectionDetailsDataProps {
@ -37,25 +26,10 @@ export interface CollectionDetailsDataProps {
symbol: string symbol: string
imageFile: File[] imageFile: File[]
externalLink?: string externalLink?: string
startTradingTime?: string
explicit: boolean
updatable: boolean
} }
export const CollectionDetails = ({ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: CollectionDetailsProps) => {
onChange,
uploadMethod,
coverImageUrl,
minterType,
importedCollectionDetails,
}: CollectionDetailsProps) => {
const [coverImage, setCoverImage] = useState<File | null>(null) 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({ const nameState = useInputState({
id: 'name', id: 'name',
@ -92,45 +66,15 @@ export const CollectionDetails = ({
description: descriptionState.value, description: descriptionState.value,
symbol: symbolState.value, symbol: symbolState.value,
imageFile: coverImage ? [coverImage] : [], imageFile: coverImage ? [coverImage] : [],
externalLink: externalLinkState.value || undefined, externalLink: externalLinkState.value,
startTradingTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
explicit,
updatable,
} }
onChange(data) onChange(data)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } }) toast.error(error.message)
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [nameState.value, descriptionState.value, coverImage, externalLinkState.value])
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])
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => { const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null) return toast.error('Error selecting cover image') if (event.target.files === null) return toast.error('Error selecting cover image')
@ -142,166 +86,24 @@ export const CollectionDetails = ({
reader.onload = (e) => { reader.onload = (e) => {
if (!e.target?.result) return toast.error('Error parsing file.') if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.') if (!event.target.files) return toast.error('No files selected.')
const imageFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { const imageFile = new File([e.target.result], event.target.files[0].name, { type: 'image/jpg' })
type: 'image/jpg',
})
setCoverImage(imageFile) setCoverImage(imageFile)
} }
reader.readAsArrayBuffer(event.target.files[0]) 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 ( return (
<div> <div>
<FormGroup subtitle="Information about your collection" title="Collection Details"> <FormGroup subtitle="Information about your collection" title="Collection Details">
<div className={clsx(minterType === 'base' ? 'grid grid-cols-2 -ml-16 max-w-5xl' : '')}> <TextInput {...nameState} isRequired />
<div className={clsx(minterType === 'base' ? 'ml-0' : '')}> <TextInput {...descriptionState} isRequired />
<TextInput {...nameState} isRequired /> <TextInput {...symbolState} 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>
<FormControl <FormControl isRequired={uploadMethod === 'new'} title="Cover Image">
className={clsx(minterType === 'base' ? '-ml-16' : '')}
isRequired={uploadMethod === 'new'}
title="Cover Image"
>
{uploadMethod === 'new' && ( {uploadMethod === 'new' && (
<input <input
accept="image/*" accept="image/*"
className={clsx( 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', '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', '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"> <div className="max-w-[200px] max-h-[200px] rounded border-2">
<img <img
alt="no-preview-available" alt="no-preview-available"
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring( src={`https://ipfs.io/ipfs/${coverImageUrl.substring(coverImageUrl.lastIndexOf('ipfs://') + 7)}`}
coverImageUrl.lastIndexOf('ipfs://') + 7,
)}`}
/> />
</div> </div>
)} )}
@ -335,88 +135,8 @@ export const CollectionDetails = ({
<span className="italic font-light ">Waiting for cover image URL to be specified.</span> <span className="italic font-light ">Waiting for cover image URL to be specified.</span>
)} )}
</FormControl> </FormControl>
<Conditional test={minterType !== 'base'}>
<div className={clsx(minterType === 'base' ? 'flex flex-col -ml-6 space-y-2' : 'flex flex-col space-y-2')}> <TextInput {...externalLinkState} />
<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>
</FormGroup> </FormGroup>
</div> </div>
) )

View File

@ -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 { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup' 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 { 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 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' import type { UploadMethod } from './UploadDetails'
interface MintingDetailsProps { interface MintingDetailsProps {
onChange: (data: MintingDetailsDataProps) => void onChange: (data: MintingDetailsDataProps) => void
numberOfTokens: number | undefined numberOfTokens: number | undefined
uploadMethod: UploadMethod uploadMethod: UploadMethod
minimumMintPrice: number
mintingTokenFromFactory?: TokenInfo
importedMintingDetails?: MintingDetailsDataProps
isPresale: boolean
whitelistStartDate?: string
} }
export interface MintingDetailsDataProps { export interface MintingDetailsDataProps {
@ -32,24 +18,10 @@ export interface MintingDetailsDataProps {
unitPrice: string unitPrice: string
perAddressLimit: number perAddressLimit: number
startTime: string startTime: string
paymentAddress?: string
selectedMintToken?: TokenInfo
} }
export const MintingDetails = ({ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: MintingDetailsProps) => {
onChange,
numberOfTokens,
uploadMethod,
minimumMintPrice,
mintingTokenFromFactory,
importedMintingDetails,
isPresale,
whitelistStartDate,
}: MintingDetailsProps) => {
const wallet = useWallet()
const { timezone } = useGlobalSettings()
const [timestamp, setTimestamp] = useState<Date | undefined>() const [timestamp, setTimestamp] = useState<Date | undefined>()
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
const numberOfTokensState = useNumberInputState({ const numberOfTokensState = useNumberInputState({
id: 'numberoftokens', id: 'numberoftokens',
@ -63,9 +35,7 @@ export const MintingDetails = ({
id: 'unitPrice', id: 'unitPrice',
name: 'unitPrice', name: 'unitPrice',
title: 'Unit Price', title: 'Unit Price',
subtitle: `Price of each token (min. ${minimumMintPrice} ${ subtitle: 'Price of each token (min. 50 STARS)',
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '50', placeholder: '50',
}) })
@ -77,68 +47,17 @@ export const MintingDetails = ({
placeholder: '1', 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(() => { useEffect(() => {
if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens) if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens)
const data: MintingDetailsDataProps = { const data: MintingDetailsDataProps = {
numTokens: numberOfTokensState.value, numTokens: numberOfTokensState.value,
unitPrice: unitPriceState.value unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : '',
? (Number(unitPriceState.value) * 1_000_000).toString()
: unitPriceState.value === 0
? '0'
: '',
perAddressLimit: perAddressLimitState.value, perAddressLimit: perAddressLimitState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
paymentAddress: paymentAddressState.value.trim(),
selectedMintToken,
} }
console.log('Timestamp:', timestamp?.getTime())
onChange(data) onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [numberOfTokens, numberOfTokensState.value, unitPriceState.value, perAddressLimitState.value, timestamp])
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])
return ( return (
<div> <div>
@ -149,58 +68,12 @@ export const MintingDetails = ({
isRequired isRequired
value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value} value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value}
/> />
<div className="flex flex-row items-end"> <NumberInput {...unitPriceState} isRequired />
<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 {...perAddressLimitState} isRequired /> <NumberInput {...perAddressLimitState} isRequired />
<FormControl <FormControl htmlId="timestamp" isRequired subtitle="Minting start time (local)" title="Start Time">
htmlId="timestamp" <InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={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> </FormControl>
</FormGroup> </FormGroup>
<TextInput className="p-4 mt-5" {...paymentAddressState} />
</div> </div>
) )
} }

View File

@ -2,14 +2,11 @@ import { Conditional } from 'components/Conditional'
import { FormGroup } from 'components/FormGroup' import { FormGroup } from 'components/FormGroup'
import { useInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useWallet } from 'utils/wallet'
import { resolveAddress } from '../../../utils/resolveAddress'
import { NumberInput, TextInput } from '../../forms/FormInput' import { NumberInput, TextInput } from '../../forms/FormInput'
interface RoyaltyDetailsProps { interface RoyaltyDetailsProps {
onChange: (data: RoyaltyDetailsDataProps) => void onChange: (data: RoyaltyDetailsDataProps) => void
importedRoyaltyDetails?: RoyaltyDetailsDataProps
} }
export interface RoyaltyDetailsDataProps { export interface RoyaltyDetailsDataProps {
@ -20,8 +17,7 @@ export interface RoyaltyDetailsDataProps {
type RoyaltyState = 'none' | 'new' type RoyaltyState = 'none' | 'new'
export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDetailsProps) => { export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
const wallet = useWallet()
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none') const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
const royaltyPaymentAddressState = useInputState({ const royaltyPaymentAddressState = useInputState({
@ -37,39 +33,19 @@ export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDeta
name: 'royaltyShare', name: 'royaltyShare',
title: 'Share Percentage', title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid', subtitle: 'Percentage of royalties to be paid',
placeholder: '5%', placeholder: '8%',
}) })
useEffect(() => { useEffect(() => {
void resolveAddress( const data: RoyaltyDetailsDataProps = {
royaltyPaymentAddressState.value royaltyType: royaltyState,
.toLowerCase() paymentAddress: royaltyPaymentAddressState.value,
.replace(/,/g, '') share: Number(royaltyShareState.value),
.replace(/"/g, '') }
.replace(/'/g, '') onChange(data)
.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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value]) }, [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 ( return (
<div className="py-3 px-8 rounded border-2 border-white/20"> <div className="py-3 px-8 rounded border-2 border-white/20">
<div className="flex justify-center"> <div className="flex justify-center">

View File

@ -1,8 +1,4 @@
/* eslint-disable eslint-comments/disable-enable-pair */ // eslint-disable-next-line 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 @typescript-eslint/no-loop-func */ /* eslint-disable @typescript-eslint/no-loop-func */
import clsx from 'clsx' import clsx from 'clsx'
import { Alert } from 'components/Alert' import { Alert } from 'components/Alert'
@ -11,38 +7,22 @@ import { AssetsPreview } from 'components/AssetsPreview'
import { Conditional } from 'components/Conditional' import { Conditional } from 'components/Conditional'
import { TextInput } from 'components/forms/FormInput' import { TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import { MetadataInput } from 'components/MetadataInput'
import { MetadataModal } from 'components/MetadataModal' 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 type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload' import type { UploadServiceType } from 'services/upload'
import { 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 { naturalCompare } from 'utils/sort'
import type { MinterType } from '../actions/Combobox'
import type { BaseMinterAcquisitionMethod } from './BaseMinterDetails'
export type UploadMethod = 'new' | 'existing' export type UploadMethod = 'new' | 'existing'
interface UploadDetailsProps { interface UploadDetailsProps {
onChange: (value: UploadDetailsDataProps) => void onChange: (value: UploadDetailsDataProps) => void
minterType: MinterType
baseMinterAcquisitionMethod?: BaseMinterAcquisitionMethod
importedUploadDetails?: UploadDetailsDataProps
} }
export interface UploadDetailsDataProps { export interface UploadDetailsDataProps {
assetFiles: File[] assetFiles: File[]
metadataFiles: File[] metadataFiles: File[]
thumbnailFiles?: File[]
thumbnailCompatibleAssetFileNames?: string[]
uploadService: UploadServiceType uploadService: UploadServiceType
nftStorageApiKey?: string nftStorageApiKey?: string
pinataApiKey?: string pinataApiKey?: string
@ -50,30 +30,15 @@ export interface UploadDetailsDataProps {
uploadMethod: UploadMethod uploadMethod: UploadMethod
baseTokenURI?: string baseTokenURI?: string
imageUrl?: string imageUrl?: string
baseMinterMetadataFile?: File
} }
export const UploadDetails = ({ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
onChange,
minterType,
baseMinterAcquisitionMethod,
importedUploadDetails,
}: UploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([]) const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([]) const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [thumbnailCompatibleAssetFileNames, setThumbnailCompatibleAssetFileNames] = useState<string[]>([])
const [thumbnailFilesArray, setThumbnailFilesArray] = useState<File[]>([])
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new') const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage') const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0) const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
const [refreshMetadata, setRefreshMetadata] = useState(false) 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({ const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key', id: 'nft-storage-api-key',
@ -100,7 +65,7 @@ export const UploadDetails = ({
const baseTokenUriState = useInputState({ const baseTokenUriState = useInputState({
id: 'baseTokenUri', id: 'baseTokenUri',
name: 'baseTokenUri', name: 'baseTokenUri',
title: minterType === 'vending' ? 'Base Token URI' : 'Token URI', title: 'Base Token URI',
placeholder: 'ipfs://', placeholder: 'ipfs://',
defaultValue: '', defaultValue: '',
}) })
@ -116,78 +81,19 @@ export const UploadDetails = ({
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => { const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFilesArray([]) setAssetFilesArray([])
setMetadataFilesArray([]) setMetadataFilesArray([])
setThumbnailFilesArray([])
setThumbnailCompatibleAssetFileNames([])
if (event.target.files === null) return if (event.target.files === null) return
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html', 'document'] //sort the files
const thumbnailCompatibleFileNamesList: string[] = [] const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
if (minterType === 'vending') { //check if the sorted file names are in numerical order
//sort the files const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name)) for (let i = 0; i < sortedFileNames.length; i++) {
//check if the sorted file names are in numerical order if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) {
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0]) toast.error('The file names should be in numerical order starting from 1.')
sortedFiles.map((file) => { //clear the input
if (thumbnailCompatibleAssetTypes.includes(getAssetType(file.name))) { event.target.value = ''
thumbnailCompatibleFileNamesList.push(file.name.split('.')[0]) return
}
})
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
}
} }
} 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 let loadedFileCount = 0
const files: File[] = [] const files: File[] = []
let reader: FileReader let reader: FileReader
@ -196,9 +102,7 @@ export const UploadDetails = ({
reader.onload = (e) => { reader.onload = (e) => {
if (!e.target?.result) return toast.error('Error parsing file.') if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.') if (!event.target.files) return toast.error('No files selected.')
const assetFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), { const assetFile = new File([e.target.result], event.target.files[i].name, { type: 'image/jpg' })
type: 'image/jpg',
})
files.push(assetFile) files.push(assetFile)
} }
reader.readAsArrayBuffer(event.target.files[i]) reader.readAsArrayBuffer(event.target.files[i])
@ -215,68 +119,20 @@ export const UploadDetails = ({
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => { const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFilesArray([]) setMetadataFilesArray([])
if (event.target.files === null) return toast.error('No files selected.') if (event.target.files === null) return toast.error('No files selected.')
if ( if (event.target.files.length !== assetFilesArray.length) {
(minterType === 'vending' || (minterType === 'base' && assetFilesArray.length > 1)) &&
event.target.files.length !== assetFilesArray.length
) {
event.target.value = '' event.target.value = ''
return toast.error('The number of metadata files should be equal to the number of asset files.') 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 //sort the files
if ( const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
minterType === 'base' && //check if the sorted file names are in numerical order
assetFilesArray.length > 1 && const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
event.target.files[0].name.split('.')[0] !== assetFilesArray[0].name.split('.')[0] for (let i = 0; i < sortedFileNames.length; i++) {
) { if (isNaN(Number(sortedFileNames[i])) || parseInt(sortedFileNames[i]) !== i + 1) {
event.target.value = '' toast.error('The file names should be in numerical order starting from 1.')
toast.error('The metadata file names should match the asset file names.') //clear the input
addLogItem({ event.target.value = ''
id: uid(), return
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
}
} }
} }
let loadedFileCount = 0 let loadedFileCount = 0
@ -284,26 +140,11 @@ export const UploadDetails = ({
let reader: FileReader let reader: FileReader
for (let i = 0; i < event.target.files.length; i++) { for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader() reader = new FileReader()
reader.onload = async (e) => { reader.onload = (e) => {
if (!e.target?.result) return toast.error('Error parsing file.') if (!e.target?.result) return toast.error('Error parsing file.')
if (!event.target.files) return toast.error('No files selected.') if (!event.target.files) return toast.error('No files selected.')
const metadataFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), { const metadataFile = new File([e.target.result], event.target.files[i].name, { type: 'application/json' })
type: 'application/json',
})
files.push(metadataFile) 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.readAsText(event.target.files[i], 'utf8')
reader.onloadend = () => { reader.onloadend = () => {
@ -321,64 +162,10 @@ export const UploadDetails = ({
setRefreshMetadata((prev) => !prev) setRefreshMetadata((prev) => !prev)
} }
const updateMetadataFileArray = (updatedMetadataFile: File) => { const updateMetadataFileArray = async (updatedMetadataFile: File) => {
metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile
} console.log('Updated Metadata File:')
console.log(JSON.parse(await metadataFilesArray[metadataFileArrayIndex]?.text()))
const updateBaseMinterMetadataFile = (updatedMetadataFile: File) => {
setBaseMinterMetadataFile(updatedMetadataFile)
console.log('Updated Base Minter Metadata File:')
console.log(baseMinterMetadataFile)
}
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
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)))
}
}
}
} }
useEffect(() => { useEffect(() => {
@ -386,39 +173,21 @@ export const UploadDetails = ({
const data: UploadDetailsDataProps = { const data: UploadDetailsDataProps = {
assetFiles: assetFilesArray, assetFiles: assetFilesArray,
metadataFiles: metadataFilesArray, metadataFiles: metadataFilesArray,
thumbnailFiles: thumbnailFilesArray,
thumbnailCompatibleAssetFileNames,
uploadService, uploadService,
nftStorageApiKey: nftStorageApiKeyState.value, nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value, pinataApiKey: pinataApiKeyState.value,
pinataSecretKey: pinataSecretKeyState.value, pinataSecretKey: pinataSecretKeyState.value,
uploadMethod, uploadMethod,
baseTokenURI: baseTokenUriState.value baseTokenURI: baseTokenUriState.value,
.replace('IPFS://', 'ipfs://') imageUrl: coverImageUrlState.value,
.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,
} }
onChange(data) onChange(data)
} catch (error: any) { } catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } }) toast.error(error.message)
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
} }
}, [ }, [
assetFilesArray, assetFilesArray,
metadataFilesArray, metadataFilesArray,
thumbnailFilesArray,
thumbnailCompatibleAssetFileNames,
uploadService, uploadService,
nftStorageApiKeyState.value, nftStorageApiKeyState.value,
pinataApiKeyState.value, pinataApiKeyState.value,
@ -426,53 +195,17 @@ export const UploadDetails = ({
uploadMethod, uploadMethod,
baseTokenUriState.value, baseTokenUriState.value,
coverImageUrlState.value, coverImageUrlState.value,
refreshMetadata,
baseMinterMetadataFile,
]) ])
useEffect(() => { useEffect(() => {
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
setMetadataFilesArray([])
if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([]) setAssetFilesArray([])
if (thumbnailFilesRef.current) thumbnailFilesRef.current.value = '' setMetadataFilesArray([])
setThumbnailFilesArray([]) baseTokenUriState.onChange('')
setThumbnailCompatibleAssetFileNames([]) coverImageUrlState.onChange('')
if (!importedUploadDetails || minterType === 'base') { }, [uploadMethod])
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])
return ( 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="flex justify-center">
<div className="mt-3 ml-4 font-bold form-check form-check-inline"> <div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input <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" 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" htmlFor="inlineRadio2"
> >
{minterType === 'base' ? 'Upload assets & metadata' : 'Upload assets & metadata'} Upload assets & metadata
</label> </label>
</div> </div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline"> <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" 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" htmlFor="inlineRadio1"
> >
{minterType === 'base' ? 'Use an existing Token URI' : 'Use an existing base URI'} Use an existing base URI
</label> </label>
</div> </div>
</div> </div>
<div className="p-3 py-5 pb-8"> <div className="p-3 py-5 pb-8">
<Conditional test={uploadMethod === 'existing' && minterType === 'vending'}> <Conditional test={uploadMethod === 'existing'}>
<div className="ml-3 flex-column"> <div className="ml-3 flex-column">
<p className="mb-5 ml-5"> <p className="mb-5 ml-5">
Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a Though Stargaze&apos;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. and upload your assets & metadata manually to get a base URI for your collection.
</p> </p>
<div> <div>
<Tooltip <TextInput {...baseTokenUriState} className="w-1/2" />
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>
</div> </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&apos;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> <div>
<Tooltip <TextInput {...coverImageUrlState} className="mt-2 w-1/2" />
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>
</div> </div>
<Conditional
test={minterType !== 'base' || (minterType === 'base' && baseMinterAcquisitionMethod === 'new')}
>
<div>
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
</div>
</Conditional>
</div> </div>
</Conditional> </Conditional>
<Conditional test={uploadMethod === 'new'}> <Conditional test={uploadMethod === 'new'}>
@ -626,22 +317,7 @@ export const UploadDetails = ({
<div className="flex w-full"> <div className="flex w-full">
<Conditional test={uploadService === 'nft-storage'}> <Conditional test={uploadService === 'nft-storage'}>
<div className="flex-col w-full"> <TextInput {...nftStorageApiKeyState} className="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>
<Conditional test={uploadService === 'pinata'}> <Conditional test={uploadService === 'pinata'}>
<TextInput {...pinataApiKeyState} className="w-full" /> <TextInput {...pinataApiKeyState} className="w-full" />
@ -680,7 +356,7 @@ export const UploadDetails = ({
)} )}
> >
<input <input
accept="image/*, audio/*, video/*, .html, .pdf" accept="image/*, audio/*, video/*"
className={clsx( className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer', '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', 'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
@ -688,7 +364,6 @@ export const UploadDetails = ({
id="assetFiles" id="assetFiles"
multiple multiple
onChange={selectAssets} onChange={selectAssets}
ref={assetFilesRef}
type="file" type="file"
/> />
</div> </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" className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="metadataFiles" htmlFor="metadataFiles"
> >
{minterType === 'vending' Metadata Selection
? 'Metadata Selection'
: assetFilesArray.length === 1
? 'Metadata Selection (optional)'
: 'Metadata Selection'}
</label> </label>
<div <div
className={clsx( className={clsx(
@ -721,80 +392,24 @@ export const UploadDetails = ({
id="metadataFiles" id="metadataFiles"
multiple multiple
onChange={selectMetadata} onChange={selectMetadata}
ref={metadataFilesRef}
type="file" type="file"
/> />
</div> </div>
</div> </div>
)} )}
{thumbnailCompatibleAssetFileNames.length > 0 && ( <MetadataModal
<div> assetFile={assetFilesArray[metadataFileArrayIndex]}
<label metadataFile={metadataFilesArray[metadataFileArrayIndex]}
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300" refresher={refreshMetadata}
htmlFor="thumbnailFiles" updateMetadata={updateMetadataFileArray}
> />
{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>
</div> </div>
<Conditional test={assetFilesArray.length > 0 && minterType === 'vending'}> <Conditional test={assetFilesArray.length > 0}>
<AssetsPreview assetFilesArray={assetFilesArray} updateMetadataFileIndex={updateMetadataFileIndex} /> <AssetsPreview assetFilesArray={assetFilesArray} updateMetadataFileIndex={updateMetadataFileIndex} />
</Conditional> </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> </div>
<Conditional test={minterType === 'base' && assetFilesArray.length === 1}>
<MetadataInput
selectedAssetFile={assetFilesArray[0]}
selectedMetadataFile={metadataFilesArray[0]}
updateMetadataToUpload={updateBaseMinterMetadataFile}
/>
</Conditional>
</div> </div>
</div> </div>
</Conditional> </Conditional>

View File

@ -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 { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup' 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 { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' 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 React, { useEffect, useState } from 'react'
import { isValidAddress } from 'utils/isValidAddress'
import { useWallet } from 'utils/wallet'
import { Conditional } from '../../Conditional' import { Conditional } from '../../Conditional'
import { AddressInput, NumberInput } from '../../forms/FormInput' import { AddressInput, NumberInput } from '../../forms/FormInput'
@ -23,44 +11,26 @@ import { WhitelistUpload } from '../../WhitelistUpload'
interface WhitelistDetailsProps { interface WhitelistDetailsProps {
onChange: (data: WhitelistDetailsDataProps) => void onChange: (data: WhitelistDetailsDataProps) => void
mintingTokenFromFactory?: TokenInfo
importedWhitelistDetails?: WhitelistDetailsDataProps
} }
export interface WhitelistDetailsDataProps { export interface WhitelistDetailsDataProps {
whitelistState: WhitelistState whitelistType: WhitelistState
whitelistType: WhitelistType
contractAddress?: string contractAddress?: string
members?: string[] | WhitelistFlexMember[] members?: string[]
unitPrice?: string unitPrice?: string
startTime?: string startTime?: string
endTime?: string endTime?: string
perAddressLimit?: number perAddressLimit?: number
memberLimit?: number memberLimit?: number
admins?: string[]
adminsMutable?: boolean
} }
type WhitelistState = 'none' | 'existing' | 'new' type WhitelistState = 'none' | 'existing' | 'new'
export type WhitelistType = 'standard' | 'flex' | 'merkletree' export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
export const WhitelistDetails = ({
onChange,
mintingTokenFromFactory,
importedWhitelistDetails,
}: WhitelistDetailsProps) => {
const wallet = useWallet()
const { timezone } = useGlobalSettings()
const [whitelistState, setWhitelistState] = useState<WhitelistState>('none') const [whitelistState, setWhitelistState] = useState<WhitelistState>('none')
const [whitelistType, setWhitelistType] = useState<WhitelistType>('standard')
const [startDate, setStartDate] = useState<Date | undefined>(undefined) const [startDate, setStartDate] = useState<Date | undefined>(undefined)
const [endDate, setEndDate] = useState<Date | undefined>(undefined) const [endDate, setEndDate] = useState<Date | undefined>(undefined)
const [whitelistStandardArray, setWhitelistStandardArray] = useState<string[]>([]) const [whitelistArray, setWhitelistArray] = useState<string[]>([])
const [whitelistFlexArray, setWhitelistFlexArray] = useState<WhitelistFlexMember[]>([])
const [whitelistMerkleTreeArray, setWhitelistMerkleTreeArray] = useState<string[]>([])
const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
const whitelistAddressState = useInputState({ const whitelistAddressState = useInputState({
id: 'whitelist-address', id: 'whitelist-address',
@ -69,13 +39,11 @@ export const WhitelistDetails = ({
defaultValue: '', defaultValue: '',
}) })
const unitPriceState = useNumberInputState({ const uniPriceState = useNumberInputState({
id: 'unit-price', id: 'unit-price',
name: 'unitPrice', name: 'unitPrice',
title: 'Unit Price', title: 'Unit Price',
subtitle: `Token price for whitelisted addresses \n (min. 0 ${ subtitle: 'Token price for whitelisted addresses \n (min. 25 STARS)',
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '25', placeholder: '25',
}) })
@ -95,161 +63,34 @@ export const WhitelistDetails = ({
placeholder: '5', placeholder: '5',
}) })
const addressListState = useAddressListState()
const whitelistFileOnChange = (data: string[]) => { const whitelistFileOnChange = (data: string[]) => {
if (whitelistType === 'standard') setWhitelistStandardArray(data) setWhitelistArray(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(() => { useEffect(() => {
const data: WhitelistDetailsDataProps = { const data: WhitelistDetailsDataProps = {
whitelistState, whitelistType: whitelistState,
whitelistType, contractAddress: whitelistAddressState.value,
contractAddress: whitelistAddressState.value members: whitelistArray,
.toLowerCase() unitPrice: uniPriceState.value ? (Number(uniPriceState.value) * 1_000_000).toString() : '',
.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() : '', startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '',
endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '', endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '',
perAddressLimit: perAddressLimitState.value, perAddressLimit: perAddressLimitState.value,
memberLimit: memberLimitState.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) onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
whitelistAddressState.value, whitelistAddressState.value,
unitPriceState.value, uniPriceState.value,
memberLimitState.value, memberLimitState.value,
perAddressLimitState.value, perAddressLimitState.value,
startDate, startDate,
endDate, endDate,
whitelistStandardArray, whitelistArray,
whitelistFlexArray,
whitelistMerkleTreeArray,
whitelistState, 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 ( return (
<div className="py-3 px-8 rounded border-2 border-white/20"> <div className="py-3 px-8 rounded border-2 border-white/20">
<div className="flex justify-center"> <div className="flex justify-center">
@ -261,7 +102,6 @@ export const WhitelistDetails = ({
name="whitelistRadioOptions1" name="whitelistRadioOptions1"
onClick={() => { onClick={() => {
setWhitelistState('none') setWhitelistState('none')
setWhitelistType('standard')
}} }}
type="radio" type="radio"
value="None" value="None"
@ -318,207 +158,34 @@ export const WhitelistDetails = ({
</Conditional> </Conditional>
<Conditional test={whitelistState === 'new'}> <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"> <div className="grid grid-cols-2">
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details"> <FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
<NumberInput isRequired {...unitPriceState} /> <NumberInput isRequired {...uniPriceState} />
<Conditional test={whitelistType !== 'merkletree'}> <NumberInput isRequired {...memberLimitState} />
<NumberInput isRequired {...memberLimitState} /> <NumberInput isRequired {...perAddressLimitState} />
</Conditional>
<Conditional test={whitelistType === 'standard' || whitelistType === 'merkletree'}>
<NumberInput isRequired {...perAddressLimitState} />
</Conditional>
<FormControl <FormControl
htmlId="start-date" htmlId="start-date"
isRequired isRequired
subtitle="Start time for minting tokens to whitelisted addresses" subtitle="Start time for minting tokens to whitelisted addresses"
title={`Whitelist Start Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`} title="Start Time"
> >
<InputDateTime <InputDateTime minDate={new Date()} onChange={(date) => setStartDate(date)} value={startDate} />
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>
<FormControl <FormControl
htmlId="end-date" htmlId="end-date"
isRequired isRequired
subtitle="Whitelist End Time dictates when public sales will start" subtitle="End time for minting tokens to whitelisted addresses"
title={`Whitelist End Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`} title="End Time"
> >
<InputDateTime <InputDateTime minDate={new Date()} onChange={(date) => setEndDate(date)} value={endDate} />
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> </FormControl>
</FormGroup> </FormGroup>
<div> <div>
<div className="mt-2 ml-3 w-[65%] form-control"> <FormGroup subtitle="TXT file that contains the whitelisted addresses" title="Whitelist File">
<label className="justify-start cursor-pointer label"> <WhitelistUpload onChange={whitelistFileOnChange} />
<span className="mr-4 font-bold">Mutable Administrator Addresses</span> </FormGroup>
<input <Conditional test={whitelistArray.length > 0}>
checked={adminsMutable} <JsonPreview content={whitelistArray} initialState title="File Contents" />
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> </Conditional>
</div> </div>
</div> </div>

View File

@ -2,32 +2,19 @@ import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter' import { matchSorter } from 'match-sorter'
import { Fragment, useEffect, useState } from 'react' import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { MinterType } from '../actions/Combobox'
import type { QueryListItem } from './query' 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 { export interface QueryComboboxProps {
value: QueryListItem | null value: QueryListItem | null
onChange: (item: QueryListItem) => void onChange: (item: QueryListItem) => void
minterType?: MinterType
} }
export const QueryCombobox = ({ value, onChange, minterType }: QueryComboboxProps) => { export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => {
const [search, setSearch] = useState('') 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'] }) 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 <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => 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} value={entry}
> >

View File

@ -5,41 +5,23 @@ import { FormControl } from 'components/FormControl'
import { AddressInput, TextInput } from 'components/forms/FormInput' import { AddressInput, TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview' import { JsonPreview } from 'components/JsonPreview'
import type { BaseMinterInstance } from 'contracts/baseMinter' import type { MinterInstance } from 'contracts/minter'
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
import type { SG721Instance } from 'contracts/sg721' import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useWallet } from 'utils/wallet'
import { resolveAddress } from '../../../utils/resolveAddress'
import type { MinterType } from '../actions/Combobox'
interface CollectionQueriesProps { interface CollectionQueriesProps {
minterContractAddress: string minterContractAddress: string
sg721ContractAddress: string sg721ContractAddress: string
royaltyRegistryContractAddress: string
sg721Messages: SG721Instance | undefined sg721Messages: SG721Instance | undefined
vendingMinterMessages: VendingMinterInstance | undefined minterMessages: MinterInstance | undefined
baseMinterMessages: BaseMinterInstance | undefined
openEditionMinterMessages: OpenEditionMinterInstance | undefined
royaltyRegistryMessages: RoyaltyRegistryInstance | undefined
minterType: MinterType
} }
export const CollectionQueries = ({ export const CollectionQueries = ({
sg721ContractAddress, sg721ContractAddress,
sg721Messages, sg721Messages,
minterContractAddress, minterContractAddress,
vendingMinterMessages, minterMessages,
openEditionMinterMessages,
baseMinterMessages,
minterType,
royaltyRegistryMessages,
}: CollectionQueriesProps) => { }: CollectionQueriesProps) => {
const wallet = useWallet()
const comboboxState = useQueryComboboxState() const comboboxState = useQueryComboboxState()
const type = comboboxState.value?.id const type = comboboxState.value?.id
@ -61,60 +43,27 @@ export const CollectionQueries = ({
const address = addressState.value const address = addressState.value
const showTokenIdField = type === 'token_info' 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( const { data: response } = useQuery(
[ [sg721Messages, minterMessages, type, tokenId, address] as const,
sg721Messages,
baseMinterMessages,
vendingMinterMessages,
openEditionMinterMessages,
royaltyRegistryMessages,
type,
tokenId,
address,
sg721ContractAddress,
] as const,
async ({ queryKey }) => { async ({ queryKey }) => {
const [ const [_sg721Messages, _minterMessages, _type, _tokenId, _address] = queryKey
_sg721Messages, const result = await dispatchQuery({
_baseMinterMessages_, tokenId: _tokenId,
_vendingMinterMessages, minterMessages: _minterMessages,
_openEditionMinterMessages, sg721Messages: _sg721Messages,
_royaltyRegistryMessages, address: _address,
_type, type: _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
}) })
return res return result
}, },
{ {
placeholderData: null, placeholderData: null,
onError: (error: any) => { onError: (error: any) => {
if (addressState.value.length > 12 && !addressState.value.includes('.')) { toast.error(error.message)
toast.error(error.message, { style: { maxWidth: 'none' } })
}
}, },
enabled: enabled: Boolean(sg721ContractAddress && minterContractAddress && type),
Boolean(type && type === 'infinity_swap_royalties' && sg721ContractAddress) ||
Boolean(sg721ContractAddress && minterContractAddress && type),
retry: false, retry: false,
}, },
) )
@ -122,7 +71,7 @@ export const CollectionQueries = ({
return ( return (
<div className="grid grid-cols-2 mt-4"> <div className="grid grid-cols-2 mt-4">
<div className="mr-2 space-y-8"> <div className="mr-2 space-y-8">
<QueryCombobox minterType={minterType} {...comboboxState} /> <QueryCombobox {...comboboxState} />
{showAddressField && <AddressInput {...addressState} />} {showAddressField && <AddressInput {...addressState} />}
{showTokenIdField && <TextInput {...tokenIdState} />} {showTokenIdField && <TextInput {...tokenIdState} />}
</div> </div>

View File

@ -1,9 +1,5 @@
import type { BaseMinterInstance } from 'contracts/baseMinter' import type { MinterInstance } from 'contracts/minter'
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter/contract'
import type { RoyaltyRegistryInstance } from 'contracts/royaltyRegistry'
import type { SG721Instance } from 'contracts/sg721' 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] export type QueryType = typeof QUERY_TYPES[number]
@ -12,13 +8,8 @@ export const QUERY_TYPES = [
'mint_price', 'mint_price',
'num_tokens', 'num_tokens',
'tokens_minted_to_user', 'tokens_minted_to_user',
'total_mint_count',
'tokens',
// 'token_owners', // 'token_owners',
'infinity_swap_royalties',
'token_info', 'token_info',
'config',
'status',
] as const ] as const
export interface QueryListItem { export interface QueryListItem {
@ -27,7 +18,7 @@ export interface QueryListItem {
description?: string description?: string
} }
export const VENDING_QUERY_LIST: QueryListItem[] = [ export const QUERY_LIST: QueryListItem[] = [
{ {
id: 'collection_info', id: 'collection_info',
name: 'Collection Info', name: 'Collection Info',
@ -38,11 +29,6 @@ export const VENDING_QUERY_LIST: QueryListItem[] = [
name: 'Mint Price', name: 'Mint Price',
description: `Get the price of minting a token.`, 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', id: 'num_tokens',
name: 'Mintable Number of Tokens', name: 'Mintable Number of Tokens',
@ -63,95 +49,6 @@ export const VENDING_QUERY_LIST: QueryListItem[] = [
name: 'Token Info', name: 'Token Info',
description: `Get information about a token in the collection.`, 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 { export interface DispatchExecuteProps {
@ -162,36 +59,21 @@ export interface DispatchExecuteProps {
type Select<T extends QueryType> = T type Select<T extends QueryType> = T
export type DispatchQueryArgs = { export type DispatchQueryArgs = {
baseMinterMessages?: BaseMinterInstance minterMessages?: MinterInstance
vendingMinterMessages?: VendingMinterInstance
openEditionMinterMessages?: OpenEditionMinterInstance
sg721Messages?: SG721Instance sg721Messages?: SG721Instance
royaltyRegistryMessages?: RoyaltyRegistryInstance
sg721ContractAddress?: string
} & ( } & (
| { type: undefined } | { type: undefined }
| { type: Select<'collection_info'> } | { type: Select<'collection_info'> }
| { type: Select<'mint_price'> } | { type: Select<'mint_price'> }
| { type: Select<'num_tokens'> } | { type: Select<'num_tokens'> }
| { type: Select<'tokens_minted_to_user'>; address: string } | { 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_owners'> }
| { type: Select<'token_info'>; tokenId: string } | { type: Select<'token_info'>; tokenId: string }
| { type: Select<'config'> }
| { type: Select<'status'> }
) )
export const dispatchQuery = async (args: DispatchQueryArgs) => { export const dispatchQuery = async (args: DispatchQueryArgs) => {
const { const { minterMessages, sg721Messages } = args
baseMinterMessages, if (!minterMessages || !sg721Messages) {
vendingMinterMessages,
openEditionMinterMessages,
sg721Messages,
royaltyRegistryMessages,
} = args
if (!baseMinterMessages || !vendingMinterMessages || !openEditionMinterMessages || !sg721Messages) {
throw new Error('Cannot execute actions') throw new Error('Cannot execute actions')
} }
switch (args.type) { switch (args.type) {
@ -199,39 +81,21 @@ export const dispatchQuery = async (args: DispatchQueryArgs) => {
return sg721Messages.collectionInfo() return sg721Messages.collectionInfo()
} }
case 'mint_price': { case 'mint_price': {
return vendingMinterMessages.getMintPrice() return minterMessages.getMintPrice()
} }
case 'num_tokens': { case 'num_tokens': {
return vendingMinterMessages.getMintableNumTokens() return minterMessages.getMintableNumTokens()
} }
case 'tokens_minted_to_user': { case 'tokens_minted_to_user': {
return vendingMinterMessages.getMintCount(args.address) return minterMessages.getMintCount(args.address)
}
case 'total_mint_count': {
return openEditionMinterMessages.getTotalMintCount()
}
case 'tokens': {
return sg721Messages.tokens(args.address)
} }
// case 'token_owners': { // 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': { case 'token_info': {
if (!args.tokenId) return if (!args.tokenId) return
return sg721Messages.allNftInfo(args.tokenId) return sg721Messages.allNftInfo(args.tokenId)
} }
case 'config': {
return baseMinterMessages.getConfig()
}
case 'status': {
return baseMinterMessages.getStatus()
}
default: { default: {
throw new Error('Unknown action') throw new Error('Unknown action')
} }

View File

@ -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) }
}

View File

@ -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>
)
}

View File

@ -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) }
}

View File

@ -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>
)
}

View File

@ -1,4 +1,4 @@
import type { ExecuteListItem } from 'contracts/splits/messages/execute' import type { ExecuteListItem } from 'contracts/minter/messages/execute'
import { useState } from 'react' import { useState } from 'react'
export const useExecuteComboboxState = () => { export const useExecuteComboboxState = () => {

View File

@ -1,8 +1,8 @@
import { Combobox, Transition } from '@headlessui/react' import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/splits/messages/execute' import type { ExecuteListItem } from 'contracts/minter/messages/execute'
import { EXECUTE_LIST } from 'contracts/splits/messages/execute' import { EXECUTE_LIST } from 'contracts/minter/messages/execute'
import { matchSorter } from 'match-sorter' import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
<Combobox.Option <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => 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} value={entry}
> >

View File

@ -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) }
}

View File

@ -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>
)
}

View File

@ -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) }
}

View File

@ -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>
)
}

View File

@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
<Combobox.Option <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => 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} value={entry}
> >

View File

@ -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) }
}

View File

@ -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>
)
}

View File

@ -1,11 +1,8 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
import { Combobox, Transition } from '@headlessui/react' import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx' import clsx from 'clsx'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/whitelist/messages/execute' import type { ExecuteListItem } from 'contracts/whitelist/messages/execute'
import { EXECUTE_LIST } 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 { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
@ -13,20 +10,13 @@ import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
export interface ExecuteComboboxProps { export interface ExecuteComboboxProps {
value: ExecuteListItem | null value: ExecuteListItem | null
onChange: (item: ExecuteListItem) => void 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 [search, setSearch] = useState('')
const filtered = const filtered =
whitelistType !== 'merkletree' search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
? 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'] })
return ( return (
<Combobox <Combobox
@ -77,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange, whitelistType }: ExecuteCombo
<Combobox.Option <Combobox.Option
key={entry.id} key={entry.id}
className={({ active }) => 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} value={entry}
> >

View File

@ -27,9 +27,5 @@ export function useAddressListState() {
}) })
} }
function reset() { return { entries, values, add, update, remove }
setRecord({})
}
return { entries, values, add, update, remove, reset }
} }

View File

@ -1,12 +1,7 @@
import { toUtf8 } from '@cosmjs/encoding'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { AddressInput } from 'components/forms/FormInput' import { AddressInput } from 'components/forms/FormInput'
import { useEffect, useId, useMemo } from 'react' import { useEffect, useId, useMemo } from 'react'
import toast from 'react-hot-toast'
import { FaMinus, FaPlus } from 'react-icons/fa' 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' import { useInputState } from './FormInput.hooks'
@ -31,7 +26,6 @@ export function AddressList(props: AddressListProps) {
{entries.map(([id], i) => ( {entries.map(([id], i) => (
<Address <Address
key={`ib-${id}`} key={`ib-${id}`}
defaultValue={entries[i][1]}
id={id} id={id}
isLast={i === entries.length - 1} isLast={i === entries.length - 1}
onAdd={onAdd} onAdd={onAdd}
@ -49,11 +43,9 @@ export interface AddressProps {
onAdd: AddressListProps['onAdd'] onAdd: AddressListProps['onAdd']
onChange: AddressListProps['onChange'] onChange: AddressListProps['onChange']
onRemove: AddressListProps['onRemove'] onRemove: AddressListProps['onRemove']
defaultValue?: Address
} }
export function Address({ id, isLast, onAdd, onChange, onRemove, defaultValue }: AddressProps) { export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps) {
const wallet = useWallet()
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast]) const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
const htmlId = useId() const htmlId = useId()
@ -62,45 +54,12 @@ export function Address({ id, isLast, onAdd, onChange, onRemove, defaultValue }:
id: `ib-address-${htmlId}`, id: `ib-address-${htmlId}`,
name: `ib-address-${htmlId}`, name: `ib-address-${htmlId}`,
title: ``, 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(() => { useEffect(() => {
if (addressState.value.endsWith('.stars')) { onChange(id, {
void resolveAddress(addressState.value.split('.')[0]) address: addressState.value,
} else { })
onChange(id, {
address: addressState.value,
})
}
}, [addressState.value, id]) }, [addressState.value, id])
return ( return (
@ -108,7 +67,7 @@ export function Address({ id, isLast, onAdd, onChange, onRemove, defaultValue }:
<AddressInput {...addressState} /> <AddressInput {...addressState} />
<div className="flex justify-end items-end pb-2 w-8"> <div className="flex justify-end items-end pb-2 w-8">
<button <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))} onClick={() => (isLast ? onAdd() : onRemove(id))}
type="button" type="button"
> >

View File

@ -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 }
}

View File

@ -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>
)
}

View File

@ -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 }
}

View File

@ -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>
)
}

View File

@ -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>( export const UrlInput = forwardRef<HTMLInputElement, FormInputProps>(
function UrlInput(props, ref) { function UrlInput(props, ref) {
return <FormInput {...props} ref={ref} type="url" /> return <FormInput {...props} ref={ref} type="url" />

View File

@ -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 }
}

View File

@ -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>
)
}

View File

@ -73,13 +73,12 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
}, [traitTypeState.value, traitValueState.value, id]) }, [traitTypeState.value, traitValueState.value, id])
return ( return (
<div className="grid relative xl:grid-cols-[6fr_6fr_1fr] xl:-space-x-8 2xl:space-x-2"> <div className="grid relative grid-cols-[1fr_1fr_auto] space-x-2">
<TraitTypeInput className="lg:w-4/5 2xl:w-full" {...traitTypeState} /> <TraitTypeInput {...traitTypeState} />
<TraitValueInput className="lg:w-4/5 xl:pr-2 xl:w-full" {...traitValueState} /> <TraitValueInput {...traitValueState} />
<div className="flex justify-end items-end pb-2 w-8"> <div className="flex justify-end items-end pb-2 w-8">
<button <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) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
isLast ? onAdd() : onRemove(id) isLast ? onAdd() : onRemove(id)

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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&apos;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>
)
}

View File

@ -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

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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,
}
}

View File

@ -1,9 +1,9 @@
{ {
"path": "/assets/", "path": "/assets/",
"appName": "Stargaze Studio", "appName": "StargazeStudio",
"appShortName": "Stargaze Studio", "appShortName": "StargazeStudio",
"appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT collection in no time.", "appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own NFT collection in no time.",
"developerName": "Stargaze Studio", "developerName": "StargazeStudio",
"developerURL": "https://", "developerURL": "https://",
"background": "#FFC27D", "background": "#FFC27D",
"theme_color": "#FFC27D", "theme_color": "#FFC27D",

View File

@ -1,2 +1,3 @@
export * from './app' export * from './app'
export * from './keplr'
export * from './network' export * from './network'

76
config/keplr.ts Normal file
View 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'],
})

View File

@ -6,6 +6,6 @@ export const meta = {
domain: 'stargaze.tools', domain: 'stargaze.tools',
url: faviconsJson.developerURL, url: faviconsJson.developerURL,
twitter: { twitter: {
username: '@StargazeZone', username: '@stargazestudio',
}, },
} }

View File

@ -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,
]

View File

@ -5,7 +5,6 @@ export const mainnetConfig: AppConfig = {
chainName: 'Stargaze', chainName: 'Stargaze',
addressPrefix: 'stars', addressPrefix: 'stars',
rpcUrl: 'https://rpc.stargaze-apis.com/', rpcUrl: 'https://rpc.stargaze-apis.com/',
httpUrl: 'https://rest.stargaze-apis.com/',
feeToken: 'ustars', feeToken: 'ustars',
stakingToken: 'ustars', stakingToken: 'ustars',
coinMap: { coinMap: {

View File

@ -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,
]

View File

@ -1,4 +1,4 @@
import { create } from 'zustand' import create from 'zustand'
export const useCollectionStore = create(() => ({ export const useCollectionStore = create(() => ({
name: 'Example', name: 'Example',

View File

@ -1,45 +1,21 @@
import type { UseBadgeHubContractProps } from 'contracts/badgeHub' import type { UseMinterContractProps } from 'contracts/minter'
import { useBadgeHubContract } from 'contracts/badgeHub' import { useMinterContract } from 'contracts/minter'
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 { UseSG721ContractProps } from 'contracts/sg721' import type { UseSG721ContractProps } from 'contracts/sg721'
import { useSG721Contract } 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 type { UseWhiteListContractProps } from 'contracts/whitelist'
import { useWhiteListContract } from 'contracts/whitelist' import { useWhiteListContract } from 'contracts/whitelist'
import { type UseWhiteListMerkleTreeContractProps, useWhiteListMerkleTreeContract } from 'contracts/whitelistMerkleTree'
import type { ReactNode, VFC } from 'react' import type { ReactNode, VFC } from 'react'
import { Fragment, useEffect } from 'react' import { Fragment, useEffect } from 'react'
import { create } from 'zustand' import type { State } from 'zustand'
import create from 'zustand'
import type { UseSplitsContractProps } from '../contracts/splits/useContract'
import { useSplitsContract } from '../contracts/splits/useContract'
/** /**
* Contracts store type definitions * Contracts store type definitions
*/ */
export interface ContractsStore { export interface ContractsStore extends State {
sg721: UseSG721ContractProps | null sg721: UseSG721ContractProps | null
vendingMinter: UseVendingMinterContractProps | null minter: UseMinterContractProps | null
baseMinter: UseBaseMinterContractProps | null
openEditionMinter: UseOpenEditionMinterContractProps | null
whitelist: UseWhiteListContractProps | 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 = { export const defaultValues: ContractsStore = {
sg721: null, sg721: null,
vendingMinter: null, minter: null,
baseMinter: null,
openEditionMinter: null,
whitelist: 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 ContractsSubscription: VFC = () => {
const sg721 = useSG721Contract() const sg721 = useSG721Contract()
const vendingMinter = useVendingMinterContract() const minter = useMinterContract()
const baseMinter = useBaseMinterContract()
const openEditionMinter = useOpenEditionMinterContract()
const whitelist = useWhiteListContract() 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(() => { useEffect(() => {
useContracts.setState({ useContracts.setState({
sg721, sg721,
vendingMinter, minter,
baseMinter,
openEditionMinter,
whitelist, whitelist,
whitelistMerkleTree,
vendingFactory,
baseFactory,
openEditionFactory,
badgeHub,
splits,
royaltyRegistry,
}) })
}, [ }, [sg721, minter, whitelist])
sg721,
vendingMinter,
baseMinter,
whitelist,
whitelistMerkleTree,
vendingFactory,
baseFactory,
badgeHub,
splits,
royaltyRegistry,
openEditionMinter,
openEditionFactory,
])
return null return null
} }

View File

@ -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 })
}

View File

@ -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