diff --git a/.env.example b/.env.example index 6067326..8d5333c 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.4.9 +APP_VERSION=0.5.0 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=1702 diff --git a/components/collections/creation/WhitelistDetails.tsx b/components/collections/creation/WhitelistDetails.tsx index 11305ab..8482e10 100644 --- a/components/collections/creation/WhitelistDetails.tsx +++ b/components/collections/creation/WhitelistDetails.tsx @@ -1,8 +1,11 @@ import { FormControl } from 'components/FormControl' import { FormGroup } from 'components/FormGroup' +import { AddressList } from 'components/forms/AddressList' +import { useAddressListState } from 'components/forms/AddressList.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { InputDateTime } from 'components/InputDateTime' import React, { useEffect, useState } from 'react' +import { isValidAddress } from 'utils/isValidAddress' import { Conditional } from '../../Conditional' import { AddressInput, NumberInput } from '../../forms/FormInput' @@ -22,6 +25,8 @@ export interface WhitelistDetailsDataProps { endTime?: string perAddressLimit?: number memberLimit?: number + admins?: string[] + adminsMutable?: boolean } type WhitelistState = 'none' | 'existing' | 'new' @@ -31,6 +36,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { const [startDate, setStartDate] = useState(undefined) const [endDate, setEndDate] = useState(undefined) const [whitelistArray, setWhitelistArray] = useState([]) + const [adminsMutable, setAdminsMutable] = useState(true) const whitelistAddressState = useInputState({ id: 'whitelist-address', @@ -67,6 +73,8 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { setWhitelistArray(data) } + const addressListState = useAddressListState() + useEffect(() => { const data: WhitelistDetailsDataProps = { whitelistType: whitelistState, @@ -82,6 +90,14 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '', perAddressLimit: perAddressLimitState.value, memberLimit: memberLimitState.value, + admins: [ + ...new Set( + addressListState.values + .map((a) => a.address.trim()) + .filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')), + ), + ], + adminsMutable, } onChange(data) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -94,6 +110,8 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { endDate, whitelistArray, whitelistState, + addressListState.values, + adminsMutable, ]) return ( @@ -186,6 +204,28 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
+
+ +
+
+ +
diff --git a/components/forms/AddressList.hooks.ts b/components/forms/AddressList.hooks.ts index a7e5415..951d31d 100644 --- a/components/forms/AddressList.hooks.ts +++ b/components/forms/AddressList.hooks.ts @@ -27,5 +27,9 @@ export function useAddressListState() { }) } - return { entries, values, add, update, remove } + function reset() { + setRecord({}) + } + + return { entries, values, add, update, remove, reset } } diff --git a/contracts/whitelist/contract.ts b/contracts/whitelist/contract.ts index 4ea6ca8..42c2979 100644 --- a/contracts/whitelist/contract.ts +++ b/contracts/whitelist/contract.ts @@ -24,6 +24,7 @@ export interface WhiteListInstance { isActive: () => Promise members: (startAfter?: string, limit?: number) => Promise hasMember: (member: string) => Promise + adminList: () => Promise config: () => Promise //Execute @@ -33,6 +34,8 @@ export interface WhiteListInstance { removeMembers: (memberList: string[]) => Promise updatePerAddressLimit: (limit: number) => Promise increaseMemberLimit: (limit: number) => Promise + updateAdmins: (admins: string[]) => Promise + freeze: () => Promise } export interface WhitelistMessages { @@ -42,6 +45,8 @@ export interface WhitelistMessages { removeMembers: (memberList: string[]) => RemoveMembersMessage updatePerAddressLimit: (limit: number) => UpdatePerAddressLimitMessage increaseMemberLimit: (limit: number) => IncreaseMemberLimitMessage + updateAdmins: (admins: string[]) => UpdateAdminsMessage + freeze: () => FreezeMessage } export interface UpdateStartTimeMessage { @@ -62,6 +67,22 @@ export interface UpdateEndTimeMessage { funds: Coin[] } +export interface UpdateAdminsMessage { + sender: string + contract: string + msg: { + update_admins: { admins: string[] } + } + funds: Coin[] +} + +export interface FreezeMessage { + sender: string + contract: string + msg: { freeze: Record } + funds: Coin[] +} + export interface AddMembersMessage { sender: string contract: string @@ -139,6 +160,12 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit }) } + const adminList = async (): Promise => { + return client.queryContractSmart(contractAddress, { + admin_list: {}, + }) + } + const config = async (): Promise => { return client.queryContractSmart(contractAddress, { config: {}, @@ -170,6 +197,32 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit return res.transactionHash } + const updateAdmins = async (admins: string[]): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + update_admins: { + admins, + }, + }, + 'auto', + ) + return res.transactionHash + } + + const freeze = async (): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + freeze: {}, + }, + 'auto', + ) + return res.transactionHash + } + const removeMembers = async (memberList: string[]): Promise => { const res = await client.execute( txSigner, @@ -199,6 +252,8 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit contractAddress, updateStartTime, updateEndTime, + updateAdmins, + freeze, addMembers, removeMembers, updatePerAddressLimit, @@ -208,6 +263,7 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit isActive, members, hasMember, + adminList, config, } } @@ -263,6 +319,28 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit } } + const updateAdmins = (admins: string[]) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_admins: { admins }, + }, + funds: [], + } + } + + const freeze = () => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + freeze: {}, + }, + funds: [], + } + } + const removeMembers = (memberList: string[]) => { return { sender: txSigner, @@ -299,10 +377,12 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit return { updateStartTime, updateEndTime, + updateAdmins, addMembers, removeMembers, updatePerAddressLimit, increaseMemberLimit, + freeze, } } diff --git a/contracts/whitelist/messages/execute.ts b/contracts/whitelist/messages/execute.ts index 0d1a885..a0fbdec 100644 --- a/contracts/whitelist/messages/execute.ts +++ b/contracts/whitelist/messages/execute.ts @@ -6,10 +6,12 @@ export type ExecuteType = typeof EXECUTE_TYPES[number] export const EXECUTE_TYPES = [ 'update_start_time', 'update_end_time', + 'update_admins', 'add_members', 'remove_members', 'update_per_address_limit', 'increase_member_limit', + 'freeze', ] as const export interface ExecuteListItem { @@ -29,6 +31,11 @@ export const EXECUTE_LIST: ExecuteListItem[] = [ name: 'Update End Time', description: `Update the end time of the whitelist`, }, + { + id: 'update_admins', + name: 'Update Admins', + description: `Update the list of administrators for the whitelist`, + }, { id: 'add_members', name: 'Add Members', @@ -49,6 +56,11 @@ export const EXECUTE_LIST: ExecuteListItem[] = [ name: 'Increase Member Limit', description: `Increase the member limit of the whitelist`, }, + { + id: 'freeze', + name: 'Freeze', + description: `Freeze the current state of the contract admin list`, + }, ] export interface DispatchExecuteProps { @@ -70,6 +82,8 @@ export type DispatchExecuteArgs = { | { type: Select<'remove_members'>; members: string[] } | { type: Select<'update_per_address_limit'>; limit: number } | { type: Select<'increase_member_limit'>; limit: number } + | { type: Select<'update_admins'>; admins: string[] } + | { type: Select<'freeze'> } ) export const dispatchExecute = async (args: DispatchExecuteArgs) => { @@ -84,6 +98,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { case 'update_end_time': { return messages.updateEndTime(args.timestamp) } + case 'update_admins': { + return messages.updateAdmins(args.admins) + } case 'add_members': { return messages.addMembers(args.members) } @@ -96,6 +113,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { case 'increase_member_limit': { return messages.increaseMemberLimit(args.limit) } + case 'freeze': { + return messages.freeze() + } default: { throw new Error('unknown execute type') } @@ -113,6 +133,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { case 'update_end_time': { return messages(contract)?.updateEndTime(args.timestamp) } + case 'update_admins': { + return messages(contract)?.updateAdmins(args.admins) + } case 'add_members': { return messages(contract)?.addMembers(args.members) } @@ -125,6 +148,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { case 'increase_member_limit': { return messages(contract)?.increaseMemberLimit(args.limit) } + case 'freeze': { + return messages(contract)?.freeze() + } default: { return {} } diff --git a/contracts/whitelist/messages/query.ts b/contracts/whitelist/messages/query.ts index 652e801..6e4f333 100644 --- a/contracts/whitelist/messages/query.ts +++ b/contracts/whitelist/messages/query.ts @@ -2,8 +2,15 @@ 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 const QUERY_TYPES = [ + 'has_started', + 'has_ended', + 'is_active', + 'members', + 'admin_list', + 'has_member', + 'config', +] as const export interface QueryListItem { id: QueryType name: string @@ -15,6 +22,7 @@ export const QUERY_LIST: QueryListItem[] = [ { 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: 'admin_list', name: 'Admin List', description: 'View the whitelist admin list' }, { id: 'has_member', name: 'Has Member', description: 'Check if a member is in the whitelist' }, { id: 'config', name: 'Config', description: 'View the whitelist configuration' }, ] @@ -36,6 +44,8 @@ export const dispatchQuery = (props: DispatchQueryProps) => { return messages?.isActive() case 'members': return messages?.members() + case 'admin_list': + return messages?.adminList() case 'has_member': return messages?.hasMember(address) case 'config': diff --git a/package.json b/package.json index 150a347..9cfca2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.4.9", + "version": "0.5.0", "workspaces": [ "packages/*" ], diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index ed93a9c..7e8e2e4 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -378,6 +378,8 @@ const CollectionCreationPage: NextPage = () => { mint_price: coin(String(Number(whitelistDetails?.unitPrice)), 'ustars'), per_address_limit: whitelistDetails?.perAddressLimit, member_limit: whitelistDetails?.memberLimit, + admins: whitelistDetails?.admins || [wallet.address], + admins_mutable: whitelistDetails?.adminsMutable, } const data = await whitelistContract.instantiate( @@ -813,8 +815,8 @@ const CollectionCreationPage: NextPage = () => { throw new Error('Per address limit is required') if (!whitelistDetails.memberLimit || whitelistDetails.memberLimit === 0) throw new Error('Member limit is required') - if (Number(whitelistDetails.startTime) > Number(whitelistDetails.endTime)) - throw new Error('Whitelist start time cannot be later than whitelist end time') + if (Number(whitelistDetails.startTime) >= Number(whitelistDetails.endTime)) + throw new Error('Whitelist start time cannot be equal to or later than the whitelist end time') if (Number(whitelistDetails.startTime) !== Number(mintingDetails?.startTime)) throw new Error('Whitelist start time must be the same as the minting start time') if (whitelistDetails.perAddressLimit && mintingDetails?.numTokens) { diff --git a/pages/contracts/whitelist/execute.tsx b/pages/contracts/whitelist/execute.tsx index b4e54e6..d38339b 100644 --- a/pages/contracts/whitelist/execute.tsx +++ b/pages/contracts/whitelist/execute.tsx @@ -64,6 +64,7 @@ const WhitelistExecutePage: NextPage = () => { 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 showAdminList = isEitherType(type, ['update_admins']) const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value]) const payload: DispatchExecuteArgs = { @@ -80,6 +81,13 @@ const WhitelistExecutePage: NextPage = () => { .concat(memberList), ), ], + admins: [ + ...new Set( + addressListState.values + .map((a) => a.address.trim()) + .filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')), + ), + ] || [wallet.address], } const { isLoading, mutate } = useMutation( async (event: FormEvent) => { @@ -146,20 +154,22 @@ const WhitelistExecutePage: NextPage = () => { setTimestamp(date)} value={timestamp} /> - + - - You may optionally choose a text file of additional member addresses. - - + + + You may optionally choose a text file of additional member addresses. + + +
diff --git a/pages/contracts/whitelist/instantiate.tsx b/pages/contracts/whitelist/instantiate.tsx index eb54478..05b4c9f 100644 --- a/pages/contracts/whitelist/instantiate.tsx +++ b/pages/contracts/whitelist/instantiate.tsx @@ -5,6 +5,8 @@ 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' @@ -22,6 +24,7 @@ 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 { isValidAddress } from 'utils/isValidAddress' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' @@ -31,6 +34,7 @@ const WhitelistInstantiatePage: NextPage = () => { const [startDate, setStartDate] = useState(undefined) const [endDate, setEndDate] = useState(undefined) + const [adminsMutable, setAdminsMutable] = useState(true) const [whitelistArray, setWhitelistArray] = useState([]) @@ -58,6 +62,8 @@ const WhitelistInstantiatePage: NextPage = () => { placeholder: '5', }) + const addressListState = useAddressListState() + const { data, isLoading, mutate } = useMutation( async (event: FormEvent): Promise => { event.preventDefault() @@ -79,6 +85,14 @@ const WhitelistInstantiatePage: NextPage = () => { mint_price: coin(String(Number(unitPriceState.value) * 1000000), 'ustars'), per_address_limit: perAddressLimitState.value, member_limit: memberLimitState.value, + admins: [ + ...new Set( + addressListState.values + .map((a) => a.address.trim()) + .filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')), + ), + ] || [wallet.address], + admins_mutable: adminsMutable, } return toast.promise( contract.instantiate(WHITELIST_CODE_ID, msg, 'Stargaze Whitelist Contract', wallet.address), @@ -119,6 +133,29 @@ const WhitelistInstantiatePage: NextPage = () => {
+
+ +
+
+ +
+ 0}> diff --git a/pages/contracts/whitelist/query.tsx b/pages/contracts/whitelist/query.tsx index bb0ffee..2555b9f 100644 --- a/pages/contracts/whitelist/query.tsx +++ b/pages/contracts/whitelist/query.tsx @@ -108,7 +108,7 @@ const WhitelistQueryPage: NextPage = () => { onChange={(e) => setType(e.target.value as QueryType)} > {QUERY_LIST.map(({ id, name }) => ( - ))}