diff --git a/.env.example b/.env.example index 160064d..839cbe1 100644 --- a/.env.example +++ b/.env.example @@ -16,8 +16,9 @@ NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars18h7ugh8eaug7wr0w4yjw0ls5s937z35pnkg93 NEXT_PUBLIC_FEATURED_VENDING_FACTORY_ADDRESS="stars14pd96yk3t6gq9l6uyrkg0n5dr09n8rt5y9v3at8x4wl4lrkxhlzq4trqmh" NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1h65nms9gwg4vdktyqj84tu50gwlm34e0eczl5w2ezllxuzfxy9esa9qlt0" NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS="stars1hvu2ghqkcnvhtj2fc6wuazxt4dqcftslp2rwkkkcxy269a35a9pq60ug2q" +NEXT_PUBLIC_VENDING_FACTORY_MERKLE_TREE_ADDRESS="stars167tudcsr9n2y9ljgk4cwxhs0cvkfkk0hh6c3dzngsz7m5s9jmqnsdgr3jy" NEXT_PUBLIC_FEATURED_VENDING_FACTORY_FLEX_ADDRESS="stars1udlmmnmmnnqamh36hy6d7azn3ycv23yymkmg6558ntalvyt2pz7s8lhgcd" -NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS= +# NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS= # NEXT_PUBLIC_VENDING_IBC_ATOM_FACTORY_ADDRESS= # NEXT_PUBLIC_VENDING_IBC_ATOM_UPDATABLE_FACTORY_ADDRESS= @@ -40,6 +41,7 @@ NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS= # NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_ADDRESS= # NEXT_PUBLIC_VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS= # NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS= +# NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS= # NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS= # NEXT_PUBLIC_VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS= @@ -100,6 +102,7 @@ NEXT_PUBLIC_ROYALTY_REGISTRY_ADDRESS="stars1crgx0f70fzksa57hq87wtl8f04h0qyk5la0h NEXT_PUBLIC_INFINITY_SWAP_PROTOCOL_ADDRESS="stars136yp6fl9h66m0cwv8weu4w4aawveuz40992ty0atj5ecjd8z0thqv9xpy5" NEXT_PUBLIC_WHITELIST_CODE_ID=3131 NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=3130 +NEXT_PUBLIC_WHITELIST_MERKLE_TREE_CODE_ID=3625 NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336 NEXT_PUBLIC_BADGE_HUB_ADDRESS="stars1dacun0xn7z73qzdcmq27q3xn6xuprg8e2ugj364784al2v27tklqynhuqa" NEXT_PUBLIC_BADGE_NFT_CODE_ID=1337 @@ -114,6 +117,7 @@ NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev NEXT_PUBLIC_BADGES_URL=https://badges.publicawesome.dev NEXT_PUBLIC_WEBSITE_URL=https:// NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL="https://..." +NEXT_PUBLIC_WHITELIST_MERKLE_TREE_API_URL="https://..." NEXT_PUBLIC_NFT_STORAGE_DEFAULT_API_KEY="..." NEXT_PUBLIC_MEILISEARCH_HOST="https://search.publicawesome.dev" diff --git a/components/WhitelistUpload.tsx b/components/WhitelistUpload.tsx index d4fbfc6..720495e 100644 --- a/components/WhitelistUpload.tsx +++ b/components/WhitelistUpload.tsx @@ -78,7 +78,7 @@ export const WhitelistUpload = ({ onChange }: WhitelistUploadProps) => { const printableData = data?.map((item) => item.replace(regex, '')) const names = printableData?.filter((address) => address !== '' && address.endsWith('.stars')) const strippedNames = names?.map((name) => name.split('.')[0]) - console.log(names) + console.log('names: ', names) if (strippedNames?.length) { await toast .promise(resolveAddresses(strippedNames), { diff --git a/components/collections/creation/WhitelistDetails.tsx b/components/collections/creation/WhitelistDetails.tsx index 2f4bc4f..0c0ca13 100644 --- a/components/collections/creation/WhitelistDetails.tsx +++ b/components/collections/creation/WhitelistDetails.tsx @@ -43,7 +43,7 @@ export interface WhitelistDetailsDataProps { type WhitelistState = 'none' | 'existing' | 'new' -type WhitelistType = 'standard' | 'flex' +type WhitelistType = 'standard' | 'flex' | 'merkletree' export const WhitelistDetails = ({ onChange, @@ -59,6 +59,7 @@ export const WhitelistDetails = ({ const [endDate, setEndDate] = useState(undefined) const [whitelistStandardArray, setWhitelistStandardArray] = useState([]) const [whitelistFlexArray, setWhitelistFlexArray] = useState([]) + const [whitelistMerkleTreeArray, setWhitelistMerkleTreeArray] = useState([]) const [adminsMutable, setAdminsMutable] = useState(true) const whitelistAddressState = useInputState({ @@ -97,7 +98,8 @@ export const WhitelistDetails = ({ const addressListState = useAddressListState() const whitelistFileOnChange = (data: string[]) => { - setWhitelistStandardArray(data) + if (whitelistType === 'standard') setWhitelistStandardArray(data) + if (whitelistType === 'merkletree') setWhitelistMerkleTreeArray(data) } const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => { @@ -130,6 +132,7 @@ export const WhitelistDetails = ({ if (!importedWhitelistDetails) { setWhitelistStandardArray([]) setWhitelistFlexArray([]) + setWhitelistMerkleTreeArray([]) } }, [whitelistType]) @@ -143,7 +146,12 @@ export const WhitelistDetails = ({ .replace(/"/g, '') .replace(/'/g, '') .replace(/ /g, ''), - members: whitelistType === 'standard' ? whitelistStandardArray : whitelistFlexArray, + members: + whitelistType === 'standard' + ? whitelistStandardArray + : whitelistType === 'merkletree' + ? whitelistMerkleTreeArray + : whitelistFlexArray, unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : unitPriceState.value === 0 @@ -173,7 +181,9 @@ export const WhitelistDetails = ({ endDate, whitelistStandardArray, whitelistFlexArray, + whitelistMerkleTreeArray, whitelistState, + whitelistType, addressListState.values, adminsMutable, ]) @@ -211,7 +221,12 @@ export const WhitelistDetails = ({ importedWhitelistDetails.members?.forEach((member) => { setWhitelistStandardArray((standardArray) => [...standardArray, member as string]) }) - } else { + } else if (importedWhitelistDetails.whitelistType === 'merkletree') { + setWhitelistMerkleTreeArray([]) + importedWhitelistDetails.members?.forEach((member) => { + setWhitelistMerkleTreeArray((merkleTreeArray) => [...merkleTreeArray, member as string]) + }) + } else if (importedWhitelistDetails.whitelistType === 'flex') { setWhitelistFlexArray([]) importedWhitelistDetails.members?.forEach((member) => { setWhitelistFlexArray((flexArray) => [ @@ -303,7 +318,7 @@ export const WhitelistDetails = ({ -
+
+
+ { + setWhitelistType('merkletree') + }} + type="radio" + value="merkletree" + /> + +
- - + + + + + + + TXT file that contains the whitelisted addresses + +
+ } + title="Whitelist File" + > + + + 0}> + + +
diff --git a/components/openEdition/OpenEditionMinterCreator.tsx b/components/openEdition/OpenEditionMinterCreator.tsx index 525d6fa..2a98654 100644 --- a/components/openEdition/OpenEditionMinterCreator.tsx +++ b/components/openEdition/OpenEditionMinterCreator.tsx @@ -628,6 +628,7 @@ export const OpenEditionMinterCreator = ({ num_tokens: mintingDetails?.limitType === ('count_limited' as LimitType) ? mintingDetails.tokenCountLimit : null, payment_address: mintingDetails?.paymentAddress || null, + // whitelist: null, }, collection_params: { code_id: collectionDetails?.updatable diff --git a/config/minter.ts b/config/minter.ts index 9d438ab..b6b7710 100644 --- a/config/minter.ts +++ b/config/minter.ts @@ -26,6 +26,7 @@ import { OPEN_EDITION_UPDATABLE_IBC_USK_FACTORY_ADDRESS, VENDING_FACTORY_ADDRESS, VENDING_FACTORY_FLEX_ADDRESS, + VENDING_FACTORY_MERKLE_TREE_ADDRESS, VENDING_FACTORY_UPDATABLE_ADDRESS, VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS, VENDING_IBC_ATOM_FACTORY_ADDRESS, @@ -44,6 +45,7 @@ import { VENDING_IBC_NBTC_UPDATABLE_FACTORY_FLEX_ADDRESS, VENDING_IBC_TIA_FACTORY_ADDRESS, VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS, + VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS, VENDING_IBC_TIA_UPDATABLE_FACTORY_ADDRESS, VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS, VENDING_IBC_USDC_FACTORY_ADDRESS, @@ -84,6 +86,7 @@ export interface MinterInfo { supportedToken: TokenInfo updatable?: boolean flexible?: boolean + merkleTree?: boolean featured?: boolean } @@ -267,6 +270,7 @@ export const vendingStarsMinter: MinterInfo = { supportedToken: stars, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -276,6 +280,7 @@ export const vendingFeaturedStarsMinter: MinterInfo = { supportedToken: stars, updatable: false, flexible: false, + merkleTree: false, featured: true, } @@ -285,6 +290,7 @@ export const vendingUpdatableStarsMinter: MinterInfo = { supportedToken: stars, updatable: true, flexible: false, + merkleTree: false, featured: false, } @@ -294,6 +300,7 @@ export const vendingIbcAtomMinter: MinterInfo = { supportedToken: ibcAtom, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -303,6 +310,7 @@ export const vendingUpdatableIbcAtomMinter: MinterInfo = { supportedToken: ibcAtom, updatable: true, flexible: false, + merkleTree: false, featured: false, } @@ -312,6 +320,7 @@ export const vendingIbcUsdcMinter: MinterInfo = { supportedToken: ibcUsdc, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -321,6 +330,7 @@ export const vendingFeaturedIbcUsdcMinter: MinterInfo = { supportedToken: ibcUsdc, updatable: false, flexible: false, + merkleTree: false, featured: true, } @@ -330,6 +340,7 @@ export const vendingIbcTiaMinter: MinterInfo = { supportedToken: ibcTia, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -339,6 +350,7 @@ export const vendingFeaturedIbcTiaMinter: MinterInfo = { supportedToken: ibcTia, updatable: false, flexible: false, + merkleTree: false, featured: true, } @@ -348,6 +360,7 @@ export const vendingIbcNbtcMinter: MinterInfo = { supportedToken: ibcNbtc, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -357,6 +370,7 @@ export const vendingUpdatableIbcUsdcMinter: MinterInfo = { supportedToken: ibcUsdc, updatable: true, flexible: false, + merkleTree: false, featured: false, } @@ -366,6 +380,7 @@ export const vendingUpdatableIbcTiaMinter: MinterInfo = { supportedToken: ibcTia, updatable: true, flexible: false, + merkleTree: false, featured: false, } @@ -375,6 +390,7 @@ export const vendingUpdatableIbcNbtcMinter: MinterInfo = { supportedToken: ibcNbtc, updatable: true, flexible: false, + merkleTree: false, featured: false, } @@ -384,6 +400,7 @@ export const vendingIbcUskMinter: MinterInfo = { supportedToken: ibcUsk, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -393,6 +410,7 @@ export const vendingUpdatableIbcUskMinter: MinterInfo = { supportedToken: ibcUsk, updatable: true, flexible: false, + merkleTree: false, featured: false, } @@ -402,6 +420,7 @@ export const vendingIbcKujiMinter: MinterInfo = { supportedToken: ibcKuji, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -411,6 +430,7 @@ export const vendingIbcHuahuaMinter: MinterInfo = { supportedToken: ibcHuahua, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -420,6 +440,7 @@ export const vendingIbcCrbrusMinter: MinterInfo = { supportedToken: ibcCrbrus, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -429,6 +450,7 @@ export const vendingNativeStardustMinter: MinterInfo = { supportedToken: nativeStardust, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -438,6 +460,7 @@ export const vendingUpdatableNativeStardustMinter: MinterInfo = { supportedToken: nativeStardust, updatable: true, flexible: false, + merkleTree: false, featured: false, } @@ -447,6 +470,7 @@ export const vendingNativeBrnchMinter: MinterInfo = { supportedToken: nativeBrnch, updatable: false, flexible: false, + merkleTree: false, featured: false, } @@ -456,6 +480,7 @@ export const vendingUpdatableNativeBrnchMinter: MinterInfo = { supportedToken: nativeBrnch, updatable: true, flexible: false, + merkleTree: false, featured: false, } @@ -490,6 +515,7 @@ export const flexibleVendingStarsMinter: MinterInfo = { supportedToken: stars, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -499,6 +525,7 @@ export const flexibleFeaturedVendingStarsMinter: MinterInfo = { supportedToken: stars, updatable: false, flexible: true, + merkleTree: false, featured: true, } @@ -508,6 +535,7 @@ export const flexibleVendingUpdatableStarsMinter: MinterInfo = { supportedToken: stars, updatable: true, flexible: true, + merkleTree: false, featured: false, } @@ -517,6 +545,7 @@ export const flexibleVendingIbcAtomMinter: MinterInfo = { supportedToken: ibcAtom, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -526,6 +555,7 @@ export const flexibleVendingUpdatableIbcAtomMinter: MinterInfo = { supportedToken: ibcAtom, updatable: true, flexible: true, + merkleTree: false, featured: false, } @@ -535,6 +565,7 @@ export const flexibleVendingIbcUsdcMinter: MinterInfo = { supportedToken: ibcUsdc, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -544,6 +575,7 @@ export const flexibleFeaturedVendingIbcUsdcMinter: MinterInfo = { supportedToken: ibcUsdc, updatable: false, flexible: true, + merkleTree: false, featured: true, } @@ -553,6 +585,7 @@ export const flexibleVendingIbcTiaMinter: MinterInfo = { supportedToken: ibcTia, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -562,6 +595,7 @@ export const flexibleFeaturedVendingIbcTiaMinter: MinterInfo = { supportedToken: ibcTia, updatable: false, flexible: true, + merkleTree: false, featured: true, } @@ -571,6 +605,7 @@ export const flexibleVendingIbcNbtcMinter: MinterInfo = { supportedToken: ibcNbtc, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -580,6 +615,7 @@ export const flexibleVendingUpdatableIbcUsdcMinter: MinterInfo = { supportedToken: ibcUsdc, updatable: true, flexible: true, + merkleTree: false, featured: false, } @@ -589,6 +625,7 @@ export const flexibleVendingUpdatableIbcTiaMinter: MinterInfo = { supportedToken: ibcTia, updatable: true, flexible: true, + merkleTree: false, featured: false, } @@ -598,6 +635,7 @@ export const flexibleVendingUpdatableIbcNbtcMinter: MinterInfo = { supportedToken: ibcNbtc, updatable: true, flexible: true, + merkleTree: false, featured: false, } @@ -607,6 +645,7 @@ export const flexibleVendingIbcUskMinter: MinterInfo = { supportedToken: ibcUsk, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -616,6 +655,7 @@ export const flexibleVendingUpdatableIbcUskMinter: MinterInfo = { supportedToken: ibcUsk, updatable: true, flexible: true, + merkleTree: false, featured: false, } @@ -625,6 +665,7 @@ export const flexibleVendingIbcKujiMinter: MinterInfo = { supportedToken: ibcKuji, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -634,6 +675,7 @@ export const flexibleVendingIbcHuahuaMinter: MinterInfo = { supportedToken: ibcHuahua, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -643,6 +685,7 @@ export const flexibleVendingIbcCrbrusMinter: MinterInfo = { supportedToken: ibcCrbrus, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -652,6 +695,7 @@ export const flexibleVendingStrdstMinter: MinterInfo = { supportedToken: nativeStardust, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -661,6 +705,7 @@ export const flexibleVendingBrnchMinter: MinterInfo = { supportedToken: nativeBrnch, updatable: false, flexible: true, + merkleTree: false, featured: false, } @@ -686,3 +731,25 @@ export const flexibleVendingMinterList = [ flexibleVendingStrdstMinter, flexibleVendingBrnchMinter, ] + +export const merkleTreeVendingStarsMinter: MinterInfo = { + id: 'merkletree-vending-stars-minter', + factoryAddress: VENDING_FACTORY_MERKLE_TREE_ADDRESS, + supportedToken: stars, + updatable: false, + flexible: false, + merkleTree: true, + featured: false, +} + +export const merkleTreeVendingIbcTiaMinter: MinterInfo = { + id: 'merkletree-vending-ibc-tia-minter', + factoryAddress: VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS, + supportedToken: ibcTia, + updatable: false, + flexible: false, + merkleTree: true, + featured: false, +} + +export const merkleTreeVendingMinterList = [merkleTreeVendingStarsMinter, merkleTreeVendingIbcTiaMinter] 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, + } +} diff --git a/env.d.ts b/env.d.ts index c7eb5fc..d684dae 100644 --- a/env.d.ts +++ b/env.d.ts @@ -22,12 +22,14 @@ declare namespace NodeJS { readonly NEXT_PUBLIC_OPEN_EDITION_SG721_UPDATABLE_CODE_ID: string readonly NEXT_PUBLIC_WHITELIST_CODE_ID: string readonly NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID: string + readonly NEXT_PUBLIC_WHITELIST_MERKLE_TREE_CODE_ID: string readonly NEXT_PUBLIC_VENDING_MINTER_CODE_ID: string readonly NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID: string readonly NEXT_PUBLIC_VENDING_FACTORY_ADDRESS: string readonly NEXT_PUBLIC_FEATURED_VENDING_FACTORY_ADDRESS: string readonly NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS: string readonly NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS: string + readonly NEXT_PUBLIC_VENDING_FACTORY_MERKLE_TREE_ADDRESS: string readonly NEXT_PUBLIC_FEATURED_VENDING_FACTORY_FLEX_ADDRESS: string readonly NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS: string readonly NEXT_PUBLIC_VENDING_IBC_ATOM_FACTORY_ADDRESS: string @@ -54,6 +56,7 @@ declare namespace NodeJS { readonly NEXT_PUBLIC_FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS: string readonly NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS: string readonly NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS: string + readonly NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS: string readonly NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS: string readonly NEXT_PUBLIC_VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS: string readonly NEXT_PUBLIC_VENDING_IBC_NBTC_FACTORY_FLEX_ADDRESS: string @@ -114,6 +117,7 @@ declare namespace NodeJS { readonly NEXT_PUBLIC_STARGAZE_WEBSITE_URL: string readonly NEXT_PUBLIC_WEBSITE_URL: string readonly NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL: string + readonly NEXT_PUBLIC_WHITELIST_MERKLE_TREE_API_URL: string readonly NEXT_PUBLIC_NFT_STORAGE_DEFAULT_API_KEY: string readonly NEXT_PUBLIC_MEILISEARCH_HOST: string diff --git a/package.json b/package.json index 883afc6..967bf2b 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,11 @@ "@cosmos-kit/leap": "^2.4.3", "@cosmos-kit/leap-metamask-cosmos-snap": "^0.3.3", "@cosmos-kit/react": "^2.9.3", + "crypto-js": "4.1.1", + "@types/crypto-js": "4.2.1", "@fontsource/jetbrains-mono": "^4", "@fontsource/roboto": "^4", + "merkletreejs": "0.3.11", "@leapwallet/cosmos-snap-provider": "0.1.24", "@pinata/sdk": "^1.1.26", "@popperjs/core": "^2", diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index 585c486..c52eb81 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -34,7 +34,12 @@ import { FormControl } from 'components/FormControl' import { LoadingModal } from 'components/LoadingModal' import type { OpenEditionMinterCreatorDataProps } from 'components/openEdition/OpenEditionMinterCreator' import { OpenEditionMinterCreator } from 'components/openEdition/OpenEditionMinterCreator' -import { flexibleVendingMinterList, openEditionMinterList, vendingMinterList } from 'config/minter' +import { + flexibleVendingMinterList, + merkleTreeVendingMinterList, + openEditionMinterList, + vendingMinterList, +} from 'config/minter' import type { TokenInfo } from 'config/token' import { useContracts } from 'contexts/contracts' import { addLogItem } from 'contexts/log' @@ -67,6 +72,8 @@ import { VENDING_FACTORY_UPDATABLE_ADDRESS, WHITELIST_CODE_ID, WHITELIST_FLEX_CODE_ID, + WHITELIST_MERKLE_TREE_API_URL, + WHITELIST_MERKLE_TREE_CODE_ID, } from 'utils/constants' import { checkTokenUri } from 'utils/isValidTokenUri' import { withMetadata } from 'utils/layout' @@ -88,6 +95,7 @@ const CollectionCreationPage: NextPage = () => { baseMinter: baseMinterContract, vendingMinter: vendingMinterContract, whitelist: whitelistContract, + whitelistMerkleTree: whitelistMerkleTreeContract, vendingFactory: vendingFactoryContract, baseFactory: baseFactoryContract, } = useContracts() @@ -513,41 +521,84 @@ const CollectionCreationPage: NextPage = () => { if (!wallet.isWalletConnected) throw new Error('Wallet not connected') if (!whitelistContract) throw new Error('Contract not found') - const standardMsg = { - members: whitelistDetails?.members, - start_time: whitelistDetails?.startTime, - end_time: whitelistDetails?.endTime, - mint_price: coin( - String(Number(whitelistDetails?.unitPrice)), - mintTokenFromVendingFactory ? mintTokenFromVendingFactory.denom : 'ustars', - ), - per_address_limit: whitelistDetails?.perAddressLimit, - member_limit: whitelistDetails?.memberLimit, - admins: whitelistDetails?.admins || [wallet.address], - admins_mutable: whitelistDetails?.adminsMutable, + if (whitelistDetails?.whitelistType === 'standard' || whitelistDetails?.whitelistType === 'flex') { + const standardMsg = { + members: whitelistDetails.members, + start_time: whitelistDetails.startTime, + end_time: whitelistDetails.endTime, + mint_price: coin( + String(Number(whitelistDetails.unitPrice)), + mintTokenFromVendingFactory ? mintTokenFromVendingFactory.denom : 'ustars', + ), + per_address_limit: whitelistDetails.perAddressLimit, + member_limit: whitelistDetails.memberLimit, + admins: whitelistDetails.admins || [wallet.address], + admins_mutable: whitelistDetails.adminsMutable, + } + + const flexMsg = { + members: whitelistDetails.members, + start_time: whitelistDetails.startTime, + end_time: whitelistDetails.endTime, + mint_price: coin( + String(Number(whitelistDetails.unitPrice)), + mintTokenFromVendingFactory ? mintTokenFromVendingFactory.denom : 'ustars', + ), + member_limit: whitelistDetails.memberLimit, + admins: whitelistDetails.admins || [wallet.address], + admins_mutable: whitelistDetails.adminsMutable, + } + + const data = await whitelistContract.instantiate( + whitelistDetails.whitelistType === 'standard' ? WHITELIST_CODE_ID : WHITELIST_FLEX_CODE_ID, + whitelistDetails.whitelistType === 'standard' ? standardMsg : flexMsg, + 'Stargaze Whitelist Contract', + wallet.address, + ) + + return data.contractAddress + } else if (whitelistDetails?.whitelistType === 'merkletree') { + const members = whitelistDetails.members as string[] + const membersCsv = members.join('\n') + const membersBlob = new Blob([membersCsv], { type: 'text/csv' }) + const membersFile = new File([membersBlob], 'members.csv', { type: 'text/csv' }) + const formData = new FormData() + formData.append('whitelist', membersFile) + const response = await axios + .post(`${WHITELIST_MERKLE_TREE_API_URL}/create_whitelist`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + .catch((error) => { + console.log('error', error) + throw new Error('Error fetching root hash from Whitelist Merkle Tree API.') + }) + const rootHash = response.data.root_hash + console.log('rootHash', rootHash) + + const merkleTreeMsg = { + merkle_root: rootHash, + merkle_tree_uri: null, + start_time: whitelistDetails.startTime, + end_time: whitelistDetails.endTime, + mint_price: coin( + String(Number(whitelistDetails.unitPrice)), + mintTokenFromVendingFactory ? mintTokenFromVendingFactory.denom : 'ustars', + ), + per_address_limit: whitelistDetails.perAddressLimit, + admins: whitelistDetails.admins || [wallet.address], + admins_mutable: whitelistDetails.adminsMutable, + } + + const data = await whitelistMerkleTreeContract?.instantiate( + WHITELIST_MERKLE_TREE_CODE_ID, + merkleTreeMsg, + 'Stargaze Whitelist Merkle Tree Contract', + wallet.address, + ) + return data?.contractAddress } - - const flexMsg = { - members: whitelistDetails?.members, - start_time: whitelistDetails?.startTime, - end_time: whitelistDetails?.endTime, - mint_price: coin( - String(Number(whitelistDetails?.unitPrice)), - mintTokenFromVendingFactory ? mintTokenFromVendingFactory.denom : 'ustars', - ), - member_limit: whitelistDetails?.memberLimit, - admins: whitelistDetails?.admins || [wallet.address], - admins_mutable: whitelistDetails?.adminsMutable, - } - - const data = await whitelistContract.instantiate( - whitelistDetails?.whitelistType === 'standard' ? WHITELIST_CODE_ID : WHITELIST_FLEX_CODE_ID, - whitelistDetails?.whitelistType === 'standard' ? standardMsg : flexMsg, - 'Stargaze Whitelist Contract', - wallet.address, - ) - - return data.contractAddress } const instantiateVendingMinter = async (baseUri: string, coverImageUri: string, whitelist?: string) => { @@ -1047,6 +1098,8 @@ const CollectionCreationPage: NextPage = () => { //check if the address belongs to a whitelist contract (see performChecks()) const config = await contract?.config() if (JSON.stringify(config).includes('whale_cap')) whitelistDetails.whitelistType = 'flex' + else if (!JSON.stringify(config).includes('member_limit') || config?.member_limit === 0) + whitelistDetails.whitelistType = 'merkletree' else whitelistDetails.whitelistType = 'standard' if (Number(config?.start_time) !== Number(mintingDetails?.startTime)) { const whitelistStartDate = new Date(Number(config?.start_time) / 1000000) @@ -1084,7 +1137,10 @@ const CollectionCreationPage: NextPage = () => { (!whitelistDetails.perAddressLimit || whitelistDetails.perAddressLimit === 0) ) throw new Error('Per address limit is required') - if (!whitelistDetails.memberLimit || whitelistDetails.memberLimit === 0) + if ( + whitelistDetails.whitelistType !== 'merkletree' && + (!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 equal to or later than the whitelist end time') @@ -1277,13 +1333,16 @@ const CollectionCreationPage: NextPage = () => { const client = await wallet.getCosmWasmClient() const vendingFactoryForSelectedDenom = vendingMinterList .concat(flexibleVendingMinterList) + .concat(merkleTreeVendingMinterList) .find( (minter) => minter.supportedToken === mintingDetails?.selectedMintToken && minter.updatable === collectionDetails?.updatable && minter.flexible === (whitelistDetails?.whitelistType === 'flex') && + minter.merkleTree === (whitelistDetails?.whitelistType === 'merkletree') && minter.featured === isFeaturedCollection, )?.factoryAddress + console.log('Vending Factory: ', vendingFactoryForSelectedDenom) if (vendingFactoryForSelectedDenom) { setIsMatchingVendingFactoryPresent(true) diff --git a/utils/constants.ts b/utils/constants.ts index 32388b1..e285660 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -11,10 +11,12 @@ export const WHITELIST_CODE_ID = parseInt(process.env.NEXT_PUBLIC_WHITELIST_CODE export const WHITELIST_FLEX_CODE_ID = parseInt(process.env.NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID, 10) export const VENDING_MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_VENDING_MINTER_CODE_ID, 10) export const VENDING_MINTER_FLEX_CODE_ID = parseInt(process.env.NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID, 10) +export const WHITELIST_MERKLE_TREE_CODE_ID = parseInt(process.env.NEXT_PUBLIC_WHITELIST_MERKLE_TREE_CODE_ID, 10) export const VENDING_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_ADDRESS export const FEATURED_VENDING_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_FEATURED_VENDING_FACTORY_ADDRESS export const VENDING_FACTORY_UPDATABLE_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS export const VENDING_FACTORY_FLEX_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS +export const VENDING_FACTORY_MERKLE_TREE_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_MERKLE_TREE_ADDRESS export const FEATURED_VENDING_FACTORY_FLEX_ADDRESS = process.env.NEXT_PUBLIC_FEATURED_VENDING_FACTORY_FLEX_ADDRESS export const VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_FLEX_ADDRESS export const VENDING_IBC_ATOM_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_VENDING_IBC_ATOM_FACTORY_ADDRESS @@ -46,6 +48,8 @@ export const FEATURED_VENDING_IBC_USDC_FACTORY_FLEX_ADDRESS = export const VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS = process.env.NEXT_PUBLIC_VENDING_IBC_USDC_UPDATABLE_FACTORY_FLEX_ADDRESS export const VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS = process.env.NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS +export const VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS = + process.env.NEXT_PUBLIC_VENDING_IBC_TIA_FACTORY_MERKLE_TREE_ADDRESS export const FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS = process.env.NEXT_PUBLIC_FEATURED_VENDING_IBC_TIA_FACTORY_FLEX_ADDRESS export const VENDING_IBC_TIA_UPDATABLE_FACTORY_FLEX_ADDRESS = @@ -117,6 +121,7 @@ export const STARGAZE_URL = process.env.NEXT_PUBLIC_STARGAZE_WEBSITE_URL export const BLOCK_EXPLORER_URL = process.env.NEXT_PUBLIC_BLOCK_EXPLORER_URL export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL export const SYNC_COLLECTIONS_API_URL = process.env.NEXT_PUBLIC_SYNC_COLLECTIONS_API_URL +export const WHITELIST_MERKLE_TREE_API_URL = process.env.NEXT_PUBLIC_WHITELIST_MERKLE_TREE_API_URL export const NFT_STORAGE_DEFAULT_API_KEY = process.env.NEXT_PUBLIC_NFT_STORAGE_DEFAULT_API_KEY export const MEILISEARCH_HOST = process.env.NEXT_PUBLIC_MEILISEARCH_HOST diff --git a/utils/merkleTree.ts b/utils/merkleTree.ts new file mode 100644 index 0000000..6883dc4 --- /dev/null +++ b/utils/merkleTree.ts @@ -0,0 +1,31 @@ +import sha256 from 'crypto-js/sha256' +import { MerkleTree } from 'merkletreejs' + +export class WhitelistMerkleTree { + tree: MerkleTree + constructor(members: string[]) { + this.tree = new MerkleTree( + members.map((member) => sha256(member)), + sha256, + { + // sort: true, + // hashLeaves: false, + // sortLeaves: true, + sortPairs: true, + }, + ) + } + + getMerkleRoot() { + return this.tree.getRoot().toString('hex') + } + + getMerkleProof(member: string) { + console.log('this.tree.getProof(sha256(member).toString()): ', this.tree.getProof(sha256(member).toString())) + return this.tree.getProof(sha256(member).toString()).map((item) => item.data.toString('hex')) + } + + verify(proof: string[], member: string) { + return this.tree.verify(proof, sha256(member).toString(), this.tree.getRoot()) + } +}