-
-
- Image Selection
-
-
-
+
+
+
+ Image Selection
+
+
+ >
+
+
diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx
index f25590a..9cdfad6 100644
--- a/components/collections/actions/Action.tsx
+++ b/components/collections/actions/Action.tsx
@@ -124,8 +124,8 @@ export const CollectionActions = ({
const priceState = useNumberInputState({
id: 'update-mint-price',
name: 'updateMintPrice',
- title: 'Update Mint Price',
- subtitle: 'New minting price in STARS',
+ title: type === 'update_discount_price' ? 'Discount Price' : 'Update Mint Price',
+ subtitle: type === 'update_discount_price' ? 'New discount price in STARS' : 'New minting price in STARS',
})
const descriptionState = useInputState({
@@ -161,7 +161,7 @@ export const CollectionActions = ({
name: 'royaltyShare',
title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid',
- placeholder: '8%',
+ placeholder: '5%',
})
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
@@ -185,7 +185,7 @@ export const CollectionActions = ({
'batch_mint_for',
])
const showAirdropFileField = type === 'airdrop'
- const showPriceField = type === 'update_mint_price'
+ const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
const showDescriptionField = type === 'update_collection_info'
const showImageField = type === 'update_collection_info'
const showExternalLinkField = type === 'update_collection_info'
@@ -379,8 +379,8 @@ export const CollectionActions = ({
{showTokenIdField &&
}
{showTokenIdListField &&
}
{showBaseUriField &&
}
- {showNumberOfTokensField &&
}
- {showPriceField &&
}
+ {showNumberOfTokensField &&
}
+ {showPriceField &&
}
{showDescriptionField &&
}
{showImageField &&
}
{showExternalLinkField &&
}
diff --git a/components/collections/actions/actions.ts b/components/collections/actions/actions.ts
index 5132da4..640861c 100644
--- a/components/collections/actions/actions.ts
+++ b/components/collections/actions/actions.ts
@@ -13,6 +13,8 @@ export const ACTION_TYPES = [
'mint_token_uri',
'purge',
'update_mint_price',
+ 'update_discount_price',
+ 'remove_discount_price',
'mint_to',
'mint_for',
'batch_mint',
@@ -90,16 +92,21 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
name: 'Mint',
description: `Mint a token`,
},
- {
- id: 'purge',
- name: 'Purge',
- description: `Purge`,
- },
{
id: 'update_mint_price',
name: 'Update Mint Price',
description: `Update mint price`,
},
+ {
+ id: 'update_discount_price',
+ name: 'Update Discount Price',
+ description: `Update discount price`,
+ },
+ {
+ id: 'remove_discount_price',
+ name: 'Remove Discount Price',
+ description: `Remove discount price`,
+ },
{
id: 'mint_to',
name: 'Mint To',
@@ -185,6 +192,11 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
name: 'Burn Remaining Tokens',
description: 'Burn remaining tokens',
},
+ {
+ id: 'purge',
+ name: 'Purge',
+ description: `Purge`,
+ },
]
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
@@ -226,6 +238,8 @@ export type DispatchExecuteArgs = {
| { type: Select<'mint_token_uri'>; tokenUri: string }
| { type: Select<'purge'> }
| { type: Select<'update_mint_price'>; price: string }
+ | { type: Select<'update_discount_price'>; price: string }
+ | { type: Select<'remove_discount_price'> }
| { type: Select<'mint_to'>; recipient: string }
| { type: Select<'mint_for'>; recipient: string; tokenId: number }
| { type: Select<'batch_mint'>; recipient: string; batchNumber: number }
@@ -266,6 +280,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'update_mint_price': {
return vendingMinterMessages.updateMintPrice(txSigner, args.price)
}
+ case 'update_discount_price': {
+ return vendingMinterMessages.updateDiscountPrice(txSigner, args.price)
+ }
+ case 'remove_discount_price': {
+ return vendingMinterMessages.removeDiscountPrice(txSigner)
+ }
case 'mint_to': {
return vendingMinterMessages.mintTo(txSigner, args.recipient)
}
@@ -353,6 +373,12 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'update_mint_price': {
return vendingMinterMessages(minterContract)?.updateMintPrice(args.price)
}
+ case 'update_discount_price': {
+ return vendingMinterMessages(minterContract)?.updateDiscountPrice(args.price)
+ }
+ case 'remove_discount_price': {
+ return vendingMinterMessages(minterContract)?.removeDiscountPrice()
+ }
case 'mint_to': {
return vendingMinterMessages(minterContract)?.mintTo(args.recipient)
}
diff --git a/components/collections/creation/RoyaltyDetails.tsx b/components/collections/creation/RoyaltyDetails.tsx
index 5ed298e..0d873e2 100644
--- a/components/collections/creation/RoyaltyDetails.tsx
+++ b/components/collections/creation/RoyaltyDetails.tsx
@@ -36,7 +36,7 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
name: 'royaltyShare',
title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid',
- placeholder: '8%',
+ placeholder: '5%',
})
useEffect(() => {
diff --git a/components/forms/MetadataAttributes.tsx b/components/forms/MetadataAttributes.tsx
index acfac6e..d911d23 100644
--- a/components/forms/MetadataAttributes.tsx
+++ b/components/forms/MetadataAttributes.tsx
@@ -73,9 +73,10 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
}, [traitTypeState.value, traitValueState.value, id])
return (
-
+
+
=> {
+ 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('keys size: ', sizeof(keys))
+ console.log(keys)
const res = await client.execute(
senderAddress,
contractAddress,
@@ -360,6 +370,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge
},
'auto',
'',
+ [coin(Math.ceil((Number(sizeof(keys)) * 1.1 * Number(feeRate.key)) / 2), 'ustars')],
)
return res.transactionHash
diff --git a/contracts/badgeHub/messages/execute.ts b/contracts/badgeHub/messages/execute.ts
index 9c9f595..5d15b56 100644
--- a/contracts/badgeHub/messages/execute.ts
+++ b/contracts/badgeHub/messages/execute.ts
@@ -32,36 +32,36 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
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: '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_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: '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',
diff --git a/contracts/badgeHub/messages/query.ts b/contracts/badgeHub/messages/query.ts
index 39538a7..cbfb4bc 100644
--- a/contracts/badgeHub/messages/query.ts
+++ b/contracts/badgeHub/messages/query.ts
@@ -14,8 +14,8 @@ 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's whitelisted' },
- // { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
+ { 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 interface DispatchQueryProps {
diff --git a/contracts/vendingMinter/contract.ts b/contracts/vendingMinter/contract.ts
index f159f44..8483fa3 100644
--- a/contracts/vendingMinter/contract.ts
+++ b/contracts/vendingMinter/contract.ts
@@ -48,6 +48,8 @@ export interface VendingMinterInstance {
withdraw: (senderAddress: string) => Promise
airdrop: (senderAddress: string, recipients: string[]) => Promise
burnRemaining: (senderAddress: string) => Promise
+ updateDiscountPrice: (senderAddress: string, price: string) => Promise
+ removeDiscountPrice: (senderAddress: string) => Promise
}
export interface VendingMinterMessages {
@@ -66,6 +68,8 @@ export interface VendingMinterMessages {
withdraw: () => WithdrawMessage
airdrop: (recipients: string[]) => CustomMessage
burnRemaining: () => BurnRemainingMessage
+ updateDiscountPrice: (price: string) => UpdateDiscountPriceMessage
+ removeDiscountPrice: () => RemoveDiscountPriceMessage
}
export interface MintMessage {
@@ -97,6 +101,26 @@ export interface UpdateMintPriceMessage {
funds: Coin[]
}
+export interface UpdateDiscountPriceMessage {
+ sender: string
+ contract: string
+ msg: {
+ update_discount_price: {
+ price: string
+ }
+ }
+ funds: Coin[]
+}
+
+export interface RemoveDiscountPriceMessage {
+ sender: string
+ contract: string
+ msg: {
+ remove_discount_price: Record
+ }
+ funds: Coin[]
+}
+
export interface SetWhitelistMessage {
sender: string
contract: string
@@ -326,6 +350,36 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
return res.transactionHash
}
+ const updateDiscountPrice = async (senderAddress: string, price: string): Promise => {
+ const res = await client.execute(
+ senderAddress,
+ contractAddress,
+ {
+ update_discount_price: {
+ price: (Number(price) * 1000000).toString(),
+ },
+ },
+ 'auto',
+ '',
+ )
+
+ return res.transactionHash
+ }
+
+ const removeDiscountPrice = async (senderAddress: string): Promise => {
+ const res = await client.execute(
+ senderAddress,
+ contractAddress,
+ {
+ remove_discount_price: {},
+ },
+ 'auto',
+ '',
+ )
+
+ return res.transactionHash
+ }
+
const setWhitelist = async (senderAddress: string, whitelist: string): Promise => {
const res = await client.execute(
senderAddress,
@@ -552,6 +606,8 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
mint,
purge,
updateMintPrice,
+ updateDiscountPrice,
+ removeDiscountPrice,
setWhitelist,
updateStartTime,
updateStartTradingTime,
@@ -633,6 +689,30 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
}
}
+ const updateDiscountPrice = (price: string): UpdateDiscountPriceMessage => {
+ return {
+ sender: txSigner,
+ contract: contractAddress,
+ msg: {
+ update_discount_price: {
+ price: (Number(price) * 1000000).toString(),
+ },
+ },
+ funds: [],
+ }
+ }
+
+ const removeDiscountPrice = (): RemoveDiscountPriceMessage => {
+ return {
+ sender: txSigner,
+ contract: contractAddress,
+ msg: {
+ remove_discount_price: {},
+ },
+ funds: [],
+ }
+ }
+
const setWhitelist = (whitelist: string): SetWhitelistMessage => {
return {
sender: txSigner,
@@ -795,6 +875,8 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
mint,
purge,
updateMintPrice,
+ updateDiscountPrice,
+ removeDiscountPrice,
setWhitelist,
updateStartTime,
updateStartTradingTime,
diff --git a/contracts/vendingMinter/messages/execute.ts b/contracts/vendingMinter/messages/execute.ts
index 391178f..d75f4e6 100644
--- a/contracts/vendingMinter/messages/execute.ts
+++ b/contracts/vendingMinter/messages/execute.ts
@@ -7,6 +7,8 @@ export const EXECUTE_TYPES = [
'mint',
'purge',
'update_mint_price',
+ 'update_discount_price',
+ 'remove_discount_price',
'set_whitelist',
'update_start_time',
'update_start_trading_time',
@@ -39,6 +41,16 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
name: 'Update Mint Price',
description: `Update mint price`,
},
+ {
+ id: 'update_discount_price',
+ name: 'Update Discount Price',
+ description: `Update discount price`,
+ },
+ {
+ id: 'remove_discount_price',
+ name: 'Remove Discount Price',
+ description: `Remove discount price`,
+ },
{
id: 'set_whitelist',
name: 'Set Whitelist',
@@ -98,6 +110,8 @@ export type DispatchExecuteArgs = {
| { type: Select<'mint'> }
| { type: Select<'purge'> }
| { type: Select<'update_mint_price'>; price: string }
+ | { type: Select<'update_discount_price'>; price: string }
+ | { type: Select<'remove_discount_price'> }
| { type: Select<'set_whitelist'>; whitelist: string }
| { type: Select<'update_start_time'>; startTime: string }
| { type: Select<'update_start_trading_time'>; startTime?: string }
@@ -123,6 +137,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'update_mint_price': {
return messages.updateMintPrice(txSigner, args.price)
}
+ case 'update_discount_price': {
+ return messages.updateDiscountPrice(txSigner, args.price)
+ }
+ case 'remove_discount_price': {
+ return messages.removeDiscountPrice(txSigner)
+ }
case 'set_whitelist': {
return messages.setWhitelist(txSigner, args.whitelist)
}
@@ -167,6 +187,12 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'update_mint_price': {
return messages(contract)?.updateMintPrice(args.price)
}
+ case 'update_discount_price': {
+ return messages(contract)?.updateDiscountPrice(args.price)
+ }
+ case 'remove_discount_price': {
+ return messages(contract)?.removeDiscountPrice()
+ }
case 'set_whitelist': {
return messages(contract)?.setWhitelist(args.whitelist)
}
diff --git a/package.json b/package.json
index 29e79d9..2a082d1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "stargaze-studio",
- "version": "0.4.5",
+ "version": "0.4.8",
"workspaces": [
"packages/*"
],
diff --git a/pages/badges/create.tsx b/pages/badges/create.tsx
index 8cc7a95..8abd492 100644
--- a/pages/badges/create.tsx
+++ b/pages/badges/create.tsx
@@ -1,5 +1,8 @@
/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable @typescript-eslint/restrict-template-expressions */
+/* eslint-disable no-nested-ternary */
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
@@ -20,12 +23,14 @@ import { useInputState } from 'components/forms/FormInput.hooks'
import { Tooltip } from 'components/Tooltip'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
+import type { Badge } from 'contracts/badgeHub'
import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute'
import { dispatchExecute as badgeHubDispatchExecute } from 'contracts/badgeHub/messages/execute'
import * as crypto from 'crypto'
import { toPng } from 'html-to-image'
import type { NextPage } from 'next'
import { NextSeo } from 'next-seo'
+import sizeof from 'object-sizeof'
import { QRCodeSVG } from 'qrcode.react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
@@ -36,8 +41,11 @@ 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 { resolveAddress } from 'utils/resolveAddress'
import { truncateMiddle } from 'utils/text'
+import { generateKeyPairs } from '../../utils/hash'
+
const BadgeCreationPage: NextPage = () => {
const wallet = useWallet()
const { badgeHub: badgeHubContract } = useContracts()
@@ -50,13 +58,18 @@ const BadgeCreationPage: NextPage = () => {
const [uploading, setUploading] = useState(false)
const [creatingBadge, setCreatingBadge] = useState(false)
+ const [isAddingKeysComplete, setIsAddingKeysComplete] = useState(false)
const [readyToCreateBadge, setReadyToCreateBadge] = useState(false)
const [mintRule, setMintRule] = useState('by_key')
+ const [resolvedMinterAddress, setResolvedMinterAddress] = useState('')
+ const [tempBadge, setTempBadge] = useState()
const [badgeId, setBadgeId] = useState(null)
const [imageUrl, setImageUrl] = useState(null)
const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined)
const [transactionHash, setTransactionHash] = useState(null)
+ const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
+ const [numberOfKeys, setNumberOfKeys] = useState(1)
const qrRef = useRef(null)
const keyState = useInputState({
@@ -66,6 +79,14 @@ const BadgeCreationPage: NextPage = () => {
subtitle: 'Part of the key pair to be utilized for post-creation access control',
})
+ const designatedMinterState = useInputState({
+ id: 'designatedMinter',
+ name: 'designatedMinter',
+ title: 'Minter Address',
+ subtitle: 'The address of the designated minter for this badge',
+ defaultValue: wallet.address,
+ })
+
const performBadgeCreationChecks = () => {
try {
setReadyToCreateBadge(false)
@@ -112,6 +133,46 @@ const BadgeCreationPage: NextPage = () => {
}
}
+ const resolveMinterAddress = async () => {
+ await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
+ setResolvedMinterAddress(resolvedAddress)
+ })
+ }
+ useEffect(() => {
+ void resolveMinterAddress()
+ }, [designatedMinterState.value])
+
+ useEffect(() => {
+ const badge = {
+ manager: badgeDetails?.manager as string,
+ metadata: {
+ name: badgeDetails?.name || undefined,
+ description: badgeDetails?.description || undefined,
+ image: imageUrl || undefined,
+ image_data: badgeDetails?.image_data || undefined,
+ external_url: badgeDetails?.external_url || undefined,
+ attributes: badgeDetails?.attributes || undefined,
+ background_color: badgeDetails?.background_color || undefined,
+ animation_url: badgeDetails?.animation_url || undefined,
+ youtube_url: badgeDetails?.youtube_url || undefined,
+ },
+ transferrable: badgeDetails?.transferrable as boolean,
+ rule:
+ mintRule === 'by_key'
+ ? {
+ by_key: keyState.value,
+ }
+ : mintRule === 'by_minter'
+ ? {
+ by_minter: resolvedMinterAddress,
+ }
+ : 'by_keys',
+ expiry: badgeDetails?.expiry || undefined,
+ max_supply: badgeDetails?.max_supply || undefined,
+ }
+ setTempBadge(badge)
+ }, [badgeDetails, keyState.value, mintRule, resolvedMinterAddress, imageUrl])
+
const createNewBadge = async () => {
try {
if (!wallet.initialized) throw new Error('Wallet not connected')
@@ -133,9 +194,16 @@ const BadgeCreationPage: NextPage = () => {
youtube_url: badgeDetails?.youtube_url || undefined,
},
transferrable: badgeDetails?.transferrable as boolean,
- rule: {
- by_key: keyState.value,
- },
+ rule:
+ mintRule === 'by_key'
+ ? {
+ by_key: keyState.value,
+ }
+ : mintRule === 'by_minter'
+ ? {
+ by_minter: resolvedMinterAddress,
+ }
+ : 'by_keys',
expiry: badgeDetails?.expiry || undefined,
max_supply: badgeDetails?.max_supply || undefined,
}
@@ -147,13 +215,50 @@ const BadgeCreationPage: NextPage = () => {
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' } })
+ if (mintRule !== 'by_keys') {
+ setBadgeId(null)
+ setIsAddingKeysComplete(false)
+ const data = await badgeHubDispatchExecute(payload)
+ console.log(data)
+ setCreatingBadge(false)
+ setTransactionHash(data.split(':')[0])
+ setBadgeId(data.split(':')[1])
+ } else {
+ setBadgeId(null)
+ setIsAddingKeysComplete(false)
+ setKeyPairs([])
+ const generatedKeyPairs = generateKeyPairs(numberOfKeys)
+ setKeyPairs(generatedKeyPairs)
+ await badgeHubDispatchExecute(payload)
+ .then(async (data) => {
+ setCreatingBadge(false)
+ setTransactionHash(data.split(':')[0])
+ setBadgeId(data.split(':')[1])
+ const res = await toast.promise(
+ badgeHubContract.use(BADGE_HUB_ADDRESS)?.addKeys(
+ wallet.address,
+ Number(data.split(':')[1]),
+ generatedKeyPairs.map((key) => key.publicKey),
+ ) as Promise,
+ {
+ loading: 'Adding keys...',
+ success: (result) => {
+ setIsAddingKeysComplete(true)
+ return `Keys added successfully! Tx Hash: ${result}`
+ },
+ error: (error: { message: any }) => `Failed to add keys: ${error.message}`,
+ },
+ )
+ })
+ .catch((error: { message: any }) => {
+ toast.error(error.message, { style: { maxWidth: 'none' } })
+ setUploading(false)
+ setIsAddingKeysComplete(false)
+ setCreatingBadge(false)
+ })
+ }
+ } catch (err: any) {
+ toast.error(err.message, { style: { maxWidth: 'none' } })
setCreatingBadge(false)
setUploading(false)
}
@@ -184,7 +289,8 @@ const BadgeCreationPage: NextPage = () => {
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 (mintRule === 'by_key' && (keyState.value === '' || !createdBadgeKey))
+ throw new Error('Please generate a public key')
if (badgeDetails.external_url) {
try {
const url = new URL(badgeDetails.external_url)
@@ -219,7 +325,15 @@ const BadgeCreationPage: NextPage = () => {
})
}
- // copy claim url to clipboard
+ const handleDownloadKeys = () => {
+ const element = document.createElement('a')
+ const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
+ element.href = URL.createObjectURL(file)
+ element.download = `badge-${badgeId as string}-keys.json`
+ document.body.appendChild(element)
+ element.click()
+ }
+
const copyClaimURL = async () => {
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
const claimURL = `${baseURL}/?id=${badgeId as string}&key=${createdBadgeKey as string}`
@@ -244,6 +358,12 @@ const BadgeCreationPage: NextPage = () => {
setReadyToCreateBadge(false)
}, [imageUploadDetails?.uploadMethod])
+ useEffect(() => {
+ if (keyPairs.length > 0) {
+ toast.success('Key pairs generated successfully.')
+ }
+ }, [keyPairs])
+
return (
@@ -265,52 +385,164 @@ const BadgeCreationPage: NextPage = () => {
-
-
-
-
-
+
+
+
+
+
+
+
+
+ }
+ onClick={() => void handleDownloadQr()}
+ >
+ Download QR Code
+
+ }
+ onClick={() => void copyClaimURL()}
+ variant="solid"
+ >
+ Copy Claim URL
+
+
-
-
}
- onClick={() => void handleDownloadQr()}
- >
- Download QR Code
-
-
}
- onClick={() => void copyClaimURL()}
- variant="solid"
- >
- Copy Claim URL
-
+
+ Badge ID:{` ${badgeId as string}`}
+
+ Private Key:
+
+ void copy(createdBadgeKey as string)}
+ type="button"
+ >
+ {truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}
+
+
+
+
+ Transaction Hash: {' '}
+
+
+ {transactionHash}
+
+
+
+
+ {transactionHash}
+
+
+
+
+
+
+ You may click{' '}
+
+ here
+ {' '}
+ or scan the QR code to claim a badge.
+
+
+
+
+ You may download the QR code or copy the claim URL to share with others.
+
+
+
+
+
+
+
+
Badge ID:{` ${badgeId as string}`}
- Private Key:
-
- void copy(createdBadgeKey as string)}
- type="button"
+ Transaction Hash: {' '}
+
+
- {truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}
-
-
-
+ {transactionHash}
+
+
+
+
+ {transactionHash}
+
+
+
+
+
+
+ Make sure to download the whitelisted keys added during badge creation.
+
+ handleDownloadKeys()}>
+ Download Keys
+
+
+
+
+
+
+ You may click{' '}
+
+ here
+ {' '}
+ and select Actions {'>'} Add Keys to add (additional) whitelisted keys or select Actions {'>'}{' '}
+ Mint by Keys to use one of the keys to mint a badge.
+
+
+
+
+
+
+
+
+
+
+ Badge ID:{` ${badgeId as string}`}
+
+ Designated Minter Address: {` ${resolvedMinterAddress}`}
Transaction Hash: {' '}
@@ -339,22 +571,19 @@ const BadgeCreationPage: NextPage = () => {
here
{' '}
- or scan the QR code to claim a badge.
+ and select Actions {'>'} Mint By Minter to mint a badge.
-
- You may download the QR code or copy the claim URL to share with others.
-
-
-
+
+
@@ -374,81 +603,136 @@ const BadgeCreationPage: NextPage = () => {
mintRule !== 'by_key' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
)}
>
- {
- setMintRule('by_key')
- setReadyToCreateBadge(false)
- }}
- type="button"
+
- Mint Rule: By Key
-
- Badges can be minted more than once with a badge specific message signed by a designated private key.
-
-
+ {
+ setMintRule('by_key')
+ setReadyToCreateBadge(false)
+ setBadgeId(null)
+ }}
+ type="button"
+ >
+ Mint Rule: By Key
+
+ Multiple badges can be minted to different addresses by the owner of a single designated key.
+
+
+
- {
- setMintRule('by_keys')
- setReadyToCreateBadge(false)
- }}
- type="button"
+
- Mint Rule: By Keys
-
- Similar to the By Key rule, however each designated private key can only be used once to mint a badge.
-
-
+ {
+ setMintRule('by_keys')
+ setReadyToCreateBadge(false)
+ setBadgeId(null)
+ }}
+ type="button"
+ >
+ Mint Rule: By Keys
+
+ Multiple key pairs are generated and designated to be only used once to mint a single badge.
+
+
+
- {
- setMintRule('by_minter')
- setReadyToCreateBadge(false)
- }}
- type="button"
+
- Mint Rule: By Minter
-
- Badges can be minted by a designated minter account.
-
-
+ {
+ setMintRule('by_minter')
+ setReadyToCreateBadge(false)
+ setBadgeId(null)
+ }}
+ type="button"
+ >
+ Mint Rule: By Minter
+
+ No key designation. Multiple badges can be minted to different addresses by a pre-determined minter
+ address.
+
+
+
+
+
+
+
+ Generate Key
+
+
+
-
-
-
- Generate Key
-
-
+
+
+
+
+ Number of Keys
+
+ The number of key pairs to be whitelisted for post-creation access control
+
+
+
setNumberOfKeys(Number(e.target.value))}
+ required
+ type="number"
+ value={numberOfKeys}
+ />
+
+
+
+
+
+
+
+
+
{
{myBadges.map((badge: any, index: any) => {
return (
-
+
{
-
{badge.name}
+
+ {badge.name ? badge.name : 'No name provided.'}
+
Badge ID: {badge.tokenId}
-
- {badge.description}
+
+ {badge.description ? badge.description : 'No description provided.'}
{/* */}
{/* */}
diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx
index d200120..902e7ea 100644
--- a/pages/collections/create.tsx
+++ b/pages/collections/create.tsx
@@ -754,12 +754,17 @@ const CollectionCreationPage: NextPage = () => {
mintingDetails.perAddressLimit > mintingDetails.numTokens
)
throw new Error('Invalid limit for tokens per address')
+ if (mintingDetails.numTokens < 100 && mintingDetails.perAddressLimit > 3)
+ throw new Error(
+ 'Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.',
+ )
if (
- mintingDetails.numTokens > 100 &&
- mintingDetails.numTokens < 100 * mintingDetails.perAddressLimit &&
- mintingDetails.perAddressLimit > mintingDetails.numTokens / 100
+ mintingDetails.numTokens >= 100 &&
+ mintingDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3)
)
- throw new Error('Invalid limit for tokens per address. The limit cannot exceed 1% of the total number of tokens.')
+ throw new Error(
+ 'Invalid limit for tokens per address. Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.',
+ )
if (mintingDetails.startTime === '') throw new Error('Start time is required')
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
}
@@ -776,15 +781,25 @@ const CollectionCreationPage: NextPage = () => {
const whitelistStartDate = new Date(Number(config?.start_time) / 1000000)
throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`)
}
- if (
- mintingDetails?.numTokens &&
- config?.per_address_limit &&
- mintingDetails.numTokens > 100 &&
- Number(config.per_address_limit) > mintingDetails.numTokens / 100
- )
- throw Error(
- `Invalid limit for tokens per address (${config.per_address_limit} tokens). The limit cannot exceed 1% of the total number of tokens.`,
- )
+
+ if (mintingDetails?.numTokens && config?.per_address_limit) {
+ if (mintingDetails.numTokens >= 100 && Number(config.per_address_limit) > 50) {
+ throw Error(
+ `Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`,
+ )
+ } else if (
+ mintingDetails.numTokens >= 100 &&
+ Number(config.per_address_limit) > Math.ceil((mintingDetails.numTokens / 100) * 3)
+ ) {
+ throw Error(
+ `Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.`,
+ )
+ } else if (mintingDetails.numTokens < 100 && Number(config.per_address_limit) > 3) {
+ throw Error(
+ `Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.`,
+ )
+ }
+ }
}
} else if (whitelistDetails.whitelistType === 'new') {
if (whitelistDetails.members?.length === 0) throw new Error('Whitelist member list cannot be empty')
@@ -801,15 +816,24 @@ const CollectionCreationPage: NextPage = () => {
throw new Error('Whitelist start time cannot be later than whitelist end time')
if (Number(whitelistDetails.startTime) !== Number(mintingDetails?.startTime))
throw new Error('Whitelist start time must be the same as the minting start time')
- if (
- mintingDetails?.numTokens &&
- whitelistDetails.perAddressLimit &&
- mintingDetails.numTokens > 100 &&
- whitelistDetails.perAddressLimit > mintingDetails.numTokens / 100
- )
- throw Error(
- `Invalid limit for tokens per address (${whitelistDetails.perAddressLimit} tokens). The limit cannot exceed 1% of the total number of tokens.`,
- )
+ if (whitelistDetails.perAddressLimit && mintingDetails?.numTokens) {
+ if (mintingDetails.numTokens >= 100 && whitelistDetails.perAddressLimit > 50) {
+ throw Error(
+ `Invalid limit for tokens per address. Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`,
+ )
+ } else if (
+ mintingDetails.numTokens >= 100 &&
+ whitelistDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3)
+ ) {
+ throw Error(
+ `Invalid limit for tokens per address. Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.`,
+ )
+ } else if (mintingDetails.numTokens < 100 && whitelistDetails.perAddressLimit > 3) {
+ throw Error(
+ `Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.`,
+ )
+ }
+ }
}
}
diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx
index 4980e61..e078be8 100644
--- a/pages/contracts/badgeHub/execute.tsx
+++ b/pages/contracts/badgeHub/execute.tsx
@@ -1,17 +1,22 @@
/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable no-nested-ternary */
/* 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 clsx from 'clsx'
import { Alert } from 'components/Alert'
+import type { MintRule } from 'components/badges/creation/ImageUploadDetails'
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 { AddressList } from 'components/forms/AddressList'
+import { useAddressListState } from 'components/forms/AddressList.hooks'
import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
@@ -20,6 +25,7 @@ import { LinkTabs } from 'components/LinkTabs'
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
import { Tooltip } from 'components/Tooltip'
import { TransactionHash } from 'components/TransactionHash'
+import { WhitelistUpload } from 'components/WhitelistUpload'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { Badge } from 'contracts/badgeHub'
@@ -40,7 +46,8 @@ 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 { generateKeyPairs, sha256 } from 'utils/hash'
+import { isValidAddress } from 'utils/isValidAddress'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import { resolveAddress } from 'utils/resolveAddress'
@@ -62,10 +69,15 @@ const BadgeHubExecutePage: NextPage = () => {
const [createdBadgeId, setCreatedBadgeId] = useState(null)
const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined)
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('')
+ const [resolvedMinterAddress, setResolvedMinterAddress] = useState('')
const [signature, setSignature] = useState('')
+ const [ownerList, setOwnerList] = useState([])
const [editFee, setEditFee] = useState(undefined)
const [triggerDispatch, setTriggerDispatch] = useState(false)
const qrRef = useRef(null)
+ const [numberOfKeys, setNumberOfKeys] = useState(0)
+ const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
+ const [mintRule, setMintRule] = useState('by_key')
const comboboxState = useExecuteComboboxState()
const type = comboboxState.value?.id
@@ -173,20 +185,26 @@ const BadgeHubExecutePage: NextPage = () => {
name: 'owner',
title: 'Owner',
subtitle: 'The owner of the badge',
+ defaultValue: wallet.address,
})
+ const ownerListState = useAddressListState()
+
const pubkeyState = useInputState({
id: 'pubkey',
name: 'pubkey',
title: 'Pubkey',
- subtitle: 'The public key for the badge',
+ subtitle: 'The whitelisted public key authorized to mint a badge',
})
const privateKeyState = useInputState({
id: 'privateKey',
name: 'privateKey',
title: 'Private Key',
- subtitle: 'The private key generated during badge creation',
+ subtitle:
+ type === 'mint_by_keys'
+ ? 'The corresponding private key for the whitelisted public key'
+ : 'The private key that was generated during badge creation',
})
const nftState = useInputState({
@@ -200,15 +218,34 @@ const BadgeHubExecutePage: NextPage = () => {
id: 'limit',
name: 'limit',
title: 'Limit',
- subtitle: 'Number of keys/owners to execute the action for',
+ subtitle: 'Number of keys/owners to execute the action for (0 for all)',
+ })
+
+ const designatedMinterState = useInputState({
+ id: 'designatedMinter',
+ name: 'designatedMinter',
+ title: 'Minter Address',
+ subtitle: 'The address of the designated minter for this badge',
+ defaultValue: wallet.address,
})
const showBadgeField = type === 'create_badge'
const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge'])
- const showIdField = isEitherType(type, ['edit_badge', 'mint_by_key'])
+ const showIdField = isEitherType(type, [
+ 'edit_badge',
+ 'add_keys',
+ 'purge_keys',
+ 'purge_owners',
+ 'mint_by_key',
+ 'mint_by_keys',
+ 'mint_by_minter',
+ ])
+ const showLimitField = isEitherType(type, ['purge_keys', 'purge_owners'])
const showNFTField = type === 'set_nft'
- const showOwnerField = type === 'mint_by_key'
- const showPrivateKeyField = type === 'mint_by_key'
+ const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
+ const showOwnerListField = isEitherType(type, ['mint_by_minter'])
+ const showPubkeyField = isEitherType(type, ['mint_by_keys'])
+ const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
const payload: DispatchExecuteArgs = {
@@ -234,9 +271,16 @@ const BadgeHubExecutePage: NextPage = () => {
youtube_url: youtubeUrlState.value || undefined,
},
transferrable,
- rule: {
- by_key: keyState.value,
- },
+ rule:
+ mintRule === 'by_key'
+ ? {
+ by_key: keyState.value,
+ }
+ : mintRule === 'by_minter'
+ ? {
+ by_minter: resolvedMinterAddress,
+ }
+ : 'by_keys',
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined,
},
@@ -260,12 +304,19 @@ const BadgeHubExecutePage: NextPage = () => {
youtube_url: youtubeUrlState.value || undefined,
},
id: badgeIdState.value,
- owner: ownerState.value,
+ owner: resolvedOwnerAddress,
pubkey: pubkeyState.value,
signature,
- keys: [],
- limit: limitState.value,
- owners: [],
+ keys: keyPairs.map((keyPair) => keyPair.publicKey),
+ limit: limitState.value || undefined,
+ owners: [
+ ...new Set(
+ ownerListState.values
+ .map((a) => a.address.trim())
+ .filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
+ .concat(ownerList),
+ ),
+ ],
nft: nftState.value,
editFee,
contract: contractState.value,
@@ -337,6 +388,7 @@ const BadgeHubExecutePage: NextPage = () => {
if (txHash) {
setLastTx(txHash.split(':')[0])
setCreatedBadgeId(txHash.split(':')[1])
+ badgeIdState.onChange(!isNaN(Number(txHash.split(':')[1])) ? Number(txHash.split(':')[1]) : 1)
}
}
},
@@ -388,6 +440,14 @@ const BadgeHubExecutePage: NextPage = () => {
link.click()
})
}
+ const handleDownloadKeys = () => {
+ const element = document.createElement('a')
+ const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
+ element.href = URL.createObjectURL(file)
+ element.download = `badge-${badgeIdState.value}-keys.json`
+ document.body.appendChild(element)
+ element.click()
+ }
const copyClaimURL = async () => {
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
@@ -409,6 +469,12 @@ const BadgeHubExecutePage: NextPage = () => {
}
}
+ useEffect(() => {
+ if (numberOfKeys > 0) {
+ setKeyPairs(generateKeyPairs(numberOfKeys))
+ }
+ }, [numberOfKeys])
+
useEffect(() => {
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value)
@@ -448,6 +514,15 @@ const BadgeHubExecutePage: NextPage = () => {
void resolveOwnerAddress()
}, [ownerState.value])
+ const resolveMinterAddress = async () => {
+ await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
+ setResolvedMinterAddress(resolvedAddress)
+ })
+ }
+ useEffect(() => {
+ void resolveMinterAddress()
+ }, [designatedMinterState.value])
+
const resolveManagerAddress = async () => {
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
setBadge({
@@ -472,9 +547,16 @@ const BadgeHubExecutePage: NextPage = () => {
youtube_url: youtubeUrlState.value || undefined,
},
transferrable,
- rule: {
- by_key: keyState.value,
- },
+ rule:
+ mintRule === 'by_key'
+ ? {
+ by_key: keyState.value,
+ }
+ : mintRule === 'by_minter'
+ ? {
+ by_minter: resolvedMinterAddress,
+ }
+ : 'by_keys',
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined,
})
@@ -508,9 +590,16 @@ const BadgeHubExecutePage: NextPage = () => {
youtube_url: youtubeUrlState.value || undefined,
},
transferrable,
- rule: {
- by_key: keyState.value,
- },
+ rule:
+ mintRule === 'by_key'
+ ? {
+ by_key: keyState.value,
+ }
+ : mintRule === 'by_minter'
+ ? {
+ by_minter: resolvedMinterAddress,
+ }
+ : 'by_keys',
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined,
})
@@ -541,7 +630,7 @@ const BadgeHubExecutePage: NextPage = () => {
/>
- {showBadgeField && createdBadgeId && createdBadgeKey && (
+ {showBadgeField && createdBadgeId && createdBadgeKey && mintRule === 'by_key' && (
@@ -598,14 +687,116 @@ const BadgeHubExecutePage: NextPage = () => {
)}
+
+ {showBadgeField && createdBadgeId && mintRule === 'by_keys' && (
+
+
+ Badge ID:{` ${createdBadgeId as string}`}
+
+
+
+
+ You may select Message Type {'>'} Add Keys to add whitelisted keys authorized to mint a badge.
+
+
+
+
+
+ )}
+
+ {showBadgeField && createdBadgeId && mintRule === 'by_minter' && (
+
+
+ Badge successfully created with ID:{` ${createdBadgeId as string}`}
+
+ Designated Minter Address: {` ${resolvedMinterAddress}`}
+
+
+
+
+ You may select Message Type {'>'} Mint by Minter to mint badges using the designated minter wallet.
+
+
+
+
+
+ )}
+