Compare commits
19 Commits
develop
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad9d755eb | ||
|
|
7bdf81d95e | ||
|
|
5e28d7dfb0 | ||
|
|
71c273c4ef | ||
|
|
2f681bbcba | ||
|
|
03ddc13911 | ||
|
|
32f4157c42 | ||
|
|
dc902313a6 | ||
|
|
35e736062a | ||
|
|
e64073499d | ||
|
|
019a0c4d73 | ||
|
|
129953f7b3 | ||
|
|
6f36284fbd | ||
|
|
f4f11dbe6a | ||
|
|
07a08ca35a | ||
|
|
eb82d10140 | ||
|
|
fc65053978 | ||
|
|
fae92e5483 | ||
|
|
2340657020 |
131
.env.example
131
.env.example
@ -1,129 +1,18 @@
|
|||||||
APP_VERSION=0.8.7
|
APP_VERSION=0.1.0
|
||||||
|
|
||||||
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
|
NEXT_PUBLIC_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
2
.github/CODEOWNERS
vendored
@ -1 +1 @@
|
|||||||
* @MightOfOaks @name-user1 @Ninjatosba
|
* @MightOfOaks @Ninjatosba
|
||||||
|
|||||||
45
.github/workflows/publish.yaml
vendored
45
.github/workflows/publish.yaml
vendored
@ -1,45 +0,0 @@
|
|||||||
name: Publish ApplicationRecord to Registry
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
env:
|
|
||||||
CERC_REGISTRY_USER_KEY: ${{ secrets.CICD_LACONIC_USER_KEY }}
|
|
||||||
CERC_REGISTRY_BOND_ID: ${{ secrets.CICD_LACONIC_BOND_ID }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cns_publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: "Clone project repository"
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18 # though you need version 14 with geojson
|
|
||||||
# - name: "Install exiftool"
|
|
||||||
# run: |
|
|
||||||
# apt-get update -y
|
|
||||||
# apt-get upgrade -y
|
|
||||||
# apt-get install exiftool -y
|
|
||||||
#- name: "Exiftool Version"
|
|
||||||
# run: |
|
|
||||||
# exiftool -ver
|
|
||||||
- name: "Install Yarn"
|
|
||||||
run: npm install -g yarn
|
|
||||||
- name: "Install registry CLI"
|
|
||||||
run: |
|
|
||||||
npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/
|
|
||||||
yarn global add @cerc-io/laconic-registry-cli
|
|
||||||
- name: "Install jq"
|
|
||||||
uses: dcarbone/install-jq-action@v2.1.0
|
|
||||||
- name: "Publish App Record"
|
|
||||||
run: scripts/publish-app-record.sh
|
|
||||||
#- name: "Create Metadata Record"
|
|
||||||
# run: scripts/create-metadata-record.sh
|
|
||||||
- name: "Request Deployment"
|
|
||||||
run: scripts/request-app-deployment.sh
|
|
||||||
@ -1,62 +1,16 @@
|
|||||||
import { toUtf8 } from '@cosmjs/encoding'
|
|
||||||
import clsx from 'clsx'
|
import 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])
|
||||||
|
|||||||
@ -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">
|
||||||
««
|
««
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable no-misleading-character-class */
|
|
||||||
/* eslint-disable no-control-regex */
|
|
||||||
import { toUtf8 } from '@cosmjs/encoding'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { SG721_NAME_ADDRESS } from '../utils/constants'
|
|
||||||
import { isValidAddress } from '../utils/isValidAddress'
|
|
||||||
|
|
||||||
interface BadgeAirdropListUploadProps {
|
|
||||||
onChange: (data: string[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BadgeAirdropListUpload = ({ onChange }: BadgeAirdropListUploadProps) => {
|
|
||||||
const wallet = useWallet()
|
|
||||||
const [resolvedAddresses, setResolvedAddresses] = useState<string[]>([])
|
|
||||||
|
|
||||||
const resolveAddresses = async (names: string[]) => {
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
let i = 0
|
|
||||||
names.map(async (name) => {
|
|
||||||
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
|
|
||||||
await (
|
|
||||||
await wallet.getCosmWasmClient()
|
|
||||||
)
|
|
||||||
.queryContractRaw(
|
|
||||||
SG721_NAME_ADDRESS,
|
|
||||||
toUtf8(
|
|
||||||
Buffer.from(
|
|
||||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
|
|
||||||
'hex',
|
|
||||||
).toString(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
|
||||||
if (tokenUri && isValidAddress(tokenUri)) resolvedAddresses.push(tokenUri)
|
|
||||||
else toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
toast.error(`Error resolving address for the name: ${name}.stars`)
|
|
||||||
})
|
|
||||||
|
|
||||||
i++
|
|
||||||
if (i === names.length) resolve(resolvedAddresses)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return resolvedAddresses
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setResolvedAddresses([])
|
|
||||||
if (!event.target.files) return toast.error('Error opening file')
|
|
||||||
if (event.target.files.length !== 1) {
|
|
||||||
toast.error('No file selected')
|
|
||||||
return onChange([])
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (event.target.files[0]?.type !== 'text/plain') {
|
|
||||||
toast.error('Invalid file type')
|
|
||||||
return onChange([])
|
|
||||||
}
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = async (e: ProgressEvent<FileReader>) => {
|
|
||||||
const text = e.target?.result?.toString()
|
|
||||||
let newline = '\n'
|
|
||||||
if (text?.includes('\r')) newline = '\r'
|
|
||||||
if (text?.includes('\r\n')) newline = '\r\n'
|
|
||||||
|
|
||||||
const cleanText = text?.toLowerCase().replace(/,/g, '').replace(/"/g, '').replace(/'/g, '').replace(/ /g, '')
|
|
||||||
const data = cleanText?.split(newline)
|
|
||||||
const regex =
|
|
||||||
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
|
|
||||||
const printableData = data?.map((item) => item.replace(regex, ''))
|
|
||||||
const names = printableData?.filter((address) => address !== '' && address.endsWith('.stars'))
|
|
||||||
const strippedNames = names?.map((name) => name.split('.')[0])
|
|
||||||
console.log(names)
|
|
||||||
if (strippedNames?.length) {
|
|
||||||
await toast
|
|
||||||
.promise(resolveAddresses(strippedNames), {
|
|
||||||
loading: 'Resolving addresses...',
|
|
||||||
success: 'Address resolution finalized.',
|
|
||||||
error: 'Address resolution failed!',
|
|
||||||
})
|
|
||||||
.then((addresses) => {
|
|
||||||
console.log(addresses)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return onChange([
|
|
||||||
...new Set(
|
|
||||||
printableData
|
|
||||||
?.filter((address) => address !== '' && isValidAddress(address) && address.startsWith('stars'))
|
|
||||||
.concat(resolvedAddresses) || [],
|
|
||||||
),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
reader.readAsText(event.target.files[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept=".txt"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="badge-airdrop-list-file"
|
|
||||||
multiple={false}
|
|
||||||
onChange={onFileChange}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import { Button } from './Button'
|
|
||||||
|
|
||||||
export interface BadgeConfirmationModalProps {
|
|
||||||
confirm: () => void
|
|
||||||
}
|
|
||||||
export const BadgeConfirmationModal = (props: BadgeConfirmationModalProps) => {
|
|
||||||
const [isChecked, setIsChecked] = useState(false)
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" />
|
|
||||||
<label className="cursor-pointer modal" htmlFor="my-modal-2">
|
|
||||||
<label
|
|
||||||
className="absolute top-[23%] bottom-5 left-1/3 max-w-[600px] max-h-[410px] border-2 no-scrollbar modal-box"
|
|
||||||
htmlFor="temp"
|
|
||||||
>
|
|
||||||
{/* <Alert type="warning"></Alert> */}
|
|
||||||
<div className="text-xl font-bold">
|
|
||||||
<div className="text-sm font-thin">
|
|
||||||
You represent and warrant that you have, or have obtained, all rights, licenses, consents, permissions,
|
|
||||||
power and/or authority necessary to grant the rights granted herein for any content that you create,
|
|
||||||
submit, post, promote, or display on or through the Service. You represent and warrant that such contain
|
|
||||||
material subject to copyright, trademark, publicity rights, or other intellectual property rights, unless
|
|
||||||
you have necessary permission or are otherwise legally entitled to post the material and to grant Stargaze
|
|
||||||
Parties the license described above, and that the content does not violate any laws. Stargaze.zone
|
|
||||||
reserves the right to exercise its discretion in concealing user-generated content, should such content be
|
|
||||||
determined to have a detrimental impact on the brand.
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div className="flex flex-row pb-4">
|
|
||||||
<label className="flex flex-col space-y-1" htmlFor="terms">
|
|
||||||
<span className="text-sm font-light text-white">I agree with the terms above.</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
checked={isChecked}
|
|
||||||
className="p-2 mb-1 ml-2"
|
|
||||||
id="terms"
|
|
||||||
name="terms"
|
|
||||||
onClick={() => setIsChecked(!isChecked)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
Are you sure to proceed with creating a new badge?
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end w-full">
|
|
||||||
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">
|
|
||||||
<label
|
|
||||||
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 rounded border-0 btn modal-button"
|
|
||||||
htmlFor="my-modal-2"
|
|
||||||
>
|
|
||||||
Go Back
|
|
||||||
</label>
|
|
||||||
</Button>
|
|
||||||
<Button className="px-0 mt-4 mb-4 max-h-12" isDisabled={!isChecked} onClick={props.confirm}>
|
|
||||||
<label
|
|
||||||
className="w-full h-full text-white bg-plumbus hover:bg-plumbus-light border-0 btn modal-button"
|
|
||||||
htmlFor="my-modal-2"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</label>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
export const BadgeLoadingModal = () => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex overflow-hidden fixed top-0 right-0 bottom-0 left-0 z-50 flex-col justify-center items-center w-full h-screen bg-gray-900 opacity-80"
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<img alt="Pixel Logo" className="mb-5 w-[50px] h-[50px] animate-spin" src="/icon.svg" />
|
|
||||||
<p className="w-1/3 font-bold text-center text-white">Uploading the image for badge creation, please wait...</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/img-redundant-alt */
|
|
||||||
import { truncateAddress } from 'utils/wallet'
|
|
||||||
|
|
||||||
export interface ClickableCollection {
|
|
||||||
contractAddress: string
|
|
||||||
name: string
|
|
||||||
media: string
|
|
||||||
onClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CollectionsTable({ collections }: { collections: ClickableCollection[] }) {
|
|
||||||
return (
|
|
||||||
<table className="w-full divide-y divide-zinc-800 table-fixed">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="py-3.5 pr-3 pl-4 text-sm text-left sm:pl-0 text-infinity-blue" scope="col">
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th className="py-3.5 px-3 text-sm text-left text-infinity-blue" scope="col">
|
|
||||||
Address
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className=" bg-black">
|
|
||||||
{collections
|
|
||||||
? collections?.map((collection) => (
|
|
||||||
<tr
|
|
||||||
key={collection.contractAddress}
|
|
||||||
className="hover:bg-zinc-900 cursor-pointer"
|
|
||||||
onClick={collection.onClick}
|
|
||||||
>
|
|
||||||
<td className="py-2 pr-3 pl-4 whitespace-nowrap sm:pl-0">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="shrink-0 w-11 h-11">
|
|
||||||
<img alt="Collection Image" src={collection.media} />
|
|
||||||
</div>
|
|
||||||
<div className="ml-4 font-medium text-white truncate">{collection.name}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="py-5 px-3 text-zinc-400 whitespace-nowrap">
|
|
||||||
<div className="text-left text-white">
|
|
||||||
{collection.contractAddress?.startsWith('stars')
|
|
||||||
? truncateAddress(collection.contractAddress)
|
|
||||||
: collection.contractAddress}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,61 +1,33 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import { Button } from './Button'
|
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
|
||||||
|
|||||||
@ -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)"
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable no-implicit-coercion */
|
|
||||||
/* eslint-disable import/no-default-export */
|
|
||||||
/* eslint-disable tsdoc/syntax */
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export interface FieldsetBaseType {
|
|
||||||
/**
|
|
||||||
* The input's required id, used to link the label and input, as well as the error message.
|
|
||||||
*/
|
|
||||||
id: string
|
|
||||||
/**
|
|
||||||
* Error message to show input validation.
|
|
||||||
*/
|
|
||||||
error?: string
|
|
||||||
/**
|
|
||||||
* Success message to show input validation.
|
|
||||||
*/
|
|
||||||
success?: string
|
|
||||||
/**
|
|
||||||
* Label to describe the input.
|
|
||||||
*/
|
|
||||||
label?: string | ReactNode
|
|
||||||
/**
|
|
||||||
* Hint to show optional fields or a hint to the user of what to enter in the input.
|
|
||||||
*/
|
|
||||||
hint?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FieldsetType = FieldsetBaseType & {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @name Fieldset
|
|
||||||
* @description A fieldset component, used to share markup for labels, hints, and errors for Input components.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <Fieldset error={error} hint={hint} id={id} label={label}>
|
|
||||||
* <input id={id} {...props} />
|
|
||||||
* </Fieldset>
|
|
||||||
*/
|
|
||||||
export default function Fieldset({ label, hint, id, children, error, success }: FieldsetType) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{!!label && (
|
|
||||||
<div className="flex justify-between mb-1">
|
|
||||||
<label className="block w-full text-sm font-medium text-zinc-700 dark:text-zinc-300" htmlFor={id}>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{typeof hint === 'string' && (
|
|
||||||
<span className="text-sm text-zinc-500 dark:text-zinc-400" id={`${id}-optional`}>
|
|
||||||
{hint}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm text-zinc-600" id={`${id}-error`}>
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{success && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm text-zinc-500" id={`${id}-success`}>
|
|
||||||
{success}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
import { useRef, useState } from 'react'
|
|
||||||
|
|
||||||
import { Button } from './Button'
|
|
||||||
|
|
||||||
export interface IncomeDashboardDisclaimerProps {
|
|
||||||
creatorAddress: string
|
|
||||||
}
|
|
||||||
export const IncomeDashboardDisclaimer = (props: IncomeDashboardDisclaimerProps) => {
|
|
||||||
const [isChecked, setIsChecked] = useState(false)
|
|
||||||
const checkBoxRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const handleCheckBox = () => {
|
|
||||||
checkBoxRef.current?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input className="modal-toggle" defaultChecked={false} id="my-modal-1" ref={checkBoxRef} type="checkbox" />
|
|
||||||
<label className="cursor-pointer modal" htmlFor="my-modal-1">
|
|
||||||
<label
|
|
||||||
className="absolute top-[25%] bottom-5 left-1/3 max-w-[600px] max-h-[450px] border-2 no-scrollbar modal-box"
|
|
||||||
htmlFor="temp"
|
|
||||||
>
|
|
||||||
{/* <Alert type="warning"></Alert> */}
|
|
||||||
<div className="text-xl font-bold">
|
|
||||||
<div className="text-sm font-thin">
|
|
||||||
The tool provided on this website is for informational purposes only and does not constitute tax, legal or
|
|
||||||
financial advice. The information provided by the tool is not intended to be used for tax planning, tax
|
|
||||||
avoidance, promoting, marketing or related purposes. Users should consult their own tax, legal or
|
|
||||||
financial advisors prior to acting on any information provided by the tool. By clicking accept below, you
|
|
||||||
agree that neither Stargaze Foundation or Public Awesome, LLC or any of its directors, officers,
|
|
||||||
employees, or advisors shall be responsible for any errors, omissions, or inaccuracies in the information
|
|
||||||
provided by the tool, and shall not be liable for any damages, losses, or expenses arising out of or in
|
|
||||||
connection with the use of the tool. Furthermore, you agree to indemnify Stargaze Foundation, Public
|
|
||||||
Awesome, LLC and any of its directors, officers, employees and advisors against any claims, suits, or
|
|
||||||
actions related to your use of the tool.
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div className="flex flex-row pb-4">
|
|
||||||
<label className="flex flex-col space-y-1" htmlFor="terms">
|
|
||||||
<span className="text-sm font-light text-white">I agree with the terms above.</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
checked={isChecked}
|
|
||||||
className="p-2 mb-1 ml-2"
|
|
||||||
id="terms"
|
|
||||||
name="terms"
|
|
||||||
onClick={() => setIsChecked(!isChecked)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
Are you sure to proceed to the Creator Revenue Dashboard?
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end w-full">
|
|
||||||
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700">
|
|
||||||
<label
|
|
||||||
className="w-full h-full text-white bg-gray-600 hover:bg-gray-700 rounded border-0 btn modal-button"
|
|
||||||
htmlFor="my-modal-1"
|
|
||||||
>
|
|
||||||
Go Back
|
|
||||||
</label>
|
|
||||||
</Button>
|
|
||||||
<a
|
|
||||||
className="my-4"
|
|
||||||
href={
|
|
||||||
isChecked
|
|
||||||
? `https://metabase.constellations.zone/public/dashboard/4d751721-51ab-46ff-ad27-075ec8d47a17?creator_address=${props.creatorAddress}&chart_granularity_(day%252Fweek%252Fmonth)=week`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Button className="px-5 w-full h-full" isDisabled={!isChecked} onClick={() => handleCheckBox()}>
|
|
||||||
Confirm
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,173 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable import/no-default-export */
|
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
||||||
/* eslint-disable jsx-a11y/autocomplete-valid */
|
|
||||||
/* eslint-disable tsdoc/syntax */
|
|
||||||
import type { PropsOf } from '@headlessui/react/dist/types'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import { forwardRef } from 'react'
|
|
||||||
import { classNames } from 'utils/css'
|
|
||||||
|
|
||||||
import type { FieldsetBaseType } from './Fieldset'
|
|
||||||
import Fieldset from './Fieldset'
|
|
||||||
import type { TrailingSelectProps } from './TrailingSelect'
|
|
||||||
import TrailingSelect from './TrailingSelect'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared styles for all input components.
|
|
||||||
*/
|
|
||||||
export const inputClassNames = {
|
|
||||||
base: [
|
|
||||||
'block w-full rounded-lg bg-white shadow-sm dark:bg-zinc-900 sm:text-sm',
|
|
||||||
'text-white placeholder:text-zinc-500 focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-primary-500 focus:ring-0 focus:ring-offset-0',
|
|
||||||
],
|
|
||||||
valid: 'border-zinc-300 focus:border-zinc-300 dark:border-zinc-800 dark:focus:border-zinc-800',
|
|
||||||
invalid: '!text-red-500 !border-red-500 focus:!border-red-500',
|
|
||||||
success: 'text-green border-green focus:border-green',
|
|
||||||
}
|
|
||||||
|
|
||||||
type InputProps = Omit<PropsOf<'input'> & FieldsetBaseType, 'className'> & {
|
|
||||||
directory?: 'true'
|
|
||||||
mozdirectory?: 'true'
|
|
||||||
webkitdirectory?: 'true'
|
|
||||||
leadingAddon?: string
|
|
||||||
trailingAddon?: string
|
|
||||||
trailingAddonIcon?: ReactNode
|
|
||||||
trailingSelectProps?: TrailingSelectProps
|
|
||||||
autoCompleteOff?: boolean
|
|
||||||
preventAutoCapitalizeFirstLetter?: boolean
|
|
||||||
className?: string
|
|
||||||
icon?: JSX.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @name Input
|
|
||||||
* @description A standard input component, defaults to the text type.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Standard input
|
|
||||||
* <Input id="first-name" name="first-name" />
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Input component with label, placeholder and type email
|
|
||||||
* <Input id="email" name="email" type="email" autoComplete="email" label="Email" placeholder="name@email.com" />
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Input component with label and leading and trailing addons
|
|
||||||
* <Input
|
|
||||||
* id="input-label-leading-trailing"
|
|
||||||
* label="Bid"
|
|
||||||
* placeholder="0.00"
|
|
||||||
* leadingAddon="$"
|
|
||||||
* trailingAddon="USD"
|
|
||||||
* />
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Input component with label and trailing select
|
|
||||||
* const [trailingSelectValue, trailingSelectValueSet] = useState('USD');
|
|
||||||
*
|
|
||||||
* <Input
|
|
||||||
* id="input-label-trailing-select"
|
|
||||||
* label="Bid"
|
|
||||||
* placeholder="0.00"
|
|
||||||
* trailingSelectProps={{
|
|
||||||
* id: 'currency',
|
|
||||||
* label: 'Currency',
|
|
||||||
* value: trailingSelectValue,
|
|
||||||
* onChange: (event) => trailingSelectValueSet(event.target.value),
|
|
||||||
* options: ['USD', 'CAD', 'EUR'],
|
|
||||||
* }}
|
|
||||||
* />
|
|
||||||
*/
|
|
||||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
error,
|
|
||||||
success,
|
|
||||||
hint,
|
|
||||||
label,
|
|
||||||
leadingAddon,
|
|
||||||
trailingAddon,
|
|
||||||
trailingAddonIcon,
|
|
||||||
trailingSelectProps,
|
|
||||||
id,
|
|
||||||
className,
|
|
||||||
type = 'text',
|
|
||||||
autoCompleteOff = false,
|
|
||||||
preventAutoCapitalizeFirstLetter,
|
|
||||||
icon,
|
|
||||||
...rest
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const cachedClassNames = classNames(
|
|
||||||
...inputClassNames.base,
|
|
||||||
className,
|
|
||||||
error ? inputClassNames.invalid : inputClassNames.valid,
|
|
||||||
success ? inputClassNames.success : inputClassNames.valid,
|
|
||||||
leadingAddon && 'pl-7',
|
|
||||||
trailingAddon && 'pr-12',
|
|
||||||
trailingSelectProps && 'pr-16',
|
|
||||||
icon && 'pl-10',
|
|
||||||
)
|
|
||||||
|
|
||||||
const describedBy = [
|
|
||||||
...(error ? [`${id}-error`] : []),
|
|
||||||
...(success ? [`${id}-success`] : []),
|
|
||||||
...(typeof hint === 'string' ? [`${id}-optional`] : []),
|
|
||||||
...(typeof trailingAddon === 'string' ? [`${id}-addon`] : []),
|
|
||||||
].join(' ')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fieldset error={error} hint={hint} id={id} label={label} success={success}>
|
|
||||||
<div className="relative rounded-md shadow-sm">
|
|
||||||
{leadingAddon && (
|
|
||||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
|
||||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm">{leadingAddon}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{icon && (
|
|
||||||
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
|
||||||
<span className="pr-10 text-zinc-500 dark:text-zinc-400 sm:text-sm">{icon}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
|
||||||
aria-describedby={describedBy}
|
|
||||||
aria-invalid={error ? 'true' : undefined}
|
|
||||||
autoCapitalize={`${preventAutoCapitalizeFirstLetter ?? 'off'}`}
|
|
||||||
autoComplete={`${autoCompleteOff ? 'off' : 'on'}`}
|
|
||||||
className={cachedClassNames}
|
|
||||||
id={id}
|
|
||||||
ref={ref}
|
|
||||||
type={type}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!trailingAddon && trailingSelectProps && <TrailingSelect {...trailingSelectProps} />}
|
|
||||||
|
|
||||||
{trailingAddon && (
|
|
||||||
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
|
|
||||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm" id={`${id}-addon`}>
|
|
||||||
{trailingAddon}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{trailingAddonIcon && (
|
|
||||||
<div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
|
|
||||||
<span className="text-zinc-500 dark:text-zinc-400 sm:text-sm" id={`${id}-addonicon`}>
|
|
||||||
{trailingAddonIcon}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Fieldset>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Input.displayName = 'Input'
|
|
||||||
|
|
||||||
export default Input
|
|
||||||
@ -66,7 +66,7 @@ export const JsonPreview = ({
|
|||||||
</div>
|
</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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|||||||
@ -1,155 +0,0 @@
|
|||||||
import { useLogStore } from 'contexts/log'
|
|
||||||
import { useRef, useState } from 'react'
|
|
||||||
import { FaCopy, FaEraser } from 'react-icons/fa'
|
|
||||||
import { copy } from 'utils/clipboard'
|
|
||||||
|
|
||||||
import type { LogItem } from '../contexts/log'
|
|
||||||
import { removeLogItem, setLogItemList } from '../contexts/log'
|
|
||||||
import { Button } from './Button'
|
|
||||||
import { Tooltip } from './Tooltip'
|
|
||||||
|
|
||||||
export interface LogModalProps {
|
|
||||||
tempLogItem?: LogItem
|
|
||||||
}
|
|
||||||
export const LogModal = (props: LogModalProps) => {
|
|
||||||
const logs = useLogStore()
|
|
||||||
const [isChecked, setIsChecked] = useState(false)
|
|
||||||
|
|
||||||
const checkBoxRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const handleCheckBox = () => {
|
|
||||||
checkBoxRef.current?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input className="modal-toggle" defaultChecked={false} id="my-modal-8" ref={checkBoxRef} type="checkbox" />
|
|
||||||
<label className="cursor-pointer modal" htmlFor="my-modal-8">
|
|
||||||
<label
|
|
||||||
className={`absolute top-[15%] bottom-5 left-[21.5%] lg:max-w-[70%] ${
|
|
||||||
logs.itemList.length > 4 ? 'max-h-[750px]' : 'max-h-[500px]'
|
|
||||||
} border-2 no-scrollbar modal-box`}
|
|
||||||
htmlFor="temp"
|
|
||||||
>
|
|
||||||
<div className="text-xl font-bold">
|
|
||||||
<table className="table w-full h-1/2">
|
|
||||||
<thead className="sticky inset-x-0 top-0 bg-blue-400/20 backdrop-blur-sm">
|
|
||||||
<tr>
|
|
||||||
<th className="text-lg font-bold bg-transparent">#</th>
|
|
||||||
<th className="text-lg font-bold bg-transparent">Type</th>
|
|
||||||
<th className="text-lg font-bold bg-transparent">Message</th>
|
|
||||||
<th className="text-lg font-bold bg-transparent">
|
|
||||||
Time (UTC +{-new Date().getTimezoneOffset() / 60}){' '}
|
|
||||||
</th>
|
|
||||||
<th className="bg-transparent" />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{logs.itemList.length > 0 &&
|
|
||||||
logs.itemList.map((logItem, index) => (
|
|
||||||
<tr key={logItem.id} className="p-0 border-b-2 border-teal-200/10 border-collapse">
|
|
||||||
<td className="ml-8 w-[5%] font-mono text-base font-bold bg-transparent">{index + 1}</td>
|
|
||||||
<td
|
|
||||||
className={`w-[5%] font-mono text-base font-bold bg-transparent ${
|
|
||||||
logItem.type === 'Error' ? 'text-red-400' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{logItem.type || 'Info'}
|
|
||||||
</td>
|
|
||||||
<td className="w-[70%] text-sm font-bold bg-transparent">
|
|
||||||
<Tooltip backgroundColor="bg-transparent" label="" placement="bottom">
|
|
||||||
<button
|
|
||||||
className="group flex overflow-auto space-x-2 max-w-xl font-mono text-base text-white/80 hover:underline no-scrollbar"
|
|
||||||
onClick={() => void copy(logItem.message)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span>{logItem.message}</span>
|
|
||||||
<FaCopy className="opacity-0 group-hover:opacity-100" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</td>
|
|
||||||
<td className="w-[20%] font-mono text-base bg-transparent">
|
|
||||||
{logItem.timestamp ? new Date(logItem.timestamp).toLocaleString() : ''}
|
|
||||||
</td>
|
|
||||||
<th className="bg-transparent">
|
|
||||||
<div className="flex items-center space-x-8">
|
|
||||||
<Button
|
|
||||||
className="bg-clip-text border-blue-200"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
removeLogItem(logItem.id)
|
|
||||||
}}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
<FaEraser />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
//line break
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<div className="flex justify-start ml-4 w-full">
|
|
||||||
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700">
|
|
||||||
<label
|
|
||||||
className="w-full h-full text-white bg-gray-600 hover:bg-gray-700 rounded border-0 btn modal-button"
|
|
||||||
htmlFor="my-modal-8"
|
|
||||||
>
|
|
||||||
Go Back
|
|
||||||
</label>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700"
|
|
||||||
onClick={() => {
|
|
||||||
window.localStorage.setItem('error_list', '')
|
|
||||||
setLogItemList([])
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
className="w-full h-full text-white bg-blue-400 hover:bg-blue-500 rounded border-0 btn modal-button"
|
|
||||||
htmlFor="my-modal-8"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</label>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end w-full">
|
|
||||||
<Button
|
|
||||||
className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-700"
|
|
||||||
onClick={() => {
|
|
||||||
const csv = logs.itemList
|
|
||||||
.map((logItem) => {
|
|
||||||
return `${logItem.type as string},${logItem.message},${
|
|
||||||
logItem.timestamp ? new Date(logItem.timestamp).toUTCString().replace(',', '') : ''
|
|
||||||
}`
|
|
||||||
})
|
|
||||||
.join('\n')
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv' })
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.setAttribute('hidden', '')
|
|
||||||
a.setAttribute('href', url)
|
|
||||||
a.setAttribute('download', 'studio_logs.csv')
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
document.body.removeChild(a)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
className="w-full h-full text-white bg-stargaze hover:bg-stargaze/80 rounded border-0 btn modal-button"
|
|
||||||
htmlFor="my-modal-8"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</label>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,8 +1,7 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
/* eslint-disable 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>
|
||||||
|
|||||||
@ -1,237 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
|
|
||||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
|
||||||
|
|
||||||
import { TextInput } from './forms/FormInput'
|
|
||||||
import { useInputState } from './forms/FormInput.hooks'
|
|
||||||
import { MetadataAttributes } from './forms/MetadataAttributes'
|
|
||||||
|
|
||||||
export interface MetadataInputProps {
|
|
||||||
selectedAssetFile: File
|
|
||||||
selectedMetadataFile: File
|
|
||||||
updateMetadataToUpload: (metadataFile: File) => void
|
|
||||||
onChange?: (metadata: any) => void
|
|
||||||
importedMetadata?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MetadataInput = (props: MetadataInputProps) => {
|
|
||||||
const emptyMetadataFile = new File(
|
|
||||||
[JSON.stringify({})],
|
|
||||||
`${props.selectedAssetFile?.name
|
|
||||||
.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))
|
|
||||||
.replaceAll('#', '')}.json`,
|
|
||||||
{ type: 'application/json' },
|
|
||||||
)
|
|
||||||
|
|
||||||
const [metadata, setMetadata] = useState<any>(null)
|
|
||||||
|
|
||||||
const nameState = useInputState({
|
|
||||||
id: 'name',
|
|
||||||
name: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
placeholder: 'Token name',
|
|
||||||
})
|
|
||||||
|
|
||||||
const descriptionState = useInputState({
|
|
||||||
id: 'description',
|
|
||||||
name: 'description',
|
|
||||||
title: 'Description',
|
|
||||||
placeholder: 'Token description',
|
|
||||||
})
|
|
||||||
|
|
||||||
const externalUrlState = useInputState({
|
|
||||||
id: 'externalUrl',
|
|
||||||
name: 'externalUrl',
|
|
||||||
title: 'External URL',
|
|
||||||
placeholder: 'https://',
|
|
||||||
})
|
|
||||||
|
|
||||||
const youtubeUrlState = useInputState({
|
|
||||||
id: 'youtubeUrl',
|
|
||||||
name: 'youtubeUrl',
|
|
||||||
title: 'Youtube URL',
|
|
||||||
placeholder: 'https://',
|
|
||||||
})
|
|
||||||
|
|
||||||
const attributesState = useMetadataAttributesState()
|
|
||||||
|
|
||||||
let parsedMetadata: any
|
|
||||||
const parseMetadata = async (metadataFile: File) => {
|
|
||||||
console.log(metadataFile.size)
|
|
||||||
console.log(`Parsing metadataFile...`)
|
|
||||||
if (metadataFile) {
|
|
||||||
attributesState.reset()
|
|
||||||
try {
|
|
||||||
parsedMetadata = JSON.parse(await metadataFile.text())
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('Parsed metadata: ', parsedMetadata)
|
|
||||||
|
|
||||||
if (!parsedMetadata.attributes || parsedMetadata.attributes?.length === 0) {
|
|
||||||
attributesState.reset()
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: '',
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
attributesState.reset()
|
|
||||||
for (let i = 0; i < parsedMetadata.attributes?.length; i++) {
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: parsedMetadata.attributes[i].trait_type,
|
|
||||||
value: parsedMetadata.attributes[i].value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!parsedMetadata.name) {
|
|
||||||
nameState.onChange('')
|
|
||||||
} else {
|
|
||||||
nameState.onChange(parsedMetadata.name)
|
|
||||||
}
|
|
||||||
if (!parsedMetadata.description) {
|
|
||||||
descriptionState.onChange('')
|
|
||||||
} else {
|
|
||||||
descriptionState.onChange(parsedMetadata.description)
|
|
||||||
}
|
|
||||||
if (!parsedMetadata.external_url) {
|
|
||||||
externalUrlState.onChange('')
|
|
||||||
} else {
|
|
||||||
externalUrlState.onChange(parsedMetadata.external_url)
|
|
||||||
}
|
|
||||||
if (!parsedMetadata.youtube_url) {
|
|
||||||
youtubeUrlState.onChange('')
|
|
||||||
} else {
|
|
||||||
youtubeUrlState.onChange(parsedMetadata.youtube_url)
|
|
||||||
}
|
|
||||||
setMetadata(parsedMetadata)
|
|
||||||
} else {
|
|
||||||
attributesState.reset()
|
|
||||||
nameState.onChange('')
|
|
||||||
descriptionState.onChange('')
|
|
||||||
externalUrlState.onChange('')
|
|
||||||
youtubeUrlState.onChange('')
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: '',
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
setMetadata(null)
|
|
||||||
props.updateMetadataToUpload(emptyMetadataFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateUpdatedMetadata = () => {
|
|
||||||
metadata.attributes = Object.values(attributesState)[1]
|
|
||||||
metadata.attributes = metadata.attributes.filter((attribute: { trait_type: string }) => attribute.trait_type !== '')
|
|
||||||
|
|
||||||
if (metadata.attributes.length === 0) delete metadata.attributes
|
|
||||||
if (nameState.value === '') delete metadata.name
|
|
||||||
else metadata.name = nameState.value
|
|
||||||
if (descriptionState.value === '') delete metadata.description
|
|
||||||
else metadata.description = descriptionState.value.replaceAll('\\n', '\n')
|
|
||||||
if (externalUrlState.value === '') delete metadata.external_url
|
|
||||||
else metadata.external_url = externalUrlState.value
|
|
||||||
if (youtubeUrlState.value === '') delete metadata.youtube_url
|
|
||||||
else metadata.youtube_url = youtubeUrlState.value
|
|
||||||
|
|
||||||
const metadataFileBlob = new Blob([JSON.stringify(metadata)], {
|
|
||||||
type: 'application/json',
|
|
||||||
})
|
|
||||||
|
|
||||||
const editedMetadataFile = new File(
|
|
||||||
[metadataFileBlob],
|
|
||||||
props.selectedMetadataFile?.name
|
|
||||||
? props.selectedMetadataFile?.name.replaceAll('#', '')
|
|
||||||
: `${props.selectedAssetFile?.name
|
|
||||||
.substring(0, props.selectedAssetFile?.name.lastIndexOf('.'))
|
|
||||||
.replaceAll('#', '')}.json`,
|
|
||||||
{ type: 'application/json' },
|
|
||||||
)
|
|
||||||
props.updateMetadataToUpload(editedMetadataFile)
|
|
||||||
//console.log(editedMetadataFile)
|
|
||||||
//console.log(`${props.assetFile?.name.substring(0, props.assetFile?.name.lastIndexOf('.'))}.json`)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(props.selectedMetadataFile?.name)
|
|
||||||
if (props.selectedMetadataFile) {
|
|
||||||
void parseMetadata(props.selectedMetadataFile)
|
|
||||||
} else if (!props.importedMetadata) {
|
|
||||||
void parseMetadata(emptyMetadataFile)
|
|
||||||
}
|
|
||||||
}, [props.selectedMetadataFile?.name, props.importedMetadata])
|
|
||||||
|
|
||||||
const nameStateMemo = useMemo(() => nameState, [nameState.value])
|
|
||||||
const descriptionStateMemo = useMemo(() => descriptionState, [descriptionState.value])
|
|
||||||
const externalUrlStateMemo = useMemo(() => externalUrlState, [externalUrlState.value])
|
|
||||||
const youtubeUrlStateMemo = useMemo(() => youtubeUrlState, [youtubeUrlState.value])
|
|
||||||
const attributesStateMemo = useMemo(() => attributesState, [attributesState.entries])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('Update metadata')
|
|
||||||
if (metadata) {
|
|
||||||
generateUpdatedMetadata()
|
|
||||||
if (props.onChange) props.onChange(metadata)
|
|
||||||
}
|
|
||||||
console.log(metadata)
|
|
||||||
}, [
|
|
||||||
nameStateMemo.value,
|
|
||||||
descriptionStateMemo.value,
|
|
||||||
externalUrlStateMemo.value,
|
|
||||||
youtubeUrlStateMemo.value,
|
|
||||||
attributesStateMemo.entries,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.importedMetadata) {
|
|
||||||
void parseMetadata(emptyMetadataFile).then(() => {
|
|
||||||
console.log('Imported metadata: ', props.importedMetadata)
|
|
||||||
nameState.onChange(props.importedMetadata.name || '')
|
|
||||||
descriptionState.onChange(props.importedMetadata.description || '')
|
|
||||||
externalUrlState.onChange(props.importedMetadata.external_url || '')
|
|
||||||
youtubeUrlState.onChange(props.importedMetadata.youtube_url || '')
|
|
||||||
if (props.importedMetadata?.attributes && props.importedMetadata?.attributes?.length > 0) {
|
|
||||||
attributesState.reset()
|
|
||||||
props.importedMetadata?.attributes?.forEach((attribute: { trait_type: string; value: string }) => {
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: attribute.trait_type,
|
|
||||||
value: attribute.value,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
attributesState.reset()
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: '',
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [props.importedMetadata])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="grid grid-cols-2 mt-4 mr-4 ml-8 w-full max-w-6xl max-h-full no-scrollbar">
|
|
||||||
<div className="mr-4">
|
|
||||||
<div className="mb-7 text-xl font-bold underline underline-offset-4">NFT Metadata</div>
|
|
||||||
<TextInput {...nameState} />
|
|
||||||
<TextInput className="mt-2" {...descriptionState} />
|
|
||||||
<TextInput className="mt-2" {...externalUrlState} />
|
|
||||||
<TextInput className="mt-2" {...youtubeUrlState} />
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<MetadataAttributes
|
|
||||||
attributes={attributesState.entries}
|
|
||||||
onAdd={attributesState.add}
|
|
||||||
onChange={attributesState.update}
|
|
||||||
onRemove={attributesState.remove}
|
|
||||||
title="Attributes"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,14 +1,12 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
/* eslint-disable 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>
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
|
|
||||||
import Input from 'components/Input'
|
|
||||||
import useSearch from 'hooks/useSearch'
|
|
||||||
import { useMemo, useState } from 'react'
|
|
||||||
import { useDebounce } from 'utils/debounce'
|
|
||||||
|
|
||||||
import { CollectionsTable } from './CollectionsTable'
|
|
||||||
|
|
||||||
export function SelectCollection({ selectCollection }: { selectCollection: (collectionAddress: string) => void }) {
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
const [isInputFocused, setInputFocus] = useState(false)
|
|
||||||
|
|
||||||
const debouncedQuery = useDebounce<string>(search, 200)
|
|
||||||
const debouncedIsInputFocused = useDebounce<boolean>(isInputFocused, 200)
|
|
||||||
const collectionsQuery = useSearch(debouncedQuery, ['collections'], 5)
|
|
||||||
const collectionsResults = useMemo(() => {
|
|
||||||
return collectionsQuery.data?.find((searchResult) => searchResult.indexUid === 'collections')
|
|
||||||
}, [collectionsQuery.data])
|
|
||||||
|
|
||||||
const clickableCollections = useMemo(() => {
|
|
||||||
return (
|
|
||||||
collectionsResults?.hits.map((hit) => ({
|
|
||||||
contractAddress: hit.id,
|
|
||||||
name: hit.name,
|
|
||||||
media: hit.thumbnail_url || hit.image_url,
|
|
||||||
onClick: () => {
|
|
||||||
selectCollection(hit.id)
|
|
||||||
setSearch(hit.name)
|
|
||||||
},
|
|
||||||
})) ?? []
|
|
||||||
)
|
|
||||||
}, [collectionsResults, selectCollection, setSearch])
|
|
||||||
|
|
||||||
const handleInputFocus = () => {
|
|
||||||
setInputFocus(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputBlur = () => {
|
|
||||||
setInputFocus(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col p-4 space-y-4 w-3/4 h-full bg-black rounded-md border-2 border-gray-600 border-solid md:p-6">
|
|
||||||
<p className="text-base font-bold text-white text-start">Select the NFT collection to take a snapshot for</p>
|
|
||||||
<Input
|
|
||||||
className="py-2 w-full text-black dark:text-white rounded-sm md:w-72"
|
|
||||||
icon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
|
|
||||||
id="collection-search"
|
|
||||||
onBlur={handleInputBlur}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
onFocus={handleInputFocus}
|
|
||||||
placeholder="Search Collections..."
|
|
||||||
value={search}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{debouncedIsInputFocused && (
|
|
||||||
<div className="overflow-auto w-full">
|
|
||||||
<CollectionsTable collections={clickableCollections} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import type { Timezone } from 'contexts/globalSettings'
|
|
||||||
import { useGlobalSettings } from 'contexts/globalSettings'
|
|
||||||
import { useRef, useState } from 'react'
|
|
||||||
|
|
||||||
import { setTimezone } from '../contexts/globalSettings'
|
|
||||||
import { Button } from './Button'
|
|
||||||
|
|
||||||
export interface SettingsModalProps {
|
|
||||||
timezone?: Timezone
|
|
||||||
}
|
|
||||||
export const SettingsModal = (props: SettingsModalProps) => {
|
|
||||||
const globalSettings = useGlobalSettings()
|
|
||||||
const [isChecked, setIsChecked] = useState(false)
|
|
||||||
|
|
||||||
const checkBoxRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input className="modal-toggle" defaultChecked={false} id="my-modal-9" ref={checkBoxRef} type="checkbox" />
|
|
||||||
<label className="cursor-pointer modal" htmlFor="my-modal-9">
|
|
||||||
<label
|
|
||||||
className={`absolute top-[42%] bottom-5 left-[260px] max-w-[450px] max-h-[250px]
|
|
||||||
border-[1px] no-scrollbar modal-box`}
|
|
||||||
htmlFor="temp"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col justify-between h-full">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<h1 className="text-2xl font-bold underline underline-offset-2">Settings</h1>
|
|
||||||
<div className="flex justify-start w-full">
|
|
||||||
<div className="flex-row mt-2 w-full form-control">
|
|
||||||
<h1 className="mt-[5px] text-lg font-bold">Time & Date: </h1>
|
|
||||||
<label className="justify-start ml-6 cursor-pointer label">
|
|
||||||
<span className="mr-2 font-bold">Local</span>
|
|
||||||
<input
|
|
||||||
checked={globalSettings.timezone === 'Local'}
|
|
||||||
className={`${globalSettings.timezone === 'Local' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
|
||||||
onClick={() => {
|
|
||||||
setTimezone('Local' as Timezone)
|
|
||||||
window.localStorage.setItem('timezone', 'Local')
|
|
||||||
}}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="justify-start ml-4 cursor-pointer label">
|
|
||||||
<span className="mr-2 font-bold">UTC</span>
|
|
||||||
<input
|
|
||||||
checked={globalSettings.timezone === 'UTC'}
|
|
||||||
className={`${globalSettings.timezone === 'UTC' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
|
||||||
onClick={() => {
|
|
||||||
setTimezone('UTC' as Timezone)
|
|
||||||
window.localStorage.setItem('timezone', 'UTC')
|
|
||||||
}}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="w-[40%] max-h-12 bg-blue-500 hover:bg-blue-600"
|
|
||||||
isWide
|
|
||||||
onClick={() => {
|
|
||||||
setTimezone('UTC' as Timezone)
|
|
||||||
window.localStorage.setItem('timezone', 'UTC')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Use Defaults
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,379 +1,84 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
|
|
||||||
|
|
||||||
import clsx from 'clsx'
|
import 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}>
|
||||||
|
|||||||
@ -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 },
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable jsx-a11y/media-has-caption */
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
|
||||||
import { getAssetType } from 'utils/getAssetType'
|
|
||||||
|
|
||||||
export interface SingleAssetPreviewProps {
|
|
||||||
subtitle: ReactNode
|
|
||||||
relatedAsset?: File
|
|
||||||
updateMetadataFileIndex?: (index: number) => void
|
|
||||||
children?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SingleAssetPreview = (props: SingleAssetPreviewProps) => {
|
|
||||||
const { subtitle, relatedAsset, updateMetadataFileIndex, children } = props
|
|
||||||
const [htmlContents, setHtmlContents] = useState<string>('')
|
|
||||||
|
|
||||||
const videoPreview = useMemo(
|
|
||||||
() => (
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
id="video"
|
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
|
||||||
src={relatedAsset ? URL.createObjectURL(relatedAsset) : ''}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[relatedAsset],
|
|
||||||
)
|
|
||||||
|
|
||||||
const audioPreview = useMemo(
|
|
||||||
() => (
|
|
||||||
<audio
|
|
||||||
controls
|
|
||||||
id="audio"
|
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
|
||||||
src={relatedAsset ? URL.createObjectURL(relatedAsset) : ''}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[relatedAsset],
|
|
||||||
)
|
|
||||||
|
|
||||||
const documentPreview = useMemo(
|
|
||||||
() => (
|
|
||||||
<div className="flex flex-col items-center mt-4 ml-2">
|
|
||||||
<img key="document-key" alt="document_icon" className={clsx('mb-2 ml-1 w-20 h-20 thumbnail')} src="/pdf.png" />
|
|
||||||
<span className="flex self-center ">{relatedAsset?.name}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
[relatedAsset],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (getAssetType(relatedAsset?.name as string) !== 'html') return
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (typeof e.target?.result === 'string') {
|
|
||||||
setHtmlContents(e.target.result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.readAsText(new Blob([relatedAsset as File]))
|
|
||||||
}, [relatedAsset])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex p-4 pt-0 mt-11 ml-24 space-x-4 w-full">
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<label className="flex flex-col space-y-1">
|
|
||||||
<div>
|
|
||||||
{/* {subtitle && <span className="text-sm text-white/50">{subtitle}</span>} */}
|
|
||||||
{relatedAsset && (
|
|
||||||
<div
|
|
||||||
className={`flex flex-row items-center mt-2 mr-4 ${
|
|
||||||
getAssetType(relatedAsset.name) === 'document' ? '' : `border-2 border-dashed`
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getAssetType(relatedAsset.name) === 'audio' && audioPreview}
|
|
||||||
{getAssetType(relatedAsset.name) === 'video' && videoPreview}
|
|
||||||
{getAssetType(relatedAsset.name) === 'document' && documentPreview}
|
|
||||||
{getAssetType(relatedAsset.name) === 'image' && (
|
|
||||||
<img alt="preview" src={URL.createObjectURL(relatedAsset)} />
|
|
||||||
)}
|
|
||||||
{getAssetType(relatedAsset.name) === 'html' && (
|
|
||||||
<iframe allowFullScreen height="300px" srcDoc={htmlContents} title="Preview" width="100%" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4 w-2/3">{children}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -6,8 +6,6 @@ import { usePopper } from 'react-popper'
|
|||||||
export interface TooltipProps extends ComponentProps<'div'> {
|
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 }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable import/no-default-export */
|
|
||||||
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import { classNames } from 'utils/css'
|
|
||||||
|
|
||||||
export interface TrailingSelectProps {
|
|
||||||
id: string
|
|
||||||
label: string
|
|
||||||
options: string[]
|
|
||||||
value: string
|
|
||||||
onChange: (event: ChangeEvent<HTMLSelectElement>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TrailingSelect({ id, label, value, onChange, options }: TrailingSelectProps) {
|
|
||||||
const cachedClassNames = classNames(
|
|
||||||
'h-full rounded-md border-transparent bg-transparent py-0 pl-2 pr-7 text-zinc-500 dark:text-zinc-400 sm:text-sm',
|
|
||||||
'focus:border-transparent focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-primary-500 focus:ring-0 focus:ring-offset-0',
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex absolute inset-y-0 right-0 items-center">
|
|
||||||
<label className="sr-only" htmlFor={id}>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<select className={cachedClassNames} id={id} name={id} onChange={onChange} value={value}>
|
|
||||||
{/* TODO - Option values in a select are supposed to be unique, remove this comment during PR review */}
|
|
||||||
{options.map((opt) => (
|
|
||||||
<option key={opt} value={opt}>
|
|
||||||
{opt}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,63 +1,36 @@
|
|||||||
import type { Coin } from '@cosmjs/proto-signing'
|
|
||||||
import { Popover, Transition } from '@headlessui/react'
|
import { 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>
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
// Styles required for @cosmos-kit/react modal
|
|
||||||
import '@interchain-ui/react/styles'
|
|
||||||
|
|
||||||
import { GasPrice } from '@cosmjs/stargate'
|
|
||||||
import { wallets as keplrExtensionWallets } from '@cosmos-kit/keplr-extension'
|
|
||||||
import { wallets as leapExtensionWallets } from '@cosmos-kit/leap-extension'
|
|
||||||
import { ChainProvider } from '@cosmos-kit/react'
|
|
||||||
import { assets, chains } from 'chain-registry'
|
|
||||||
import { getConfig } from 'config'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import { NETWORK } from 'utils/constants'
|
|
||||||
|
|
||||||
export const WalletProvider = ({ children }: { children: ReactNode }) => {
|
|
||||||
const { gasPrice, feeToken } = getConfig(NETWORK)
|
|
||||||
return (
|
|
||||||
<ChainProvider
|
|
||||||
assetLists={assets}
|
|
||||||
chains={chains}
|
|
||||||
endpointOptions={{
|
|
||||||
endpoints: {
|
|
||||||
stargaze: {
|
|
||||||
rpc: ['https://rpc.stargaze-apis.com/'],
|
|
||||||
rest: ['https://rest.stargaze-apis.com/'],
|
|
||||||
},
|
|
||||||
stargazetestnet: {
|
|
||||||
rpc: ['https://rpc.elgafar-1.stargaze-apis.com/'],
|
|
||||||
rest: ['https://rest.elgafar-1.stargaze-apis.com/'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isLazy: true,
|
|
||||||
}}
|
|
||||||
sessionOptions={{
|
|
||||||
duration: 1000 * 60 * 60 * 12, // 12 hours
|
|
||||||
}}
|
|
||||||
signerOptions={{
|
|
||||||
signingCosmwasm: () => ({
|
|
||||||
gasPrice: GasPrice.fromString(`${gasPrice}${feeToken}`),
|
|
||||||
}),
|
|
||||||
signingStargate: () => ({
|
|
||||||
gasPrice: GasPrice.fromString(`${gasPrice}${feeToken}`),
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
wallets={[...keplrExtensionWallets, ...leapExtensionWallets]}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ChainProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
import { toUtf8 } from '@cosmjs/encoding'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { SG721_NAME_ADDRESS } from 'utils/constants'
|
|
||||||
import { csvToFlexList } from 'utils/csvToFlexList'
|
|
||||||
import { isValidAddress } from 'utils/isValidAddress'
|
|
||||||
import { isValidFlexListFile } from 'utils/isValidFlexListFile'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
export interface WhitelistFlexMember {
|
|
||||||
address: string
|
|
||||||
mint_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WhitelistFlexUploadProps {
|
|
||||||
onChange: (data: WhitelistFlexMember[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WhitelistFlexUpload = ({ onChange }: WhitelistFlexUploadProps) => {
|
|
||||||
const wallet = useWallet()
|
|
||||||
const [resolvedMemberData, setResolvedMemberData] = useState<WhitelistFlexMember[]>([])
|
|
||||||
|
|
||||||
const resolveMemberData = async (memberData: WhitelistFlexMember[]) => {
|
|
||||||
if (!memberData.length) return []
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
let i = 0
|
|
||||||
memberData.map(async (data) => {
|
|
||||||
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
|
|
||||||
await (
|
|
||||||
await wallet.getCosmWasmClient()
|
|
||||||
)
|
|
||||||
.queryContractRaw(
|
|
||||||
SG721_NAME_ADDRESS,
|
|
||||||
toUtf8(
|
|
||||||
Buffer.from(
|
|
||||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(
|
|
||||||
data.address.trim().substring(0, data.address.lastIndexOf('.stars')),
|
|
||||||
).toString('hex')}`,
|
|
||||||
'hex',
|
|
||||||
).toString(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
|
||||||
if (tokenUri && isValidAddress(tokenUri))
|
|
||||||
resolvedMemberData.push({ address: tokenUri, mint_count: Number(data.mint_count) })
|
|
||||||
else toast.error(`Resolved address is empty or invalid for the name: ${data.address}`)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
toast.error(`Error resolving address for the name: ${data.address}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
i++
|
|
||||||
if (i === memberData.length) resolve(resolvedMemberData)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return resolvedMemberData
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setResolvedMemberData([])
|
|
||||||
if (!event.target.files) return toast.error('Error opening file')
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (!event.target.files[0]?.name.endsWith('.csv')) {
|
|
||||||
toast.error('Please select a .csv file!')
|
|
||||||
return onChange([])
|
|
||||||
}
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = async (e: ProgressEvent<FileReader>) => {
|
|
||||||
try {
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
||||||
const memberData = csvToFlexList(e.target.result.toString())
|
|
||||||
console.log(memberData)
|
|
||||||
if (!isValidFlexListFile(memberData)) {
|
|
||||||
event.target.value = ''
|
|
||||||
return onChange([])
|
|
||||||
}
|
|
||||||
await resolveMemberData(memberData.filter((data) => data.address.trim().endsWith('.stars'))).finally(() => {
|
|
||||||
return onChange(
|
|
||||||
memberData
|
|
||||||
.filter((data) => data.address.startsWith('stars') && !data.address.endsWith('.stars'))
|
|
||||||
.map((data) => ({
|
|
||||||
address: data.address.trim(),
|
|
||||||
mint_count: Number(data.mint_count),
|
|
||||||
}))
|
|
||||||
.concat(resolvedMemberData),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.readAsText(event.target.files[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept=".csv"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="whitelist-flex-file"
|
|
||||||
onChange={onFileChange}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,106 +1,24 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable no-misleading-character-class */
|
|
||||||
/* eslint-disable no-control-regex */
|
|
||||||
import { toUtf8 } from '@cosmjs/encoding'
|
|
||||||
import clsx from 'clsx'
|
import 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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,639 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
// import { AirdropUpload } from 'components/AirdropUpload'
|
|
||||||
import { toUtf8 } from '@cosmjs/encoding'
|
|
||||||
import { Alert } from 'components/Alert'
|
|
||||||
import type { DispatchExecuteArgs } from 'components/badges/actions/actions'
|
|
||||||
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions'
|
|
||||||
import { ActionsCombobox } from 'components/badges/actions/Combobox'
|
|
||||||
import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks'
|
|
||||||
import { Button } from 'components/Button'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { FormGroup } from 'components/FormGroup'
|
|
||||||
import { AddressList } from 'components/forms/AddressList'
|
|
||||||
import { useAddressListState } from 'components/forms/AddressList.hooks'
|
|
||||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { MetadataAttributes } from 'components/forms/MetadataAttributes'
|
|
||||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
|
||||||
import { JsonPreview } from 'components/JsonPreview'
|
|
||||||
import { TransactionHash } from 'components/TransactionHash'
|
|
||||||
import { WhitelistUpload } from 'components/WhitelistUpload'
|
|
||||||
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
|
|
||||||
import sizeof from 'object-sizeof'
|
|
||||||
import type { FormEvent } from 'react'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { FaArrowRight } from 'react-icons/fa'
|
|
||||||
import { useMutation } from 'react-query'
|
|
||||||
import * as secp256k1 from 'secp256k1'
|
|
||||||
import { generateKeyPairs, sha256 } from 'utils/hash'
|
|
||||||
import { isValidAddress } from 'utils/isValidAddress'
|
|
||||||
import { resolveAddress } from 'utils/resolveAddress'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
|
|
||||||
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
|
||||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
|
||||||
|
|
||||||
interface BadgeActionsProps {
|
|
||||||
badgeHubContractAddress: string
|
|
||||||
badgeId: number
|
|
||||||
badgeHubMessages: BadgeHubInstance | undefined
|
|
||||||
mintRule: MintRule
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransferrableType = true | false | undefined
|
|
||||||
|
|
||||||
export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeActionsProps) => {
|
|
||||||
const wallet = useWallet()
|
|
||||||
const [lastTx, setLastTx] = useState('')
|
|
||||||
|
|
||||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
|
||||||
const [airdropAllocationArray, setAirdropAllocationArray] = useState<string[]>([])
|
|
||||||
const [badge, setBadge] = useState<Badge>()
|
|
||||||
const [transferrable, setTransferrable] = useState<TransferrableType>(undefined)
|
|
||||||
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
|
||||||
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
|
||||||
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
|
||||||
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
|
|
||||||
const [signature, setSignature] = useState<string>('')
|
|
||||||
const [ownerList, setOwnerList] = useState<string[]>([])
|
|
||||||
const [numberOfKeys, setNumberOfKeys] = useState(0)
|
|
||||||
|
|
||||||
const actionComboboxState = useActionsComboboxState()
|
|
||||||
const type = actionComboboxState.value?.id
|
|
||||||
|
|
||||||
const maxSupplyState = useNumberInputState({
|
|
||||||
id: 'max-supply',
|
|
||||||
name: 'max-supply',
|
|
||||||
title: 'Max Supply',
|
|
||||||
subtitle: 'Maximum number of badges that can be minted',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Metadata related fields
|
|
||||||
const managerState = useInputState({
|
|
||||||
id: 'manager-address',
|
|
||||||
name: 'manager',
|
|
||||||
title: 'Manager',
|
|
||||||
subtitle: 'Badge Hub Manager',
|
|
||||||
defaultValue: wallet.address,
|
|
||||||
})
|
|
||||||
|
|
||||||
const nameState = useInputState({
|
|
||||||
id: 'metadata-name',
|
|
||||||
name: 'metadata-name',
|
|
||||||
title: 'Name',
|
|
||||||
subtitle: 'Name of the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const descriptionState = useInputState({
|
|
||||||
id: 'metadata-description',
|
|
||||||
name: 'metadata-description',
|
|
||||||
title: 'Description',
|
|
||||||
subtitle: 'Description of the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const imageState = useInputState({
|
|
||||||
id: 'metadata-image',
|
|
||||||
name: 'metadata-image',
|
|
||||||
title: 'Image',
|
|
||||||
subtitle: 'Badge Image URL',
|
|
||||||
})
|
|
||||||
|
|
||||||
const imageDataState = useInputState({
|
|
||||||
id: 'metadata-image-data',
|
|
||||||
name: 'metadata-image-data',
|
|
||||||
title: 'Image Data',
|
|
||||||
subtitle: 'Raw SVG image data',
|
|
||||||
})
|
|
||||||
|
|
||||||
const externalUrlState = useInputState({
|
|
||||||
id: 'metadata-external-url',
|
|
||||||
name: 'metadata-external-url',
|
|
||||||
title: 'External URL',
|
|
||||||
subtitle: 'External URL for the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const attributesState = useMetadataAttributesState()
|
|
||||||
|
|
||||||
const backgroundColorState = useInputState({
|
|
||||||
id: 'metadata-background-color',
|
|
||||||
name: 'metadata-background-color',
|
|
||||||
title: 'Background Color',
|
|
||||||
subtitle: 'Background color of the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const animationUrlState = useInputState({
|
|
||||||
id: 'metadata-animation-url',
|
|
||||||
name: 'metadata-animation-url',
|
|
||||||
title: 'Animation URL',
|
|
||||||
subtitle: 'Animation URL for the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const youtubeUrlState = useInputState({
|
|
||||||
id: 'metadata-youtube-url',
|
|
||||||
name: 'metadata-youtube-url',
|
|
||||||
title: 'YouTube URL',
|
|
||||||
subtitle: 'YouTube URL for the badge',
|
|
||||||
})
|
|
||||||
// Rules related fields
|
|
||||||
const keyState = useInputState({
|
|
||||||
id: 'key',
|
|
||||||
name: 'key',
|
|
||||||
title: 'Key',
|
|
||||||
subtitle: 'The key generated for the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const ownerState = useInputState({
|
|
||||||
id: 'owner-address',
|
|
||||||
name: 'owner',
|
|
||||||
title: 'Owner',
|
|
||||||
subtitle: 'The owner of the badge',
|
|
||||||
defaultValue: wallet.address,
|
|
||||||
})
|
|
||||||
|
|
||||||
const ownerListState = useAddressListState()
|
|
||||||
|
|
||||||
const pubKeyState = useInputState({
|
|
||||||
id: 'pubKey',
|
|
||||||
name: 'pubKey',
|
|
||||||
title: 'Public Key',
|
|
||||||
subtitle:
|
|
||||||
type === 'mint_by_keys'
|
|
||||||
? 'The whitelisted public key authorized to mint a badge'
|
|
||||||
: 'The public key to check whether it can be used to mint a badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const privateKeyState = useInputState({
|
|
||||||
id: 'privateKey',
|
|
||||||
name: 'privateKey',
|
|
||||||
title: 'Private Key',
|
|
||||||
subtitle:
|
|
||||||
type === 'mint_by_keys'
|
|
||||||
? 'The corresponding private key for the whitelisted public key'
|
|
||||||
: 'The private key that was generated during badge creation',
|
|
||||||
})
|
|
||||||
|
|
||||||
const nftState = useInputState({
|
|
||||||
id: 'nft-address',
|
|
||||||
name: 'nft-address',
|
|
||||||
title: 'NFT Contract Address',
|
|
||||||
subtitle: 'The NFT Contract Address for the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const limitState = useNumberInputState({
|
|
||||||
id: 'limit',
|
|
||||||
name: 'limit',
|
|
||||||
title: 'Limit',
|
|
||||||
subtitle: 'Number of keys/owners to execute the action for (0 for all)',
|
|
||||||
})
|
|
||||||
|
|
||||||
const showMetadataField = isEitherType(type, ['edit_badge'])
|
|
||||||
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'mint_by_minter'])
|
|
||||||
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key'])
|
|
||||||
const showAirdropFileField = isEitherType(type, ['airdrop_by_key'])
|
|
||||||
const showOwnerList = isEitherType(type, ['mint_by_minter'])
|
|
||||||
const showPubKeyField = isEitherType(type, ['mint_by_keys'])
|
|
||||||
const showLimitState = isEitherType(type, ['purge_keys', 'purge_owners'])
|
|
||||||
|
|
||||||
const payload: DispatchExecuteArgs = {
|
|
||||||
badge: {
|
|
||||||
manager: badge?.manager || managerState.value,
|
|
||||||
metadata: {
|
|
||||||
name: nameState.value || undefined,
|
|
||||||
description: descriptionState.value || undefined,
|
|
||||||
image: imageState.value || undefined,
|
|
||||||
image_data: imageDataState.value || undefined,
|
|
||||||
external_url: externalUrlState.value || undefined,
|
|
||||||
attributes:
|
|
||||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
|
||||||
? attributesState.values
|
|
||||||
.map((attr) => ({
|
|
||||||
trait_type: attr.trait_type,
|
|
||||||
value: attr.value,
|
|
||||||
}))
|
|
||||||
.filter((attr) => attr.trait_type && attr.value)
|
|
||||||
: undefined,
|
|
||||||
background_color: backgroundColorState.value || undefined,
|
|
||||||
animation_url: animationUrlState.value || undefined,
|
|
||||||
youtube_url: youtubeUrlState.value || undefined,
|
|
||||||
},
|
|
||||||
transferrable: transferrable === true,
|
|
||||||
rule: {
|
|
||||||
by_key: keyState.value,
|
|
||||||
},
|
|
||||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
|
||||||
max_supply: maxSupplyState.value || undefined,
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
name: nameState.value || undefined,
|
|
||||||
description: descriptionState.value || undefined,
|
|
||||||
image: imageState.value || undefined,
|
|
||||||
image_data: imageDataState.value || undefined,
|
|
||||||
external_url: externalUrlState.value || undefined,
|
|
||||||
attributes:
|
|
||||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
|
||||||
? attributesState.values
|
|
||||||
.map((attr) => ({
|
|
||||||
trait_type: attr.trait_type,
|
|
||||||
value: attr.value,
|
|
||||||
}))
|
|
||||||
.filter((attr) => attr.trait_type && attr.value)
|
|
||||||
: undefined,
|
|
||||||
background_color: backgroundColorState.value || undefined,
|
|
||||||
animation_url: animationUrlState.value || undefined,
|
|
||||||
youtube_url: youtubeUrlState.value || undefined,
|
|
||||||
},
|
|
||||||
id: badgeId,
|
|
||||||
editFee,
|
|
||||||
owner: resolvedOwnerAddress,
|
|
||||||
pubkey: pubKeyState.value,
|
|
||||||
signature,
|
|
||||||
keys: keyPairs.map((keyPair) => keyPair.publicKey),
|
|
||||||
limit: limitState.value || undefined,
|
|
||||||
owners: [
|
|
||||||
...new Set(
|
|
||||||
ownerListState.values
|
|
||||||
.map((a) => a.address.trim())
|
|
||||||
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
|
|
||||||
.concat(ownerList),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
recipients: airdropAllocationArray,
|
|
||||||
privateKey: privateKeyState.value,
|
|
||||||
nft: nftState.value,
|
|
||||||
badgeHubMessages,
|
|
||||||
badgeHubContract: badgeHubContractAddress,
|
|
||||||
txSigner: wallet.address || '',
|
|
||||||
type,
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveOwnerAddress = async () => {
|
|
||||||
await resolveAddress(ownerState.value.trim(), wallet).then((resolvedAddress) => {
|
|
||||||
setResolvedOwnerAddress(resolvedAddress)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
|
||||||
void resolveOwnerAddress()
|
|
||||||
}, [ownerState.value])
|
|
||||||
|
|
||||||
const resolveManagerAddress = async () => {
|
|
||||||
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
|
|
||||||
setBadge({
|
|
||||||
manager: resolvedAddress,
|
|
||||||
metadata: {
|
|
||||||
name: nameState.value || undefined,
|
|
||||||
description: descriptionState.value || undefined,
|
|
||||||
image: imageState.value || undefined,
|
|
||||||
image_data: imageDataState.value || undefined,
|
|
||||||
external_url: externalUrlState.value || undefined,
|
|
||||||
attributes:
|
|
||||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
|
||||||
? attributesState.values
|
|
||||||
.map((attr) => ({
|
|
||||||
trait_type: attr.trait_type,
|
|
||||||
value: attr.value,
|
|
||||||
}))
|
|
||||||
.filter((attr) => attr.trait_type && attr.value)
|
|
||||||
: undefined,
|
|
||||||
background_color: backgroundColorState.value || undefined,
|
|
||||||
animation_url: animationUrlState.value || undefined,
|
|
||||||
youtube_url: youtubeUrlState.value || undefined,
|
|
||||||
},
|
|
||||||
transferrable: transferrable === true,
|
|
||||||
rule: {
|
|
||||||
by_key: keyState.value,
|
|
||||||
},
|
|
||||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
|
||||||
max_supply: maxSupplyState.value || undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void resolveManagerAddress()
|
|
||||||
}, [managerState.value])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setBadge({
|
|
||||||
manager: managerState.value,
|
|
||||||
metadata: {
|
|
||||||
name: nameState.value || undefined,
|
|
||||||
description: descriptionState.value || undefined,
|
|
||||||
image: imageState.value || undefined,
|
|
||||||
image_data: imageDataState.value || undefined,
|
|
||||||
external_url: externalUrlState.value || undefined,
|
|
||||||
attributes:
|
|
||||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
|
||||||
? attributesState.values
|
|
||||||
.map((attr) => ({
|
|
||||||
trait_type: attr.trait_type,
|
|
||||||
value: attr.value,
|
|
||||||
}))
|
|
||||||
.filter((attr) => attr.trait_type && attr.value)
|
|
||||||
: undefined,
|
|
||||||
background_color: backgroundColorState.value || undefined,
|
|
||||||
animation_url: animationUrlState.value || undefined,
|
|
||||||
youtube_url: youtubeUrlState.value || undefined,
|
|
||||||
},
|
|
||||||
transferrable: transferrable === true,
|
|
||||||
rule: {
|
|
||||||
by_key: keyState.value,
|
|
||||||
},
|
|
||||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
|
||||||
max_supply: maxSupplyState.value || undefined,
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
nameState.value,
|
|
||||||
descriptionState.value,
|
|
||||||
imageState.value,
|
|
||||||
imageDataState.value,
|
|
||||||
externalUrlState.value,
|
|
||||||
attributesState.values,
|
|
||||||
backgroundColorState.value,
|
|
||||||
animationUrlState.value,
|
|
||||||
youtubeUrlState.value,
|
|
||||||
transferrable,
|
|
||||||
keyState.value,
|
|
||||||
timestamp,
|
|
||||||
maxSupplyState.value,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (attributesState.values.length === 0)
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: '',
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void dispatchEditBadgeMessage().catch((err) => {
|
|
||||||
toast.error(String(err), { style: { maxWidth: 'none' } })
|
|
||||||
})
|
|
||||||
}, [triggerDispatch])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
|
|
||||||
handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value)
|
|
||||||
}, [privateKeyState.value, resolvedOwnerAddress])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (numberOfKeys > 0) {
|
|
||||||
setKeyPairs(generateKeyPairs(numberOfKeys))
|
|
||||||
}
|
|
||||||
}, [numberOfKeys])
|
|
||||||
|
|
||||||
const handleDownloadKeys = () => {
|
|
||||||
const element = document.createElement('a')
|
|
||||||
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
|
|
||||||
element.href = URL.createObjectURL(file)
|
|
||||||
element.download = `badge-${badgeId.toString()}-keys.json`
|
|
||||||
document.body.appendChild(element)
|
|
||||||
element.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isLoading, mutate } = useMutation(
|
|
||||||
async (event: FormEvent) => {
|
|
||||||
if (!wallet.isWalletConnected) {
|
|
||||||
throw new Error('Please connect your wallet.')
|
|
||||||
}
|
|
||||||
event.preventDefault()
|
|
||||||
if (!type) {
|
|
||||||
throw new Error('Please select an action.')
|
|
||||||
}
|
|
||||||
if (badgeHubContractAddress === '') {
|
|
||||||
throw new Error('Please enter the Badge Hub contract addresses.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'mint_by_key' && privateKeyState.value.length !== 64) {
|
|
||||||
throw new Error('Please enter a valid private key.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'edit_badge') {
|
|
||||||
const feeRateRaw = await (
|
|
||||||
await wallet.getCosmWasmClient()
|
|
||||||
).queryContractRaw(
|
|
||||||
badgeHubContractAddress,
|
|
||||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
|
||||||
)
|
|
||||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
|
||||||
|
|
||||||
await toast
|
|
||||||
.promise(
|
|
||||||
(
|
|
||||||
await wallet.getCosmWasmClient()
|
|
||||||
).queryContractSmart(badgeHubContractAddress, {
|
|
||||||
badge: { id: badgeId },
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
error: `Edit Fee calculation failed!`,
|
|
||||||
loading: 'Calculating Edit Fee...',
|
|
||||||
success: (currentBadge) => {
|
|
||||||
console.log('Current badge: ', currentBadge)
|
|
||||||
return `Current metadata is ${
|
|
||||||
Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes))
|
|
||||||
} bytes in size.`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then((currentBadge) => {
|
|
||||||
// TODO - Go over the calculation
|
|
||||||
const currentBadgeMetadataSize =
|
|
||||||
Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes) * 2)
|
|
||||||
console.log('Current badge metadata size: ', currentBadgeMetadataSize)
|
|
||||||
const newBadgeMetadataSize =
|
|
||||||
Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) * 2
|
|
||||||
console.log('New badge metadata size: ', newBadgeMetadataSize)
|
|
||||||
if (newBadgeMetadataSize > currentBadgeMetadataSize) {
|
|
||||||
const calculatedFee = ((newBadgeMetadataSize - currentBadgeMetadataSize) * Number(feeRate.metadata)) / 2
|
|
||||||
setEditFee(calculatedFee)
|
|
||||||
setTriggerDispatch(!triggerDispatch)
|
|
||||||
} else {
|
|
||||||
setEditFee(undefined)
|
|
||||||
setTriggerDispatch(!triggerDispatch)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const txHash = await toast.promise(dispatchExecute(payload), {
|
|
||||||
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
|
|
||||||
loading: 'Executing message...',
|
|
||||||
success: (tx) => `Transaction ${tx} success!`,
|
|
||||||
})
|
|
||||||
if (txHash) {
|
|
||||||
setLastTx(txHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(String(error), { style: { maxWidth: 'none' } })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const dispatchEditBadgeMessage = async () => {
|
|
||||||
if (type) {
|
|
||||||
const txHash = await toast.promise(dispatchExecute(payload), {
|
|
||||||
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
|
|
||||||
loading: 'Executing message...',
|
|
||||||
success: (tx) => `Transaction ${tx} success!`,
|
|
||||||
})
|
|
||||||
if (txHash) {
|
|
||||||
setLastTx(txHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const airdropFileOnChange = (data: string[]) => {
|
|
||||||
console.log(data)
|
|
||||||
setAirdropAllocationArray(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGenerateSignature = (id: number, owner: string, privateKey: string) => {
|
|
||||||
try {
|
|
||||||
const message = `claim badge ${id} for user ${owner}`
|
|
||||||
|
|
||||||
const privKey = Buffer.from(privateKey, 'hex')
|
|
||||||
// const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true))
|
|
||||||
const msgBytes = Buffer.from(message, 'utf8')
|
|
||||||
const msgHashBytes = sha256(msgBytes)
|
|
||||||
const signedMessage = secp256k1.ecdsaSign(msgHashBytes, privKey)
|
|
||||||
setSignature(Buffer.from(signedMessage.signature).toString('hex'))
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
toast.error('Error generating signature.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form>
|
|
||||||
<div className="grid grid-cols-2 mt-4">
|
|
||||||
<div className="mr-2">
|
|
||||||
<ActionsCombobox mintRule={mintRule} {...actionComboboxState} />
|
|
||||||
{showMetadataField && (
|
|
||||||
<div className="p-4 mt-2 rounded-md border-2 border-gray-800">
|
|
||||||
<span className="text-gray-400">Metadata</span>
|
|
||||||
<TextInput className="mt-2" {...nameState} />
|
|
||||||
<TextInput className="mt-2" {...descriptionState} />
|
|
||||||
<TextInput className="mt-2" {...imageState} />
|
|
||||||
<TextInput className="mt-2" {...imageDataState} />
|
|
||||||
<TextInput className="mt-2" {...externalUrlState} />
|
|
||||||
<div className="mt-2">
|
|
||||||
<MetadataAttributes
|
|
||||||
attributes={attributesState.entries}
|
|
||||||
onAdd={attributesState.add}
|
|
||||||
onChange={attributesState.update}
|
|
||||||
onRemove={attributesState.remove}
|
|
||||||
title="Traits"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TextInput className="mt-2" {...backgroundColorState} />
|
|
||||||
<TextInput className="mt-2" {...animationUrlState} />
|
|
||||||
<TextInput className="mt-2" {...youtubeUrlState} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showOwnerField && (
|
|
||||||
<AddressInput
|
|
||||||
className="mt-2"
|
|
||||||
{...ownerState}
|
|
||||||
subtitle="The address that the badge will be minted to"
|
|
||||||
title="Owner"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showPubKeyField && <TextInput className="mt-2" {...pubKeyState} />}
|
|
||||||
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
|
|
||||||
{showLimitState && <NumberInput className="mt-2" {...limitState} />}
|
|
||||||
<Conditional test={isEitherType(type, ['purge_owners', 'purge_keys'])}>
|
|
||||||
<Alert className="mt-4" type="info">
|
|
||||||
This action is only available if the badge with the specified id is either minted out or expired.
|
|
||||||
</Alert>
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
<Conditional test={type === 'add_keys'}>
|
|
||||||
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
|
|
||||||
<div className="grid grid-cols-2 gap-24">
|
|
||||||
<div className="flex flex-col ml-4">
|
|
||||||
<span className="font-bold">Number of Keys</span>
|
|
||||||
<span className="text-sm text-white/80">
|
|
||||||
The number of public keys to be whitelisted for minting badges
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
|
|
||||||
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
|
|
||||||
required
|
|
||||||
type="number"
|
|
||||||
value={numberOfKeys}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
|
|
||||||
<Alert type="info">
|
|
||||||
<div className="pt-2">
|
|
||||||
<span className="mt-2">
|
|
||||||
Make sure to download the whitelisted public keys together with their private key counterparts.
|
|
||||||
</span>
|
|
||||||
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
|
|
||||||
Download Key Pairs
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
<Conditional test={showOwnerList}>
|
|
||||||
<div className="mt-4">
|
|
||||||
<AddressList
|
|
||||||
entries={ownerListState.entries}
|
|
||||||
isRequired
|
|
||||||
onAdd={ownerListState.add}
|
|
||||||
onChange={ownerListState.update}
|
|
||||||
onRemove={ownerListState.remove}
|
|
||||||
subtitle="Enter the owner addresses"
|
|
||||||
title="Addresses"
|
|
||||||
/>
|
|
||||||
<Alert className="mt-8" type="info">
|
|
||||||
You may optionally choose a text file of additional owner addresses.
|
|
||||||
</Alert>
|
|
||||||
<WhitelistUpload onChange={setOwnerList} />
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
{showAirdropFileField && (
|
|
||||||
<FormGroup
|
|
||||||
subtitle="TXT file that contains the addresses to airdrop a badge for"
|
|
||||||
title="Badge Airdrop List File"
|
|
||||||
>
|
|
||||||
<BadgeAirdropListUpload onChange={airdropFileOnChange} />
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="-mt-6">
|
|
||||||
<div className="relative mb-2">
|
|
||||||
<Button
|
|
||||||
className="absolute top-0 right-0"
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={mutate}
|
|
||||||
rightIcon={<FaArrowRight />}
|
|
||||||
>
|
|
||||||
Execute
|
|
||||||
</Button>
|
|
||||||
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
|
|
||||||
<TransactionHash hash={lastTx} />
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<FormControl subtitle="View current message to be sent" title="Payload Preview">
|
|
||||||
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import type { ActionListItem } from './actions'
|
|
||||||
|
|
||||||
export const useActionsComboboxState = () => {
|
|
||||||
const [value, setValue] = useState<ActionListItem | null>(null)
|
|
||||||
return { value, onChange: (item: ActionListItem) => setValue(item) }
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
import { Combobox, Transition } from '@headlessui/react'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { matchSorter } from 'match-sorter'
|
|
||||||
import { Fragment, useEffect, useState } from 'react'
|
|
||||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
|
||||||
|
|
||||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
|
||||||
import type { ActionListItem } from './actions'
|
|
||||||
import { BY_KEY_ACTION_LIST, BY_KEYS_ACTION_LIST, BY_MINTER_ACTION_LIST } from './actions'
|
|
||||||
|
|
||||||
export interface ActionsComboboxProps {
|
|
||||||
value: ActionListItem | null
|
|
||||||
onChange: (item: ActionListItem) => void
|
|
||||||
mintRule?: MintRule
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ActionsCombobox = ({ value, onChange, mintRule }: ActionsComboboxProps) => {
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
const [ACTION_LIST, SET_ACTION_LIST] = useState<ActionListItem[]>(BY_KEY_ACTION_LIST)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mintRule === 'by_keys') {
|
|
||||||
SET_ACTION_LIST(BY_KEYS_ACTION_LIST)
|
|
||||||
} else if (mintRule === 'by_minter') {
|
|
||||||
SET_ACTION_LIST(BY_MINTER_ACTION_LIST)
|
|
||||||
} else {
|
|
||||||
SET_ACTION_LIST(BY_KEY_ACTION_LIST)
|
|
||||||
}
|
|
||||||
}, [mintRule])
|
|
||||||
|
|
||||||
const filtered =
|
|
||||||
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
as={FormControl}
|
|
||||||
htmlId="action"
|
|
||||||
labelAs={Combobox.Label}
|
|
||||||
onChange={onChange}
|
|
||||||
subtitle="Badge actions"
|
|
||||||
title=""
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Combobox.Input
|
|
||||||
className={clsx(
|
|
||||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
|
||||||
'placeholder:text-white/50',
|
|
||||||
'focus:ring focus:ring-plumbus-20',
|
|
||||||
)}
|
|
||||||
displayValue={(val?: ActionListItem) => val?.name ?? ''}
|
|
||||||
id="message-type"
|
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
|
||||||
placeholder="Select action"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox.Button
|
|
||||||
className={clsx(
|
|
||||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
|
||||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
|
||||||
</Combobox.Button>
|
|
||||||
|
|
||||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
|
||||||
<Combobox.Options
|
|
||||||
className={clsx(
|
|
||||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
|
||||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
|
||||||
'divide-y divide-stone-500/50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filtered.length < 1 && (
|
|
||||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
|
||||||
Action not found
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filtered.map((entry) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={entry.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
|
||||||
}
|
|
||||||
value={entry}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{entry.name}</span>
|
|
||||||
<span className="max-w-md text-sm">{entry.description}</span>
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{value && (
|
|
||||||
<div className="flex space-x-2 text-white/50">
|
|
||||||
<div className="mt-1">
|
|
||||||
<FaInfoCircle className="w-3 h-3" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm">{value.description}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,206 +0,0 @@
|
|||||||
import type { Badge, BadgeHubInstance, Metadata } from 'contracts/badgeHub'
|
|
||||||
import { useBadgeHubContract } from 'contracts/badgeHub'
|
|
||||||
|
|
||||||
export type ActionType = typeof ACTION_TYPES[number]
|
|
||||||
|
|
||||||
export const ACTION_TYPES = [
|
|
||||||
'create_badge',
|
|
||||||
'edit_badge',
|
|
||||||
'add_keys',
|
|
||||||
'purge_keys',
|
|
||||||
'purge_owners',
|
|
||||||
'mint_by_minter',
|
|
||||||
'mint_by_key',
|
|
||||||
'airdrop_by_key',
|
|
||||||
'mint_by_keys',
|
|
||||||
'set_nft',
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export interface ActionListItem {
|
|
||||||
id: ActionType
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BY_KEY_ACTION_LIST: ActionListItem[] = [
|
|
||||||
{
|
|
||||||
id: 'edit_badge',
|
|
||||||
name: 'Edit Badge',
|
|
||||||
description: `Edit badge metadata for the badge with the specified ID`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mint_by_key',
|
|
||||||
name: 'Mint by Key',
|
|
||||||
description: `Mint a badge to a specified address`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'airdrop_by_key',
|
|
||||||
name: 'Airdrop by Key',
|
|
||||||
description: `Airdrop badges to a list of specified addresses`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'purge_owners',
|
|
||||||
name: 'Purge Owners',
|
|
||||||
description: `Purge owners from the badge with the specified ID`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
|
||||||
{
|
|
||||||
id: 'edit_badge',
|
|
||||||
name: 'Edit Badge',
|
|
||||||
description: `Edit badge metadata for the badge with the specified ID`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mint_by_keys',
|
|
||||||
name: 'Mint by Keys',
|
|
||||||
description: `Mint a new badge with a whitelisted private key`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'add_keys',
|
|
||||||
name: 'Add Keys',
|
|
||||||
description: `Add keys to the badge with the specified ID`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'purge_keys',
|
|
||||||
name: 'Purge Keys',
|
|
||||||
description: `Purge keys from the badge with the specified ID`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'purge_owners',
|
|
||||||
name: 'Purge Owners',
|
|
||||||
description: `Purge owners from the badge with the specified ID`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
|
|
||||||
{
|
|
||||||
id: 'edit_badge',
|
|
||||||
name: 'Edit Badge',
|
|
||||||
description: `Edit badge metadata for the badge with the specified ID`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mint_by_minter',
|
|
||||||
name: 'Mint by Minter',
|
|
||||||
description: `Mint a new badge to specified owner addresses`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'purge_owners',
|
|
||||||
name: 'Purge Owners',
|
|
||||||
description: `Purge owners from the badge with the specified ID`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export interface DispatchExecuteProps {
|
|
||||||
type: ActionType
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type Select<T extends ActionType> = T
|
|
||||||
|
|
||||||
/** @see {@link BadgeHubInstance}*/
|
|
||||||
export type DispatchExecuteArgs = {
|
|
||||||
badgeHubContract: string
|
|
||||||
badgeHubMessages?: BadgeHubInstance
|
|
||||||
txSigner: string
|
|
||||||
} & (
|
|
||||||
| { type: undefined }
|
|
||||||
| { type: Select<'create_badge'>; badge: Badge }
|
|
||||||
| { type: Select<'edit_badge'>; id: number; metadata: Metadata; editFee?: number }
|
|
||||||
| { type: Select<'add_keys'>; id: number; keys: string[] }
|
|
||||||
| { type: Select<'purge_keys'>; id: number; limit?: number }
|
|
||||||
| { type: Select<'purge_owners'>; id: number; limit?: number }
|
|
||||||
| { type: Select<'mint_by_minter'>; id: number; owners: string[] }
|
|
||||||
| { type: Select<'mint_by_key'>; id: number; owner: string; signature: string }
|
|
||||||
| { type: Select<'airdrop_by_key'>; id: number; recipients: string[]; privateKey: string }
|
|
||||||
| { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string }
|
|
||||||
| { type: Select<'set_nft'>; nft: string }
|
|
||||||
)
|
|
||||||
|
|
||||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
|
||||||
const { badgeHubMessages, txSigner } = args
|
|
||||||
if (!badgeHubMessages) {
|
|
||||||
throw new Error('Cannot execute actions')
|
|
||||||
}
|
|
||||||
switch (args.type) {
|
|
||||||
case 'create_badge': {
|
|
||||||
return badgeHubMessages.createBadge(txSigner, args.badge)
|
|
||||||
}
|
|
||||||
case 'edit_badge': {
|
|
||||||
return badgeHubMessages.editBadge(txSigner, args.id, args.metadata, args.editFee)
|
|
||||||
}
|
|
||||||
case 'add_keys': {
|
|
||||||
return badgeHubMessages.addKeys(txSigner, args.id, args.keys)
|
|
||||||
}
|
|
||||||
case 'purge_keys': {
|
|
||||||
return badgeHubMessages.purgeKeys(txSigner, args.id, args.limit)
|
|
||||||
}
|
|
||||||
case 'purge_owners': {
|
|
||||||
return badgeHubMessages.purgeOwners(txSigner, args.id, args.limit)
|
|
||||||
}
|
|
||||||
case 'mint_by_minter': {
|
|
||||||
return badgeHubMessages.mintByMinter(txSigner, args.id, args.owners)
|
|
||||||
}
|
|
||||||
case 'mint_by_key': {
|
|
||||||
return badgeHubMessages.mintByKey(txSigner, args.id, args.owner, args.signature)
|
|
||||||
}
|
|
||||||
case 'airdrop_by_key': {
|
|
||||||
return badgeHubMessages.airdropByKey(txSigner, args.id, args.recipients, args.privateKey)
|
|
||||||
}
|
|
||||||
case 'mint_by_keys': {
|
|
||||||
return badgeHubMessages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature)
|
|
||||||
}
|
|
||||||
case 'set_nft': {
|
|
||||||
return badgeHubMessages.setNft(txSigner, args.nft)
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error('Unknown action')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const { messages: badgeHubMessages } = useBadgeHubContract()
|
|
||||||
|
|
||||||
const { badgeHubContract } = args
|
|
||||||
switch (args.type) {
|
|
||||||
case 'create_badge': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.createBadge(args.badge)
|
|
||||||
}
|
|
||||||
case 'edit_badge': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.editBadge(args.id, args.metadata)
|
|
||||||
}
|
|
||||||
case 'add_keys': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.addKeys(args.id, args.keys)
|
|
||||||
}
|
|
||||||
case 'purge_keys': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.purgeKeys(args.id, args.limit)
|
|
||||||
}
|
|
||||||
case 'purge_owners': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.purgeOwners(args.id, args.limit)
|
|
||||||
}
|
|
||||||
case 'mint_by_minter': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.mintByMinter(args.id, args.owners)
|
|
||||||
}
|
|
||||||
case 'mint_by_key': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.mintByKey(args.id, args.owner, args.signature)
|
|
||||||
}
|
|
||||||
case 'airdrop_by_key': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.airdropByKey(args.id, args.recipients, args.privateKey)
|
|
||||||
}
|
|
||||||
case 'mint_by_keys': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature)
|
|
||||||
}
|
|
||||||
case 'set_nft': {
|
|
||||||
return badgeHubMessages(badgeHubContract)?.setNft(args.nft)
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isEitherType = <T extends ActionType>(type: unknown, arr: T[]): type is T => {
|
|
||||||
return arr.some((val) => type === val)
|
|
||||||
}
|
|
||||||
@ -1,382 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
|
|
||||||
import { toUtf8 } from '@cosmjs/encoding'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
|
||||||
import { InputDateTime } from 'components/InputDateTime'
|
|
||||||
import { useGlobalSettings } from 'contexts/globalSettings'
|
|
||||||
import type { Trait } from 'contracts/badgeHub'
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { BADGE_HUB_ADDRESS } from 'utils/constants'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
|
||||||
import { MetadataAttributes } from '../../forms/MetadataAttributes'
|
|
||||||
import { Tooltip } from '../../Tooltip'
|
|
||||||
import type { MintRule, UploadMethod } from './ImageUploadDetails'
|
|
||||||
|
|
||||||
interface BadgeDetailsProps {
|
|
||||||
onChange: (data: BadgeDetailsDataProps) => void
|
|
||||||
uploadMethod: UploadMethod | undefined
|
|
||||||
mintRule: MintRule
|
|
||||||
metadataSize: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BadgeDetailsDataProps {
|
|
||||||
manager: string
|
|
||||||
name?: string
|
|
||||||
description?: string
|
|
||||||
attributes?: Trait[]
|
|
||||||
expiry?: number
|
|
||||||
transferrable: boolean
|
|
||||||
max_supply?: number
|
|
||||||
image_data?: string
|
|
||||||
external_url?: string
|
|
||||||
background_color?: string
|
|
||||||
animation_url?: string
|
|
||||||
youtube_url?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BadgeDetails = ({ metadataSize, onChange, uploadMethod }: BadgeDetailsProps) => {
|
|
||||||
const { address = '', isWalletConnected, getCosmWasmClient } = useWallet()
|
|
||||||
const { timezone } = useGlobalSettings()
|
|
||||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
|
||||||
const [transferrable, setTransferrable] = useState<boolean>(false)
|
|
||||||
const [metadataFile, setMetadataFile] = useState<File>()
|
|
||||||
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
|
|
||||||
|
|
||||||
const metadataFileRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
|
|
||||||
const managerState = useInputState({
|
|
||||||
id: 'manager-address',
|
|
||||||
name: 'manager',
|
|
||||||
title: 'Manager',
|
|
||||||
subtitle: 'Badge Hub Manager',
|
|
||||||
defaultValue: address,
|
|
||||||
})
|
|
||||||
|
|
||||||
const nameState = useInputState({
|
|
||||||
id: 'name',
|
|
||||||
name: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
placeholder: 'My Awesome Collection',
|
|
||||||
})
|
|
||||||
|
|
||||||
const descriptionState = useInputState({
|
|
||||||
id: 'description',
|
|
||||||
name: 'description',
|
|
||||||
title: 'Description',
|
|
||||||
placeholder: 'My Awesome Collection Description',
|
|
||||||
})
|
|
||||||
|
|
||||||
const imageDataState = useInputState({
|
|
||||||
id: 'metadata-image-data',
|
|
||||||
name: 'metadata-image-data',
|
|
||||||
title: 'Image Data',
|
|
||||||
subtitle: 'Raw SVG image data',
|
|
||||||
})
|
|
||||||
|
|
||||||
const externalUrlState = useInputState({
|
|
||||||
id: 'metadata-external-url',
|
|
||||||
name: 'metadata-external-url',
|
|
||||||
title: 'External URL',
|
|
||||||
subtitle: 'External URL for the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const attributesState = useMetadataAttributesState()
|
|
||||||
|
|
||||||
const maxSupplyState = useNumberInputState({
|
|
||||||
id: 'max-supply',
|
|
||||||
name: 'max-supply',
|
|
||||||
title: 'Max Supply',
|
|
||||||
subtitle: 'Maximum number of badges that can be minted',
|
|
||||||
})
|
|
||||||
|
|
||||||
const backgroundColorState = useInputState({
|
|
||||||
id: 'metadata-background-color',
|
|
||||||
name: 'metadata-background-color',
|
|
||||||
title: 'Background Color',
|
|
||||||
subtitle: 'Background color of the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const animationUrlState = useInputState({
|
|
||||||
id: 'metadata-animation-url',
|
|
||||||
name: 'metadata-animation-url',
|
|
||||||
title: 'Animation URL',
|
|
||||||
subtitle: 'Animation URL for the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const youtubeUrlState = useInputState({
|
|
||||||
id: 'metadata-youtube-url',
|
|
||||||
name: 'metadata-youtube-url',
|
|
||||||
title: 'YouTube URL',
|
|
||||||
subtitle: 'YouTube URL for the badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const parseMetadata = async () => {
|
|
||||||
try {
|
|
||||||
let parsedMetadata: any
|
|
||||||
if (metadataFile) {
|
|
||||||
attributesState.reset()
|
|
||||||
parsedMetadata = JSON.parse(await metadataFile.text())
|
|
||||||
|
|
||||||
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: '',
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: parsedMetadata.attributes[i].trait_type,
|
|
||||||
value: parsedMetadata.attributes[i].value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
|
|
||||||
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
|
|
||||||
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
|
|
||||||
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
|
|
||||||
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
|
|
||||||
backgroundColorState.onChange(parsedMetadata.background_color ? parsedMetadata.background_color : '')
|
|
||||||
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
|
|
||||||
} else {
|
|
||||||
attributesState.reset()
|
|
||||||
nameState.onChange('')
|
|
||||||
descriptionState.onChange('')
|
|
||||||
externalUrlState.onChange('')
|
|
||||||
youtubeUrlState.onChange('')
|
|
||||||
animationUrlState.onChange('')
|
|
||||||
backgroundColorState.onChange('')
|
|
||||||
imageDataState.onChange('')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Error parsing metadata file: Invalid JSON format.')
|
|
||||||
if (metadataFileRef.current) metadataFileRef.current.value = ''
|
|
||||||
setMetadataFile(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setMetadataFile(undefined)
|
|
||||||
if (event.target.files === null) return
|
|
||||||
|
|
||||||
let selectedFile: File
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), {
|
|
||||||
type: 'application/json',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
|
||||||
else return toast.error('No file selected.')
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
setMetadataFile(selectedFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void parseMetadata()
|
|
||||||
if (!metadataFile)
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: '',
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
}, [metadataFile])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
animationUrlState.onChange('')
|
|
||||||
}, [uploadMethod])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const data: BadgeDetailsDataProps = {
|
|
||||||
manager: managerState.value,
|
|
||||||
name: nameState.value || undefined,
|
|
||||||
description: descriptionState.value || undefined,
|
|
||||||
attributes:
|
|
||||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
|
||||||
? attributesState.values
|
|
||||||
.map((attr) => ({
|
|
||||||
trait_type: attr.trait_type,
|
|
||||||
value: attr.value,
|
|
||||||
}))
|
|
||||||
.filter((attr) => attr.trait_type && attr.value)
|
|
||||||
: undefined,
|
|
||||||
expiry: timestamp ? timestamp.getTime() / 1000 : undefined,
|
|
||||||
max_supply: maxSupplyState.value || undefined,
|
|
||||||
transferrable,
|
|
||||||
image_data: imageDataState.value || undefined,
|
|
||||||
external_url: externalUrlState.value || undefined,
|
|
||||||
background_color: backgroundColorState.value || undefined,
|
|
||||||
animation_url: animationUrlState.value || undefined,
|
|
||||||
youtube_url: youtubeUrlState.value || undefined,
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
managerState.value,
|
|
||||||
nameState.value,
|
|
||||||
descriptionState.value,
|
|
||||||
timestamp,
|
|
||||||
maxSupplyState.value,
|
|
||||||
transferrable,
|
|
||||||
imageDataState.value,
|
|
||||||
externalUrlState.value,
|
|
||||||
attributesState.values,
|
|
||||||
backgroundColorState.value,
|
|
||||||
animationUrlState.value,
|
|
||||||
youtubeUrlState.value,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const retrieveFeeRate = async () => {
|
|
||||||
try {
|
|
||||||
if (isWalletConnected) {
|
|
||||||
const feeRateRaw = await (
|
|
||||||
await getCosmWasmClient()
|
|
||||||
).queryContractRaw(
|
|
||||||
BADGE_HUB_ADDRESS,
|
|
||||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
|
||||||
)
|
|
||||||
console.log('Fee Rate Raw: ', feeRateRaw)
|
|
||||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
|
||||||
setMetadataFeeRate(Number(feeRate.metadata))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Error retrieving metadata fee rate.')
|
|
||||||
setMetadataFeeRate(0)
|
|
||||||
console.log('Error retrieving fee rate: ', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void retrieveFeeRate()
|
|
||||||
}, [isWalletConnected, getCosmWasmClient])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={clsx('grid grid-cols-2 ml-4 max-w-5xl')}>
|
|
||||||
<div className={clsx('mt-2')}>
|
|
||||||
<AddressInput {...managerState} isRequired />
|
|
||||||
<TextInput className="mt-2" {...nameState} />
|
|
||||||
<TextInput className="mt-2" {...descriptionState} />
|
|
||||||
<NumberInput className="mt-2" {...maxSupplyState} />
|
|
||||||
{uploadMethod === 'existing' ? <TextInput className="mt-2" {...animationUrlState} /> : null}
|
|
||||||
<TextInput className="mt-2" {...externalUrlState} />
|
|
||||||
|
|
||||||
<FormControl
|
|
||||||
className="mt-2"
|
|
||||||
htmlId="expiry-date"
|
|
||||||
subtitle={`Badge minting expiry date ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
|
||||||
title="Expiry Date"
|
|
||||||
>
|
|
||||||
<InputDateTime
|
|
||||||
minDate={
|
|
||||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
}
|
|
||||||
onChange={(date) =>
|
|
||||||
setTimestamp(
|
|
||||||
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
timezone === 'Local'
|
|
||||||
? timestamp
|
|
||||||
: timestamp
|
|
||||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="grid grid-cols-2">
|
|
||||||
<div className="mt-2 w-1/3 form-control">
|
|
||||||
<label className="justify-start cursor-pointer label">
|
|
||||||
<span className="mr-4 font-bold">Transferrable</span>
|
|
||||||
<input
|
|
||||||
checked={transferrable}
|
|
||||||
className={`toggle ${transferrable ? `bg-stargaze` : `bg-gray-600`}`}
|
|
||||||
onClick={() => setTransferrable(!transferrable)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<Conditional test={managerState.value !== ''}>
|
|
||||||
<Tooltip
|
|
||||||
backgroundColor="bg-stargaze"
|
|
||||||
className="bg-yellow-600"
|
|
||||||
label="This is only an estimate. Be sure to check the final amount before signing the transaction."
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-2 ml-12 w-full">
|
|
||||||
<div className="mt-4 font-bold">Fee Estimate:</div>
|
|
||||||
<span className="mt-4">{(metadataSize * Number(metadataFeeRate)) / 1000000} stars</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={clsx('ml-10')}>
|
|
||||||
<div>
|
|
||||||
<MetadataAttributes
|
|
||||||
attributes={attributesState.entries}
|
|
||||||
onAdd={attributesState.add}
|
|
||||||
onChange={attributesState.update}
|
|
||||||
onRemove={attributesState.remove}
|
|
||||||
title="Traits"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Tooltip
|
|
||||||
backgroundColor="bg-blue-500"
|
|
||||||
label="A metadata file can be selected to automatically fill in the related fields."
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
|
|
||||||
htmlFor="assetFile"
|
|
||||||
>
|
|
||||||
Metadata File Selection (optional)
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="application/json"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="metadataFile"
|
|
||||||
onChange={selectMetadata}
|
|
||||||
ref={metadataFileRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,352 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable jsx-a11y/media-has-caption */
|
|
||||||
/* eslint-disable no-misleading-character-class */
|
|
||||||
/* eslint-disable no-control-regex */
|
|
||||||
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Anchor } from 'components/Anchor'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { TextInput } from 'components/forms/FormInput'
|
|
||||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import type { UploadServiceType } from 'services/upload'
|
|
||||||
import { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
|
|
||||||
import { getAssetType } from 'utils/getAssetType'
|
|
||||||
|
|
||||||
export type UploadMethod = 'new' | 'existing'
|
|
||||||
export type MintRule = 'by_key' | 'by_minter' | 'by_keys' | 'not_resolved'
|
|
||||||
|
|
||||||
interface ImageUploadDetailsProps {
|
|
||||||
onChange: (value: ImageUploadDetailsDataProps) => void
|
|
||||||
mintRule: MintRule
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImageUploadDetailsDataProps {
|
|
||||||
assetFile: File | undefined
|
|
||||||
uploadService: UploadServiceType
|
|
||||||
nftStorageApiKey?: string
|
|
||||||
pinataApiKey?: string
|
|
||||||
pinataSecretKey?: string
|
|
||||||
uploadMethod: UploadMethod
|
|
||||||
imageUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsProps) => {
|
|
||||||
const [assetFile, setAssetFile] = useState<File>()
|
|
||||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
|
||||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
|
||||||
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
|
|
||||||
|
|
||||||
const assetFileRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
|
|
||||||
const nftStorageApiKeyState = useInputState({
|
|
||||||
id: 'nft-storage-api-key',
|
|
||||||
name: 'nftStorageApiKey',
|
|
||||||
title: 'NFT.Storage API Key',
|
|
||||||
placeholder: 'Enter NFT.Storage API Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
const pinataApiKeyState = useInputState({
|
|
||||||
id: 'pinata-api-key',
|
|
||||||
name: 'pinataApiKey',
|
|
||||||
title: 'Pinata API Key',
|
|
||||||
placeholder: 'Enter Pinata API Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
const pinataSecretKeyState = useInputState({
|
|
||||||
id: 'pinata-secret-key',
|
|
||||||
name: 'pinataSecretKey',
|
|
||||||
title: 'Pinata Secret Key',
|
|
||||||
placeholder: 'Enter Pinata Secret Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const imageUrlState = useInputState({
|
|
||||||
id: 'imageUrl',
|
|
||||||
name: 'imageUrl',
|
|
||||||
title: 'Image URL',
|
|
||||||
placeholder: 'ipfs://',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setAssetFile(undefined)
|
|
||||||
if (event.target.files === null) return
|
|
||||||
|
|
||||||
let selectedFile: File
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/jpg' })
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
|
||||||
else return toast.error('No file selected.')
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
setAssetFile(selectedFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex =
|
|
||||||
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const data: ImageUploadDetailsDataProps = {
|
|
||||||
assetFile,
|
|
||||||
uploadService,
|
|
||||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
|
||||||
pinataApiKey: pinataApiKeyState.value,
|
|
||||||
pinataSecretKey: pinataSecretKeyState.value,
|
|
||||||
uploadMethod,
|
|
||||||
imageUrl: imageUrlState.value
|
|
||||||
.replace('IPFS://', 'ipfs://')
|
|
||||||
.replace(/,/g, '')
|
|
||||||
.replace(/"/g, '')
|
|
||||||
.replace(/'/g, '')
|
|
||||||
.replace(regex, '')
|
|
||||||
.trim(),
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
assetFile,
|
|
||||||
uploadService,
|
|
||||||
nftStorageApiKeyState.value,
|
|
||||||
pinataApiKeyState.value,
|
|
||||||
pinataSecretKeyState.value,
|
|
||||||
uploadMethod,
|
|
||||||
imageUrlState.value,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (assetFileRef.current) assetFileRef.current.value = ''
|
|
||||||
setAssetFile(undefined)
|
|
||||||
imageUrlState.onChange('')
|
|
||||||
}, [uploadMethod, mintRule])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (useDefaultApiKey) {
|
|
||||||
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
|
|
||||||
} else {
|
|
||||||
nftStorageApiKeyState.onChange('')
|
|
||||||
}
|
|
||||||
}, [useDefaultApiKey])
|
|
||||||
|
|
||||||
const videoPreview = useMemo(
|
|
||||||
() => (
|
|
||||||
<video
|
|
||||||
className="ml-4"
|
|
||||||
controls
|
|
||||||
id="video"
|
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
|
||||||
src={
|
|
||||||
imageUrlState.value ? imageUrlState.value.replace('ipfs://', 'https://ipfs-gw.stargaze-apis.com/ipfs/') : ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[imageUrlState.value],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadMethod === 'new'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio2"
|
|
||||||
name="inlineRadioOptions2"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadMethod('new')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="New"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio2"
|
|
||||||
>
|
|
||||||
Upload New Image
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadMethod === 'existing'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio1"
|
|
||||||
name="inlineRadioOptions1"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadMethod('existing')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="Existing"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio1"
|
|
||||||
>
|
|
||||||
Use an existing Image URL
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 py-5 pb-4">
|
|
||||||
<Conditional test={uploadMethod === 'existing'}>
|
|
||||||
<div className="ml-3 flex-column">
|
|
||||||
<p className="mb-5 ml-5">
|
|
||||||
Though the Badge Hub contract allows for off-chain image storage, it is recommended to use a decentralized
|
|
||||||
storage solution, such as IPFS. <br /> You may head over to{' '}
|
|
||||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
|
||||||
NFT.Storage
|
|
||||||
</Anchor>{' '}
|
|
||||||
or{' '}
|
|
||||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
|
||||||
Pinata
|
|
||||||
</Anchor>{' '}
|
|
||||||
and upload your image manually to get an image URL for your badge.
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-row w-full">
|
|
||||||
<TextInput {...imageUrlState} className="mt-2 ml-6 w-full max-w-2xl" />
|
|
||||||
<Conditional test={imageUrlState.value !== ''}>
|
|
||||||
{getAssetType(imageUrlState.value) === 'image' && (
|
|
||||||
<div className="mt-2 ml-4 w-1/4 border-2 border-dashed">
|
|
||||||
<img
|
|
||||||
alt="badge-preview"
|
|
||||||
className="w-full"
|
|
||||||
src={imageUrlState.value.replace('IPFS://', 'ipfs://').replace(/,/g, '').replace(/"/g, '').trim()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{getAssetType(imageUrlState.value) === 'video' && videoPreview}
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
<Conditional test={uploadMethod === 'new'}>
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col items-center px-8 w-full">
|
|
||||||
<div className="flex justify-items-start mb-5 w-full font-bold">
|
|
||||||
<div className="form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadService === 'nft-storage'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio3"
|
|
||||||
name="inlineRadioOptions3"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadService('nft-storage')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="nft-storage"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio3"
|
|
||||||
>
|
|
||||||
Upload using NFT.Storage
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-2 form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadService === 'pinata'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio4"
|
|
||||||
name="inlineRadioOptions4"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadService('pinata')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="pinata"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio4"
|
|
||||||
>
|
|
||||||
Upload using Pinata
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full">
|
|
||||||
<Conditional test={uploadService === 'nft-storage'}>
|
|
||||||
<div className="flex-col w-full">
|
|
||||||
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
|
|
||||||
<div className="flex-row mt-2 w-full form-control">
|
|
||||||
<label className="cursor-pointer label">
|
|
||||||
<span className="mr-2 font-bold">Use Default API Key</span>
|
|
||||||
<input
|
|
||||||
checked={useDefaultApiKey}
|
|
||||||
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
|
||||||
onClick={() => {
|
|
||||||
setUseDefaultApiKey(!useDefaultApiKey)
|
|
||||||
}}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={uploadService === 'pinata'}>
|
|
||||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
|
||||||
<div className="w-[20px]" />
|
|
||||||
<TextInput {...pinataSecretKeyState} className="w-full" />
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="grid grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<div className="w-full">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
|
||||||
htmlFor="assetFile"
|
|
||||||
>
|
|
||||||
Image Selection
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="image/*, video/*"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="assetFile"
|
|
||||||
onChange={selectAsset}
|
|
||||||
ref={assetFileRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Conditional test={assetFile !== undefined}>
|
|
||||||
<SingleAssetPreview
|
|
||||||
relatedAsset={assetFile}
|
|
||||||
subtitle={`Asset filename: ${assetFile?.name as string}`}
|
|
||||||
/>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import type { QueryListItem } from './query'
|
|
||||||
|
|
||||||
export const useQueryComboboxState = () => {
|
|
||||||
const [value, setValue] = useState<QueryListItem | null>(null)
|
|
||||||
return { value, onChange: (item: QueryListItem) => setValue(item) }
|
|
||||||
}
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import { Combobox, Transition } from '@headlessui/react'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { matchSorter } from 'match-sorter'
|
|
||||||
import { Fragment, useEffect, useState } from 'react'
|
|
||||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
|
||||||
|
|
||||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
|
||||||
import type { QueryListItem } from './query'
|
|
||||||
import { BY_KEY_QUERY_LIST, BY_KEYS_QUERY_LIST, BY_MINTER_QUERY_LIST } from './query'
|
|
||||||
|
|
||||||
export interface QueryComboboxProps {
|
|
||||||
value: QueryListItem | null
|
|
||||||
onChange: (item: QueryListItem) => void
|
|
||||||
mintRule?: MintRule
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QueryCombobox = ({ value, onChange, mintRule }: QueryComboboxProps) => {
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
const [QUERY_LIST, SET_QUERY_LIST] = useState<QueryListItem[]>(BY_KEY_QUERY_LIST)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mintRule === 'by_keys') {
|
|
||||||
SET_QUERY_LIST(BY_KEYS_QUERY_LIST)
|
|
||||||
} else if (mintRule === 'by_minter') {
|
|
||||||
SET_QUERY_LIST(BY_MINTER_QUERY_LIST)
|
|
||||||
} else {
|
|
||||||
SET_QUERY_LIST(BY_KEY_QUERY_LIST)
|
|
||||||
}
|
|
||||||
}, [mintRule])
|
|
||||||
|
|
||||||
const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
as={FormControl}
|
|
||||||
htmlId="query"
|
|
||||||
labelAs={Combobox.Label}
|
|
||||||
onChange={onChange}
|
|
||||||
subtitle="Badge queries"
|
|
||||||
title=""
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Combobox.Input
|
|
||||||
className={clsx(
|
|
||||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
|
||||||
'placeholder:text-white/50',
|
|
||||||
'focus:ring focus:ring-plumbus-20',
|
|
||||||
)}
|
|
||||||
displayValue={(val?: QueryListItem) => val?.name ?? ''}
|
|
||||||
id="message-type"
|
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
|
||||||
placeholder="Select query"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox.Button
|
|
||||||
className={clsx(
|
|
||||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
|
||||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
|
||||||
</Combobox.Button>
|
|
||||||
|
|
||||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
|
||||||
<Combobox.Options
|
|
||||||
className={clsx(
|
|
||||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
|
||||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
|
||||||
'divide-y divide-stone-500/50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filtered.length < 1 && (
|
|
||||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
|
||||||
Query not found
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filtered.map((entry) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={entry.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
|
||||||
}
|
|
||||||
value={entry}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{entry.name}</span>
|
|
||||||
<span className="max-w-md text-sm">{entry.description}</span>
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{value && (
|
|
||||||
<div className="flex space-x-2 text-white/50">
|
|
||||||
<div className="mt-1">
|
|
||||||
<FaInfoCircle className="w-3 h-3" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm">{value.description}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
import { QueryCombobox } from 'components/badges/queries/Combobox'
|
|
||||||
import { useQueryComboboxState } from 'components/badges/queries/Combobox.hooks'
|
|
||||||
import { dispatchQuery } from 'components/badges/queries/query'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { NumberInput, TextInput } from 'components/forms/FormInput'
|
|
||||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { JsonPreview } from 'components/JsonPreview'
|
|
||||||
import type { BadgeHubInstance } from 'contracts/badgeHub'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
|
|
||||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
|
||||||
|
|
||||||
interface BadgeQueriesProps {
|
|
||||||
badgeHubContractAddress: string
|
|
||||||
badgeId: number
|
|
||||||
badgeHubMessages: BadgeHubInstance | undefined
|
|
||||||
mintRule: MintRule
|
|
||||||
}
|
|
||||||
export const BadgeQueries = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeQueriesProps) => {
|
|
||||||
const comboboxState = useQueryComboboxState()
|
|
||||||
const type = comboboxState.value?.id
|
|
||||||
|
|
||||||
const pubkeyState = useInputState({
|
|
||||||
id: 'pubkey',
|
|
||||||
name: 'pubkey',
|
|
||||||
title: 'Public Key',
|
|
||||||
subtitle: 'The public key to check whether it can be used to mint a badge',
|
|
||||||
})
|
|
||||||
|
|
||||||
const startAfterNumberState = useNumberInputState({
|
|
||||||
id: 'start-after-number',
|
|
||||||
name: 'start-after-number',
|
|
||||||
title: 'Start After (optional)',
|
|
||||||
subtitle: 'The id to start the pagination after',
|
|
||||||
})
|
|
||||||
|
|
||||||
const startAfterStringState = useInputState({
|
|
||||||
id: 'start-after-string',
|
|
||||||
name: 'start-after-string',
|
|
||||||
title: 'Start After (optional)',
|
|
||||||
subtitle: 'The public key to start the pagination after',
|
|
||||||
})
|
|
||||||
|
|
||||||
const paginationLimitState = useNumberInputState({
|
|
||||||
id: 'pagination-limit',
|
|
||||||
name: 'pagination-limit',
|
|
||||||
title: 'Pagination Limit (optional)',
|
|
||||||
subtitle: 'The number of items to return (max: 30)',
|
|
||||||
defaultValue: 5,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: response } = useQuery(
|
|
||||||
[
|
|
||||||
badgeHubMessages,
|
|
||||||
type,
|
|
||||||
badgeId,
|
|
||||||
pubkeyState.value,
|
|
||||||
startAfterNumberState.value,
|
|
||||||
startAfterStringState.value,
|
|
||||||
paginationLimitState.value,
|
|
||||||
] as const,
|
|
||||||
async ({ queryKey }) => {
|
|
||||||
const [_badgeHubMessages, _type, _badgeId, _pubKey, _startAfterNumber, _startAfterString, _limit] = queryKey
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const result = await dispatchQuery({
|
|
||||||
badgeHubMessages: _badgeHubMessages,
|
|
||||||
id: _badgeId,
|
|
||||||
startAfterNumber: _startAfterNumber,
|
|
||||||
startAfterString: _startAfterString,
|
|
||||||
limit: _limit,
|
|
||||||
type: _type,
|
|
||||||
pubkey: _pubKey,
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
{
|
|
||||||
placeholderData: null,
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
||||||
},
|
|
||||||
enabled: Boolean(badgeHubContractAddress && type && badgeId),
|
|
||||||
retry: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-2 mt-4">
|
|
||||||
<div className="mr-2 space-y-8">
|
|
||||||
<QueryCombobox mintRule={mintRule} {...comboboxState} />
|
|
||||||
<Conditional test={type === 'getKey'}>
|
|
||||||
<TextInput {...pubkeyState} />
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={type === 'getBadges'}>
|
|
||||||
<NumberInput {...startAfterNumberState} />
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={type === 'getBadges' || type === 'getKeys'}>
|
|
||||||
<NumberInput {...paginationLimitState} />
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={type === 'getKeys'}>
|
|
||||||
<TextInput {...startAfterStringState} />
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-8">
|
|
||||||
<FormControl title="Query Response">
|
|
||||||
<JsonPreview content={response || {}} isCopyable />
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
import type { BadgeHubInstance } from 'contracts/badgeHub'
|
|
||||||
|
|
||||||
export type QueryType = typeof QUERY_TYPES[number]
|
|
||||||
|
|
||||||
export const QUERY_TYPES = ['config', 'getBadge', 'getBadges', 'getKey', 'getKeys'] as const
|
|
||||||
|
|
||||||
export interface QueryListItem {
|
|
||||||
id: QueryType
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BY_KEY_QUERY_LIST: QueryListItem[] = [
|
|
||||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
|
||||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
|
||||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
|
||||||
]
|
|
||||||
export const BY_KEYS_QUERY_LIST: QueryListItem[] = [
|
|
||||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
|
||||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
|
||||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
|
||||||
{ id: 'getKey', name: 'Query Key', description: "Query a key by ID to see if it's whitelisted" },
|
|
||||||
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
|
|
||||||
]
|
|
||||||
export const BY_MINTER_QUERY_LIST: QueryListItem[] = [
|
|
||||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
|
||||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
|
||||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export interface DispatchExecuteProps {
|
|
||||||
type: QueryType
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type Select<T extends QueryType> = T
|
|
||||||
|
|
||||||
export type DispatchQueryArgs = {
|
|
||||||
badgeHubMessages?: BadgeHubInstance
|
|
||||||
} & (
|
|
||||||
| { type: undefined }
|
|
||||||
| { type: Select<'config'> }
|
|
||||||
| { type: Select<'getBadge'>; id: number }
|
|
||||||
| { type: Select<'getBadges'>; startAfterNumber: number; limit: number }
|
|
||||||
| { type: Select<'getKey'>; id: number; pubkey: string }
|
|
||||||
| { type: Select<'getKeys'>; id: number; startAfterString: string; limit: number }
|
|
||||||
)
|
|
||||||
|
|
||||||
export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
|
||||||
const { badgeHubMessages } = args
|
|
||||||
if (!badgeHubMessages) {
|
|
||||||
throw new Error('Cannot perform a query. Please connect your wallet first.')
|
|
||||||
}
|
|
||||||
switch (args.type) {
|
|
||||||
case 'config': {
|
|
||||||
return badgeHubMessages?.getConfig()
|
|
||||||
}
|
|
||||||
case 'getBadge': {
|
|
||||||
return badgeHubMessages?.getBadge(args.id)
|
|
||||||
}
|
|
||||||
case 'getBadges': {
|
|
||||||
return badgeHubMessages?.getBadges(args.startAfterNumber, args.limit)
|
|
||||||
}
|
|
||||||
case 'getKey': {
|
|
||||||
return badgeHubMessages?.getKey(args.id, args.pubkey)
|
|
||||||
}
|
|
||||||
case 'getKeys': {
|
|
||||||
return badgeHubMessages?.getKeys(args.id, args.startAfterString, args.limit)
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error('Unknown action')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +1,5 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
import { toUtf8 } from '@cosmjs/encoding'
|
import { 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">
|
||||||
|
|||||||
@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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 {}
|
||||||
|
|||||||
@ -1,301 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import { toUtf8 } from '@cosmjs/encoding'
|
|
||||||
import axios from 'axios'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Alert } from 'components/Alert'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { API_URL } from 'utils/constants'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { useDebounce } from '../../../utils/debounce'
|
|
||||||
import { TextInput } from '../../forms/FormInput'
|
|
||||||
import type { MinterType } from '../actions/Combobox'
|
|
||||||
|
|
||||||
export type BaseMinterAcquisitionMethod = 'existing' | 'new'
|
|
||||||
|
|
||||||
export interface MinterInfo {
|
|
||||||
name: string
|
|
||||||
minter: string
|
|
||||||
contractAddress: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseMinterDetailsProps {
|
|
||||||
onChange: (data: BaseMinterDetailsDataProps) => void
|
|
||||||
minterType: MinterType
|
|
||||||
importedBaseMinterDetails?: BaseMinterDetailsDataProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BaseMinterDetailsDataProps {
|
|
||||||
baseMinterAcquisitionMethod: BaseMinterAcquisitionMethod
|
|
||||||
existingBaseMinter: string | undefined
|
|
||||||
selectedCollectionAddress: string | undefined
|
|
||||||
collectionTokenCount: number | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BaseMinterDetails = ({ onChange, minterType, importedBaseMinterDetails }: BaseMinterDetailsProps) => {
|
|
||||||
const wallet = useWallet()
|
|
||||||
|
|
||||||
const [myBaseMinterContracts, setMyBaseMinterContracts] = useState<MinterInfo[]>([])
|
|
||||||
const [baseMinterAcquisitionMethod, setBaseMinterAcquisitionMethod] = useState<BaseMinterAcquisitionMethod>('new')
|
|
||||||
const [selectedCollectionAddress, setSelectedCollectionAddress] = useState<string | undefined>(undefined)
|
|
||||||
const [collectionTokenCount, setCollectionTokenCount] = useState<number | undefined>(undefined)
|
|
||||||
|
|
||||||
const existingBaseMinterState = useInputState({
|
|
||||||
id: 'existingMinter',
|
|
||||||
name: 'existingMinter',
|
|
||||||
title: 'Existing Base Minter Contract Address',
|
|
||||||
subtitle: '',
|
|
||||||
placeholder: 'stars1...',
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchMinterContracts = async (): Promise<MinterInfo[]> => {
|
|
||||||
const contracts: MinterInfo[] = await axios
|
|
||||||
.get(`${API_URL}/api/v1beta/collections/${wallet.address || ''}`)
|
|
||||||
.then((response) => {
|
|
||||||
const collectionData = response.data
|
|
||||||
const minterContracts = collectionData.map((collection: any) => {
|
|
||||||
return { name: collection.name, minter: collection.minter, contractAddress: collection.contractAddress }
|
|
||||||
})
|
|
||||||
return minterContracts
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
console.log(contracts)
|
|
||||||
return contracts
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMinterContractType(minterContractAddress: string) {
|
|
||||||
if (wallet.isWalletConnected && minterContractAddress.length > 0) {
|
|
||||||
const client = await wallet.getCosmWasmClient()
|
|
||||||
const data = await client.queryContractRaw(
|
|
||||||
minterContractAddress,
|
|
||||||
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
|
|
||||||
)
|
|
||||||
const contractType: string = JSON.parse(new TextDecoder().decode(data as Uint8Array)).contract
|
|
||||||
return contractType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterBaseMinterContracts = async () => {
|
|
||||||
await fetchMinterContracts()
|
|
||||||
.then((minterContracts) =>
|
|
||||||
minterContracts.map(async (minterContract: any) => {
|
|
||||||
await getMinterContractType(minterContract.minter)
|
|
||||||
.then((contractType) => {
|
|
||||||
if (contractType?.includes('sg-base-minter')) {
|
|
||||||
setMyBaseMinterContracts((prevState) => [...prevState, minterContract])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
console.log('Unable to retrieve contract type')
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
console.log('Unable to fetch base minter contracts')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const debouncedMyBaseMinterContracts = useDebounce(myBaseMinterContracts, 500)
|
|
||||||
|
|
||||||
const renderBaseMinterContracts = useCallback(() => {
|
|
||||||
return debouncedMyBaseMinterContracts.map((baseMinterContract, index) => {
|
|
||||||
return (
|
|
||||||
<option key={index} className="mt-2 text-lg bg-[#1A1A1A]">
|
|
||||||
{`${baseMinterContract.name} - ${baseMinterContract.minter}`}
|
|
||||||
</option>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [debouncedMyBaseMinterContracts])
|
|
||||||
|
|
||||||
const debouncedWalletAddress = useDebounce(wallet.address, 300)
|
|
||||||
|
|
||||||
const debouncedExistingBaseMinterContract = useDebounce(existingBaseMinterState.value, 300)
|
|
||||||
|
|
||||||
const displayToast = async () => {
|
|
||||||
await toast.promise(filterBaseMinterContracts(), {
|
|
||||||
loading: 'Retrieving previous 1/1 collections...',
|
|
||||||
success: 'Collection retrieval finalized.',
|
|
||||||
error: 'Unable to retrieve any 1/1 collections.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchSg721Address = async () => {
|
|
||||||
if (debouncedExistingBaseMinterContract.length === 0) return
|
|
||||||
await (
|
|
||||||
await wallet.getCosmWasmClient()
|
|
||||||
)
|
|
||||||
.queryContractSmart(debouncedExistingBaseMinterContract, {
|
|
||||||
config: {},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
console.log(response.collection_address)
|
|
||||||
setSelectedCollectionAddress(response.collection_address)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
console.log('Unable to retrieve collection address')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchCollectionTokenCount = async () => {
|
|
||||||
if (selectedCollectionAddress === undefined) return
|
|
||||||
await (
|
|
||||||
await wallet.getCosmWasmClient()
|
|
||||||
)
|
|
||||||
.queryContractSmart(selectedCollectionAddress, {
|
|
||||||
num_tokens: {},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
console.log(response)
|
|
||||||
setCollectionTokenCount(Number(response.count))
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
console.log('Unable to retrieve collection token count')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (debouncedWalletAddress && baseMinterAcquisitionMethod === 'existing') {
|
|
||||||
setMyBaseMinterContracts([])
|
|
||||||
existingBaseMinterState.onChange('')
|
|
||||||
void displayToast()
|
|
||||||
} else if (baseMinterAcquisitionMethod === 'new' || !wallet.isWalletConnected) {
|
|
||||||
setMyBaseMinterContracts([])
|
|
||||||
existingBaseMinterState.onChange('')
|
|
||||||
}
|
|
||||||
}, [debouncedWalletAddress, baseMinterAcquisitionMethod])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (baseMinterAcquisitionMethod === 'existing') {
|
|
||||||
void fetchSg721Address()
|
|
||||||
}
|
|
||||||
}, [debouncedExistingBaseMinterContract])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (baseMinterAcquisitionMethod === 'existing') {
|
|
||||||
void fetchCollectionTokenCount()
|
|
||||||
}
|
|
||||||
}, [selectedCollectionAddress])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const data: BaseMinterDetailsDataProps = {
|
|
||||||
baseMinterAcquisitionMethod,
|
|
||||||
existingBaseMinter: existingBaseMinterState.value,
|
|
||||||
selectedCollectionAddress,
|
|
||||||
collectionTokenCount,
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
existingBaseMinterState.value,
|
|
||||||
baseMinterAcquisitionMethod,
|
|
||||||
wallet.isWalletConnected,
|
|
||||||
selectedCollectionAddress,
|
|
||||||
collectionTokenCount,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (importedBaseMinterDetails) {
|
|
||||||
setBaseMinterAcquisitionMethod(importedBaseMinterDetails.baseMinterAcquisitionMethod)
|
|
||||||
existingBaseMinterState.onChange(
|
|
||||||
importedBaseMinterDetails.existingBaseMinter ? importedBaseMinterDetails.existingBaseMinter : '',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [importedBaseMinterDetails])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-10 mb-4 rounded border-2 border-white/20">
|
|
||||||
<div className="flex justify-center mb-2">
|
|
||||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={baseMinterAcquisitionMethod === 'new'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio5"
|
|
||||||
name="inlineRadioOptions5"
|
|
||||||
onClick={() => {
|
|
||||||
setBaseMinterAcquisitionMethod('new')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="New"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio5"
|
|
||||||
>
|
|
||||||
Create a New 1/1 Collection
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={baseMinterAcquisitionMethod === 'existing'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio6"
|
|
||||||
name="inlineRadioOptions6"
|
|
||||||
onClick={() => {
|
|
||||||
setBaseMinterAcquisitionMethod('existing')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="Existing"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio6"
|
|
||||||
>
|
|
||||||
Add a New Token to an Existing 1/1 Collection
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{baseMinterAcquisitionMethod === 'existing' && (
|
|
||||||
<div>
|
|
||||||
<div className={clsx('my-4 mx-10')}>
|
|
||||||
<Conditional test={myBaseMinterContracts.length !== 0}>
|
|
||||||
<select
|
|
||||||
className="mt-4 w-full max-w-3xl text-base bg-white/10 select select-bordered"
|
|
||||||
onChange={(e) => {
|
|
||||||
existingBaseMinterState.onChange(e.target.value.slice(e.target.value.indexOf('stars1')))
|
|
||||||
e.preventDefault()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option className="mt-2 text-lg bg-[#1A1A1A]" disabled selected>
|
|
||||||
Select one of your existing 1/1 collections
|
|
||||||
</option>
|
|
||||||
{renderBaseMinterContracts()}
|
|
||||||
</select>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={myBaseMinterContracts.length === 0}>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Conditional test={wallet.isWalletConnected}>
|
|
||||||
<Alert className="my-2 w-[90%]" type="info">
|
|
||||||
No previous 1/1 collections were found. You may create a new 1/1 collection or fill in the minter
|
|
||||||
contract address manually.
|
|
||||||
</Alert>
|
|
||||||
<TextInput
|
|
||||||
className="w-3/5"
|
|
||||||
defaultValue={existingBaseMinterState.value}
|
|
||||||
{...existingBaseMinterState}
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={!wallet.isWalletConnected}>
|
|
||||||
<Alert className="my-2 w-[90%]" type="warning">
|
|
||||||
Please connect your wallet first.
|
|
||||||
</Alert>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,34 +1,23 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
/* eslint-disable 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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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's sg721 contract allows for off-chain metadata storage, it is recommended to use a
|
Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a
|
||||||
@ -530,53 +263,11 @@ export const UploadDetails = ({
|
|||||||
and upload your assets & metadata manually to get a base URI for your collection.
|
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'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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
export const useExecuteComboboxState = () => {
|
|
||||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
|
||||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { Combobox, Transition } from '@headlessui/react'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute'
|
|
||||||
import { EXECUTE_LIST } from 'contracts/badgeHub/messages/execute'
|
|
||||||
import { matchSorter } from 'match-sorter'
|
|
||||||
import { Fragment, useState } from 'react'
|
|
||||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
|
||||||
|
|
||||||
export interface ExecuteComboboxProps {
|
|
||||||
value: ExecuteListItem | null
|
|
||||||
onChange: (item: ExecuteListItem) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
|
|
||||||
const filtered =
|
|
||||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
as={FormControl}
|
|
||||||
htmlId="message-type"
|
|
||||||
labelAs={Combobox.Label}
|
|
||||||
onChange={onChange}
|
|
||||||
subtitle="Contract execute message type"
|
|
||||||
title="Message Type"
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Combobox.Input
|
|
||||||
className={clsx(
|
|
||||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
|
||||||
'placeholder:text-white/50',
|
|
||||||
'focus:ring focus:ring-plumbus-20',
|
|
||||||
)}
|
|
||||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
|
||||||
id="message-type"
|
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
|
||||||
placeholder="Select message type"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox.Button
|
|
||||||
className={clsx(
|
|
||||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
|
||||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
|
||||||
</Combobox.Button>
|
|
||||||
|
|
||||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
|
||||||
<Combobox.Options
|
|
||||||
className={clsx(
|
|
||||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
|
||||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
|
||||||
'divide-y divide-stone-500/50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filtered.length < 1 && (
|
|
||||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
|
||||||
Message type not found.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filtered.map((entry) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={entry.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
|
||||||
}
|
|
||||||
value={entry}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{entry.name}</span>
|
|
||||||
<span className="max-w-md text-sm">{entry.description}</span>
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{value && (
|
|
||||||
<div className="flex space-x-2 text-white/50">
|
|
||||||
<div className="mt-1">
|
|
||||||
<FaInfoCircle className="w-3 h-3" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm">{value.description}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import type { ExecuteListItem } from 'contracts/baseMinter/messages/execute'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
export const useExecuteComboboxState = () => {
|
|
||||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
|
||||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { Combobox, Transition } from '@headlessui/react'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import type { ExecuteListItem } from 'contracts/baseMinter/messages/execute'
|
|
||||||
import { EXECUTE_LIST } from 'contracts/baseMinter/messages/execute'
|
|
||||||
import { matchSorter } from 'match-sorter'
|
|
||||||
import { Fragment, useState } from 'react'
|
|
||||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
|
||||||
|
|
||||||
export interface ExecuteComboboxProps {
|
|
||||||
value: ExecuteListItem | null
|
|
||||||
onChange: (item: ExecuteListItem) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
|
|
||||||
const filtered =
|
|
||||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
as={FormControl}
|
|
||||||
htmlId="message-type"
|
|
||||||
labelAs={Combobox.Label}
|
|
||||||
onChange={onChange}
|
|
||||||
subtitle="Contract execute message type"
|
|
||||||
title="Message Type"
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Combobox.Input
|
|
||||||
className={clsx(
|
|
||||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
|
||||||
'placeholder:text-white/50',
|
|
||||||
'focus:ring focus:ring-plumbus-20',
|
|
||||||
)}
|
|
||||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
|
||||||
id="message-type"
|
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
|
||||||
placeholder="Select message type"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox.Button
|
|
||||||
className={clsx(
|
|
||||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
|
||||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
|
||||||
</Combobox.Button>
|
|
||||||
|
|
||||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
|
||||||
<Combobox.Options
|
|
||||||
className={clsx(
|
|
||||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
|
||||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
|
||||||
'divide-y divide-stone-500/50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filtered.length < 1 && (
|
|
||||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
|
||||||
Message type not found.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filtered.map((entry) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={entry.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
|
||||||
}
|
|
||||||
value={entry}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{entry.name}</span>
|
|
||||||
<span className="max-w-md text-sm">{entry.description}</span>
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{value && (
|
|
||||||
<div className="flex space-x-2 text-white/50">
|
|
||||||
<div className="mt-1">
|
|
||||||
<FaInfoCircle className="w-3 h-3" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm">{value.description}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ExecuteListItem } from 'contracts/splits/messages/execute'
|
import type { ExecuteListItem } from 'contracts/minter/messages/execute'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
export const useExecuteComboboxState = () => {
|
export const useExecuteComboboxState = () => {
|
||||||
@ -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}
|
||||||
>
|
>
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import type { ExecuteListItem } from 'contracts/openEditionMinter/messages/execute'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
export const useExecuteComboboxState = () => {
|
|
||||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
|
||||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { Combobox, Transition } from '@headlessui/react'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import type { ExecuteListItem } from 'contracts/openEditionMinter/messages/execute'
|
|
||||||
import { EXECUTE_LIST } from 'contracts/openEditionMinter/messages/execute'
|
|
||||||
import { matchSorter } from 'match-sorter'
|
|
||||||
import { Fragment, useState } from 'react'
|
|
||||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
|
||||||
|
|
||||||
export interface ExecuteComboboxProps {
|
|
||||||
value: ExecuteListItem | null
|
|
||||||
onChange: (item: ExecuteListItem) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
|
|
||||||
const filtered =
|
|
||||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
as={FormControl}
|
|
||||||
htmlId="message-type"
|
|
||||||
labelAs={Combobox.Label}
|
|
||||||
onChange={onChange}
|
|
||||||
subtitle="Contract execute message type"
|
|
||||||
title="Message Type"
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Combobox.Input
|
|
||||||
className={clsx(
|
|
||||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
|
||||||
'placeholder:text-white/50',
|
|
||||||
'focus:ring focus:ring-plumbus-20',
|
|
||||||
)}
|
|
||||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
|
||||||
id="message-type"
|
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
|
||||||
placeholder="Select message type"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox.Button
|
|
||||||
className={clsx(
|
|
||||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
|
||||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
|
||||||
</Combobox.Button>
|
|
||||||
|
|
||||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
|
||||||
<Combobox.Options
|
|
||||||
className={clsx(
|
|
||||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
|
||||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
|
||||||
'divide-y divide-stone-500/50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filtered.length < 1 && (
|
|
||||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
|
||||||
Message type not found.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filtered.map((entry) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={entry.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
|
||||||
}
|
|
||||||
value={entry}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{entry.name}</span>
|
|
||||||
<span className="max-w-md text-sm">{entry.description}</span>
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{value && (
|
|
||||||
<div className="flex space-x-2 text-white/50">
|
|
||||||
<div className="mt-1">
|
|
||||||
<FaInfoCircle className="w-3 h-3" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm">{value.description}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import type { ExecuteListItem } from 'contracts/royaltyRegistry/messages/execute'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
export const useExecuteComboboxState = () => {
|
|
||||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
|
||||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { Combobox, Transition } from '@headlessui/react'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import type { ExecuteListItem } from 'contracts/royaltyRegistry/messages/execute'
|
|
||||||
import { EXECUTE_LIST } from 'contracts/royaltyRegistry/messages/execute'
|
|
||||||
import { matchSorter } from 'match-sorter'
|
|
||||||
import { Fragment, useState } from 'react'
|
|
||||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
|
||||||
|
|
||||||
export interface ExecuteComboboxProps {
|
|
||||||
value: ExecuteListItem | null
|
|
||||||
onChange: (item: ExecuteListItem) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
|
|
||||||
const filtered =
|
|
||||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
as={FormControl}
|
|
||||||
htmlId="message-type"
|
|
||||||
labelAs={Combobox.Label}
|
|
||||||
onChange={onChange}
|
|
||||||
subtitle="Contract execute message type"
|
|
||||||
title="Message Type"
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Combobox.Input
|
|
||||||
className={clsx(
|
|
||||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
|
||||||
'placeholder:text-white/50',
|
|
||||||
'focus:ring focus:ring-plumbus-20',
|
|
||||||
)}
|
|
||||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
|
||||||
id="message-type"
|
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
|
||||||
placeholder="Select message type"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox.Button
|
|
||||||
className={clsx(
|
|
||||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
|
||||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
|
||||||
</Combobox.Button>
|
|
||||||
|
|
||||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
|
||||||
<Combobox.Options
|
|
||||||
className={clsx(
|
|
||||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
|
||||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
|
||||||
'divide-y divide-stone-500/50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filtered.length < 1 && (
|
|
||||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
|
||||||
Message type not found.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filtered.map((entry) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={entry.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
|
||||||
}
|
|
||||||
value={entry}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{entry.name}</span>
|
|
||||||
<span className="max-w-md text-sm">{entry.description}</span>
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{value && (
|
|
||||||
<div className="flex space-x-2 text-white/50">
|
|
||||||
<div className="mt-1">
|
|
||||||
<FaInfoCircle className="w-3 h-3" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm">{value.description}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -67,7 +67,7 @@ export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
|||||||
<Combobox.Option
|
<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}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import type { ExecuteListItem } from 'contracts/vendingMinter/messages/execute'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
export const useExecuteComboboxState = () => {
|
|
||||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
|
||||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { Combobox, Transition } from '@headlessui/react'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import type { ExecuteListItem } from 'contracts/vendingMinter/messages/execute'
|
|
||||||
import { EXECUTE_LIST } from 'contracts/vendingMinter/messages/execute'
|
|
||||||
import { matchSorter } from 'match-sorter'
|
|
||||||
import { Fragment, useState } from 'react'
|
|
||||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
|
||||||
|
|
||||||
export interface ExecuteComboboxProps {
|
|
||||||
value: ExecuteListItem | null
|
|
||||||
onChange: (item: ExecuteListItem) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
|
|
||||||
const filtered =
|
|
||||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox
|
|
||||||
as={FormControl}
|
|
||||||
htmlId="message-type"
|
|
||||||
labelAs={Combobox.Label}
|
|
||||||
onChange={onChange}
|
|
||||||
subtitle="Contract execute message type"
|
|
||||||
title="Message Type"
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Combobox.Input
|
|
||||||
className={clsx(
|
|
||||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
|
||||||
'placeholder:text-white/50',
|
|
||||||
'focus:ring focus:ring-plumbus-20',
|
|
||||||
)}
|
|
||||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
|
||||||
id="message-type"
|
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
|
||||||
placeholder="Select message type"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox.Button
|
|
||||||
className={clsx(
|
|
||||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
|
||||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
|
||||||
</Combobox.Button>
|
|
||||||
|
|
||||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
|
||||||
<Combobox.Options
|
|
||||||
className={clsx(
|
|
||||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
|
||||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
|
||||||
'divide-y divide-stone-500/50',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{filtered.length < 1 && (
|
|
||||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
|
||||||
Message type not found.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{filtered.map((entry) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={entry.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
|
||||||
}
|
|
||||||
value={entry}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{entry.name}</span>
|
|
||||||
<span className="max-w-md text-sm">{entry.description}</span>
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{value && (
|
|
||||||
<div className="flex space-x-2 text-white/50">
|
|
||||||
<div className="mt-1">
|
|
||||||
<FaInfoCircle className="w-3 h-3" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm">{value.description}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,11 +1,8 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
import { Combobox, Transition } from '@headlessui/react'
|
import { 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}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -27,9 +27,5 @@ export function useAddressListState() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
return { entries, values, add, update, remove }
|
||||||
setRecord({})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { entries, values, add, update, remove, reset }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react'
|
|
||||||
import { uid } from 'utils/random'
|
|
||||||
|
|
||||||
import type { DenomUnit } from './DenomUnits'
|
|
||||||
|
|
||||||
export function useDenomUnitsState() {
|
|
||||||
const [record, setRecord] = useState<Record<string, DenomUnit>>(() => ({}))
|
|
||||||
|
|
||||||
const entries = useMemo(() => Object.entries(record), [record])
|
|
||||||
const values = useMemo(() => Object.values(record), [record])
|
|
||||||
|
|
||||||
function add(attribute: DenomUnit = { denom: '', exponent: 0, aliases: '' }) {
|
|
||||||
setRecord((prev) => ({ ...prev, [uid()]: attribute }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(key: string, attribute = record[key]) {
|
|
||||||
setRecord((prev) => ({ ...prev, [key]: attribute }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(key: string) {
|
|
||||||
return setRecord((prev) => {
|
|
||||||
const latest = { ...prev }
|
|
||||||
delete latest[key]
|
|
||||||
return latest
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
setRecord({})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { entries, values, add, update, remove, reset }
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { NumberInput, TextInput } from 'components/forms/FormInput'
|
|
||||||
import { useEffect, useId, useMemo } from 'react'
|
|
||||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { useInputState, useNumberInputState } from './FormInput.hooks'
|
|
||||||
|
|
||||||
export interface DenomUnit {
|
|
||||||
denom: string
|
|
||||||
exponent: number
|
|
||||||
aliases: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DenomUnitsProps {
|
|
||||||
title: string
|
|
||||||
subtitle?: string
|
|
||||||
isRequired?: boolean
|
|
||||||
attributes: [string, DenomUnit][]
|
|
||||||
onAdd: () => void
|
|
||||||
onChange: (key: string, attribute: DenomUnit) => void
|
|
||||||
onRemove: (key: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DenomUnits(props: DenomUnitsProps) {
|
|
||||||
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
|
||||||
{attributes.map(([id], i) => (
|
|
||||||
<DenomUnit
|
|
||||||
key={`ma-${id}`}
|
|
||||||
defaultAttribute={attributes[i][1]}
|
|
||||||
id={id}
|
|
||||||
isLast={i === attributes.length - 1}
|
|
||||||
onAdd={onAdd}
|
|
||||||
onChange={onChange}
|
|
||||||
onRemove={onRemove}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</FormControl>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DenomUnitProps {
|
|
||||||
id: string
|
|
||||||
isLast: boolean
|
|
||||||
onAdd: DenomUnitsProps['onAdd']
|
|
||||||
onChange: DenomUnitsProps['onChange']
|
|
||||||
onRemove: DenomUnitsProps['onRemove']
|
|
||||||
defaultAttribute: DenomUnit
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DenomUnit({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: DenomUnitProps) {
|
|
||||||
const wallet = useWallet()
|
|
||||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
|
||||||
|
|
||||||
const htmlId = useId()
|
|
||||||
|
|
||||||
const denomState = useInputState({
|
|
||||||
id: `ma-denom-${htmlId}`,
|
|
||||||
name: `ma-denom-${htmlId}`,
|
|
||||||
title: `Denom`,
|
|
||||||
defaultValue: defaultAttribute.denom,
|
|
||||||
})
|
|
||||||
|
|
||||||
const exponentState = useNumberInputState({
|
|
||||||
id: `mint-exponent-${htmlId}`,
|
|
||||||
name: `mint-exponent-${htmlId}`,
|
|
||||||
title: `Exponent`,
|
|
||||||
defaultValue: defaultAttribute.exponent,
|
|
||||||
})
|
|
||||||
|
|
||||||
const aliasesState = useInputState({
|
|
||||||
id: `ma-aliases-${htmlId}`,
|
|
||||||
name: `ma-aliases-${htmlId}`,
|
|
||||||
title: `Aliases`,
|
|
||||||
defaultValue: defaultAttribute.aliases,
|
|
||||||
placeholder: 'Comma separated aliases',
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChange(id, { denom: denomState.value, exponent: exponentState.value, aliases: aliasesState.value })
|
|
||||||
}, [id, denomState.value, exponentState.value, aliasesState.value])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid relative md:grid-cols-[40%_18%_35_7%] lg:grid-cols-[55%_13%_25%_7%] 2xl:grid-cols-[55%_13%_25%_7%] 2xl:space-x-2">
|
|
||||||
<TextInput {...denomState} />
|
|
||||||
<NumberInput className="ml-2" {...exponentState} />
|
|
||||||
<TextInput className="ml-2" {...aliasesState} />
|
|
||||||
|
|
||||||
<div className="flex justify-end items-end pb-2 w-8">
|
|
||||||
<button
|
|
||||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
isLast ? onAdd() : onRemove(id)
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Icon className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
|
|
||||||
import { useMemo, useState } from 'react'
|
|
||||||
import { uid } from 'utils/random'
|
|
||||||
|
|
||||||
export function useFlexMemberAttributesState() {
|
|
||||||
const [record, setRecord] = useState<Record<string, WhitelistFlexMember>>(() => ({}))
|
|
||||||
|
|
||||||
const entries = useMemo(() => Object.entries(record), [record])
|
|
||||||
const values = useMemo(() => Object.values(record), [record])
|
|
||||||
|
|
||||||
function add(attribute: WhitelistFlexMember = { address: '', mint_count: 0 }) {
|
|
||||||
setRecord((prev) => ({ ...prev, [uid()]: attribute }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(key: string, attribute = record[key]) {
|
|
||||||
setRecord((prev) => ({ ...prev, [key]: attribute }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(key: string) {
|
|
||||||
return setRecord((prev) => {
|
|
||||||
const latest = { ...prev }
|
|
||||||
delete latest[key]
|
|
||||||
return latest
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
setRecord({})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { entries, values, add, update, remove, reset }
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
import { toUtf8 } from '@cosmjs/encoding'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
|
||||||
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
|
|
||||||
import { useEffect, useId, useMemo } from 'react'
|
|
||||||
import toast from 'react-hot-toast'
|
|
||||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
|
||||||
import { SG721_NAME_ADDRESS } from 'utils/constants'
|
|
||||||
import { isValidAddress } from 'utils/isValidAddress'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { useInputState, useNumberInputState } from './FormInput.hooks'
|
|
||||||
|
|
||||||
export interface FlexMemberAttributesProps {
|
|
||||||
title: string
|
|
||||||
subtitle?: string
|
|
||||||
isRequired?: boolean
|
|
||||||
attributes: [string, WhitelistFlexMember][]
|
|
||||||
onAdd: () => void
|
|
||||||
onChange: (key: string, attribute: WhitelistFlexMember) => void
|
|
||||||
onRemove: (key: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FlexMemberAttributes(props: FlexMemberAttributesProps) {
|
|
||||||
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
|
||||||
{attributes.map(([id], i) => (
|
|
||||||
<FlexMemberAttribute
|
|
||||||
key={`ma-${id}`}
|
|
||||||
defaultAttribute={attributes[i][1]}
|
|
||||||
id={id}
|
|
||||||
isLast={i === attributes.length - 1}
|
|
||||||
onAdd={onAdd}
|
|
||||||
onChange={onChange}
|
|
||||||
onRemove={onRemove}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</FormControl>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemberAttributeProps {
|
|
||||||
id: string
|
|
||||||
isLast: boolean
|
|
||||||
onAdd: FlexMemberAttributesProps['onAdd']
|
|
||||||
onChange: FlexMemberAttributesProps['onChange']
|
|
||||||
onRemove: FlexMemberAttributesProps['onRemove']
|
|
||||||
defaultAttribute: WhitelistFlexMember
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FlexMemberAttribute({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: MemberAttributeProps) {
|
|
||||||
const wallet = useWallet()
|
|
||||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
|
||||||
|
|
||||||
const htmlId = useId()
|
|
||||||
|
|
||||||
const addressState = useInputState({
|
|
||||||
id: `ma-address-${htmlId}`,
|
|
||||||
name: `ma-address-${htmlId}`,
|
|
||||||
title: `Address`,
|
|
||||||
defaultValue: defaultAttribute.address,
|
|
||||||
})
|
|
||||||
|
|
||||||
const mintCountState = useNumberInputState({
|
|
||||||
id: `mint-count-${htmlId}`,
|
|
||||||
name: `mint-count-${htmlId}`,
|
|
||||||
title: `Mint Count`,
|
|
||||||
defaultValue: defaultAttribute.mint_count,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChange(id, { address: addressState.value, mint_count: mintCountState.value })
|
|
||||||
}, [addressState.value, mintCountState.value, id])
|
|
||||||
|
|
||||||
const resolveAddress = async (name: string) => {
|
|
||||||
if (!wallet.isWalletConnected) throw new Error('Wallet not connected')
|
|
||||||
await (
|
|
||||||
await wallet.getCosmWasmClient()
|
|
||||||
)
|
|
||||||
.queryContractRaw(
|
|
||||||
SG721_NAME_ADDRESS,
|
|
||||||
toUtf8(
|
|
||||||
Buffer.from(
|
|
||||||
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
|
|
||||||
'hex',
|
|
||||||
).toString(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
|
|
||||||
if (tokenUri && isValidAddress(tokenUri)) onChange(id, { address: tokenUri, mint_count: mintCountState.value })
|
|
||||||
else {
|
|
||||||
toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
|
|
||||||
onChange(id, { address: '', mint_count: mintCountState.value })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(`Error resolving address for the name: ${name}.stars`)
|
|
||||||
console.error(err)
|
|
||||||
onChange(id, { address: '', mint_count: mintCountState.value })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
|
||||||
if (addressState.value.endsWith('.stars')) {
|
|
||||||
void resolveAddress(addressState.value.split('.')[0])
|
|
||||||
} else {
|
|
||||||
onChange(id, {
|
|
||||||
address: addressState.value,
|
|
||||||
mint_count: mintCountState.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [addressState.value, id])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid relative md:grid-cols-[50%_43%_7%] lg:grid-cols-[65%_28%_7%] 2xl:grid-cols-[70%_23%_7%] 2xl:space-x-2">
|
|
||||||
<AddressInput {...addressState} />
|
|
||||||
<NumberInput className="ml-2" {...mintCountState} />
|
|
||||||
|
|
||||||
<div className="flex justify-end items-end pb-2 w-8">
|
|
||||||
<button
|
|
||||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
isLast ? onAdd() : onRemove(id)
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Icon className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -69,26 +69,6 @@ export const TextInput = forwardRef<HTMLInputElement, FormInputProps>(
|
|||||||
//
|
//
|
||||||
)
|
)
|
||||||
|
|
||||||
export const CheckBoxInput = forwardRef<HTMLInputElement, FormInputProps>(
|
|
||||||
function CheckBoxInput(props, ref) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<label className="flex flex-col space-y-1" htmlFor="explicit">
|
|
||||||
<span className="font-bold first-letter:capitalize">Explicit Content</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
|
|
||||||
id="explicit"
|
|
||||||
name="explicit"
|
|
||||||
type="checkbox"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
//
|
|
||||||
)
|
|
||||||
|
|
||||||
export const UrlInput = forwardRef<HTMLInputElement, FormInputProps>(
|
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" />
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react'
|
|
||||||
import { uid } from 'utils/random'
|
|
||||||
|
|
||||||
import type { Attribute } from './MemberAttributes'
|
|
||||||
|
|
||||||
export function useMemberAttributesState() {
|
|
||||||
const [record, setRecord] = useState<Record<string, Attribute>>(() => ({}))
|
|
||||||
|
|
||||||
const entries = useMemo(() => Object.entries(record), [record])
|
|
||||||
const values = useMemo(() => Object.values(record), [record])
|
|
||||||
|
|
||||||
function add(attribute: Attribute = { address: '', weight: 0 }) {
|
|
||||||
setRecord((prev) => ({ ...prev, [uid()]: attribute }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(key: string, attribute = record[key]) {
|
|
||||||
setRecord((prev) => ({ ...prev, [key]: attribute }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(key: string) {
|
|
||||||
return setRecord((prev) => {
|
|
||||||
const latest = { ...prev }
|
|
||||||
delete latest[key]
|
|
||||||
return latest
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
setRecord({})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { entries, values, add, update, remove, reset }
|
|
||||||
}
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
|
||||||
import { useEffect, useId, useMemo } from 'react'
|
|
||||||
import { FaMinus, FaPlus } from 'react-icons/fa'
|
|
||||||
|
|
||||||
import { useInputState, useNumberInputState } from './FormInput.hooks'
|
|
||||||
|
|
||||||
export interface Attribute {
|
|
||||||
address: string
|
|
||||||
weight: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemberAttributesProps {
|
|
||||||
title: string
|
|
||||||
subtitle?: string
|
|
||||||
isRequired?: boolean
|
|
||||||
attributes: [string, Attribute][]
|
|
||||||
onAdd: () => void
|
|
||||||
onChange: (key: string, attribute: Attribute) => void
|
|
||||||
onRemove: (key: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MemberAttributes(props: MemberAttributesProps) {
|
|
||||||
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
|
|
||||||
|
|
||||||
const calculateMemberPercent = (id: string) => {
|
|
||||||
const total = attributes.reduce((acc, [, { weight }]) => acc + (weight ? weight : 0), 0)
|
|
||||||
// return attributes.map(([id, { weight }]) => [id, weight / total])
|
|
||||||
const memberWeight = attributes.find(([memberId]) => memberId === id)?.[1].weight
|
|
||||||
return memberWeight ? memberWeight / total : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
|
||||||
{attributes.map(([id], i) => (
|
|
||||||
<MemberAttribute
|
|
||||||
key={`ma-${id}`}
|
|
||||||
defaultAttribute={attributes[i][1]}
|
|
||||||
id={id}
|
|
||||||
isLast={i === attributes.length - 1}
|
|
||||||
onAdd={onAdd}
|
|
||||||
onChange={onChange}
|
|
||||||
onRemove={onRemove}
|
|
||||||
percentage={(Number(calculateMemberPercent(id)) * 100).toFixed(2)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</FormControl>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemberAttributeProps {
|
|
||||||
id: string
|
|
||||||
isLast: boolean
|
|
||||||
onAdd: MemberAttributesProps['onAdd']
|
|
||||||
onChange: MemberAttributesProps['onChange']
|
|
||||||
onRemove: MemberAttributesProps['onRemove']
|
|
||||||
defaultAttribute: Attribute
|
|
||||||
percentage?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MemberAttribute({
|
|
||||||
id,
|
|
||||||
isLast,
|
|
||||||
onAdd,
|
|
||||||
onChange,
|
|
||||||
onRemove,
|
|
||||||
defaultAttribute,
|
|
||||||
percentage,
|
|
||||||
}: MemberAttributeProps) {
|
|
||||||
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
|
|
||||||
|
|
||||||
const htmlId = useId()
|
|
||||||
|
|
||||||
const addressState = useInputState({
|
|
||||||
id: `ma-address-${htmlId}`,
|
|
||||||
name: `ma-address-${htmlId}`,
|
|
||||||
title: `Address`,
|
|
||||||
defaultValue: defaultAttribute.address,
|
|
||||||
})
|
|
||||||
|
|
||||||
const weightState = useNumberInputState({
|
|
||||||
id: `ma-weight-${htmlId}`,
|
|
||||||
name: `ma-weight-${htmlId}`,
|
|
||||||
title: `Weight ${percentage ? `: ${percentage}%` : ''}`,
|
|
||||||
defaultValue: defaultAttribute.weight,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChange(id, { address: addressState.value, weight: weightState.value })
|
|
||||||
}, [addressState.value, weightState.value, id])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid relative md:grid-cols-[50%_43%_7%] lg:grid-cols-[65%_28%_7%] 2xl:grid-cols-[70%_23%_7%] 2xl:space-x-2">
|
|
||||||
<AddressInput {...addressState} />
|
|
||||||
<NumberInput {...weightState} />
|
|
||||||
|
|
||||||
<div className="flex justify-end items-end pb-2 w-8">
|
|
||||||
<button
|
|
||||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
isLast ? onAdd() : onRemove(id)
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Icon className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -73,13 +73,12 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
|
|||||||
}, [traitTypeState.value, traitValueState.value, id])
|
}, [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)
|
||||||
|
|||||||
@ -1,400 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
/* eslint-disable jsx-a11y/media-has-caption */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { FormGroup } from 'components/FormGroup'
|
|
||||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { InputDateTime } from 'components/InputDateTime'
|
|
||||||
import { Tooltip } from 'components/Tooltip'
|
|
||||||
import { useGlobalSettings } from 'contexts/globalSettings'
|
|
||||||
import { addLogItem } from 'contexts/log'
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS, SG721_OPEN_EDITION_UPDATABLE_CODE_ID } from 'utils/constants'
|
|
||||||
import { getAssetType } from 'utils/getAssetType'
|
|
||||||
import { uid } from 'utils/random'
|
|
||||||
|
|
||||||
import { TextInput } from '../forms/FormInput'
|
|
||||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
|
||||||
import type { MetadataStorageMethod } from './OpenEditionMinterCreator'
|
|
||||||
|
|
||||||
interface CollectionDetailsProps {
|
|
||||||
onChange: (data: CollectionDetailsDataProps) => void
|
|
||||||
uploadMethod: UploadMethod
|
|
||||||
coverImageUrl: string
|
|
||||||
metadataStorageMethod: MetadataStorageMethod
|
|
||||||
importedCollectionDetails?: CollectionDetailsDataProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CollectionDetailsDataProps {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
symbol: string
|
|
||||||
imageFile: File[]
|
|
||||||
externalLink?: string
|
|
||||||
startTradingTime?: string
|
|
||||||
explicit: boolean
|
|
||||||
updatable: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CollectionDetails = ({
|
|
||||||
onChange,
|
|
||||||
uploadMethod,
|
|
||||||
metadataStorageMethod,
|
|
||||||
coverImageUrl,
|
|
||||||
importedCollectionDetails,
|
|
||||||
}: CollectionDetailsProps) => {
|
|
||||||
const [coverImage, setCoverImage] = useState<File | null>(null)
|
|
||||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
|
||||||
const [explicit, setExplicit] = useState<boolean>(false)
|
|
||||||
const [updatable, setUpdatable] = useState<boolean>(false)
|
|
||||||
const { timezone } = useGlobalSettings()
|
|
||||||
|
|
||||||
const initialRender = useRef(true)
|
|
||||||
|
|
||||||
const coverImageInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const nameState = useInputState({
|
|
||||||
id: 'name',
|
|
||||||
name: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
placeholder: 'My Awesome Collection',
|
|
||||||
})
|
|
||||||
|
|
||||||
const descriptionState = useInputState({
|
|
||||||
id: 'description',
|
|
||||||
name: 'description',
|
|
||||||
title: 'Description',
|
|
||||||
placeholder: 'My Awesome Collection Description',
|
|
||||||
})
|
|
||||||
|
|
||||||
const symbolState = useInputState({
|
|
||||||
id: 'symbol',
|
|
||||||
name: 'symbol',
|
|
||||||
title: 'Symbol',
|
|
||||||
placeholder: 'SYMBOL',
|
|
||||||
})
|
|
||||||
|
|
||||||
const externalLinkState = useInputState({
|
|
||||||
id: 'external-link',
|
|
||||||
name: 'externalLink',
|
|
||||||
title: 'External Link (optional)',
|
|
||||||
placeholder: 'https://my-collection...',
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const data: CollectionDetailsDataProps = {
|
|
||||||
name: nameState.value,
|
|
||||||
description: descriptionState.value,
|
|
||||||
symbol: symbolState.value,
|
|
||||||
imageFile: coverImage ? [coverImage] : [],
|
|
||||||
externalLink: externalLinkState.value || undefined,
|
|
||||||
startTradingTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
|
||||||
explicit,
|
|
||||||
updatable,
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
||||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
nameState.value,
|
|
||||||
descriptionState.value,
|
|
||||||
symbolState.value,
|
|
||||||
externalLinkState.value,
|
|
||||||
coverImage,
|
|
||||||
timestamp,
|
|
||||||
explicit,
|
|
||||||
updatable,
|
|
||||||
])
|
|
||||||
|
|
||||||
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (event.target.files === null) return toast.error('Error selecting cover image')
|
|
||||||
if (event.target.files.length === 0) {
|
|
||||||
setCoverImage(null)
|
|
||||||
return toast.error('No files selected.')
|
|
||||||
}
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
if (!event.target.files) return toast.error('No files selected.')
|
|
||||||
const imageFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), {
|
|
||||||
type: 'image/jpg',
|
|
||||||
})
|
|
||||||
setCoverImage(imageFile)
|
|
||||||
}
|
|
||||||
reader.readAsArrayBuffer(event.target.files[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCoverImage(null)
|
|
||||||
// empty the element so that the same file can be selected again
|
|
||||||
if (coverImageInputRef.current) coverImageInputRef.current.value = ''
|
|
||||||
}, [metadataStorageMethod])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialRender.current) {
|
|
||||||
initialRender.current = false
|
|
||||||
} else if (updatable) {
|
|
||||||
toast.success('Token metadata will be updatable upon collection creation.', {
|
|
||||||
style: { maxWidth: 'none' },
|
|
||||||
icon: '✅📝',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
toast.error('Token metadata will not be updatable upon collection creation.', {
|
|
||||||
style: { maxWidth: 'none' },
|
|
||||||
icon: '⛔🔏',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [updatable])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (importedCollectionDetails) {
|
|
||||||
nameState.onChange(importedCollectionDetails.name)
|
|
||||||
descriptionState.onChange(importedCollectionDetails.description)
|
|
||||||
symbolState.onChange(importedCollectionDetails.symbol)
|
|
||||||
//setCoverImage(importedCollectionDetails.imageFile[0] || null)
|
|
||||||
externalLinkState.onChange(importedCollectionDetails.externalLink || '')
|
|
||||||
setTimestamp(
|
|
||||||
importedCollectionDetails.startTradingTime
|
|
||||||
? new Date(parseInt(importedCollectionDetails.startTradingTime) / 1_000_000)
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
setExplicit(importedCollectionDetails.explicit)
|
|
||||||
setUpdatable(importedCollectionDetails.updatable)
|
|
||||||
}
|
|
||||||
}, [importedCollectionDetails])
|
|
||||||
|
|
||||||
const videoPreview = useMemo(() => {
|
|
||||||
if (uploadMethod === 'new' && coverImage) {
|
|
||||||
return (
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
id="video"
|
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
|
||||||
src={URL.createObjectURL(coverImage)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else if (uploadMethod === 'existing' && coverImageUrl && coverImageUrl.includes('ipfs://')) {
|
|
||||||
return (
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
id="video"
|
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
|
||||||
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
|
|
||||||
coverImageUrl.lastIndexOf('ipfs://') + 7,
|
|
||||||
)}`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else if (uploadMethod === 'existing' && coverImageUrl && !coverImageUrl.includes('ipfs://')) {
|
|
||||||
return (
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
id="video"
|
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
|
||||||
src={coverImageUrl}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [coverImage, coverImageUrl, uploadMethod])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormGroup subtitle="Information about your collection" title="Collection Details">
|
|
||||||
<div className={clsx('')}>
|
|
||||||
<div className="">
|
|
||||||
<TextInput {...nameState} isRequired />
|
|
||||||
<TextInput className="mt-2" {...descriptionState} isRequired />
|
|
||||||
<TextInput className="mt-2" {...symbolState} isRequired />
|
|
||||||
</div>
|
|
||||||
<div className={clsx('')}>
|
|
||||||
<TextInput className={clsx('mt-2')} {...externalLinkState} />
|
|
||||||
{/* Currently trading starts immediately for 1/1 Collections */}
|
|
||||||
|
|
||||||
<FormControl
|
|
||||||
className={clsx('mt-2')}
|
|
||||||
htmlId="timestamp"
|
|
||||||
subtitle="Trading start time offset will be set as 1 week by default."
|
|
||||||
title={`Trading Start Time (optional | ${timezone === 'Local' ? 'local)' : 'UTC)'}`}
|
|
||||||
>
|
|
||||||
<InputDateTime
|
|
||||||
minDate={
|
|
||||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
}
|
|
||||||
onChange={(date) =>
|
|
||||||
date
|
|
||||||
? setTimestamp(
|
|
||||||
timezone === 'Local'
|
|
||||||
? date
|
|
||||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
|
||||||
)
|
|
||||||
: setTimestamp(undefined)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
timezone === 'Local'
|
|
||||||
? timestamp
|
|
||||||
: timestamp
|
|
||||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormControl className={clsx('')} isRequired={uploadMethod === 'new'} title="Cover Image">
|
|
||||||
{uploadMethod === 'new' && (
|
|
||||||
<input
|
|
||||||
accept="image/*, video/*"
|
|
||||||
className={clsx(
|
|
||||||
'w-full',
|
|
||||||
'p-[13px] rounded border-2 border-white/20 border-dashed cursor-pointer h-18',
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="cover-image"
|
|
||||||
onChange={selectCoverImage}
|
|
||||||
ref={coverImageInputRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Conditional
|
|
||||||
test={coverImage !== null && uploadMethod === 'new' && getAssetType(coverImage.name) === 'image'}
|
|
||||||
>
|
|
||||||
{coverImage !== null && (
|
|
||||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
|
||||||
<img alt="no-preview-available" src={URL.createObjectURL(coverImage)} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Conditional>
|
|
||||||
<Conditional
|
|
||||||
test={coverImage !== null && uploadMethod === 'new' && getAssetType(coverImage.name) === 'video'}
|
|
||||||
>
|
|
||||||
{coverImage !== null && videoPreview}
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
{uploadMethod === 'existing' && coverImageUrl?.includes('ipfs://') && (
|
|
||||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
|
||||||
<Conditional test={getAssetType(coverImageUrl) !== 'video'}>
|
|
||||||
<img
|
|
||||||
alt="no-preview-available"
|
|
||||||
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
|
|
||||||
coverImageUrl.lastIndexOf('ipfs://') + 7,
|
|
||||||
)}`}
|
|
||||||
/>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={getAssetType(coverImageUrl) === 'video'}>{videoPreview}</Conditional>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{uploadMethod === 'existing' && coverImageUrl && !coverImageUrl?.includes('ipfs://') && (
|
|
||||||
<div>
|
|
||||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
|
||||||
<Conditional test={getAssetType(coverImageUrl) !== 'video'}>
|
|
||||||
<img alt="no-preview-available" src={coverImageUrl} />
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={getAssetType(coverImageUrl) === 'video'}>{videoPreview}</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{uploadMethod === 'existing' && !coverImageUrl && (
|
|
||||||
<span className="italic font-light ">Waiting for cover image URL to be specified.</span>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<div className={clsx('flex flex-col space-y-2')}>
|
|
||||||
<div>
|
|
||||||
<div className="flex mt-4">
|
|
||||||
<span className="mt-1 ml-[2px] text-sm first-letter:capitalize">
|
|
||||||
Does the collection contain explicit content?
|
|
||||||
</span>
|
|
||||||
<div className="ml-2 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={explicit}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="explicitRadio1"
|
|
||||||
name="explicitRadioOptions1"
|
|
||||||
onClick={() => {
|
|
||||||
setExplicit(true)
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="explicitRadio1"
|
|
||||||
>
|
|
||||||
YES
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="ml-2 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={!explicit}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="explicitRadio2"
|
|
||||||
name="explicitRadioOptions2"
|
|
||||||
onClick={() => {
|
|
||||||
setExplicit(false)
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="explicitRadio2"
|
|
||||||
>
|
|
||||||
NO
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Conditional
|
|
||||||
test={
|
|
||||||
false && SG721_OPEN_EDITION_UPDATABLE_CODE_ID > 0 && OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS !== undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
backgroundColor="bg-blue-500"
|
|
||||||
label={
|
|
||||||
<div className="grid grid-flow-row">
|
|
||||||
<span>
|
|
||||||
ℹ️ When enabled, the metadata for tokens can be updated after the collection is created until the
|
|
||||||
collection is frozen by the creator.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<div className={clsx('flex flex-col space-y-2 w-full form-control')}>
|
|
||||||
<label className="justify-start cursor-pointer label">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="mr-4 font-bold">Updatable Token Metadata</span>
|
|
||||||
<span className="mr-4">(Price: 2000 STARS)</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
checked={updatable}
|
|
||||||
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
|
|
||||||
onClick={() => setUpdatable(!updatable)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</Conditional>
|
|
||||||
</FormGroup>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,454 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable jsx-a11y/media-has-caption */
|
|
||||||
|
|
||||||
/* eslint-disable no-misleading-character-class */
|
|
||||||
/* eslint-disable no-control-regex */
|
|
||||||
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Anchor } from 'components/Anchor'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { TextInput } from 'components/forms/FormInput'
|
|
||||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import type { UploadServiceType } from 'services/upload'
|
|
||||||
import { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
|
|
||||||
import type { AssetType } from 'utils/getAssetType'
|
|
||||||
import { getAssetType } from 'utils/getAssetType'
|
|
||||||
|
|
||||||
export type UploadMethod = 'new' | 'existing'
|
|
||||||
|
|
||||||
interface ImageUploadDetailsProps {
|
|
||||||
onChange: (value: ImageUploadDetailsDataProps) => void
|
|
||||||
importedImageUploadDetails?: ImageUploadDetailsDataProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImageUploadDetailsDataProps {
|
|
||||||
assetFile: File | undefined
|
|
||||||
thumbnailFile?: File | undefined
|
|
||||||
isThumbnailCompatible?: boolean
|
|
||||||
uploadService: UploadServiceType
|
|
||||||
nftStorageApiKey?: string
|
|
||||||
pinataApiKey?: string
|
|
||||||
pinataSecretKey?: string
|
|
||||||
uploadMethod: UploadMethod
|
|
||||||
imageUrl?: string
|
|
||||||
coverImageUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: ImageUploadDetailsProps) => {
|
|
||||||
const [assetFile, setAssetFile] = useState<File>()
|
|
||||||
const [thumbnailFile, setThumbnailFile] = useState<File>()
|
|
||||||
const [isThumbnailCompatible, setIsThumbnailCompatible] = useState<boolean>(false)
|
|
||||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
|
||||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
|
||||||
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
|
|
||||||
|
|
||||||
const assetFileRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
const thumbnailFileRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
|
|
||||||
const nftStorageApiKeyState = useInputState({
|
|
||||||
id: 'nft-storage-api-key',
|
|
||||||
name: 'nftStorageApiKey',
|
|
||||||
title: 'NFT.Storage API Key',
|
|
||||||
placeholder: 'Enter NFT.Storage API Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
const pinataApiKeyState = useInputState({
|
|
||||||
id: 'pinata-api-key',
|
|
||||||
name: 'pinataApiKey',
|
|
||||||
title: 'Pinata API Key',
|
|
||||||
placeholder: 'Enter Pinata API Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
const pinataSecretKeyState = useInputState({
|
|
||||||
id: 'pinata-secret-key',
|
|
||||||
name: 'pinataSecretKey',
|
|
||||||
title: 'Pinata Secret Key',
|
|
||||||
placeholder: 'Enter Pinata Secret Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const imageUrlState = useInputState({
|
|
||||||
id: 'imageUrl',
|
|
||||||
name: 'imageUrl',
|
|
||||||
title: 'Asset URL',
|
|
||||||
placeholder: 'ipfs://',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const coverImageUrlState = useInputState({
|
|
||||||
id: 'coverImageUrl',
|
|
||||||
name: 'coverImageUrl',
|
|
||||||
title: 'Cover Image URL',
|
|
||||||
placeholder: 'ipfs://',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html']
|
|
||||||
|
|
||||||
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setAssetFile(undefined)
|
|
||||||
setThumbnailFile(undefined)
|
|
||||||
setIsThumbnailCompatible(false)
|
|
||||||
if (event.target.files === null) return
|
|
||||||
|
|
||||||
let selectedFile: File
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/jpg' })
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
|
||||||
else return toast.error('No file selected.')
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
|
|
||||||
setIsThumbnailCompatible(true)
|
|
||||||
}
|
|
||||||
setAssetFile(selectedFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setThumbnailFile(undefined)
|
|
||||||
if (event.target.files === null) return
|
|
||||||
|
|
||||||
let selectedFile: File
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/*' })
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
|
||||||
else return toast.error('No file selected.')
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
setThumbnailFile(selectedFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex =
|
|
||||||
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const data: ImageUploadDetailsDataProps = {
|
|
||||||
assetFile,
|
|
||||||
thumbnailFile,
|
|
||||||
isThumbnailCompatible,
|
|
||||||
uploadService,
|
|
||||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
|
||||||
pinataApiKey: pinataApiKeyState.value,
|
|
||||||
pinataSecretKey: pinataSecretKeyState.value,
|
|
||||||
uploadMethod,
|
|
||||||
imageUrl: imageUrlState.value
|
|
||||||
.replace('IPFS://', 'ipfs://')
|
|
||||||
.replace(/,/g, '')
|
|
||||||
.replace(/"/g, '')
|
|
||||||
.replace(/'/g, '')
|
|
||||||
.replace(regex, '')
|
|
||||||
.trim(),
|
|
||||||
coverImageUrl: coverImageUrlState.value.trim(),
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
assetFile,
|
|
||||||
thumbnailFile,
|
|
||||||
isThumbnailCompatible,
|
|
||||||
uploadService,
|
|
||||||
nftStorageApiKeyState.value,
|
|
||||||
pinataApiKeyState.value,
|
|
||||||
pinataSecretKeyState.value,
|
|
||||||
uploadMethod,
|
|
||||||
imageUrlState.value,
|
|
||||||
coverImageUrlState.value,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (assetFileRef.current) assetFileRef.current.value = ''
|
|
||||||
setAssetFile(undefined)
|
|
||||||
setThumbnailFile(undefined)
|
|
||||||
setIsThumbnailCompatible(false)
|
|
||||||
imageUrlState.onChange('')
|
|
||||||
}, [uploadMethod])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (importedImageUploadDetails) {
|
|
||||||
setUploadMethod(importedImageUploadDetails.uploadMethod)
|
|
||||||
setUploadService(importedImageUploadDetails.uploadService)
|
|
||||||
nftStorageApiKeyState.onChange(importedImageUploadDetails.nftStorageApiKey || '')
|
|
||||||
pinataApiKeyState.onChange(importedImageUploadDetails.pinataApiKey || '')
|
|
||||||
pinataSecretKeyState.onChange(importedImageUploadDetails.pinataSecretKey || '')
|
|
||||||
imageUrlState.onChange(importedImageUploadDetails.imageUrl || '')
|
|
||||||
coverImageUrlState.onChange(importedImageUploadDetails.coverImageUrl || '')
|
|
||||||
}
|
|
||||||
}, [importedImageUploadDetails])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (useDefaultApiKey) {
|
|
||||||
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
|
|
||||||
} else {
|
|
||||||
nftStorageApiKeyState.onChange('')
|
|
||||||
}
|
|
||||||
}, [useDefaultApiKey])
|
|
||||||
|
|
||||||
const previewUrl = imageUrlState.value.toLowerCase().trim().startsWith('ipfs://')
|
|
||||||
? `https://ipfs-gw.stargaze-apis.com/ipfs/${imageUrlState.value.substring(7)}`
|
|
||||||
: imageUrlState.value
|
|
||||||
|
|
||||||
const audioPreview = useMemo(
|
|
||||||
() => (
|
|
||||||
<audio
|
|
||||||
controls
|
|
||||||
id="audio"
|
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
|
||||||
src={imageUrlState.value ? previewUrl : ''}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[imageUrlState.value],
|
|
||||||
)
|
|
||||||
|
|
||||||
const videoPreview = useMemo(
|
|
||||||
() => (
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
id="video"
|
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
|
||||||
src={imageUrlState.value ? previewUrl : ''}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[imageUrlState.value],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadMethod === 'new'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio2"
|
|
||||||
name="inlineRadioOptions2"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadMethod('new')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="New"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio2"
|
|
||||||
>
|
|
||||||
Upload New Asset
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadMethod === 'existing'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio1"
|
|
||||||
name="inlineRadioOptions1"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadMethod('existing')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="Existing"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio1"
|
|
||||||
>
|
|
||||||
Use an existing Asset URL
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 py-5 pb-4">
|
|
||||||
<Conditional test={uploadMethod === 'existing'}>
|
|
||||||
<div className="ml-3 flex-column">
|
|
||||||
<p className="mb-5 ml-5">
|
|
||||||
Though the Open Edition contracts allow for off-chain asset storage, it is recommended to use a
|
|
||||||
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
|
|
||||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
|
||||||
NFT.Storage
|
|
||||||
</Anchor>{' '}
|
|
||||||
or{' '}
|
|
||||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
|
||||||
Pinata
|
|
||||||
</Anchor>{' '}
|
|
||||||
and upload your asset manually to get an asset URL for your NFT.
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-row w-full">
|
|
||||||
<div className="flex flex-col w-3/5">
|
|
||||||
<TextInput {...imageUrlState} className="mt-2 ml-6" />
|
|
||||||
<TextInput {...coverImageUrlState} className="mt-4 ml-6" />
|
|
||||||
</div>
|
|
||||||
<Conditional test={imageUrlState.value !== ''}>
|
|
||||||
<div className="flex mt-2 ml-8 w-1/3 border-2 border-dashed">
|
|
||||||
{getAssetType(imageUrlState.value) === 'audio' && audioPreview}
|
|
||||||
{getAssetType(imageUrlState.value) === 'video' && videoPreview}
|
|
||||||
{getAssetType(imageUrlState.value) === 'image' && <img alt="asset-preview" src={previewUrl} />}
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
<Conditional test={uploadMethod === 'new'}>
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col items-center px-8 w-full">
|
|
||||||
<div className="flex justify-items-start mb-5 w-full font-bold">
|
|
||||||
<div className="form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadService === 'nft-storage'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio3"
|
|
||||||
name="inlineRadioOptions3"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadService('nft-storage')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="nft-storage"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio3"
|
|
||||||
>
|
|
||||||
Upload using NFT.Storage
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-2 form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadService === 'pinata'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio4"
|
|
||||||
name="inlineRadioOptions4"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadService('pinata')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="pinata"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio4"
|
|
||||||
>
|
|
||||||
Upload using Pinata
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full">
|
|
||||||
<Conditional test={uploadService === 'nft-storage'}>
|
|
||||||
<div className="flex-col w-full">
|
|
||||||
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
|
|
||||||
<div className="flex-row mt-2 w-full form-control">
|
|
||||||
<label className="cursor-pointer label">
|
|
||||||
<span className="mr-2 font-bold">Use Default API Key</span>
|
|
||||||
<input
|
|
||||||
checked={useDefaultApiKey}
|
|
||||||
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
|
||||||
onClick={() => {
|
|
||||||
setUseDefaultApiKey(!useDefaultApiKey)
|
|
||||||
}}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={uploadService === 'pinata'}>
|
|
||||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
|
||||||
<div className="w-[20px]" />
|
|
||||||
<TextInput {...pinataSecretKeyState} className="w-full" />
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="grid grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<div className="w-full">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
|
||||||
htmlFor="assetFile"
|
|
||||||
>
|
|
||||||
Asset Selection
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="image/*, audio/*, video/*, .html, .pdf"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="assetFile"
|
|
||||||
onChange={selectAsset}
|
|
||||||
ref={assetFileRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Conditional test={isThumbnailCompatible}>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
|
||||||
htmlFor="thumbnailFile"
|
|
||||||
>
|
|
||||||
Thumbnail Selection (optional)
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="image/*"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="thumbnailFile"
|
|
||||||
onChange={selectThumbnail}
|
|
||||||
ref={thumbnailFileRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Conditional test={assetFile !== undefined}>
|
|
||||||
<SingleAssetPreview
|
|
||||||
relatedAsset={assetFile}
|
|
||||||
subtitle={`Asset filename: ${assetFile?.name as string}`}
|
|
||||||
/>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,280 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { FormGroup } from 'components/FormGroup'
|
|
||||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { InputDateTime } from 'components/InputDateTime'
|
|
||||||
import { openEditionMinterList } from 'config/minter'
|
|
||||||
import type { TokenInfo } from 'config/token'
|
|
||||||
import { stars, tokensList } from 'config/token'
|
|
||||||
import { useGlobalSettings } from 'contexts/globalSettings'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { resolveAddress } from 'utils/resolveAddress'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { NumberInput, TextInput } from '../forms/FormInput'
|
|
||||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
|
||||||
|
|
||||||
export type LimitType = 'count_limited' | 'time_limited' | 'time_and_count_limited'
|
|
||||||
|
|
||||||
interface MintingDetailsProps {
|
|
||||||
onChange: (data: MintingDetailsDataProps) => void
|
|
||||||
uploadMethod: UploadMethod
|
|
||||||
minimumMintPrice: number
|
|
||||||
mintTokenFromFactory?: TokenInfo | undefined
|
|
||||||
importedMintingDetails?: MintingDetailsDataProps
|
|
||||||
isPresale: boolean
|
|
||||||
whitelistStartDate?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MintingDetailsDataProps {
|
|
||||||
unitPrice: string
|
|
||||||
perAddressLimit: number
|
|
||||||
startTime: string
|
|
||||||
endTime?: string
|
|
||||||
tokenCountLimit?: number
|
|
||||||
paymentAddress?: string
|
|
||||||
selectedMintToken?: TokenInfo
|
|
||||||
limitType: LimitType
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MintingDetails = ({
|
|
||||||
onChange,
|
|
||||||
uploadMethod,
|
|
||||||
minimumMintPrice,
|
|
||||||
mintTokenFromFactory,
|
|
||||||
importedMintingDetails,
|
|
||||||
isPresale,
|
|
||||||
whitelistStartDate,
|
|
||||||
}: MintingDetailsProps) => {
|
|
||||||
const wallet = useWallet()
|
|
||||||
|
|
||||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
|
||||||
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>()
|
|
||||||
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
|
|
||||||
const [mintingDetailsImported, setMintingDetailsImported] = useState(false)
|
|
||||||
const [limitType, setLimitType] = useState<LimitType>('time_limited')
|
|
||||||
const { timezone } = useGlobalSettings()
|
|
||||||
|
|
||||||
const unitPriceState = useNumberInputState({
|
|
||||||
id: 'unitPrice',
|
|
||||||
name: 'unitPrice',
|
|
||||||
title: 'Mint Price',
|
|
||||||
subtitle: `Price of each token (min. ${minimumMintPrice} ${
|
|
||||||
mintTokenFromFactory ? mintTokenFromFactory.displayName : 'STARS'
|
|
||||||
})`,
|
|
||||||
placeholder: '50',
|
|
||||||
})
|
|
||||||
|
|
||||||
const perAddressLimitState = useNumberInputState({
|
|
||||||
id: 'peraddresslimit',
|
|
||||||
name: 'peraddresslimit',
|
|
||||||
title: 'Per Address Limit',
|
|
||||||
subtitle: '',
|
|
||||||
placeholder: '1',
|
|
||||||
})
|
|
||||||
|
|
||||||
const tokenCountLimitState = useNumberInputState({
|
|
||||||
id: 'tokencountlimit',
|
|
||||||
name: 'tokencountlimit',
|
|
||||||
title: 'Maximum Token Count',
|
|
||||||
subtitle: 'Total number of mintable tokens',
|
|
||||||
placeholder: '100',
|
|
||||||
})
|
|
||||||
|
|
||||||
const paymentAddressState = useInputState({
|
|
||||||
id: 'payment-address',
|
|
||||||
name: 'paymentAddress',
|
|
||||||
title: 'Payment Address (optional)',
|
|
||||||
subtitle: 'Address to receive minting revenues (defaults to current wallet address)',
|
|
||||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
|
||||||
})
|
|
||||||
|
|
||||||
const resolvePaymentAddress = async () => {
|
|
||||||
await resolveAddress(paymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
|
|
||||||
paymentAddressState.onChange(resolvedAddress)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!importedMintingDetails || (importedMintingDetails && mintingDetailsImported)) {
|
|
||||||
void resolvePaymentAddress()
|
|
||||||
}
|
|
||||||
}, [paymentAddressState.value])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const data: MintingDetailsDataProps = {
|
|
||||||
unitPrice: unitPriceState.value
|
|
||||||
? (Number(unitPriceState.value) * 1_000_000).toString()
|
|
||||||
: unitPriceState.value === 0
|
|
||||||
? '0'
|
|
||||||
: '',
|
|
||||||
perAddressLimit: perAddressLimitState.value,
|
|
||||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
|
||||||
endTime:
|
|
||||||
limitType === 'time_limited' || limitType === 'time_and_count_limited'
|
|
||||||
? endTimestamp
|
|
||||||
? (endTimestamp.getTime() * 1_000_000).toString()
|
|
||||||
: ''
|
|
||||||
: undefined,
|
|
||||||
paymentAddress: paymentAddressState.value.trim(),
|
|
||||||
selectedMintToken,
|
|
||||||
limitType,
|
|
||||||
tokenCountLimit:
|
|
||||||
limitType === 'count_limited' || limitType === 'time_and_count_limited'
|
|
||||||
? tokenCountLimitState.value
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
unitPriceState.value,
|
|
||||||
perAddressLimitState.value,
|
|
||||||
timestamp,
|
|
||||||
endTimestamp,
|
|
||||||
paymentAddressState.value,
|
|
||||||
selectedMintToken,
|
|
||||||
tokenCountLimitState.value,
|
|
||||||
limitType,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (importedMintingDetails) {
|
|
||||||
console.log('Selected Token ID: ', importedMintingDetails.selectedMintToken?.id)
|
|
||||||
unitPriceState.onChange(Number(importedMintingDetails.unitPrice) / 1000000)
|
|
||||||
perAddressLimitState.onChange(importedMintingDetails.perAddressLimit)
|
|
||||||
setLimitType(importedMintingDetails.limitType)
|
|
||||||
tokenCountLimitState.onChange(importedMintingDetails.tokenCountLimit ? importedMintingDetails.tokenCountLimit : 0)
|
|
||||||
setTimestamp(new Date(Number(importedMintingDetails.startTime) / 1_000_000))
|
|
||||||
setEndTimestamp(new Date(Number(importedMintingDetails.endTime) / 1_000_000))
|
|
||||||
paymentAddressState.onChange(importedMintingDetails.paymentAddress ? importedMintingDetails.paymentAddress : '')
|
|
||||||
setSelectedMintToken(tokensList.find((token) => token.id === importedMintingDetails.selectedMintToken?.id))
|
|
||||||
setMintingDetailsImported(true)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [importedMintingDetails])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isPresale) {
|
|
||||||
setTimestamp(whitelistStartDate ? new Date(Number(whitelistStartDate) / 1_000_000) : undefined)
|
|
||||||
}
|
|
||||||
}, [whitelistStartDate, isPresale])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border-l-[1px] border-gray-500 border-opacity-20">
|
|
||||||
<FormGroup subtitle="Information about your minting settings" title="Minting Details">
|
|
||||||
<div className="flex flex-row items-end">
|
|
||||||
<NumberInput {...unitPriceState} isRequired />
|
|
||||||
<select
|
|
||||||
className="py-[9px] px-4 ml-4 placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
|
|
||||||
onChange={(e) => setSelectedMintToken(tokensList.find((t) => t.displayName === e.target.value))}
|
|
||||||
value={selectedMintToken?.displayName}
|
|
||||||
>
|
|
||||||
{openEditionMinterList
|
|
||||||
.filter((minter) => minter.factoryAddress !== undefined && minter.updatable === false)
|
|
||||||
.map((minter) => (
|
|
||||||
<option key={minter.id} className="bg-black" value={minter.supportedToken.displayName}>
|
|
||||||
{minter.supportedToken.displayName}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NumberInput {...perAddressLimitState} isRequired />
|
|
||||||
<FormControl
|
|
||||||
htmlId="timestamp"
|
|
||||||
isRequired
|
|
||||||
subtitle={`Minting start time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
|
||||||
title="Start Time"
|
|
||||||
>
|
|
||||||
<InputDateTime
|
|
||||||
disabled={isPresale}
|
|
||||||
minDate={
|
|
||||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
}
|
|
||||||
onChange={(date) =>
|
|
||||||
date
|
|
||||||
? setTimestamp(
|
|
||||||
timezone === 'Local' ? date : new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
|
||||||
)
|
|
||||||
: setTimestamp(undefined)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
timezone === 'Local'
|
|
||||||
? timestamp
|
|
||||||
: timestamp
|
|
||||||
? new Date(timestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<div className="flex-row mt-2 w-full form-control">
|
|
||||||
<h1 className="mt-2 font-bold text-md">Limit Type: </h1>
|
|
||||||
<label className="justify-start ml-6 cursor-pointer label">
|
|
||||||
<span className="mr-2">Time</span>
|
|
||||||
<input
|
|
||||||
checked={limitType === 'time_limited' || limitType === 'time_and_count_limited'}
|
|
||||||
className={`${limitType === 'time_limited' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
|
||||||
onClick={() => {
|
|
||||||
if (limitType === 'time_and_count_limited') setLimitType('count_limited' as LimitType)
|
|
||||||
else if (limitType === 'count_limited') setLimitType('time_and_count_limited' as LimitType)
|
|
||||||
else setLimitType('count_limited' as LimitType)
|
|
||||||
}}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="justify-start ml-4 cursor-pointer label">
|
|
||||||
<span className="mr-2">Token Count</span>
|
|
||||||
<input
|
|
||||||
checked={limitType === 'count_limited' || limitType === 'time_and_count_limited'}
|
|
||||||
className={`${limitType === 'count_limited' ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
|
||||||
onClick={() => {
|
|
||||||
if (limitType === 'time_and_count_limited') setLimitType('time_limited' as LimitType)
|
|
||||||
else if (limitType === 'time_limited') setLimitType('time_and_count_limited' as LimitType)
|
|
||||||
else setLimitType('time_limited' as LimitType)
|
|
||||||
}}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<Conditional test={limitType === 'time_limited' || limitType === 'time_and_count_limited'}>
|
|
||||||
<FormControl
|
|
||||||
htmlId="endTimestamp"
|
|
||||||
isRequired
|
|
||||||
subtitle={`Minting end time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
|
||||||
title="End Time"
|
|
||||||
>
|
|
||||||
<InputDateTime
|
|
||||||
minDate={
|
|
||||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
}
|
|
||||||
onChange={(date) =>
|
|
||||||
date
|
|
||||||
? setEndTimestamp(
|
|
||||||
timezone === 'Local'
|
|
||||||
? date
|
|
||||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
|
||||||
)
|
|
||||||
: setEndTimestamp(undefined)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
timezone === 'Local'
|
|
||||||
? endTimestamp
|
|
||||||
: endTimestamp
|
|
||||||
? new Date(endTimestamp.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={limitType === 'count_limited' || limitType === 'time_and_count_limited'}>
|
|
||||||
<NumberInput {...tokenCountLimitState} isRequired />
|
|
||||||
</Conditional>
|
|
||||||
</FormGroup>
|
|
||||||
<TextInput className="pr-4 pl-4 mt-3" {...paymentAddressState} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,579 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
|
|
||||||
/* eslint-disable no-misleading-character-class */
|
|
||||||
/* eslint-disable no-control-regex */
|
|
||||||
/* eslint-disable @typescript-eslint/no-loop-func */
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Alert } from 'components/Alert'
|
|
||||||
import { Anchor } from 'components/Anchor'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { TextInput } from 'components/forms/FormInput'
|
|
||||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { MetadataInput } from 'components/MetadataInput'
|
|
||||||
import { MetadataModal } from 'components/MetadataModal'
|
|
||||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
|
||||||
import { Tooltip } from 'components/Tooltip'
|
|
||||||
import { addLogItem } from 'contexts/log'
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import type { UploadServiceType } from 'services/upload'
|
|
||||||
import { NFT_STORAGE_DEFAULT_API_KEY } from 'utils/constants'
|
|
||||||
import type { AssetType } from 'utils/getAssetType'
|
|
||||||
import { getAssetType } from 'utils/getAssetType'
|
|
||||||
import { uid } from 'utils/random'
|
|
||||||
import { naturalCompare } from 'utils/sort'
|
|
||||||
|
|
||||||
import type { MetadataStorageMethod } from './OpenEditionMinterCreator'
|
|
||||||
|
|
||||||
export type UploadMethod = 'new' | 'existing'
|
|
||||||
|
|
||||||
interface OffChainMetadataUploadDetailsProps {
|
|
||||||
onChange: (value: OffChainMetadataUploadDetailsDataProps) => void
|
|
||||||
metadataStorageMethod?: MetadataStorageMethod
|
|
||||||
importedOffChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OffChainMetadataUploadDetailsDataProps {
|
|
||||||
assetFiles: File[]
|
|
||||||
metadataFiles: File[]
|
|
||||||
thumbnailFile?: File
|
|
||||||
isThumbnailCompatible?: boolean
|
|
||||||
uploadService: UploadServiceType
|
|
||||||
nftStorageApiKey?: string
|
|
||||||
pinataApiKey?: string
|
|
||||||
pinataSecretKey?: string
|
|
||||||
uploadMethod: UploadMethod
|
|
||||||
tokenURI?: string
|
|
||||||
imageUrl?: string
|
|
||||||
openEditionMinterMetadataFile?: File
|
|
||||||
exportedMetadata?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OffChainMetadataUploadDetails = ({
|
|
||||||
onChange,
|
|
||||||
metadataStorageMethod,
|
|
||||||
importedOffChainMetadataUploadDetails,
|
|
||||||
}: OffChainMetadataUploadDetailsProps) => {
|
|
||||||
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
|
|
||||||
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
|
|
||||||
const [thumbnailFile, setThumbnailFile] = useState<File>()
|
|
||||||
const [isThumbnailCompatible, setIsThumbnailCompatible] = useState<boolean>(false)
|
|
||||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
|
||||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
|
||||||
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
|
|
||||||
const [refreshMetadata, setRefreshMetadata] = useState(false)
|
|
||||||
const [exportedMetadata, setExportedMetadata] = useState(undefined)
|
|
||||||
const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>()
|
|
||||||
const [useDefaultApiKey, setUseDefaultApiKey] = useState(false)
|
|
||||||
|
|
||||||
const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html', 'document']
|
|
||||||
|
|
||||||
const assetFilesRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
const thumbnailFilesRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
|
|
||||||
const nftStorageApiKeyState = useInputState({
|
|
||||||
id: 'nft-storage-api-key',
|
|
||||||
name: 'nftStorageApiKey',
|
|
||||||
title: 'NFT.Storage API Key',
|
|
||||||
placeholder: 'Enter NFT.Storage API Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
const pinataApiKeyState = useInputState({
|
|
||||||
id: 'pinata-api-key',
|
|
||||||
name: 'pinataApiKey',
|
|
||||||
title: 'Pinata API Key',
|
|
||||||
placeholder: 'Enter Pinata API Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
const pinataSecretKeyState = useInputState({
|
|
||||||
id: 'pinata-secret-key',
|
|
||||||
name: 'pinataSecretKey',
|
|
||||||
title: 'Pinata Secret Key',
|
|
||||||
placeholder: 'Enter Pinata Secret Key',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const tokenUriState = useInputState({
|
|
||||||
id: 'tokenUri',
|
|
||||||
name: 'tokenUri',
|
|
||||||
title: 'Token URI',
|
|
||||||
placeholder: 'ipfs://',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const coverImageUrlState = useInputState({
|
|
||||||
id: 'coverImageUrl',
|
|
||||||
name: 'coverImageUrl',
|
|
||||||
title: 'Cover Image URL',
|
|
||||||
placeholder: 'ipfs://',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setAssetFilesArray([])
|
|
||||||
setMetadataFilesArray([])
|
|
||||||
setThumbnailFile(undefined)
|
|
||||||
setIsThumbnailCompatible(false)
|
|
||||||
if (event.target.files === null) return
|
|
||||||
if (thumbnailCompatibleAssetTypes.includes(getAssetType(event.target.files[0].name))) {
|
|
||||||
setIsThumbnailCompatible(true)
|
|
||||||
}
|
|
||||||
let loadedFileCount = 0
|
|
||||||
const files: File[] = []
|
|
||||||
let reader: FileReader
|
|
||||||
for (let i = 0; i < event.target.files.length; i++) {
|
|
||||||
reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
if (!event.target.files) return toast.error('No files selected.')
|
|
||||||
const assetFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), {
|
|
||||||
type: 'image/jpg',
|
|
||||||
})
|
|
||||||
files.push(assetFile)
|
|
||||||
}
|
|
||||||
reader.readAsArrayBuffer(event.target.files[i])
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
loadedFileCount++
|
|
||||||
if (loadedFileCount === event.target.files.length) {
|
|
||||||
setAssetFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setMetadataFilesArray([])
|
|
||||||
if (event.target.files === null) return toast.error('No files selected.')
|
|
||||||
|
|
||||||
let loadedFileCount = 0
|
|
||||||
const files: File[] = []
|
|
||||||
let reader: FileReader
|
|
||||||
for (let i = 0; i < event.target.files.length; i++) {
|
|
||||||
reader = new FileReader()
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
if (!event.target.files) return toast.error('No files selected.')
|
|
||||||
const metadataFile = new File([e.target.result], event.target.files[i].name.replaceAll('#', ''), {
|
|
||||||
type: 'application/json',
|
|
||||||
})
|
|
||||||
files.push(metadataFile)
|
|
||||||
try {
|
|
||||||
const parsedMetadata = JSON.parse(await metadataFile.text())
|
|
||||||
if (!parsedMetadata || typeof parsedMetadata !== 'object') {
|
|
||||||
event.target.value = ''
|
|
||||||
setMetadataFilesArray([])
|
|
||||||
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
event.target.value = ''
|
|
||||||
setMetadataFilesArray([])
|
|
||||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
|
||||||
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.readAsText(event.target.files[i], 'utf8')
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
loadedFileCount++
|
|
||||||
if (loadedFileCount === event.target.files.length) {
|
|
||||||
setMetadataFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectThumbnail = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setThumbnailFile(undefined)
|
|
||||||
if (event.target.files === null) return
|
|
||||||
|
|
||||||
let selectedFile: File
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), { type: 'image/*' })
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
|
||||||
else return toast.error('No file selected.')
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
setThumbnailFile(selectedFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateMetadataFileIndex = (index: number) => {
|
|
||||||
setMetadataFileArrayIndex(index)
|
|
||||||
setRefreshMetadata((prev) => !prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateMetadataFileArray = (updatedMetadataFile: File) => {
|
|
||||||
metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateOpenEditionMinterMetadataFile = (updatedMetadataFile: File) => {
|
|
||||||
setOpenEditionMinterMetadataFile(updatedMetadataFile)
|
|
||||||
console.log('Updated Open Edition Minter Metadata File:')
|
|
||||||
console.log(openEditionMinterMetadataFile)
|
|
||||||
}
|
|
||||||
const regex =
|
|
||||||
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const data: OffChainMetadataUploadDetailsDataProps = {
|
|
||||||
assetFiles: assetFilesArray,
|
|
||||||
metadataFiles: metadataFilesArray,
|
|
||||||
thumbnailFile,
|
|
||||||
isThumbnailCompatible,
|
|
||||||
uploadService,
|
|
||||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
|
||||||
pinataApiKey: pinataApiKeyState.value,
|
|
||||||
pinataSecretKey: pinataSecretKeyState.value,
|
|
||||||
uploadMethod,
|
|
||||||
tokenURI: tokenUriState.value
|
|
||||||
.replace('IPFS://', 'ipfs://')
|
|
||||||
.replace(/,/g, '')
|
|
||||||
.replace(/"/g, '')
|
|
||||||
.replace(/'/g, '')
|
|
||||||
.replace(regex, '')
|
|
||||||
.trim(),
|
|
||||||
imageUrl: coverImageUrlState.value
|
|
||||||
.replace('IPFS://', 'ipfs://')
|
|
||||||
.replace(/,/g, '')
|
|
||||||
.replace(/"/g, '')
|
|
||||||
.replace(/'/g, '')
|
|
||||||
.replace(regex, '')
|
|
||||||
.trim(),
|
|
||||||
openEditionMinterMetadataFile,
|
|
||||||
exportedMetadata,
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
||||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
assetFilesArray,
|
|
||||||
metadataFilesArray,
|
|
||||||
thumbnailFile,
|
|
||||||
isThumbnailCompatible,
|
|
||||||
uploadService,
|
|
||||||
nftStorageApiKeyState.value,
|
|
||||||
pinataApiKeyState.value,
|
|
||||||
pinataSecretKeyState.value,
|
|
||||||
uploadMethod,
|
|
||||||
tokenUriState.value,
|
|
||||||
coverImageUrlState.value,
|
|
||||||
refreshMetadata,
|
|
||||||
openEditionMinterMetadataFile,
|
|
||||||
exportedMetadata,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
|
|
||||||
setMetadataFilesArray([])
|
|
||||||
if (assetFilesRef.current) assetFilesRef.current.value = ''
|
|
||||||
setAssetFilesArray([])
|
|
||||||
setThumbnailFile(undefined)
|
|
||||||
setIsThumbnailCompatible(false)
|
|
||||||
if (!importedOffChainMetadataUploadDetails) {
|
|
||||||
tokenUriState.onChange('')
|
|
||||||
coverImageUrlState.onChange('')
|
|
||||||
}
|
|
||||||
}, [uploadMethod, metadataStorageMethod])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (importedOffChainMetadataUploadDetails) {
|
|
||||||
setUploadService(importedOffChainMetadataUploadDetails.uploadService)
|
|
||||||
nftStorageApiKeyState.onChange(importedOffChainMetadataUploadDetails.nftStorageApiKey || '')
|
|
||||||
pinataApiKeyState.onChange(importedOffChainMetadataUploadDetails.pinataApiKey || '')
|
|
||||||
pinataSecretKeyState.onChange(importedOffChainMetadataUploadDetails.pinataSecretKey || '')
|
|
||||||
setUploadMethod(importedOffChainMetadataUploadDetails.uploadMethod)
|
|
||||||
tokenUriState.onChange(importedOffChainMetadataUploadDetails.tokenURI || '')
|
|
||||||
coverImageUrlState.onChange(importedOffChainMetadataUploadDetails.imageUrl || '')
|
|
||||||
// setOpenEditionMinterMetadataFile(importedOffChainMetadataUploadDetails.openEditionMinterMetadataFile)
|
|
||||||
}
|
|
||||||
}, [importedOffChainMetadataUploadDetails])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (useDefaultApiKey) {
|
|
||||||
nftStorageApiKeyState.onChange(NFT_STORAGE_DEFAULT_API_KEY || '')
|
|
||||||
} else {
|
|
||||||
nftStorageApiKeyState.onChange('')
|
|
||||||
}
|
|
||||||
}, [useDefaultApiKey])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadMethod === 'new'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio2"
|
|
||||||
name="inlineRadioOptions2"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadMethod('new')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="New"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio2"
|
|
||||||
>
|
|
||||||
Upload asset & metadata
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadMethod === 'existing'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio1"
|
|
||||||
name="inlineRadioOptions1"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadMethod('existing')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="Existing"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio1"
|
|
||||||
>
|
|
||||||
Use an existing Token URI
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 py-5 pb-8">
|
|
||||||
<Conditional test={uploadMethod === 'existing'}>
|
|
||||||
<div className="ml-3 flex-column">
|
|
||||||
<p className="mb-5 ml-5">
|
|
||||||
Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a
|
|
||||||
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
|
|
||||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
|
||||||
NFT.Storage
|
|
||||||
</Anchor>{' '}
|
|
||||||
or{' '}
|
|
||||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
|
||||||
Pinata
|
|
||||||
</Anchor>{' '}
|
|
||||||
and upload your asset & metadata manually to get a URI for your token before minting.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<Tooltip
|
|
||||||
backgroundColor="bg-blue-500"
|
|
||||||
className="mb-2 ml-4"
|
|
||||||
label="The token URI that points directly to the metadata file stored on IPFS."
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<TextInput {...tokenUriState} className="ml-4 w-1/2" />
|
|
||||||
</Tooltip>
|
|
||||||
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={uploadMethod === 'new'}>
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col items-center px-8 w-full">
|
|
||||||
<div className="flex justify-items-start mb-5 w-full font-bold">
|
|
||||||
<div className="form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadService === 'nft-storage'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio3"
|
|
||||||
name="inlineRadioOptions3"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadService('nft-storage')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="nft-storage"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio3"
|
|
||||||
>
|
|
||||||
Upload using NFT.Storage
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-2 form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={uploadService === 'pinata'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio4"
|
|
||||||
name="inlineRadioOptions4"
|
|
||||||
onClick={() => {
|
|
||||||
setUploadService('pinata')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="pinata"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio4"
|
|
||||||
>
|
|
||||||
Upload using Pinata
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full">
|
|
||||||
<Conditional test={uploadService === 'nft-storage'}>
|
|
||||||
<div className="flex-col w-full">
|
|
||||||
<TextInput {...nftStorageApiKeyState} className="w-full" disabled={useDefaultApiKey} />
|
|
||||||
<div className="flex-row mt-2 w-full form-control">
|
|
||||||
<label className="cursor-pointer label">
|
|
||||||
<span className="mr-2 font-bold">Use Default API Key</span>
|
|
||||||
<input
|
|
||||||
checked={useDefaultApiKey}
|
|
||||||
className={`${useDefaultApiKey ? `bg-stargaze` : `bg-gray-600`} checkbox`}
|
|
||||||
onClick={() => {
|
|
||||||
setUseDefaultApiKey(!useDefaultApiKey)
|
|
||||||
}}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={uploadService === 'pinata'}>
|
|
||||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
|
||||||
<div className="w-[20px]" />
|
|
||||||
<TextInput {...pinataSecretKeyState} className="w-full" />
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="grid grid-cols-2">
|
|
||||||
<div className="w-full">
|
|
||||||
<Conditional
|
|
||||||
test={
|
|
||||||
assetFilesArray.length > 0 &&
|
|
||||||
metadataFilesArray.length > 0 &&
|
|
||||||
assetFilesArray.length !== metadataFilesArray.length
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Alert className="mt-4 ml-8 w-3/4" type="warning">
|
|
||||||
The number of assets and metadata files should match.
|
|
||||||
</Alert>
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
|
||||||
htmlFor="assetFiles"
|
|
||||||
>
|
|
||||||
Asset Selection
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="image/*, audio/*, video/*, .html, .pdf"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="assetFiles"
|
|
||||||
onChange={selectAssets}
|
|
||||||
ref={assetFilesRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Conditional test={isThumbnailCompatible}>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
|
||||||
htmlFor="thumbnailFiles"
|
|
||||||
>
|
|
||||||
Thumbnail Selection (optional)
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="image/*"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="thumbnailFiles"
|
|
||||||
onChange={selectThumbnail}
|
|
||||||
ref={thumbnailFilesRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
{assetFilesArray.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
|
||||||
htmlFor="metadataFiles"
|
|
||||||
>
|
|
||||||
Metadata Selection (optional)
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="application/json"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="metadataFiles"
|
|
||||||
onChange={selectMetadata}
|
|
||||||
ref={metadataFilesRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Conditional test={assetFilesArray.length >= 1}>
|
|
||||||
<MetadataModal
|
|
||||||
assetFile={assetFilesArray[metadataFileArrayIndex]}
|
|
||||||
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
|
|
||||||
refresher={refreshMetadata}
|
|
||||||
updateMetadata={updateMetadataFileArray}
|
|
||||||
/>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
<SingleAssetPreview
|
|
||||||
relatedAsset={assetFilesArray[0]}
|
|
||||||
subtitle={`Asset filename: ${assetFilesArray[0]?.name}`}
|
|
||||||
updateMetadataFileIndex={updateMetadataFileIndex}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<MetadataInput
|
|
||||||
importedMetadata={importedOffChainMetadataUploadDetails?.exportedMetadata}
|
|
||||||
onChange={setExportedMetadata}
|
|
||||||
selectedAssetFile={assetFilesArray[0]}
|
|
||||||
selectedMetadataFile={metadataFilesArray[0]}
|
|
||||||
updateMetadataToUpload={updateOpenEditionMinterMetadataFile}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,285 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
|
||||||
import type { Trait } from 'contracts/badgeHub'
|
|
||||||
import type { ChangeEvent } from 'react'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
|
|
||||||
import { TextInput } from '../forms/FormInput'
|
|
||||||
import { MetadataAttributes } from '../forms/MetadataAttributes'
|
|
||||||
import { Tooltip } from '../Tooltip'
|
|
||||||
import type { UploadMethod } from './ImageUploadDetails'
|
|
||||||
|
|
||||||
interface OnChainMetadataInputDetailsProps {
|
|
||||||
onChange: (data: OnChainMetadataInputDetailsDataProps) => void
|
|
||||||
uploadMethod: UploadMethod | undefined
|
|
||||||
importedOnChainMetadataInputDetails?: OnChainMetadataInputDetailsDataProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OnChainMetadataInputDetailsDataProps {
|
|
||||||
name?: string
|
|
||||||
description?: string
|
|
||||||
attributes?: Trait[]
|
|
||||||
image_data?: string
|
|
||||||
external_url?: string
|
|
||||||
background_color?: string
|
|
||||||
animation_url?: string
|
|
||||||
youtube_url?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OnChainMetadataInputDetails = ({
|
|
||||||
onChange,
|
|
||||||
uploadMethod,
|
|
||||||
importedOnChainMetadataInputDetails,
|
|
||||||
}: OnChainMetadataInputDetailsProps) => {
|
|
||||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
|
||||||
const [metadataFile, setMetadataFile] = useState<File>()
|
|
||||||
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
|
|
||||||
|
|
||||||
const metadataFileRef = useRef<HTMLInputElement | null>(null)
|
|
||||||
|
|
||||||
const nameState = useInputState({
|
|
||||||
id: 'name',
|
|
||||||
name: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
placeholder: 'My Awesome Collection',
|
|
||||||
})
|
|
||||||
|
|
||||||
const descriptionState = useInputState({
|
|
||||||
id: 'description',
|
|
||||||
name: 'description',
|
|
||||||
title: 'Description',
|
|
||||||
placeholder: 'My Awesome Collection Description',
|
|
||||||
})
|
|
||||||
|
|
||||||
const imageDataState = useInputState({
|
|
||||||
id: 'metadata-image-data',
|
|
||||||
name: 'metadata-image-data',
|
|
||||||
title: 'Image Data',
|
|
||||||
subtitle: 'Raw SVG image data',
|
|
||||||
})
|
|
||||||
|
|
||||||
const externalUrlState = useInputState({
|
|
||||||
id: 'metadata-external-url',
|
|
||||||
name: 'metadata-external-url',
|
|
||||||
title: 'External URL',
|
|
||||||
subtitle: 'External URL for the token',
|
|
||||||
placeholder: 'https://',
|
|
||||||
})
|
|
||||||
|
|
||||||
const attributesState = useMetadataAttributesState()
|
|
||||||
|
|
||||||
const animationUrlState = useInputState({
|
|
||||||
id: 'metadata-animation-url',
|
|
||||||
name: 'metadata-animation-url',
|
|
||||||
title: 'Animation URL',
|
|
||||||
subtitle: 'Animation URL for the token',
|
|
||||||
placeholder: 'https://',
|
|
||||||
})
|
|
||||||
|
|
||||||
const youtubeUrlState = useInputState({
|
|
||||||
id: 'metadata-youtube-url',
|
|
||||||
name: 'metadata-youtube-url',
|
|
||||||
title: 'YouTube URL',
|
|
||||||
subtitle: 'YouTube URL for the token',
|
|
||||||
placeholder: 'https://',
|
|
||||||
})
|
|
||||||
|
|
||||||
const parseMetadata = async () => {
|
|
||||||
try {
|
|
||||||
let parsedMetadata: any
|
|
||||||
if (metadataFile) {
|
|
||||||
attributesState.reset()
|
|
||||||
parsedMetadata = JSON.parse(await metadataFile.text())
|
|
||||||
|
|
||||||
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: '',
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: parsedMetadata.attributes[i].trait_type,
|
|
||||||
value: parsedMetadata.attributes[i].value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
|
|
||||||
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
|
|
||||||
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
|
|
||||||
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
|
|
||||||
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
|
|
||||||
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
|
|
||||||
} else {
|
|
||||||
attributesState.reset()
|
|
||||||
nameState.onChange('')
|
|
||||||
descriptionState.onChange('')
|
|
||||||
externalUrlState.onChange('')
|
|
||||||
youtubeUrlState.onChange('')
|
|
||||||
animationUrlState.onChange('')
|
|
||||||
imageDataState.onChange('')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Error parsing metadata file: Invalid JSON format.')
|
|
||||||
if (metadataFileRef.current) metadataFileRef.current.value = ''
|
|
||||||
setMetadataFile(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setMetadataFile(undefined)
|
|
||||||
if (event.target.files === null) return
|
|
||||||
|
|
||||||
let selectedFile: File
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
|
||||||
selectedFile = new File([e.target.result], event.target.files[0].name.replaceAll('#', ''), {
|
|
||||||
type: 'application/json',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
|
||||||
else return toast.error('No file selected.')
|
|
||||||
reader.onloadend = () => {
|
|
||||||
if (!event.target.files) return toast.error('No file selected.')
|
|
||||||
setMetadataFile(selectedFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void parseMetadata()
|
|
||||||
if (!metadataFile)
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: '',
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
}, [metadataFile])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
const data: OnChainMetadataInputDetailsDataProps = {
|
|
||||||
name: nameState.value || undefined,
|
|
||||||
description: descriptionState.value || undefined,
|
|
||||||
attributes:
|
|
||||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
|
||||||
? attributesState.values
|
|
||||||
.map((attr) => ({
|
|
||||||
trait_type: attr.trait_type,
|
|
||||||
value: attr.value,
|
|
||||||
}))
|
|
||||||
.filter((attr) => attr.trait_type && attr.value)
|
|
||||||
: undefined,
|
|
||||||
image_data: imageDataState.value || undefined,
|
|
||||||
external_url: externalUrlState.value || undefined,
|
|
||||||
animation_url: animationUrlState.value.trim() || undefined,
|
|
||||||
youtube_url: youtubeUrlState.value || undefined,
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
nameState.value,
|
|
||||||
descriptionState.value,
|
|
||||||
timestamp,
|
|
||||||
imageDataState.value,
|
|
||||||
externalUrlState.value,
|
|
||||||
attributesState.values,
|
|
||||||
animationUrlState.value,
|
|
||||||
youtubeUrlState.value,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (importedOnChainMetadataInputDetails) {
|
|
||||||
nameState.onChange(importedOnChainMetadataInputDetails.name || '')
|
|
||||||
descriptionState.onChange(importedOnChainMetadataInputDetails.description || '')
|
|
||||||
externalUrlState.onChange(importedOnChainMetadataInputDetails.external_url || '')
|
|
||||||
youtubeUrlState.onChange(importedOnChainMetadataInputDetails.youtube_url || '')
|
|
||||||
animationUrlState.onChange(importedOnChainMetadataInputDetails.animation_url || '')
|
|
||||||
imageDataState.onChange(importedOnChainMetadataInputDetails.image_data || '')
|
|
||||||
if (importedOnChainMetadataInputDetails.attributes) {
|
|
||||||
attributesState.reset()
|
|
||||||
importedOnChainMetadataInputDetails.attributes.forEach((attr) => {
|
|
||||||
attributesState.add({
|
|
||||||
trait_type: attr.trait_type,
|
|
||||||
value: attr.value,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [importedOnChainMetadataInputDetails])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-3 px-8 rounded border-2 border-white/20">
|
|
||||||
<span className="ml-4 text-xl font-bold underline underline-offset-4">NFT Metadata</span>
|
|
||||||
<div className={clsx('grid grid-cols-2 mt-4 mb-2 ml-4 max-w-6xl')}>
|
|
||||||
<div className={clsx('mt-6')}>
|
|
||||||
<TextInput className="mt-2" {...nameState} />
|
|
||||||
<TextInput className="mt-2" {...descriptionState} />
|
|
||||||
<TextInput className="mt-2" {...externalUrlState} />
|
|
||||||
<Conditional test={uploadMethod === 'existing'}>
|
|
||||||
<TextInput className="mt-2" {...animationUrlState} />
|
|
||||||
</Conditional>
|
|
||||||
<TextInput className="mt-2" {...youtubeUrlState} />
|
|
||||||
</div>
|
|
||||||
<div className={clsx('ml-10')}>
|
|
||||||
<div>
|
|
||||||
<MetadataAttributes
|
|
||||||
attributes={attributesState.entries}
|
|
||||||
onAdd={attributesState.add}
|
|
||||||
onChange={attributesState.update}
|
|
||||||
onRemove={attributesState.remove}
|
|
||||||
title="Traits"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Tooltip
|
|
||||||
backgroundColor="bg-blue-500"
|
|
||||||
label="A metadata file can be selected to automatically fill in the related fields."
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
|
|
||||||
htmlFor="assetFile"
|
|
||||||
>
|
|
||||||
Metadata File Selection (optional)
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="application/json"
|
|
||||||
className={clsx(
|
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
|
||||||
)}
|
|
||||||
id="metadataFile"
|
|
||||||
onChange={selectMetadata}
|
|
||||||
ref={metadataFileRef}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,127 +0,0 @@
|
|||||||
import { Conditional } from 'components/Conditional'
|
|
||||||
import { FormGroup } from 'components/FormGroup'
|
|
||||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { resolveAddress } from 'utils/resolveAddress'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { NumberInput, TextInput } from '../forms/FormInput'
|
|
||||||
|
|
||||||
interface RoyaltyDetailsProps {
|
|
||||||
onChange: (data: RoyaltyDetailsDataProps) => void
|
|
||||||
importedRoyaltyDetails?: RoyaltyDetailsDataProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RoyaltyDetailsDataProps {
|
|
||||||
royaltyType: RoyaltyState
|
|
||||||
paymentAddress: string
|
|
||||||
share: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoyaltyState = 'none' | 'new'
|
|
||||||
|
|
||||||
export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDetailsProps) => {
|
|
||||||
const wallet = useWallet()
|
|
||||||
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
|
|
||||||
const [royaltyDetailsImported, setRoyaltyDetailsImported] = useState(false)
|
|
||||||
|
|
||||||
const royaltyPaymentAddressState = useInputState({
|
|
||||||
id: 'royalty-payment-address',
|
|
||||||
name: 'royaltyPaymentAddress',
|
|
||||||
title: 'Payment Address',
|
|
||||||
subtitle: 'Address to receive royalties',
|
|
||||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
|
||||||
})
|
|
||||||
|
|
||||||
const royaltyShareState = useInputState({
|
|
||||||
id: 'royalty-share',
|
|
||||||
name: 'royaltyShare',
|
|
||||||
title: 'Share Percentage',
|
|
||||||
subtitle: 'Percentage of royalties to be paid',
|
|
||||||
placeholder: '5%',
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!importedRoyaltyDetails || (importedRoyaltyDetails && royaltyDetailsImported)) {
|
|
||||||
void resolveAddress(
|
|
||||||
royaltyPaymentAddressState.value
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/,/g, '')
|
|
||||||
.replace(/"/g, '')
|
|
||||||
.replace(/'/g, '')
|
|
||||||
.replace(/ /g, ''),
|
|
||||||
wallet,
|
|
||||||
).then((royaltyPaymentAddress) => {
|
|
||||||
royaltyPaymentAddressState.onChange(royaltyPaymentAddress)
|
|
||||||
const data: RoyaltyDetailsDataProps = {
|
|
||||||
royaltyType: royaltyState,
|
|
||||||
paymentAddress: royaltyPaymentAddressState.value,
|
|
||||||
share: Number(royaltyShareState.value),
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (importedRoyaltyDetails) {
|
|
||||||
setRoyaltyState(importedRoyaltyDetails.royaltyType)
|
|
||||||
royaltyPaymentAddressState.onChange(importedRoyaltyDetails.paymentAddress.toString())
|
|
||||||
royaltyShareState.onChange(importedRoyaltyDetails.share.toString())
|
|
||||||
setRoyaltyDetailsImported(true)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [importedRoyaltyDetails])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-3 px-8 mx-10 rounded border-2 border-white/20">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={royaltyState === 'none'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="royaltyRadio1"
|
|
||||||
name="royaltyRadioOptions1"
|
|
||||||
onClick={() => {
|
|
||||||
setRoyaltyState('none')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="None"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="royaltyRadio1"
|
|
||||||
>
|
|
||||||
No royalty
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={royaltyState === 'new'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="royaltyRadio2"
|
|
||||||
name="royaltyRadioOptions2"
|
|
||||||
onClick={() => {
|
|
||||||
setRoyaltyState('new')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="Existing"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="royaltyRadio2"
|
|
||||||
>
|
|
||||||
Configure royalty details
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Conditional test={royaltyState === 'new'}>
|
|
||||||
<FormGroup subtitle="Information about royalty" title="Royalty Details">
|
|
||||||
<TextInput {...royaltyPaymentAddressState} isRequired />
|
|
||||||
<NumberInput {...royaltyShareState} isRequired />
|
|
||||||
</FormGroup>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,528 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
import { Button } from 'components/Button'
|
|
||||||
import { FormControl } from 'components/FormControl'
|
|
||||||
import { FormGroup } from 'components/FormGroup'
|
|
||||||
import { AddressList } from 'components/forms/AddressList'
|
|
||||||
import { useAddressListState } from 'components/forms/AddressList.hooks'
|
|
||||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
|
||||||
import { InputDateTime } from 'components/InputDateTime'
|
|
||||||
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
|
|
||||||
import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
|
|
||||||
import type { TokenInfo } from 'config/token'
|
|
||||||
import { useGlobalSettings } from 'contexts/globalSettings'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { isValidAddress } from 'utils/isValidAddress'
|
|
||||||
import { useWallet } from 'utils/wallet'
|
|
||||||
|
|
||||||
import { Conditional } from '../Conditional'
|
|
||||||
import { AddressInput, NumberInput } from '../forms/FormInput'
|
|
||||||
import { JsonPreview } from '../JsonPreview'
|
|
||||||
import { WhitelistUpload } from '../WhitelistUpload'
|
|
||||||
|
|
||||||
interface WhitelistDetailsProps {
|
|
||||||
onChange: (data: WhitelistDetailsDataProps) => void
|
|
||||||
mintingTokenFromFactory?: TokenInfo
|
|
||||||
importedWhitelistDetails?: WhitelistDetailsDataProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WhitelistDetailsDataProps {
|
|
||||||
whitelistState: WhitelistState
|
|
||||||
whitelistType: WhitelistType
|
|
||||||
contractAddress?: string
|
|
||||||
members?: string[] | WhitelistFlexMember[]
|
|
||||||
unitPrice?: string
|
|
||||||
startTime?: string
|
|
||||||
endTime?: string
|
|
||||||
perAddressLimit?: number
|
|
||||||
memberLimit?: number
|
|
||||||
admins?: string[]
|
|
||||||
adminsMutable?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type WhitelistState = 'none' | 'existing' | 'new'
|
|
||||||
|
|
||||||
export type WhitelistType = 'standard' | 'flex' | 'merkletree'
|
|
||||||
|
|
||||||
export const WhitelistDetails = ({
|
|
||||||
onChange,
|
|
||||||
mintingTokenFromFactory,
|
|
||||||
importedWhitelistDetails,
|
|
||||||
}: WhitelistDetailsProps) => {
|
|
||||||
const wallet = useWallet()
|
|
||||||
const { timezone } = useGlobalSettings()
|
|
||||||
|
|
||||||
const [whitelistState, setWhitelistState] = useState<WhitelistState>('none')
|
|
||||||
const [whitelistType, setWhitelistType] = useState<WhitelistType>('standard')
|
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>(undefined)
|
|
||||||
const [endDate, setEndDate] = useState<Date | undefined>(undefined)
|
|
||||||
const [whitelistStandardArray, setWhitelistStandardArray] = useState<string[]>([])
|
|
||||||
const [whitelistFlexArray, setWhitelistFlexArray] = useState<WhitelistFlexMember[]>([])
|
|
||||||
const [whitelistMerkleTreeArray, setWhitelistMerkleTreeArray] = useState<string[]>([])
|
|
||||||
const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
|
|
||||||
|
|
||||||
const whitelistAddressState = useInputState({
|
|
||||||
id: 'whitelist-address',
|
|
||||||
name: 'whitelistAddress',
|
|
||||||
title: 'Whitelist Address',
|
|
||||||
defaultValue: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const unitPriceState = useNumberInputState({
|
|
||||||
id: 'unit-price',
|
|
||||||
name: 'unitPrice',
|
|
||||||
title: 'Unit Price',
|
|
||||||
subtitle: `Token price for whitelisted addresses \n (min. 0 ${
|
|
||||||
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
|
|
||||||
})`,
|
|
||||||
placeholder: '25',
|
|
||||||
})
|
|
||||||
|
|
||||||
const memberLimitState = useNumberInputState({
|
|
||||||
id: 'member-limit',
|
|
||||||
name: 'memberLimit',
|
|
||||||
title: 'Member Limit',
|
|
||||||
subtitle: 'Maximum number of whitelisted addresses',
|
|
||||||
placeholder: '1000',
|
|
||||||
})
|
|
||||||
|
|
||||||
const perAddressLimitState = useNumberInputState({
|
|
||||||
id: 'per-address-limit',
|
|
||||||
name: 'perAddressLimit',
|
|
||||||
title: 'Per Address Limit',
|
|
||||||
subtitle: 'Maximum number of tokens per whitelisted address',
|
|
||||||
placeholder: '5',
|
|
||||||
})
|
|
||||||
|
|
||||||
const addressListState = useAddressListState()
|
|
||||||
|
|
||||||
const whitelistFileOnChange = (data: string[]) => {
|
|
||||||
if (whitelistType === 'standard') setWhitelistStandardArray(data)
|
|
||||||
if (whitelistType === 'merkletree') setWhitelistMerkleTreeArray(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
|
|
||||||
setWhitelistFlexArray(whitelistData)
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadSampleWhitelistFlexFile = () => {
|
|
||||||
const csvData =
|
|
||||||
'address,mint_count\nstars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e,3\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz,1\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3,2'
|
|
||||||
const blob = new Blob([csvData], { type: 'text/csv' })
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.setAttribute('href', url)
|
|
||||||
a.setAttribute('download', 'sample_whitelist_flex.csv')
|
|
||||||
a.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadSampleWhitelistFile = () => {
|
|
||||||
const txtData =
|
|
||||||
'stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e\nstars1xkes5r2k8u3m3ayfpverlkcrq3k4jhdk8ws0uz\nstars1s8qx0zvz8yd6e4x0mqmqf7fr9vvfn622wtp3g3'
|
|
||||||
const blob = new Blob([txtData], { type: 'text/txt' })
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.setAttribute('href', url)
|
|
||||||
a.setAttribute('download', 'sample_whitelist.txt')
|
|
||||||
a.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!importedWhitelistDetails) {
|
|
||||||
setWhitelistStandardArray([])
|
|
||||||
setWhitelistFlexArray([])
|
|
||||||
setWhitelistMerkleTreeArray([])
|
|
||||||
}
|
|
||||||
}, [whitelistType])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const data: WhitelistDetailsDataProps = {
|
|
||||||
whitelistState,
|
|
||||||
whitelistType,
|
|
||||||
contractAddress: whitelistAddressState.value
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/,/g, '')
|
|
||||||
.replace(/"/g, '')
|
|
||||||
.replace(/'/g, '')
|
|
||||||
.replace(/ /g, ''),
|
|
||||||
members:
|
|
||||||
whitelistType === 'standard'
|
|
||||||
? whitelistStandardArray
|
|
||||||
: whitelistType === 'merkletree'
|
|
||||||
? whitelistMerkleTreeArray
|
|
||||||
: whitelistFlexArray,
|
|
||||||
unitPrice: unitPriceState.value
|
|
||||||
? (Number(unitPriceState.value) * 1_000_000).toString()
|
|
||||||
: unitPriceState.value === 0
|
|
||||||
? '0'
|
|
||||||
: undefined,
|
|
||||||
startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '',
|
|
||||||
endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '',
|
|
||||||
perAddressLimit: perAddressLimitState.value,
|
|
||||||
memberLimit: memberLimitState.value,
|
|
||||||
admins: [
|
|
||||||
...new Set(
|
|
||||||
addressListState.values
|
|
||||||
.map((a) => a.address.trim())
|
|
||||||
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
adminsMutable,
|
|
||||||
}
|
|
||||||
onChange(data)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
whitelistAddressState.value,
|
|
||||||
unitPriceState.value,
|
|
||||||
memberLimitState.value,
|
|
||||||
perAddressLimitState.value,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
whitelistStandardArray,
|
|
||||||
whitelistFlexArray,
|
|
||||||
whitelistMerkleTreeArray,
|
|
||||||
whitelistState,
|
|
||||||
whitelistType,
|
|
||||||
addressListState.values,
|
|
||||||
adminsMutable,
|
|
||||||
])
|
|
||||||
|
|
||||||
// make the necessary changes with respect to imported whitelist details
|
|
||||||
useEffect(() => {
|
|
||||||
if (importedWhitelistDetails) {
|
|
||||||
setWhitelistState(importedWhitelistDetails.whitelistState)
|
|
||||||
setWhitelistType(importedWhitelistDetails.whitelistType)
|
|
||||||
whitelistAddressState.onChange(
|
|
||||||
importedWhitelistDetails.contractAddress ? importedWhitelistDetails.contractAddress : '',
|
|
||||||
)
|
|
||||||
unitPriceState.onChange(
|
|
||||||
importedWhitelistDetails.unitPrice ? Number(importedWhitelistDetails.unitPrice) / 1000000 : 0,
|
|
||||||
)
|
|
||||||
memberLimitState.onChange(importedWhitelistDetails.memberLimit ? importedWhitelistDetails.memberLimit : 0)
|
|
||||||
perAddressLimitState.onChange(
|
|
||||||
importedWhitelistDetails.perAddressLimit ? importedWhitelistDetails.perAddressLimit : 0,
|
|
||||||
)
|
|
||||||
setStartDate(
|
|
||||||
importedWhitelistDetails.startTime
|
|
||||||
? new Date(Number(importedWhitelistDetails.startTime) / 1_000_000)
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
setEndDate(
|
|
||||||
importedWhitelistDetails.endTime ? new Date(Number(importedWhitelistDetails.endTime) / 1_000_000) : undefined,
|
|
||||||
)
|
|
||||||
setAdminsMutable(importedWhitelistDetails.adminsMutable ? importedWhitelistDetails.adminsMutable : true)
|
|
||||||
importedWhitelistDetails.admins?.forEach((admin) => {
|
|
||||||
addressListState.reset()
|
|
||||||
addressListState.add({ address: admin })
|
|
||||||
})
|
|
||||||
if (importedWhitelistDetails.whitelistType === 'standard') {
|
|
||||||
setWhitelistStandardArray([])
|
|
||||||
importedWhitelistDetails.members?.forEach((member) => {
|
|
||||||
setWhitelistStandardArray((standardArray) => [...standardArray, member as string])
|
|
||||||
})
|
|
||||||
} else if (importedWhitelistDetails.whitelistType === 'merkletree') {
|
|
||||||
setWhitelistMerkleTreeArray([])
|
|
||||||
// importedWhitelistDetails.members?.forEach((member) => {
|
|
||||||
// setWhitelistMerkleTreeArray((merkleTreeArray) => [...merkleTreeArray, member as string])
|
|
||||||
// })
|
|
||||||
} else if (importedWhitelistDetails.whitelistType === 'flex') {
|
|
||||||
setWhitelistFlexArray([])
|
|
||||||
importedWhitelistDetails.members?.forEach((member) => {
|
|
||||||
setWhitelistFlexArray((flexArray) => [
|
|
||||||
...flexArray,
|
|
||||||
{
|
|
||||||
address: (member as WhitelistFlexMember).address,
|
|
||||||
mint_count: (member as WhitelistFlexMember).mint_count,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [importedWhitelistDetails])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (whitelistState === 'new' && wallet.address) {
|
|
||||||
addressListState.reset()
|
|
||||||
addressListState.add({ address: wallet.address })
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [whitelistState, wallet.address])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-3 px-8 rounded border-2 border-white/20">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={whitelistState === 'none'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="whitelistRadio1"
|
|
||||||
name="whitelistRadioOptions1"
|
|
||||||
onClick={() => {
|
|
||||||
setWhitelistState('none')
|
|
||||||
setWhitelistType('standard')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="None"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="whitelistRadio1"
|
|
||||||
>
|
|
||||||
No whitelist
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={whitelistState === 'existing'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="whitelistRadio2"
|
|
||||||
name="whitelistRadioOptions2"
|
|
||||||
onClick={() => {
|
|
||||||
setWhitelistState('existing')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="Existing"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="whitelistRadio2"
|
|
||||||
>
|
|
||||||
Existing whitelist
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4 font-bold form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={whitelistState === 'new'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="whitelistRadio3"
|
|
||||||
name="whitelistRadioOptions3"
|
|
||||||
onClick={() => {
|
|
||||||
setWhitelistState('new')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="New"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="whitelistRadio3"
|
|
||||||
>
|
|
||||||
New whitelist
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Conditional test={whitelistState === 'existing'}>
|
|
||||||
<AddressInput {...whitelistAddressState} className="pb-5" isRequired />
|
|
||||||
</Conditional>
|
|
||||||
|
|
||||||
<Conditional test={whitelistState === 'new'}>
|
|
||||||
<div className="flex justify-between mb-5 ml-6 max-w-[300px] text-lg font-bold">
|
|
||||||
<div className="form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={whitelistType === 'standard'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio7"
|
|
||||||
name="inlineRadioOptions7"
|
|
||||||
onClick={() => {
|
|
||||||
setWhitelistType('standard')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="standard"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio7"
|
|
||||||
>
|
|
||||||
Standard Whitelist
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={whitelistType === 'flex'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio8"
|
|
||||||
name="inlineRadioOptions8"
|
|
||||||
onClick={() => {
|
|
||||||
setWhitelistType('flex')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="flex"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio8"
|
|
||||||
>
|
|
||||||
Whitelist Flex
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{/* <div className="form-check form-check-inline">
|
|
||||||
<input
|
|
||||||
checked={whitelistType === 'merkletree'}
|
|
||||||
className="peer sr-only"
|
|
||||||
id="inlineRadio9"
|
|
||||||
name="inlineRadioOptions9"
|
|
||||||
onClick={() => {
|
|
||||||
setWhitelistType('merkletree')
|
|
||||||
}}
|
|
||||||
type="radio"
|
|
||||||
value="merkletree"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
|
||||||
htmlFor="inlineRadio9"
|
|
||||||
>
|
|
||||||
Whitelist Merkle Tree
|
|
||||||
</label>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2">
|
|
||||||
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
|
|
||||||
<NumberInput isRequired {...unitPriceState} />
|
|
||||||
<Conditional test={whitelistType !== 'merkletree'}>
|
|
||||||
<NumberInput isRequired {...memberLimitState} />
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={whitelistType === 'standard' || whitelistType === 'merkletree'}>
|
|
||||||
<NumberInput isRequired {...perAddressLimitState} />
|
|
||||||
</Conditional>
|
|
||||||
<FormControl
|
|
||||||
htmlId="start-date"
|
|
||||||
isRequired
|
|
||||||
subtitle="Start time for minting tokens to whitelisted addresses"
|
|
||||||
title={`Whitelist Start Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
|
||||||
>
|
|
||||||
<InputDateTime
|
|
||||||
minDate={
|
|
||||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
}
|
|
||||||
onChange={(date) =>
|
|
||||||
date
|
|
||||||
? setStartDate(
|
|
||||||
timezone === 'Local'
|
|
||||||
? date
|
|
||||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
|
||||||
)
|
|
||||||
: setStartDate(undefined)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
timezone === 'Local'
|
|
||||||
? startDate
|
|
||||||
: startDate
|
|
||||||
? new Date(startDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl
|
|
||||||
htmlId="end-date"
|
|
||||||
isRequired
|
|
||||||
subtitle="Whitelist End Time dictates when public sales will start"
|
|
||||||
title={`Whitelist End Time ${timezone === 'Local' ? '(local)' : '(UTC)'}`}
|
|
||||||
>
|
|
||||||
<InputDateTime
|
|
||||||
minDate={
|
|
||||||
timezone === 'Local' ? new Date() : new Date(Date.now() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
}
|
|
||||||
onChange={(date) =>
|
|
||||||
date
|
|
||||||
? setEndDate(
|
|
||||||
timezone === 'Local'
|
|
||||||
? date
|
|
||||||
: new Date(date.getTime() - new Date().getTimezoneOffset() * 60 * 1000),
|
|
||||||
)
|
|
||||||
: setEndDate(undefined)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
timezone === 'Local'
|
|
||||||
? endDate
|
|
||||||
: endDate
|
|
||||||
? new Date(endDate.getTime() + new Date().getTimezoneOffset() * 60 * 1000)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormGroup>
|
|
||||||
<div>
|
|
||||||
<div className="mt-2 ml-3 w-[65%] form-control">
|
|
||||||
<label className="justify-start cursor-pointer label">
|
|
||||||
<span className="mr-4 font-bold">Mutable Administrator Addresses</span>
|
|
||||||
<input
|
|
||||||
checked={adminsMutable}
|
|
||||||
className={`toggle ${adminsMutable ? `bg-stargaze` : `bg-gray-600`}`}
|
|
||||||
onClick={() => setAdminsMutable(!adminsMutable)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="my-4 ml-4">
|
|
||||||
<AddressList
|
|
||||||
entries={addressListState.entries}
|
|
||||||
onAdd={addressListState.add}
|
|
||||||
onChange={addressListState.update}
|
|
||||||
onRemove={addressListState.remove}
|
|
||||||
subtitle="The list of administrator addresses"
|
|
||||||
title="Administrator Addresses"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Conditional test={whitelistType === 'standard'}>
|
|
||||||
<FormGroup
|
|
||||||
subtitle={
|
|
||||||
<div>
|
|
||||||
<span>TXT file that contains the whitelisted addresses</span>
|
|
||||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
|
|
||||||
Download Sample File
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
title="Whitelist File"
|
|
||||||
>
|
|
||||||
<WhitelistUpload onChange={whitelistFileOnChange} />
|
|
||||||
</FormGroup>
|
|
||||||
<Conditional test={whitelistStandardArray.length > 0}>
|
|
||||||
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
|
|
||||||
</Conditional>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={whitelistType === 'flex'}>
|
|
||||||
<FormGroup
|
|
||||||
subtitle={
|
|
||||||
<div>
|
|
||||||
<span>CSV file that contains the whitelisted addresses and corresponding mint counts</span>
|
|
||||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFlexFile}>
|
|
||||||
Download Sample File
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
title="Whitelist File"
|
|
||||||
>
|
|
||||||
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
|
|
||||||
</FormGroup>
|
|
||||||
<Conditional test={whitelistFlexArray.length > 0}>
|
|
||||||
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
|
|
||||||
</Conditional>
|
|
||||||
</Conditional>
|
|
||||||
<Conditional test={whitelistType === 'merkletree'}>
|
|
||||||
<FormGroup
|
|
||||||
subtitle={
|
|
||||||
<div>
|
|
||||||
<span>TXT file that contains the whitelisted addresses</span>
|
|
||||||
<Button className="mt-2 text-sm text-white" onClick={downloadSampleWhitelistFile}>
|
|
||||||
Download Sample File
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
title="Whitelist File"
|
|
||||||
>
|
|
||||||
<WhitelistUpload onChange={whitelistFileOnChange} />
|
|
||||||
</FormGroup>
|
|
||||||
<Conditional test={whitelistStandardArray.length > 0}>
|
|
||||||
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
|
|
||||||
</Conditional>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Conditional>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
324
config/authz.ts
324
config/authz.ts
@ -1,324 +0,0 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
|
||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
|
|
||||||
import { GenericAuthorization } from 'cosmjs-types/cosmos/authz/v1beta1/authz'
|
|
||||||
import { MsgGrant } from 'cosmjs-types/cosmos/authz/v1beta1/tx'
|
|
||||||
import { SendAuthorization } from 'cosmjs-types/cosmos/bank/v1beta1/authz'
|
|
||||||
import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'
|
|
||||||
import type { AuthorizationType } from 'cosmjs-types/cosmos/staking/v1beta1/authz'
|
|
||||||
import { StakeAuthorization, StakeAuthorization_Validators } from 'cosmjs-types/cosmos/staking/v1beta1/authz'
|
|
||||||
import {
|
|
||||||
AcceptedMessageKeysFilter,
|
|
||||||
AllowAllMessagesFilter,
|
|
||||||
CombinedLimit,
|
|
||||||
ContractExecutionAuthorization,
|
|
||||||
ContractMigrationAuthorization,
|
|
||||||
MaxCallsLimit,
|
|
||||||
MaxFundsLimit,
|
|
||||||
} from 'cosmjs-types/cosmwasm/wasm/v1/authz'
|
|
||||||
import type { AuthorizationMode, GenericAuthorizationType, GrantAuthorizationType } from 'pages/authz/grant'
|
|
||||||
|
|
||||||
export interface Msg {
|
|
||||||
typeUrl: string
|
|
||||||
value: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthzMessage {
|
|
||||||
authzMode: AuthorizationMode
|
|
||||||
authzType: GrantAuthorizationType
|
|
||||||
displayName: string
|
|
||||||
typeUrl: string
|
|
||||||
genericAuthzType?: GenericAuthorizationType
|
|
||||||
}
|
|
||||||
|
|
||||||
export const grantGenericStakeAuthorization: AuthzMessage = {
|
|
||||||
authzMode: 'Grant',
|
|
||||||
authzType: 'Generic',
|
|
||||||
displayName: 'Stake',
|
|
||||||
typeUrl: '/cosmos.staking.v1beta1.MsgDelegate',
|
|
||||||
genericAuthzType: 'MsgDelegate',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const grantGenericSendAuthorization: AuthzMessage = {
|
|
||||||
authzMode: 'Grant',
|
|
||||||
authzType: 'Generic',
|
|
||||||
displayName: 'Send',
|
|
||||||
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
|
|
||||||
genericAuthzType: 'MsgSend',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const authzMessages: AuthzMessage[] = [grantGenericStakeAuthorization, grantGenericSendAuthorization]
|
|
||||||
|
|
||||||
const msgAuthzGrantTypeUrl = '/cosmos.authz.v1beta1.MsgGrant'
|
|
||||||
|
|
||||||
export function AuthzSendGrantMsg(
|
|
||||||
granter: string,
|
|
||||||
grantee: string,
|
|
||||||
denom: string,
|
|
||||||
spendLimit: number,
|
|
||||||
expiration: number,
|
|
||||||
allowList?: string[],
|
|
||||||
): Msg {
|
|
||||||
const sendAuthValue = SendAuthorization.encode(
|
|
||||||
SendAuthorization.fromPartial({
|
|
||||||
spendLimit: [
|
|
||||||
Coin.fromPartial({
|
|
||||||
amount: String(spendLimit),
|
|
||||||
denom,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
// Needs cosmos-sdk >= 0.47
|
|
||||||
// allowList,
|
|
||||||
}),
|
|
||||||
).finish()
|
|
||||||
|
|
||||||
const grantValue = MsgGrant.fromPartial({
|
|
||||||
grant: {
|
|
||||||
authorization: {
|
|
||||||
typeUrl: '/cosmos.bank.v1beta1.SendAuthorization',
|
|
||||||
value: sendAuthValue,
|
|
||||||
},
|
|
||||||
expiration: expiration ? { seconds: BigInt(expiration) } : undefined,
|
|
||||||
},
|
|
||||||
grantee,
|
|
||||||
granter,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
typeUrl: msgAuthzGrantTypeUrl,
|
|
||||||
value: grantValue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthzExecuteContractGrantMsg(
|
|
||||||
granter: string,
|
|
||||||
grantee: string,
|
|
||||||
contract: string,
|
|
||||||
expiration: number,
|
|
||||||
callsRemaining?: number,
|
|
||||||
amounts?: Coin[],
|
|
||||||
allowedMessages?: string[],
|
|
||||||
): Msg {
|
|
||||||
const sendAuthValue = ContractExecutionAuthorization.encode(
|
|
||||||
ContractExecutionAuthorization.fromPartial({
|
|
||||||
grants: [
|
|
||||||
{
|
|
||||||
contract,
|
|
||||||
filter: {
|
|
||||||
typeUrl: allowedMessages
|
|
||||||
? '/cosmwasm.wasm.v1.AcceptedMessageKeysFilter'
|
|
||||||
: '/cosmwasm.wasm.v1.AllowAllMessagesFilter',
|
|
||||||
value: allowedMessages
|
|
||||||
? AcceptedMessageKeysFilter.encode({ keys: allowedMessages }).finish()
|
|
||||||
: AllowAllMessagesFilter.encode({}).finish(),
|
|
||||||
},
|
|
||||||
limit:
|
|
||||||
callsRemaining || amounts
|
|
||||||
? {
|
|
||||||
typeUrl:
|
|
||||||
callsRemaining && amounts
|
|
||||||
? '/cosmwasm.wasm.v1.CombinedLimit'
|
|
||||||
: callsRemaining
|
|
||||||
? '/cosmwasm.wasm.v1.MaxCallsLimit'
|
|
||||||
: '/cosmwasm.wasm.v1.MaxFundsLimit',
|
|
||||||
value:
|
|
||||||
callsRemaining && amounts
|
|
||||||
? CombinedLimit.encode({
|
|
||||||
callsRemaining: BigInt(callsRemaining),
|
|
||||||
amounts,
|
|
||||||
}).finish()
|
|
||||||
: callsRemaining
|
|
||||||
? MaxCallsLimit.encode({
|
|
||||||
remaining: BigInt(callsRemaining),
|
|
||||||
}).finish()
|
|
||||||
: MaxFundsLimit.encode({
|
|
||||||
amounts: amounts || [],
|
|
||||||
}).finish(),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
// limit: undefined is not accepted
|
|
||||||
typeUrl: '/cosmwasm.wasm.v1.MaxCallsLimit',
|
|
||||||
value: MaxCallsLimit.encode({
|
|
||||||
remaining: BigInt(100000),
|
|
||||||
}).finish(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
).finish()
|
|
||||||
|
|
||||||
const grantValue = MsgGrant.fromPartial({
|
|
||||||
grant: {
|
|
||||||
authorization: {
|
|
||||||
typeUrl: '/cosmwasm.wasm.v1.ContractExecutionAuthorization',
|
|
||||||
value: sendAuthValue,
|
|
||||||
},
|
|
||||||
expiration: expiration ? { seconds: BigInt(expiration), nanos: 0 } : undefined,
|
|
||||||
},
|
|
||||||
grantee,
|
|
||||||
granter,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
typeUrl: msgAuthzGrantTypeUrl,
|
|
||||||
value: grantValue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthzMigrateContractGrantMsg(
|
|
||||||
granter: string,
|
|
||||||
grantee: string,
|
|
||||||
contract: string,
|
|
||||||
expiration: number,
|
|
||||||
callsRemaining?: number,
|
|
||||||
amounts?: Coin[],
|
|
||||||
allowedMessages?: string[],
|
|
||||||
): Msg {
|
|
||||||
const sendAuthValue = ContractMigrationAuthorization.encode(
|
|
||||||
ContractMigrationAuthorization.fromPartial({
|
|
||||||
grants: [
|
|
||||||
{
|
|
||||||
contract,
|
|
||||||
filter: {
|
|
||||||
typeUrl: allowedMessages
|
|
||||||
? '/cosmwasm.wasm.v1.AcceptedMessageKeysFilter'
|
|
||||||
: '/cosmwasm.wasm.v1.AllowAllMessagesFilter',
|
|
||||||
value: allowedMessages
|
|
||||||
? AcceptedMessageKeysFilter.encode({ keys: allowedMessages }).finish()
|
|
||||||
: AllowAllMessagesFilter.encode({}).finish(),
|
|
||||||
},
|
|
||||||
limit:
|
|
||||||
callsRemaining || amounts
|
|
||||||
? {
|
|
||||||
typeUrl:
|
|
||||||
callsRemaining && amounts
|
|
||||||
? '/cosmwasm.wasm.v1.CombinedLimit'
|
|
||||||
: callsRemaining
|
|
||||||
? '/cosmwasm.wasm.v1.MaxCallsLimit'
|
|
||||||
: '/cosmwasm.wasm.v1.MaxFundsLimit',
|
|
||||||
value:
|
|
||||||
callsRemaining && amounts
|
|
||||||
? CombinedLimit.encode({
|
|
||||||
callsRemaining: BigInt(callsRemaining),
|
|
||||||
amounts,
|
|
||||||
}).finish()
|
|
||||||
: callsRemaining
|
|
||||||
? MaxCallsLimit.encode({
|
|
||||||
remaining: BigInt(callsRemaining),
|
|
||||||
}).finish()
|
|
||||||
: MaxFundsLimit.encode({
|
|
||||||
amounts: amounts || [],
|
|
||||||
}).finish(),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
// limit: undefined is not accepted
|
|
||||||
typeUrl: '/cosmwasm.wasm.v1.MaxCallsLimit',
|
|
||||||
value: MaxCallsLimit.encode({
|
|
||||||
remaining: BigInt(100000),
|
|
||||||
}).finish(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
).finish()
|
|
||||||
|
|
||||||
const grantValue = MsgGrant.fromPartial({
|
|
||||||
grant: {
|
|
||||||
authorization: {
|
|
||||||
typeUrl: '/cosmwasm.wasm.v1.ContractMigrationAuthorization',
|
|
||||||
value: sendAuthValue,
|
|
||||||
},
|
|
||||||
expiration: expiration ? { seconds: BigInt(expiration), nanos: 0 } : undefined,
|
|
||||||
},
|
|
||||||
grantee,
|
|
||||||
granter,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
typeUrl: msgAuthzGrantTypeUrl,
|
|
||||||
value: grantValue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthzGenericGrantMsg(granter: string, grantee: string, typeURL: string, expiration: number): Msg {
|
|
||||||
return {
|
|
||||||
typeUrl: msgAuthzGrantTypeUrl,
|
|
||||||
value: {
|
|
||||||
grant: {
|
|
||||||
authorization: {
|
|
||||||
typeUrl: '/cosmos.authz.v1beta1.GenericAuthorization',
|
|
||||||
value: GenericAuthorization.encode(
|
|
||||||
GenericAuthorization.fromPartial({
|
|
||||||
msg: typeURL,
|
|
||||||
}),
|
|
||||||
).finish(),
|
|
||||||
},
|
|
||||||
expiration: expiration ? { seconds: expiration } : undefined,
|
|
||||||
},
|
|
||||||
grantee,
|
|
||||||
granter,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthzStakeGrantMsg({
|
|
||||||
expiration,
|
|
||||||
grantee,
|
|
||||||
granter,
|
|
||||||
allowList,
|
|
||||||
denyList,
|
|
||||||
maxTokens,
|
|
||||||
denom,
|
|
||||||
stakeAuthzType,
|
|
||||||
}: {
|
|
||||||
granter: string
|
|
||||||
grantee: string
|
|
||||||
expiration: number
|
|
||||||
allowList?: string[]
|
|
||||||
denyList?: string[]
|
|
||||||
maxTokens?: string
|
|
||||||
denom?: string
|
|
||||||
stakeAuthzType: AuthorizationType
|
|
||||||
}): Msg {
|
|
||||||
const allow_list = StakeAuthorization_Validators.encode(
|
|
||||||
StakeAuthorization_Validators.fromPartial({
|
|
||||||
address: allowList,
|
|
||||||
}),
|
|
||||||
).finish()
|
|
||||||
const deny_list = StakeAuthorization_Validators.encode(
|
|
||||||
StakeAuthorization_Validators.fromPartial({
|
|
||||||
address: denyList,
|
|
||||||
}),
|
|
||||||
).finish()
|
|
||||||
const stakeAuthValue = StakeAuthorization.encode(
|
|
||||||
StakeAuthorization.fromPartial({
|
|
||||||
authorizationType: stakeAuthzType,
|
|
||||||
allowList: allowList?.length ? StakeAuthorization_Validators.decode(allow_list) : undefined,
|
|
||||||
denyList: denyList?.length ? StakeAuthorization_Validators.decode(deny_list) : undefined,
|
|
||||||
maxTokens: maxTokens
|
|
||||||
? Coin.fromPartial({
|
|
||||||
amount: maxTokens,
|
|
||||||
denom,
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
}),
|
|
||||||
).finish()
|
|
||||||
const grantValue = MsgGrant.fromPartial({
|
|
||||||
grant: {
|
|
||||||
authorization: {
|
|
||||||
typeUrl: '/cosmos.staking.v1beta1.StakeAuthorization',
|
|
||||||
value: stakeAuthValue,
|
|
||||||
},
|
|
||||||
expiration: { seconds: BigInt(expiration) },
|
|
||||||
},
|
|
||||||
grantee,
|
|
||||||
granter,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
typeUrl: msgAuthzGrantTypeUrl,
|
|
||||||
value: grantValue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"path": "/assets/",
|
"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",
|
||||||
|
|||||||
@ -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
76
config/keplr.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import type { ChainInfo } from '@keplr-wallet/types'
|
||||||
|
|
||||||
|
import type { AppConfig } from './app'
|
||||||
|
|
||||||
|
export interface KeplrCoin {
|
||||||
|
readonly coinDenom: string
|
||||||
|
readonly coinMinimalDenom: string
|
||||||
|
readonly coinDecimals: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeplrConfig {
|
||||||
|
readonly chainId: string
|
||||||
|
readonly chainName: string
|
||||||
|
readonly rpc: string
|
||||||
|
readonly rest?: string
|
||||||
|
readonly bech32Config: {
|
||||||
|
readonly bech32PrefixAccAddr: string
|
||||||
|
readonly bech32PrefixAccPub: string
|
||||||
|
readonly bech32PrefixValAddr: string
|
||||||
|
readonly bech32PrefixValPub: string
|
||||||
|
readonly bech32PrefixConsAddr: string
|
||||||
|
readonly bech32PrefixConsPub: string
|
||||||
|
}
|
||||||
|
readonly currencies: readonly KeplrCoin[]
|
||||||
|
readonly feeCurrencies: readonly KeplrCoin[]
|
||||||
|
readonly stakeCurrency: KeplrCoin
|
||||||
|
readonly gasPriceStep: {
|
||||||
|
readonly low: number
|
||||||
|
readonly average: number
|
||||||
|
readonly high: number
|
||||||
|
}
|
||||||
|
readonly bip44: { readonly coinType: number }
|
||||||
|
readonly coinType: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const keplrConfig = (config: AppConfig): ChainInfo => ({
|
||||||
|
chainId: config.chainId,
|
||||||
|
chainName: config.chainName,
|
||||||
|
rpc: config.rpcUrl,
|
||||||
|
rest: config.httpUrl!,
|
||||||
|
bech32Config: {
|
||||||
|
bech32PrefixAccAddr: `${config.addressPrefix}`,
|
||||||
|
bech32PrefixAccPub: `${config.addressPrefix}pub`,
|
||||||
|
bech32PrefixValAddr: `${config.addressPrefix}valoper`,
|
||||||
|
bech32PrefixValPub: `${config.addressPrefix}valoperpub`,
|
||||||
|
bech32PrefixConsAddr: `${config.addressPrefix}valcons`,
|
||||||
|
bech32PrefixConsPub: `${config.addressPrefix}valconspub`,
|
||||||
|
},
|
||||||
|
currencies: [
|
||||||
|
{
|
||||||
|
coinDenom: config.coinMap[config.feeToken].denom,
|
||||||
|
coinMinimalDenom: config.feeToken,
|
||||||
|
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
feeCurrencies: [
|
||||||
|
{
|
||||||
|
coinDenom: config.coinMap[config.feeToken].denom,
|
||||||
|
coinMinimalDenom: config.feeToken,
|
||||||
|
coinDecimals: config.coinMap[config.feeToken].fractionalDigits,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stakeCurrency: {
|
||||||
|
coinDenom: config.coinMap[config.stakingToken].denom,
|
||||||
|
coinMinimalDenom: config.stakingToken,
|
||||||
|
coinDecimals: config.coinMap[config.stakingToken].fractionalDigits,
|
||||||
|
},
|
||||||
|
gasPriceStep: {
|
||||||
|
low: config.gasPrice / 2,
|
||||||
|
average: config.gasPrice,
|
||||||
|
high: config.gasPrice * 2,
|
||||||
|
},
|
||||||
|
bip44: { coinType: 118 },
|
||||||
|
coinType: 118,
|
||||||
|
features: ['ibc-transfer', 'cosmwasm', 'ibc-go'],
|
||||||
|
})
|
||||||
@ -6,6 +6,6 @@ export const meta = {
|
|||||||
domain: 'stargaze.tools',
|
domain: 'stargaze.tools',
|
||||||
url: faviconsJson.developerURL,
|
url: faviconsJson.developerURL,
|
||||||
twitter: {
|
twitter: {
|
||||||
username: '@StargazeZone',
|
username: '@stargazestudio',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
844
config/minter.ts
844
config/minter.ts
@ -1,844 +0,0 @@
|
|||||||
import {
|
|
||||||
FEATURED_IBC_TIA_FACTORY_ADDRESS,
|
|
||||||
FEATURED_IBC_USDC_FACTORY_ADDRESS,
|
|
||||||
FEATURED_VENDING_FACTORY_ADDRESS,
|
|
||||||
FEATURED_VENDING_FACTORY_FLEX_ADDRESS,
|
|
||||||
FEATURED_VENDING_FACTORY_MERKLE_TREE_ADDRESS,
|
|
||||||
FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
|
||||||
FEATURED_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
|
|
||||||
FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
|
||||||
OPEN_EDITION_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_FACTORY_FLEX_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_CRBRUS_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_KUJI_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_NBTC_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_TIA_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
|
||||||
OPEN_EDITION_IBC_USK_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_NATIVE_BRNCH_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_NATIVE_STRDST_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_UPDATABLE_IBC_NBTC_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_UPDATABLE_IBC_TIA_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
|
|
||||||
OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS,
|
|
||||||
VENDING_FACTORY_ADDRESS,
|
|
||||||
VENDING_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_FACTORY_MERKLE_TREE_ADDRESS,
|
|
||||||
VENDING_FACTORY_UPDATABLE_ADDRESS,
|
|
||||||
VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_ATOM_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_ATOM_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_CRBRUS_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_CRBRUS_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_KUJI_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_KUJI_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_NBTC_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_NBTC_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_NBTC_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_NBTC_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_TIA_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
|
|
||||||
VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_USDC_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_USK_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_USK_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_IBC_USK_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
VENDING_IBC_USK_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
VENDING_NATIVE_BRNCH_FACTORY_ADDRESS,
|
|
||||||
VENDING_NATIVE_BRNCH_FLEX_FACTORY_ADDRESS,
|
|
||||||
VENDING_NATIVE_BRNCH_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
VENDING_NATIVE_STARDUST_FACTORY_ADDRESS,
|
|
||||||
VENDING_NATIVE_STARDUST_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
VENDING_NATIVE_STRDST_FLEX_FACTORY_ADDRESS,
|
|
||||||
} from 'utils/constants'
|
|
||||||
|
|
||||||
import type { TokenInfo } from './token'
|
|
||||||
import {
|
|
||||||
ibcAtom,
|
|
||||||
ibcCrbrus,
|
|
||||||
ibcFrnz,
|
|
||||||
// ibcHuahua,
|
|
||||||
ibcKuji,
|
|
||||||
ibcNbtc,
|
|
||||||
ibcTia,
|
|
||||||
ibcUsdc,
|
|
||||||
ibcUsk,
|
|
||||||
nativeBrnch,
|
|
||||||
nativeStardust,
|
|
||||||
stars,
|
|
||||||
} from './token'
|
|
||||||
|
|
||||||
export interface MinterInfo {
|
|
||||||
id: string
|
|
||||||
factoryAddress: string
|
|
||||||
supportedToken: TokenInfo
|
|
||||||
updatable?: boolean
|
|
||||||
flexible?: boolean
|
|
||||||
merkleTree?: boolean
|
|
||||||
featured?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionStarsMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-stars-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_FACTORY_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionUpdatableStarsMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-updatable-stars-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: true,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionIbcAtomMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-ibc-atom-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_ATOM_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcAtom,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionUpdatableIbcAtomMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-updatable-ibc-atom-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_ATOM_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcAtom,
|
|
||||||
updatable: true,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-ibc-usdc-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_USDC_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-ibc-tia-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_TIA_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionIbcNbtcMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-ibc-nbtc-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_NBTC_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcNbtc,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionUpdatableIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-updatable-ibc-usdc-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_USDC_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: true,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionUpdatableIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-updatable-ibc-tia-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_TIA_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: true,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionUpdatableIbcNbtcMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-updatable-ibc-nbtc-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_NBTC_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcNbtc,
|
|
||||||
updatable: true,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionIbcFrnzMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-ibc-frnz-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcFrnz,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionUpdatableIbcFrnzMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-updatable-ibc-frnz-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcFrnz,
|
|
||||||
updatable: true,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionIbcUskMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-ibc-usk-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_USK_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsk,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionUpdatableIbcUskMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-updatable-ibc-usk-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsk,
|
|
||||||
updatable: true,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionIbcKujiMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-ibc-kuji-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_KUJI_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcKuji,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// export const openEditionIbcHuahuaMinter: MinterInfo = {
|
|
||||||
// id: 'open-edition-ibc-huahua-minter',
|
|
||||||
// factoryAddress: OPEN_EDITION_IBC_HUAHUA_FACTORY_ADDRESS,
|
|
||||||
// supportedToken: ibcHuahua,
|
|
||||||
// updatable: false,
|
|
||||||
// featured: false,
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const openEditionIbcCrbrusMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-ibc-crbrus-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_CRBRUS_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcCrbrus,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionNativeStrdstMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-native-strdst-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_NATIVE_STRDST_FACTORY_ADDRESS,
|
|
||||||
supportedToken: nativeStardust,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionNativeBrnchMinter: MinterInfo = {
|
|
||||||
id: 'open-edition-native-brnch-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_NATIVE_BRNCH_FACTORY_ADDRESS,
|
|
||||||
supportedToken: nativeBrnch,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openEditionMinterList = [
|
|
||||||
openEditionStarsMinter,
|
|
||||||
openEditionUpdatableStarsMinter,
|
|
||||||
openEditionUpdatableIbcAtomMinter,
|
|
||||||
openEditionIbcAtomMinter,
|
|
||||||
openEditionIbcFrnzMinter,
|
|
||||||
openEditionUpdatableIbcFrnzMinter,
|
|
||||||
openEditionIbcUsdcMinter,
|
|
||||||
openEditionUpdatableIbcUsdcMinter,
|
|
||||||
openEditionIbcTiaMinter,
|
|
||||||
openEditionUpdatableIbcTiaMinter,
|
|
||||||
openEditionIbcNbtcMinter,
|
|
||||||
openEditionUpdatableIbcNbtcMinter,
|
|
||||||
openEditionIbcUskMinter,
|
|
||||||
openEditionUpdatableIbcUskMinter,
|
|
||||||
openEditionIbcKujiMinter,
|
|
||||||
// openEditionIbcHuahuaMinter,
|
|
||||||
openEditionIbcCrbrusMinter,
|
|
||||||
openEditionNativeStrdstMinter,
|
|
||||||
openEditionNativeBrnchMinter,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const flexibleOpenEditionStarsMinter: MinterInfo = {
|
|
||||||
id: 'flexible-open-edition-stars-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleOpenEditionIbcAtomMinter: MinterInfo = {
|
|
||||||
id: 'flexible-open-edition-ibc-atom-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcAtom,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleOpenEditionIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'flexible-open-edition-ibc-usdc-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleOpenEditionIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'flexible-open-edition-ibc-tia-minter',
|
|
||||||
factoryAddress: OPEN_EDITION_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: false,
|
|
||||||
featured: false,
|
|
||||||
flexible: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleOpenEditionMinterList = [
|
|
||||||
flexibleOpenEditionStarsMinter,
|
|
||||||
flexibleOpenEditionIbcAtomMinter,
|
|
||||||
flexibleOpenEditionIbcUsdcMinter,
|
|
||||||
flexibleOpenEditionIbcTiaMinter,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const vendingStarsMinter: MinterInfo = {
|
|
||||||
id: 'vending-stars-minter',
|
|
||||||
factoryAddress: VENDING_FACTORY_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingFeaturedStarsMinter: MinterInfo = {
|
|
||||||
id: 'vending-stars-minter',
|
|
||||||
factoryAddress: FEATURED_VENDING_FACTORY_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingUpdatableStarsMinter: MinterInfo = {
|
|
||||||
id: 'vending-updatable-stars-minter',
|
|
||||||
factoryAddress: VENDING_FACTORY_UPDATABLE_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: true,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingIbcAtomMinter: MinterInfo = {
|
|
||||||
id: 'vending-ibc-atom-minter',
|
|
||||||
factoryAddress: VENDING_IBC_ATOM_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcAtom,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingUpdatableIbcAtomMinter: MinterInfo = {
|
|
||||||
id: 'vending-updatable-ibc-atom-minter',
|
|
||||||
factoryAddress: VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcAtom,
|
|
||||||
updatable: true,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'vending-ibc-usdc-minter',
|
|
||||||
factoryAddress: VENDING_IBC_USDC_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingFeaturedIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'vending-featured-ibc-usdc-minter',
|
|
||||||
factoryAddress: FEATURED_IBC_USDC_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'vending-ibc-tia-minter',
|
|
||||||
factoryAddress: VENDING_IBC_TIA_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingFeaturedIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'vending-featured-ibc-tia-minter',
|
|
||||||
factoryAddress: FEATURED_IBC_TIA_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingIbcNbtcMinter: MinterInfo = {
|
|
||||||
id: 'vending-ibc-nbtc-minter',
|
|
||||||
factoryAddress: VENDING_IBC_NBTC_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcNbtc,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingUpdatableIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'vending-updatable-ibc-usdc-minter',
|
|
||||||
factoryAddress: VENDING_IBC_USDC_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: true,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingUpdatableIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'vending-updatable-ibc-tia-minter',
|
|
||||||
factoryAddress: VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: true,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingUpdatableIbcNbtcMinter: MinterInfo = {
|
|
||||||
id: 'vending-updatable-ibc-nbtc-minter',
|
|
||||||
factoryAddress: VENDING_IBC_NBTC_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcNbtc,
|
|
||||||
updatable: true,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingIbcUskMinter: MinterInfo = {
|
|
||||||
id: 'vending-ibc-usk-minter',
|
|
||||||
factoryAddress: VENDING_IBC_USK_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsk,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingUpdatableIbcUskMinter: MinterInfo = {
|
|
||||||
id: 'vending-updatable-ibc-usk-minter',
|
|
||||||
factoryAddress: VENDING_IBC_USK_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcUsk,
|
|
||||||
updatable: true,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingIbcKujiMinter: MinterInfo = {
|
|
||||||
id: 'vending-ibc-kuji-minter',
|
|
||||||
factoryAddress: VENDING_IBC_KUJI_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcKuji,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// export const vendingIbcHuahuaMinter: MinterInfo = {
|
|
||||||
// id: 'vending-ibc-huahua-minter',
|
|
||||||
// factoryAddress: VENDING_IBC_HUAHUA_FACTORY_ADDRESS,
|
|
||||||
// supportedToken: ibcHuahua,
|
|
||||||
// updatable: false,
|
|
||||||
// flexible: false,
|
|
||||||
// merkleTree: false,
|
|
||||||
// featured: false,
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const vendingIbcCrbrusMinter: MinterInfo = {
|
|
||||||
id: 'vending-ibc-crbrus-minter',
|
|
||||||
factoryAddress: VENDING_IBC_CRBRUS_FACTORY_ADDRESS,
|
|
||||||
supportedToken: ibcCrbrus,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingNativeStardustMinter: MinterInfo = {
|
|
||||||
id: 'vending-native-stardust-minter',
|
|
||||||
factoryAddress: VENDING_NATIVE_STARDUST_FACTORY_ADDRESS,
|
|
||||||
supportedToken: nativeStardust,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingUpdatableNativeStardustMinter: MinterInfo = {
|
|
||||||
id: 'vending-native-stardust-minter',
|
|
||||||
factoryAddress: VENDING_NATIVE_STARDUST_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
supportedToken: nativeStardust,
|
|
||||||
updatable: true,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingNativeBrnchMinter: MinterInfo = {
|
|
||||||
id: 'vending-native-brnch-minter',
|
|
||||||
factoryAddress: VENDING_NATIVE_BRNCH_FACTORY_ADDRESS,
|
|
||||||
supportedToken: nativeBrnch,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingUpdatableNativeBrnchMinter: MinterInfo = {
|
|
||||||
id: 'vending-native-brnch-minter',
|
|
||||||
factoryAddress: VENDING_NATIVE_BRNCH_UPDATABLE_FACTORY_ADDRESS,
|
|
||||||
supportedToken: nativeBrnch,
|
|
||||||
updatable: true,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vendingMinterList = [
|
|
||||||
vendingStarsMinter,
|
|
||||||
vendingFeaturedStarsMinter,
|
|
||||||
vendingUpdatableStarsMinter,
|
|
||||||
vendingIbcAtomMinter,
|
|
||||||
vendingUpdatableIbcAtomMinter,
|
|
||||||
vendingIbcUsdcMinter,
|
|
||||||
vendingFeaturedIbcUsdcMinter,
|
|
||||||
vendingUpdatableIbcUsdcMinter,
|
|
||||||
vendingIbcTiaMinter,
|
|
||||||
vendingFeaturedIbcTiaMinter,
|
|
||||||
vendingUpdatableIbcTiaMinter,
|
|
||||||
vendingIbcNbtcMinter,
|
|
||||||
vendingUpdatableIbcNbtcMinter,
|
|
||||||
vendingIbcUskMinter,
|
|
||||||
vendingUpdatableIbcUskMinter,
|
|
||||||
vendingIbcKujiMinter,
|
|
||||||
// vendingIbcHuahuaMinter,
|
|
||||||
vendingIbcCrbrusMinter,
|
|
||||||
vendingNativeStardustMinter,
|
|
||||||
vendingUpdatableNativeStardustMinter,
|
|
||||||
vendingNativeBrnchMinter,
|
|
||||||
vendingUpdatableNativeBrnchMinter,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const flexibleVendingStarsMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-stars-minter',
|
|
||||||
factoryAddress: VENDING_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleFeaturedVendingStarsMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-stars-minter',
|
|
||||||
factoryAddress: FEATURED_VENDING_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingUpdatableStarsMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-updatable-stars-minter',
|
|
||||||
factoryAddress: VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: true,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingIbcAtomMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-ibc-atom-minter',
|
|
||||||
factoryAddress: VENDING_IBC_ATOM_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcAtom,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingUpdatableIbcAtomMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-updatable-ibc-atom-minter',
|
|
||||||
factoryAddress: VENDING_IBC_ATOM_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcAtom,
|
|
||||||
updatable: true,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-ibc-usdc-minter',
|
|
||||||
factoryAddress: VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleFeaturedVendingIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'flexible-featured-vending-ibc-usdc-minter',
|
|
||||||
factoryAddress: FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-ibc-tia-minter',
|
|
||||||
factoryAddress: VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleFeaturedVendingIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'flexible-featured-vending-ibc-tia-minter',
|
|
||||||
factoryAddress: FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingIbcNbtcMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-ibc-nbtc-minter',
|
|
||||||
factoryAddress: VENDING_IBC_NBTC_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcNbtc,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingUpdatableIbcUsdcMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-updatable-ibc-usdc-minter',
|
|
||||||
factoryAddress: VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcUsdc,
|
|
||||||
updatable: true,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingUpdatableIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-updatable-ibc-tia-minter',
|
|
||||||
factoryAddress: VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: true,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingUpdatableIbcNbtcMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-updatable-ibc-nbtc-minter',
|
|
||||||
factoryAddress: VENDING_IBC_NBTC_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcNbtc,
|
|
||||||
updatable: true,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingIbcUskMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-ibc-usk-minter',
|
|
||||||
factoryAddress: VENDING_IBC_USK_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcUsk,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingUpdatableIbcUskMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-updatable-ibc-usk-minter',
|
|
||||||
factoryAddress: VENDING_IBC_USK_UPDATABLE_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcUsk,
|
|
||||||
updatable: true,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingIbcKujiMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-ibc-kuji-minter',
|
|
||||||
factoryAddress: VENDING_IBC_KUJI_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcKuji,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// export const flexibleVendingIbcHuahuaMinter: MinterInfo = {
|
|
||||||
// id: 'flexible-vending-ibc-huahua-minter',
|
|
||||||
// factoryAddress: VENDING_IBC_HUAHUA_FACTORY_FLEX_ADDRESS,
|
|
||||||
// supportedToken: ibcHuahua,
|
|
||||||
// updatable: false,
|
|
||||||
// flexible: true,
|
|
||||||
// merkleTree: false,
|
|
||||||
// featured: false,
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const flexibleVendingIbcCrbrusMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-ibc-crbrus-minter',
|
|
||||||
factoryAddress: VENDING_IBC_CRBRUS_FACTORY_FLEX_ADDRESS,
|
|
||||||
supportedToken: ibcCrbrus,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingStrdstMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-native-strdst-minter',
|
|
||||||
factoryAddress: VENDING_NATIVE_STRDST_FLEX_FACTORY_ADDRESS,
|
|
||||||
supportedToken: nativeStardust,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingBrnchMinter: MinterInfo = {
|
|
||||||
id: 'flexible-vending-native-brnch-minter',
|
|
||||||
factoryAddress: VENDING_NATIVE_BRNCH_FLEX_FACTORY_ADDRESS,
|
|
||||||
supportedToken: nativeBrnch,
|
|
||||||
updatable: false,
|
|
||||||
flexible: true,
|
|
||||||
merkleTree: false,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexibleVendingMinterList = [
|
|
||||||
flexibleVendingStarsMinter,
|
|
||||||
flexibleFeaturedVendingStarsMinter,
|
|
||||||
flexibleVendingUpdatableStarsMinter,
|
|
||||||
flexibleVendingIbcAtomMinter,
|
|
||||||
flexibleVendingUpdatableIbcAtomMinter,
|
|
||||||
flexibleVendingIbcUsdcMinter,
|
|
||||||
flexibleFeaturedVendingIbcUsdcMinter,
|
|
||||||
flexibleVendingUpdatableIbcUsdcMinter,
|
|
||||||
flexibleVendingIbcTiaMinter,
|
|
||||||
flexibleFeaturedVendingIbcTiaMinter,
|
|
||||||
flexibleVendingUpdatableIbcTiaMinter,
|
|
||||||
flexibleVendingIbcNbtcMinter,
|
|
||||||
flexibleVendingUpdatableIbcNbtcMinter,
|
|
||||||
flexibleVendingIbcUskMinter,
|
|
||||||
flexibleVendingUpdatableIbcUskMinter,
|
|
||||||
flexibleVendingIbcKujiMinter,
|
|
||||||
// flexibleVendingIbcHuahuaMinter,
|
|
||||||
flexibleVendingIbcCrbrusMinter,
|
|
||||||
flexibleVendingStrdstMinter,
|
|
||||||
flexibleVendingBrnchMinter,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const merkleTreeVendingStarsMinter: MinterInfo = {
|
|
||||||
id: 'merkletree-vending-stars-minter',
|
|
||||||
factoryAddress: VENDING_FACTORY_MERKLE_TREE_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: true,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const merkleTreeVendingFeaturedStarsMinter: MinterInfo = {
|
|
||||||
id: 'merkletree-vending-featured-stars-minter',
|
|
||||||
factoryAddress: FEATURED_VENDING_FACTORY_MERKLE_TREE_ADDRESS,
|
|
||||||
supportedToken: stars,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: true,
|
|
||||||
featured: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const merkleTreeVendingIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'merkletree-vending-ibc-tia-minter',
|
|
||||||
factoryAddress: VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: true,
|
|
||||||
featured: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const merkleTreeVendingFeaturedIbcTiaMinter: MinterInfo = {
|
|
||||||
id: 'merkletree-vending-featured-ibc-tia-minter',
|
|
||||||
factoryAddress: FEATURED_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS,
|
|
||||||
supportedToken: ibcTia,
|
|
||||||
updatable: false,
|
|
||||||
flexible: false,
|
|
||||||
merkleTree: true,
|
|
||||||
featured: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const merkleTreeVendingMinterList = [
|
|
||||||
merkleTreeVendingStarsMinter,
|
|
||||||
merkleTreeVendingIbcTiaMinter,
|
|
||||||
merkleTreeVendingFeaturedStarsMinter,
|
|
||||||
merkleTreeVendingFeaturedIbcTiaMinter,
|
|
||||||
]
|
|
||||||
@ -5,7 +5,6 @@ export const mainnetConfig: AppConfig = {
|
|||||||
chainName: 'Stargaze',
|
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: {
|
||||||
|
|||||||
135
config/token.ts
135
config/token.ts
@ -1,135 +0,0 @@
|
|||||||
import { NETWORK } from 'utils/constants'
|
|
||||||
|
|
||||||
export interface TokenInfo {
|
|
||||||
id: string
|
|
||||||
denom: string
|
|
||||||
displayName: string
|
|
||||||
decimalPlaces: number
|
|
||||||
imageURL?: string
|
|
||||||
symbol?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const stars: TokenInfo = {
|
|
||||||
id: 'stars',
|
|
||||||
denom: 'ustars',
|
|
||||||
displayName: 'STARS',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ibcAtom: TokenInfo = {
|
|
||||||
id: 'ibc-atom',
|
|
||||||
denom: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2',
|
|
||||||
displayName: 'ATOM',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ibcUsdc: TokenInfo = {
|
|
||||||
id: 'ibc-usdc',
|
|
||||||
denom:
|
|
||||||
NETWORK === 'mainnet'
|
|
||||||
? 'ibc/4A1C18CA7F50544760CF306189B810CE4C1CB156C7FC870143D401FE7280E591'
|
|
||||||
: 'factory/stars1paqkeyluuw47pflgwwqaaj8y679zj96aatg5a7/uusdc',
|
|
||||||
displayName: 'USDC',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
export const ibcUsk: TokenInfo = {
|
|
||||||
id: 'ibc-usk',
|
|
||||||
denom:
|
|
||||||
NETWORK === 'mainnet'
|
|
||||||
? 'ibc/938CEB62ABCBA6366AA369A8362E310B2A0B1A54835E4F3FF01D69D860959128'
|
|
||||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uusk',
|
|
||||||
displayName: 'USK',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ibcKuji: TokenInfo = {
|
|
||||||
id: 'ibc-kuji',
|
|
||||||
denom:
|
|
||||||
NETWORK === 'mainnet'
|
|
||||||
? 'ibc/0E57658B71E9CC4BB0F6FE3E01712966713B49E6FD292E6B66E3F111B103D361'
|
|
||||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/ukuji',
|
|
||||||
displayName: 'KUJI',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ibcFrnz: TokenInfo = {
|
|
||||||
id: 'ibc-frnz',
|
|
||||||
denom:
|
|
||||||
NETWORK === 'mainnet'
|
|
||||||
? 'ibc/9C40A8368C0E1CAA4144DBDEBBCE2E7A5CC2D128F0A9F785ECB71ECFF575114C'
|
|
||||||
: 'factory/stars1paqkeyluuw47pflgwwqaaj8y679zj96aatg5a7/ufrienzies',
|
|
||||||
displayName: 'FRNZ',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ibcNbtc: TokenInfo = {
|
|
||||||
id: 'ibc-nBTC',
|
|
||||||
denom: NETWORK === 'mainnet' ? 'Not available' : 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/unbtc',
|
|
||||||
displayName: 'nBTC',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
// export const ibcHuahua: TokenInfo = {
|
|
||||||
// id: 'ibc-huahua',
|
|
||||||
// denom:
|
|
||||||
// NETWORK === 'mainnet'
|
|
||||||
// ? 'ibc/CAD8A9F306CAAC55731C66930D6BEE539856DD12E59061C965E44D82AA26A0E7'
|
|
||||||
// : 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uhuahua',
|
|
||||||
// displayName: 'HUAHUA',
|
|
||||||
// decimalPlaces: 6,
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const ibcCrbrus: TokenInfo = {
|
|
||||||
id: 'ibc-crbrus',
|
|
||||||
denom:
|
|
||||||
NETWORK === 'mainnet'
|
|
||||||
? 'ibc/71CEEB5CC09F75A3ACDC417108C14514351B6B2A540ACE9B37A80BF930845134'
|
|
||||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uCRBRUS',
|
|
||||||
displayName: 'CRBRUS',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ibcTia: TokenInfo = {
|
|
||||||
id: 'ibc-tia',
|
|
||||||
denom:
|
|
||||||
NETWORK === 'mainnet'
|
|
||||||
? 'ibc/14D1406D84227FDF4B055EA5CB2298095BBCA3F3BC3EF583AE6DF36F0FB179C8'
|
|
||||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/utia',
|
|
||||||
displayName: 'TIA',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const nativeStardust: TokenInfo = {
|
|
||||||
id: 'native-strdst',
|
|
||||||
denom:
|
|
||||||
NETWORK === 'mainnet'
|
|
||||||
? 'factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust'
|
|
||||||
: 'factory/stars18vxuarvh44wxltxqsyac36972nvaqc377sdh40/dust',
|
|
||||||
displayName: 'STRDST',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const nativeBrnch: TokenInfo = {
|
|
||||||
id: 'native-brnch',
|
|
||||||
denom:
|
|
||||||
NETWORK === 'mainnet'
|
|
||||||
? 'factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/uBRNCH'
|
|
||||||
: 'factory/stars153w5xhuqu3et29lgqk4dsynj6gjn96lr33wx4e/uBRNCH',
|
|
||||||
displayName: 'BRNCH',
|
|
||||||
decimalPlaces: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tokensList = [
|
|
||||||
stars,
|
|
||||||
ibcAtom,
|
|
||||||
ibcUsdc,
|
|
||||||
ibcUsk,
|
|
||||||
ibcFrnz,
|
|
||||||
ibcNbtc,
|
|
||||||
ibcKuji,
|
|
||||||
// ibcHuahua,
|
|
||||||
ibcCrbrus,
|
|
||||||
ibcTia,
|
|
||||||
nativeStardust,
|
|
||||||
nativeBrnch,
|
|
||||||
]
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { create } from 'zustand'
|
import create from 'zustand'
|
||||||
|
|
||||||
export const useCollectionStore = create(() => ({
|
export const useCollectionStore = create(() => ({
|
||||||
name: 'Example',
|
name: 'Example',
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
import { create } from 'zustand'
|
|
||||||
|
|
||||||
export type Timezone = 'UTC' | 'Local'
|
|
||||||
|
|
||||||
export const useGlobalSettings = create(() => ({
|
|
||||||
timezone: 'UTC' as Timezone,
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const setTimezone = (timezone: Timezone) => {
|
|
||||||
useGlobalSettings.setState({ timezone })
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import { create } from 'zustand'
|
|
||||||
|
|
||||||
export interface LogItem {
|
|
||||||
id: string
|
|
||||||
message: string
|
|
||||||
type?: string
|
|
||||||
timestamp?: Date
|
|
||||||
code?: number
|
|
||||||
source?: string
|
|
||||||
connectedWallet?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useLogStore = create(() => ({
|
|
||||||
itemList: [] as LogItem[],
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const setLogItemList = (list: LogItem[]) => {
|
|
||||||
useLogStore.setState({ itemList: list })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addLogItem = (item: LogItem) => {
|
|
||||||
useLogStore.setState((prev) => ({ itemList: [...prev.itemList, item] }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeLogItem = (id: string) => {
|
|
||||||
useLogStore.setState((prev) => ({ itemList: prev.itemList.filter((item) => item.id !== id) }))
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user