/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable no-nested-ternary */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ //import { coin } from '@cosmjs/proto-signing' import { Sidetab } from '@typeform/embed-react' import clsx from 'clsx' import { Alert } from 'components/Alert' import { Anchor } from 'components/Anchor' import { BadgeConfirmationModal } from 'components/BadgeConfirmationModal' import { BadgeLoadingModal } from 'components/BadgeLoadingModal' import type { BadgeDetailsDataProps } from 'components/badges/creation/BadgeDetails' import { BadgeDetails } from 'components/badges/creation/BadgeDetails' import type { ImageUploadDetailsDataProps, MintRule } from 'components/badges/creation/ImageUploadDetails' import { ImageUploadDetails } from 'components/badges/creation/ImageUploadDetails' import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' import { TextInput } from 'components/forms/FormInput' import { useInputState } from 'components/forms/FormInput.hooks' import { Tooltip } from 'components/Tooltip' import { useContracts } from 'contexts/contracts' import { addLogItem } from 'contexts/log' import type { Badge } from 'contracts/badgeHub' import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' import { dispatchExecute as badgeHubDispatchExecute } from 'contracts/badgeHub/messages/execute' import * as crypto from 'crypto' import { toPng } from 'html-to-image' import type { NextPage } from 'next' import { NextSeo } from 'next-seo' import sizeof from 'object-sizeof' import { QRCodeSVG } from 'qrcode.react' import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'react-hot-toast' import { FaCopy, FaSave } from 'react-icons/fa' import * as secp256k1 from 'secp256k1' import { upload } from 'services/upload' import { copy } from 'utils/clipboard' import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK } from 'utils/constants' import { getAssetType } from 'utils/getAssetType' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' import { uid } from 'utils/random' import { resolveAddress } from 'utils/resolveAddress' import { truncateMiddle } from 'utils/text' import { useWallet } from 'utils/wallet' import { generateKeyPairs } from '../../utils/hash' const BadgeCreationPage: NextPage = () => { const wallet = useWallet() const { badgeHub: badgeHubContract } = useContracts() const scrollRef = useRef(null) const badgeHubMessages = useMemo(() => badgeHubContract?.use(BADGE_HUB_ADDRESS), [badgeHubContract, wallet.address]) const [imageUploadDetails, setImageUploadDetails] = useState(null) const [badgeDetails, setBadgeDetails] = useState(null) const [uploading, setUploading] = useState(false) const [creatingBadge, setCreatingBadge] = useState(false) const [isAddingKeysComplete, setIsAddingKeysComplete] = useState(false) const [readyToCreateBadge, setReadyToCreateBadge] = useState(false) const [mintRule, setMintRule] = useState('by_key') const [resolvedMinterAddress, setResolvedMinterAddress] = useState('') const [tempBadge, setTempBadge] = useState() const [badgeId, setBadgeId] = useState(null) const [imageUrl, setImageUrl] = useState(null) const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) const [transactionHash, setTransactionHash] = useState(null) const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([]) const [numberOfKeys, setNumberOfKeys] = useState(1) const qrRef = useRef(null) const keyState = useInputState({ id: 'key', name: 'key', title: 'Public Key', subtitle: 'Part of the key pair to be utilized for post-creation access control', }) const designatedMinterState = useInputState({ id: 'designatedMinter', name: 'designatedMinter', title: 'Minter Address', subtitle: 'The address of the designated minter for this badge', defaultValue: wallet.address, }) const performBadgeCreationChecks = () => { try { setReadyToCreateBadge(false) checkImageUploadDetails() checkBadgeDetails() setTimeout(() => { setReadyToCreateBadge(true) }, 100) } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setReadyToCreateBadge(false) } } const handleImageUrl = async () => { try { setImageUrl(null) setBadgeId(null) setTransactionHash(null) if (imageUploadDetails?.uploadMethod === 'new') { setUploading(true) const coverUrl = await upload( [imageUploadDetails.assetFile] as File[], imageUploadDetails.uploadService, 'cover', imageUploadDetails.nftStorageApiKey as string, imageUploadDetails.pinataApiKey as string, imageUploadDetails.pinataSecretKey as string, ).then((imageBaseUrl) => { setUploading(false) return `ipfs://${imageBaseUrl}/${imageUploadDetails.assetFile?.name as string}` }) setImageUrl(coverUrl) return coverUrl } setImageUrl(imageUploadDetails?.imageUrl as string) return imageUploadDetails?.imageUrl as string } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setCreatingBadge(false) setUploading(false) throw new Error("Couldn't upload the image.") } } const resolveMinterAddress = async () => { await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => { setResolvedMinterAddress(resolvedAddress) }) } useEffect(() => { void resolveMinterAddress() }, [designatedMinterState.value]) useEffect(() => { const badge = { manager: badgeDetails?.manager as string, metadata: { name: badgeDetails?.name || undefined, description: badgeDetails?.description || undefined, image: imageUrl || undefined, image_data: badgeDetails?.image_data || undefined, external_url: badgeDetails?.external_url || undefined, attributes: badgeDetails?.attributes || undefined, background_color: badgeDetails?.background_color || undefined, animation_url: badgeDetails?.animation_url || undefined, youtube_url: badgeDetails?.youtube_url || undefined, }, transferrable: badgeDetails?.transferrable as boolean, rule: mintRule === 'by_key' ? { by_key: keyState.value, } : mintRule === 'by_minter' ? { by_minter: resolvedMinterAddress, } : 'by_keys', expiry: badgeDetails?.expiry || undefined, max_supply: badgeDetails?.max_supply || undefined, } setTempBadge(badge) }, [badgeDetails, keyState.value, mintRule, resolvedMinterAddress, imageUrl]) const createNewBadge = async () => { try { if (!wallet.isWalletConnected) throw new Error('Wallet not connected') if (!badgeHubContract) throw new Error('Contract not found') setCreatingBadge(true) const coverUrl = await handleImageUrl() const badge = { manager: badgeDetails?.manager as string, metadata: { name: badgeDetails?.name || undefined, description: badgeDetails?.description?.replaceAll('\\n', '\n') || undefined, image: coverUrl || undefined, image_data: badgeDetails?.image_data || undefined, external_url: badgeDetails?.external_url || undefined, attributes: badgeDetails?.attributes || undefined, background_color: badgeDetails?.background_color || undefined, animation_url: badgeDetails?.animation_url ? badgeDetails.animation_url : imageUploadDetails?.assetFile && getAssetType(imageUploadDetails.assetFile.name) === 'video' ? coverUrl : undefined, youtube_url: badgeDetails?.youtube_url || undefined, }, transferrable: badgeDetails?.transferrable as boolean, rule: mintRule === 'by_key' ? { by_key: keyState.value, } : mintRule === 'by_minter' ? { by_minter: resolvedMinterAddress, } : 'by_keys', expiry: badgeDetails?.expiry || undefined, max_supply: badgeDetails?.max_supply || undefined, } const payload: BadgeHubDispatchExecuteArgs = { contract: BADGE_HUB_ADDRESS, messages: badgeHubMessages, txSigner: wallet.address || '', badge, type: 'create_badge', } if (mintRule !== 'by_keys') { setBadgeId(null) setIsAddingKeysComplete(false) const data = await badgeHubDispatchExecute(payload) console.log(data) setCreatingBadge(false) setTransactionHash(data.split(':')[0]) setBadgeId(data.split(':')[1]) } else { setBadgeId(null) setIsAddingKeysComplete(false) setKeyPairs([]) const generatedKeyPairs = generateKeyPairs(numberOfKeys) setKeyPairs(generatedKeyPairs) await badgeHubDispatchExecute(payload) .then(async (data) => { setCreatingBadge(false) setTransactionHash(data.split(':')[0]) setBadgeId(data.split(':')[1]) const res = await toast.promise( badgeHubContract.use(BADGE_HUB_ADDRESS)?.addKeys( wallet.address || '', Number(data.split(':')[1]), generatedKeyPairs.map((key) => key.publicKey), ) as Promise, { loading: 'Adding keys...', success: (result) => { setIsAddingKeysComplete(true) return `Keys added successfully! Tx Hash: ${result}` }, error: (error: { message: any }) => `Failed to add keys: ${error.message}`, }, ) }) .catch((error: { message: any }) => { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setIsAddingKeysComplete(false) setCreatingBadge(false) }) } } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setCreatingBadge(false) setUploading(false) } } const checkImageUploadDetails = () => { if (!wallet.isWalletConnected) throw new Error('Wallet not connected.') if (!imageUploadDetails) { throw new Error('Please specify the image related details.') } if (imageUploadDetails.uploadMethod === 'new' && imageUploadDetails.assetFile === undefined) { throw new Error('Please select the image file') } if (imageUploadDetails.uploadMethod === 'new') { if (imageUploadDetails.uploadService === 'nft-storage') { if (imageUploadDetails.nftStorageApiKey === '') { throw new Error('Please enter a valid NFT.Storage API key') } } else if (imageUploadDetails.pinataApiKey === '' || imageUploadDetails.pinataSecretKey === '') { throw new Error('Please enter Pinata API and secret keys') } } if (imageUploadDetails.uploadMethod === 'existing' && !imageUploadDetails.imageUrl?.includes('ipfs://')) { throw new Error('Please specify a valid image URL') } } const checkBadgeDetails = () => { if (!badgeDetails) throw new Error('Please fill out the required fields') if (mintRule === 'by_key' && (keyState.value === '' || !createdBadgeKey)) throw new Error('Please generate a public key') if (badgeDetails.external_url) { try { const url = new URL(badgeDetails.external_url) } catch (e: any) { throw new Error(`Invalid external url: Make sure to include the protocol (e.g. https://)`) addLogItem({ id: uid(), message: 'Invalid external url: Make sure to include the protocol (e.g. https://)', type: 'Error', timestamp: new Date(), }) } } } const handleGenerateKey = () => { let privKey: Buffer do { privKey = crypto.randomBytes(32) } while (!secp256k1.privateKeyVerify(privKey)) const privateKey = privKey.toString('hex') setCreatedBadgeKey(privateKey) console.log('Private Key: ', privateKey) const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') setBadgeId(null) keyState.onChange(publicKey) } const handleDownloadQr = async () => { const qrElement = qrRef.current await toPng(qrElement as HTMLElement).then((dataUrl) => { const link = document.createElement('a') link.download = `badge-${badgeId as string}.png` link.href = dataUrl link.click() }) } 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 as string}-keys.json` document.body.appendChild(element) element.click() } const copyClaimURL = async () => { const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone' const claimURL = `${baseURL}/?id=${badgeId as string}&key=${createdBadgeKey as string}` await navigator.clipboard.writeText(claimURL) toast.success('Copied claim URL to clipboard') } const checkwalletBalance = () => { if (!wallet.isWalletConnected) throw new Error('Wallet not connected.') // TODO: estimate creation cost and check wallet balance } useEffect(() => { if (badgeId !== null) scrollRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [badgeId]) useEffect(() => { setImageUrl(imageUploadDetails?.imageUrl as string) }, [imageUploadDetails?.imageUrl]) useEffect(() => { setBadgeId(null) setReadyToCreateBadge(false) }, [imageUploadDetails?.uploadMethod]) useEffect(() => { if (keyPairs.length > 0) { toast.success('Key pairs generated successfully.') } }, [keyPairs]) return (

Create Badge

Make sure you check our{' '} documentation {' '} on how to create a new badge.

Badge ID:{` ${badgeId as string}`}
Private Key:
Transaction Hash: {' '} {transactionHash} {transactionHash}
You may click{' '} here {' '} or scan the QR code to claim a badge.

You may download the QR code or copy the claim URL to share with others.

Badge ID:{` ${badgeId as string}`}
Transaction Hash: {' '} {transactionHash} {transactionHash}
Make sure to download the whitelisted keys added during badge creation.
You may click{' '} here {' '} and select Actions {'>'} Add Keys to add (additional) whitelisted keys or select Actions {'>'}{' '} Mint by Keys to use one of the keys to mint a badge.
Badge ID:{` ${badgeId as string}`}
Designated Minter Address: {` ${resolvedMinterAddress}`}
Transaction Hash: {' '} {transactionHash} {transactionHash}
You may click{' '} here {' '} and select Actions {'>'} Mint By Minter to mint a badge.
Number of Keys The number of key pairs to be whitelisted for post-creation access control
setNumberOfKeys(Number(e.target.value))} required type="number" value={numberOfKeys} />
) } export default withMetadata(BadgeCreationPage, { center: false })