From aa42f8763a6b6d7a5a67e3eafca2293c61117d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20Nak=C4=B1=C5=9F=C3=A7=C4=B1?= Date: Tue, 19 Jul 2022 10:53:03 +0300 Subject: [PATCH] Implement contract UIs (#2) * Add instantiate page for minter * Add query page to minter contract * Add execute page for minter contract * Add contracts index page * Refaactor sg721 helper files * Add instantiate page * Add query page for sg721 * Add execute page for sg721 contract * Copy page templates for whitelist contracts * Add instantitate for whitelist contract * Add query page to whitelist contract * Add execute page for whitelist contract --- .vscode/settings.json | 2 +- components/LinkTabs.data.ts | 36 ++ components/Sidebar.tsx | 19 +- .../contracts/minter/ExecuteCombobox.hooks.ts | 7 + .../contracts/minter/ExecuteCombobox.tsx | 92 +++ .../contracts/sg721/ExecuteCombobox.hooks.ts | 7 + .../contracts/sg721/ExecuteCombobox.tsx | 92 +++ .../whitelist/ExecuteCombobox.hooks.ts | 7 + .../contracts/whitelist/ExecuteCombobox.tsx | 92 +++ components/forms/FormTextArea.tsx | 5 +- config/network.ts | 2 +- contexts/contracts.tsx | 38 +- contracts/minter/contract.ts | 314 ++++++++-- contracts/minter/messages/execute.ts | 158 +++++ contracts/minter/messages/query.ts | 53 ++ contracts/minter/useContract.ts | 43 +- contracts/sg721/contract.ts | 549 +++++++++++------- contracts/sg721/messages/execute.ts | 162 ++++++ contracts/sg721/messages/query.ts | 95 +++ contracts/sg721/useContract.ts | 38 +- contracts/whitelist/contract.ts | 249 +++++--- contracts/whitelist/messages/execute.ts | 136 +++++ contracts/whitelist/messages/query.ts | 47 ++ contracts/whitelist/useContract.ts | 61 +- env.d.ts | 8 +- pages/contracts/index.tsx | 41 ++ pages/contracts/minter/execute.tsx | 166 ++++++ pages/contracts/minter/index.tsx | 1 + pages/contracts/minter/instantiate.tsx | 279 +++++++++ pages/contracts/minter/query.tsx | 120 ++++ pages/contracts/sg721/execute.tsx | 156 +++++ pages/contracts/sg721/index.tsx | 1 + pages/contracts/sg721/instantiate.tsx | 186 ++++++ pages/contracts/sg721/query.tsx | 135 +++++ pages/contracts/whitelist/execute.tsx | 162 ++++++ pages/contracts/whitelist/index.tsx | 1 + pages/contracts/whitelist/instantiate.tsx | 153 +++++ pages/contracts/whitelist/query.tsx | 122 ++++ utils/constants.ts | 10 +- utils/links.ts | 7 +- 40 files changed, 3389 insertions(+), 463 deletions(-) create mode 100644 components/contracts/minter/ExecuteCombobox.hooks.ts create mode 100644 components/contracts/minter/ExecuteCombobox.tsx create mode 100644 components/contracts/sg721/ExecuteCombobox.hooks.ts create mode 100644 components/contracts/sg721/ExecuteCombobox.tsx create mode 100644 components/contracts/whitelist/ExecuteCombobox.hooks.ts create mode 100644 components/contracts/whitelist/ExecuteCombobox.tsx create mode 100644 contracts/minter/messages/execute.ts create mode 100644 contracts/minter/messages/query.ts create mode 100644 contracts/sg721/messages/execute.ts create mode 100644 contracts/sg721/messages/query.ts create mode 100644 contracts/whitelist/messages/execute.ts create mode 100644 contracts/whitelist/messages/query.ts create mode 100644 pages/contracts/index.tsx create mode 100644 pages/contracts/minter/execute.tsx create mode 100644 pages/contracts/minter/index.tsx create mode 100644 pages/contracts/minter/instantiate.tsx create mode 100644 pages/contracts/minter/query.tsx create mode 100644 pages/contracts/sg721/execute.tsx create mode 100644 pages/contracts/sg721/index.tsx create mode 100644 pages/contracts/sg721/instantiate.tsx create mode 100644 pages/contracts/sg721/query.tsx create mode 100644 pages/contracts/whitelist/execute.tsx create mode 100644 pages/contracts/whitelist/index.tsx create mode 100644 pages/contracts/whitelist/instantiate.tsx create mode 100644 pages/contracts/whitelist/query.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 7eb5fab..c22b012 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,7 @@ "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "[typescriptreact]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "css.validate": false, "editor.formatOnSave": true, diff --git a/components/LinkTabs.data.ts b/components/LinkTabs.data.ts index 5017c4a..3a38fe3 100644 --- a/components/LinkTabs.data.ts +++ b/components/LinkTabs.data.ts @@ -17,3 +17,39 @@ export const sg721LinkTabs: LinkTabProps[] = [ href: '/contracts/sg721/execute', }, ] + +export const minterLinkTabs: LinkTabProps[] = [ + { + title: 'Instantiate', + description: `Initialize a new Minter contract`, + href: '/contracts/minter/instantiate', + }, + { + title: 'Query', + description: `Dispatch queries with your Minter contract`, + href: '/contracts/minter/query', + }, + { + title: 'Execute', + description: `Execute Minter contract actions`, + href: '/contracts/minter/execute', + }, +] + +export const whitelistLinkTabs: LinkTabProps[] = [ + { + title: 'Instantiate', + description: `Initialize a new Whitelist contract`, + href: '/contracts/whitelist/instantiate', + }, + { + title: 'Query', + description: `Dispatch queries with your Whitelist contract`, + href: '/contracts/whitelist/query', + }, + { + title: 'Execute', + description: `Execute Whitelist contract actions`, + href: '/contracts/whitelist/execute', + }, +] diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 0c5e1a6..1fc0244 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx' import { Anchor } from 'components/Anchor' import { useWallet } from 'contexts/wallet' import { useRouter } from 'next/router' -import BrandText from 'public/brand/brand-text.svg' +// import BrandText from 'public/brand/brand-text.svg' import { footerLinks, links, socialsLinks } from 'utils/links' import { SidebarLayout } from './SidebarLayout' @@ -10,6 +10,7 @@ import { WalletLoader } from './WalletLoader' const routes = [ { text: 'Create Collection', href: `/collection/` }, + { text: 'Contract Dashboards', href: `/contracts/` }, ] export const Sidebar = () => { @@ -20,14 +21,14 @@ export const Sidebar = () => { {/* Stargaze brand as home button */} [e.preventDefault(), router.push('/brand')]}> -
- Home{/* */} -
+
+ Home{/* */} +
{/* wallet button */} diff --git a/components/contracts/minter/ExecuteCombobox.hooks.ts b/components/contracts/minter/ExecuteCombobox.hooks.ts new file mode 100644 index 0000000..19830ab --- /dev/null +++ b/components/contracts/minter/ExecuteCombobox.hooks.ts @@ -0,0 +1,7 @@ +import type { ExecuteListItem } from 'contracts/minter/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/minter/ExecuteCombobox.tsx b/components/contracts/minter/ExecuteCombobox.tsx new file mode 100644 index 0000000..e327c6d --- /dev/null +++ b/components/contracts/minter/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/minter/messages/execute' +import { EXECUTE_LIST } from 'contracts/minter/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-plumbus-70': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
+ + {value && ( +
+
+ +
+ {value.description} +
+ )} +
+ ) +} diff --git a/components/contracts/sg721/ExecuteCombobox.hooks.ts b/components/contracts/sg721/ExecuteCombobox.hooks.ts new file mode 100644 index 0000000..88ecab2 --- /dev/null +++ b/components/contracts/sg721/ExecuteCombobox.hooks.ts @@ -0,0 +1,7 @@ +import type { ExecuteListItem } from 'contracts/sg721/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/sg721/ExecuteCombobox.tsx b/components/contracts/sg721/ExecuteCombobox.tsx new file mode 100644 index 0000000..aa67719 --- /dev/null +++ b/components/contracts/sg721/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/sg721/messages/execute' +import { EXECUTE_LIST } from 'contracts/sg721/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-plumbus-70': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
+ + {value && ( +
+
+ +
+ {value.description} +
+ )} +
+ ) +} diff --git a/components/contracts/whitelist/ExecuteCombobox.hooks.ts b/components/contracts/whitelist/ExecuteCombobox.hooks.ts new file mode 100644 index 0000000..9d44daa --- /dev/null +++ b/components/contracts/whitelist/ExecuteCombobox.hooks.ts @@ -0,0 +1,7 @@ +import type { ExecuteListItem } from 'contracts/whitelist/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/whitelist/ExecuteCombobox.tsx b/components/contracts/whitelist/ExecuteCombobox.tsx new file mode 100644 index 0000000..7dce2a9 --- /dev/null +++ b/components/contracts/whitelist/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/whitelist/messages/execute' +import { EXECUTE_LIST } from 'contracts/whitelist/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-plumbus-70': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
+ + {value && ( +
+
+ +
+ {value.description} +
+ )} +
+ ) +} diff --git a/components/forms/FormTextArea.tsx b/components/forms/FormTextArea.tsx index 335ddbd..9690529 100644 --- a/components/forms/FormTextArea.tsx +++ b/components/forms/FormTextArea.tsx @@ -11,6 +11,7 @@ interface BaseProps { name: string title: string subtitle?: string + isRequired?: boolean } type SlicedInputProps = Omit, keyof BaseProps> @@ -19,10 +20,10 @@ export type FormTextAreaProps = BaseProps & SlicedInputProps export const FormTextArea = forwardRef( function FormTextArea(props, ref) { - const { id, name, title, subtitle, ...rest } = props + const { id, name, title, subtitle, isRequired, ...rest } = props return ( - + ) diff --git a/config/network.ts b/config/network.ts index 1c6993b..cc9ef0b 100644 --- a/config/network.ts +++ b/config/network.ts @@ -20,7 +20,7 @@ export const mainnetConfig: AppConfig = { export const testnetConfig: AppConfig = { chainId: 'elgafar-1', - chainName: 'elgafar-1', + chainName: 'elgarfar-1', addressPrefix: 'stars', rpcUrl: 'https://rpc.elgafar-1.stargaze-apis.com/', feeToken: 'ustars', diff --git a/contexts/contracts.tsx b/contexts/contracts.tsx index d1f74b1..4171df4 100644 --- a/contexts/contracts.tsx +++ b/contexts/contracts.tsx @@ -1,13 +1,13 @@ -import { useMinterContract, UseMinterContractProps } from 'contracts/minter' -import { useSG721Contract, UseSG721ContractProps } from 'contracts/sg721' -import { - useWhiteListContract, - useWhiteListContractProps, -} from 'contracts/whitelist' - -import { Fragment, ReactNode, useEffect, VFC } from 'react' -import create, { State } from 'zustand' - +import type { UseMinterContractProps } from 'contracts/minter' +import { useMinterContract } from 'contracts/minter' +import type { UseSG721ContractProps } from 'contracts/sg721' +import { useSG721Contract } from 'contracts/sg721' +import type { UseWhiteListContractProps } from 'contracts/whitelist' +import { useWhiteListContract } from 'contracts/whitelist' +import type { ReactNode, VFC } from 'react' +import { Fragment, useEffect } from 'react' +import type { State } from 'zustand' +import create from 'zustand' /** * Contracts store type definitions @@ -15,7 +15,7 @@ import create, { State } from 'zustand' export interface ContractsStore extends State { sg721: UseSG721ContractProps | null minter: UseMinterContractProps | null - whitelist: useWhiteListContractProps | null + whitelist: UseWhiteListContractProps | null } /** @@ -40,35 +40,25 @@ export const useContracts = create(() => ({ */ export const ContractsProvider = ({ children }: { children: ReactNode }) => { return ( - + <> {children} - + ) } -/** - * Contracts store subscriptions (side effects) - * - * @todo refactor all contract logics to zustand store - */ const ContractsSubscription: VFC = () => { const sg721 = useSG721Contract() const minter = useMinterContract() const whitelist = useWhiteListContract() - useEffect(() => { useContracts.setState({ sg721, minter, whitelist, }) - }, [ - sg721, - minter, - whitelist, - ]) + }, [sg721, minter, whitelist]) return null } diff --git a/contracts/minter/contract.ts b/contracts/minter/contract.ts index 608671f..0261b6b 100644 --- a/contracts/minter/contract.ts +++ b/contracts/minter/contract.ts @@ -1,7 +1,8 @@ -import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' -import { Coin } from '@cosmjs/proto-signing' -import { logs } from '@cosmjs/stargate' -import { Timestamp } from '@stargazezone/types/contracts/minter/shared-types' +import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import type { Coin } from '@cosmjs/proto-signing' +import { coin } from '@cosmjs/proto-signing' +import type { logs } from '@cosmjs/stargate' +import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types' export interface InstantiateResponse { readonly contractAddress: string @@ -9,7 +10,7 @@ export interface InstantiateResponse { readonly logs: readonly logs.Log[] } -export type RoyalityInfo = { +export interface RoyalityInfo { payment_address: string share: string } @@ -25,22 +26,108 @@ export interface MinterInstance { getMintCount: (address: string) => Promise //Execute - mint: (senderAddress: string) => Promise + mint: (senderAddress: string, price: string) => Promise setWhitelist: (senderAddress: string, whitelist: string) => Promise updateStartTime: (senderAddress: string, time: Timestamp) => Promise - updatePerAddressLimit: ( - senderAddress: string, - per_address_limit: number - ) => Promise + updatePerAddressLimit: (senderAddress: string, perAddressLimit: number) => Promise mintTo: (senderAddress: string, recipient: string) => Promise - mintFor: ( - senderAddress: string, - token_id: number, - recipient: string - ) => Promise + mintFor: (senderAddress: string, recipient: string, tokenId: number) => Promise + shuffle: (senderAddress: string) => Promise withdraw: (senderAddress: string) => Promise } +export interface MinterMessages { + mint: (contractAddress: string, price: string) => MintMessage + setWhitelist: (contractAddress: string, whitelist: string) => SetWhitelistMessage + updateStartTime: (contractAddress: string, time: Timestamp) => UpdateStarTimeMessage + updatePerAddressLimit: (contractAddress: string, perAddressLimit: number) => UpdatePerAddressLimitMessage + mintTo: (contractAddress: string, recipient: string) => MintToMessage + mintFor: (contractAddress: string, recipient: string, tokenId: number) => MintForMessage + shuffle: (contractAddress: string) => ShuffleMessage + withdraw: (contractAddress: string) => WithdrawMessage +} + +export interface MintMessage { + sender: string + contract: string + msg: { + mint: Record + } + funds: Coin[] +} + +export interface SetWhitelistMessage { + sender: string + contract: string + msg: { + set_whitelist: { + whitelist: string + } + } + funds: Coin[] +} + +export interface UpdateStarTimeMessage { + sender: string + contract: string + msg: { + update_start_time: string + } + funds: Coin[] +} + +export interface UpdatePerAddressLimitMessage { + sender: string + contract: string + msg: { + update_per_address_limit: { + per_address_limit: number + } + } + funds: Coin[] +} + +export interface MintToMessage { + sender: string + contract: string + msg: { + mint_to: { + recipient: string + } + } + funds: Coin[] +} + +export interface MintForMessage { + sender: string + contract: string + msg: { + mint_for: { + recipient: string + token_id: number + } + } + funds: Coin[] +} + +export interface ShuffleMessage { + sender: string + contract: string + msg: { + shuffle: Record + } + funds: Coin[] +} + +export interface WithdrawMessage { + sender: string + contract: string + msg: { + withdraw: Record + } + funds: Coin[] +} + export interface MinterContract { instantiate: ( senderAddress: string, @@ -48,13 +135,15 @@ export interface MinterContract { initMsg: Record, label: string, admin?: string, - funds?: Coin[] + funds?: Coin[], ) => Promise use: (contractAddress: string) => MinterInstance + + messages: () => MinterMessages } -export const minter = (client: SigningCosmWasmClient): MinterContract => { +export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterContract => { const use = (contractAddress: string): MinterInstance => { //Query const getConfig = async (): Promise => { @@ -93,7 +182,7 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { } //Execute - const mint = async (senderAddress: string): Promise => { + const mint = async (senderAddress: string, price: string): Promise => { const res = await client.execute( senderAddress, contractAddress, @@ -101,16 +190,14 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { mint: {}, }, 'auto', - '' + '', + [coin(price, 'ustars')], ) return res.transactionHash } - const setWhitelist = async ( - senderAddress: string, - whitelist: string - ): Promise => { + const setWhitelist = async (senderAddress: string, whitelist: string): Promise => { const res = await client.execute( senderAddress, contractAddress, @@ -118,16 +205,13 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { set_whitelist: { whitelist }, }, 'auto', - '' + '', ) return res.transactionHash } - const updateStartTime = async ( - senderAddress: string, - time: Timestamp - ): Promise => { + const updateStartTime = async (senderAddress: string, time: Timestamp): Promise => { const res = await client.execute( senderAddress, contractAddress, @@ -135,33 +219,27 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { update_start_time: { time }, }, 'auto', - '' + '', ) return res.transactionHash } - const updatePerAddressLimit = async ( - senderAddress: string, - per_address_limit: number - ): Promise => { + const updatePerAddressLimit = async (senderAddress: string, perAddressLimit: number): Promise => { const res = await client.execute( senderAddress, contractAddress, { - update_per_address_limit: { per_address_limit }, + update_per_address_limit: { per_address_limit: perAddressLimit }, }, 'auto', - '' + '', ) return res.transactionHash } - const mintTo = async ( - senderAddress: string, - recipient: string - ): Promise => { + const mintTo = async (senderAddress: string, recipient: string): Promise => { const res = await client.execute( senderAddress, contractAddress, @@ -169,25 +247,35 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { mint_to: { recipient }, }, 'auto', - '' + '', ) return res.transactionHash } - const mintFor = async ( - senderAddress: string, - token_id: number, - recipient: string - ): Promise => { + const mintFor = async (senderAddress: string, recipient: string, tokenId: number): Promise => { const res = await client.execute( senderAddress, contractAddress, { - mint_for: { token_id, recipient }, + mint_for: { token_id: tokenId, recipient }, }, 'auto', - '' + '', + ) + + return res.transactionHash + } + + const shuffle = async (senderAddress: string): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + shuffle: {}, + }, + 'auto', + '', ) return res.transactionHash @@ -201,7 +289,7 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { withdraw: {}, }, 'auto', - '' + '', ) return res.transactionHash @@ -220,6 +308,7 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { updatePerAddressLimit, mintTo, mintFor, + shuffle, withdraw, } } @@ -229,21 +318,10 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { codeId: number, initMsg: Record, label: string, - admin?: string, - funds?: Coin[] ): Promise => { - console.log(funds) - const result = await client.instantiate( - senderAddress, - codeId, - initMsg, - label, - 'auto', - { - funds, - admin, - } - ) + const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', { + funds: [coin('1000000000', 'ustars')], + }) return { contractAddress: result.contractAddress, @@ -252,5 +330,115 @@ export const minter = (client: SigningCosmWasmClient): MinterContract => { } } - return { use, instantiate } + const messages = () => { + const mint = (contractAddress: string, price: string): MintMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + mint: {}, + }, + funds: [coin(price, 'ustars')], + } + } + + const setWhitelist = (contractAddress: string, whitelist: string): SetWhitelistMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + set_whitelist: { + whitelist, + }, + }, + funds: [], + } + } + + const updateStartTime = (contractAddress: string, startTime: string): UpdateStarTimeMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_start_time: startTime, + }, + funds: [], + } + } + + const updatePerAddressLimit = (contractAddress: string, limit: number): UpdatePerAddressLimitMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_per_address_limit: { + per_address_limit: limit, + }, + }, + funds: [], + } + } + + const mintTo = (contractAddress: string, recipient: string): MintToMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + mint_to: { + recipient, + }, + }, + funds: [], + } + } + + const mintFor = (contractAddress: string, recipient: string, tokenId: number): MintForMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + mint_for: { + recipient, + token_id: tokenId, + }, + }, + funds: [], + } + } + + const shuffle = (contractAddress: string): ShuffleMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + shuffle: {}, + }, + funds: [], + } + } + + const withdraw = (contractAddress: string): WithdrawMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + withdraw: {}, + }, + funds: [], + } + } + + return { + mint, + setWhitelist, + updateStartTime, + updatePerAddressLimit, + mintTo, + mintFor, + shuffle, + withdraw, + } + } + + return { use, instantiate, messages } } diff --git a/contracts/minter/messages/execute.ts b/contracts/minter/messages/execute.ts new file mode 100644 index 0000000..2002918 --- /dev/null +++ b/contracts/minter/messages/execute.ts @@ -0,0 +1,158 @@ +import type { MinterInstance } from '../index' +import { useMinterContract } from '../index' + +export type ExecuteType = typeof EXECUTE_TYPES[number] + +export const EXECUTE_TYPES = [ + 'mint', + 'set_whitelist', + 'update_start_time', + 'update_per_address_limit', + 'mint_to', + 'mint_for', + 'shuffle', + 'withdraw', +] as const + +export interface ExecuteListItem { + id: ExecuteType + name: string + description?: string +} + +export const EXECUTE_LIST: ExecuteListItem[] = [ + { + id: 'mint', + name: 'Mint', + description: `Mint new tokens for a given address`, + }, + { + id: 'set_whitelist', + name: 'Set Whitelist', + description: `Set whitelist contract address`, + }, + { + id: 'update_start_time', + name: 'Update Start Time', + description: `Update start time for minting`, + }, + { + id: 'update_per_address_limit', + name: 'Update Per Address Limit', + description: `Update token per address limit`, + }, + { + id: 'mint_to', + name: 'Mint To', + description: `Mint tokens to a given address`, + }, + { + id: 'mint_for', + name: 'Mint For', + description: `Mint tokens for a given address with a given token ID`, + }, + { + id: 'shuffle', + name: 'Shuffle', + description: `Shuffle the token IDs`, + }, +] + +export interface DispatchExecuteProps { + type: ExecuteType + [k: string]: unknown +} + +type Select = T + +/** @see {@link MinterInstance} */ +export type DispatchExecuteArgs = { + contract: string + messages?: MinterInstance + txSigner: string +} & ( + | { type: undefined } + | { type: Select<'mint'>; price: string } + | { type: Select<'set_whitelist'>; whitelist: string } + | { type: Select<'update_start_time'>; startTime: string } + | { type: Select<'update_per_address_limit'>; limit: number } + | { type: Select<'mint_to'>; recipient: string } + | { type: Select<'mint_for'>; recipient: string; tokenId: number } + | { type: Select<'shuffle'> } + | { type: Select<'withdraw'> } +) + +export const dispatchExecute = async (args: DispatchExecuteArgs) => { + const { messages, txSigner } = args + if (!messages) { + throw new Error('cannot dispatch execute, messages is not defined') + } + switch (args.type) { + case 'mint': { + return messages.mint(txSigner, args.price === '' ? '0' : args.price) + } + case 'set_whitelist': { + return messages.setWhitelist(txSigner, args.whitelist) + } + case 'update_start_time': { + return messages.updateStartTime(txSigner, args.startTime) + } + case 'update_per_address_limit': { + return messages.updatePerAddressLimit(txSigner, args.limit) + } + case 'mint_to': { + return messages.mintTo(txSigner, args.recipient) + } + case 'mint_for': { + return messages.mintFor(txSigner, args.recipient, args.tokenId) + } + case 'shuffle': { + return messages.shuffle(txSigner) + } + case 'withdraw': { + return messages.withdraw(txSigner) + } + default: { + throw new Error('unknown execute type') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages } = useMinterContract() + const { contract } = args + switch (args.type) { + case 'mint': { + return messages()?.mint(contract, args.price === '' ? '0' : args.price) + } + case 'set_whitelist': { + return messages()?.setWhitelist(contract, args.whitelist) + } + case 'update_start_time': { + return messages()?.updateStartTime(contract, args.startTime) + } + case 'update_per_address_limit': { + return messages()?.updatePerAddressLimit(contract, args.limit) + } + case 'mint_to': { + return messages()?.mintTo(contract, args.recipient) + } + case 'mint_for': { + return messages()?.mintFor(contract, args.recipient, args.tokenId) + } + case 'shuffle': { + return messages()?.shuffle(contract) + } + case 'withdraw': { + return messages()?.withdraw(contract) + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} diff --git a/contracts/minter/messages/query.ts b/contracts/minter/messages/query.ts new file mode 100644 index 0000000..496ca5e --- /dev/null +++ b/contracts/minter/messages/query.ts @@ -0,0 +1,53 @@ +import type { MinterInstance } from '../contract' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = ['config', 'mintable_num_tokens', 'start_time', 'mint_price', 'mint_count'] as const + +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const QUERY_LIST: QueryListItem[] = [ + { id: 'config', name: 'Config', description: 'View current config' }, + { id: 'mintable_num_tokens', name: 'Total Mintable Tokens', description: 'View the total amount of mintable tokens' }, + { id: 'start_time', name: 'Start Time', description: 'View the start time for minting' }, + { id: 'mint_price', name: 'Mint Price', description: 'View the mint price' }, + { + id: 'mint_count', + name: 'Total Minted Count', + description: 'View the total amount of minted tokens for an address', + }, +] + +export interface DispatchQueryProps { + address: string + messages: MinterInstance | undefined + type: QueryType +} + +export const dispatchQuery = (props: DispatchQueryProps) => { + const { address, messages, type } = props + switch (type) { + case 'config': { + return messages?.getConfig() + } + case 'mintable_num_tokens': { + return messages?.getMintableNumTokens() + } + case 'start_time': { + return messages?.getStartTime() + } + case 'mint_price': { + return messages?.getMintPrice() + } + case 'mint_count': { + return messages?.getMintCount(address) + } + default: { + throw new Error('unknown query type') + } + } +} diff --git a/contracts/minter/useContract.ts b/contracts/minter/useContract.ts index e02903f..f42945f 100644 --- a/contracts/minter/useContract.ts +++ b/contracts/minter/useContract.ts @@ -1,13 +1,10 @@ -import { Coin } from '@cosmjs/proto-signing' -import { logs } from '@cosmjs/stargate' +import type { Coin } from '@cosmjs/proto-signing' +import type { logs } from '@cosmjs/stargate' import { useWallet } from 'contexts/wallet' import { useCallback, useEffect, useState } from 'react' -import { - minter as initContract, - MinterContract, - MinterInstance, -} from './contract' +import type { MinterContract, MinterInstance, MinterMessages } from './contract' +import { minter as initContract } from './contract' /*export interface InstantiateResponse { /** The address of the newly instantiated contract *-/ @@ -33,11 +30,12 @@ export interface UseMinterContractProps { initMsg: Record, label: string, admin?: string, - funds?: Coin[] + funds?: Coin[], ) => Promise use: (customAddress: string) => MinterInstance | undefined updateContractAddress: (contractAddress: string) => void getContractAddress: () => string | undefined + messages: () => MinterMessages | undefined } export function useMinterContract(): UseMinterContractProps { @@ -52,12 +50,8 @@ export function useMinterContract(): UseMinterContractProps { useEffect(() => { if (wallet.initialized) { - const getMinterBaseInstance = async (): Promise => { - const MinterBaseContract = initContract(wallet.getClient()) - setMinter(MinterBaseContract) - } - - getMinterBaseInstance() + const MinterBaseContract = initContract(wallet.getClient(), wallet.address) + setMinter(MinterBaseContract) } }, [wallet]) @@ -66,33 +60,38 @@ export function useMinterContract(): UseMinterContractProps { } const instantiate = useCallback( - (codeId, initMsg, label, admin?, funds?): Promise => { + (codeId: number, initMsg: Record, label: string, admin?: string): Promise => { return new Promise((resolve, reject) => { - if (!minter) return reject('Contract is not initialized.') - minter - .instantiate(wallet.address, codeId, initMsg, label, admin, funds) - .then(resolve) - .catch(reject) + if (!minter) { + reject(new Error('Contract is not initialized.')) + return + } + minter.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject) }) }, - [minter, wallet] + [minter, wallet], ) const use = useCallback( (customAddress = ''): MinterInstance | undefined => { return minter?.use(address || customAddress) }, - [minter, address] + [minter, address], ) const getContractAddress = (): string | undefined => { return address } + const messages = useCallback((): MinterMessages | undefined => { + return minter?.messages() + }, [minter]) + return { instantiate, use, updateContractAddress, getContractAddress, + messages, } } diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index 5e104ee..c12d24b 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -1,111 +1,181 @@ -import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' -import { Coin } from '@cosmjs/stargate' +import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import { toBase64, toUtf8 } from '@cosmjs/encoding' +import type { Coin } from '@cosmjs/stargate' +import { coin } from '@cosmjs/stargate' export interface InstantiateResponse { readonly contractAddress: string readonly transactionHash: string } -export type Expiration = - | { at_height: number } - | { at_time: string } - | { never: {} } +export type Expiration = { at_height: number } | { at_time: string } | { never: Record } export interface SG721Instance { readonly contractAddress: string // queries - getOwnerOf: ( - token_id: string, - include_expired: boolean | null - ) => Promise + ownerOf: (tokenId: string, includeExpired?: boolean | null) => Promise - getApproval: ( - token_id: string, - spender: string, - include_expired: boolean | null - ) => Promise + approval: (tokenId: string, spender: string, includeExpired?: boolean | null) => Promise - getApprovals: ( - token_id: string, - include_expired: boolean | null - ) => Promise + approvals: (tokenId: string, includeExpired?: boolean | null) => Promise - getAllOperators: ( + allOperators: ( owner: string, - include_expired: boolean | null, - start_after: string | null, - limit: number | null + includeExpired?: boolean | null, + startAfter?: string | null, + limit?: number | null, ) => Promise - getNumTokens: () => Promise + numTokens: () => Promise - getContractInfo: () => Promise + contractInfo: () => Promise - getNftInfo: (token_id: string) => Promise + nftInfo: (tokenId: string) => Promise - getAllNftInfo: ( - token_id: string, - include_expired: boolean | null - ) => Promise + allNftInfo: (tokenId: string, includeExpired?: boolean | null) => Promise - getTokens: ( - owner: string, - start_after: string | null, - limit: number | null - ) => Promise + tokens: (owner: string, startAfter?: string | null, limit?: number | null) => Promise - getAllTokens: ( - start_after: string | null, - limit: number | null - ) => Promise + allTokens: (startAfter?: string | null, limit?: number | null) => Promise - getMinter: () => Promise + minter: () => Promise - getCollectionInfo: () => Promise + collectionInfo: () => Promise //Execute - transferNft: ( - senderAddress: string, - recipient: string, - token_id: string - ) => Promise + transferNft: (recipient: string, tokenId: string) => Promise /// Send is a base message to transfer a token to a contract and trigger an action /// on the receiving contract. sendNft: ( - senderAddress: string, contract: string, - token_id: string, - msg: string //Binary + tokenId: string, + msg: Record, //Binary ) => Promise /// Allows operator to transfer / send the token from the owner's account. /// If expiration is set, then this allowance has a time/height limit - approve: ( - senderAddress: string, - spender: string, - token_id: string, - expires: Expiration | null - ) => Promise + approve: (spender: string, tokenId: string, expires?: Expiration) => Promise /// Remove previously granted Approval - revoke: ( - senderAddress: string, - spender: string, - token_id: string - ) => Promise + revoke: (spender: string, tokenId: string) => Promise /// Allows operator to transfer / send any token from the owner's account. /// If expiration is set, then this allowance has a time/height limit - approveAll: ( - senderAddress: string, - operator: string, - expires: Expiration | null - ) => Promise + approveAll: (operator: string, expires?: Expiration) => Promise /// Remove previously granted ApproveAll permission - revokeAll: (senderAddress: string, operator: string) => Promise + revokeAll: (operator: string) => Promise /// Mint a new NFT, can only be called by the contract minter - mint: (senderAddress: string, msg: string) => Promise //MintMsg + mint: (tokenId: string, owner: string, tokenURI?: string) => Promise //MintMsg /// Burn an NFT the sender has access to - burn: (senderAddress: string, token_id: string) => Promise + burn: (tokenId: string) => Promise +} + +export interface Sg721Messages { + transferNft: (recipient: string, tokenId: string) => TransferNFTMessage + sendNft: (contract: string, tokenId: string, msg: Record) => SendNFTMessage + approve: (recipient: string, tokenId: string, expires?: Expiration) => ApproveMessage + revoke: (recipient: string, tokenId: string) => RevokeMessage + approveAll: (operator: string, expires?: Expiration) => ApproveAllMessage + revokeAll: (operator: string) => RevokeAllMessage + mint: (tokenId: string, owner: string, tokenURI?: string) => MintMessage + burn: (tokenId: string) => BurnMessage +} + +export interface TransferNFTMessage { + sender: string + contract: string + msg: { + transfer_nft: { + recipient: string + token_id: string + } + } + funds: Coin[] +} + +export interface SendNFTMessage { + sender: string + contract: string + msg: { + send_nft: { + contract: string + token_id: string + msg: Record + } + } + funds: Coin[] +} + +export interface ApproveMessage { + sender: string + contract: string + msg: { + approve: { + spender: string + token_id: string + expires?: Expiration + } + } + funds: Coin[] +} + +export interface RevokeMessage { + sender: string + contract: string + + msg: { + revoke: { + spender: string + token_id: string + } + } + funds: Coin[] +} + +export interface ApproveAllMessage { + sender: string + contract: string + msg: { + approve_all: { + operator: string + expires?: Expiration + } + } + funds: Coin[] +} + +export interface RevokeAllMessage { + sender: string + contract: string + msg: { + revoke_all: { + operator: string + } + } + funds: Coin[] +} + +export interface MintMessage { + sender: string + contract: string + msg: { + mint: { + token_id: string + owner: string + token_uri?: string + } + } + funds: Coin[] +} + +export interface BurnMessage { + sender: string + contract: string + msg: { + burn: { + token_id: string + } + } + funds: Coin[] } export interface SG721Contract { @@ -114,121 +184,103 @@ export interface SG721Contract { codeId: number, initMsg: Record, label: string, - funds: Coin[], - admin?: string + admin?: string, ) => Promise use: (contractAddress: string) => SG721Instance + + messages: (contractAddress: string) => Sg721Messages } -export const SG721 = (client: SigningCosmWasmClient): SG721Contract => { +export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Contract => { const use = (contractAddress: string): SG721Instance => { - const encode = (str: string): string => - Buffer.from(str, 'binary').toString('base64') + const jsonToBinary = (json: Record): string => { + return toBase64(toUtf8(JSON.stringify(json))) + } - const getOwnerOf = async ( - token_id: string, - include_expired: boolean | null - ): Promise => { + const ownerOf = async (tokenId: string, includeExpired?: boolean | null): Promise => { const res = await client.queryContractSmart(contractAddress, { - owner_of: { token_id, include_expired }, + owner_of: { token_id: tokenId, include_expired: includeExpired }, }) return res } - const getApproval = async ( - token_id: string, - spender: string, - include_expired: boolean | null - ): Promise => { + const approval = async (tokenId: string, spender: string, includeExpired?: boolean | null): Promise => { const res = await client.queryContractSmart(contractAddress, { - approval: { token_id, spender, include_expired }, + approval: { token_id: tokenId, spender, include_expired: includeExpired }, }) return res } - const getApprovals = async ( - token_id: string, - include_expired: boolean | null - ): Promise => { + const approvals = async (tokenId: string, includeExpired?: boolean | null): Promise => { const res = await client.queryContractSmart(contractAddress, { - approvals: { token_id, include_expired }, + approvals: { token_id: tokenId, include_expired: includeExpired }, }) return res } - const getAllOperators = async ( + const allOperators = async ( owner: string, - include_expired: boolean | null, - start_after: string | null, - limit: number | null + includeExpired?: boolean | null, + startAfter?: string | null, + limit?: number | null, ): Promise => { const res = await client.queryContractSmart(contractAddress, { - all_operators: { owner, include_expired, start_after, limit }, + all_operators: { owner, include_expired: includeExpired, start_after: startAfter, limit }, }) return res } - const getNumTokens = async (): Promise => { + const numTokens = async (): Promise => { const res = await client.queryContractSmart(contractAddress, { num_tokens: {}, }) return res } - const getContractInfo = async (): Promise => { + const contractInfo = async (): Promise => { const res = await client.queryContractSmart(contractAddress, { contract_info: {}, }) return res } - const getNftInfo = async (token_id: string): Promise => { + const nftInfo = async (tokenId: string): Promise => { const res = await client.queryContractSmart(contractAddress, { - nft_info: { token_id }, + nft_info: { token_id: tokenId }, }) return res } - const getAllNftInfo = async ( - token_id: string, - include_expired: boolean | null - ): Promise => { + const allNftInfo = async (tokenId: string, includeExpired?: boolean | null): Promise => { const res = await client.queryContractSmart(contractAddress, { - all_nft_info: { token_id, include_expired }, + all_nft_info: { token_id: tokenId, include_expired: includeExpired }, }) return res } - const getTokens = async ( - owner: string, - start_after: string | null, - limit: number | null - ): Promise => { + const tokens = async (owner: string, startAfter?: string | null, limit?: number | null): Promise => { const res = await client.queryContractSmart(contractAddress, { - tokens: { owner, start_after, limit }, + tokens: { owner, start_after: startAfter, limit }, }) return res } - const getAllTokens = async ( - start_after: string | null, - limit: number | null - ): Promise => { + const allTokens = async (startAfter?: string | null, limit?: number | null): Promise => { const res = await client.queryContractSmart(contractAddress, { - all_tokens: { start_after, limit }, + all_tokens: { start_after: startAfter, limit }, }) return res } - const getMinter = async (): Promise => { + const minter = async (): Promise => { const res = await client.queryContractSmart(contractAddress, { minter: {}, }) return res } - const getCollectionInfo = async (): Promise => { + const collectionInfo = async (): Promise => { const res = await client.queryContractSmart(contractAddress, { collection_info: {}, }) @@ -236,144 +288,121 @@ export const SG721 = (client: SigningCosmWasmClient): SG721Contract => { } //Execute - const transferNft = async ( - senderAddress: string, - recipient: string, - token_id: string - ): Promise => { + const transferNft = async (recipient: string, tokenId: string): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, { - transfer_nft: { recipient, token_id }, + transfer_nft: { recipient, token_id: tokenId }, }, 'auto', - '' + '', ) return res.transactionHash } const sendNft = async ( - senderAddress: string, contract: string, - token_id: string, - msg: string //Binary + tokenId: string, + msg: Record, //Binary ): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, { - send_nft: { contract, token_id, msg: encode(msg) }, + send_nft: { contract, token_id: tokenId, msg: jsonToBinary(msg) }, }, 'auto', - '' + '', ) return res.transactionHash } - const approve = async ( - senderAddress: string, - spender: string, - token_id: string, - expires: Expiration | null - ): Promise => { + const approve = async (spender: string, tokenId: string, expires?: Expiration): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, { - approve: { spender, token_id, expires }, + approve: { spender, token_id: tokenId, expires }, }, 'auto', - '' + '', ) return res.transactionHash } - const revoke = async ( - senderAddress: string, - spender: string, - token_id: string - ): Promise => { + const revoke = async (spender: string, tokenId: string): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, { - revoke: { spender, token_id }, + revoke: { spender, token_id: tokenId }, }, 'auto', - '' + '', ) return res.transactionHash } - const approveAll = async ( - senderAddress: string, - operator: string, - expires: Expiration | null - ): Promise => { + const approveAll = async (operator: string, expires?: Expiration): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, { approve_all: { operator, expires }, }, 'auto', - '' + '', ) return res.transactionHash } - const revokeAll = async ( - senderAddress: string, - operator: string - ): Promise => { + const revokeAll = async (operator: string): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, { revoke_all: { operator }, }, 'auto', - '' + '', ) return res.transactionHash } - const mint = async ( - senderAddress: string, - msg: string - ): Promise => { + const mint = async (tokenId: string, owner: string, tokenURI?: string): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, { - mint: { msg }, + mint: { + token_id: tokenId, + owner, + token_uri: tokenURI, + }, }, 'auto', - '' + '', ) return res.transactionHash } - const burn = async ( - senderAddress: string, - token_id: string - ): Promise => { + const burn = async (tokenId: string): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, { - burn: { token_id }, + burn: { token_id: tokenId }, }, 'auto', - '' + '', ) return res.transactionHash @@ -381,18 +410,18 @@ export const SG721 = (client: SigningCosmWasmClient): SG721Contract => { return { contractAddress, - getOwnerOf, - getApproval, - getApprovals, - getAllOperators, - getNumTokens, - getContractInfo, - getNftInfo, - getAllNftInfo, - getTokens, - getAllTokens, - getMinter, - getCollectionInfo, + ownerOf, + approval, + approvals, + allOperators, + numTokens, + contractInfo, + nftInfo, + allNftInfo, + tokens, + allTokens, + minter, + collectionInfo, transferNft, sendNft, approve, @@ -409,26 +438,144 @@ export const SG721 = (client: SigningCosmWasmClient): SG721Contract => { codeId: number, initMsg: Record, label: string, - funds: Coin[], - admin?: string + admin?: string, ): Promise => { - const result = await client.instantiate( - senderAddress, - codeId, - initMsg, - label, - 'auto', - { - funds, - memo: '', - admin, - } - ) + const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', { + funds: [coin('1000000000', 'ustars')], + memo: '', + admin, + }) return { contractAddress: result.contractAddress, transactionHash: result.transactionHash, } } - return { use, instantiate } + const messages = (contractAddress: string) => { + const transferNft = (recipient: string, tokenId: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + transfer_nft: { + recipient, + token_id: tokenId, + }, + }, + funds: [], + } + } + + const sendNft = (contract: string, tokenId: string, msg: Record) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + send_nft: { + contract, + token_id: tokenId, + msg, + }, + }, + funds: [], + } + } + + const approve = (spender: string, tokenId: string, expires?: Expiration) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + approve: { + spender, + token_id: tokenId, + expires, + }, + }, + funds: [], + } + } + + const revoke = (spender: string, tokenId: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + revoke: { + spender, + token_id: tokenId, + }, + }, + funds: [], + } + } + + const approveAll = (operator: string, expires?: Expiration) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + approve_all: { + operator, + expires, + }, + }, + funds: [], + } + } + + const revokeAll = (operator: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + revoke_all: { + operator, + }, + }, + funds: [], + } + } + + const mint = (tokenId: string, owner: string, tokenURI?: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + mint: { + token_id: tokenId, + owner, + token_uri: tokenURI, + }, + }, + funds: [], + } + } + + const burn = (tokenId: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + burn: { + token_id: tokenId, + }, + }, + funds: [], + } + } + + return { + transferNft, + sendNft, + approve, + revoke, + approveAll, + revokeAll, + mint, + burn, + } + } + + return { use, instantiate, messages } } diff --git a/contracts/sg721/messages/execute.ts b/contracts/sg721/messages/execute.ts new file mode 100644 index 0000000..de628b5 --- /dev/null +++ b/contracts/sg721/messages/execute.ts @@ -0,0 +1,162 @@ +import type { Expiration, SG721Instance } from '../index' +import { useSG721Contract } from '../index' + +export type ExecuteType = typeof EXECUTE_TYPES[number] + +export const EXECUTE_TYPES = [ + 'transfer_nft', + 'send_nft', + 'approve', + 'revoke', + 'approve_all', + 'revoke_all', + 'mint', + 'burn', +] as const + +export interface ExecuteListItem { + id: ExecuteType + name: string + description?: string +} + +export const EXECUTE_LIST: ExecuteListItem[] = [ + { + id: 'transfer_nft', + name: 'Transfer NFT', + description: `Transfer a token to an address`, + }, + { + id: 'send_nft', + name: 'Send NFT', + description: `Send a token to a contract and execute a message afterwards`, + }, + { + id: 'approve', + name: 'Approve', + description: `Allow an operator to transfer/send a given token from the owner's account`, + }, + { + id: 'revoke', + name: 'Revoke', + description: `Remove permissions of an operator from the owner's account`, + }, + { + id: 'approve_all', + name: 'Approve All', + description: `Allow an operator to transfer/send all tokens from owner's account`, + }, + { + id: 'revoke_all', + name: 'Revoke All', + description: `Remove permissions of an operator from the owner's account`, + }, + { + id: 'mint', + name: 'Mint', + description: `Mint a new token to owner's account`, + }, + { + id: 'burn', + name: 'Burn', + description: `Burn a token transaction sender has access to`, + }, +] + +export interface DispatchExecuteProps { + type: ExecuteType + [k: string]: unknown +} + +type Select = T + +/** @see {@link SG721Instance} */ +export type DispatchExecuteArgs = { + contract: string + messages?: SG721Instance +} & ( + | { type: undefined } + | { type: Select<'transfer_nft'>; recipient: string; tokenId: string } + | { type: Select<'send_nft'>; recipient: string; tokenId: string; msg: Record } + | { type: Select<'approve'>; recipient: string; tokenId: string; expiration?: Expiration } + | { type: Select<'revoke'>; recipient: string; tokenId: string } + | { type: Select<'approve_all'>; operator: string; expiration?: Expiration } + | { type: Select<'revoke_all'>; operator: string } + | { type: Select<'mint'>; recipient: string; tokenId: string; tokenURI?: string } + | { type: Select<'burn'>; tokenId: string } +) + +export const dispatchExecute = async (args: DispatchExecuteArgs) => { + const { messages } = args + if (!messages) { + throw new Error('cannot dispatch execute, messages is not defined') + } + switch (args.type) { + case 'transfer_nft': { + return messages.transferNft(args.recipient, args.tokenId) + } + case 'send_nft': { + return messages.sendNft(args.recipient, args.tokenId, args.msg) + } + case 'approve': { + return messages.approve(args.recipient, args.tokenId, args.expiration) + } + case 'revoke': { + return messages.revoke(args.recipient, args.tokenId) + } + case 'approve_all': { + return messages.approveAll(args.operator, args.expiration) + } + case 'revoke_all': { + return messages.revokeAll(args.operator) + } + case 'mint': { + return messages.mint(args.recipient, args.tokenId, args.tokenURI) + } + case 'burn': { + return messages.burn(args.tokenId) + } + default: { + throw new Error('unknown execute type') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages } = useSG721Contract() + const { contract } = args + switch (args.type) { + case 'transfer_nft': { + return messages(contract)?.transferNft(args.recipient, args.tokenId) + } + case 'send_nft': { + return messages(contract)?.sendNft(args.recipient, args.tokenId, args.msg) + } + case 'approve': { + return messages(contract)?.approve(args.recipient, args.tokenId, args.expiration) + } + case 'revoke': { + return messages(contract)?.revoke(args.recipient, args.tokenId) + } + case 'approve_all': { + return messages(contract)?.approveAll(args.operator, args.expiration) + } + case 'revoke_all': { + return messages(contract)?.revokeAll(args.operator) + } + case 'mint': { + return messages(contract)?.mint(args.recipient, args.tokenId, args.tokenURI) + } + case 'burn': { + return messages(contract)?.burn(args.tokenId) + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} diff --git a/contracts/sg721/messages/query.ts b/contracts/sg721/messages/query.ts new file mode 100644 index 0000000..2a505b3 --- /dev/null +++ b/contracts/sg721/messages/query.ts @@ -0,0 +1,95 @@ +import type { SG721Instance } from '../contract' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = [ + 'owner_of', + 'approval', + 'approvals', + 'all_operators', + 'num_tokens', + 'contract_info', + 'nft_info', + 'all_nft_info', + 'tokens', + 'all_tokens', + 'minter', + 'collection_info', +] as const + +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const QUERY_LIST: QueryListItem[] = [ + { id: 'owner_of', name: 'Owner Of', description: 'View current owner of given token' }, + { id: 'approval', name: 'Approval', description: 'View address that has access to given token' }, + { id: 'approvals', name: 'Approvals', description: 'View all approvals of a given token' }, + { + id: 'all_operators', + name: 'All Operators', + description: "List all the operators that has access all of the owner's tokens", + }, + { id: 'num_tokens', name: 'Number of Tokens', description: 'View total number of tokens minted' }, + { id: 'contract_info', name: 'Contract Info', description: 'View top-level metadata of contract' }, + { id: 'nft_info', name: 'NFT Info', description: 'View metadata of a given token' }, + { id: 'all_nft_info', name: 'All NFT Info', description: 'View metadata and owner info of a given token' }, + { id: 'tokens', name: 'Tokens', description: 'View all the tokens owned by given address' }, + { id: 'all_tokens', name: 'All Tokens', description: 'List all the tokens controlled by the contract' }, + { id: 'minter', name: 'Minter', description: 'View current minter of the contract' }, + { id: 'collection_info', name: 'Collection Info', description: 'View metadata of a given collection' }, +] + +export interface DispatchQueryProps { + messages: SG721Instance | undefined + type: QueryType + tokenId: string + address: string +} + +export const dispatchQuery = (props: DispatchQueryProps) => { + const { tokenId, messages, type, address } = props + switch (type) { + case 'owner_of': { + return messages?.ownerOf(tokenId) + } + case 'approval': { + return messages?.approval(tokenId, address) + } + case 'approvals': { + return messages?.approvals(tokenId) + } + case 'all_operators': { + return messages?.allOperators(address) + } + case 'num_tokens': { + return messages?.numTokens() + } + case 'contract_info': { + return messages?.contractInfo() + } + case 'nft_info': { + return messages?.nftInfo(tokenId) + } + case 'all_nft_info': { + return messages?.allNftInfo(tokenId, null) + } + case 'tokens': { + return messages?.tokens(address) + } + case 'all_tokens': { + return messages?.allTokens() + } + case 'minter': { + return messages?.minter() + } + case 'collection_info': { + return messages?.collectionInfo() + } + default: { + throw new Error('unknown query type') + } + } +} diff --git a/contracts/sg721/useContract.ts b/contracts/sg721/useContract.ts index d29ca6c..5de02f4 100644 --- a/contracts/sg721/useContract.ts +++ b/contracts/sg721/useContract.ts @@ -1,8 +1,9 @@ +import type { Coin } from '@cosmjs/proto-signing' import { useWallet } from 'contexts/wallet' -import { Coin } from 'cosmwasm' import { useCallback, useEffect, useState } from 'react' -import { SG721 as initContract, SG721Contract, SG721Instance } from './contract' +import type { SG721Contract, SG721Instance, Sg721Messages } from './contract' +import { SG721 as initContract } from './contract' interface InstantiateResponse { readonly contractAddress: string @@ -14,11 +15,12 @@ export interface UseSG721ContractProps { codeId: number, initMsg: Record, label: string, - funds: Coin[], - admin?: string + admin?: string, + funds?: Coin[], ) => Promise use: (customAddress: string) => SG721Instance | undefined updateContractAddress: (contractAddress: string) => void + messages: (contractAddress: string) => Sg721Messages | undefined } export function useSG721Contract(): UseSG721ContractProps { @@ -33,12 +35,8 @@ export function useSG721Contract(): UseSG721ContractProps { useEffect(() => { if (wallet.initialized) { - const getSG721Instance = async (): Promise => { - const SG721Contract = initContract(wallet.getClient()) - setSG721(SG721Contract) - } - - getSG721Instance() + const contract = initContract(wallet.getClient(), wallet.address) + setSG721(contract) } }, [wallet]) @@ -47,27 +45,33 @@ export function useSG721Contract(): UseSG721ContractProps { } const instantiate = useCallback( - (codeId, initMsg, label, admin?): Promise => { + (codeId: number, initMsg: Record, label: string, admin?: string): Promise => { return new Promise((resolve, reject) => { - if (!SG721) return reject('Contract is not initialized.') - SG721.instantiate(wallet.address, codeId, initMsg, label, admin) - .then(resolve) - .catch(reject) + if (!SG721) { + reject(new Error('Contract is not initialized.')) + return + } + SG721.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject) }) }, - [SG721, wallet] + [SG721, wallet], ) const use = useCallback( (customAddress = ''): SG721Instance | undefined => { return SG721?.use(address || customAddress) }, - [SG721, address] + [SG721, address], ) + const messages = useCallback((): Sg721Messages | undefined => { + return SG721?.messages(address) + }, [SG721, address]) + return { instantiate, use, updateContractAddress, + messages, } } diff --git a/contracts/whitelist/contract.ts b/contracts/whitelist/contract.ts index 012aa6a..3d7e618 100644 --- a/contracts/whitelist/contract.ts +++ b/contracts/whitelist/contract.ts @@ -1,7 +1,6 @@ -import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' -import { Coin } from '@cosmjs/proto-signing' - -type Expiration = { at_height: number } | { at_time: string } | { never: {} } +import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import type { Coin } from '@cosmjs/proto-signing' +import { coin } from '@cosmjs/proto-signing' export interface InstantiateResponse { readonly contractAddress: string @@ -23,39 +22,98 @@ export interface WhiteListInstance { hasStarted: () => Promise hasEnded: () => Promise isActive: () => Promise - members: (limit: number, startAfter?: string) => Promise + members: (startAfter?: string, limit?: number) => Promise hasMember: (member: string) => Promise config: () => Promise //Execute updateStartTime: (startTime: string) => Promise updateEndTime: (endTime: string) => Promise - addMembers: (to_add: string[]) => Promise - removeMembers: (to_remove: string[]) => Promise + addMembers: (memberList: string[]) => Promise + removeMembers: (memberList: string[]) => Promise updatePerAddressLimit: (limit: number) => Promise increaseMemberLimit: (limit: number) => Promise } +export interface WhitelistMessages { + updateStartTime: (startTime: string) => UpdateStartTimeMessage + updateEndTime: (endTime: string) => UpdateEndTimeMessage + addMembers: (memberList: string[]) => AddMembersMessage + removeMembers: (memberList: string[]) => RemoveMembersMessage + updatePerAddressLimit: (limit: number) => UpdatePerAddressLimitMessage + increaseMemberLimit: (limit: number) => IncreaseMemberLimitMessage +} + +export interface UpdateStartTimeMessage { + sender: string + contract: string + msg: { + update_start_time: string + } + funds: Coin[] +} + +export interface UpdateEndTimeMessage { + sender: string + contract: string + msg: { + update_end_time: string + } + funds: Coin[] +} + +export interface AddMembersMessage { + sender: string + contract: string + msg: { + add_members: { to_add: string[] } + } + funds: Coin[] +} + +export interface RemoveMembersMessage { + sender: string + contract: string + msg: { + remove_members: { to_remove: string[] } + } + funds: Coin[] +} + +export interface UpdatePerAddressLimitMessage { + sender: string + + contract: string + msg: { + update_per_address_limit: number + } + funds: Coin[] +} + +export interface IncreaseMemberLimitMessage { + sender: string + contract: string + msg: { + increase_member_limit: number + } + funds: Coin[] +} + export interface WhiteListContract { instantiate: ( - senderAddress: string, codeId: number, initMsg: Record, label: string, admin?: string, - funds?: Coin[] ) => Promise use: (contractAddress: string) => WhiteListInstance + + messages: (contractAddress: string) => WhitelistMessages } -export const WhiteList = ( - client: SigningCosmWasmClient, - senderAddress: string -): WhiteListContract => { +export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): WhiteListContract => { const use = (contractAddress: string): WhiteListInstance => { - console.log(client, 'client') - console.log(senderAddress, 'senderAddress') ///QUERY START const hasStarted = async (): Promise => { return client.queryContractSmart(contractAddress, { has_started: {} }) @@ -69,10 +127,7 @@ export const WhiteList = ( return client.queryContractSmart(contractAddress, { is_active: {} }) } - const members = async ( - limit: number, - startAfter?: string - ): Promise => { + const members = async (startAfter?: string, limit?: number): Promise => { return client.queryContractSmart(contractAddress, { members: { limit, start_after: startAfter }, }) @@ -92,63 +147,50 @@ export const WhiteList = ( /// QUERY END /// EXECUTE START const updateStartTime = async (startTime: string): Promise => { - const res = await client.execute( - senderAddress, - contractAddress, - { update_start_time: startTime }, - 'auto', - 'memo' - ) + const res = await client.execute(txSigner, contractAddress, { update_start_time: startTime }, 'auto') return res.transactionHash } const updateEndTime = async (endTime: string): Promise => { + const res = await client.execute(txSigner, contractAddress, { update_end_time: endTime }, 'auto') + return res.transactionHash + } + + const addMembers = async (memberList: string[]): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, - { update_end_time: endTime }, - 'auto' + { + add_members: { + to_add: memberList, + }, + }, + 'auto', ) return res.transactionHash } - const addMembers = async (to_add: string[]): Promise => { + const removeMembers = async (memberList: string[]): Promise => { const res = await client.execute( - senderAddress, + txSigner, contractAddress, - { add_members: to_add }, - 'auto' - ) - return res.transactionHash - } - - const removeMembers = async (to_remove: string[]): Promise => { - const res = await client.execute( - senderAddress, - contractAddress, - { remove_members: to_remove }, - 'auto' + { + remove_members: { + to_remove: memberList, + }, + }, + 'auto', ) return res.transactionHash } const updatePerAddressLimit = async (limit: number): Promise => { - const res = await client.execute( - senderAddress, - contractAddress, - { update_per_address_limit: limit }, - 'auto' - ) + const res = await client.execute(txSigner, contractAddress, { update_per_address_limit: limit }, 'auto') return res.transactionHash } const increaseMemberLimit = async (limit: number): Promise => { - const res = await client.execute( - senderAddress, - contractAddress, - { increase_member_limit: limit }, - 'auto' - ) + const res = await client.execute(txSigner, contractAddress, { increase_member_limit: limit }, 'auto') return res.transactionHash } /// EXECUTE END @@ -171,25 +213,15 @@ export const WhiteList = ( } const instantiate = async ( - senderAddress: string, codeId: number, initMsg: Record, label: string, admin?: string, - funds?: Coin[] ): Promise => { - console.log('Funds:' + funds) - const result = await client.instantiate( - senderAddress, - codeId, - initMsg, - label, - 'auto', - { - funds, - admin, - } - ) + const result = await client.instantiate(txSigner, codeId, initMsg, label, 'auto', { + funds: [coin('100000000', 'ustars')], + admin, + }) return { contractAddress: result.contractAddress, @@ -197,5 +229,82 @@ export const WhiteList = ( } } - return { use, instantiate } + const messages = (contractAddress: string) => { + const updateStartTime = (startTime: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_start_time: startTime, + }, + funds: [], + } + } + + const updateEndTime = (endTime: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_end_time: endTime, + }, + funds: [], + } + } + + const addMembers = (memberList: string[]) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + add_members: { to_add: memberList }, + }, + funds: [], + } + } + + const removeMembers = (memberList: string[]) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + remove_members: { to_remove: memberList }, + }, + funds: [], + } + } + + const updatePerAddressLimit = (limit: number) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_per_address_limit: limit, + }, + funds: [], + } + } + + const increaseMemberLimit = (limit: number) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + increase_member_limit: limit, + }, + funds: [], + } + } + + return { + updateStartTime, + updateEndTime, + addMembers, + removeMembers, + updatePerAddressLimit, + increaseMemberLimit, + } + } + + return { use, instantiate, messages } } diff --git a/contracts/whitelist/messages/execute.ts b/contracts/whitelist/messages/execute.ts new file mode 100644 index 0000000..0d1a885 --- /dev/null +++ b/contracts/whitelist/messages/execute.ts @@ -0,0 +1,136 @@ +import type { WhiteListInstance } from '../index' +import { useWhiteListContract } from '../index' + +export type ExecuteType = typeof EXECUTE_TYPES[number] + +export const EXECUTE_TYPES = [ + 'update_start_time', + 'update_end_time', + 'add_members', + 'remove_members', + 'update_per_address_limit', + 'increase_member_limit', +] as const + +export interface ExecuteListItem { + id: ExecuteType + name: string + description?: string +} + +export const EXECUTE_LIST: ExecuteListItem[] = [ + { + id: 'update_start_time', + name: 'Update Start Time', + description: `Update the start time of the whitelist`, + }, + { + id: 'update_end_time', + name: 'Update End Time', + description: `Update the end time of the whitelist`, + }, + { + id: 'add_members', + name: 'Add Members', + description: `Add members to the whitelist`, + }, + { + id: 'remove_members', + name: 'Remove Members', + description: `Remove members from the whitelist`, + }, + { + id: 'update_per_address_limit', + name: 'Update Per Address Limit', + description: `Update tokens per address limit`, + }, + { + id: 'increase_member_limit', + name: 'Increase Member Limit', + description: `Increase the member limit of the whitelist`, + }, +] + +export interface DispatchExecuteProps { + type: ExecuteType + [k: string]: unknown +} + +type Select = T + +/** @see {@link WhiteListInstance} */ +export type DispatchExecuteArgs = { + contract: string + messages?: WhiteListInstance +} & ( + | { type: undefined } + | { type: Select<'update_start_time'>; timestamp: string } + | { type: Select<'update_end_time'>; timestamp: string } + | { type: Select<'add_members'>; members: string[] } + | { type: Select<'remove_members'>; members: string[] } + | { type: Select<'update_per_address_limit'>; limit: number } + | { type: Select<'increase_member_limit'>; limit: number } +) + +export const dispatchExecute = async (args: DispatchExecuteArgs) => { + const { messages } = args + if (!messages) { + throw new Error('cannot dispatch execute, messages is not defined') + } + switch (args.type) { + case 'update_start_time': { + return messages.updateStartTime(args.timestamp) + } + case 'update_end_time': { + return messages.updateEndTime(args.timestamp) + } + case 'add_members': { + return messages.addMembers(args.members) + } + case 'remove_members': { + return messages.removeMembers(args.members) + } + case 'update_per_address_limit': { + return messages.updatePerAddressLimit(args.limit) + } + case 'increase_member_limit': { + return messages.increaseMemberLimit(args.limit) + } + default: { + throw new Error('unknown execute type') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages } = useWhiteListContract() + const { contract } = args + switch (args.type) { + case 'update_start_time': { + return messages(contract)?.updateStartTime(args.timestamp) + } + case 'update_end_time': { + return messages(contract)?.updateEndTime(args.timestamp) + } + case 'add_members': { + return messages(contract)?.addMembers(args.members) + } + case 'remove_members': { + return messages(contract)?.removeMembers(args.members) + } + case 'update_per_address_limit': { + return messages(contract)?.updatePerAddressLimit(args.limit) + } + case 'increase_member_limit': { + return messages(contract)?.increaseMemberLimit(args.limit) + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} diff --git a/contracts/whitelist/messages/query.ts b/contracts/whitelist/messages/query.ts new file mode 100644 index 0000000..652e801 --- /dev/null +++ b/contracts/whitelist/messages/query.ts @@ -0,0 +1,47 @@ +import type { WhiteListInstance } from '../contract' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = ['has_started', 'has_ended', 'is_active', 'members', 'has_member', 'config'] as const + +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const QUERY_LIST: QueryListItem[] = [ + { id: 'has_started', name: 'Has Started', description: 'Check if the whitelist minting has started' }, + { id: 'has_ended', name: 'Has Ended', description: 'Check if the whitelist minting has ended' }, + { id: 'is_active', name: 'Is Active', description: 'Check if the whitelist minting is active' }, + { id: 'members', name: 'Members', description: 'View the whitelist members' }, + { id: 'has_member', name: 'Has Member', description: 'Check if a member is in the whitelist' }, + { id: 'config', name: 'Config', description: 'View the whitelist configuration' }, +] + +export interface DispatchQueryProps { + messages: WhiteListInstance | undefined + type: QueryType + address: string +} + +export const dispatchQuery = (props: DispatchQueryProps) => { + const { messages, type, address } = props + switch (type) { + case 'has_started': + return messages?.hasStarted() + case 'has_ended': + return messages?.hasEnded() + case 'is_active': + return messages?.isActive() + case 'members': + return messages?.members() + case 'has_member': + return messages?.hasMember(address) + case 'config': + return messages?.config() + default: { + throw new Error('unknown query type') + } + } +} diff --git a/contracts/whitelist/useContract.ts b/contracts/whitelist/useContract.ts index bb5ab48..d412827 100644 --- a/contracts/whitelist/useContract.ts +++ b/contracts/whitelist/useContract.ts @@ -1,33 +1,29 @@ -import { Coin } from '@cosmjs/proto-signing' import { useWallet } from 'contexts/wallet' import { useCallback, useEffect, useState } from 'react' -import { WhiteList } from './contract' -import { - InstantiateResponse, - WhiteList as initContract, - WhiteListContract, - WhiteListInstance, -} from './contract' +import type { InstantiateResponse, WhiteListContract, WhiteListInstance, WhitelistMessages } from './contract' +import { WhiteList as initContract } from './contract' -export interface useWhiteListContractProps { +export interface UseWhiteListContractProps { instantiate: ( codeId: number, initMsg: Record, label: string, admin?: string, - funds?: Coin[] ) => Promise - use: (customAddress: string) => WhiteListInstance | undefined + use: (customAddress?: string) => WhiteListInstance | undefined + updateContractAddress: (contractAddress: string) => void + + messages: (contractAddress: string) => WhitelistMessages | undefined } -export function useWhiteListContract(): useWhiteListContractProps { +export function useWhiteListContract(): UseWhiteListContractProps { const wallet = useWallet() const [address, setAddress] = useState('') - const [WhiteList, setWhiteList] = useState() + const [whiteList, setWhiteList] = useState() useEffect(() => { setAddress(localStorage.getItem('contract_address') || '') @@ -35,13 +31,9 @@ export function useWhiteListContract(): useWhiteListContractProps { useEffect(() => { if (wallet.initialized) { - const getWhiteListInstance = async (): Promise => { - const client = wallet.getClient() - const whiteListContract = initContract(client, wallet.address) - setWhiteList(whiteListContract) - } - - getWhiteListInstance() + const client = wallet.getClient() + const whiteListContract = initContract(client, wallet.address) + setWhiteList(whiteListContract) } }, [wallet]) @@ -50,34 +42,33 @@ export function useWhiteListContract(): useWhiteListContractProps { } const instantiate = useCallback( - (codeId, initMsg, label, admin?, funds?): Promise => { + (codeId: number, initMsg: Record, label: string, admin?: string): Promise => { return new Promise((resolve, reject) => { - if (!WhiteList) return reject('Contract is not initialized.') - WhiteList.instantiate( - wallet.address, - codeId, - initMsg, - label, - admin, - funds - ) - .then(resolve) - .catch(reject) + if (!whiteList) { + reject(new Error('Contract is not initialized.')) + return + } + whiteList.instantiate(codeId, initMsg, label, admin).then(resolve).catch(reject) }) }, - [WhiteList, wallet] + [whiteList], ) const use = useCallback( (customAddress = ''): WhiteListInstance | undefined => { - return WhiteList?.use(address || customAddress) + return whiteList?.use(address || customAddress) }, - [WhiteList] + [whiteList, address], ) + const messages = useCallback((): WhitelistMessages | undefined => { + return whiteList?.messages(address) + }, [whiteList, address]) + return { instantiate, use, updateContractAddress, + messages, } } diff --git a/env.d.ts b/env.d.ts index f527c1b..9be10d9 100644 --- a/env.d.ts +++ b/env.d.ts @@ -15,17 +15,13 @@ declare namespace NodeJS { readonly APP_VERSION: string readonly NEXT_PUBLIC_SG721_CODE_ID: string + readonly NEXT_PUBLIC_MINTER_CODE_ID: string + readonly NEXT_PUBLIC_WHITELIST_CODE_ID: string readonly NEXT_PUBLIC_API_URL: string readonly NEXT_PUBLIC_BLOCK_EXPLORER_URL: string readonly NEXT_PUBLIC_NETWORK: string readonly NEXT_PUBLIC_WEBSITE_URL: string - - readonly NEXT_PUBLIC_S3_BUCKET: string - readonly NEXT_PUBLIC_S3_ENDPOINT: string - readonly NEXT_PUBLIC_S3_KEY: string - readonly NEXT_PUBLIC_S3_REGION: string - readonly NEXT_PUBLIC_S3_SECRET: string } } diff --git a/pages/contracts/index.tsx b/pages/contracts/index.tsx new file mode 100644 index 0000000..9666371 --- /dev/null +++ b/pages/contracts/index.tsx @@ -0,0 +1,41 @@ +import { HomeCard } from 'components/HomeCard' +import type { NextPage } from 'next' +// import Brand from 'public/brand/brand.svg' +import { withMetadata } from 'utils/layout' + +const HomePage: NextPage = () => { + return ( +
+
+ {/* */} +
+

Smart Contracts

+

+ Here you can invoke and query different smart contracts and see the results. +
+

+ +
+ +
+ +
+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +
+
+ ) +} + +export default withMetadata(HomePage, { center: false }) diff --git a/pages/contracts/minter/execute.tsx b/pages/contracts/minter/execute.tsx new file mode 100644 index 0000000..8523cb4 --- /dev/null +++ b/pages/contracts/minter/execute.tsx @@ -0,0 +1,166 @@ +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { ExecuteCombobox } from 'components/contracts/minter/ExecuteCombobox' +import { useExecuteComboboxState } from 'components/contracts/minter/ExecuteCombobox.hooks' +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { InputDateTime } from 'components/InputDateTime' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { minterLinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { DispatchExecuteArgs } from 'contracts/minter/messages/execute' +import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/minter/messages/execute' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import type { FormEvent } from 'react' +import { useMemo, useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaArrowRight } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const MinterExecutePage: NextPage = () => { + const { minter: contract } = useContracts() + const wallet = useWallet() + const [lastTx, setLastTx] = useState('') + + const [timestamp, setTimestamp] = useState(undefined) + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + + const limitState = useNumberInputState({ + id: 'per-address-limi', + name: 'perAddressLimit', + title: 'Per Address Limit', + subtitle: 'Enter the per address limit', + }) + + const tokenIdState = useNumberInputState({ + id: 'token-id', + name: 'tokenId', + title: 'Token ID', + subtitle: 'Enter the token ID', + }) + + const priceState = useNumberInputState({ + id: 'price', + name: 'price', + title: 'Price', + subtitle: 'Enter the token price', + }) + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Minter Address', + subtitle: 'Address of the Minter contract', + }) + + const recipientState = useInputState({ + id: 'recipient-address', + name: 'recipient', + title: 'Recipient Address', + subtitle: 'Address of the recipient', + }) + + const whitelistState = useInputState({ + id: 'whitelist-address', + name: 'whitelistAddress', + title: 'Whitelist Address', + subtitle: 'Address of the whitelist contract', + }) + + const showWhitelistField = type === 'set_whitelist' + const showDateField = type === 'update_start_time' + const showLimitField = type === 'update_per_address_limit' + const showTokenIdField = type === 'mint_for' + const showRecipientField = isEitherType(type, ['mint_to', 'mint_for']) + const showPriceField = type === 'mint' + + const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value]) + const payload: DispatchExecuteArgs = { + whitelist: whitelistState.value, + startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', + limit: limitState.value, + contract: contractState.value, + tokenId: tokenIdState.value, + messages, + recipient: recipientState.value, + txSigner: wallet.address, + price: priceState.value ? (Number(priceState.value) * 1_000_000).toString() : '0', + type, + } + const { isLoading, mutate } = useMutation( + async (event: FormEvent) => { + event.preventDefault() + if (!type) { + throw new Error('Please select message 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) + } + }, + { + onError: (error) => { + toast.error(String(error)) + }, + }, + ) + + return ( +
+ + + + +
+
+ + + {showRecipientField && } + {showWhitelistField && } + {showLimitField && } + {showTokenIdField && } + {showPriceField && } + {/* TODO: Fix address execute message */} + + + setTimestamp(date)} value={timestamp} /> + + +
+
+
+ + + + +
+ + + +
+
+
+ ) +} + +export default withMetadata(MinterExecutePage, { center: false }) diff --git a/pages/contracts/minter/index.tsx b/pages/contracts/minter/index.tsx new file mode 100644 index 0000000..561b4b3 --- /dev/null +++ b/pages/contracts/minter/index.tsx @@ -0,0 +1 @@ +export { default } from './instantiate' diff --git a/pages/contracts/minter/instantiate.tsx b/pages/contracts/minter/instantiate.tsx new file mode 100644 index 0000000..4080c55 --- /dev/null +++ b/pages/contracts/minter/instantiate.tsx @@ -0,0 +1,279 @@ +import { coin } from '@cosmjs/proto-signing' +import { Alert } from 'components/Alert' +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { FormControl } from 'components/FormControl' +import { FormGroup } from 'components/FormGroup' +import { NumberInput, TextInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { FormTextArea } from 'components/forms/FormTextArea' +import { InputDateTime } from 'components/InputDateTime' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { minterLinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { InstantiateResponse } from 'contracts/minter' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import type { FormEvent } from 'react' +import { useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaAsterisk } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { MINTER_CODE_ID } from 'utils/constants' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const MinterInstantiatePage: NextPage = () => { + const wallet = useWallet() + const contract = useContracts().minter + + const [startDate, setStartDate] = useState(undefined) + + const nameState = useInputState({ + id: 'name', + name: 'name', + title: 'Name', + placeholder: 'My Awesome SG721 Contract', + subtitle: 'Name of the sg721 contract', + }) + + const symbolState = useInputState({ + id: 'symbol', + name: 'symbol', + title: 'Symbol', + placeholder: 'AWSM', + subtitle: 'Symbol of the sg721 contract', + }) + + const minterState = useInputState({ + id: 'minter-address', + name: 'minterAddress', + title: 'Minter Address', + placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', + subtitle: 'Address that has the permissions to mint on sg721 contract', + }) + + const codeIdState = useNumberInputState({ + id: 'code-id', + name: 'code-id', + title: 'Code ID', + subtitle: 'Code ID for the sg721 contract', + placeholder: '1', + }) + + const creatorState = useInputState({ + id: 'creator-address', + name: 'creatorAddress', + title: 'Creator Address', + placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', + subtitle: 'Address of the collection creator', + }) + + const descriptionState = useInputState({ + id: 'description', + name: 'description', + title: 'Description', + subtitle: 'Description of the collection', + }) + + const imageState = useInputState({ + id: 'image', + name: 'image', + title: 'Image', + subtitle: 'Image of the collection', + placeholder: 'ipfs://bafybe....', + }) + + const externalLinkState = useInputState({ + id: 'external-link', + name: 'externalLink', + title: 'External Link', + subtitle: 'External link to the collection', + }) + + const royaltyPaymentAddressState = useInputState({ + id: 'royalty-payment-address', + name: 'royaltyPaymentAddress', + title: 'Payment Address', + subtitle: 'Address to receive royalties', + placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', + }) + + const royaltyShareState = useNumberInputState({ + id: 'royalty-share', + name: 'royaltyShare', + title: 'Share Percentage', + subtitle: 'Percentage of royalties to be paid', + placeholder: '8', + }) + + const unitPriceState = useNumberInputState({ + id: 'unit-price', + name: 'unitPrice', + title: 'Unit Price', + subtitle: 'Price of each tokens in collection', + placeholder: '500', + }) + + const baseTokenUriState = useInputState({ + id: 'base-token-uri', + name: 'baseTokenUri', + title: 'Base Token URI', + subtitle: 'IPFS uri for the tokens', + placeholder: 'ipfs://bafybe....', + }) + + const tokenNumberState = useNumberInputState({ + id: 'token-number', + name: 'tokenNumber', + title: 'Token Amount', + subtitle: 'Number of tokens in collection', + placeholder: '1000', + }) + + const perAddressLimitState = useNumberInputState({ + id: 'per-address-limit', + name: 'perAddressLimit', + title: 'Per Address Limit', + subtitle: 'Limit of tokens per address', + placeholder: '5', + }) + + const whitelistAddressState = useInputState({ + id: 'whitelist-address', + name: 'whitelistAddress', + title: 'Whitelist Address', + subtitle: 'Address to whitelist contract', + placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', + }) + + const { data, isLoading, mutate } = useMutation( + async (event: FormEvent): Promise => { + event.preventDefault() + if (!contract) { + throw new Error('Smart contract connection failed') + } + + let royaltyInfo = null + if (royaltyPaymentAddressState.value && royaltyShareState.value) { + royaltyInfo = { + paymentAddress: royaltyPaymentAddressState.value, + share: royaltyShareState.value, + } + } + + if (tokenNumberState.value < 1 || tokenNumberState.value > 10000) { + throw new Error('Token amount must be between 1 and 10000') + } + + if (perAddressLimitState.value < 1 || perAddressLimitState.value > 50) { + throw new Error('Per address limit must be between 1 and 50') + } + + if (Number(unitPriceState.value) < 500) { + throw new Error('Unit price must be greater than 500 STARS') + } + + if (!startDate) { + throw new Error('Start date is required') + } + + const msg = { + base_token_uri: baseTokenUriState.value, + num_tokens: tokenNumberState.value, + sg721_code_id: codeIdState.value, + sg721_instantiate_msg: { + name: nameState.value, + symbol: symbolState.value, + minter: minterState.value, + collection_info: { + creator: creatorState.value, + description: descriptionState.value, + image: imageState.value, + external_link: externalLinkState.value || null, + royalty_info: royaltyInfo, + }, + }, + per_address_limit: perAddressLimitState.value, + unit_price: coin(String(Number(unitPriceState.value) * 1000000), 'ustars'), + whitelist_address: whitelistAddressState.value || null, + start_time: (startDate.getTime() * 1_000_000).toString(), + } + return toast.promise(contract.instantiate(MINTER_CODE_ID, msg, 'Stargaze Minter Contract', wallet.address), { + loading: 'Instantiating contract...', + error: 'Instantiation failed!', + success: 'Instantiation success!', + }) + }, + { + onError: (error) => { + toast.error(String(error)) + }, + }, + ) + + const txHash = data?.transactionHash + + return ( +
+ + + + + + + Instantiate success! Here is the transaction result containing the contract address and the transaction + hash. + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + setStartDate(date)} value={startDate} /> + + + + +
+
+ +
+ + ) +} + +export default withMetadata(MinterInstantiatePage, { center: false }) diff --git a/pages/contracts/minter/query.tsx b/pages/contracts/minter/query.tsx new file mode 100644 index 0000000..b347148 --- /dev/null +++ b/pages/contracts/minter/query.tsx @@ -0,0 +1,120 @@ +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 { minterLinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { QueryType } from 'contracts/minter/messages/query' +import { dispatchQuery, QUERY_LIST } from 'contracts/minter/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 { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const MinterQueryPage: NextPage = () => { + const { minter: contract } = useContracts() + const wallet = useWallet() + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Minter Address', + subtitle: 'Address of the Minter contract', + }) + const contractAddress = contractState.value + + const addressState = useInputState({ + id: 'address', + name: 'address', + title: 'Address', + subtitle: 'Address of the user - defaults to current address', + }) + const address = addressState.value + + const [type, setType] = useState('config') + + const { data: response } = useQuery( + [contractAddress, type, contract, wallet, address] as const, + async ({ queryKey }) => { + const [_contractAddress, _type, _contract, _wallet] = queryKey + const messages = contract?.use(_contractAddress) + const result = await dispatchQuery({ + address, + messages, + type, + }) + return result + }, + { + placeholderData: null, + onError: (error: any) => { + toast.error(error.message) + }, + 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(MinterQueryPage, { center: false }) diff --git a/pages/contracts/sg721/execute.tsx b/pages/contracts/sg721/execute.tsx new file mode 100644 index 0000000..f0f0558 --- /dev/null +++ b/pages/contracts/sg721/execute.tsx @@ -0,0 +1,156 @@ +import { Button } from 'components/Button' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { ExecuteCombobox } from 'components/contracts/sg721/ExecuteCombobox' +import { useExecuteComboboxState } from 'components/contracts/sg721/ExecuteCombobox.hooks' +import { FormControl } from 'components/FormControl' +import { AddressInput, TextInput } from 'components/forms/FormInput' +import { useInputState } from 'components/forms/FormInput.hooks' +import { JsonTextArea } from 'components/forms/FormTextArea' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { sg721LinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import type { DispatchExecuteArgs } from 'contracts/sg721/messages/execute' +import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/sg721/messages/execute' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import type { FormEvent } from 'react' +import { useMemo, useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaArrowRight } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { parseJson } from 'utils/json' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const Sg721ExecutePage: NextPage = () => { + const { sg721: contract } = useContracts() + const [lastTx, setLastTx] = useState('') + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + + const tokenIdState = useInputState({ + id: 'token-id', + name: 'tokenId', + title: 'Token ID', + subtitle: 'Enter the token ID', + placeholder: '1', + }) + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Minter Address', + subtitle: 'Address of the Minter contract', + }) + + const messageState = useInputState({ + id: 'message', + name: 'message', + title: 'Message', + subtitle: 'Message to execute on the contract', + defaultValue: JSON.stringify({ key: 'value' }, null, 2), + }) + + const recipientState = useInputState({ + id: 'recipient-address', + name: 'recipient', + title: 'Recipient Address', + subtitle: 'Address of the recipient', + }) + + const operatorState = useInputState({ + id: 'operator-address', + name: 'operator', + title: 'Operator Address', + subtitle: 'Address of the operator', + }) + + const tokenURIState = useInputState({ + id: 'token-uri', + name: 'tokenURI', + title: 'Token URI', + subtitle: 'URI for the token', + placeholder: 'ipfs://xyz...', + }) + + const showTokenIdField = isEitherType(type, ['transfer_nft', 'send_nft', 'approve', 'revoke', 'mint', 'burn']) + const showRecipientField = isEitherType(type, ['transfer_nft', 'send_nft', 'approve', 'revoke', 'mint']) + const showOperatorField = isEitherType(type, ['approve_all', 'revoke_all']) + const showMessageField = type === 'send_nft' + const showTokenURIField = type === 'mint' + + const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value]) + const payload: DispatchExecuteArgs = { + contract: contractState.value, + tokenId: tokenIdState.value, + messages, + recipient: recipientState.value, + operator: operatorState.value, + type, + tokenURI: tokenURIState.value, + msg: parseJson(messageState.value) || {}, + } + const { isLoading, mutate } = useMutation( + async (event: FormEvent) => { + event.preventDefault() + if (!type) { + throw new Error('Please select message 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) + } + }, + { + onError: (error) => { + toast.error(String(error)) + }, + }, + ) + + return ( +
+ + + + +
+
+ + + {showRecipientField && } + {showOperatorField && } + {showTokenIdField && } + {showTokenURIField && } + {showMessageField && } +
+
+
+ + + + +
+ + + +
+
+
+ ) +} + +export default withMetadata(Sg721ExecutePage, { center: false }) diff --git a/pages/contracts/sg721/index.tsx b/pages/contracts/sg721/index.tsx new file mode 100644 index 0000000..561b4b3 --- /dev/null +++ b/pages/contracts/sg721/index.tsx @@ -0,0 +1 @@ +export { default } from './instantiate' diff --git a/pages/contracts/sg721/instantiate.tsx b/pages/contracts/sg721/instantiate.tsx new file mode 100644 index 0000000..f02483e --- /dev/null +++ b/pages/contracts/sg721/instantiate.tsx @@ -0,0 +1,186 @@ +import { Alert } from 'components/Alert' +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { FormGroup } from 'components/FormGroup' +import { NumberInput, TextInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { FormTextArea } from 'components/forms/FormTextArea' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { sg721LinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { InstantiateResponse } from 'contracts/sg721' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import type { FormEvent } from 'react' +import { toast } from 'react-hot-toast' +import { FaAsterisk } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { SG721_CODE_ID } from 'utils/constants' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const Sg721InstantiatePage: NextPage = () => { + const wallet = useWallet() + const contract = useContracts().sg721 + + const nameState = useInputState({ + id: 'name', + name: 'name', + title: 'Name', + placeholder: 'My Awesome SG721 Contract', + subtitle: 'Name of the sg721 contract', + }) + + const symbolState = useInputState({ + id: 'symbol', + name: 'symbol', + title: 'Symbol', + placeholder: 'AWSM', + subtitle: 'Symbol of the sg721 contract', + }) + + const minterState = useInputState({ + id: 'minter-address', + name: 'minterAddress', + title: 'Minter Address', + placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', + subtitle: 'Address that has the permissions to mint on sg721 contract', + }) + + const creatorState = useInputState({ + id: 'creator-address', + name: 'creatorAddress', + title: 'Creator Address', + placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', + subtitle: 'Address of the collection creator', + }) + + const descriptionState = useInputState({ + id: 'description', + name: 'description', + title: 'Description', + subtitle: 'Description of the collection', + }) + + const imageState = useInputState({ + id: 'image', + name: 'image', + title: 'Image', + subtitle: 'Image of the collection', + placeholder: 'ipfs://bafybe....', + }) + + const externalLinkState = useInputState({ + id: 'external-link', + name: 'externalLink', + title: 'External Link', + subtitle: 'External link to the collection', + }) + + const royaltyPaymentAddressState = useInputState({ + id: 'royalty-payment-address', + name: 'royaltyPaymentAddress', + title: 'Payment Address', + subtitle: 'Address to receive royalties', + placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', + }) + + const royaltyShareState = useNumberInputState({ + id: 'royalty-share', + name: 'royaltyShare', + title: 'Share Percentage', + subtitle: 'Percentage of royalties to be paid', + placeholder: '8', + }) + + const { data, isLoading, mutate } = useMutation( + async (event: FormEvent): Promise => { + event.preventDefault() + if (!contract) { + throw new Error('Smart contract connection failed') + } + + let royaltyInfo = null + if (royaltyPaymentAddressState.value && royaltyShareState.value) { + royaltyInfo = { + paymentAddress: royaltyPaymentAddressState.value, + share: royaltyShareState.value, + } + } + + const msg = { + name: nameState.value, + symbol: symbolState.value, + minter: minterState.value, + collection_info: { + creator: creatorState.value, + description: descriptionState.value, + image: imageState.value, + external_link: externalLinkState.value || null, + royalty_info: royaltyInfo, + }, + } + return toast.promise(contract.instantiate(SG721_CODE_ID, msg, 'Stargaze Sg721 Contract', wallet.address), { + loading: 'Instantiating contract...', + error: 'Instantiation failed!', + success: 'Instantiation success!', + }) + }, + { + onError: (error) => { + toast.error(String(error)) + }, + }, + ) + + return ( +
+ + + + + + + Instantiate success! Here is the transaction result containing the contract address and the transaction + hash. + + +
+
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+ + ) +} + +export default withMetadata(Sg721InstantiatePage, { center: false }) diff --git a/pages/contracts/sg721/query.tsx b/pages/contracts/sg721/query.tsx new file mode 100644 index 0000000..05138f8 --- /dev/null +++ b/pages/contracts/sg721/query.tsx @@ -0,0 +1,135 @@ +import clsx from 'clsx' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { FormControl } from 'components/FormControl' +import { AddressInput, TextInput } from 'components/forms/FormInput' +import { useInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { sg721LinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { QueryType } from 'contracts/sg721/messages/query' +import { dispatchQuery, QUERY_LIST } from 'contracts/sg721/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 { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const Sg721QueryPage: NextPage = () => { + const { sg721: contract } = useContracts() + const wallet = useWallet() + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Sg721 Address', + subtitle: 'Address of the Sg721 contract', + }) + const contractAddress = contractState.value + + const addressState = useInputState({ + id: 'address', + name: 'address', + title: 'Address', + subtitle: 'Address of the user - defaults to current address', + }) + const address = addressState.value + + const tokenIdState = useInputState({ + id: 'token-id', + name: 'tokenId', + title: 'Token ID', + subtitle: 'Token ID of a given token', + }) + const tokenId = tokenIdState.value + + const [type, setType] = useState('owner_of') + + const addressVisible = ['approval', 'all_operators', 'tokens'].includes(type) + const tokenIdVisible = ['owner_of', 'approval', 'approvals', 'nft_info', 'all_nft_info'].includes(type) + + const { data: response } = useQuery( + [contractAddress, type, contract, wallet, tokenId, address] as const, + async ({ queryKey }) => { + const [_contractAddress, _type, _contract, _wallet, _tokenId, _address] = queryKey + const messages = contract?.use(contractAddress) + const result = await dispatchQuery({ + messages, + type, + tokenId: _tokenId, + address: _address, + }) + return result + }, + { + placeholderData: null, + onError: (error: any) => { + toast.error(error.message) + }, + 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(Sg721QueryPage, { center: false }) diff --git a/pages/contracts/whitelist/execute.tsx b/pages/contracts/whitelist/execute.tsx new file mode 100644 index 0000000..b182097 --- /dev/null +++ b/pages/contracts/whitelist/execute.tsx @@ -0,0 +1,162 @@ +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { ExecuteCombobox } from 'components/contracts/whitelist/ExecuteCombobox' +import { useExecuteComboboxState } from 'components/contracts/whitelist/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' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { whitelistLinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import type { DispatchExecuteArgs } from 'contracts/whitelist/messages/execute' +import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/whitelist/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 { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const WhitelistExecutePage: NextPage = () => { + const { whitelist: contract } = useContracts() + const [lastTx, setLastTx] = useState('') + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + + const [timestamp, setTimestamp] = useState() + + const addressListState = useAddressListState() + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Whitelist Address', + subtitle: 'Address of the Whitelist contract', + }) + const contractAddress = contractState.value + + const limitState = useNumberInputState({ + id: 'limit', + name: 'limit', + title: 'Limit', + subtitle: 'Limit value', + placeholder: '5', + }) + + const showLimitState = isEitherType(type, ['update_per_address_limit', 'increase_member_limit']) + const showTimestamp = isEitherType(type, ['update_start_time', 'update_end_time']) + const showMemberList = isEitherType(type, ['add_members', 'remove_members']) + + const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value]) + const payload: DispatchExecuteArgs = { + contract: contractState.value, + messages, + type, + limit: limitState.value, + timestamp: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', + members: addressListState.values.map((a) => a.address), + } + const { isLoading, mutate } = useMutation( + async (event: FormEvent) => { + event.preventDefault() + if (!type) { + throw new Error('Please select message 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) + } + }, + { + onError: (error) => { + toast.error(String(error)) + }, + }, + ) + + 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 ( +
+ + + + +
+
+ + + + + + + + setTimestamp(date)} value={timestamp} /> + + + + + +
+
+
+ + + + +
+ + + +
+
+
+ ) +} + +export default withMetadata(WhitelistExecutePage, { center: false }) diff --git a/pages/contracts/whitelist/index.tsx b/pages/contracts/whitelist/index.tsx new file mode 100644 index 0000000..561b4b3 --- /dev/null +++ b/pages/contracts/whitelist/index.tsx @@ -0,0 +1 @@ +export { default } from './instantiate' diff --git a/pages/contracts/whitelist/instantiate.tsx b/pages/contracts/whitelist/instantiate.tsx new file mode 100644 index 0000000..39b94f7 --- /dev/null +++ b/pages/contracts/whitelist/instantiate.tsx @@ -0,0 +1,153 @@ +import { coin } from '@cosmjs/proto-signing' +import { Alert } from 'components/Alert' +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { FormControl } from 'components/FormControl' +import { FormGroup } from 'components/FormGroup' +import { AddressList } from 'components/forms/AddressList' +import { useAddressListState } from 'components/forms/AddressList.hooks' +import { NumberInput } from 'components/forms/FormInput' +import { useNumberInputState } from 'components/forms/FormInput.hooks' +import { InputDateTime } from 'components/InputDateTime' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { whitelistLinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { InstantiateResponse } from 'contracts/sg721' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import { type FormEvent, useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaAsterisk } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { WHITELIST_CODE_ID } from 'utils/constants' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const Sg721InstantiatePage: NextPage = () => { + const wallet = useWallet() + const { whitelist: contract } = useContracts() + + const [startDate, setStartDate] = useState(undefined) + const [endDate, setEndDate] = useState(undefined) + + const addressListState = useAddressListState() + + const unitPriceState = useNumberInputState({ + id: 'unit-price', + name: 'unitPrice', + title: 'Unit Price', + subtitle: 'Price of each tokens in collection', + placeholder: '500', + }) + + const memberLimitState = useNumberInputState({ + id: 'member-limit', + name: 'memberLimit', + title: 'Member Limit', + subtitle: 'Limit of the whitelisted members', + placeholder: '1000', + }) + + const perAddressLimitState = useNumberInputState({ + id: 'per-address-limit', + name: 'perAddressLimit', + title: 'Per Address Limit', + subtitle: 'Limit of tokens per address', + placeholder: '5', + }) + + const { data, isLoading, mutate } = useMutation( + async (event: FormEvent): Promise => { + event.preventDefault() + if (!contract) { + throw new Error('Smart contract connection failed') + } + + if (!startDate) { + throw new Error('Start date is required') + } + if (!endDate) { + throw new Error('End date is required') + } + + const msg = { + members: addressListState.values.map((a) => a.address), + start_time: (startDate.getTime() * 1_000_000).toString(), + end_time: (endDate.getTime() * 1_000_000).toString(), + unit_price: coin(String(Number(unitPriceState.value) * 1000000), 'ustars'), + per_address_limit: perAddressLimitState.value, + member_limit: memberLimitState.value, + } + return toast.promise( + contract.instantiate(WHITELIST_CODE_ID, msg, 'Stargaze Whitelist Contract', wallet.address), + { + loading: 'Instantiating contract...', + error: 'Instantiation failed!', + success: 'Instantiation success!', + }, + ) + }, + { + onError: (error) => { + toast.error(String(error)) + }, + }, + ) + + return ( +
+ + + + + + + Instantiate success! Here is the transaction result containing the contract address and the transaction + hash. + + +
+
+ + + + + + + + + + + setStartDate(date)} value={startDate} /> + + + setEndDate(date)} value={endDate} /> + + + +
+
+ +
+ + ) +} + +export default withMetadata(Sg721InstantiatePage, { center: false }) diff --git a/pages/contracts/whitelist/query.tsx b/pages/contracts/whitelist/query.tsx new file mode 100644 index 0000000..4bc28b0 --- /dev/null +++ b/pages/contracts/whitelist/query.tsx @@ -0,0 +1,122 @@ +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 { whitelistLinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { QueryType } from 'contracts/whitelist/messages/query' +import { dispatchQuery, QUERY_LIST } from 'contracts/whitelist/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 { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const WhitelistQueryPage: NextPage = () => { + const { whitelist: contract } = useContracts() + const wallet = useWallet() + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Whitelist Address', + subtitle: 'Address of the Whitelist contract', + }) + const contractAddress = contractState.value + + const addressState = useInputState({ + id: 'address', + name: 'address', + title: 'Address', + subtitle: 'Address of the user - defaults to current address', + }) + const address = addressState.value + + const [type, setType] = useState('has_started') + + const addressVisible = type === 'has_member' + + const { data: response } = useQuery( + [contractAddress, type, contract, wallet, address] as const, + async ({ queryKey }) => { + const [_contractAddress, _type, _contract, _wallet, _address] = queryKey + const messages = contract?.use(contractAddress) + const result = await dispatchQuery({ + messages, + type, + address: _address, + }) + return result + }, + { + placeholderData: null, + onError: (error: any) => { + toast.error(error.message) + }, + 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(WhitelistQueryPage, { center: false }) diff --git a/utils/constants.ts b/utils/constants.ts index 5396cb1..858b239 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,13 +1,9 @@ -export const CW721_BASE_CODE_ID = parseInt(process.env.NEXT_PUBLIC_CW721_BASE_CODE_ID, 10) +export const SG721_CODE_ID = parseInt(process.env.NEXT_PUBLIC_SG721_CODE_ID, 10) +export const MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_MINTER_CODE_ID, 10) +export const WHITELIST_CODE_ID = parseInt(process.env.NEXT_PUBLIC_WHITELIST_CODE_ID, 10) export const NETWORK = process.env.NEXT_PUBLIC_NETWORK -export const S3_ENDPOINT = process.env.NEXT_PUBLIC_S3_ENDPOINT -export const S3_REGION = process.env.NEXT_PUBLIC_S3_REGION -export const S3_KEY = process.env.NEXT_PUBLIC_S3_KEY -export const S3_SECRET = process.env.NEXT_PUBLIC_S3_SECRET -export const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET - export const BLOCK_EXPLORER_URL = process.env.NEXT_PUBLIC_BLOCK_EXPLORER_URL export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL diff --git a/utils/links.ts b/utils/links.ts index 3a4a87a..f4656ec 100644 --- a/utils/links.ts +++ b/utils/links.ts @@ -13,16 +13,13 @@ export const links = { Telegram: `https://t.me/joinchat/ZQ95YmIn3AI0ODFh`, Twitter: `https://twitter.com/stargazezone`, Explorer: BLOCK_EXPLORER_URL, - - // reference links - 'Docs Create Collection': ``, - 'Docs CW721 Base': ``, + Documentation: 'https://docs.stargaze.zone/guides/readme', } export const footerLinks = [ { text: 'Block Explorer', href: links.Explorer }, { text: 'Documentation', href: links.Docs }, - { text: 'Submit an issue', href: `${links.GitHub}/issues/new/choose` }, + { text: 'Submit an issue', href: `${links.GitHub}/issues/new` }, { text: 'Powered by Stargaze', href: links.Stargaze }, ]