Merge pull request #103 from public-awesome/badge-integration

Badge integration
This commit is contained in:
Adnan Deniz corlu 2023-02-23 15:45:37 +03:00 committed by GitHub
commit d308c90fb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 5339 additions and 85 deletions

View File

@ -1,4 +1,4 @@
APP_VERSION=0.4.4
APP_VERSION=0.4.5
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_SG721_CODE_ID=793
@ -8,10 +8,15 @@ NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1c6juqgd7cm80afpmuszun66rl9zdc4kgfht8fk34
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
NEXT_PUBLIC_BASE_MINTER_CODE_ID=613
NEXT_PUBLIC_WHITELIST_CODE_ID=277
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_API_URL=https://nft-api.elgafar-1.stargaze-apis.com
NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze
NEXT_PUBLIC_NETWORK=testnet
NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev
NEXT_PUBLIC_BADGES_URL=https://badges.publicawesome.dev
NEXT_PUBLIC_WEBSITE_URL=https://

View File

@ -0,0 +1,126 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import React, { useState } from 'react'
import { toast } from 'react-hot-toast'
import { useWallet } from '../contexts/wallet'
import { SG721_NAME_ADDRESS } from '../utils/constants'
import { isValidAddress } from '../utils/isValidAddress'
interface BadgeAirdropListUploadProps {
onChange: (data: string[]) => void
}
export const BadgeAirdropListUpload = ({ onChange }: BadgeAirdropListUploadProps) => {
const wallet = useWallet()
const [resolvedAddresses, setResolvedAddresses] = useState<string[]>([])
const resolveAddresses = async (names: string[]) => {
await new Promise((resolve) => {
let i = 0
names.map(async (name) => {
if (!wallet.client) throw new Error('Wallet not connected')
await wallet.client
.queryContractRaw(
SG721_NAME_ADDRESS,
toUtf8(
Buffer.from(
`0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`,
'hex',
).toString(),
),
)
.then((res) => {
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
if (tokenUri && isValidAddress(tokenUri)) resolvedAddresses.push(tokenUri)
else toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`)
})
.catch((e) => {
console.log(e)
toast.error(`Error resolving address for the name: ${name}.stars`)
})
i++
if (i === names.length) resolve(resolvedAddresses)
})
})
return resolvedAddresses
}
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setResolvedAddresses([])
if (!event.target.files) return toast.error('Error opening file')
if (event.target.files.length !== 1) {
toast.error('No file selected')
return onChange([])
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]?.type !== 'text/plain') {
toast.error('Invalid file type')
return onChange([])
}
const reader = new FileReader()
reader.onload = async (e: ProgressEvent<FileReader>) => {
const text = e.target?.result?.toString()
let newline = '\n'
if (text?.includes('\r')) newline = '\r'
if (text?.includes('\r\n')) newline = '\r\n'
const cleanText = text?.toLowerCase().replace(/,/g, '').replace(/"/g, '').replace(/'/g, '').replace(/ /g, '')
const data = cleanText?.split(newline)
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
const printableData = data?.map((item) => item.replace(regex, ''))
const names = printableData?.filter((address) => address !== '' && address.endsWith('.stars'))
const strippedNames = names?.map((name) => name.split('.')[0])
console.log(names)
if (strippedNames?.length) {
await toast
.promise(resolveAddresses(strippedNames), {
loading: 'Resolving addresses...',
success: 'Address resolution finalized.',
error: 'Address resolution failed!',
})
.then((addresses) => {
console.log(addresses)
})
.catch((error) => {
console.log(error)
})
}
return onChange([
...new Set(
printableData
?.filter((address) => address !== '' && isValidAddress(address) && address.startsWith('stars'))
.concat(resolvedAddresses) || [],
),
])
}
reader.readAsText(event.target.files[0])
}
return (
<div
className={clsx(
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept=".txt"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="badge-airdrop-list-file"
multiple={false}
onChange={onFileChange}
type="file"
/>
</div>
)
}

View File

@ -0,0 +1,67 @@
import { useState } from 'react'
import { Button } from './Button'
export interface BadgeConfirmationModalProps {
confirm: () => void
}
export const BadgeConfirmationModal = (props: BadgeConfirmationModalProps) => {
const [isChecked, setIsChecked] = useState(false)
return (
<div>
<input className="modal-toggle" defaultChecked id="my-modal-2" type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-2">
<label
className="absolute top-[25%] bottom-5 left-1/3 max-w-[600px] max-h-[400px] border-2 no-scrollbar modal-box"
htmlFor="temp"
>
{/* <Alert type="warning"></Alert> */}
<div className="text-xl font-bold">
<div className="text-sm font-thin">
You represent and warrant that you have, or have obtained, all rights, licenses, consents, permissions,
power and/or authority necessary to grant the rights granted herein for any content that you create,
submit, post, promote, or display on or through the Service. You represent and warrant that such contain
material subject to copyright, trademark, publicity rights, or other intellectual property rights, unless
you have necessary permission or are otherwise legally entitled to post the material and to grant Stargaze
Parties the license described above, and that the content does not violate any laws.
</div>
<br />
<div className="flex flex-row pb-4">
<label className="flex flex-col space-y-1" htmlFor="terms">
<span className="text-sm font-light text-white">I agree with the terms above.</span>
</label>
<input
checked={isChecked}
className="p-2 mb-1 ml-2"
id="terms"
name="terms"
onClick={() => setIsChecked(!isChecked)}
type="checkbox"
/>
</div>
<br />
Are you sure to proceed with creating a new badge?
</div>
<div className="flex justify-end w-full">
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">
<label
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 rounded border-0 btn modal-button"
htmlFor="my-modal-2"
>
Go Back
</label>
</Button>
<Button className="px-0 mt-4 mb-4 max-h-12" isDisabled={!isChecked} onClick={props.confirm}>
<label
className="w-full h-full text-white bg-plumbus hover:bg-plumbus-light border-0 btn modal-button"
htmlFor="my-modal-2"
>
Confirm
</label>
</Button>
</div>
</label>
</label>
</div>
)
}

View File

@ -0,0 +1,11 @@
export const BadgeLoadingModal = () => {
return (
<div
className="flex overflow-hidden fixed top-0 right-0 bottom-0 left-0 z-50 flex-col justify-center items-center w-full h-screen bg-gray-900 opacity-80"
style={{ margin: 0 }}
>
<img alt="Pixel Logo" className="mb-5 w-[50px] h-[50px] animate-spin" src="/icon.svg" />
<p className="w-1/3 font-bold text-center text-white">Uploading the image for badge creation, please wait...</p>
</div>
)
}

View File

@ -53,9 +53,9 @@ export const IncomeDashboardDisclaimer = (props: IncomeDashboardDisclaimerProps)
Are you sure to proceed to the Creator Income 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-600">
<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-500 border-0 btn modal-button"
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

View File

@ -81,3 +81,26 @@ export const whitelistLinkTabs: LinkTabProps[] = [
href: '/contracts/whitelist/execute',
},
]
export const badgeHubLinkTabs: LinkTabProps[] = [
{
title: 'Instantiate',
description: `Initialize a new Badge Hub contract`,
href: '/contracts/badgeHub/instantiate',
},
{
title: 'Query',
description: `Dispatch queries for your Badge Hub contract`,
href: '/contracts/badgeHub/query',
},
{
title: 'Execute',
description: `Execute Badge Hub contract actions`,
href: '/contracts/badgeHub/execute',
},
{
title: 'Migrate',
description: `Migrate Badge Hub contract`,
href: '/contracts/badgeHub/migrate',
},
]

View File

@ -1,39 +1,24 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import clsx from 'clsx'
import { Anchor } from 'components/Anchor'
import { useWallet } from 'contexts/wallet'
import Link from 'next/link'
import { useRouter } from 'next/router'
// import BrandText from 'public/brand/brand-text.svg'
import { footerLinks, socialsLinks } from 'utils/links'
import { BASE_FACTORY_ADDRESS, NETWORK } from '../utils/constants'
import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS, NETWORK } from '../utils/constants'
import { Conditional } from './Conditional'
import { IncomeDashboardDisclaimer } from './IncomeDashboardDisclaimer'
import { SidebarLayout } from './SidebarLayout'
import { WalletLoader } from './WalletLoader'
const routes = [
{ text: 'Collections', href: `/collections/`, isChild: false },
{ text: 'Create a Collection', href: `/collections/create/`, isChild: true },
{ text: 'My Collections', href: `/collections/myCollections/`, isChild: true },
{ text: 'Collection Actions', href: `/collections/actions/`, isChild: true },
{ text: 'Creator Income Dashboard', href: `/`, isChild: true },
{ text: 'Contract Dashboards', href: `/contracts/`, isChild: false },
{ text: 'Base Minter Contract', href: `/contracts/baseMinter/`, isChild: true },
{ text: 'Vending Minter Contract', href: `/contracts/vendingMinter/`, isChild: true },
{ text: 'SG721 Contract', href: `/contracts/sg721/`, isChild: true },
{ text: 'Whitelist Contract', href: `/contracts/whitelist/`, isChild: true },
]
export const Sidebar = () => {
const router = useRouter()
const wallet = useWallet()
let tempRoutes = routes
if (BASE_FACTORY_ADDRESS === undefined) {
tempRoutes = routes.filter((route) => route.href !== '/contracts/baseMinter/')
}
return (
<SidebarLayout>
{/* Stargaze brand as home button */}
@ -44,46 +29,171 @@ export const Sidebar = () => {
{/* wallet button */}
<WalletLoader />
{/* main navigation routes */}
{tempRoutes.map(({ text, href, isChild }) =>
text !== 'Creator Income Dashboard' ? (
<Anchor
key={href}
className={clsx(
'px-2 -mx-5 font-extrabold uppercase rounded-lg', // styling
'hover:bg-white/5 transition-colors', // hover styling
{ 'py-0 -ml-2 text-sm font-bold': isChild },
{
'text-gray hover:text-white':
!router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild,
},
{
'text-stargaze':
router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild,
}, // active route styling
// { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling
)}
href={href}
>
{text}
</Anchor>
) : NETWORK === 'mainnet' ? (
<button
className={clsx(
'font-extrabold uppercase bg-clip-text border-none', // styling
'text-gray hover:text-white hover:bg-white/5 transition-colors', // hover styling
'py-0 -mt-3 -ml-11 text-sm font-bold',
)}
type="button"
>
<label
className="w-full h-full text-gray hover:text-white bg-clip-text bg-transparent hover:bg-white/5 border-none btn modal-button"
htmlFor="my-modal-1"
<div className="absolute top-[20%] left-[5%]">
<ul className="group p-2 w-full bg-transparent menu rounded-box">
<li tabIndex={0}>
<div
className={clsx(
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
'hover:bg-white/5 transition-colors',
router.asPath.includes('/collections/') ? 'text-white' : 'text-gray',
)}
>
Income Dashboard
</label>
</button>
) : null,
)}
<Link href="/collections/" passHref>
Collections
</Link>
</div>
<ul className="z-50 p-2 bg-base-200">
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/collections/create') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/collections/create/">Create a Collection</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/collections/myCollections/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/collections/myCollections/">My Collections</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/collections/actions/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/collections/actions/">Collection Actions</Link>
</li>
<Conditional test={NETWORK === 'mainnet'}>
<li className={clsx('text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded')} tabIndex={-1}>
<label
className="w-full h-full text-lg font-bold text-gray hover:text-white normal-case bg-clip-text bg-transparent border-none animate-none btn modal-button"
htmlFor="my-modal-1"
>
Income Dashboard
</label>
</li>
</Conditional>
</ul>
</li>
</ul>
<Conditional test={BADGE_HUB_ADDRESS !== undefined}>
<ul className="group p-2 w-full bg-transparent menu rounded-box">
<li tabIndex={0}>
<span
className={clsx(
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
'hover:bg-white/5 transition-colors',
router.asPath.includes('/badges/') ? 'text-white' : 'text-gray',
)}
>
<Link href="/badges/"> Badges </Link>
</span>
<ul className="z-50 p-2 rounded-box bg-base-200">
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/badges/create/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/badges/create/">Create a Badge</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/badges/myBadges/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/badges/myBadges/">My Badges</Link>
</li>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/badges/actions/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/badges/actions/">Badge Actions</Link>
</li>
</ul>
</li>
</ul>
</Conditional>
<ul className="group p-2 w-full bg-transparent menu rounded-box">
<li tabIndex={0}>
<span
className={clsx(
'z-40 text-xl font-bold group-hover:text-white bg-transparent rounded-lg small-caps',
'hover:bg-white/5 transition-colors',
router.asPath.includes('/contracts/') ? 'text-white' : 'text-gray',
)}
>
<Link href="/contracts/"> Contract Dashboards </Link>
</span>
<ul className="z-50 p-2 bg-base-200">
<Conditional test={BASE_FACTORY_ADDRESS !== undefined}>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/baseMinter/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/baseMinter/">Base Minter Contract</Link>
</li>
</Conditional>
<li
className={clsx(
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
router.asPath.includes('/contracts/vendingMinter/') ? 'text-white' : 'text-gray',
)}
tabIndex={-1}
>
<Link href="/contracts/vendingMinter/">Vending Minter Contract</Link>
</li>
<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>
</ul>
</li>
</ul>
</div>
<IncomeDashboardDisclaimer creatorAddress={wallet.address ? wallet.address : ''} />

View File

@ -15,7 +15,7 @@ export const SidebarLayout = ({ children }: SidebarLayoutProps) => {
{/* fixed component */}
<div
className={clsx(
'overflow-auto fixed top-0 left-0 min-w-[250px] max-w-[250px] no-scrollbar',
'overflow-x-visible fixed top-0 left-0 min-w-[250px] max-w-[250px] no-scrollbar',
'border-r-[1px] border-r-plumbus-light',
{ 'translate-x-[-230px]': !isOpen },
)}

View File

@ -7,7 +7,7 @@ import { getAssetType } from 'utils/getAssetType'
export interface SingleAssetPreviewProps {
subtitle: ReactNode
relatedAsset?: File
updateMetadataFileIndex: (index: number) => void
updateMetadataFileIndex?: (index: number) => void
children?: ReactNode
}

View File

@ -16,7 +16,7 @@ export const WalletLoader = () => {
const displayName = useWalletStore((store) => store.name || getShortAddress(store.address))
return (
<Popover className="my-8">
<Popover className="mt-8 mb-2">
{({ close }) => (
<>
<div className="grid -mx-4">
@ -44,7 +44,7 @@ export const WalletLoader = () => {
>
<Popover.Panel
className={clsx(
'absolute inset-x-4 mt-2',
'absolute inset-x-4 z-50 mt-2',
'bg-stone-800/80 rounded shadow-lg shadow-black/90 backdrop-blur-sm',
'flex flex-col items-stretch text-sm divide-y divide-white/10',
)}

View File

@ -0,0 +1,550 @@
/* 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 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 { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
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 { useWallet } from 'contexts/wallet'
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
import * as crypto from 'crypto'
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 { sha256 } from 'utils/hash'
import { resolveAddress } from 'utils/resolveAddress'
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
import { AddressInput, 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<string[]>([])
const [signature, setSignature] = useState<string>('')
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 pubkeyState = useInputState({
id: 'pubkey',
name: 'pubkey',
title: 'Pubkey',
subtitle: '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: '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',
})
const showMetadataField = isEitherType(type, ['edit_badge'])
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key'])
const showAirdropFileField = isEitherType(type, ['airdrop_by_key'])
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: [],
limit: limitState.value,
owners: [],
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])
const { isLoading, mutate } = useMutation(
async (event: FormEvent) => {
if (!wallet.client) {
throw new Error('Please connect your wallet.')
}
event.preventDefault()
if (!type) {
throw new Error('Please select an action.')
}
if (badgeHubContractAddress === '') {
throw new Error('Please enter the Badge Hub contract addresses.')
}
if (type === 'mint_by_key' && privateKeyState.value.length !== 64) {
throw new Error('Please enter a valid private key.')
}
if (wallet.client && type === 'edit_badge') {
const feeRateRaw = await wallet.client.queryContractRaw(
badgeHubContractAddress,
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
)
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
await toast
.promise(
wallet.client.queryContractSmart(badgeHubContractAddress, {
badge: { id: badgeId },
}),
{
error: `Edit Fee calculation failed!`,
loading: 'Calculating Edit Fee...',
success: (currentBadge) => {
console.log('Current badge: ', currentBadge)
return `Current metadata is ${
Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes))
} bytes in size.`
},
},
)
.then((currentBadge) => {
// TODO - Go over the calculation
const currentBadgeMetadataSize =
Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes) * 2)
console.log('Current badge metadata size: ', currentBadgeMetadataSize)
const newBadgeMetadataSize =
Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) * 2
console.log('New badge metadata size: ', newBadgeMetadataSize)
if (newBadgeMetadataSize > currentBadgeMetadataSize) {
const calculatedFee = ((newBadgeMetadataSize - currentBadgeMetadataSize) * Number(feeRate.metadata)) / 2
setEditFee(calculatedFee)
setTriggerDispatch(!triggerDispatch)
} else {
setEditFee(undefined)
setTriggerDispatch(!triggerDispatch)
}
})
.catch((error) => {
throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7))
})
} else {
const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...',
success: (tx) => `Transaction ${tx} success!`,
})
if (txHash) {
setLastTx(txHash)
}
}
},
{
onError: (error) => {
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)
const dispatchEditBadgeMessage = async () => {
if (type) {
const txHash = await toast.promise(dispatchExecute(payload), {
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
loading: 'Executing message...',
success: (tx) => `Transaction ${tx} success!`,
})
if (txHash) {
setLastTx(txHash)
}
}
}
const airdropFileOnChange = (data: string[]) => {
console.log(data)
setAirdropAllocationArray(data)
}
const handleGenerateSignature = (id: number, owner: string, privateKey: string) => {
try {
const message = `claim badge ${id} for user ${owner}`
const privKey = Buffer.from(privateKey, 'hex')
// const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true))
const msgBytes = Buffer.from(message, 'utf8')
const msgHashBytes = sha256(msgBytes)
const signedMessage = secp256k1.ecdsaSign(msgHashBytes, privKey)
setSignature(Buffer.from(signedMessage.signature).toString('hex'))
} catch (error) {
console.log(error)
toast.error('Error generating signature.')
}
}
const handleGenerateKeys = (amount: number) => {
for (let i = 0; i < amount; i++) {
let privKey: Buffer
do {
privKey = crypto.randomBytes(32)
} while (!secp256k1.privateKeyVerify(privKey))
const privateKey = privKey.toString('hex')
const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex')
keyPairs.push(publicKey.concat(',', privateKey))
}
}
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"
/>
)}
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
{showAirdropFileField && (
<FormGroup
subtitle="TXT file that contains the addresses to airdrop a badge for"
title="Badge Airdrop List File"
>
<BadgeAirdropListUpload onChange={airdropFileOnChange} />
</FormGroup>
)}
</div>
<div className="-mt-6">
<div className="relative mb-2">
<Button
className="absolute top-0 right-0"
isLoading={isLoading}
onClick={mutate}
rightIcon={<FaArrowRight />}
>
Execute
</Button>
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
<TransactionHash hash={lastTx} />
</FormControl>
</div>
<FormControl subtitle="View current message to be sent" title="Payload Preview">
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
</FormControl>
</div>
</div>
</form>
)
}

View File

@ -0,0 +1,8 @@
import { useState } from 'react'
import type { ActionListItem } from './actions'
export const useActionsComboboxState = () => {
const [value, setValue] = useState<ActionListItem | null>(null)
return { value, onChange: (item: ActionListItem) => setValue(item) }
}

View File

@ -0,0 +1,106 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { MintRule } from '../creation/ImageUploadDetails'
import type { ActionListItem } from './actions'
import { BY_KEY_ACTION_LIST, BY_KEYS_ACTION_LIST, BY_MINTER_ACTION_LIST } from './actions'
export interface ActionsComboboxProps {
value: ActionListItem | null
onChange: (item: ActionListItem) => void
mintRule?: MintRule
}
export const ActionsCombobox = ({ value, onChange, mintRule }: ActionsComboboxProps) => {
const [search, setSearch] = useState('')
const [ACTION_LIST, SET_ACTION_LIST] = useState<ActionListItem[]>(BY_KEY_ACTION_LIST)
useEffect(() => {
if (mintRule === 'by_keys') {
SET_ACTION_LIST(BY_KEYS_ACTION_LIST)
} else if (mintRule === 'by_minter') {
SET_ACTION_LIST(BY_MINTER_ACTION_LIST)
} else {
SET_ACTION_LIST(BY_KEY_ACTION_LIST)
}
}, [mintRule])
const filtered =
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="action"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Badge actions"
title=""
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ActionListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select action"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Action not found
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -0,0 +1,206 @@
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: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from 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`,
},
]
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: 'add_keys',
name: 'Add Keys',
description: `Add keys to the badge with the specified ID`,
},
{
id: 'purge_keys',
name: 'Purge Keys',
description: `Purge keys from the badge with the specified ID`,
},
{
id: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`,
},
{
id: 'mint_by_keys',
name: 'Mint by Keys',
description: `Mint a new badge with a whitelisted private key`,
},
]
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: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`,
},
{
id: 'mint_by_minter',
name: 'Mint by Minter',
description: `Mint a new badge to the specified addresses`,
},
]
export interface DispatchExecuteProps {
type: ActionType
[k: string]: unknown
}
type Select<T extends ActionType> = T
/** @see {@link BadgeHubInstance}*/
export type DispatchExecuteArgs = {
badgeHubContract: string
badgeHubMessages?: BadgeHubInstance
txSigner: string
} & (
| { type: undefined }
| { type: Select<'create_badge'>; badge: Badge }
| { type: Select<'edit_badge'>; id: number; metadata: Metadata; editFee?: number }
| { type: Select<'add_keys'>; id: number; keys: string[] }
| { type: Select<'purge_keys'>; id: number; limit?: number }
| { type: Select<'purge_owners'>; id: number; limit?: number }
| { type: Select<'mint_by_minter'>; id: number; owners: string[] }
| { type: Select<'mint_by_key'>; id: number; owner: string; signature: string }
| { type: Select<'airdrop_by_key'>; id: number; recipients: string[]; privateKey: string }
| { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string }
| { type: Select<'set_nft'>; nft: string }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { badgeHubMessages, txSigner } = args
if (!badgeHubMessages) {
throw new Error('Cannot execute actions')
}
switch (args.type) {
case 'create_badge': {
return badgeHubMessages.createBadge(txSigner, args.badge)
}
case 'edit_badge': {
return badgeHubMessages.editBadge(txSigner, args.id, args.metadata, args.editFee)
}
case 'add_keys': {
return badgeHubMessages.addKeys(txSigner, args.id, args.keys)
}
case 'purge_keys': {
return badgeHubMessages.purgeKeys(txSigner, args.id, args.limit)
}
case 'purge_owners': {
return badgeHubMessages.purgeOwners(txSigner, args.id, args.limit)
}
case 'mint_by_minter': {
return badgeHubMessages.mintByMinter(txSigner, args.id, args.owners)
}
case 'mint_by_key': {
return badgeHubMessages.mintByKey(txSigner, args.id, args.owner, args.signature)
}
case 'airdrop_by_key': {
return badgeHubMessages.airdropByKey(txSigner, args.id, args.recipients, args.privateKey)
}
case 'mint_by_keys': {
return badgeHubMessages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature)
}
case 'set_nft': {
return badgeHubMessages.setNft(txSigner, args.nft)
}
default: {
throw new Error('Unknown action')
}
}
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: badgeHubMessages } = useBadgeHubContract()
const { badgeHubContract } = args
switch (args.type) {
case 'create_badge': {
return badgeHubMessages(badgeHubContract)?.createBadge(args.badge)
}
case 'edit_badge': {
return badgeHubMessages(badgeHubContract)?.editBadge(args.id, args.metadata)
}
case 'add_keys': {
return badgeHubMessages(badgeHubContract)?.addKeys(args.id, args.keys)
}
case 'purge_keys': {
return badgeHubMessages(badgeHubContract)?.purgeKeys(args.id, args.limit)
}
case 'purge_owners': {
return badgeHubMessages(badgeHubContract)?.purgeOwners(args.id, args.limit)
}
case 'mint_by_minter': {
return badgeHubMessages(badgeHubContract)?.mintByMinter(args.id, args.owners)
}
case 'mint_by_key': {
return badgeHubMessages(badgeHubContract)?.mintByKey(args.id, args.owner, args.signature)
}
case 'airdrop_by_key': {
return badgeHubMessages(badgeHubContract)?.airdropByKey(args.id, args.recipients, args.privateKey)
}
case 'mint_by_keys': {
return badgeHubMessages(badgeHubContract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature)
}
case 'set_nft': {
return badgeHubMessages(badgeHubContract)?.setNft(args.nft)
}
default: {
return {}
}
}
}
export const isEitherType = <T extends ActionType>(type: unknown, arr: T[]): type is T => {
return arr.some((val) => type === val)
}

View File

@ -0,0 +1,204 @@
/* 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 { FormControl } from 'components/FormControl'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { useWallet } from 'contexts/wallet'
import type { Trait } from 'contracts/badgeHub'
import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
import { MetadataAttributes } from '../../forms/MetadataAttributes'
import type { MintRule, UploadMethod } from './ImageUploadDetails'
interface BadgeDetailsProps {
onChange: (data: BadgeDetailsDataProps) => void
uploadMethod: UploadMethod | undefined
mintRule: MintRule
}
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 = ({ onChange }: BadgeDetailsProps) => {
const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [transferrable, setTransferrable] = useState<boolean>(false)
const managerState = useInputState({
id: 'manager-address',
name: 'manager',
title: 'Manager',
subtitle: 'Badge Hub Manager',
defaultValue: wallet.address ? wallet.address : '',
})
const nameState = useInputState({
id: 'name',
name: 'name',
title: 'Name',
placeholder: 'My Awesome Collection',
})
const descriptionState = useInputState({
id: 'description',
name: 'description',
title: 'Description',
placeholder: 'My Awesome Collection Description',
})
const imageDataState = useInputState({
id: 'metadata-image-data',
name: 'metadata-image-data',
title: 'Image Data',
subtitle: 'Raw SVG image data',
})
const externalUrlState = useInputState({
id: 'metadata-external-url',
name: 'metadata-external-url',
title: 'External URL',
subtitle: 'External URL for the badge',
})
const attributesState = useMetadataAttributesState()
const maxSupplyState = useNumberInputState({
id: 'max-supply',
name: 'max-supply',
title: 'Max Supply',
subtitle: 'Maximum number of badges that can be minted',
})
const backgroundColorState = useInputState({
id: 'metadata-background-color',
name: 'metadata-background-color',
title: 'Background Color',
subtitle: 'Background color of the badge',
})
const animationUrlState = useInputState({
id: 'metadata-animation-url',
name: 'metadata-animation-url',
title: 'Animation URL',
subtitle: 'Animation URL for the badge',
})
const youtubeUrlState = useInputState({
id: 'metadata-youtube-url',
name: 'metadata-youtube-url',
title: 'YouTube URL',
subtitle: 'YouTube URL for the badge',
})
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(() => {
if (attributesState.values.length === 0)
attributesState.add({
trait_type: '',
value: '',
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div>
<div className={clsx('grid grid-cols-2 ml-4 max-w-5xl')}>
<div className={clsx('mt-2')}>
<AddressInput {...managerState} isRequired />
<TextInput className="mt-2" {...nameState} />
<TextInput className="mt-2" {...descriptionState} />
<NumberInput className="mt-2" {...maxSupplyState} />
<TextInput className="mt-2" {...externalUrlState} />
<FormControl className="mt-2" htmlId="expiry-date" subtitle="Badge minting expiry date" title="Expiry Date">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl>
<div className="mt-2 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>
</div>
<div className={clsx('ml-10')}>
<div>
<MetadataAttributes
attributes={attributesState.entries}
onAdd={attributesState.add}
onChange={attributesState.update}
onRemove={attributesState.remove}
title="Traits"
/>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,296 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-misleading-character-class */
/* eslint-disable no-control-regex */
import clsx from 'clsx'
import { Anchor } from 'components/Anchor'
import { Conditional } from 'components/Conditional'
import { TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { SingleAssetPreview } from 'components/SingleAssetPreview'
import type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload'
export type UploadMethod = 'new' | 'existing'
export type MintRule = 'by_key' | 'by_minter' | 'by_keys' | 'not_resolved'
interface ImageUploadDetailsProps {
onChange: (value: ImageUploadDetailsDataProps) => void
mintRule: MintRule
}
export interface ImageUploadDetailsDataProps {
assetFile: File | undefined
uploadService: UploadServiceType
nftStorageApiKey?: string
pinataApiKey?: string
pinataSecretKey?: string
uploadMethod: UploadMethod
imageUrl?: string
}
export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsProps) => {
const [assetFile, setAssetFile] = useState<File>()
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const assetFileRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key',
name: 'nftStorageApiKey',
title: 'NFT.Storage API Key',
placeholder: 'Enter NFT.Storage API Key',
defaultValue: '',
})
const pinataApiKeyState = useInputState({
id: 'pinata-api-key',
name: 'pinataApiKey',
title: 'Pinata API Key',
placeholder: 'Enter Pinata API Key',
defaultValue: '',
})
const pinataSecretKeyState = useInputState({
id: 'pinata-secret-key',
name: 'pinataSecretKey',
title: 'Pinata Secret Key',
placeholder: 'Enter Pinata Secret Key',
defaultValue: '',
})
const imageUrlState = useInputState({
id: 'imageUrl',
name: 'imageUrl',
title: 'Image URL',
placeholder: 'ipfs://',
defaultValue: '',
})
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
setAssetFile(undefined)
if (event.target.files === null) return
let selectedFile: File
const reader = new FileReader()
reader.onload = (e) => {
if (!event.target.files) return toast.error('No file selected.')
if (!e.target?.result) return toast.error('Error parsing file.')
selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'image/jpg' })
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
else return toast.error('No file selected.')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
setAssetFile(selectedFile)
}
}
const regex =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
useEffect(() => {
try {
const data: ImageUploadDetailsDataProps = {
assetFile,
uploadService,
nftStorageApiKey: nftStorageApiKeyState.value,
pinataApiKey: pinataApiKeyState.value,
pinataSecretKey: pinataSecretKeyState.value,
uploadMethod,
imageUrl: imageUrlState.value
.replace('IPFS://', 'ipfs://')
.replace(/,/g, '')
.replace(/"/g, '')
.replace(/'/g, '')
.replace(/ /g, '')
.replace(regex, ''),
}
onChange(data)
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
}
}, [
assetFile,
uploadService,
nftStorageApiKeyState.value,
pinataApiKeyState.value,
pinataSecretKeyState.value,
uploadMethod,
imageUrlState.value,
])
useEffect(() => {
if (assetFileRef.current) assetFileRef.current.value = ''
setAssetFile(undefined)
imageUrlState.onChange('')
}, [uploadMethod, mintRule])
return (
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
<div className="flex justify-center">
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'new'}
className="peer sr-only"
id="inlineRadio2"
name="inlineRadioOptions2"
onClick={() => {
setUploadMethod('new')
}}
type="radio"
value="New"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio2"
>
Upload New Image
</label>
</div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
<input
checked={uploadMethod === 'existing'}
className="peer sr-only"
id="inlineRadio1"
name="inlineRadioOptions1"
onClick={() => {
setUploadMethod('existing')
}}
type="radio"
value="Existing"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio1"
>
Use an existing Image URL
</label>
</div>
</div>
<div className="p-3 py-5 pb-8">
<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>
<TextInput {...imageUrlState} className="mt-2 ml-4 w-1/2" />
</div>
</div>
</Conditional>
<Conditional test={uploadMethod === 'new'}>
<div>
<div className="flex flex-col items-center px-8 w-full">
<div className="flex justify-items-start mb-5 w-full font-bold">
<div className="form-check form-check-inline">
<input
checked={uploadService === 'nft-storage'}
className="peer sr-only"
id="inlineRadio3"
name="inlineRadioOptions3"
onClick={() => {
setUploadService('nft-storage')
}}
type="radio"
value="nft-storage"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio3"
>
Upload using NFT.Storage
</label>
</div>
<div className="ml-2 form-check form-check-inline">
<input
checked={uploadService === 'pinata'}
className="peer sr-only"
id="inlineRadio4"
name="inlineRadioOptions4"
onClick={() => {
setUploadService('pinata')
}}
type="radio"
value="pinata"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio4"
>
Upload using Pinata
</label>
</div>
</div>
<div className="flex w-full">
<Conditional test={uploadService === 'nft-storage'}>
<TextInput {...nftStorageApiKeyState} className="w-full" />
</Conditional>
<Conditional test={uploadService === 'pinata'}>
<TextInput {...pinataApiKeyState} className="w-full" />
<div className="w-[20px]" />
<TextInput {...pinataSecretKeyState} className="w-full" />
</Conditional>
</div>
</div>
<div className="mt-6">
<div className="grid grid-cols-2">
<div className="w-full">
<div>
<label
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
htmlFor="assetFile"
>
Image Selection
</label>
<div
className={clsx(
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="assetFile"
onChange={selectAsset}
ref={assetFileRef}
type="file"
/>
</div>
</div>
</div>
<Conditional test={assetFile !== undefined}>
<SingleAssetPreview
relatedAsset={assetFile}
subtitle={`Asset filename: ${assetFile?.name as string}`}
/>
</Conditional>
</div>
</div>
</div>
</Conditional>
</div>
</div>
)
}

View File

@ -0,0 +1,8 @@
import { useState } from 'react'
import type { QueryListItem } from './query'
export const useQueryComboboxState = () => {
const [value, setValue] = useState<QueryListItem | null>(null)
return { value, onChange: (item: QueryListItem) => setValue(item) }
}

View File

@ -0,0 +1,105 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { MintRule } from '../creation/ImageUploadDetails'
import type { QueryListItem } from './query'
import { BY_KEY_QUERY_LIST, BY_KEYS_QUERY_LIST, BY_MINTER_QUERY_LIST } from './query'
export interface QueryComboboxProps {
value: QueryListItem | null
onChange: (item: QueryListItem) => void
mintRule?: MintRule
}
export const QueryCombobox = ({ value, onChange, mintRule }: QueryComboboxProps) => {
const [search, setSearch] = useState('')
const [QUERY_LIST, SET_QUERY_LIST] = useState<QueryListItem[]>(BY_KEY_QUERY_LIST)
useEffect(() => {
if (mintRule === 'by_keys') {
SET_QUERY_LIST(BY_KEYS_QUERY_LIST)
} else if (mintRule === 'by_minter') {
SET_QUERY_LIST(BY_MINTER_QUERY_LIST)
} else {
SET_QUERY_LIST(BY_KEY_QUERY_LIST)
}
}, [mintRule])
const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="query"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Badge queries"
title=""
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: QueryListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select query"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Query not found
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -0,0 +1,116 @@
import { QueryCombobox } from 'components/badges/queries/Combobox'
import { useQueryComboboxState } from 'components/badges/queries/Combobox.hooks'
import { dispatchQuery } from 'components/badges/queries/query'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl'
import { NumberInput, TextInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview'
import type { BadgeHubInstance } from 'contracts/badgeHub'
import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query'
import { useWallet } from '../../../contexts/wallet'
import type { MintRule } from '../creation/ImageUploadDetails'
interface BadgeQueriesProps {
badgeHubContractAddress: string
badgeId: number
badgeHubMessages: BadgeHubInstance | undefined
mintRule: MintRule
}
export const BadgeQueries = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeQueriesProps) => {
const wallet = useWallet()
const comboboxState = useQueryComboboxState()
const type = comboboxState.value?.id
const pubkeyState = useInputState({
id: 'pubkey',
name: 'pubkey',
title: 'Public Key',
subtitle: 'The public key to check whether it can be used to mint a badge',
})
const startAfterNumberState = useNumberInputState({
id: 'start-after-number',
name: 'start-after-number',
title: 'Start After (optional)',
subtitle: 'The id to start the pagination after',
})
const startAfterStringState = useInputState({
id: 'start-after-string',
name: 'start-after-string',
title: 'Start After (optional)',
subtitle: 'The public key to start the pagination after',
})
const paginationLimitState = useNumberInputState({
id: 'pagination-limit',
name: 'pagination-limit',
title: 'Pagination Limit (optional)',
subtitle: 'The number of items to return (max: 30)',
defaultValue: 5,
})
const { data: response } = useQuery(
[
badgeHubMessages,
type,
badgeId,
pubkeyState.value,
startAfterNumberState.value,
startAfterStringState.value,
paginationLimitState.value,
] as const,
async ({ queryKey }) => {
const [_badgeHubMessages, _type, _badgeId, _pubKey, _startAfterNumber, _startAfterString, _limit] = queryKey
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result = await dispatchQuery({
badgeHubMessages: _badgeHubMessages,
id: _badgeId,
startAfterNumber: _startAfterNumber,
startAfterString: _startAfterString,
limit: _limit,
type: _type,
pubkey: _pubKey,
})
return result
},
{
placeholderData: null,
onError: (error: any) => {
toast.error(error.message, { style: { maxWidth: 'none' } })
},
enabled: Boolean(badgeHubContractAddress && type && badgeId),
retry: false,
},
)
return (
<div className="grid grid-cols-2 mt-4">
<div className="mr-2 space-y-8">
<QueryCombobox mintRule={mintRule} {...comboboxState} />
<Conditional test={type === 'getKey'}>
<TextInput {...pubkeyState} />
</Conditional>
<Conditional test={type === 'getBadges'}>
<NumberInput {...startAfterNumberState} />
</Conditional>
<Conditional test={type === 'getBadges' || type === 'getKeys'}>
<NumberInput {...paginationLimitState} />
</Conditional>
<Conditional test={type === 'getKeys'}>
<TextInput {...startAfterStringState} />
</Conditional>
</div>
<div className="space-y-8">
<FormControl title="Query Response">
<JsonPreview content={response || {}} isCopyable />
</FormControl>
</div>
</div>
)
}

View File

@ -0,0 +1,76 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import type { BadgeHubInstance } from 'contracts/badgeHub'
export type QueryType = typeof QUERY_TYPES[number]
export const QUERY_TYPES = ['config', 'getBadge', 'getBadges', 'getKey', 'getKeys'] as const
export interface QueryListItem {
id: QueryType
name: string
description?: string
}
export const BY_KEY_QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' },
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
]
export const BY_KEYS_QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' },
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
{ id: 'getKey', name: 'Query Key', description: "Query a key by ID to see if it's whitelisted" },
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
]
export const BY_MINTER_QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' },
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
]
export interface DispatchExecuteProps {
type: QueryType
[k: string]: unknown
}
type Select<T extends QueryType> = T
export type DispatchQueryArgs = {
badgeHubMessages?: BadgeHubInstance
} & (
| { type: undefined }
| { type: Select<'config'> }
| { type: Select<'getBadge'>; id: number }
| { type: Select<'getBadges'>; startAfterNumber: number; limit: number }
| { type: Select<'getKey'>; id: number; pubkey: string }
| { type: Select<'getKeys'>; id: number; startAfterString: string; limit: number }
)
export const dispatchQuery = async (args: DispatchQueryArgs) => {
const { badgeHubMessages } = args
if (!badgeHubMessages) {
throw new Error('Cannot perform a query')
}
switch (args.type) {
case 'config': {
return badgeHubMessages?.getConfig()
}
case 'getBadge': {
return badgeHubMessages?.getBadge(args.id)
}
case 'getBadges': {
return badgeHubMessages?.getBadges(args.startAfterNumber, args.limit)
}
case 'getKey': {
return badgeHubMessages?.getKey(args.id, args.pubkey)
}
case 'getKeys': {
return badgeHubMessages?.getKeys(args.id, args.startAfterString, args.limit)
}
default: {
throw new Error('Unknown action')
}
}
}

View File

@ -0,0 +1,7 @@
import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute'
import { useState } from 'react'
export const useExecuteComboboxState = () => {
const [value, setValue] = useState<ExecuteListItem | null>(null)
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
}

View File

@ -0,0 +1,92 @@
import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute'
import { EXECUTE_LIST } from 'contracts/badgeHub/messages/execute'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
export interface ExecuteComboboxProps {
value: ExecuteListItem | null
onChange: (item: ExecuteListItem) => void
}
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
const [search, setSearch] = useState('')
const filtered =
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
return (
<Combobox
as={FormControl}
htmlId="message-type"
labelAs={Combobox.Label}
onChange={onChange}
subtitle="Contract execute message type"
title="Message Type"
value={value}
>
<div className="relative">
<Combobox.Input
className={clsx(
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
id="message-type"
onChange={(event) => setSearch(event.target.value)}
placeholder="Select message type"
/>
<Combobox.Button
className={clsx(
'flex absolute inset-y-0 right-0 items-center p-4',
'opacity-50 hover:opacity-100 active:opacity-100',
)}
>
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
</Combobox.Button>
<Transition afterLeave={() => setSearch('')} as={Fragment}>
<Combobox.Options
className={clsx(
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
'divide-y divide-stone-500/50',
)}
>
{filtered.length < 1 && (
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
Message type not found.
</span>
)}
{filtered.map((entry) => (
<Combobox.Option
key={entry.id}
className={({ active }) =>
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
}
value={entry}
>
<span className="font-bold">{entry.name}</span>
<span className="max-w-md text-sm">{entry.description}</span>
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
{value && (
<div className="flex space-x-2 text-white/50">
<div className="mt-1">
<FaInfoCircle className="w-3 h-3" />
</div>
<span className="text-sm">{value.description}</span>
</div>
)}
</Combobox>
)
}

View File

@ -1,3 +1,5 @@
import type { UseBadgeHubContractProps } from 'contracts/badgeHub'
import { useBadgeHubContract } from 'contracts/badgeHub'
import type { UseBaseFactoryContractProps } from 'contracts/baseFactory'
import { useBaseFactoryContract } from 'contracts/baseFactory'
import type { UseBaseMinterContractProps } from 'contracts/baseMinter'
@ -25,6 +27,7 @@ export interface ContractsStore extends State {
whitelist: UseWhiteListContractProps | null
vendingFactory: UseVendingFactoryContractProps | null
baseFactory: UseBaseFactoryContractProps | null
badgeHub: UseBadgeHubContractProps | null
}
/**
@ -37,6 +40,7 @@ export const defaultValues: ContractsStore = {
whitelist: null,
vendingFactory: null,
baseFactory: null,
badgeHub: null,
}
/**
@ -66,6 +70,7 @@ const ContractsSubscription: VFC = () => {
const whitelist = useWhiteListContract()
const vendingFactory = useVendingFactoryContract()
const baseFactory = useBaseFactoryContract()
const badgeHub = useBadgeHubContract()
useEffect(() => {
useContracts.setState({
@ -75,8 +80,9 @@ const ContractsSubscription: VFC = () => {
whitelist,
vendingFactory,
baseFactory,
badgeHub,
})
}, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory])
}, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub])
return null
}

View File

@ -0,0 +1,718 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable camelcase */
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { toUtf8 } from '@cosmjs/encoding'
import type { Coin } from '@cosmjs/proto-signing'
import { coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
import sizeof from 'object-sizeof'
import { generateSignature } from '../../utils/hash'
export interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface MigrateResponse {
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface Rule {
by_key?: string
by_minter?: string
by_keys?: string[]
}
export interface Trait {
display_type?: string
trait_type: string
value: string
}
export interface Metadata {
name?: string
image?: string
image_data?: string
external_url?: string
description?: string
attributes?: Trait[]
background_color?: string
animation_url?: string
youtube_url?: string
}
export interface Badge {
manager: string
metadata: Metadata
transferrable: boolean
rule: Rule
expiry?: number
max_supply?: number
}
export interface BadgeHubInstance {
readonly contractAddress: string
//Query
getConfig: () => Promise<any>
getBadge: (id: number) => Promise<any>
getBadges: (start_after?: number, limit?: number) => Promise<any>
getKey: (id: number, pubkey: string) => Promise<any>
getKeys: (id: number, start_after?: string, limit?: number) => Promise<any>
//Execute
createBadge: (senderAddress: string, badge: Badge) => Promise<string>
editBadge: (senderAddress: string, id: number, metadata: Metadata, editFee?: number) => Promise<string>
addKeys: (senderAddress: string, id: number, keys: string[]) => Promise<string>
purgeKeys: (senderAddress: string, id: number, limit?: number) => Promise<string>
purgeOwners: (senderAddress: string, id: number, limit?: number) => Promise<string>
mintByMinter: (senderAddress: string, id: number, owners: string[]) => Promise<string>
mintByKey: (senderAddress: string, id: number, owner: string, signature: string) => Promise<string>
airdropByKey: (senderAddress: string, id: number, recipients: string[], privateKey: string) => Promise<string>
mintByKeys: (senderAddress: string, id: number, owner: string, pubkey: string, signature: string) => Promise<string>
setNft: (senderAddress: string, nft: string) => Promise<string>
}
export interface BadgeHubMessages {
createBadge: (badge: Badge) => CreateBadgeMessage
editBadge: (id: number, metadata: Metadata, editFee?: number) => EditBadgeMessage
addKeys: (id: number, keys: string[]) => AddKeysMessage
purgeKeys: (id: number, limit?: number) => PurgeKeysMessage
purgeOwners: (id: number, limit?: number) => PurgeOwnersMessage
mintByMinter: (id: number, owners: string[]) => MintByMinterMessage
mintByKey: (id: number, owner: string, signature: string) => MintByKeyMessage
airdropByKey: (id: number, recipients: string[], privateKey: string) => CustomMessage
mintByKeys: (id: number, owner: string, pubkey: string, signature: string) => MintByKeysMessage
setNft: (nft: string) => SetNftMessage
}
export interface CreateBadgeMessage {
sender: string
contract: string
msg: {
create_badge: {
manager: string
metadata: Metadata
transferrable: boolean
rule: Rule
expiry?: number
max_supply?: number
}
}
funds: Coin[]
}
export interface EditBadgeMessage {
sender: string
contract: string
msg: {
edit_badge: {
id: number
metadata: Metadata
}
}
funds: Coin[]
}
export interface AddKeysMessage {
sender: string
contract: string
msg: {
add_keys: {
id: number
keys: string[]
}
}
funds: Coin[]
}
export interface PurgeKeysMessage {
sender: string
contract: string
msg: {
purge_keys: {
id: number
limit?: number
}
}
funds: Coin[]
}
export interface PurgeOwnersMessage {
sender: string
contract: string
msg: {
purge_owners: {
id: number
limit?: number
}
}
funds: Coin[]
}
export interface MintByMinterMessage {
sender: string
contract: string
msg: {
mint_by_minter: {
id: number
owners: string[]
}
}
funds: Coin[]
}
export interface MintByKeyMessage {
sender: string
contract: string
msg: {
mint_by_key: {
id: number
owner: string
signature: string
}
}
funds: Coin[]
}
export interface CustomMessage {
sender: string
contract: string
msg: Record<string, unknown>[]
funds: Coin[]
}
export interface MintByKeysMessage {
sender: string
contract: string
msg: {
mint_by_keys: {
id: number
owner: string
pubkey: string
signature: string
}
}
funds: Coin[]
}
export interface SetNftMessage {
sender: string
contract: string
msg: {
set_nft: {
nft: string
}
}
funds: Coin[]
}
export interface BadgeHubContract {
instantiate: (
senderAddress: string,
codeId: number,
initMsg: Record<string, unknown>,
label: string,
admin?: string,
funds?: Coin[],
) => Promise<InstantiateResponse>
migrate: (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
) => Promise<MigrateResponse>
use: (contractAddress: string) => BadgeHubInstance
messages: (contractAddress: string) => BadgeHubMessages
}
export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): BadgeHubContract => {
const use = (contractAddress: string): BadgeHubInstance => {
//Query
const getConfig = async (): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
config: {},
})
return res
}
const getBadge = async (id: number): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
badge: { id },
})
return res
}
const getBadges = async (start_after?: number, limit?: number): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
badges: { start_after, limit },
})
return res
}
const getKey = async (id: number, pubkey: string): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
key: { id, pubkey },
})
return res
}
const getKeys = async (id: number, start_after?: string, limit?: number): Promise<any> => {
const res = await client.queryContractSmart(contractAddress, {
keys: { id, start_after, limit },
})
return res
}
//Execute
const createBadge = async (senderAddress: string, badge: Badge): Promise<string> => {
const feeRateRaw = await client.queryContractRaw(
contractAddress,
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
)
console.log('Fee Rate Raw: ', feeRateRaw)
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
console.log('Fee Rate:', feeRate)
console.log('badge size: ', sizeof(badge))
console.log('metadata size', sizeof(badge.metadata))
console.log('size of attributes ', sizeof(badge.metadata.attributes))
console.log('Total: ', Number(sizeof(badge)) + Number(sizeof(badge.metadata.attributes)))
const res = await client.execute(
senderAddress,
contractAddress,
{
create_badge: {
manager: badge.manager,
metadata: badge.metadata,
transferrable: badge.transferrable,
rule: badge.rule,
expiry: badge.expiry,
max_supply: badge.max_supply,
},
},
'auto',
'',
[
coin(
(Number(sizeof(badge)) + Number(sizeof(badge.metadata.attributes))) * Number(feeRate.metadata),
'ustars',
),
],
//[coin(1, 'ustars')],
)
const events = res.logs
.map((log) => log.events)
.flat()
.find(
(event) =>
event.attributes.findIndex((attr) => attr.key === 'action' && attr.value === 'badges/hub/create_badge') > 0,
)!
const id = Number(events.attributes.find((attr) => attr.key === 'id')!.value)
return res.transactionHash.concat(`:${id}`)
}
const editBadge = async (
senderAddress: string,
id: number,
metadata: Metadata,
editFee?: number,
): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
edit_badge: {
id,
metadata,
},
},
'auto',
'',
editFee ? [coin(editFee, 'ustars')] : [],
)
return res.transactionHash
}
const addKeys = async (senderAddress: string, id: number, keys: string[]): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
add_keys: {
id,
keys,
},
},
'auto',
'',
)
return res.transactionHash
}
const purgeKeys = async (senderAddress: string, id: number, limit?: number): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
purge_keys: {
id,
limit,
},
},
'auto',
'',
)
return res.transactionHash
}
const purgeOwners = async (senderAddress: string, id: number, limit?: number): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
purge_owners: {
id,
limit,
},
},
'auto',
'',
)
return res.transactionHash
}
const mintByMinter = async (senderAddress: string, id: number, owners: string[]): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
mint_by_minter: {
id,
owners,
},
},
'auto',
'',
)
return res.transactionHash
}
const mintByKey = async (senderAddress: string, id: number, owner: string, signature: string): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
mint_by_key: {
id,
owner,
signature,
},
},
'auto',
'',
)
return res.transactionHash
}
const airdropByKey = async (
senderAddress: string,
id: number,
recipients: string[],
privateKey: string,
): Promise<string> => {
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
for (let i = 0; i < recipients.length; i++) {
const msg = {
mint_by_key: { id, owner: recipients[i], signature: generateSignature(id, recipients[i], privateKey) },
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: senderAddress,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
}),
}
executeContractMsgs.push(executeContractMsg)
}
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop_by_key')
return res.transactionHash
}
const mintByKeys = async (
senderAddress: string,
id: number,
owner: string,
pubkey: string,
signature: string,
): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
mint_by_keys: {
id,
owner,
pubkey,
signature,
},
},
'auto',
'',
)
return res.transactionHash
}
const setNft = async (senderAddress: string, nft: string): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
set_nft: {
nft,
},
},
'auto',
'',
)
return res.transactionHash
}
return {
contractAddress,
getConfig,
getBadge,
getBadges,
getKey,
getKeys,
createBadge,
editBadge,
addKeys,
purgeKeys,
purgeOwners,
mintByMinter,
mintByKey,
airdropByKey,
mintByKeys,
setNft,
}
}
const migrate = async (
senderAddress: string,
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
): Promise<MigrateResponse> => {
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
return {
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const instantiate = async (
senderAddress: string,
codeId: number,
initMsg: Record<string, unknown>,
label: string,
): Promise<InstantiateResponse> => {
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto')
return {
contractAddress: result.contractAddress,
transactionHash: result.transactionHash,
logs: result.logs,
}
}
const messages = (contractAddress: string) => {
const createBadge = (badge: Badge): CreateBadgeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
create_badge: {
manager: badge.manager,
metadata: badge.metadata,
transferrable: badge.transferrable,
rule: badge.rule,
expiry: badge.expiry,
max_supply: badge.max_supply,
},
},
funds: [],
}
}
const editBadge = (id: number, metadata: Metadata, editFee?: number): EditBadgeMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
edit_badge: {
id,
metadata,
},
},
funds: editFee ? [coin(editFee, 'ustars')] : [],
}
}
const addKeys = (id: number, keys: string[]): AddKeysMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
add_keys: {
id,
keys,
},
},
funds: [],
}
}
const purgeKeys = (id: number, limit?: number): PurgeKeysMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
purge_keys: {
id,
limit,
},
},
funds: [],
}
}
const purgeOwners = (id: number, limit?: number): PurgeOwnersMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
purge_owners: {
id,
limit,
},
},
funds: [],
}
}
const mintByMinter = (id: number, owners: string[]): MintByMinterMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint_by_minter: {
id,
owners,
},
},
funds: [],
}
}
const mintByKey = (id: number, owner: string, signature: string): MintByKeyMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint_by_key: {
id,
owner,
signature,
},
},
funds: [],
}
}
const airdropByKey = (id: number, recipients: string[], privateKey: string): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < recipients.length; i++) {
const signature = generateSignature(id, recipients[i], privateKey)
msg.push({
mint_by_key: { id, owner: recipients[i], signature },
})
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
const mintByKeys = (id: number, owner: string, pubkey: string, signature: string): MintByKeysMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
mint_by_keys: {
id,
owner,
pubkey,
signature,
},
},
funds: [],
}
}
const setNft = (nft: string): SetNftMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
set_nft: {
nft,
},
},
funds: [],
}
}
return {
createBadge,
editBadge,
addKeys,
purgeKeys,
purgeOwners,
mintByMinter,
mintByKey,
airdropByKey,
mintByKeys,
setNft,
}
}
return { use, instantiate, migrate, messages }
}

View File

@ -0,0 +1,2 @@
export * from './contract'
export * from './useContract'

View File

@ -0,0 +1,176 @@
import type { Badge, BadgeHubInstance, Metadata } from '../index'
import { useBadgeHubContract } from '../index'
export type ExecuteType = typeof EXECUTE_TYPES[number]
export const EXECUTE_TYPES = [
'create_badge',
'edit_badge',
'add_keys',
'purge_keys',
'purge_owners',
'mint_by_minter',
'mint_by_key',
'mint_by_keys',
'set_nft',
] as const
export interface ExecuteListItem {
id: ExecuteType
name: string
description?: string
}
export const EXECUTE_LIST: ExecuteListItem[] = [
{
id: 'create_badge',
name: 'Create Badge',
description: `Create a new badge with the specified mint rule and metadata`,
},
{
id: 'edit_badge',
name: 'Edit Badge',
description: ` Edit badge metadata for the badge with the specified ID`,
},
// {
// id: 'add_keys',
// name: 'Add Keys',
// description: `Add keys to the badge with the specified ID`,
// },
// {
// id: 'purge_keys',
// name: 'Purge Keys',
// description: `Purge keys from the badge with the specified ID`,
// },
{
id: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`,
},
// {
// id: 'mint_by_minter',
// name: 'Mint by Minter',
// description: `Mint a new token by the minter with the specified ID`,
// },
{
id: 'mint_by_key',
name: 'Mint by Key',
description: `Mint a new token by the key with the specified ID`,
},
// {
// id: 'mint_by_keys',
// name: 'Mint by Keys',
// description: `Mint a new token by the keys with the specified ID`,
// },
{
id: 'set_nft',
name: 'Set NFT',
description: `Set the Badge NFT contract address for the Badge Hub contract`,
},
]
export interface DispatchExecuteProps {
type: ExecuteType
[k: string]: unknown
}
type Select<T extends ExecuteType> = T
/** @see {@link BadgeHubInstance} */
export type DispatchExecuteArgs = {
contract: string
messages?: BadgeHubInstance
txSigner: string
} & (
| { type: undefined }
| { type: Select<'create_badge'>; badge: Badge }
| { type: Select<'edit_badge'>; id: number; metadata: Metadata; editFee?: number }
| { type: Select<'add_keys'>; id: number; keys: string[] }
| { type: Select<'purge_keys'>; id: number; limit?: number }
| { type: Select<'purge_owners'>; id: number; limit?: number }
| { type: Select<'mint_by_minter'>; id: number; owners: string[] }
| { type: Select<'mint_by_key'>; id: number; owner: string; signature: string }
| { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string }
| { type: Select<'set_nft'>; nft: string }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { messages, txSigner } = args
if (!messages) {
throw new Error('cannot dispatch execute, messages is not defined')
}
switch (args.type) {
case 'create_badge': {
return messages.createBadge(txSigner, args.badge)
}
case 'edit_badge': {
return messages.editBadge(txSigner, args.id, args.metadata, args.editFee)
}
case 'add_keys': {
return messages.addKeys(txSigner, args.id, args.keys)
}
case 'purge_keys': {
return messages.purgeKeys(txSigner, args.id, args.limit)
}
case 'purge_owners': {
return messages.purgeOwners(txSigner, args.id, args.limit)
}
case 'mint_by_minter': {
return messages.mintByMinter(txSigner, args.id, args.owners)
}
case 'mint_by_key': {
return messages.mintByKey(txSigner, args.id, args.owner, args.signature)
}
case 'mint_by_keys': {
return messages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature)
}
case 'set_nft': {
return messages.setNft(txSigner, args.nft)
}
default: {
throw new Error('unknown execute type')
}
}
}
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages } = useBadgeHubContract()
const { contract } = args
switch (args.type) {
case 'create_badge': {
return messages(contract)?.createBadge(args.badge)
}
case 'edit_badge': {
return messages(contract)?.editBadge(args.id, args.metadata)
}
case 'add_keys': {
return messages(contract)?.addKeys(args.id, args.keys)
}
case 'purge_keys': {
return messages(contract)?.purgeKeys(args.id, args.limit)
}
case 'purge_owners': {
return messages(contract)?.purgeOwners(args.id, args.limit)
}
case 'mint_by_minter': {
return messages(contract)?.mintByMinter(args.id, args.owners)
}
case 'mint_by_key': {
return messages(contract)?.mintByKey(args.id, args.owner, args.signature)
}
case 'mint_by_keys': {
return messages(contract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature)
}
case 'set_nft': {
return messages(contract)?.setNft(args.nft)
}
default: {
return {}
}
}
}
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
return arr.some((val) => type === val)
}

View File

@ -0,0 +1,53 @@
import type { BadgeHubInstance } from '../contract'
export type QueryType = typeof QUERY_TYPES[number]
export const QUERY_TYPES = ['config', 'getBadge', 'getBadges', 'getKey', 'getKeys'] as const
export interface QueryListItem {
id: QueryType
name: string
description?: string
}
export const QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' },
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
// { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it&apos;s whitelisted' },
// { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
]
export interface DispatchQueryProps {
id: number
pubkey: string
messages: BadgeHubInstance | undefined
type: QueryType
startAfterNumber: number
startAfterString: string
limit: number
}
export const dispatchQuery = (props: DispatchQueryProps) => {
const { id, pubkey, messages, type, startAfterNumber, startAfterString, limit } = props
switch (type) {
case 'config': {
return messages?.getConfig()
}
case 'getBadge': {
return messages?.getBadge(id)
}
case 'getBadges': {
return messages?.getBadges(startAfterNumber, limit)
}
case 'getKey': {
return messages?.getKey(id, pubkey)
}
case 'getKeys': {
return messages?.getKeys(id, startAfterString, limit)
}
default: {
throw new Error('unknown query type')
}
}
}

View File

@ -0,0 +1,114 @@
import type { Coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import { useWallet } from 'contexts/wallet'
import { useCallback, useEffect, useState } from 'react'
import type { BadgeHubContract, BadgeHubInstance, BadgeHubMessages, MigrateResponse } from './contract'
import { badgeHub as initContract } from './contract'
/*export interface InstantiateResponse {
/** The address of the newly instantiated contract *-/
readonly contractAddress: string
readonly logs: readonly logs.Log[]
/** Block height in which the transaction is included *-/
readonly height: number
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex *-/
readonly transactionHash: string
readonly gasWanted: number
readonly gasUsed: number
}*/
interface InstantiateResponse {
readonly contractAddress: string
readonly transactionHash: string
readonly logs: readonly logs.Log[]
}
export interface UseBadgeHubContractProps {
instantiate: (
codeId: number,
initMsg: Record<string, unknown>,
label: string,
admin?: string,
funds?: Coin[],
) => Promise<InstantiateResponse>
migrate: (contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>) => Promise<MigrateResponse>
use: (customAddress: string) => BadgeHubInstance | undefined
updateContractAddress: (contractAddress: string) => void
getContractAddress: () => string | undefined
messages: (contractAddress: string) => BadgeHubMessages | undefined
}
export function useBadgeHubContract(): UseBadgeHubContractProps {
const wallet = useWallet()
const [address, setAddress] = useState<string>('')
const [badgeHub, setBadgeHub] = useState<BadgeHubContract>()
useEffect(() => {
setAddress(localStorage.getItem('contract_address') || '')
}, [])
useEffect(() => {
const BadgeHubBaseContract = initContract(wallet.getClient(), wallet.address)
setBadgeHub(BadgeHubBaseContract)
}, [wallet])
const updateContractAddress = (contractAddress: string) => {
setAddress(contractAddress)
}
const instantiate = useCallback(
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
return new Promise((resolve, reject) => {
if (!badgeHub) {
reject(new Error('Contract is not initialized.'))
return
}
badgeHub.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
})
},
[badgeHub, wallet],
)
const migrate = useCallback(
(contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>): Promise<MigrateResponse> => {
return new Promise((resolve, reject) => {
if (!badgeHub) {
reject(new Error('Contract is not initialized.'))
return
}
console.log(wallet.address, contractAddress, codeId)
badgeHub.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
})
},
[badgeHub, wallet],
)
const use = useCallback(
(customAddress = ''): BadgeHubInstance | undefined => {
return badgeHub?.use(address || customAddress)
},
[badgeHub, address],
)
const getContractAddress = (): string | undefined => {
return address
}
const messages = useCallback(
(customAddress = ''): BadgeHubMessages | undefined => {
return badgeHub?.messages(address || customAddress)
},
[badgeHub, address],
)
return {
instantiate,
use,
updateContractAddress,
getContractAddress,
messages,
migrate,
}
}

4
env.d.ts vendored
View File

@ -21,6 +21,10 @@ declare namespace NodeJS {
readonly NEXT_PUBLIC_BASE_FACTORY_ADDRESS: string
readonly NEXT_PUBLIC_SG721_NAME_ADDRESS: string
readonly NEXT_PUBLIC_BASE_MINTER_CODE_ID: string
readonly NEXT_PUBLIC_BADGE_HUB_CODE_ID: string
readonly NEXT_PUBLIC_BADGE_HUB_ADDRESS: string
readonly NEXT_PUBLIC_BADGE_NFT_CODE_ID: string
readonly NEXT_PUBLIC_BADGE_NFT_ADDRESS: string
readonly NEXT_PUBLIC_PINATA_ENDPOINT_URL: string
readonly NEXT_PUBLIC_API_URL: string

View File

@ -1,6 +1,6 @@
{
"name": "stargaze-studio",
"version": "0.4.4",
"version": "0.4.5",
"workspaces": [
"packages/*"
],
@ -21,6 +21,7 @@
"@fontsource/jetbrains-mono": "^4",
"@fontsource/roboto": "^4",
"@headlessui/react": "^1",
"jscrypto": "^1.0.3",
"@keplr-wallet/cosmos": "^0.9.16",
"@pinata/sdk": "^1.1.26",
"@popperjs/core": "^2",
@ -31,10 +32,12 @@
"clsx": "^1",
"compare-versions": "^4",
"daisyui": "^2.19.0",
"html-to-image": "1.11.11",
"match-sorter": "^6",
"next": "^12",
"next-seo": "^4",
"nft.storage": "^6.3.0",
"qrcode.react": "3.1.0",
"react": "^18",
"react-datetime-picker": "^3",
"react-dom": "^18",
@ -45,6 +48,8 @@
"react-query": "^3",
"react-tracked": "^1",
"scheduler": "^0",
"secp256k1": "^4.0.3",
"tailwindcss-opentype": "1.1.0",
"zustand": "^3"
},
"devDependencies": {
@ -52,9 +57,11 @@
"@types/node": "^14",
"@types/react": "^18",
"@types/react-datetime-picker": "^3",
"@types/secp256k1": "^4.0.2",
"autoprefixer": "^10",
"husky": "^7",
"lint-staged": "^12",
"object-sizeof": "^1.6.0",
"postcss": "^8",
"tailwindcss": "^3",
"typescript": "^4"

214
pages/badges/actions.tsx Normal file
View File

@ -0,0 +1,214 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type { MintRule } from 'components/badges/creation/ImageUploadDetails'
import { BadgeQueries } from 'components/badges/queries/Queries'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import { useEffect, useMemo, useState } from 'react'
import toast from 'react-hot-toast'
import { useDebounce } from 'utils/debounce'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import { BadgeActions } from '../../components/badges/actions/Action'
import { useNumberInputState } from '../../components/forms/FormInput.hooks'
import { BADGE_HUB_ADDRESS } from '../../utils/constants'
const BadgeActionsPage: NextPage = () => {
const { badgeHub: badgeHubContract } = useContracts()
const wallet = useWallet()
const [action, setAction] = useState<boolean>(false)
const [mintRule, setMintRule] = useState<MintRule>('by_key')
const badgeHubContractState = useInputState({
id: 'badge-hub-contract-address',
name: 'badge-hub-contract-address',
title: 'Badge Hub Contract Address',
subtitle: 'Address of the Badge Hub contract',
defaultValue: BADGE_HUB_ADDRESS,
})
const badgeIdState = useNumberInputState({
id: 'badge-id',
name: 'badge-id',
title: 'Badge ID',
subtitle: 'The ID of the badge to interact with',
defaultValue: 1,
})
const debouncedBadgeHubContractState = useDebounce(badgeHubContractState.value, 300)
const debouncedBadgeIdState = useDebounce(badgeIdState.value, 300)
const badgeHubMessages = useMemo(
() => badgeHubContract?.use(badgeHubContractState.value),
[badgeHubContract, badgeHubContractState.value],
)
const badgeHubContractAddress = badgeHubContractState.value
const badgeId = badgeIdState.value
const router = useRouter()
useEffect(() => {
if (badgeHubContractAddress.length > 0 && badgeId < 1) {
void router.replace({ query: { badgeHubContractAddress } })
}
if (badgeId > 0 && badgeHubContractAddress.length === 0) {
void router.replace({ query: { badgeId } })
}
if (badgeId > 0 && badgeHubContractAddress.length > 0) {
void router.replace({ query: { badgeHubContractAddress, badgeId } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [badgeHubContractAddress, badgeId])
useEffect(() => {
const initialBadgeHub = new URL(document.URL).searchParams.get('badgeHubContractAddress')
const initialBadgeId = new URL(document.URL).searchParams.get('badgeId')
if (initialBadgeHub && initialBadgeHub.length > 0) badgeHubContractState.onChange(initialBadgeHub)
if (initialBadgeId && initialBadgeId.length > 0)
badgeIdState.onChange(isNaN(parseInt(initialBadgeId)) ? 0 : parseInt(initialBadgeId))
}, [])
useEffect(() => {
async function getMintRule() {
if (wallet.client && debouncedBadgeHubContractState.length > 0 && debouncedBadgeIdState > 0) {
const client = wallet.client
const data = await toast.promise(
client.queryContractSmart(debouncedBadgeHubContractState, {
badge: {
id: badgeId,
},
}),
{
loading: 'Retrieving Mint Rule...',
error: 'Mint Rule retrieval failed.',
success: 'Mint Rule retrieved.',
},
)
console.log(data)
const rule = data.rule
console.log(rule)
return rule
}
}
void getMintRule()
.then((rule) => {
if (JSON.stringify(rule).includes('keys')) {
setMintRule('by_keys')
} else if (JSON.stringify(rule).includes('minter')) {
setMintRule('by_minter')
} else {
setMintRule('by_key')
}
})
.catch((err) => {
console.log(err)
setMintRule('not_resolved')
console.log('Unable to retrieve Mint Rule. Defaulting to "by_key".')
})
}, [debouncedBadgeHubContractState, debouncedBadgeIdState, wallet.client])
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Badge Actions" />
<ContractPageHeader
description="Here you can execute various actions and queries for a badge."
link={links.Documentation}
title="Badge Actions"
/>
<form className="p-4">
<div className="grid grid-cols-2">
<AddressInput {...badgeHubContractState} className="mr-2" />
<div className="flex-row">
<NumberInput className="w-1/2" {...badgeIdState} />
<div className="mt-2">
<span className="font-bold">Mint Rule: </span>
<span>
{mintRule
.toString()
.split('_')
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
.join(' ')}
</span>
</div>
</div>
</div>
<div className="mt-4">
<div className="mr-2">
<div className="flex justify-items-start font-bold">
<div className="form-check form-check-inline">
<input
checked={!action}
className="peer sr-only"
id="inlineRadio4"
name="inlineRadioOptions4"
onClick={() => {
setAction(false)
}}
type="radio"
value="false"
/>
<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"
>
Queries
</label>
</div>
<div className="ml-2 form-check form-check-inline">
<input
checked={action}
className="peer sr-only"
id="inlineRadio3"
name="inlineRadioOptions3"
onClick={() => {
setAction(true)
}}
type="radio"
value="true"
/>
<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"
>
Actions
</label>
</div>
</div>
<div>
{(action && (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
<BadgeActions
badgeHubContractAddress={badgeHubContractState.value}
badgeHubMessages={badgeHubMessages}
badgeId={badgeIdState.value}
mintRule={mintRule}
/>
)) || (
<BadgeQueries
badgeHubContractAddress={badgeHubContractState.value}
badgeHubMessages={badgeHubMessages}
badgeId={badgeIdState.value}
mintRule={mintRule}
/>
)}
</div>
</div>
</div>
</form>
</section>
)
}
export default withMetadata(BadgeActionsPage, { center: false })

477
pages/badges/create.tsx Normal file
View File

@ -0,0 +1,477 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
//import { coin } from '@cosmjs/proto-signing'
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 { useWallet } from 'contexts/wallet'
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 { 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 { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import { truncateMiddle } from 'utils/text'
const BadgeCreationPage: NextPage = () => {
const wallet = useWallet()
const { badgeHub: badgeHubContract } = useContracts()
const scrollRef = useRef<HTMLDivElement>(null)
const badgeHubMessages = useMemo(() => badgeHubContract?.use(BADGE_HUB_ADDRESS), [badgeHubContract, wallet.address])
const [imageUploadDetails, setImageUploadDetails] = useState<ImageUploadDetailsDataProps | null>(null)
const [badgeDetails, setBadgeDetails] = useState<BadgeDetailsDataProps | null>(null)
const [uploading, setUploading] = useState(false)
const [creatingBadge, setCreatingBadge] = useState(false)
const [readyToCreateBadge, setReadyToCreateBadge] = useState(false)
const [mintRule, setMintRule] = useState<MintRule>('by_key')
const [badgeId, setBadgeId] = useState<string | null>(null)
const [imageUrl, setImageUrl] = useState<string | null>(null)
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
const [transactionHash, setTransactionHash] = useState<string | null>(null)
const qrRef = useRef<HTMLDivElement>(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 performBadgeCreationChecks = () => {
try {
setReadyToCreateBadge(false)
checkImageUploadDetails()
checkBadgeDetails()
setTimeout(() => {
setReadyToCreateBadge(true)
}, 100)
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
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' } })
setCreatingBadge(false)
setUploading(false)
throw new Error("Couldn't upload the image.")
}
}
const createNewBadge = async () => {
try {
if (!wallet.initialized) 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 || 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 || undefined,
youtube_url: badgeDetails?.youtube_url || undefined,
},
transferrable: badgeDetails?.transferrable as boolean,
rule: {
by_key: keyState.value,
},
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',
}
const data = await badgeHubDispatchExecute(payload)
console.log(data)
setCreatingBadge(false)
setTransactionHash(data.split(':')[0])
setBadgeId(data.split(':')[1])
} catch (error: any) {
toast.error(error.message, { style: { maxWidth: 'none' } })
setCreatingBadge(false)
setUploading(false)
}
}
const checkImageUploadDetails = () => {
if (!wallet.initialized) 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 (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://)`)
}
}
}
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()
})
}
// copy claim url to clipboard
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.initialized) 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])
return (
<div>
<NextSeo title="Create Badge" />
<div className="mt-5 space-y-5 text-center">
<h1 className="font-heading text-4xl font-bold">Create Badge</h1>
<Conditional test={uploading}>
<BadgeLoadingModal />
</Conditional>
<p>
Make sure you check our{' '}
<Anchor className="font-bold text-plumbus hover:underline" external href={links['Docs']}>
documentation
</Anchor>{' '}
on how to create a new badge.
</p>
</div>
<div className="mx-10" ref={scrollRef}>
<Conditional test={badgeId !== null}>
<Alert className="mt-5" type="info">
<div className="flex flex-row">
<div>
<div className="w-[384px] h-[384px]" ref={qrRef}>
<QRCodeSVG
className="mx-auto"
level="H"
size={384}
value={`${
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
/>
</div>
<div className="grid grid-cols-2 gap-2 mt-2 w-[384px]">
<Button
className="items-center w-full text-sm text-center rounded"
leftIcon={<FaSave />}
onClick={() => void handleDownloadQr()}
>
Download QR Code
</Button>
<Button
className="w-full text-sm text-center rounded"
isWide
leftIcon={<FaCopy />}
onClick={() => void copyClaimURL()}
variant="solid"
>
Copy Claim URL
</Button>
</div>
</div>
<div className="ml-4 text-lg">
Badge ID:{` ${badgeId as string}`}
<br />
Private Key:
<Tooltip label="Click to copy the private key">
<button
className="group flex space-x-2 font-mono text-base text-white/50 hover:underline"
onClick={() => void copy(createdBadgeKey as string)}
type="button"
>
<span>{truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}</span>
<FaCopy className="opacity-50 group-hover:opacity-100" />
</button>
</Tooltip>
<br />
Transaction Hash: {' '}
<Conditional test={NETWORK === 'testnet'}>
<Anchor
className="text-stargaze hover:underline"
external
href={`${BLOCK_EXPLORER_URL}/tx/${transactionHash as string}`}
>
{transactionHash}
</Anchor>
</Conditional>
<Conditional test={NETWORK === 'mainnet'}>
<Anchor
className="text-stargaze hover:underline"
external
href={`${BLOCK_EXPLORER_URL}/txs/${transactionHash as string}`}
>
{transactionHash}
</Anchor>
</Conditional>
<br />
<div className="text-base">
<div className="flex-row pt-4 mt-4 border-t-2">
<span>
You may click{' '}
<Anchor
className="text-stargaze hover:underline"
external
href={`${
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
>
here
</Anchor>{' '}
or scan the QR code to claim a badge.
</span>
</div>
<br />
<span className="mt-4">You may download the QR code or copy the claim URL to share with others.</span>
</div>
<br />
</div>
</div>
</Alert>
</Conditional>
</div>
<div>
<div
className={clsx(
'mx-10 mt-5',
'grid before:absolute relative grid-cols-3 grid-flow-col items-stretch rounded',
'before:inset-x-0 before:bottom-0 before:border-white/25',
)}
>
<div
className={clsx(
'isolate space-y-1 border-2',
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
mintRule === 'by_key' ? 'border-stargaze' : 'border-transparent',
mintRule !== 'by_key' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
)}
>
<button
className="p-4 w-full h-full text-left bg-transparent"
onClick={() => {
setMintRule('by_key')
setReadyToCreateBadge(false)
}}
type="button"
>
<h4 className="font-bold">Mint Rule: By Key</h4>
<span className="text-sm text-white/80 line-clamp-2">
Badges can be minted more than once with a badge specific message signed by a designated private key.
</span>
</button>
</div>
<div
className={clsx(
'isolate space-y-1 border-2',
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
mintRule === 'by_keys' ? 'border-stargaze' : 'border-transparent',
mintRule !== 'by_keys' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5',
)}
>
<button
className="p-4 w-full h-full text-left bg-transparent"
disabled
onClick={() => {
setMintRule('by_keys')
setReadyToCreateBadge(false)
}}
type="button"
>
<h4 className="font-bold">Mint Rule: By Keys</h4>
<span className="text-sm text-slate-500 line-clamp-2">
Similar to the By Key rule, however each designated private key can only be used once to mint a badge.
</span>
</button>
</div>
<div
className={clsx(
'isolate space-y-1 border-2',
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
mintRule === 'by_minter' ? 'border-stargaze' : 'border-transparent',
mintRule !== 'by_minter' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5',
)}
>
<button
className="p-4 w-full h-full text-left bg-transparent"
disabled
onClick={() => {
setMintRule('by_minter')
setReadyToCreateBadge(false)
}}
type="button"
>
<h4 className="font-bold">Mint Rule: By Minter</h4>
<span className="text-sm line-clamp-2 text-slate/500">
Badges can be minted by a designated minter account.
</span>
</button>
</div>
</div>
</div>
<div className="mx-10">
<ImageUploadDetails mintRule={mintRule} onChange={setImageUploadDetails} />
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
<TextInput className="ml-4 w-full max-w-2xl" {...keyState} disabled required />
<Button className="mt-14 ml-4" isDisabled={creatingBadge} onClick={handleGenerateKey}>
Generate Key
</Button>
</div>
<div className="flex justify-between py-3 px-8 rounded border-2 border-white/20 grid-col-2">
<BadgeDetails
mintRule={mintRule}
onChange={setBadgeDetails}
uploadMethod={imageUploadDetails?.uploadMethod ? imageUploadDetails.uploadMethod : 'new'}
/>
</div>
<Conditional test={readyToCreateBadge}>
<BadgeConfirmationModal confirm={createNewBadge} />
</Conditional>
<div className="flex justify-end w-full">
<Button
className="relative justify-center p-2 mt-2 mb-6 max-h-12 text-white bg-plumbus hover:bg-plumbus-light border-0"
isLoading={creatingBadge}
onClick={() => performBadgeCreationChecks()}
variant="solid"
>
Create Badge
</Button>
</div>
</div>
</div>
)
}
export default withMetadata(BadgeCreationPage, { center: false })

37
pages/badges/index.tsx Normal file
View File

@ -0,0 +1,37 @@
import { HomeCard } from 'components/HomeCard'
import type { NextPage } from 'next'
// import Brand from 'public/brand/brand.svg'
import { withMetadata } from 'utils/layout'
const HomePage: NextPage = () => {
return (
<section className="px-8 pt-4 pb-16 mx-auto space-y-8 max-w-4xl">
<div className="flex justify-center items-center py-8 max-w-xl">
{/* <Brand className="w-full text-plumbus" /> */}
</div>
<h1 className="font-heading text-4xl font-bold">Badges</h1>
<p className="text-xl">
Here you can create badges, execute badge related actions and query the results.
<br />
</p>
<br />
<br />
<div className="grid gap-8 md:grid-cols-2">
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/badges/create" title="Create a Badge">
Select an asset, enter badge metadata and create a new badge.
</HomeCard>
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/badges/myBadges" title="My Badges">
View a list of your badges.
</HomeCard>
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/badges/actions" title="Badge Actions">
Execute badge related actions.
</HomeCard>
</div>
</section>
)
}
export default withMetadata(HomePage, { center: false })

121
pages/badges/myBadges.tsx Normal file
View File

@ -0,0 +1,121 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import axios from 'axios'
import { Alert } from 'components/Alert'
import { Anchor } from 'components/Anchor'
import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next'
import { NextSeo } from 'next-seo'
import { useCallback, useEffect, useState } from 'react'
import { FaSlidersH, FaUser } from 'react-icons/fa'
import { API_URL, BADGE_HUB_ADDRESS, STARGAZE_URL } from 'utils/constants'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
const BadgeList: NextPage = () => {
const wallet = useWallet()
const [myBadges, setMyBadges] = useState<any[]>([])
useEffect(() => {
const fetchBadges = async () => {
await axios
.get(`${API_URL}/api/v1beta/badges/${wallet.address}`)
.then((response) => {
const badgeData = response.data
setMyBadges(badgeData)
})
.catch(console.error)
}
fetchBadges().catch(console.error)
}, [wallet.address])
const renderTable = useCallback(() => {
return (
<div className="overflow-x-auto w-full">
{myBadges.length > 0 && (
<table className="table w-full">
<thead>
<tr>
<th className="pl-36 text-lg font-bold text-left bg-black">Badge Name</th>
<th className="text-lg font-bold bg-black">Badge Description</th>
<th className="bg-black" />
</tr>
</thead>
<tbody>
{myBadges.map((badge: any, index: any) => {
return (
<tr key={index}>
<td className="w-[55%] bg-black">
<div className="flex items-center space-x-3">
<div className="avatar">
<div className="w-28 h-28 mask mask-squircle">
<img
alt="Cover"
src={
(badge?.image as string).startsWith('ipfs')
? `https://ipfs-gw.stargaze-apis.com/ipfs/${(badge?.image as string).substring(7)}`
: badge?.image
}
/>
</div>
</div>
<div className="pl-2">
<p className="overflow-auto max-w-xs font-bold no-scrollbar ">{badge.name}</p>
<p className="max-w-xs text-sm truncate opacity-50">Badge ID: {badge.tokenId}</p>
</div>
</div>
</td>
<td className="overflow-auto w-[35%] max-w-xl bg-black no-scrollbar">
{badge.description}
{/* <br /> */}
{/* <span className="badge badge-ghost badge-sm"></span> */}
</td>
<th className="bg-black">
<div className="flex items-center space-x-8">
<Anchor
className="text-xl text-plumbus"
href={`/badges/actions?badgeHubContractAddress=${BADGE_HUB_ADDRESS}&badgeId=${
(badge.tokenId as string).split('|')[0]
}`}
>
<FaSlidersH />
</Anchor>
<Anchor
className="text-xl text-plumbus"
external
href={`${STARGAZE_URL}/profile/${wallet.address}`}
>
<FaUser />
</Anchor>
</div>
</th>
</tr>
)
})}
</tbody>
</table>
)}
</div>
)
}, [myBadges, wallet.address])
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="My Badges" />
<ContractPageHeader description="A list of your badges." link={links.Documentation} title="My Badges" />
<hr />
<div>{renderTable()}</div>
<br />
<Conditional test={myBadges.length === 0}>
<Alert type="info">You currently don&apos;t own any badges.</Alert>
</Conditional>
</section>
)
}
export default withMetadata(BadgeList, { center: false })

View File

@ -0,0 +1,682 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { toUtf8 } from '@cosmjs/encoding'
import { Alert } from 'components/Alert'
import { Button } from 'components/Button'
import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox'
import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks'
import { FormControl } from 'components/FormControl'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview'
import { LinkTabs } from 'components/LinkTabs'
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
import { Tooltip } from 'components/Tooltip'
import { TransactionHash } from 'components/TransactionHash'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { Badge } from 'contracts/badgeHub'
import type { DispatchExecuteArgs } from 'contracts/badgeHub/messages/execute'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/badgeHub/messages/execute'
import * as crypto from 'crypto'
import { toPng } from 'html-to-image'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import sizeof from 'object-sizeof'
import { QRCodeCanvas } from 'qrcode.react'
import type { FormEvent } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { FaArrowRight, FaCopy, FaSave } from 'react-icons/fa'
import { useMutation } from 'react-query'
import * as secp256k1 from 'secp256k1'
import { copy } from 'utils/clipboard'
import { NETWORK } from 'utils/constants'
import { sha256 } from 'utils/hash'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import { resolveAddress } from 'utils/resolveAddress'
import { truncateMiddle } from 'utils/text'
import { TextInput } from '../../../components/forms/FormInput'
import { MetadataAttributes } from '../../../components/forms/MetadataAttributes'
import { useMetadataAttributesState } from '../../../components/forms/MetadataAttributes.hooks'
import { BADGE_HUB_ADDRESS } from '../../../utils/constants'
const BadgeHubExecutePage: NextPage = () => {
const { badgeHub: contract } = useContracts()
const wallet = useWallet()
const [lastTx, setLastTx] = useState('')
const [badge, setBadge] = useState<Badge>()
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [transferrable, setTransferrable] = useState<boolean>(false)
const [createdBadgeId, setCreatedBadgeId] = useState<string | null>(null)
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
const [signature, setSignature] = useState<string>('')
const [editFee, setEditFee] = useState<number | undefined>(undefined)
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
const qrRef = useRef<HTMLDivElement>(null)
const comboboxState = useExecuteComboboxState()
const type = comboboxState.value?.id
const badgeIdState = useNumberInputState({
id: 'badge-id',
name: 'badgeId',
title: 'Badge ID',
subtitle: 'Enter the badge ID',
defaultValue: 1,
})
const maxSupplyState = useNumberInputState({
id: 'max-supply',
name: 'max-supply',
title: 'Max Supply',
subtitle: 'Maximum number of badges that can be minted',
})
const contractState = useInputState({
id: 'contract-address',
name: 'contract-address',
title: 'Badge Hub Address',
subtitle: 'Address of the Badge Hub contract',
defaultValue: BADGE_HUB_ADDRESS,
})
const contractAddress = contractState.value
// 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: 'Public Key',
subtitle: 'Part of the key pair to be utilized for post-creation access control',
})
const ownerState = useInputState({
id: 'owner-address',
name: 'owner',
title: 'Owner',
subtitle: 'The owner of the badge',
})
const pubkeyState = useInputState({
id: 'pubkey',
name: 'pubkey',
title: 'Pubkey',
subtitle: 'The public key for the badge',
})
const privateKeyState = useInputState({
id: 'privateKey',
name: 'privateKey',
title: 'Private Key',
subtitle: 'The private key 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',
})
const showBadgeField = type === 'create_badge'
const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge'])
const showIdField = isEitherType(type, ['edit_badge', 'mint_by_key'])
const showNFTField = type === 'set_nft'
const showOwnerField = type === 'mint_by_key'
const showPrivateKeyField = type === 'mint_by_key'
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
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,
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: badgeIdState.value,
owner: ownerState.value,
pubkey: pubkeyState.value,
signature,
keys: [],
limit: limitState.value,
owners: [],
nft: nftState.value,
editFee,
contract: contractState.value,
messages,
txSigner: wallet.address,
type,
}
const { isLoading, mutate } = useMutation(
async (event: FormEvent) => {
event.preventDefault()
if (!type) {
throw new Error('Please select message type!')
}
if (!wallet.initialized) {
throw new Error('Please connect your wallet.')
}
if (contractState.value === '') {
throw new Error('Please enter the contract address.')
}
if (wallet.client && type === 'edit_badge') {
const feeRateRaw = await wallet.client.queryContractRaw(
contractAddress,
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
)
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
await toast
.promise(
wallet.client.queryContractSmart(contractAddress, {
badge: { id: badgeIdState.value },
}),
{
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.split(':')[0]} success!`,
})
if (txHash) {
setLastTx(txHash.split(':')[0])
setCreatedBadgeId(txHash.split(':')[1])
}
}
},
{
onError: (error) => {
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)
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')
keyState.onChange(publicKey)
setCreatedBadgeId(null)
}
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.')
}
}
const handleDownloadQr = async () => {
const qrElement = qrRef.current
await toPng(qrElement as HTMLElement).then((dataUrl) => {
const link = document.createElement('a')
link.download = `badge-${createdBadgeId as string}.png`
link.href = dataUrl
link.click()
})
}
const copyClaimURL = async () => {
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
const claimURL = `${baseURL}/?id=${createdBadgeId as string}&key=${createdBadgeKey as string}`
await navigator.clipboard.writeText(claimURL)
toast.success('Copied claim URL to clipboard')
}
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)
}
}
}
useEffect(() => {
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value)
}, [privateKeyState.value, resolvedOwnerAddress])
const router = useRouter()
useEffect(() => {
if (contractAddress.length > 0) {
void router.replace({ query: { contractAddress } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contractAddress])
useEffect(() => {
const initial = new URL(document.URL).searchParams.get('contractAddress')
if (initial && initial.length > 0) contractState.onChange(initial)
if (attributesState.values.length === 0)
attributesState.add({
trait_type: '',
value: '',
})
}, [])
useEffect(() => {
void dispatchEditBadgeMessage().catch((err) => {
toast.error(String(err), { style: { maxWidth: 'none' } })
})
}, [triggerDispatch])
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,
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,
rule: {
by_key: keyState.value,
},
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined,
})
}, [
managerState.value,
nameState.value,
descriptionState.value,
imageState.value,
imageDataState.value,
externalUrlState.value,
attributesState.values,
backgroundColorState.value,
animationUrlState.value,
youtubeUrlState.value,
transferrable,
keyState.value,
timestamp,
maxSupplyState.value,
])
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Execute Badge Hub Contract" />
<ContractPageHeader
description="The Badge Hub contract dashboard is where event organizers create, mint, or edit badges."
link={links.Documentation}
title="Badge Hub Contract"
/>
<LinkTabs activeIndex={2} data={badgeHubLinkTabs} />
{showBadgeField && createdBadgeId && createdBadgeKey && (
<div className="flex flex-row">
<div className="ml-4">
<div className="w-[384px] h-[384px]" ref={qrRef}>
<QRCodeCanvas
size={384}
value={`${
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
}/?id=${createdBadgeId}&key=${createdBadgeKey}`}
/>
</div>
{/* <div className="flex flex-row items-center mt-2 space-x-2 w-[384px] h-12"> */}
<div className="grid grid-cols-2 gap-2 mt-2 w-[384px]">
<Button
className="items-center w-full text-sm text-center rounded"
leftIcon={<FaSave />}
onClick={() => void handleDownloadQr()}
>
Download QR Code
</Button>
<Button
className="w-full text-sm text-center rounded"
isWide
leftIcon={<FaCopy />}
onClick={() => void copyClaimURL()}
variant="solid"
>
Copy Claim URL
</Button>
</div>
</div>
<div className="ml-4">
<Alert className="text-white" type="info">
<div>
<span className="font-bold text-white">Badge ID: </span>
<span className="text-white/80">{createdBadgeId} </span>
</div>
<span className="font-bold text-white">Private Key:</span>
<Tooltip label="Click to copy the private key">
<button
className="group flex space-x-2 font-mono text-base text-white/80 hover:underline"
onClick={() => void copy(createdBadgeKey as string)}
type="button"
>
<span>{truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}</span>
<FaCopy className="opacity-50 group-hover:opacity-100" />
</button>
</Tooltip>
</Alert>
<br />
<Alert className="text-white" type="warning">
Please make sure to save the Badge ID and the Private Key.
</Alert>
</div>
</div>
)}
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
<div className="space-y-8">
<AddressInput {...contractState} />
<ExecuteCombobox {...comboboxState} />
{showIdField && <NumberInput {...badgeIdState} />}
{showBadgeField && <AddressInput {...managerState} />}
{showBadgeField && <TextInput {...keyState} />}
{showBadgeField && <Button onClick={handleGenerateKey}>Generate Key</Button>}
{showMetadataField && (
<div className="p-4 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"
/>
)}
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
{showNFTField && <AddressInput {...nftState} />}
</div>
<div className="space-y-8">
<div className="relative">
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
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 className="pt-9">
<Conditional test={showBadgeField}>
<FormControl htmlId="expiry-date" subtitle="Badge minting expiry date" title="Expiry Date">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl>
</Conditional>
{showBadgeField && <NumberInput className="mt-2" {...maxSupplyState} />}
{showBadgeField && (
<div className="mt-2 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>
)}
</div>
</div>
</form>
</section>
)
}
export default withMetadata(BadgeHubExecutePage, { center: false })

View File

@ -0,0 +1 @@
export { default } from './instantiate'

View File

@ -0,0 +1,119 @@
import { Alert } from 'components/Alert'
import { Button } from 'components/Button'
import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { FormGroup } from 'components/FormGroup'
import { NumberInput } from 'components/forms/FormInput'
import { useNumberInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview'
import { LinkTabs } from 'components/LinkTabs'
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { InstantiateResponse } from 'contracts/sg721'
import type { NextPage } from 'next'
import { NextSeo } from 'next-seo'
import { type FormEvent } from 'react'
import { toast } from 'react-hot-toast'
import { FaAsterisk } from 'react-icons/fa'
import { useMutation } from 'react-query'
import { BADGE_HUB_CODE_ID } from 'utils/constants'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
export interface FeeRate {
metadata: number
key: number
}
const BadgeHubInstantiatePage: NextPage = () => {
const wallet = useWallet()
const { badgeHub: contract } = useContracts()
const metadataFeeRateState = useNumberInputState({
id: 'metadata-fee-rate',
name: 'Metadata Fee Rate',
title: 'Metadata Fee Rate',
subtitle: 'The fee rate, in ustars per byte, for storing metadata on-chain',
placeholder: '500',
})
const keyFeeRateState = useNumberInputState({
id: 'key-fee-rate',
name: 'Key Fee Rate',
title: 'Key Fee Rate',
subtitle: 'The fee rate, in ustars per byte, for storing claim keys on-chain',
placeholder: '500',
})
const { data, isLoading, mutate } = useMutation(
async (event: FormEvent): Promise<InstantiateResponse | null> => {
event.preventDefault()
if (!contract) {
throw new Error('Smart contract connection failed')
}
if (!keyFeeRateState.value) {
throw new Error('Key fee rate is required')
}
if (!metadataFeeRateState.value) {
throw new Error('Metadata fee rate is required')
}
const msg = {
fee_rate: {
metadata: metadataFeeRateState.value.toString(),
key: keyFeeRateState.value.toString(),
},
}
return toast.promise(
contract.instantiate(BADGE_HUB_CODE_ID, msg, 'Stargaze Badge Hub Contract', wallet.address),
{
loading: 'Instantiating contract...',
error: 'Instantiation failed!',
success: 'Instantiation success!',
},
)
},
{
onError: (error) => {
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)
return (
<form className="py-6 px-12 space-y-4" onSubmit={mutate}>
<NextSeo title="Instantiate Badge Hub Contract" />
<ContractPageHeader
description="The badge-hub contract dashboard is where event organizers create, mint, or edit badges."
link={links.Documentation}
title="Badge Hub Contract"
/>
<LinkTabs activeIndex={0} data={badgeHubLinkTabs} />
<Conditional test={Boolean(data)}>
<Alert type="info">
<b>Instantiate success!</b> Here is the transaction result containing the contract address and the transaction
hash.
</Alert>
<JsonPreview content={data} title="Transaction Result" />
<br />
</Conditional>
<FormGroup subtitle title="Fee Rate Details">
<NumberInput isRequired {...metadataFeeRateState} />
<NumberInput isRequired {...keyFeeRateState} />
</FormGroup>
<div className="flex items-center p-4">
<div className="flex-grow" />
<Button isLoading={isLoading} isWide rightIcon={<FaAsterisk />} type="submit">
Instantiate Contract
</Button>
</div>
</form>
)
}
export default withMetadata(BadgeHubInstantiatePage, { center: false })

View File

@ -0,0 +1,132 @@
import { Button } from 'components/Button'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks'
import { FormControl } from 'components/FormControl'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview'
import { LinkTabs } from 'components/LinkTabs'
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
import { TransactionHash } from 'components/TransactionHash'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { MigrateResponse } from 'contracts/badgeHub'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
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 { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
const BadgeHubMigratePage: NextPage = () => {
const { badgeHub: contract } = useContracts()
const wallet = useWallet()
const [lastTx, setLastTx] = useState('')
const comboboxState = useExecuteComboboxState()
const type = comboboxState.value?.id
const codeIdState = useNumberInputState({
id: 'code-id',
name: 'code-id',
title: 'Code ID',
subtitle: 'Code ID of the New Badge Hub contract',
placeholder: '1',
})
const contractState = useInputState({
id: 'contract-address',
name: 'contract-address',
title: 'Badge Hub Contract Address',
subtitle: 'Address of the Badge Hub contract',
})
const contractAddress = contractState.value
const { data, isLoading, mutate } = useMutation(
async (event: FormEvent): Promise<MigrateResponse | null> => {
event.preventDefault()
if (!contract) {
throw new Error('Smart contract connection failed')
}
if (!wallet.initialized) {
throw new Error('Please connect your wallet.')
}
const migrateMsg = {}
return toast.promise(contract.migrate(contractAddress, codeIdState.value, migrateMsg), {
error: `Migration failed!`,
loading: 'Executing message...',
success: (tx) => {
if (tx) {
setLastTx(tx.transactionHash)
}
return `Transaction success!`
},
})
},
{
onError: (error) => {
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)
const router = useRouter()
useEffect(() => {
if (contractAddress.length > 0) {
void router.replace({ query: { contractAddress } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contractAddress])
useEffect(() => {
const initial = new URL(document.URL).searchParams.get('contractAddress')
if (initial && initial.length > 0) contractState.onChange(initial)
}, [])
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Migrate Badge Hub Contract" />
<ContractPageHeader
description="The Badge Hub contract dashboard is where event organizers create, mint, or edit badges."
link={links.Documentation}
title="Badge Hub Contract"
/>
<LinkTabs activeIndex={3} data={badgeHubLinkTabs} />
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
<div className="space-y-8">
<AddressInput {...contractState} />
<NumberInput isRequired {...codeIdState} />
</div>
<div className="space-y-8">
<div className="relative">
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
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={{
sender: wallet.address,
contract: contractAddress,
code_id: codeIdState.value,
msg: {},
}}
isCopyable
/>
</FormControl>
</div>
</form>
</section>
)
}
export default withMetadata(BadgeHubMigratePage, { center: false })

View File

@ -0,0 +1,179 @@
import clsx from 'clsx'
import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader'
import { FormControl } from 'components/FormControl'
import { AddressInput, NumberInput, TextInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview'
import { LinkTabs } from 'components/LinkTabs'
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { QueryType } from 'contracts/badgeHub/messages/query'
import { dispatchQuery, QUERY_LIST } from 'contracts/badgeHub/messages/query'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import { BADGE_HUB_ADDRESS } from '../../../utils/constants'
const BadgeHubQueryPage: NextPage = () => {
const { badgeHub: contract } = useContracts()
const wallet = useWallet()
const contractState = useInputState({
id: 'contract-address',
name: 'contract-address',
title: 'Badge Hub Address',
subtitle: 'Address of the Badge Hub contract',
defaultValue: BADGE_HUB_ADDRESS,
})
const contractAddress = contractState.value
const idState = useNumberInputState({
id: 'id',
name: 'id',
title: 'ID',
subtitle: 'The ID of the badge',
defaultValue: 1,
})
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 [type, setType] = useState<QueryType>('config')
const { data: response } = useQuery(
[
contractAddress,
type,
contract,
wallet,
idState.value,
pubkeyState.value,
startAfterNumberState.value,
startAfterStringState.value,
paginationLimitState.value,
] as const,
async ({ queryKey }) => {
const [_contractAddress, _type, _contract, _wallet, id, pubkey, startAfterNumber, startAfterString, limit] =
queryKey
const messages = contract?.use(_contractAddress)
const result = await dispatchQuery({
id,
pubkey,
messages,
type,
startAfterNumber,
startAfterString,
limit,
})
return result
},
{
placeholderData: null,
onError: (error: any) => {
toast.error(error.message, { style: { maxWidth: 'none' } })
},
enabled: Boolean(contractAddress && contract && wallet),
},
)
const router = useRouter()
useEffect(() => {
if (contractAddress.length > 0) {
void router.replace({ query: { contractAddress } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contractAddress])
useEffect(() => {
const initial = new URL(document.URL).searchParams.get('contractAddress')
if (initial && initial.length > 0) contractState.onChange(initial)
}, [])
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Query Badge Hub Contract" />
<ContractPageHeader
description="The Badge Hub contract dashboard is where event organizers create, mint, or edit badges."
link={links.Documentation}
title="Badge Hub Contract"
/>
<LinkTabs activeIndex={1} data={badgeHubLinkTabs} />
<div className="grid grid-cols-2 p-4 space-x-8">
<div className="space-y-8">
<AddressInput {...contractState} />
<FormControl htmlId="contract-query-type" subtitle="Type of query to be dispatched" title="Query Type">
<select
className={clsx(
'bg-white/10 rounded border-2 border-white/20 form-select',
'placeholder:text-white/50',
'focus:ring focus:ring-plumbus-20',
)}
id="contract-query-type"
name="query-type"
onChange={(e) => setType(e.target.value as QueryType)}
>
{QUERY_LIST.map(({ id, name }) => (
<option key={`query-${id}`} className="mt-2 text-lg bg-[#1A1A1A]" value={id}>
{name}
</option>
))}
</select>
</FormControl>
<Conditional test={type === 'getBadge' || type === 'getKey'}>
<NumberInput {...idState} />
</Conditional>
<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>
<JsonPreview content={contractAddress ? { type, response } : null} title="Query Response" />
</div>
</section>
)
}
export default withMetadata(BadgeHubQueryPage, { center: false })

View File

@ -70,7 +70,7 @@ const BaseMinterMigratePage: NextPage = () => {
},
{
onError: (error) => {
toast.error(String(error))
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)

View File

@ -4,7 +4,7 @@ import type { NextPage } from 'next'
// import Brand from 'public/brand/brand.svg'
import { withMetadata } from 'utils/layout'
import { BASE_FACTORY_ADDRESS } from '../../utils/constants'
import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS } from '../../utils/constants'
const HomePage: NextPage = () => {
return (
@ -12,9 +12,9 @@ const HomePage: NextPage = () => {
<div className="flex justify-center items-center py-8 max-w-xl">
{/* <Brand className="w-full text-plumbus" /> */}
</div>
<h1 className="font-heading text-4xl font-bold">Smart Contracts</h1>
<h1 className="font-heading text-4xl font-bold">Smart Contract Dashboards</h1>
<p className="text-xl">
Here you can invoke and query different smart contracts and see the results.
Here you can execute actions and queries on different smart contracts and see the results.
<br />
</p>
@ -27,7 +27,7 @@ const HomePage: NextPage = () => {
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/contracts/baseMinter"
title="Base Minter contract"
title="Base Minter Contract"
>
Execute messages and run queries on Stargaze&apos;s Base Minter contract.
</HomeCard>
@ -35,20 +35,29 @@ const HomePage: NextPage = () => {
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/contracts/vendingMinter"
title="Vending Minter contract"
title="Vending Minter Contract"
>
Execute messages and run queries on Stargaze&apos;s Vending Minter contract.
</HomeCard>
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/contracts/sg721" title="Sg721 Contract">
Execute messages and run queries on Stargaze&apos;s sg721 contract.
Execute messages and run queries on Stargaze&apos;s SG721 contract.
</HomeCard>
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/contracts/whitelist"
title="Whitelist Contract"
>
Execute messages and run queries on Stargaze&apos;s whitelist contract.
Execute messages and run queries on Stargaze&apos;s Whitelist contract.
</HomeCard>
<Conditional test={BADGE_HUB_ADDRESS !== undefined}>
<HomeCard
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
link="/contracts/badgeHub"
title="Badge Hub Contract"
>
Execute messages and run queries on the Badge Hub contract designed for event organizers.
</HomeCard>
</Conditional>
</div>
</section>
)

View File

@ -70,7 +70,7 @@ const Sg721MigratePage: NextPage = () => {
},
{
onError: (error) => {
toast.error(String(error))
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)

View File

@ -70,7 +70,7 @@ const VendingMinterMigratePage: NextPage = () => {
},
{
onError: (error) => {
toast.error(String(error))
toast.error(String(error), { style: { maxWidth: 'none' } })
},
},
)

View File

@ -25,7 +25,7 @@ import { WHITELIST_CODE_ID } from 'utils/constants'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
const Sg721InstantiatePage: NextPage = () => {
const WhitelistInstantiatePage: NextPage = () => {
const wallet = useWallet()
const { whitelist: contract } = useContracts()
@ -148,4 +148,4 @@ const Sg721InstantiatePage: NextPage = () => {
)
}
export default withMetadata(Sg721InstantiatePage, { center: false })
export default withMetadata(WhitelistInstantiatePage, { center: false })

View File

@ -22,11 +22,14 @@ const HomePage: NextPage = () => {
<br />
<div className="grid gap-8 md:grid-cols-2">
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/collections/create" title="Create">
Upload your assets, enter collection metadata and deploy your collection.
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/collections/" title="Collections">
Create a collection, view a list of your collections or execute collection actions and queries.
</HomeCard>
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/collections" title="My Collections">
Manage your collections with available actions and queries.
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/badges" title="Badges">
Create badges, view a list of them or execute badge related actions and queries.
</HomeCard>
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/contracts" title="Contract Dashboards">
Execute actions and queries for a variety of contracts.
</HomeCard>
</div>
</section>

1
public/icon.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" height="180" viewBox="0 0 180 180" width="180" xmlns="http://www.w3.org/2000/svg"><g fill="#fff"><path clip-rule="evenodd" d="m90 172c45.287 0 82-36.713 82-82 0-45.2873-36.713-82-82-82-45.2873 0-82 36.7127-82 82 0 45.287 36.7127 82 82 82zm90-82c0 49.706-40.294 90-90 90-49.7056 0-90-40.294-90-90 0-49.7056 40.2944-90 90-90 49.706 0 90 40.2944 90 90z" fill-rule="evenodd"/><path d="m106.69 22.47 3.63 50.41 49.06 12.12-46.82 19.03 3.63 50.41-32.5599-38.65-46.82 19.04 26.69-42.92-32.57-38.65 49.06 12.13z"/></g></svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@ -56,6 +56,7 @@ module.exports = {
strategy: 'class',
}),
require('@tailwindcss/line-clamp'),
require('tailwindcss-opentype'),
// custom gradient background
plugin(({ addUtilities }) => {

View File

@ -5,6 +5,10 @@ export const VENDING_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_A
export const BASE_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_ADDRESS
export const SG721_NAME_ADDRESS = process.env.NEXT_PUBLIC_SG721_NAME_ADDRESS
export const BASE_MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_VENDING_MINTER_CODE_ID, 10)
export const BADGE_HUB_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_HUB_CODE_ID, 10)
export const BADGE_HUB_ADDRESS = process.env.NEXT_PUBLIC_BADGE_HUB_ADDRESS
export const BADGE_NFT_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_NFT_CODE_ID, 10)
export const BADGE_NFT_ADDRESS = process.env.NEXT_PUBLIC_BADGE_NFT_ADDRESS
export const PINATA_ENDPOINT_URL = process.env.NEXT_PUBLIC_PINATA_ENDPOINT_URL
export const NETWORK = process.env.NEXT_PUBLIC_NETWORK

25
utils/hash.ts Normal file
View File

@ -0,0 +1,25 @@
/* eslint-disable eslint-comments/disable-enable-pair */
import { Word32Array } from 'jscrypto'
import { SHA256 } from 'jscrypto/SHA256'
import * as secp256k1 from 'secp256k1'
export function sha256(data: Buffer): Buffer {
return Buffer.from(SHA256.hash(new Word32Array(data)).toUint8Array())
}
export function generateSignature(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)
return Buffer.from(signedMessage.signature).toString('hex')
} catch (e) {
console.log(e)
return ''
}
}

View File

@ -2829,6 +2829,13 @@
resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/secp256k1@^4.0.2":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.3.tgz#1b8e55d8e00f08ee7220b4d59a6abe89c37a901c"
integrity sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@5.17.0":
version "5.17.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.17.0.tgz"
@ -4112,7 +4119,7 @@ electron-to-chromium@^1.4.118:
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.124.tgz"
integrity sha512-VhaE9VUYU6d2eIb+4xf83CATD+T+3bTzvxvlADkQE+c2hisiw3sZmvEDtsW704+Zky9WZGhBuQXijDVqSriQLA==
elliptic@^6.4.0, elliptic@^6.5.3:
elliptic@^6.4.0, elliptic@^6.5.3, elliptic@^6.5.4:
version "6.5.4"
resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
@ -5019,6 +5026,11 @@ hosted-git-info@^4.0.1:
dependencies:
lru-cache "^6.0.0"
html-to-image@1.11.11:
version "1.11.11"
resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.11.11.tgz#c0f8a34dc9e4b97b93ff7ea286eb8562642ebbea"
integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
@ -5615,6 +5627,11 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
jscrypto@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/jscrypto/-/jscrypto-1.0.3.tgz#598febca2a939d6f679c54f56e1fe364cef30cc9"
integrity sha512-lryZl0flhodv4SZHOqyb1bx5sKcJxj0VBo0Kzb4QMAg3L021IC9uGpl0RCZa+9KJwlRGSK2C80ITcwbe19OKLQ==
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz"
@ -6253,10 +6270,20 @@ nft.storage@^6.3.0:
streaming-iterables "^6.0.0"
throttled-queue "^2.1.2"
node-addon-api@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
node-fetch@^2.6.1, "node-fetch@https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz":
version "2.6.7"
resolved "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz"
node-gyp-build@^4.2.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055"
integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==
node-releases@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.3.tgz"
@ -6326,6 +6353,13 @@ object-keys@^1.1.1:
resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
object-sizeof@^1.6.0:
version "1.6.3"
resolved "https://registry.yarnpkg.com/object-sizeof/-/object-sizeof-1.6.3.tgz#6edbbf26825b971fd7a32125a800ed2a9895af95"
integrity sha512-LGtilAKuDGKCcvu1Xg3UvAhAeJJlFmblo3faltmOQ80xrGwAHxnauIXucalKdTEksHp/Pq9tZGz1hfyEmjFJPQ==
dependencies:
buffer "^5.6.0"
object.assign@^4.1.0, object.assign@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz"
@ -6693,6 +6727,11 @@ punycode@^2.1.0:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qrcode.react@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8"
integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
@ -7130,6 +7169,15 @@ scheduler@^0, scheduler@^0.22.0:
dependencies:
loose-envify "^1.1.0"
secp256k1@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303"
integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==
dependencies:
elliptic "^6.5.4"
node-addon-api "^2.0.0"
node-gyp-build "^4.2.0"
secretjs@^0.17.0:
version "0.17.5"
resolved "https://registry.npmjs.org/secretjs/-/secretjs-0.17.5.tgz"
@ -7482,6 +7530,11 @@ symbol-observable@^2.0.3:
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz"
integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==
tailwindcss-opentype@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tailwindcss-opentype/-/tailwindcss-opentype-1.1.0.tgz#68aebc6f8a24151167b0a4f372370afe375a3b38"
integrity sha512-d/+/oBITS2JX/Nn+20WSfnxQbYNcSHMNrKeBwmgetxf/9Nmi5k1pOae6OJC4WD+5M8jX9Xb8TnLZ2lkp6qv09A==
tailwindcss@^3, tailwindcss@^3.0, tailwindcss@^3.0.7:
version "3.0.24"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.24.tgz"