Implement editBadge for Badge Hub Dashboard > Execute

This commit is contained in:
Serkan Reis 2023-02-21 10:50:13 +03:00
parent edccae535e
commit 4a11d08ca9
2 changed files with 184 additions and 24 deletions

View File

@ -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)

View File

@ -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<Badge>()
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [transferrable, setTransferrable] = useState<boolean>(false)
const [createdBadgeId, setCreatedBadgeId] = useState<string | undefined>(undefined)
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
const [resolvedOwnerAddress, setResolvedOwnerAddress] = 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 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 (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Execute Badge Hub Contract" />
@ -380,6 +539,7 @@ const BadgeHubExecutePage: NextPage = () => {
<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>}