diff --git a/contracts/badgeHub/messages/execute.ts b/contracts/badgeHub/messages/execute.ts index a3182ca..d94796e 100644 --- a/contracts/badgeHub/messages/execute.ts +++ b/contracts/badgeHub/messages/execute.ts @@ -84,7 +84,7 @@ export type DispatchExecuteArgs = { } & ( | { type: undefined } | { type: Select<'create_badge'>; badge: Badge } - | { type: Select<'edit_badge'>; id: number; metadata: Metadata } + | { 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 } @@ -104,7 +104,7 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { return messages.createBadge(txSigner, args.badge) } case 'edit_badge': { - return messages.editBadge(txSigner, args.id, args.metadata) + return messages.editBadge(txSigner, args.id, args.metadata, args.editFee) } case 'add_keys': { return messages.addKeys(txSigner, args.id, args.keys) diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 631e994..4039b2e 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -1,3 +1,4 @@ +import { toUtf8 } from '@cosmjs/encoding' import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' import { ContractPageHeader } from 'components/ContractPageHeader' @@ -13,6 +14,7 @@ import { badgeHubLinkTabs } from 'components/LinkTabs.data' 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' @@ -20,6 +22,7 @@ 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' @@ -30,30 +33,37 @@ import * as secp256k1 from 'secp256k1' import { NETWORK } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' +import { resolveAddress } from 'utils/resolveAddress' 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() const [timestamp, setTimestamp] = useState(undefined) const [transferrable, setTransferrable] = useState(false) const [createdBadgeId, setCreatedBadgeId] = useState(undefined) const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) + const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') + const [editFee, setEditFee] = useState(undefined) + const [triggerDispatch, setTriggerDispatch] = useState(false) const qrRef = useRef(null) const comboboxState = useExecuteComboboxState() const type = comboboxState.value?.id - const tokenIdState = useNumberInputState({ - id: 'token-id', - name: 'tokenId', - title: 'Token ID', - subtitle: 'Enter the token ID', + const badgeIdState = useNumberInputState({ + id: 'badge-id', + name: 'badgeId', + title: 'Badge ID', + subtitle: 'Enter the badge ID', + defaultValue: 1, }) const maxSupplyState = useNumberInputState({ @@ -68,6 +78,7 @@ const BadgeHubExecutePage: NextPage = () => { name: 'contract-address', title: 'Badge Hub Address', subtitle: 'Address of the Badge Hub contract', + defaultValue: BADGE_HUB_ADDRESS, }) const contractAddress = contractState.value @@ -145,13 +156,6 @@ const BadgeHubExecutePage: NextPage = () => { subtitle: 'The key generated for the badge', }) - const idState = useNumberInputState({ - id: 'id', - name: 'id', - title: 'ID', - subtitle: 'The ID of the badge', - }) - const ownerState = useInputState({ id: 'owner-address', name: 'owner', @@ -241,7 +245,7 @@ const BadgeHubExecutePage: NextPage = () => { animation_url: animationUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined, }, - id: idState.value, + id: badgeIdState.value, owner: ownerState.value, pubkey: pubkeyState.value, signature: signatureState.value, @@ -249,6 +253,7 @@ const BadgeHubExecutePage: NextPage = () => { limit: limitState.value, owners: [], nft: nftState.value, + editFee, contract: contractState.value, messages, txSigner: wallet.address, @@ -266,15 +271,58 @@ const BadgeHubExecutePage: NextPage = () => { if (contractState.value === '') { throw new Error('Please enter the contract address.') } - 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]) - console.log(txHash.split(':')[1]) + 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} success!`, + }) + if (txHash) { + setLastTx(txHash) + } } }, { @@ -316,6 +364,19 @@ const BadgeHubExecutePage: NextPage = () => { 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) + } + } + } + const router = useRouter() useEffect(() => { @@ -335,6 +396,104 @@ const BadgeHubExecutePage: NextPage = () => { }) }, []) + 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 (
@@ -380,6 +539,7 @@ const BadgeHubExecutePage: NextPage = () => {
+ {showIdField && } {showBadgeField && } {showBadgeField && } {showBadgeField && }