diff --git a/components/badges/actions/Action.tsx b/components/badges/actions/Action.tsx new file mode 100644 index 0000000..1848f4c --- /dev/null +++ b/components/badges/actions/Action.tsx @@ -0,0 +1,572 @@ +// import { AirdropUpload } from 'components/AirdropUpload' +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 { Conditional } from 'components/Conditional' +import { FormControl } from 'components/FormControl' +// import { FormGroup } from 'components/FormGroup' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { MetadataAttributes } from 'components/forms/MetadataAttributes' +import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' +import { InputDateTime } from 'components/InputDateTime' +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 { toPng } from 'html-to-image' +import { QRCodeCanvas } from 'qrcode.react' +import type { FormEvent } from 'react' +import { useEffect, 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 { NETWORK } from 'utils/constants' +import type { AirdropAllocation } from 'utils/isValidAccountsFile' +import { resolveAddress } from 'utils/resolveAddress' + +import { 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(undefined) + const [airdropAllocationArray, setAirdropAllocationArray] = useState([]) + const [airdropArray, setAirdropArray] = useState([]) + const [badge, setBadge] = useState() + const [transferrable, setTransferrable] = useState(undefined) + const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') + + const [createdBadgeId, setCreatedBadgeId] = useState(undefined) + const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) + const qrRef = useRef(null) + + 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', + }) + + const pubkeyState = useInputState({ + id: 'pubkey', + name: 'pubkey', + title: 'Pubkey', + subtitle: 'The public key for the badge', + }) + + const signatureState = useInputState({ + id: 'signature', + name: 'signature', + title: 'Signature', + subtitle: 'The signature for the badge', + }) + + 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 = type === 'edit_badge' + const showNFTField = type === 'set_nft' + + const payload: DispatchExecuteArgs = { + 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, + owner: resolvedOwnerAddress, + pubkey: pubkeyState.value, + signature: signatureState.value, + keys: [], + limit: limitState.value, + owners: [], + 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, + }) + }, [ + 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, + ]) + + useEffect(() => { + const addresses: string[] = [] + airdropAllocationArray.forEach((allocation) => { + for (let i = 0; i < Number(allocation.amount); i++) { + addresses.push(allocation.address) + } + }) + //shuffle the addresses array + for (let i = addresses.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[addresses[i], addresses[j]] = [addresses[j], addresses[i]] + } + setAirdropArray(addresses) + }, [airdropAllocationArray]) + + const { isLoading, mutate } = useMutation( + async (event: FormEvent) => { + event.preventDefault() + if (!type) { + throw new Error('Please select an action.') + } + if (badgeHubContractAddress === '') { + throw new Error('Please enter the Badge Hub contract addresses.') + } + + // if (wallet.client && type === 'update_mint_price') { + // const contractConfig = wallet.client.queryContractSmart(minterContractAddress, { + // config: {}, + // }) + // await toast + // .promise( + // wallet.client.queryContractSmart(minterContractAddress, { + // mint_price: {}, + // }), + // { + // error: `Querying mint price failed!`, + // loading: 'Querying current mint price...', + // success: (price) => { + // console.log('Current mint price: ', price) + // return `Current mint price is ${Number(price.public_price.amount) / 1000000} STARS` + // }, + // }, + // ) + // .then(async (price) => { + // if (Number(price.public_price.amount) / 1000000 <= priceState.value) { + // await contractConfig + // .then((config) => { + // console.log(config.start_time, Date.now() * 1000000) + // if (Number(config.start_time) < Date.now() * 1000000) { + // throw new Error( + // `Minting has already started on ${new Date( + // Number(config.start_time) / 1000000, + // ).toLocaleString()}. Updated mint price cannot be higher than the current price of ${ + // Number(price.public_price.amount) / 1000000 + // } STARS`, + // ) + // } + // }) + // .catch((error) => { + // throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7)) + // }) + // } + // }) + // } + + 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 airdropFileOnChange = (data: AirdropAllocation[]) => { + setAirdropAllocationArray(data) + } + + 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) + } + + 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') + } + + return ( +
+
+
+ + {showBadgeField && createdBadgeId && createdBadgeKey && ( +
+
+ +
+ {/*
*/} +
+ + +
+
+ )} + + {showBadgeField && } + {showBadgeField && } + {showBadgeField && } + {showMetadataField && ( +
+ Metadata + + + + + +
+ +
+ + + +
+ )} + {showNFTField && } + + {/* {showAirdropFileField && ( + + + + )} */} + + + setTimestamp(date)} value={timestamp} /> + + + + + + setTimestamp(date)} value={timestamp} /> + + + {showBadgeField && } + {showBadgeField && ( +
+ +
+ )} +
+
+
+ + + + +
+ + + +
+
+ + ) +}