From 6914a402581eedb7a9595fa6ac84462070146e3c Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sat, 18 Mar 2023 20:47:39 +0300 Subject: [PATCH 01/18] Init splits contract helpers --- contracts/splits/contract.ts | 165 +++++++++++++++++++++++++++ contracts/splits/index.ts | 2 + contracts/splits/messages/execute.ts | 77 +++++++++++++ contracts/splits/messages/query.ts | 43 +++++++ contracts/splits/useContract.ts | 76 ++++++++++++ 5 files changed, 363 insertions(+) create mode 100644 contracts/splits/contract.ts create mode 100644 contracts/splits/index.ts create mode 100644 contracts/splits/messages/execute.ts create mode 100644 contracts/splits/messages/query.ts create mode 100644 contracts/splits/useContract.ts diff --git a/contracts/splits/contract.ts b/contracts/splits/contract.ts new file mode 100644 index 0000000..f62a476 --- /dev/null +++ b/contracts/splits/contract.ts @@ -0,0 +1,165 @@ +import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import type { Coin } from '@cosmjs/proto-signing' + +export interface InstantiateResponse { + readonly contractAddress: string + readonly transactionHash: string +} + +export interface SplitsInstance { + readonly contractAddress: string + //Query + getAdmin: () => Promise + getMemberWeight: (member: string) => Promise + listMembers: (startAfter?: string, limit?: number) => Promise + getGroup: () => Promise + + //Execute + updateAdmin: (admin: string) => Promise + distribute: () => Promise +} + +export interface SplitsMessages { + updateAdmin: (admin: string) => UpdateAdminMessage + distribute: () => DistributeMessage +} + +export interface UpdateAdminMessage { + sender: string + contract: string + msg: { + update_admin: { admin: string } + } + funds: Coin[] +} + +export interface DistributeMessage { + sender: string + contract: string + msg: { distribute: Record } + funds: Coin[] +} + +export interface SplitsContract { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ) => Promise + + use: (contractAddress: string) => SplitsInstance + + messages: (contractAddress: string) => SplitsMessages +} + +export const Splits = (client: SigningCosmWasmClient, txSigner: string): SplitsContract => { + const use = (contractAddress: string): SplitsInstance => { + ///QUERY + const listMembers = async (startAfter?: string, limit?: number): Promise => { + return client.queryContractSmart(contractAddress, { + list_members: { limit, start_after: startAfter }, + }) + } + + const getMemberWeight = async (address: string): Promise => { + return client.queryContractSmart(contractAddress, { + member: { address }, + }) + } + + const getAdmin = async (): Promise => { + return client.queryContractSmart(contractAddress, { + admin: {}, + }) + } + + const getGroup = async (): Promise => { + return client.queryContractSmart(contractAddress, { + group: {}, + }) + } + /// EXECUTE + const updateAdmin = async (admin: string): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + update_admin: { + admin, + }, + }, + 'auto', + ) + return res.transactionHash + } + + const distribute = async (): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + distribute: {}, + }, + 'auto', + ) + return res.transactionHash + } + return { + contractAddress, + updateAdmin, + distribute, + getMemberWeight, + getAdmin, + listMembers, + getGroup, + } + } + + const instantiate = async ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ): Promise => { + const result = await client.instantiate(txSigner, codeId, initMsg, label, 'auto', { + admin, + }) + + return { + contractAddress: result.contractAddress, + transactionHash: result.transactionHash, + } + } + + const messages = (contractAddress: string) => { + const updateAdmin = (admin: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_admin: { admin }, + }, + funds: [], + } + } + + const distribute = () => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + distribute: {}, + }, + funds: [], + } + } + + return { + updateAdmin, + distribute, + } + } + + return { use, instantiate, messages } +} diff --git a/contracts/splits/index.ts b/contracts/splits/index.ts new file mode 100644 index 0000000..6dc6461 --- /dev/null +++ b/contracts/splits/index.ts @@ -0,0 +1,2 @@ +export * from './contract' +export * from './useContract' diff --git a/contracts/splits/messages/execute.ts b/contracts/splits/messages/execute.ts new file mode 100644 index 0000000..9ee2c96 --- /dev/null +++ b/contracts/splits/messages/execute.ts @@ -0,0 +1,77 @@ +import type { SplitsInstance } from '../index' +import { useSplitsContract } from '../index' + +export type ExecuteType = typeof EXECUTE_TYPES[number] + +export const EXECUTE_TYPES = ['update_admin', 'distribute'] as const + +export interface ExecuteListItem { + id: ExecuteType + name: string + description?: string +} + +export const EXECUTE_LIST: ExecuteListItem[] = [ + { + id: 'update_admin', + name: 'Update Admin', + description: `Update the splits contract admin`, + }, + { + id: 'distribute', + name: 'Distribute', + description: `Distribute the revenue to the group members`, + }, +] + +export interface DispatchExecuteProps { + type: ExecuteType + [k: string]: unknown +} + +type Select = T + +/** @see {@link SplitsInstance} */ +export type DispatchExecuteArgs = { + contract: string + messages?: SplitsInstance +} & ({ type: Select<'update_admin'>; admin: string } | { type: Select<'distribute'> }) + +export const dispatchExecute = async (args: DispatchExecuteArgs) => { + const { messages } = args + if (!messages) { + throw new Error('Cannot dispatch execute, messages are not defined') + } + switch (args.type) { + case 'update_admin': { + return messages.updateAdmin(args.admin) + } + case 'distribute': { + return messages.distribute() + } + default: { + throw new Error('Unknown execution type') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages } = useSplitsContract() + const { contract } = args + switch (args.type) { + case 'update_admin': { + return messages(contract)?.updateAdmin(args.admin) + } + case 'distribute': { + return messages(contract)?.distribute() + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} diff --git a/contracts/splits/messages/query.ts b/contracts/splits/messages/query.ts new file mode 100644 index 0000000..6a6b564 --- /dev/null +++ b/contracts/splits/messages/query.ts @@ -0,0 +1,43 @@ +import type { SplitsInstance } from '../contract' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = ['admin', 'group', 'member', 'list_members'] as const + +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const QUERY_LIST: QueryListItem[] = [ + { id: 'admin', name: 'Query Admin', description: 'View the splits contract admin' }, + { id: 'member', name: 'Query Member Weight', description: 'Check the weight of a member in the group' }, + { id: 'list_members', name: 'Query Members', description: 'View the group members' }, + { id: 'group', name: 'Query Group Contract Address', description: 'View the group contract address' }, +] + +export interface DispatchQueryProps { + messages: SplitsInstance | undefined + type: QueryType + address: string + startAfter: string + limit: number +} + +export const dispatchQuery = (props: DispatchQueryProps) => { + const { messages, type, address, startAfter, limit } = props + switch (type) { + case 'list_members': + return messages?.listMembers(startAfter, limit) + case 'admin': + return messages?.getAdmin() + case 'member': + return messages?.getMemberWeight(address) + case 'group': + return messages?.getGroup() + default: { + throw new Error('unknown query type') + } + } +} diff --git a/contracts/splits/useContract.ts b/contracts/splits/useContract.ts new file mode 100644 index 0000000..70ae0f9 --- /dev/null +++ b/contracts/splits/useContract.ts @@ -0,0 +1,76 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ + +import { useWallet } from 'contexts/wallet' +import { useCallback, useEffect, useState } from 'react' + +import type { InstantiateResponse, SplitsContract, SplitsInstance, SplitsMessages } from './contract' +import { Splits as initContract } from './contract' + +export interface UseSplitsContractProps { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ) => Promise + + use: (customAddress?: string) => SplitsInstance | undefined + + updateContractAddress: (contractAddress: string) => void + + messages: (contractAddress: string) => SplitsMessages | undefined +} + +export function useSplitsContract(): UseSplitsContractProps { + const wallet = useWallet() + + const [address, setAddress] = useState('') + const [splits, setSplits] = useState() + + useEffect(() => { + setAddress(localStorage.getItem('contract_address') || '') + }, []) + + useEffect(() => { + const splitsContract = initContract(wallet.getClient(), wallet.address) + setSplits(splitsContract) + }, [wallet]) + + const updateContractAddress = (contractAddress: string) => { + setAddress(contractAddress) + } + + const instantiate = useCallback( + (codeId: number, initMsg: Record, label: string, admin?: string): Promise => { + return new Promise((resolve, reject) => { + if (!splits) { + reject(new Error('Contract is not initialized.')) + return + } + splits.instantiate(codeId, initMsg, label, admin).then(resolve).catch(reject) + }) + }, + [splits], + ) + + const use = useCallback( + (customAddress = ''): SplitsInstance | undefined => { + return splits?.use(address || customAddress) + }, + [splits, address], + ) + + const messages = useCallback( + (customAddress = ''): SplitsMessages | undefined => { + return splits?.messages(address || customAddress) + }, + [splits, address], + ) + + return { + instantiate, + use, + updateContractAddress, + messages, + } +} From 51af711d9b3ff6127c9f294d31f2ba768fcb5aca Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 09:55:18 +0300 Subject: [PATCH 02/18] Update link tabs for splits contract --- components/LinkTabs.data.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/components/LinkTabs.data.ts b/components/LinkTabs.data.ts index 4fb6fd4..40e67ba 100644 --- a/components/LinkTabs.data.ts +++ b/components/LinkTabs.data.ts @@ -26,7 +26,7 @@ export const vendingMinterLinkTabs: LinkTabProps[] = [ }, { title: 'Query', - description: `Dispatch queries with your Vending Minter contract`, + description: `Dispatch queries for your Vending Minter contract`, href: '/contracts/vendingMinter/query', }, { @@ -49,7 +49,7 @@ export const baseMinterLinkTabs: LinkTabProps[] = [ }, { title: 'Query', - description: `Dispatch queries with your Base Minter contract`, + description: `Dispatch queries for your Base Minter contract`, href: '/contracts/baseMinter/query', }, { @@ -72,7 +72,7 @@ export const whitelistLinkTabs: LinkTabProps[] = [ }, { title: 'Query', - description: `Dispatch queries with your Whitelist contract`, + description: `Dispatch queries for your Whitelist contract`, href: '/contracts/whitelist/query', }, { @@ -104,3 +104,26 @@ export const badgeHubLinkTabs: LinkTabProps[] = [ href: '/contracts/badgeHub/migrate', }, ] + +export const splitsLinkTabs: LinkTabProps[] = [ + { + title: 'Instantiate', + description: `Initialize a new Splits contract`, + href: '/contracts/splits/instantiate', + }, + { + title: 'Query', + description: `Dispatch queries for your Splits contract`, + href: '/contracts/splits/query', + }, + { + title: 'Execute', + description: `Execute Splits contract actions`, + href: '/contracts/splits/execute', + }, + { + title: 'Migrate', + description: `Migrate Splits contract`, + href: '/contracts/splits/migrate', + }, +] From 2dd56b1ddd85adaf962c85366750a5521612671b Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 09:56:41 +0300 Subject: [PATCH 03/18] Update actions types for splits --- .../contracts/splits/ExecuteCombobox.hooks.ts | 7 ++ .../contracts/splits/ExecuteCombobox.tsx | 92 +++++++++++++++++++ contracts/splits/messages/execute.ts | 2 +- 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 components/contracts/splits/ExecuteCombobox.hooks.ts create mode 100644 components/contracts/splits/ExecuteCombobox.tsx diff --git a/components/contracts/splits/ExecuteCombobox.hooks.ts b/components/contracts/splits/ExecuteCombobox.hooks.ts new file mode 100644 index 0000000..530deed --- /dev/null +++ b/components/contracts/splits/ExecuteCombobox.hooks.ts @@ -0,0 +1,7 @@ +import type { ExecuteListItem } from 'contracts/splits/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/splits/ExecuteCombobox.tsx b/components/contracts/splits/ExecuteCombobox.tsx new file mode 100644 index 0000000..ce29378 --- /dev/null +++ b/components/contracts/splits/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/splits/messages/execute' +import { EXECUTE_LIST } from 'contracts/splits/messages/execute' +import { matchSorter } from 'match-sorter' +import { Fragment, useState } from 'react' +import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' + +export interface ExecuteComboboxProps { + value: ExecuteListItem | null + onChange: (item: ExecuteListItem) => void +} + +export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => { + const [search, setSearch] = useState('') + + const filtered = + search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] }) + + return ( + +
+ val?.name ?? ''} + id="message-type" + onChange={(event) => setSearch(event.target.value)} + placeholder="Select message type" + /> + + + {({ open }) => + + setSearch('')} as={Fragment}> + + {filtered.length < 1 && ( + + Message type not found. + + )} + {filtered.map((entry) => ( + + clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
+ + {value && ( +
+
+ +
+ {value.description} +
+ )} +
+ ) +} diff --git a/contracts/splits/messages/execute.ts b/contracts/splits/messages/execute.ts index 9ee2c96..fc21469 100644 --- a/contracts/splits/messages/execute.ts +++ b/contracts/splits/messages/execute.ts @@ -35,7 +35,7 @@ type Select = T export type DispatchExecuteArgs = { contract: string messages?: SplitsInstance -} & ({ type: Select<'update_admin'>; admin: string } | { type: Select<'distribute'> }) +} & ({ type: Select<'update_admin'>; admin: string } | { type: Select<'distribute'> | undefined }) export const dispatchExecute = async (args: DispatchExecuteArgs) => { const { messages } = args From c375e94bf84344a3af67d9fa414cc3b366c44ea8 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 09:57:42 +0300 Subject: [PATCH 04/18] Update contracts context to include splits contract --- contexts/contracts.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contexts/contracts.tsx b/contexts/contracts.tsx index 4df8488..bfcabeb 100644 --- a/contexts/contracts.tsx +++ b/contexts/contracts.tsx @@ -17,6 +17,9 @@ import { Fragment, useEffect } from 'react' import type { State } from 'zustand' import create from 'zustand' +import type { UseSplitsContractProps } from '../contracts/splits/useContract' +import { useSplitsContract } from '../contracts/splits/useContract' + /** * Contracts store type definitions */ @@ -28,6 +31,7 @@ export interface ContractsStore extends State { vendingFactory: UseVendingFactoryContractProps | null baseFactory: UseBaseFactoryContractProps | null badgeHub: UseBadgeHubContractProps | null + splits: UseSplitsContractProps | null } /** @@ -41,6 +45,7 @@ export const defaultValues: ContractsStore = { vendingFactory: null, baseFactory: null, badgeHub: null, + splits: null, } /** @@ -71,6 +76,7 @@ const ContractsSubscription: VFC = () => { const vendingFactory = useVendingFactoryContract() const baseFactory = useBaseFactoryContract() const badgeHub = useBadgeHubContract() + const splits = useSplitsContract() useEffect(() => { useContracts.setState({ @@ -81,8 +87,9 @@ const ContractsSubscription: VFC = () => { vendingFactory, baseFactory, badgeHub, + splits, }) - }, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub]) + }, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub, splits]) return null } From 8fed125b4c2667d17aca9f22faba63f2a59c8578 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 09:59:00 +0300 Subject: [PATCH 05/18] Init splits contract dashboard > Execute --- pages/contracts/splits/execute.tsx | 135 +++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 pages/contracts/splits/execute.tsx diff --git a/pages/contracts/splits/execute.tsx b/pages/contracts/splits/execute.tsx new file mode 100644 index 0000000..c0952ed --- /dev/null +++ b/pages/contracts/splits/execute.tsx @@ -0,0 +1,135 @@ +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { ExecuteCombobox } from 'components/contracts/splits/ExecuteCombobox' +import { useExecuteComboboxState } from 'components/contracts/splits/ExecuteCombobox.hooks' +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 { splitsLinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { DispatchExecuteArgs } from 'contracts/splits/messages/execute' +import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/splits/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 SplitsExecutePage: NextPage = () => { + const { splits: contract } = useContracts() + const wallet = useWallet() + + const [lastTx, setLastTx] = useState('') + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Splits Address', + subtitle: 'Address of the Splits contract', + }) + const contractAddress = contractState.value + + const adminAddressState = useInputState({ + id: 'admin-address', + name: 'admin-address', + title: 'Admin Address', + subtitle: 'Address of the new administrator', + }) + + const showAdminAddress = isEitherType(type, ['update_admin']) + + const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value]) + const payload: DispatchExecuteArgs = { + contract: contractState.value, + messages, + type, + admin: adminAddressState.value.trim(), + } + const { isLoading, mutate } = useMutation( + async (event: FormEvent) => { + event.preventDefault() + if (!type) { + throw new Error('Please select message type!') + } + if (!wallet.initialized) { + throw new Error('Please connect your wallet.') + } + const txHash = await toast.promise(dispatchExecute(payload), { + error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, + loading: 'Executing message...', + success: (tx) => `Transaction ${tx} success!`, + }) + if (txHash) { + setLastTx(txHash) + } + }, + { + onError: (error) => { + toast.error(String(error), { style: { maxWidth: 'none' } }) + }, + }, + ) + + const router = useRouter() + + useEffect(() => { + if (contractAddress.length > 0) { + void router.replace({ query: { contractAddress } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contractAddress]) + useEffect(() => { + const initial = new URL(document.URL).searchParams.get('contractAddress') + if (initial && initial.length > 0) contractState.onChange(initial) + }, []) + + return ( +
+ + + + +
+
+ + + + + +
+
+
+ + + + +
+ + + +
+
+
+ ) +} + +export default withMetadata(SplitsExecutePage, { center: false }) From fddf22d91947308911934887a4217c2ffb2604d2 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 11:04:13 +0300 Subject: [PATCH 06/18] Init splits contract dashboard > Query --- contracts/splits/messages/query.ts | 4 +- pages/contracts/splits/index.tsx | 1 + pages/contracts/splits/query.tsx | 156 +++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 pages/contracts/splits/index.tsx create mode 100644 pages/contracts/splits/query.tsx diff --git a/contracts/splits/messages/query.ts b/contracts/splits/messages/query.ts index 6a6b564..8c316d5 100644 --- a/contracts/splits/messages/query.ts +++ b/contracts/splits/messages/query.ts @@ -11,9 +11,9 @@ export interface QueryListItem { } export const QUERY_LIST: QueryListItem[] = [ - { id: 'admin', name: 'Query Admin', description: 'View the splits contract admin' }, - { id: 'member', name: 'Query Member Weight', description: 'Check the weight of a member in the group' }, { id: 'list_members', name: 'Query Members', description: 'View the group members' }, + { id: 'member', name: 'Query Member Weight', description: 'Query the weight of a member in the group' }, + { id: 'admin', name: 'Query Admin', description: 'View the splits contract admin' }, { id: 'group', name: 'Query Group Contract Address', description: 'View the group contract address' }, ] diff --git a/pages/contracts/splits/index.tsx b/pages/contracts/splits/index.tsx new file mode 100644 index 0000000..561b4b3 --- /dev/null +++ b/pages/contracts/splits/index.tsx @@ -0,0 +1 @@ +export { default } from './instantiate' diff --git a/pages/contracts/splits/query.tsx b/pages/contracts/splits/query.tsx new file mode 100644 index 0000000..ab43ac0 --- /dev/null +++ b/pages/contracts/splits/query.tsx @@ -0,0 +1,156 @@ +import clsx from 'clsx' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput, TextInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { splitsLinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { QueryType } from 'contracts/splits/messages/query' +import { dispatchQuery, QUERY_LIST } from 'contracts/splits/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' +import { resolveAddress } from 'utils/resolveAddress' + +const SplitsQueryPage: NextPage = () => { + const { splits: contract } = useContracts() + const wallet = useWallet() + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Splits Address', + subtitle: 'Address of the Splits contract', + }) + const contractAddress = contractState.value + + const memberAddressState = useInputState({ + id: 'member-address', + name: 'member-address', + title: 'Member Address', + subtitle: 'Member address to query the weight for', + }) + + const memberAddress = memberAddressState.value + + const startAfterStringState = useInputState({ + id: 'start-after-string', + name: 'start-after-string', + title: 'Start After (optional)', + subtitle: 'The public key to start the pagination after', + }) + + const paginationLimitState = useNumberInputState({ + id: 'pagination-limit', + name: 'pagination-limit', + title: 'Pagination Limit (optional)', + subtitle: 'The number of items to return (max: 30)', + defaultValue: 5, + }) + + const [type, setType] = useState('list_members') + + const { data: response } = useQuery( + [ + contractAddress, + type, + contract, + wallet, + memberAddress, + startAfterStringState.value, + paginationLimitState.value, + ] as const, + async ({ queryKey }) => { + const [_contractAddress, _type, _contract, _wallet, _memberAddress, startAfter, limit] = queryKey + const messages = contract?.use(contractAddress) + const res = await resolveAddress(_memberAddress, wallet).then(async (resolvedAddress) => { + const result = await dispatchQuery({ + messages, + type, + address: resolvedAddress, + startAfter, + limit, + }) + return result + }) + return res + }, + { + placeholderData: null, + onError: (error: any) => { + toast.error(error.message, { style: { maxWidth: 'none' } }) + }, + enabled: Boolean(contractAddress && contract && wallet), + }, + ) + + const router = useRouter() + + useEffect(() => { + if (contractAddress.length > 0) { + void router.replace({ query: { contractAddress } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contractAddress]) + useEffect(() => { + const initial = new URL(document.URL).searchParams.get('contractAddress') + if (initial && initial.length > 0) contractState.onChange(initial) + }, []) + + return ( +
+ + + + +
+
+ + + + + + + + + + + + +
+ +
+
+ ) +} + +export default withMetadata(SplitsQueryPage, { center: false }) From 74ea4c90dd7c7ba7b2ed9575a25c17ac21548b51 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 11:57:37 +0300 Subject: [PATCH 07/18] Implement MemberAttributes --- components/forms/MemberAttributes.hooks.ts | 33 ++++++++ components/forms/MemberAttributes.tsx | 94 ++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 components/forms/MemberAttributes.hooks.ts create mode 100644 components/forms/MemberAttributes.tsx diff --git a/components/forms/MemberAttributes.hooks.ts b/components/forms/MemberAttributes.hooks.ts new file mode 100644 index 0000000..3cc36e2 --- /dev/null +++ b/components/forms/MemberAttributes.hooks.ts @@ -0,0 +1,33 @@ +import { useMemo, useState } from 'react' +import { uid } from 'utils/random' + +import type { Attribute } from './MemberAttributes' + +export function useMemberAttributesState() { + const [record, setRecord] = useState>(() => ({})) + + const entries = useMemo(() => Object.entries(record), [record]) + const values = useMemo(() => Object.values(record), [record]) + + function add(attribute: Attribute = { address: '', weight: 0 }) { + setRecord((prev) => ({ ...prev, [uid()]: attribute })) + } + + function update(key: string, attribute = record[key]) { + setRecord((prev) => ({ ...prev, [key]: attribute })) + } + + function remove(key: string) { + return setRecord((prev) => { + const latest = { ...prev } + delete latest[key] + return latest + }) + } + + function reset() { + setRecord({}) + } + + return { entries, values, add, update, remove, reset } +} diff --git a/components/forms/MemberAttributes.tsx b/components/forms/MemberAttributes.tsx new file mode 100644 index 0000000..e36ed94 --- /dev/null +++ b/components/forms/MemberAttributes.tsx @@ -0,0 +1,94 @@ +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useEffect, useId, useMemo } from 'react' +import { FaMinus, FaPlus } from 'react-icons/fa' + +import { useInputState, useNumberInputState } from './FormInput.hooks' + +export interface Attribute { + address: string + weight: number +} + +export interface MemberAttributesProps { + title: string + subtitle?: string + isRequired?: boolean + attributes: [string, Attribute][] + onAdd: () => void + onChange: (key: string, attribute: Attribute) => void + onRemove: (key: string) => void +} + +export function MemberAttributes(props: MemberAttributesProps) { + const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props + + return ( + + {attributes.map(([id], i) => ( + + ))} + + ) +} + +export interface MemberAttributeProps { + id: string + isLast: boolean + onAdd: MemberAttributesProps['onAdd'] + onChange: MemberAttributesProps['onChange'] + onRemove: MemberAttributesProps['onRemove'] + defaultAttribute: Attribute +} + +export function MemberAttribute({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: MemberAttributeProps) { + const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast]) + + const htmlId = useId() + + const addressState = useInputState({ + id: `ma-address-${htmlId}`, + name: `ma-address-${htmlId}`, + title: `Address`, + defaultValue: defaultAttribute.address, + }) + + const weightState = useNumberInputState({ + id: `ma-weight-${htmlId}`, + name: `ma-weight-${htmlId}`, + title: `Weight`, + defaultValue: defaultAttribute.weight, + }) + + useEffect(() => { + onChange(id, { address: addressState.value, weight: weightState.value }) + }, [addressState.value, weightState.value, id]) + + return ( +
+ + + +
+ +
+
+ ) +} From 4bde7c8ed6681e70aeeb08ea4241947ac5e05167 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 11:58:37 +0300 Subject: [PATCH 08/18] Fetch splits & cw4-group code ids from env --- env.d.ts | 2 ++ utils/constants.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/env.d.ts b/env.d.ts index c4e8faf..38d4234 100644 --- a/env.d.ts +++ b/env.d.ts @@ -28,6 +28,8 @@ declare namespace NodeJS { readonly NEXT_PUBLIC_BADGE_HUB_ADDRESS: string readonly NEXT_PUBLIC_BADGE_NFT_CODE_ID: string readonly NEXT_PUBLIC_BADGE_NFT_ADDRESS: string + readonly NEXT_PUBLIC_SPLITS_CODE_ID: string + readonly NEXT_PUBLIC_CW4_GROUP_CODE_ID: string readonly NEXT_PUBLIC_PINATA_ENDPOINT_URL: string readonly NEXT_PUBLIC_API_URL: string diff --git a/utils/constants.ts b/utils/constants.ts index f00df0b..ec36024 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -12,6 +12,8 @@ export const BADGE_HUB_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_HUB_CODE export const BADGE_HUB_ADDRESS = process.env.NEXT_PUBLIC_BADGE_HUB_ADDRESS export const BADGE_NFT_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_NFT_CODE_ID, 10) export const BADGE_NFT_ADDRESS = process.env.NEXT_PUBLIC_BADGE_NFT_ADDRESS +export const SPLITS_CODE_ID = parseInt(process.env.NEXT_PUBLIC_SPLITS_CODE_ID, 10) +export const CW4_GROUP_CODE_ID = parseInt(process.env.NEXT_PUBLIC_CW4_GROUP_CODE_ID, 10) export const PINATA_ENDPOINT_URL = process.env.NEXT_PUBLIC_PINATA_ENDPOINT_URL export const NETWORK = process.env.NEXT_PUBLIC_NETWORK From 625411d0c66b725d43252d058b43d38a1c323280 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:10:11 +0300 Subject: [PATCH 09/18] Update Query Members --- contracts/splits/contract.ts | 2 +- contracts/splits/messages/query.ts | 4 ++-- pages/contracts/splits/query.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/splits/contract.ts b/contracts/splits/contract.ts index f62a476..ccd1900 100644 --- a/contracts/splits/contract.ts +++ b/contracts/splits/contract.ts @@ -58,7 +58,7 @@ export const Splits = (client: SigningCosmWasmClient, txSigner: string): SplitsC ///QUERY const listMembers = async (startAfter?: string, limit?: number): Promise => { return client.queryContractSmart(contractAddress, { - list_members: { limit, start_after: startAfter }, + list_members: { start_after: startAfter ? startAfter : undefined, limit }, }) } diff --git a/contracts/splits/messages/query.ts b/contracts/splits/messages/query.ts index 8c316d5..f97f5be 100644 --- a/contracts/splits/messages/query.ts +++ b/contracts/splits/messages/query.ts @@ -21,8 +21,8 @@ export interface DispatchQueryProps { messages: SplitsInstance | undefined type: QueryType address: string - startAfter: string - limit: number + startAfter?: string + limit?: number } export const dispatchQuery = (props: DispatchQueryProps) => { diff --git a/pages/contracts/splits/query.tsx b/pages/contracts/splits/query.tsx index ab43ac0..b34f16f 100644 --- a/pages/contracts/splits/query.tsx +++ b/pages/contracts/splits/query.tsx @@ -77,8 +77,8 @@ const SplitsQueryPage: NextPage = () => { messages, type, address: resolvedAddress, - startAfter, - limit, + startAfter: startAfter.length > 0 ? startAfter : undefined, + limit: limit > 0 ? limit : undefined, }) return result }) From d7f2e6a23108351927170455e80bb497d284ed93 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:13:43 +0300 Subject: [PATCH 10/18] Include code ids for splits & cw4-group in .env.example --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 6ef4451..cd575c0 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,8 @@ NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336 NEXT_PUBLIC_BADGE_HUB_ADDRESS="stars1dacun0xn7z73qzdcmq27q3xn6xuprg8e2ugj364784al2v27tklqynhuqa" NEXT_PUBLIC_BADGE_NFT_CODE_ID=1337 NEXT_PUBLIC_BADGE_NFT_ADDRESS="stars1vlw4y54dyzt3zg7phj8yey9fg4zj49czknssngwmgrnwymyktztstalg7t" +NEXT_PUBLIC_SPLITS_CODE_ID=1904 +NEXT_PUBLIC_CW4_GROUP_CODE_ID=1905 NEXT_PUBLIC_API_URL=https://nft-api.elgafar-1.stargaze-apis.com From 1874d5d7d2defeee14aa065491c23bee5214cfc1 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:14:14 +0300 Subject: [PATCH 11/18] Init splits contract dashboard > Instantiate --- pages/contracts/splits/instantiate.tsx | 228 +++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 pages/contracts/splits/instantiate.tsx diff --git a/pages/contracts/splits/instantiate.tsx b/pages/contracts/splits/instantiate.tsx new file mode 100644 index 0000000..921974b --- /dev/null +++ b/pages/contracts/splits/instantiate.tsx @@ -0,0 +1,228 @@ +import { toBase64, toUtf8 } from '@cosmjs/encoding' +import { Alert } from 'components/Alert' +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { AddressInput } from 'components/forms/FormInput' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { splitsLinkTabs } 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, useEffect, useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaAsterisk } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { isValidAddress } from 'utils/isValidAddress' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +import { useInputState } from '../../../components/forms/FormInput.hooks' +import type { Attribute } from '../../../components/forms/MemberAttributes' +import { MemberAttributes } from '../../../components/forms/MemberAttributes' +import { useMemberAttributesState } from '../../../components/forms/MemberAttributes.hooks' +import { CW4_GROUP_CODE_ID, SPLITS_CODE_ID } from '../../../utils/constants' +import { resolveAddress } from '../../../utils/resolveAddress' + +export type CW4Method = 'new' | 'existing' + +const SplitsInstantiatePage: NextPage = () => { + const wallet = useWallet() + const { splits: contract } = useContracts() + const [members, setMembers] = useState([]) + const [cw4Method, setCw4Method] = useState('new') + + const cw4GroupAddressState = useInputState({ + id: 'cw4-group-address', + name: 'cw4-group-address', + title: 'CW4 Group Address', + subtitle: 'Address of the CW4 Group contract', + placeholder: 'stars1...', + }) + + const splitsAdminState = useInputState({ + id: 'splits-admin', + name: 'splits-admin', + title: 'Splits Contract Admin', + subtitle: 'Address of the Splits Contract administrator', + defaultValue: wallet.address, + }) + + const cw4GroupAdminState = useInputState({ + id: 'cw4-group-admin', + name: 'cw4-group-admin', + title: 'CW4 Group Admin', + subtitle: 'Address of the CW4 Group administrator', + defaultValue: wallet.address, + }) + + const memberListState = useMemberAttributesState() + + useEffect(() => { + memberListState.reset() + memberListState.add({ + address: '', + weight: 0, + }) + }, []) + + const { data, isLoading, mutate } = useMutation( + async (event: FormEvent): Promise => { + event.preventDefault() + if (!contract) { + throw new Error('Smart contract connection failed') + } + const msg = + cw4Method === 'existing' + ? { admin: splitsAdminState.value, group: { cw4_address: cw4GroupAddressState.value } } + : { + admin: splitsAdminState.value ? splitsAdminState.value : undefined, + group: { + cw4_instantiate: { + code_id: CW4_GROUP_CODE_ID, + label: 'cw4-group', + msg: toBase64( + toUtf8( + JSON.stringify({ + admin: cw4GroupAdminState.value ? cw4GroupAdminState.value : undefined, + members: [ + ...new Set( + members + .filter( + (member) => + member.address !== '' && + member.weight > 0 && + isValidAddress(member.address) && + member.address.startsWith('stars'), + ) + .map((member) => ({ addr: member.address, weight: member.weight })), + ), + ], + }), + ), + ), + }, + }, + } + return toast.promise(contract.instantiate(SPLITS_CODE_ID, msg, 'Stargaze Splits Contract', wallet.address), { + loading: 'Instantiating contract...', + error: 'Instantiation failed!', + success: 'Instantiation success!', + }) + }, + { + onError: (error) => { + toast.error(String(error), { style: { maxWidth: 'none' } }) + }, + }, + ) + + const resolveMemberAddresses = () => { + const tempMembers: Attribute[] = [] + memberListState.values.map(async (member) => { + await resolveAddress(member.address.trim(), wallet).then((resolvedAddress) => { + tempMembers.push({ address: resolvedAddress, weight: member.weight }) + }) + }) + setMembers(tempMembers) + console.log('Members:', members) + } + + useEffect(() => { + resolveMemberAddresses() + }, [memberListState.values]) + + return ( +
+ + + + + + + Instantiate success! Here is the transaction result containing the contract address and the transaction + hash. + + +
+
+ +
+
+
+ { + setCw4Method('new') + }} + type="radio" + value="New" + /> + +
+
+ { + setCw4Method('existing') + }} + type="radio" + value="Existing" + /> + +
+
+
+ + + + + +
+ +
+
+ + + + +
+
+ +
+ + ) +} + +export default withMetadata(SplitsInstantiatePage, { center: false }) From 45ecf7aaba79674df4807577173a60a75d7f94b0 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:18:43 +0300 Subject: [PATCH 12/18] Include splits contract dashboard link on the sidebar --- components/Sidebar.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 50517d0..cde52fc 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -190,6 +190,15 @@ export const Sidebar = () => { Badge Hub Contract +
  • + Splits Contract +
  • From 8501f6bf64c522b25c3ed1adac5c7e667d335340 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:23:09 +0300 Subject: [PATCH 13/18] Include splits contract dashboard homecard on the dashboard landing page --- pages/contracts/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pages/contracts/index.tsx b/pages/contracts/index.tsx index 3f1ef14..7ef81e4 100644 --- a/pages/contracts/index.tsx +++ b/pages/contracts/index.tsx @@ -58,6 +58,9 @@ const HomePage: NextPage = () => { Execute messages and run queries on the Badge Hub contract designed for event organizers. + + Execute messages and run queries on the Splits contract designed for revenue distribution. +
    ) From d27dfbd452a6be4d6a1bfbb59b253c0d3452fc9f Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:28:33 +0300 Subject: [PATCH 14/18] Minor UI updates --- pages/contracts/splits/instantiate.tsx | 40 +++++++++++++++----------- pages/contracts/splits/query.tsx | 2 +- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pages/contracts/splits/instantiate.tsx b/pages/contracts/splits/instantiate.tsx index 921974b..d6fe678 100644 --- a/pages/contracts/splits/instantiate.tsx +++ b/pages/contracts/splits/instantiate.tsx @@ -196,24 +196,30 @@ const SplitsInstantiatePage: NextPage = () => { - - - - - -
    - +
    +
    + + + + + + +
    - - - - +
    + +
    + +
    +
    +
    +
    diff --git a/pages/contracts/splits/query.tsx b/pages/contracts/splits/query.tsx index b34f16f..e47c34e 100644 --- a/pages/contracts/splits/query.tsx +++ b/pages/contracts/splits/query.tsx @@ -46,7 +46,7 @@ const SplitsQueryPage: NextPage = () => { id: 'start-after-string', name: 'start-after-string', title: 'Start After (optional)', - subtitle: 'The public key to start the pagination after', + subtitle: 'The member address to start the pagination after', }) const paginationLimitState = useNumberInputState({ From 2e080488e382096b01b0fa092acb2369711d55d5 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:40:54 +0300 Subject: [PATCH 15/18] Init splits contract dashboard > Migrate --- contracts/splits/contract.ts | 28 +++++- contracts/splits/useContract.ts | 19 ++++- pages/contracts/splits/migrate.tsx | 133 +++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 pages/contracts/splits/migrate.tsx diff --git a/contracts/splits/contract.ts b/contracts/splits/contract.ts index ccd1900..06593d0 100644 --- a/contracts/splits/contract.ts +++ b/contracts/splits/contract.ts @@ -1,11 +1,17 @@ import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import type { Coin } from '@cosmjs/proto-signing' +import type { logs } from '@cosmjs/stargate' export interface InstantiateResponse { readonly contractAddress: string readonly transactionHash: string } +export interface MigrateResponse { + readonly transactionHash: string + readonly logs: readonly logs.Log[] +} + export interface SplitsInstance { readonly contractAddress: string //Query @@ -50,6 +56,13 @@ export interface SplitsContract { use: (contractAddress: string) => SplitsInstance + migrate: ( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: Record, + ) => Promise + messages: (contractAddress: string) => SplitsMessages } @@ -132,6 +145,19 @@ export const Splits = (client: SigningCosmWasmClient, txSigner: string): SplitsC } } + const migrate = async ( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: Record, + ): Promise => { + const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto') + return { + transactionHash: result.transactionHash, + logs: result.logs, + } + } + const messages = (contractAddress: string) => { const updateAdmin = (admin: string) => { return { @@ -161,5 +187,5 @@ export const Splits = (client: SigningCosmWasmClient, txSigner: string): SplitsC } } - return { use, instantiate, messages } + return { use, instantiate, migrate, messages } } diff --git a/contracts/splits/useContract.ts b/contracts/splits/useContract.ts index 70ae0f9..69a1c63 100644 --- a/contracts/splits/useContract.ts +++ b/contracts/splits/useContract.ts @@ -3,7 +3,7 @@ import { useWallet } from 'contexts/wallet' import { useCallback, useEffect, useState } from 'react' -import type { InstantiateResponse, SplitsContract, SplitsInstance, SplitsMessages } from './contract' +import type { InstantiateResponse, MigrateResponse, SplitsContract, SplitsInstance, SplitsMessages } from './contract' import { Splits as initContract } from './contract' export interface UseSplitsContractProps { @@ -14,6 +14,8 @@ export interface UseSplitsContractProps { admin?: string, ) => Promise + migrate: (contractAddress: string, codeId: number, migrateMsg: Record) => Promise + use: (customAddress?: string) => SplitsInstance | undefined updateContractAddress: (contractAddress: string) => void @@ -53,6 +55,20 @@ export function useSplitsContract(): UseSplitsContractProps { [splits], ) + const migrate = useCallback( + (contractAddress: string, codeId: number, migrateMsg: Record): Promise => { + return new Promise((resolve, reject) => { + if (!splits) { + reject(new Error('Contract is not initialized.')) + return + } + console.log(wallet.address, contractAddress, codeId) + splits.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject) + }) + }, + [splits, wallet], + ) + const use = useCallback( (customAddress = ''): SplitsInstance | undefined => { return splits?.use(address || customAddress) @@ -69,6 +85,7 @@ export function useSplitsContract(): UseSplitsContractProps { return { instantiate, + migrate, use, updateContractAddress, messages, diff --git a/pages/contracts/splits/migrate.tsx b/pages/contracts/splits/migrate.tsx new file mode 100644 index 0000000..75d1314 --- /dev/null +++ b/pages/contracts/splits/migrate.tsx @@ -0,0 +1,133 @@ +import { Button } from 'components/Button' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { useExecuteComboboxState } from 'components/contracts/splits/ExecuteCombobox.hooks' +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { splitsLinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { MigrateResponse } from 'contracts/splits' +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' +import type { FormEvent } from 'react' +import { useEffect, 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 SplitsMigratePage: NextPage = () => { + const { splits: contract } = useContracts() + const wallet = useWallet() + + const [lastTx, setLastTx] = useState('') + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + const codeIdState = useNumberInputState({ + id: 'code-id', + name: 'code-id', + title: 'Code ID', + subtitle: 'Code ID of the New Splits contract', + placeholder: '1', + }) + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Splits Address', + subtitle: 'Address of the Splits contract', + }) + const contractAddress = contractState.value + + const { data, isLoading, mutate } = useMutation( + async (event: FormEvent): Promise => { + event.preventDefault() + if (!contract) { + throw new Error('Smart contract connection failed') + } + if (!wallet.initialized) { + throw new Error('Please connect your wallet.') + } + + const migrateMsg = {} + + return toast.promise(contract.migrate(contractAddress, codeIdState.value, migrateMsg), { + error: `Migration failed!`, + loading: 'Executing message...', + success: (tx) => { + if (tx) { + setLastTx(tx.transactionHash) + } + return `Transaction success!` + }, + }) + }, + { + onError: (error) => { + toast.error(String(error), { style: { maxWidth: 'none' } }) + }, + }, + ) + + const router = useRouter() + + useEffect(() => { + if (contractAddress.length > 0) { + void router.replace({ query: { contractAddress } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contractAddress]) + useEffect(() => { + const initial = new URL(document.URL).searchParams.get('contractAddress') + if (initial && initial.length > 0) contractState.onChange(initial) + }, []) + + return ( +
    + + + + +
    +
    + + +
    +
    +
    + + + + +
    + + + +
    +
    +
    + ) +} + +export default withMetadata(SplitsMigratePage, { center: false }) From fbfcdf9cd41e6deec48da163f12352e4856d1938 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:49:33 +0300 Subject: [PATCH 16/18] Allow splits admin to be undefined when using an existing cw4-group --- pages/contracts/splits/instantiate.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pages/contracts/splits/instantiate.tsx b/pages/contracts/splits/instantiate.tsx index d6fe678..0bb27cc 100644 --- a/pages/contracts/splits/instantiate.tsx +++ b/pages/contracts/splits/instantiate.tsx @@ -77,7 +77,10 @@ const SplitsInstantiatePage: NextPage = () => { } const msg = cw4Method === 'existing' - ? { admin: splitsAdminState.value, group: { cw4_address: cw4GroupAddressState.value } } + ? { + admin: splitsAdminState.value ? splitsAdminState.value : undefined, + group: { cw4_address: cw4GroupAddressState.value }, + } : { admin: splitsAdminState.value ? splitsAdminState.value : undefined, group: { From cdf80647685bb37d916cf9dd0c4ebe00ac6c88c3 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:50:38 +0300 Subject: [PATCH 17/18] Bump Studio version --- .env.example | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index cd575c0..c69fa70 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.5.0 +APP_VERSION=0.5.2 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=1911 diff --git a/package.json b/package.json index 9cfca2b..e107d51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.5.0", + "version": "0.5.2", "workspaces": [ "packages/*" ], From 4cee660462764f345efd47f4041d60f194568eaa Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Mar 2023 21:54:02 +0300 Subject: [PATCH 18/18] Update splits contract description --- pages/contracts/splits/execute.tsx | 2 +- pages/contracts/splits/instantiate.tsx | 2 +- pages/contracts/splits/migrate.tsx | 2 +- pages/contracts/splits/query.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/contracts/splits/execute.tsx b/pages/contracts/splits/execute.tsx index c0952ed..24305e6 100644 --- a/pages/contracts/splits/execute.tsx +++ b/pages/contracts/splits/execute.tsx @@ -100,7 +100,7 @@ const SplitsExecutePage: NextPage = () => {
    diff --git a/pages/contracts/splits/instantiate.tsx b/pages/contracts/splits/instantiate.tsx index 0bb27cc..88dcc59 100644 --- a/pages/contracts/splits/instantiate.tsx +++ b/pages/contracts/splits/instantiate.tsx @@ -142,7 +142,7 @@ const SplitsInstantiatePage: NextPage = () => {
    diff --git a/pages/contracts/splits/migrate.tsx b/pages/contracts/splits/migrate.tsx index 75d1314..c03a3a1 100644 --- a/pages/contracts/splits/migrate.tsx +++ b/pages/contracts/splits/migrate.tsx @@ -93,7 +93,7 @@ const SplitsMigratePage: NextPage = () => {
    diff --git a/pages/contracts/splits/query.tsx b/pages/contracts/splits/query.tsx index e47c34e..b43a418 100644 --- a/pages/contracts/splits/query.tsx +++ b/pages/contracts/splits/query.tsx @@ -110,7 +110,7 @@ const SplitsQueryPage: NextPage = () => {