diff --git a/contexts/contracts.tsx b/contexts/contracts.tsx index d4e01c5..3af6f3e 100644 --- a/contexts/contracts.tsx +++ b/contexts/contracts.tsx @@ -16,6 +16,7 @@ import type { UseVendingMinterContractProps } from 'contracts/vendingMinter' import { useVendingMinterContract } from 'contracts/vendingMinter' import type { UseWhiteListContractProps } from 'contracts/whitelist' import { useWhiteListContract } from 'contracts/whitelist' +import { type UseWhiteListMerkleTreeContractProps, useWhiteListMerkleTreeContract } from 'contracts/whitelistMerkleTree' import type { ReactNode, VFC } from 'react' import { Fragment, useEffect } from 'react' import { create } from 'zustand' @@ -32,6 +33,7 @@ export interface ContractsStore { baseMinter: UseBaseMinterContractProps | null openEditionMinter: UseOpenEditionMinterContractProps | null whitelist: UseWhiteListContractProps | null + whitelistMerkleTree: UseWhiteListMerkleTreeContractProps | null vendingFactory: UseVendingFactoryContractProps | null baseFactory: UseBaseFactoryContractProps | null openEditionFactory: UseOpenEditionFactoryContractProps | null @@ -49,6 +51,7 @@ export const defaultValues: ContractsStore = { baseMinter: null, openEditionMinter: null, whitelist: null, + whitelistMerkleTree: null, vendingFactory: null, baseFactory: null, openEditionFactory: null, @@ -83,6 +86,7 @@ const ContractsSubscription: VFC = () => { const baseMinter = useBaseMinterContract() const openEditionMinter = useOpenEditionMinterContract() const whitelist = useWhiteListContract() + const whitelistMerkleTree = useWhiteListMerkleTreeContract() const vendingFactory = useVendingFactoryContract() const baseFactory = useBaseFactoryContract() const openEditionFactory = useOpenEditionFactoryContract() @@ -97,6 +101,7 @@ const ContractsSubscription: VFC = () => { baseMinter, openEditionMinter, whitelist, + whitelistMerkleTree, vendingFactory, baseFactory, openEditionFactory, @@ -104,7 +109,20 @@ const ContractsSubscription: VFC = () => { splits, royaltyRegistry, }) - }, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub, splits, royaltyRegistry]) + }, [ + sg721, + vendingMinter, + baseMinter, + whitelist, + whitelistMerkleTree, + vendingFactory, + baseFactory, + badgeHub, + splits, + royaltyRegistry, + openEditionMinter, + openEditionFactory, + ]) return null } diff --git a/contracts/whitelistMerkleTree/contract.ts b/contracts/whitelistMerkleTree/contract.ts new file mode 100644 index 0000000..aa4912a --- /dev/null +++ b/contracts/whitelistMerkleTree/contract.ts @@ -0,0 +1,374 @@ +import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import type { Coin } from '@cosmjs/proto-signing' +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' + +export interface InstantiateResponse { + readonly contractAddress: string + readonly transactionHash: string +} + +export interface ConfigResponse { + readonly per_address_limit: number + readonly start_time: string + readonly end_time: string + readonly mint_price: Coin + readonly is_active: boolean +} +export interface WhiteListMerkleTreeInstance { + readonly contractAddress: string + //Query + hasStarted: () => Promise + hasEnded: () => Promise + isActive: () => Promise + hasMember: (member: string, proof_hashes: string[]) => Promise + adminList: () => Promise + config: () => Promise + canExecute: (sender: string, msg: string) => Promise + merkleRoot: () => Promise + merkleTreeUri: () => Promise + + //Execute + updateStartTime: (startTime: string) => Promise + updateEndTime: (endTime: string) => Promise + addMembers: (memberList: string[] | WhitelistFlexMember[]) => Promise + removeMembers: (memberList: string[]) => Promise + // updatePerAddressLimit: (limit: number) => Promise + updateAdmins: (admins: string[]) => Promise + freeze: () => Promise +} + +export interface WhiteListMerkleTreeMessages { + updateStartTime: (startTime: string) => UpdateStartTimeMessage + updateEndTime: (endTime: string) => UpdateEndTimeMessage + addMembers: (memberList: string[] | WhitelistFlexMember[]) => AddMembersMessage + removeMembers: (memberList: string[]) => RemoveMembersMessage + // updatePerAddressLimit: (limit: number) => UpdatePerAddressLimitMessage + updateAdmins: (admins: string[]) => UpdateAdminsMessage + freeze: () => FreezeMessage +} + +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 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 + msg: { + add_members: { to_add: string[] | WhitelistFlexMember[] } + } + 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 WhiteListMerkleTreeContract { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ) => Promise + + use: (contractAddress: string) => WhiteListMerkleTreeInstance + + messages: (contractAddress: string) => WhiteListMerkleTreeMessages +} + +export const WhiteListMerkleTree = (client: SigningCosmWasmClient, txSigner: string): WhiteListMerkleTreeContract => { + const use = (contractAddress: string): WhiteListMerkleTreeInstance => { + ///QUERY START + const hasStarted = async (): Promise => { + return client.queryContractSmart(contractAddress, { has_started: {} }) + } + + const hasEnded = async (): Promise => { + return client.queryContractSmart(contractAddress, { has_ended: {} }) + } + + const isActive = async (): Promise => { + return client.queryContractSmart(contractAddress, { is_active: {} }) + } + + const hasMember = async (member: string, proofHashes: string[]): Promise => { + return client.queryContractSmart(contractAddress, { + has_member: { member, proof_hashes: proofHashes }, + }) + } + + const adminList = async (): Promise => { + return client.queryContractSmart(contractAddress, { + admin_list: {}, + }) + } + + const config = async (): Promise => { + return client.queryContractSmart(contractAddress, { + config: {}, + }) + } + + const merkleRoot = async (): Promise => { + return client.queryContractSmart(contractAddress, { + merkle_root: {}, + }) + } + + const merkleTreeUri = async (): Promise => { + return client.queryContractSmart(contractAddress, { + merkle_tree_uri: {}, + }) + } + + const canExecute = async (sender: string, msg: string): Promise => { + return client.queryContractSmart(contractAddress, { + can_execute: { sender, msg }, + }) + } + /// QUERY END + /// EXECUTE START + const updateStartTime = async (startTime: string): Promise => { + 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[] | WhitelistFlexMember[]): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + add_members: { + to_add: memberList, + }, + }, + 'auto', + ) + 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, + contractAddress, + { + remove_members: { + to_remove: memberList, + }, + }, + 'auto', + ) + return res.transactionHash + } + + // const updatePerAddressLimit = async (limit: number): Promise => { + // const res = await client.execute(txSigner, contractAddress, { update_per_address_limit: limit }, 'auto') + // return res.transactionHash + // } + + /// EXECUTE END + + return { + contractAddress, + updateStartTime, + updateEndTime, + updateAdmins, + freeze, + addMembers, + removeMembers, + // updatePerAddressLimit, + hasStarted, + hasEnded, + isActive, + hasMember, + adminList, + config, + merkleRoot, + merkleTreeUri, + canExecute, + } + } + + 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 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[] | WhitelistFlexMember[]) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + add_members: { to_add: memberList }, + }, + funds: [], + } + } + + 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, + 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: [], + // } + // } + + return { + updateStartTime, + updateEndTime, + updateAdmins, + addMembers, + removeMembers, + // updatePerAddressLimit, + freeze, + } + } + + return { use, instantiate, messages } +} diff --git a/contracts/whitelistMerkleTree/index.ts b/contracts/whitelistMerkleTree/index.ts new file mode 100644 index 0000000..6dc6461 --- /dev/null +++ b/contracts/whitelistMerkleTree/index.ts @@ -0,0 +1,2 @@ +export * from './contract' +export * from './useContract' diff --git a/contracts/whitelistMerkleTree/messages/execute.ts b/contracts/whitelistMerkleTree/messages/execute.ts new file mode 100644 index 0000000..f52b7f2 --- /dev/null +++ b/contracts/whitelistMerkleTree/messages/execute.ts @@ -0,0 +1,144 @@ +import type { WhitelistFlexMember } from '../../../components/WhitelistFlexUpload' +import type { WhiteListMerkleTreeInstance } from '../index' +import { useWhiteListMerkleTreeContract } from '../index' + +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', + 'freeze', +] 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: 'update_admins', + name: 'Update Admins', + description: `Update the list of administrators for 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: 'freeze', + name: 'Freeze', + description: `Freeze the current state of the contract admin list`, + }, +] + +export interface DispatchExecuteProps { + type: ExecuteType + [k: string]: unknown +} + +/** @see {@link WhiteListMerkleTreeInstance} */ +export interface DispatchExecuteArgs { + contract: string + messages?: WhiteListMerkleTreeInstance + type: string | undefined + timestamp: string + members: string[] | WhitelistFlexMember[] + limit: number + admins: 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 'update_start_time': { + return messages.updateStartTime(args.timestamp) + } + 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) + } + case 'remove_members': { + return messages.removeMembers(args.members as string[]) + } + // case 'update_per_address_limit': { + // return messages.updatePerAddressLimit(args.limit) + // } + case 'freeze': { + return messages.freeze() + } + default: { + throw new Error('unknown execute type') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages } = useWhiteListMerkleTreeContract() + 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 'update_admins': { + return messages(contract)?.updateAdmins(args.admins) + } + case 'add_members': { + return messages(contract)?.addMembers(args.members) + } + case 'remove_members': { + return messages(contract)?.removeMembers(args.members as string[]) + } + // case 'update_per_address_limit': { + // return messages(contract)?.updatePerAddressLimit(args.limit) + // } + case 'freeze': { + return messages(contract)?.freeze() + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} diff --git a/contracts/whitelistMerkleTree/messages/query.ts b/contracts/whitelistMerkleTree/messages/query.ts new file mode 100644 index 0000000..39c6383 --- /dev/null +++ b/contracts/whitelistMerkleTree/messages/query.ts @@ -0,0 +1,66 @@ +import type { WhiteListMerkleTreeInstance } from '../contract' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = [ + 'has_started', + 'has_ended', + 'is_active', + 'admin_list', + 'has_member', + 'config', + 'merkle_root', + 'merkle_tree_uri', +] 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: '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' }, + { id: 'merkle_root', name: 'Merkle Root', description: 'View the whitelist merkle root' }, + { id: 'merkle_tree_uri', name: 'Merkle Tree URI', description: 'View the whitelist merkle tree URI' }, +] + +export interface DispatchQueryProps { + messages: WhiteListMerkleTreeInstance | undefined + type: QueryType + address: string + startAfter?: string + limit?: number + proofHashes?: string[] +} + +export const dispatchQuery = (props: DispatchQueryProps) => { + const { messages, type, address, proofHashes } = props + switch (type) { + case 'has_started': + return messages?.hasStarted() + case 'has_ended': + return messages?.hasEnded() + case 'is_active': + return messages?.isActive() + case 'admin_list': + return messages?.adminList() + case 'has_member': + return messages?.hasMember(address, proofHashes || []) + case 'config': + return messages?.config() + case 'merkle_root': + return messages?.merkleRoot() + case 'merkle_tree_uri': + return messages?.merkleTreeUri() + + default: { + throw new Error('unknown query type') + } + } +} diff --git a/contracts/whitelistMerkleTree/useContract.ts b/contracts/whitelistMerkleTree/useContract.ts new file mode 100644 index 0000000..e61f0e7 --- /dev/null +++ b/contracts/whitelistMerkleTree/useContract.ts @@ -0,0 +1,89 @@ +import { useCallback, useEffect, useState } from 'react' +import { useWallet } from 'utils/wallet' + +import type { + InstantiateResponse, + WhiteListMerkleTreeContract, + WhiteListMerkleTreeInstance, + WhiteListMerkleTreeMessages, +} from './contract' +import { WhiteListMerkleTree as initContract } from './contract' + +export interface UseWhiteListMerkleTreeContractProps { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ) => Promise + + use: (customAddress?: string) => WhiteListMerkleTreeInstance | undefined + + updateContractAddress: (contractAddress: string) => void + + messages: (contractAddress: string) => WhiteListMerkleTreeMessages | undefined +} + +export function useWhiteListMerkleTreeContract(): UseWhiteListMerkleTreeContractProps { + const wallet = useWallet() + + const [address, setAddress] = useState('') + const [whiteListMerkleTree, setWhiteListMerkleTree] = useState() + + useEffect(() => { + setAddress(localStorage.getItem('contract_address') || '') + }, []) + + useEffect(() => { + if (!wallet.isWalletConnected) { + return + } + + const load = async () => { + const client = await wallet.getSigningCosmWasmClient() + const contract = initContract(client, wallet.address || '') + setWhiteListMerkleTree(contract) + } + + load().catch(console.error) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wallet.isWalletConnected, wallet.address]) + + const updateContractAddress = (contractAddress: string) => { + setAddress(contractAddress) + } + + const instantiate = useCallback( + (codeId: number, initMsg: Record, label: string, admin?: string): Promise => { + return new Promise((resolve, reject) => { + if (!whiteListMerkleTree) { + reject(new Error('Contract is not initialized.')) + return + } + whiteListMerkleTree.instantiate(codeId, initMsg, label, admin).then(resolve).catch(reject) + }) + }, + [whiteListMerkleTree], + ) + + const use = useCallback( + (customAddress = ''): WhiteListMerkleTreeInstance | undefined => { + return whiteListMerkleTree?.use(address || customAddress) + }, + [whiteListMerkleTree, address], + ) + + const messages = useCallback( + (customAddress = ''): WhiteListMerkleTreeMessages | undefined => { + return whiteListMerkleTree?.messages(address || customAddress) + }, + [whiteListMerkleTree, address], + ) + + return { + instantiate, + use, + updateContractAddress, + messages, + } +}