diff --git a/.env.example b/.env.example index 54d70c4..9b264f6 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.7.9 +APP_VERSION=0.7.10 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=2595 @@ -39,6 +39,7 @@ NEXT_PUBLIC_OPEN_EDITION_IBC_FRNZ_FACTORY_ADDRESS="stars1vzffawsjhvspstu5lvtzz2x NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS="stars1tc09vlgdg8rqyapcxwm9qdq8naj4gym9px4ntue9cs0kse5rvess0nee3a" NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr" +NEXT_PUBLIC_ROYALTY_REGISTRY_ADDRESS="stars1crgx0f70fzksa57hq87wtl8f04h0qyk5la0hk0fu8dyhl67ju80qaxzr5z" NEXT_PUBLIC_WHITELIST_CODE_ID=2602 NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=2603 NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336 diff --git a/components/LinkTabs.data.ts b/components/LinkTabs.data.ts index 381cb93..cfc268a 100644 --- a/components/LinkTabs.data.ts +++ b/components/LinkTabs.data.ts @@ -145,3 +145,16 @@ export const splitsLinkTabs: LinkTabProps[] = [ href: '/contracts/splits/migrate', }, ] + +export const royaltyRegistryLinkTabs: LinkTabProps[] = [ + { + title: 'Query', + description: `Dispatch queries for your Royalty Registry contract`, + href: '/contracts/royaltyRegistry/query', + }, + { + title: 'Execute', + description: `Execute Royalty Registry contract actions`, + href: '/contracts/royaltyRegistry/execute', + }, +] diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index e240af3..f5e228e 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -233,6 +233,15 @@ export const Sidebar = () => { > Splits Contract +
  • + Royalty Registry +
  • diff --git a/components/contracts/royaltyRegistry/ExecuteCombobox.hooks.ts b/components/contracts/royaltyRegistry/ExecuteCombobox.hooks.ts new file mode 100644 index 0000000..2bd614a --- /dev/null +++ b/components/contracts/royaltyRegistry/ExecuteCombobox.hooks.ts @@ -0,0 +1,7 @@ +import type { ExecuteListItem } from 'contracts/royaltyRegistry/messages/execute' +import { useState } from 'react' + +export const useExecuteComboboxState = () => { + const [value, setValue] = useState(null) + return { value, onChange: (item: ExecuteListItem) => setValue(item) } +} diff --git a/components/contracts/royaltyRegistry/ExecuteCombobox.tsx b/components/contracts/royaltyRegistry/ExecuteCombobox.tsx new file mode 100644 index 0000000..e28a2d6 --- /dev/null +++ b/components/contracts/royaltyRegistry/ExecuteCombobox.tsx @@ -0,0 +1,92 @@ +import { Combobox, Transition } from '@headlessui/react' +import clsx from 'clsx' +import { FormControl } from 'components/FormControl' +import type { ExecuteListItem } from 'contracts/royaltyRegistry/messages/execute' +import { EXECUTE_LIST } from 'contracts/royaltyRegistry/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 ( + +
    + val?.name ?? ''} + id="message-type" + onChange={(event) => setSearch(event.target.value)} + placeholder="Select message type" + /> + + + {({ open }) => + + setSearch('')} as={Fragment}> + + {filtered.length < 1 && ( + + Message type not found. + + )} + {filtered.map((entry) => ( + + clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
    + + {value && ( +
    +
    + +
    + {value.description} +
    + )} +
    + ) +} diff --git a/contexts/contracts.tsx b/contexts/contracts.tsx index b498185..13790ff 100644 --- a/contexts/contracts.tsx +++ b/contexts/contracts.tsx @@ -6,6 +6,8 @@ import type { UseBaseMinterContractProps } from 'contracts/baseMinter' import { useBaseMinterContract } from 'contracts/baseMinter' import { type UseOpenEditionFactoryContractProps, useOpenEditionFactoryContract } from 'contracts/openEditionFactory' import { type UseOpenEditionMinterContractProps, useOpenEditionMinterContract } from 'contracts/openEditionMinter' +import type { UseRoyaltyRegistryContractProps } from 'contracts/royaltyRegistry' +import { useRoyaltyRegistryContract } from 'contracts/royaltyRegistry' import type { UseSG721ContractProps } from 'contracts/sg721' import { useSG721Contract } from 'contracts/sg721' import type { UseVendingFactoryContractProps } from 'contracts/vendingFactory' @@ -36,6 +38,7 @@ export interface ContractsStore extends State { openEditionFactory: UseOpenEditionFactoryContractProps | null badgeHub: UseBadgeHubContractProps | null splits: UseSplitsContractProps | null + royaltyRegistry: UseRoyaltyRegistryContractProps | null } /** @@ -52,6 +55,7 @@ export const defaultValues: ContractsStore = { openEditionFactory: null, badgeHub: null, splits: null, + royaltyRegistry: null, } /** @@ -85,6 +89,7 @@ const ContractsSubscription: VFC = () => { const openEditionFactory = useOpenEditionFactoryContract() const badgeHub = useBadgeHubContract() const splits = useSplitsContract() + const royaltyRegistry = useRoyaltyRegistryContract() useEffect(() => { useContracts.setState({ @@ -98,8 +103,9 @@ const ContractsSubscription: VFC = () => { openEditionFactory, badgeHub, splits, + royaltyRegistry, }) - }, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub, splits]) + }, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub, splits, royaltyRegistry]) return null } diff --git a/contracts/royaltyRegistry/contract.ts b/contracts/royaltyRegistry/contract.ts new file mode 100644 index 0000000..25e21cf --- /dev/null +++ b/contracts/royaltyRegistry/contract.ts @@ -0,0 +1,407 @@ +import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import type { Coin } from '@cosmjs/proto-signing' +import type { logs } from '@cosmjs/stargate' + +export interface InstantiateResponse { + readonly contractAddress: string + readonly transactionHash: string +} + +export interface MigrateResponse { + readonly transactionHash: string + readonly logs: readonly logs.Log[] +} + +export interface RoyaltyRegistryInstance { + readonly contractAddress: string + //Query + config: () => Promise + collectionRoyaltyDefault: (collection: string) => Promise + collectionRoyaltyProtocol: (collection: string, protocol: string) => Promise + // RoyaltyProtocolByCollection: (collection: string, queryOptions: QqueryOptions) => Promise + royaltyPayment: (collection: string, protocol?: string) => Promise + + //Execute + initializeCollectionRoyalty: (collection: string) => Promise + setCollectionRoyaltyDefault: (collection: string, recipient: string, share: number) => Promise + updateCollectionRoyaltyDefault: ( + collection: string, + recipient?: string, + shareDelta?: number, + decrement?: boolean, + ) => Promise + setCollectionRoyaltyProtocol: ( + collection: string, + protocol: string, + recipient: string, + share: number, + ) => Promise + updateCollectionRoyaltyProtocol: ( + collection: string, + protocol?: string, + recipient?: string, + shareDelta?: number, + decrement?: boolean, + ) => Promise +} + +export interface RoyaltyRegistryMessages { + initializeCollectionRoyalty: (collection: string) => InitializeCollectionRoyaltyMessage + setCollectionRoyaltyDefault: ( + collection: string, + recipient: string, + share: number, + ) => SetCollectionRoyaltyDefaultMessage + updateCollectionRoyaltyDefault: ( + collection: string, + recipient?: string, + shareDelta?: number, + decrement?: boolean, + ) => UpdateCollectionRoyaltyDefaultMessage + setCollectionRoyaltyProtocol: ( + collection: string, + protocol: string, + recipient: string, + share: number, + ) => SetCollectionRoyaltyProtocolMessage + updateCollectionRoyaltyProtocol: ( + collection: string, + protocol?: string, + recipient?: string, + shareDelta?: number, + decrement?: boolean, + ) => UpdateCollectionRoyaltyProtocolMessage +} + +export interface InitializeCollectionRoyaltyMessage { + sender: string + contract: string + msg: { + initialize_collection_royalty: { collection: string } + } + funds: Coin[] +} + +export interface SetCollectionRoyaltyDefaultMessage { + sender: string + contract: string + msg: { + set_collection_royalty_default: { collection: string; recipient: string; share: number } + } + funds: Coin[] +} + +export interface UpdateCollectionRoyaltyDefaultMessage { + sender: string + contract: string + msg: { + update_collection_royalty_default: { + collection: string + recipient?: string + share_delta?: number + decrement?: boolean + } + } + funds: Coin[] +} + +export interface SetCollectionRoyaltyProtocolMessage { + sender: string + contract: string + msg: { + set_collection_royalty_protocol: { + collection: string + protocol: string + recipient: string + share: number + } + } + funds: Coin[] +} + +export interface UpdateCollectionRoyaltyProtocolMessage { + sender: string + contract: string + msg: { + update_collection_royalty_protocol: { + collection: string + protocol?: string + recipient?: string + share_delta?: number + decrement?: boolean + } + } + funds: Coin[] +} + +export interface RoyaltyRegistryContract { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ) => Promise + + use: (contractAddress: string) => RoyaltyRegistryInstance + + migrate: ( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: Record, + ) => Promise + + messages: (contractAddress: string) => RoyaltyRegistryMessages +} + +export const RoyaltyRegistry = (client: SigningCosmWasmClient, txSigner: string): RoyaltyRegistryContract => { + const use = (contractAddress: string): RoyaltyRegistryInstance => { + ///QUERY + const config = async (): Promise => { + return client.queryContractSmart(contractAddress, { + config: {}, + }) + } + + const collectionRoyaltyDefault = async (collection: string): Promise => { + return client.queryContractSmart(contractAddress, { + collection_royalty_default: { collection }, + }) + } + + const collectionRoyaltyProtocol = async (collection: string, protocol: string): Promise => { + return client.queryContractSmart(contractAddress, { + collection_royalty_protocol: { collection, protocol }, + }) + } + + const royaltyPayment = async (collection: string, protocol?: string): Promise => { + return client.queryContractSmart(contractAddress, { + royalty_payment: { collection, protocol }, + }) + } + + /// EXECUTE + const initializeCollectionRoyalty = async (collection: string): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + initialize_collection_royalty: { collection }, + }, + 'auto', + ) + return res.transactionHash + } + + const setCollectionRoyaltyDefault = async ( + collection: string, + recipient: string, + share: number, + ): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + set_collection_royalty_default: { collection, recipient, share: (share / 100).toString() }, + }, + 'auto', + ) + return res.transactionHash + } + + const updateCollectionRoyaltyDefault = async ( + collection: string, + recipient?: string, + shareDelta?: number, + decrement?: boolean, + ): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + update_collection_royalty_default: { + collection, + recipient, + share_delta: shareDelta ? (shareDelta / 100).toString() : undefined, + decrement, + }, + }, + 'auto', + ) + return res.transactionHash + } + + const setCollectionRoyaltyProtocol = async ( + collection: string, + protocol: string, + recipient: string, + share: number, + ): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + set_collection_royalty_protocol: { collection, protocol, recipient, share: (share / 100).toString() }, + }, + 'auto', + ) + return res.transactionHash + } + + const updateCollectionRoyaltyProtocol = async ( + collection: string, + protocol?: string, + recipient?: string, + shareDelta?: number, + decrement?: boolean, + ): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + update_collection_royalty_protocol: { + collection, + protocol, + recipient, + share_delta: shareDelta ? (shareDelta / 100).toString() : undefined, + decrement, + }, + }, + 'auto', + ) + return res.transactionHash + } + + return { + contractAddress, + config, + collectionRoyaltyDefault, + collectionRoyaltyProtocol, + royaltyPayment, + initializeCollectionRoyalty, + setCollectionRoyaltyDefault, + updateCollectionRoyaltyDefault, + setCollectionRoyaltyProtocol, + updateCollectionRoyaltyProtocol, + } + } + + const instantiate = async ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ): Promise => { + const result = await client.instantiate(txSigner, codeId, initMsg, label, 'auto', { + admin, + }) + + return { + contractAddress: result.contractAddress, + transactionHash: result.transactionHash, + } + } + + const migrate = async ( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: Record, + ): Promise => { + const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto') + return { + transactionHash: result.transactionHash, + logs: result.logs, + } + } + + const messages = (contractAddress: string) => { + const initializeCollectionRoyalty = (collection: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + initialize_collection_royalty: { collection }, + }, + funds: [], + } + } + + const setCollectionRoyaltyDefault = (collection: string, recipient: string, share: number) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + set_collection_royalty_default: { collection, recipient, share: share / 100 }, + }, + funds: [], + } + } + + const updateCollectionRoyaltyDefault = ( + collection: string, + recipient?: string, + shareDelta?: number, + decrement?: boolean, + ) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_collection_royalty_default: { + collection, + recipient, + share_delta: shareDelta ? shareDelta / 100 : undefined, + decrement, + }, + }, + funds: [], + } + } + + const setCollectionRoyaltyProtocol = (collection: string, protocol: string, recipient: string, share: number) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + set_collection_royalty_protocol: { collection, protocol, recipient, share: share / 100 }, + }, + funds: [], + } + } + + const updateCollectionRoyaltyProtocol = ( + collection: string, + protocol?: string, + recipient?: string, + shareDelta?: number, + decrement?: boolean, + ) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_collection_royalty_protocol: { + collection, + protocol, + recipient, + share_delta: shareDelta ? shareDelta / 100 : undefined, + decrement, + }, + }, + funds: [], + } + } + + return { + initializeCollectionRoyalty, + setCollectionRoyaltyDefault, + updateCollectionRoyaltyDefault, + setCollectionRoyaltyProtocol, + updateCollectionRoyaltyProtocol, + } + } + + return { use, instantiate, migrate, messages } +} diff --git a/contracts/royaltyRegistry/index.ts b/contracts/royaltyRegistry/index.ts new file mode 100644 index 0000000..6dc6461 --- /dev/null +++ b/contracts/royaltyRegistry/index.ts @@ -0,0 +1,2 @@ +export * from './contract' +export * from './useContract' diff --git a/contracts/royaltyRegistry/messages/execute.ts b/contracts/royaltyRegistry/messages/execute.ts new file mode 100644 index 0000000..4d37d3b --- /dev/null +++ b/contracts/royaltyRegistry/messages/execute.ts @@ -0,0 +1,144 @@ +import type { RoyaltyRegistryInstance } from '../index' +import { useRoyaltyRegistryContract } from '../index' + +export type ExecuteType = typeof EXECUTE_TYPES[number] + +export const EXECUTE_TYPES = [ + 'initialize_collection_royalty', + 'set_collection_royalty_default', + 'update_collection_royalty_default', + 'set_collection_royalty_protocol', + 'update_collection_royalty_protocol', +] as const + +export interface ExecuteListItem { + id: ExecuteType + name: string + description?: string +} + +export const EXECUTE_LIST: ExecuteListItem[] = [ + { + id: 'initialize_collection_royalty', + name: 'Initialize Collection Royalty', + description: 'Initialize collection royalty', + }, + { + id: 'set_collection_royalty_default', + name: 'Set Collection Royalty Default', + description: 'Set collection royalty default', + }, + { + id: 'update_collection_royalty_default', + name: 'Update Collection Royalty Default', + description: 'Update collection royalty default', + }, + { + id: 'set_collection_royalty_protocol', + name: 'Set Collection Royalty Protocol', + description: 'Set collection royalty protocol', + }, + { + id: 'update_collection_royalty_protocol', + name: 'Update Collection Royalty Protocol', + description: 'Update collection royalty protocol', + }, +] + +export interface DispatchExecuteProps { + type: ExecuteType + [k: string]: unknown +} + +type Select = T +/** @see {@link RoyaltyRegistryInstance} */ +export interface DispatchExecuteArgs { + contract: string + collection: string + protocol: string + recipient: string + share: number + shareDelta: number + decrement: boolean + messages?: RoyaltyRegistryInstance + type: string | undefined +} + +export const dispatchExecute = async (args: DispatchExecuteArgs) => { + const { messages } = args + if (!messages) { + throw new Error('Cannot dispatch execute, messages are not defined') + } + switch (args.type) { + case 'initialize_collection_royalty': { + return messages.initializeCollectionRoyalty(args.collection) + } + case 'set_collection_royalty_default': { + return messages.setCollectionRoyaltyDefault(args.collection, args.recipient, args.share) + } + case 'update_collection_royalty_default': { + return messages.updateCollectionRoyaltyDefault(args.collection, args.recipient, args.shareDelta, args.decrement) + } + case 'set_collection_royalty_protocol': { + return messages.setCollectionRoyaltyProtocol(args.collection, args.protocol, args.recipient, args.share) + } + case 'update_collection_royalty_protocol': { + return messages.updateCollectionRoyaltyProtocol( + args.collection, + args.protocol, + args.recipient, + args.shareDelta, + args.decrement, + ) + } + default: { + throw new Error('Unknown execution type') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages } = useRoyaltyRegistryContract() + const { contract } = args + switch (args.type) { + case 'initialize_collection_royalty': { + return messages(contract)?.initializeCollectionRoyalty(args.collection) + } + case 'set_collection_royalty_default': { + return messages(contract)?.setCollectionRoyaltyDefault(args.collection, args.recipient, args.share) + } + case 'update_collection_royalty_default': { + return messages(contract)?.updateCollectionRoyaltyDefault( + args.collection, + args.recipient, + args.shareDelta, + args.decrement, + ) + } + case 'set_collection_royalty_protocol': { + return messages(contract)?.setCollectionRoyaltyProtocol( + args.collection, + args.protocol, + args.recipient, + args.share, + ) + } + case 'update_collection_royalty_protocol': { + return messages(contract)?.updateCollectionRoyaltyProtocol( + args.collection, + args.protocol, + args.recipient, + args.shareDelta, + args.decrement, + ) + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} diff --git a/contracts/royaltyRegistry/messages/query.ts b/contracts/royaltyRegistry/messages/query.ts new file mode 100644 index 0000000..07d6909 --- /dev/null +++ b/contracts/royaltyRegistry/messages/query.ts @@ -0,0 +1,64 @@ +import type { RoyaltyRegistryInstance } from '../contract' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = [ + 'config', + 'collection_royalty_default', + 'collection_royalty_protocol', + 'royalty_payment', +] as const +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const QUERY_LIST: QueryListItem[] = [ + { id: 'config', name: 'Query Config', description: 'View the contract config' }, + { + id: 'collection_royalty_default', + name: 'Query Collection Royalty Details', + description: 'View the collection royalty details', + }, + { + id: 'collection_royalty_protocol', + name: 'Query Collection Royalty Protocol', + description: 'View the collection royalty protocol', + }, + { + id: 'royalty_payment', + name: 'Query Royalty Payment', + description: 'View the royalty payment', + }, +] +/* + //Query + config: () => Promise + collectionRoyaltyDefault: (collection: string) => Promise + collectionRoyaltyProtocol: (collection: string, protocol: string) => Promise + // RoyaltyProtocolByCollection: (collection: string, queryOptions: QqueryOptions) => Promise + royaltyPayment: (collection: string, protocol?: string) => Promise + */ + +export interface DispatchQueryProps { + messages: RoyaltyRegistryInstance | undefined + type: QueryType + collection: string + protocol: string +} + +export const dispatchQuery = (props: DispatchQueryProps) => { + const { messages, type, collection, protocol } = props + switch (type) { + case 'config': + return messages?.config() + case 'collection_royalty_default': + return messages?.collectionRoyaltyDefault(collection) + case 'collection_royalty_protocol': + return messages?.collectionRoyaltyProtocol(collection, protocol) + default: { + throw new Error('unknown query type') + } + } +} diff --git a/contracts/royaltyRegistry/useContract.ts b/contracts/royaltyRegistry/useContract.ts new file mode 100644 index 0000000..7d73394 --- /dev/null +++ b/contracts/royaltyRegistry/useContract.ts @@ -0,0 +1,99 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ + +import { useWallet } from 'contexts/wallet' +import { useCallback, useEffect, useState } from 'react' + +import type { + InstantiateResponse, + MigrateResponse, + RoyaltyRegistryContract, + RoyaltyRegistryInstance, + RoyaltyRegistryMessages, +} from './contract' +import { RoyaltyRegistry as initContract } from './contract' + +export interface UseRoyaltyRegistryContractProps { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ) => Promise + + migrate: (contractAddress: string, codeId: number, migrateMsg: Record) => Promise + + use: (customAddress?: string) => RoyaltyRegistryInstance | undefined + + updateContractAddress: (contractAddress: string) => void + + messages: (contractAddress: string) => RoyaltyRegistryMessages | undefined +} + +export function useRoyaltyRegistryContract(): UseRoyaltyRegistryContractProps { + const wallet = useWallet() + + const [address, setAddress] = useState('') + const [royaltyRegistry, setRoyaltyRegistry] = useState() + + useEffect(() => { + setAddress(localStorage.getItem('contract_address') || '') + }, []) + + useEffect(() => { + const royaltyRegistryContract = initContract(wallet.getClient(), wallet.address) + setRoyaltyRegistry(royaltyRegistryContract) + }, [wallet]) + + const updateContractAddress = (contractAddress: string) => { + setAddress(contractAddress) + } + + const instantiate = useCallback( + (codeId: number, initMsg: Record, label: string, admin?: string): Promise => { + return new Promise((resolve, reject) => { + if (!royaltyRegistry) { + reject(new Error('Contract is not initialized.')) + return + } + royaltyRegistry.instantiate(codeId, initMsg, label, admin).then(resolve).catch(reject) + }) + }, + [royaltyRegistry], + ) + + const migrate = useCallback( + (contractAddress: string, codeId: number, migrateMsg: Record): Promise => { + return new Promise((resolve, reject) => { + if (!royaltyRegistry) { + reject(new Error('Contract is not initialized.')) + return + } + console.log(wallet.address, contractAddress, codeId) + royaltyRegistry.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject) + }) + }, + [royaltyRegistry, wallet], + ) + + const use = useCallback( + (customAddress = ''): RoyaltyRegistryInstance | undefined => { + return royaltyRegistry?.use(address || customAddress) + }, + [royaltyRegistry, address], + ) + + const messages = useCallback( + (customAddress = ''): RoyaltyRegistryMessages | undefined => { + return royaltyRegistry?.messages(address || customAddress) + }, + [royaltyRegistry, address], + ) + + return { + instantiate, + migrate, + use, + updateContractAddress, + messages, + } +} diff --git a/env.d.ts b/env.d.ts index 3d4984a..ac2bfb4 100644 --- a/env.d.ts +++ b/env.d.ts @@ -46,6 +46,7 @@ declare namespace NodeJS { readonly NEXT_PUBLIC_BASE_FACTORY_ADDRESS: string readonly NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS: string readonly NEXT_PUBLIC_SG721_NAME_ADDRESS: string + readonly NEXT_PUBLIC_ROYALTY_REGISTRY_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 diff --git a/package.json b/package.json index 85365c0..0253cb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.7.9", + "version": "0.7.10", "workspaces": [ "packages/*" ], diff --git a/pages/contracts/royaltyRegistry/execute.tsx b/pages/contracts/royaltyRegistry/execute.tsx new file mode 100644 index 0000000..7c316d1 --- /dev/null +++ b/pages/contracts/royaltyRegistry/execute.tsx @@ -0,0 +1,211 @@ +import clsx from 'clsx' +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { ExecuteCombobox } from 'components/contracts/royaltyRegistry/ExecuteCombobox' +import { useExecuteComboboxState } from 'components/contracts/royaltyRegistry/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 { royaltyRegistryLinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { DispatchExecuteArgs } from 'contracts/royaltyRegistry/messages/execute' +import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/royaltyRegistry/messages/execute' +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' +import type { FormEvent } from 'react' +import { useEffect, useMemo, useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaArrowRight } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { ROYALTY_REGISTRY_ADDRESS } from 'utils/constants' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const RoyaltyRegistryExecutePage: NextPage = () => { + const { royaltyRegistry: contract } = useContracts() + const wallet = useWallet() + + const [lastTx, setLastTx] = useState('') + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Royalty Registry Address', + subtitle: 'Address of the Royalty Registry contract', + defaultValue: ROYALTY_REGISTRY_ADDRESS, + }) + const contractAddress = contractState.value + + const collectionAddressState = useInputState({ + id: 'collection-address', + name: 'collection-address', + title: 'Collection Address', + subtitle: 'Address of the collection', + }) + + const protocolAddressState = useInputState({ + id: 'protocol-address', + name: 'protocol-address', + title: 'Protocol Address', + subtitle: 'Address of the protocol', + }) + + const recipientAddressState = useInputState({ + id: 'recipient-address', + name: 'recipient-address', + title: 'Recipient Address', + subtitle: 'Address of the recipient', + }) + + const shareState = useNumberInputState({ + id: 'share', + name: 'share', + title: 'Share', + subtitle: 'Share percentage', + placeholder: '4%', + }) + + const shareDeltaState = useNumberInputState({ + id: 'share-delta', + name: 'share-delta', + title: 'Share Delta', + subtitle: 'The change of share percentage', + placeholder: '1%', + }) + + const [decrement, setDecrement] = useState(false) + + const showRecipientAddress = isEitherType(type, [ + 'set_collection_royalty_default', + 'set_collection_royalty_protocol', + 'update_collection_royalty_default', + 'update_collection_royalty_protocol', + ]) + + const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value]) + const payload: DispatchExecuteArgs = { + contract: contractState.value, + messages, + type, + collection: collectionAddressState.value, + protocol: protocolAddressState.value, + recipient: recipientAddressState.value, + share: shareState.value, + shareDelta: shareDeltaState.value, + decrement, + } + 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.') + } + 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 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 ( +
    + + + + +
    +
    + + + + + + + + + + + + + + +
    +
    + +
    + Decrement +
    +
    +
    + +
    +
    + + + + +
    + + + +
    +
    +
    + ) +} + +export default withMetadata(RoyaltyRegistryExecutePage, { center: false }) diff --git a/pages/contracts/royaltyRegistry/index.tsx b/pages/contracts/royaltyRegistry/index.tsx new file mode 100644 index 0000000..6d40435 --- /dev/null +++ b/pages/contracts/royaltyRegistry/index.tsx @@ -0,0 +1 @@ +export { default } from './execute' diff --git a/pages/contracts/royaltyRegistry/query.tsx b/pages/contracts/royaltyRegistry/query.tsx new file mode 100644 index 0000000..b59a009 --- /dev/null +++ b/pages/contracts/royaltyRegistry/query.tsx @@ -0,0 +1,146 @@ +import clsx from 'clsx' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { FormControl } from 'components/FormControl' +import { AddressInput } from 'components/forms/FormInput' +import { useInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { royaltyRegistryLinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { QueryType } from 'contracts/royaltyRegistry/messages/query' +import { dispatchQuery, QUERY_LIST } from 'contracts/royaltyRegistry/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 { ROYALTY_REGISTRY_ADDRESS } from 'utils/constants' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' +import { resolveAddress } from 'utils/resolveAddress' + +const RoyaltyRegistryQueryPage: NextPage = () => { + const { royaltyRegistry: contract } = useContracts() + const wallet = useWallet() + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Royalty Registry Address', + subtitle: 'Address of the Royalty Registry contract', + defaultValue: ROYALTY_REGISTRY_ADDRESS, + }) + const contractAddress = contractState.value + + const collectionAddressState = useInputState({ + id: 'collection-address', + name: 'collection-address', + title: 'Collection Address', + subtitle: 'Address of the collection', + }) + + const protocolAddressState = useInputState({ + id: 'protocol-address', + name: 'protocol-address', + title: 'Protocol Address', + subtitle: 'Address of the protocol', + }) + + const collectionAddress = collectionAddressState.value + const protocolAddress = protocolAddressState.value + + const [type, setType] = useState('config') + + const { data: response } = useQuery( + [contractAddress, type, contract, wallet, collectionAddress, protocolAddress] as const, + async ({ queryKey }) => { + const [_contractAddress, _type, _contract, _wallet, _collectionAddress, _protocolAddress] = queryKey + const messages = contract?.use(contractAddress) + const res = await resolveAddress(_collectionAddress, wallet).then(async (resolvedAddress) => { + const result = await dispatchQuery({ + messages, + type, + collection: resolvedAddress, + protocol: _protocolAddress, + }) + return result + }) + return res + }, + { + 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 ( +
    + + + + +
    +
    + + + + + + + + + + +
    + +
    +
    + ) +} + +export default withMetadata(RoyaltyRegistryQueryPage, { center: false }) diff --git a/utils/constants.ts b/utils/constants.ts index 4911df1..a115208 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -40,6 +40,7 @@ export const OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_IBC_FRNZ_FACTORY_ADDRESS export const OPEN_EDITION_MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_OPEN_EDITION_MINTER_CODE_ID, 10) export const SG721_NAME_ADDRESS = process.env.NEXT_PUBLIC_SG721_NAME_ADDRESS +export const ROYALTY_REGISTRY_ADDRESS = process.env.NEXT_PUBLIC_ROYALTY_REGISTRY_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