diff --git a/.env.example b/.env.example index 51989e5..2e84c4f 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,19 @@ -APP_VERSION=0.5.8 +APP_VERSION=0.5.9 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=1911 NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID=1912 NEXT_PUBLIC_VENDING_MINTER_CODE_ID=1909 +NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID=2080 NEXT_PUBLIC_BASE_MINTER_CODE_ID=1910 NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1ynec878x5phexq3hj4zdgvp6r5ayfmxks38kvunwyjugqn3hqeqq3cgtuw" NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1fnfywcnzzwledr93at65qm8gf953tjxgh6u2u4r8n9vsdv7u75eqe7ecn3" +NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS="stars1l5v0vgly8r9jw4exeajnftymr29kn70n23gpl2g5fylaww2pzkhq0rks7c" NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars10rmaxgnjvskuumgv7e2awqkhdqdcygkwrz8a8vvt88szj7fc7xlq5jcs3f" NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS="stars13pw8r33dsnghlxfj2upaywf38z2fc6npuw9maq9e5cpet4v285sscgzjp2" NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr" NEXT_PUBLIC_WHITELIST_CODE_ID=1913 +NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=2005 NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336 NEXT_PUBLIC_BADGE_HUB_ADDRESS="stars1dacun0xn7z73qzdcmq27q3xn6xuprg8e2ugj364784al2v27tklqynhuqa" NEXT_PUBLIC_BADGE_NFT_CODE_ID=1337 diff --git a/components/WhitelistFlexUpload.tsx b/components/WhitelistFlexUpload.tsx new file mode 100644 index 0000000..96033f4 --- /dev/null +++ b/components/WhitelistFlexUpload.tsx @@ -0,0 +1,116 @@ +import { toUtf8 } from '@cosmjs/encoding' +import clsx from 'clsx' +import { useWallet } from 'contexts/wallet' +import React, { useState } from 'react' +import { toast } from 'react-hot-toast' +import { SG721_NAME_ADDRESS } from 'utils/constants' +import { csvToFlexList } from 'utils/csvToFlexList' +import { isValidAddress } from 'utils/isValidAddress' +import { isValidFlexListFile } from 'utils/isValidFlexListFile' + +export interface WhitelistFlexMember { + address: string + mint_count: number +} + +interface WhitelistFlexUploadProps { + onChange: (data: WhitelistFlexMember[]) => void +} + +export const WhitelistFlexUpload = ({ onChange }: WhitelistFlexUploadProps) => { + const wallet = useWallet() + const [resolvedMemberData, setResolvedMemberData] = useState([]) + + const resolveMemberData = async (memberData: WhitelistFlexMember[]) => { + if (!memberData.length) return [] + await new Promise((resolve) => { + let i = 0 + memberData.map(async (data) => { + if (!wallet.client) throw new Error('Wallet not connected') + await wallet.client + .queryContractRaw( + SG721_NAME_ADDRESS, + toUtf8( + Buffer.from( + `0006${Buffer.from('tokens').toString('hex')}${Buffer.from( + data.address.trim().substring(0, data.address.lastIndexOf('.stars')), + ).toString('hex')}`, + 'hex', + ).toString(), + ), + ) + .then((res) => { + const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri + if (tokenUri && isValidAddress(tokenUri)) + resolvedMemberData.push({ address: tokenUri, mint_count: Number(data.mint_count) }) + else toast.error(`Resolved address is empty or invalid for the name: ${data.address}`) + }) + .catch((e) => { + console.log(e) + toast.error(`Error resolving address for the name: ${data.address}`) + }) + + i++ + if (i === memberData.length) resolve(resolvedMemberData) + }) + }) + return resolvedMemberData + } + + const onFileChange = (event: React.ChangeEvent) => { + setResolvedMemberData([]) + if (!event.target.files) return toast.error('Error opening file') + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!event.target.files[0]?.name.endsWith('.csv')) { + toast.error('Please select a .csv file!') + return onChange([]) + } + const reader = new FileReader() + reader.onload = async (e: ProgressEvent) => { + try { + if (!e.target?.result) return toast.error('Error parsing file.') + // eslint-disable-next-line @typescript-eslint/no-base-to-string + const memberData = csvToFlexList(e.target.result.toString()) + console.log(memberData) + if (!isValidFlexListFile(memberData)) { + event.target.value = '' + return onChange([]) + } + await resolveMemberData(memberData.filter((data) => data.address.trim().endsWith('.stars'))).finally(() => { + return onChange( + memberData + .filter((data) => data.address.startsWith('stars') && !data.address.endsWith('.stars')) + .map((data) => ({ + address: data.address.trim(), + mint_count: Number(data.mint_count), + })) + .concat(resolvedMemberData), + ) + }) + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + } + } + reader.readAsText(event.target.files[0]) + } + + return ( +
+ +
+ ) +} diff --git a/components/collections/creation/WhitelistDetails.tsx b/components/collections/creation/WhitelistDetails.tsx index 66fde9e..488a3e7 100644 --- a/components/collections/creation/WhitelistDetails.tsx +++ b/components/collections/creation/WhitelistDetails.tsx @@ -4,6 +4,8 @@ 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 type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' +import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload' import React, { useEffect, useState } from 'react' import { isValidAddress } from 'utils/isValidAddress' @@ -17,9 +19,10 @@ interface WhitelistDetailsProps { } export interface WhitelistDetailsDataProps { - whitelistType: WhitelistState + whitelistState: WhitelistState + whitelistType: WhitelistType contractAddress?: string - members?: string[] + members?: string[] | WhitelistFlexMember[] unitPrice?: string startTime?: string endTime?: string @@ -31,11 +34,15 @@ export interface WhitelistDetailsDataProps { type WhitelistState = 'none' | 'existing' | 'new' +type WhitelistType = 'standard' | 'flex' + export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { const [whitelistState, setWhitelistState] = useState('none') + const [whitelistType, setWhitelistType] = useState('standard') const [startDate, setStartDate] = useState(undefined) const [endDate, setEndDate] = useState(undefined) - const [whitelistArray, setWhitelistArray] = useState([]) + const [whitelistStandardArray, setWhitelistStandardArray] = useState([]) + const [whitelistFlexArray, setWhitelistFlexArray] = useState([]) const [adminsMutable, setAdminsMutable] = useState(true) const whitelistAddressState = useInputState({ @@ -72,19 +79,29 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { const addressListState = useAddressListState() const whitelistFileOnChange = (data: string[]) => { - setWhitelistArray(data) + setWhitelistStandardArray(data) + } + + const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => { + setWhitelistFlexArray(whitelistData) } + useEffect(() => { + setWhitelistStandardArray([]) + setWhitelistFlexArray([]) + }, [whitelistType]) + useEffect(() => { const data: WhitelistDetailsDataProps = { - whitelistType: whitelistState, + whitelistState, + whitelistType, contractAddress: whitelistAddressState.value .toLowerCase() .replace(/,/g, '') .replace(/"/g, '') .replace(/'/g, '') .replace(/ /g, ''), - members: whitelistArray, + members: whitelistType === 'standard' ? whitelistStandardArray : whitelistFlexArray, unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : '', startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '', endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '', @@ -108,7 +125,8 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { perAddressLimitState.value, startDate, endDate, - whitelistArray, + whitelistStandardArray, + whitelistFlexArray, whitelistState, addressListState.values, adminsMutable, @@ -125,6 +143,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { name="whitelistRadioOptions1" onClick={() => { setWhitelistState('none') + setWhitelistType('standard') }} type="radio" value="None" @@ -181,11 +200,54 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { +
+
+ { + setWhitelistType('standard') + }} + type="radio" + value="nft-storage" + /> + +
+ +
+ { + setWhitelistType('flex') + }} + type="radio" + value="flex" + /> + +
+
- + + + { title="Administrator Addresses" />
- - - - 0}> - + + + + + 0}> + + + + + + + + 0}> + + diff --git a/components/forms/FlexMemberAttributes.hooks.ts b/components/forms/FlexMemberAttributes.hooks.ts new file mode 100644 index 0000000..3cc1dc8 --- /dev/null +++ b/components/forms/FlexMemberAttributes.hooks.ts @@ -0,0 +1,32 @@ +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' +import { useMemo, useState } from 'react' +import { uid } from 'utils/random' + +export function useFlexMemberAttributesState() { + const [record, setRecord] = useState>(() => ({})) + + const entries = useMemo(() => Object.entries(record), [record]) + const values = useMemo(() => Object.values(record), [record]) + + function add(attribute: WhitelistFlexMember = { address: '', mint_count: 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/FlexMemberAttributes.tsx b/components/forms/FlexMemberAttributes.tsx new file mode 100644 index 0000000..4934145 --- /dev/null +++ b/components/forms/FlexMemberAttributes.tsx @@ -0,0 +1,133 @@ +import { toUtf8 } from '@cosmjs/encoding' +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' +import { useWallet } from 'contexts/wallet' +import { useEffect, useId, useMemo } from 'react' +import toast from 'react-hot-toast' +import { FaMinus, FaPlus } from 'react-icons/fa' +import { SG721_NAME_ADDRESS } from 'utils/constants' +import { isValidAddress } from 'utils/isValidAddress' + +import { useInputState, useNumberInputState } from './FormInput.hooks' + +export interface FlexMemberAttributesProps { + title: string + subtitle?: string + isRequired?: boolean + attributes: [string, WhitelistFlexMember][] + onAdd: () => void + onChange: (key: string, attribute: WhitelistFlexMember) => void + onRemove: (key: string) => void +} + +export function FlexMemberAttributes(props: FlexMemberAttributesProps) { + const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props + + return ( + + {attributes.map(([id], i) => ( + + ))} + + ) +} + +export interface MemberAttributeProps { + id: string + isLast: boolean + onAdd: FlexMemberAttributesProps['onAdd'] + onChange: FlexMemberAttributesProps['onChange'] + onRemove: FlexMemberAttributesProps['onRemove'] + defaultAttribute: WhitelistFlexMember +} + +export function FlexMemberAttribute({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: MemberAttributeProps) { + const wallet = useWallet() + 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 mintCountState = useNumberInputState({ + id: `mint-count-${htmlId}`, + name: `mint-count-${htmlId}`, + title: `Mint Count`, + defaultValue: defaultAttribute.mint_count, + }) + + useEffect(() => { + onChange(id, { address: addressState.value, mint_count: mintCountState.value }) + }, [addressState.value, mintCountState.value, id]) + + const resolveAddress = async (name: string) => { + if (!wallet.client) throw new Error('Wallet not connected') + await wallet.client + .queryContractRaw( + SG721_NAME_ADDRESS, + toUtf8( + Buffer.from( + `0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`, + 'hex', + ).toString(), + ), + ) + .then((res) => { + const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri + if (tokenUri && isValidAddress(tokenUri)) onChange(id, { address: tokenUri, mint_count: mintCountState.value }) + else { + toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`) + onChange(id, { address: '', mint_count: mintCountState.value }) + } + }) + .catch((err) => { + toast.error(`Error resolving address for the name: ${name}.stars`) + console.error(err) + onChange(id, { address: '', mint_count: mintCountState.value }) + }) + } + useEffect(() => { + if (addressState.value.endsWith('.stars')) { + void resolveAddress(addressState.value.split('.')[0]) + } else { + onChange(id, { + address: addressState.value, + mint_count: mintCountState.value, + }) + } + }, [addressState.value, id]) + + return ( +
+ + + +
+ +
+
+ ) +} diff --git a/contracts/vendingFactory/contract.ts b/contracts/vendingFactory/contract.ts index f58a34e..30f59d0 100644 --- a/contracts/vendingFactory/contract.ts +++ b/contracts/vendingFactory/contract.ts @@ -1,7 +1,9 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable no-nested-ternary */ import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import type { Coin } from '@cosmjs/proto-signing' import type { logs } from '@cosmjs/stargate' -import { VENDING_FACTORY_ADDRESS } from 'utils/constants' +import { VENDING_FACTORY_ADDRESS, VENDING_FACTORY_FLEX_ADDRESS } from 'utils/constants' import { VENDING_FACTORY_UPDATABLE_ADDRESS } from '../../utils/constants' @@ -23,11 +25,17 @@ export interface VendingFactoryInstance { msg: Record, funds: Coin[], updatable?: boolean, + flex?: boolean, ) => Promise } export interface VendingFactoryMessages { - createVendingMinter: (msg: Record, funds: Coin[], updatable?: boolean) => CreateVendingMinterMessage + createVendingMinter: ( + msg: Record, + funds: Coin[], + updatable?: boolean, + flex?: boolean, + ) => CreateVendingMinterMessage } export interface CreateVendingMinterMessage { @@ -53,10 +61,11 @@ export const vendingFactory = (client: SigningCosmWasmClient, txSigner: string): msg: Record, funds: Coin[], updatable?: boolean, + flex?: boolean, ): Promise => { const result = await client.execute( senderAddress, - updatable ? VENDING_FACTORY_UPDATABLE_ADDRESS : VENDING_FACTORY_ADDRESS, + flex ? VENDING_FACTORY_FLEX_ADDRESS : updatable ? VENDING_FACTORY_UPDATABLE_ADDRESS : VENDING_FACTORY_ADDRESS, msg, 'auto', '', @@ -82,6 +91,7 @@ export const vendingFactory = (client: SigningCosmWasmClient, txSigner: string): msg: Record, funds: Coin[], updatable?: boolean, + flex?: boolean, ): CreateVendingMinterMessage => { return { sender: txSigner, diff --git a/contracts/vendingFactory/messages/execute.ts b/contracts/vendingFactory/messages/execute.ts index 884ac6d..adc4ce4 100644 --- a/contracts/vendingFactory/messages/execute.ts +++ b/contracts/vendingFactory/messages/execute.ts @@ -11,6 +11,7 @@ export interface DispatchExecuteArgs { msg: Record funds: Coin[] updatable?: boolean + flex?: boolean } export const dispatchExecute = async (args: DispatchExecuteArgs) => { @@ -18,12 +19,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { if (!messages) { throw new Error('cannot dispatch execute, messages is not defined') } - return messages.createVendingMinter(txSigner, args.msg, args.funds, args.updatable) + return messages.createVendingMinter(txSigner, args.msg, args.funds, args.updatable, args.flex) } export const previewExecutePayload = (args: DispatchExecuteArgs) => { // eslint-disable-next-line react-hooks/rules-of-hooks const { messages } = useVendingFactoryContract() const { contract } = args - return messages(contract)?.createVendingMinter(args.msg, args.funds, args.updatable) + return messages(contract)?.createVendingMinter(args.msg, args.funds, args.updatable, args.flex) } diff --git a/contracts/whitelist/contract.ts b/contracts/whitelist/contract.ts index 296602d..bccd07f 100644 --- a/contracts/whitelist/contract.ts +++ b/contracts/whitelist/contract.ts @@ -1,6 +1,7 @@ import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import type { Coin } from '@cosmjs/proto-signing' import { coin } from '@cosmjs/proto-signing' +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' export interface InstantiateResponse { readonly contractAddress: string @@ -30,7 +31,7 @@ export interface WhiteListInstance { //Execute updateStartTime: (startTime: string) => Promise updateEndTime: (endTime: string) => Promise - addMembers: (memberList: string[]) => Promise + addMembers: (memberList: string[] | WhitelistFlexMember[]) => Promise removeMembers: (memberList: string[]) => Promise updatePerAddressLimit: (limit: number) => Promise increaseMemberLimit: (limit: number) => Promise @@ -41,7 +42,7 @@ export interface WhiteListInstance { export interface WhitelistMessages { updateStartTime: (startTime: string) => UpdateStartTimeMessage updateEndTime: (endTime: string) => UpdateEndTimeMessage - addMembers: (memberList: string[]) => AddMembersMessage + addMembers: (memberList: string[] | WhitelistFlexMember[]) => AddMembersMessage removeMembers: (memberList: string[]) => RemoveMembersMessage updatePerAddressLimit: (limit: number) => UpdatePerAddressLimitMessage increaseMemberLimit: (limit: number) => IncreaseMemberLimitMessage @@ -86,7 +87,7 @@ export interface AddMembersMessage { sender: string contract: string msg: { - add_members: { to_add: string[] } + add_members: { to_add: string[] | WhitelistFlexMember[] } } funds: Coin[] } @@ -182,7 +183,7 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit return res.transactionHash } - const addMembers = async (memberList: string[]): Promise => { + const addMembers = async (memberList: string[] | WhitelistFlexMember[]): Promise => { const res = await client.execute( txSigner, contractAddress, @@ -307,7 +308,7 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit } } - const addMembers = (memberList: string[]) => { + const addMembers = (memberList: string[] | WhitelistFlexMember[]) => { return { sender: txSigner, contract: contractAddress, diff --git a/contracts/whitelist/messages/execute.ts b/contracts/whitelist/messages/execute.ts index a0fbdec..2e528bd 100644 --- a/contracts/whitelist/messages/execute.ts +++ b/contracts/whitelist/messages/execute.ts @@ -1,3 +1,4 @@ +import type { WhitelistFlexMember } from '../../../components/WhitelistFlexUpload' import type { WhiteListInstance } from '../index' import { useWhiteListContract } from '../index' @@ -68,23 +69,16 @@ export interface DispatchExecuteProps { [k: string]: unknown } -type Select = T - /** @see {@link WhiteListInstance} */ -export type DispatchExecuteArgs = { +export interface DispatchExecuteArgs { contract: string messages?: WhiteListInstance -} & ( - | { type: undefined } - | { type: Select<'update_start_time'>; timestamp: string } - | { type: Select<'update_end_time'>; timestamp: string } - | { type: Select<'add_members'>; members: string[] } - | { type: Select<'remove_members'>; members: string[] } - | { type: Select<'update_per_address_limit'>; limit: number } - | { type: Select<'increase_member_limit'>; limit: number } - | { type: Select<'update_admins'>; admins: string[] } - | { type: Select<'freeze'> } -) + type: string | undefined + timestamp: string + members: string[] | WhitelistFlexMember[] + limit: number + admins: string[] +} export const dispatchExecute = async (args: DispatchExecuteArgs) => { const { messages } = args @@ -105,7 +99,7 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { return messages.addMembers(args.members) } case 'remove_members': { - return messages.removeMembers(args.members) + return messages.removeMembers(args.members as string[]) } case 'update_per_address_limit': { return messages.updatePerAddressLimit(args.limit) @@ -140,7 +134,7 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { return messages(contract)?.addMembers(args.members) } case 'remove_members': { - return messages(contract)?.removeMembers(args.members) + return messages(contract)?.removeMembers(args.members as string[]) } case 'update_per_address_limit': { return messages(contract)?.updatePerAddressLimit(args.limit) diff --git a/env.d.ts b/env.d.ts index 38d4234..8652b55 100644 --- a/env.d.ts +++ b/env.d.ts @@ -17,9 +17,12 @@ declare namespace NodeJS { readonly NEXT_PUBLIC_SG721_CODE_ID: string readonly NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID: string readonly NEXT_PUBLIC_WHITELIST_CODE_ID: string + readonly NEXT_PUBLIC_WHITELIST_FLEX_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_VENDING_FACTORY_UPDATABLE_ADDRESS: string + readonly NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS: string readonly NEXT_PUBLIC_BASE_FACTORY_ADDRESS: string readonly NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS: string readonly NEXT_PUBLIC_SG721_NAME_ADDRESS: string diff --git a/package.json b/package.json index daa9b97..662d7f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.5.8", + "version": "0.5.9", "workspaces": [ "packages/*" ], diff --git a/pages/collections/actions.tsx b/pages/collections/actions.tsx index 445a199..c56a059 100644 --- a/pages/collections/actions.tsx +++ b/pages/collections/actions.tsx @@ -145,7 +145,7 @@ const CollectionActionsPage: NextPage = () => { }) .catch((err) => { console.log(err) - setMinterType('base') + setSg721Type('base') console.log('Unable to retrieve contract type. Defaulting to "base".') }) }, [debouncedSg721ContractState, wallet.client]) diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index 54b67a4..bc3fc0b 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -49,8 +49,10 @@ import { SG721_UPDATABLE_CODE_ID, STARGAZE_URL, VENDING_FACTORY_ADDRESS, + VENDING_FACTORY_FLEX_ADDRESS, VENDING_FACTORY_UPDATABLE_ADDRESS, WHITELIST_CODE_ID, + WHITELIST_FLEX_CODE_ID, } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' @@ -93,9 +95,11 @@ const CollectionCreationPage: NextPage = () => { const [vendingMinterCreationFee, setVendingMinterCreationFee] = useState(null) const [baseMinterCreationFee, setBaseMinterCreationFee] = useState(null) const [vendingMinterUpdatableCreationFee, setVendingMinterUpdatableCreationFee] = useState(null) + const [vendingMinterFlexCreationFee, setVendingMinterFlexCreationFee] = useState(null) const [baseMinterUpdatableCreationFee, setBaseMinterUpdatableCreationFee] = useState(null) const [minimumMintPrice, setMinimumMintPrice] = useState('0') const [minimumUpdatableMintPrice, setMinimumUpdatableMintPrice] = useState('0') + const [minimumFlexMintPrice, setMinimumFlexMintPrice] = useState('0') const [uploading, setUploading] = useState(false) const [isMintingComplete, setIsMintingComplete] = useState(false) @@ -221,8 +225,8 @@ const CollectionCreationPage: NextPage = () => { setCoverImageUrl(coverImageUri) let whitelist: string | undefined - if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress - else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() + if (whitelistDetails?.whitelistState === 'existing') whitelist = whitelistDetails.contractAddress + else if (whitelistDetails?.whitelistState === 'new') whitelist = await instantiateWhitelist() setWhitelistContractAddress(whitelist as string) await instantiateVendingMinter(baseUri, coverImageUri, whitelist) @@ -231,8 +235,8 @@ const CollectionCreationPage: NextPage = () => { setCoverImageUrl(uploadDetails?.imageUrl as string) let whitelist: string | undefined - if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress - else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() + if (whitelistDetails?.whitelistState === 'existing') whitelist = whitelistDetails.contractAddress + else if (whitelistDetails?.whitelistState === 'new') whitelist = await instantiateWhitelist() setWhitelistContractAddress(whitelist as string) await instantiateVendingMinter(baseTokenUri as string, coverImageUrl as string, whitelist) @@ -401,7 +405,7 @@ const CollectionCreationPage: NextPage = () => { if (!wallet.initialized) throw new Error('Wallet not connected') if (!whitelistContract) throw new Error('Contract not found') - const msg = { + const standardMsg = { members: whitelistDetails?.members, start_time: whitelistDetails?.startTime, end_time: whitelistDetails?.endTime, @@ -412,9 +416,19 @@ const CollectionCreationPage: NextPage = () => { admins_mutable: whitelistDetails?.adminsMutable, } + const flexMsg = { + members: whitelistDetails?.members, + start_time: whitelistDetails?.startTime, + end_time: whitelistDetails?.endTime, + mint_price: coin(String(Number(whitelistDetails?.unitPrice)), 'ustars'), + member_limit: whitelistDetails?.memberLimit, + admins: whitelistDetails?.admins || [wallet.address], + admins_mutable: whitelistDetails?.adminsMutable, + } + const data = await whitelistContract.instantiate( - WHITELIST_CODE_ID, - msg, + whitelistDetails?.whitelistType === 'standard' ? WHITELIST_CODE_ID : WHITELIST_FLEX_CODE_ID, + whitelistDetails?.whitelistType === 'standard' ? standardMsg : flexMsg, 'Stargaze Whitelist Contract', wallet.address, ) @@ -469,20 +483,40 @@ const CollectionCreationPage: NextPage = () => { }, } + console.log('Whitelist State: ', whitelistDetails?.whitelistState) + console.log('Whitelist Type: ', whitelistDetails?.whitelistType) + console.log( + 'Factory Address: ', + whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex' + ? VENDING_FACTORY_FLEX_ADDRESS + : collectionDetails?.updatable + ? VENDING_FACTORY_UPDATABLE_ADDRESS + : VENDING_FACTORY_ADDRESS, + ) + + console.log('Whitelist: ', whitelist) const payload: VendingFactoryDispatchExecuteArgs = { - contract: collectionDetails?.updatable ? VENDING_FACTORY_UPDATABLE_ADDRESS : VENDING_FACTORY_ADDRESS, + contract: + whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex' + ? VENDING_FACTORY_FLEX_ADDRESS + : collectionDetails?.updatable + ? VENDING_FACTORY_UPDATABLE_ADDRESS + : VENDING_FACTORY_ADDRESS, messages: vendingFactoryMessages, txSigner: wallet.address, msg, funds: [ coin( - collectionDetails?.updatable + whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex' + ? (vendingMinterFlexCreationFee as string) + : collectionDetails?.updatable ? (vendingMinterUpdatableCreationFee as string) : (vendingMinterCreationFee as string), 'ustars', ), ], updatable: collectionDetails?.updatable, + flex: whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex', } const data = await vendingFactoryDispatchExecute(payload) setTransactionHash(data.transactionHash) @@ -786,7 +820,10 @@ const CollectionCreationPage: NextPage = () => { const checkMintingDetails = () => { if (!mintingDetails) throw new Error('Please fill out the minting details') if (mintingDetails.numTokens < 1 || mintingDetails.numTokens > 10000) throw new Error('Invalid number of tokens') - if (collectionDetails?.updatable) { + if (whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex') { + if (Number(mintingDetails.unitPrice) < Number(minimumFlexMintPrice)) + throw new Error(`Invalid unit price: The minimum unit price is ${Number(minimumFlexMintPrice) / 1000000} STARS`) + } else if (collectionDetails?.updatable) { if (Number(mintingDetails.unitPrice) < Number(minimumUpdatableMintPrice)) throw new Error( `Invalid unit price: The minimum unit price is ${Number(minimumUpdatableMintPrice) / 1000000} STARS`, @@ -822,12 +859,14 @@ const CollectionCreationPage: NextPage = () => { const checkWhitelistDetails = async () => { if (!whitelistDetails) throw new Error('Please fill out the whitelist details') - if (whitelistDetails.whitelistType === 'existing') { + if (whitelistDetails.whitelistState === 'existing') { if (whitelistDetails.contractAddress === '') throw new Error('Whitelist contract address is required') else { const contract = whitelistContract?.use(whitelistDetails.contractAddress) //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 whitelistDetails.whitelistType = 'standard' if (Number(config?.start_time) !== Number(mintingDetails?.startTime)) { const whitelistStartDate = new Date(Number(config?.start_time) / 1000000) throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`) @@ -852,14 +891,17 @@ const CollectionCreationPage: NextPage = () => { } } } - } else if (whitelistDetails.whitelistType === 'new') { + } else if (whitelistDetails.whitelistState === 'new') { if (whitelistDetails.members?.length === 0) throw new Error('Whitelist member list cannot be empty') if (whitelistDetails.unitPrice === '') throw new Error('Whitelist unit price is required') if (Number(whitelistDetails.unitPrice) < 0) throw new Error('Invalid unit price: The unit price cannot be negative') if (whitelistDetails.startTime === '') throw new Error('Start time is required') if (whitelistDetails.endTime === '') throw new Error('End time is required') - if (!whitelistDetails.perAddressLimit || whitelistDetails.perAddressLimit === 0) + if ( + whitelistDetails.whitelistType === 'standard' && + (!whitelistDetails.perAddressLimit || whitelistDetails.perAddressLimit === 0) + ) throw new Error('Per address limit is required') if (!whitelistDetails.memberLimit || whitelistDetails.memberLimit === 0) throw new Error('Member limit is required') @@ -945,14 +987,25 @@ const CollectionCreationPage: NextPage = () => { setVendingMinterUpdatableCreationFee(vendingFactoryUpdatableParameters?.params?.creation_fee?.amount) setMinimumUpdatableMintPrice(vendingFactoryUpdatableParameters?.params?.min_mint_price?.amount) } + if (VENDING_FACTORY_FLEX_ADDRESS) { + const vendingFactoryFlexParameters = await client.queryContractSmart(VENDING_FACTORY_FLEX_ADDRESS, { + params: {}, + }) + setVendingMinterFlexCreationFee(vendingFactoryFlexParameters?.params?.creation_fee?.amount) + setMinimumFlexMintPrice(vendingFactoryFlexParameters?.params?.min_mint_price?.amount) + } } const checkwalletBalance = () => { if (!wallet.initialized) throw new Error('Wallet not connected.') - if (minterType === 'vending' && whitelistDetails?.whitelistType === 'new' && whitelistDetails.memberLimit) { + if (minterType === 'vending' && whitelistDetails?.whitelistState === 'new' && whitelistDetails.memberLimit) { const amountNeeded = Math.ceil(Number(whitelistDetails.memberLimit) / 1000) * 100000000 + - (collectionDetails?.updatable ? Number(vendingMinterUpdatableCreationFee) : Number(vendingMinterCreationFee)) + (whitelistDetails.whitelistType === 'flex' + ? Number(vendingMinterFlexCreationFee) + : collectionDetails?.updatable + ? Number(vendingMinterUpdatableCreationFee) + : Number(vendingMinterCreationFee)) if (amountNeeded >= Number(wallet.balance[0].amount)) throw new Error( `Insufficient wallet balance to instantiate the required contracts. Needed amount: ${( @@ -962,7 +1015,9 @@ const CollectionCreationPage: NextPage = () => { } else { const amountNeeded = minterType === 'vending' - ? collectionDetails?.updatable + ? whitelistDetails?.whitelistState === 'existing' && whitelistDetails.whitelistType === 'flex' + ? Number(vendingMinterFlexCreationFee) + : collectionDetails?.updatable ? Number(vendingMinterUpdatableCreationFee) : Number(vendingMinterCreationFee) : collectionDetails?.updatable diff --git a/pages/contracts/whitelist/execute.tsx b/pages/contracts/whitelist/execute.tsx index d38339b..b085b05 100644 --- a/pages/contracts/whitelist/execute.tsx +++ b/pages/contracts/whitelist/execute.tsx @@ -1,3 +1,6 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable no-nested-ternary */ +import { toUtf8 } from '@cosmjs/encoding' import { Alert } from 'components/Alert' import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' @@ -7,6 +10,8 @@ import { useExecuteComboboxState } from 'components/contracts/whitelist/ExecuteC import { FormControl } from 'components/FormControl' import { AddressList } from 'components/forms/AddressList' import { useAddressListState } from 'components/forms/AddressList.hooks' +import { FlexMemberAttributes } from 'components/forms/FlexMemberAttributes' +import { useFlexMemberAttributesState } from 'components/forms/FlexMemberAttributes.hooks' import { AddressInput, NumberInput } from 'components/forms/FormInput' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { InputDateTime } from 'components/InputDateTime' @@ -14,6 +19,8 @@ import { JsonPreview } from 'components/JsonPreview' import { LinkTabs } from 'components/LinkTabs' import { whitelistLinkTabs } from 'components/LinkTabs.data' import { TransactionHash } from 'components/TransactionHash' +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' +import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload' import { WhitelistUpload } from 'components/WhitelistUpload' import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' @@ -27,6 +34,7 @@ import { useEffect, useMemo, useState } from 'react' import { toast } from 'react-hot-toast' import { FaArrowRight } from 'react-icons/fa' import { useMutation } from 'react-query' +import { useDebounce } from 'utils/debounce' import { isValidAddress } from 'utils/isValidAddress' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' @@ -37,6 +45,8 @@ const WhitelistExecutePage: NextPage = () => { const [lastTx, setLastTx] = useState('') const [memberList, setMemberList] = useState([]) + const [flexMemberList, setFlexMemberList] = useState([]) + const [whitelistType, setWhitelistType] = useState<'standard' | 'flex'>('standard') const comboboxState = useExecuteComboboxState() const type = comboboxState.value?.id @@ -45,6 +55,8 @@ const WhitelistExecutePage: NextPage = () => { const addressListState = useAddressListState() + const flexAddressListState = useFlexMemberAttributesState() + const contractState = useInputState({ id: 'contract-address', name: 'contract-address', @@ -53,6 +65,8 @@ const WhitelistExecutePage: NextPage = () => { }) const contractAddress = contractState.value + const debouncedWhitelistContractState = useDebounce(contractState.value, 300) + const limitState = useNumberInputState({ id: 'limit', name: 'limit', @@ -63,7 +77,9 @@ 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 showMemberList = isEitherType(type, ['add_members']) + const showFlexMemberList = isEitherType(type, ['add_members']) + const showRemoveMemberList = isEitherType(type, ['remove_members']) const showAdminList = isEitherType(type, ['update_admins']) const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value]) @@ -73,14 +89,44 @@ const WhitelistExecutePage: NextPage = () => { type, limit: limitState.value, timestamp: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', - members: [ - ...new Set( - addressListState.values - .map((a) => a.address.trim()) - .filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')) - .concat(memberList), - ), - ], + members: + whitelistType === 'standard' + ? [ + ...new Set( + addressListState.values + .map((a) => a.address.trim()) + .filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')) + .concat(memberList), + ), + ] + : type === 'add_members' + ? [ + ...new Set( + flexAddressListState.values + .concat(flexMemberList) + .filter((obj, index, self) => index === self.findIndex((t) => t.address.trim() === obj.address.trim())) + .filter( + (member) => + member.address !== '' && + isValidAddress(member.address.trim()) && + member.address.startsWith('stars'), + ) + .map((member) => { + return { + address: member.address.trim(), + mint_count: Math.round(member.mint_count), + } + }), + ), + ] + : [ + ...new Set( + addressListState.values + .map((a) => a.address.trim()) + .filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')) + .concat(memberList), + ), + ], admins: [ ...new Set( addressListState.values @@ -122,11 +168,55 @@ const WhitelistExecutePage: NextPage = () => { } // 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) }, []) + useEffect(() => { + flexAddressListState.reset() + flexAddressListState.add({ + address: '', + mint_count: 0, + }) + }, []) + + useEffect(() => { + async function getWhitelistContractType() { + if (wallet.client && debouncedWhitelistContractState.length > 0) { + const client = wallet.client + const data = await toast.promise( + client.queryContractRaw( + debouncedWhitelistContractState, + toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()), + ), + { + loading: 'Retrieving Whitelist type...', + error: 'Whitelist type retrieval failed.', + success: 'Whitelist type retrieved.', + }, + ) + const contractType: string = JSON.parse(new TextDecoder().decode(data as Uint8Array)).contract + console.log(contractType) + return contractType + } + } + void getWhitelistContractType() + .then((contractType) => { + if (contractType?.includes('flex')) { + setWhitelistType('flex') + } else { + setWhitelistType('standard') + } + }) + .catch((err) => { + console.log(err) + setWhitelistType('standard') + console.log('Unable to retrieve contract type. Defaulting to "standard".') + }) + }, [debouncedWhitelistContractState, wallet.client]) + return (
@@ -154,7 +244,7 @@ const WhitelistExecutePage: NextPage = () => { setTimestamp(date)} value={timestamp} /> - + { subtitle={type === 'update_admins' ? 'Enter the admin addresses' : 'Enter the member addresses'} title="Addresses" /> - + You may optionally choose a text file of additional member addresses. + + + + + + You may optionally choose a .csv file of additional member addresses and mint counts. + + + +
diff --git a/pages/contracts/whitelist/instantiate.tsx b/pages/contracts/whitelist/instantiate.tsx index d77aed8..b470bea 100644 --- a/pages/contracts/whitelist/instantiate.tsx +++ b/pages/contracts/whitelist/instantiate.tsx @@ -13,13 +13,14 @@ import { InputDateTime } from 'components/InputDateTime' import { JsonPreview } from 'components/JsonPreview' import { LinkTabs } from 'components/LinkTabs' import { whitelistLinkTabs } from 'components/LinkTabs.data' +import { type WhitelistFlexMember, WhitelistFlexUpload } from 'components/WhitelistFlexUpload' import { WhitelistUpload } from 'components/WhitelistUpload' import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' import type { InstantiateResponse } from 'contracts/sg721' import type { NextPage } from 'next' import { NextSeo } from 'next-seo' -import { type FormEvent, useState } from 'react' +import { type FormEvent, useEffect, useState } from 'react' import { toast } from 'react-hot-toast' import { FaAsterisk } from 'react-icons/fa' import { useMutation } from 'react-query' @@ -27,7 +28,7 @@ import { isValidAddress } from 'utils/isValidAddress' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' -import { WHITELIST_CODE_ID } from '../../../utils/constants' +import { WHITELIST_CODE_ID, WHITELIST_FLEX_CODE_ID } from '../../../utils/constants' const WhitelistInstantiatePage: NextPage = () => { const wallet = useWallet() @@ -36,8 +37,10 @@ const WhitelistInstantiatePage: NextPage = () => { const [startDate, setStartDate] = useState(undefined) const [endDate, setEndDate] = useState(undefined) const [adminsMutable, setAdminsMutable] = useState(true) + const [whitelistType, setWhitelistType] = useState<'standard' | 'flex'>('standard') - const [whitelistArray, setWhitelistArray] = useState([]) + const [whitelistStandardArray, setWhitelistStandardArray] = useState([]) + const [whitelistFlexArray, setWhitelistFlexArray] = useState([]) const unitPriceState = useNumberInputState({ id: 'unit-price', @@ -63,6 +66,13 @@ const WhitelistInstantiatePage: NextPage = () => { placeholder: '5', }) + const whaleCapState = useNumberInputState({ + id: 'whale-cap', + name: 'whaleCap', + title: 'Whale Cap (optional)', + subtitle: 'Maximum number of tokens a single address can mint', + }) + const addressListState = useAddressListState() const { data, isLoading, mutate } = useMutation( @@ -79,8 +89,8 @@ const WhitelistInstantiatePage: NextPage = () => { throw new Error('End date is required') } - const msg = { - members: whitelistArray, + const standardMsg = { + members: whitelistStandardArray, start_time: (startDate.getTime() * 1_000_000).toString(), end_time: (endDate.getTime() * 1_000_000).toString(), mint_price: coin(String(Number(unitPriceState.value) * 1000000), 'ustars'), @@ -95,8 +105,31 @@ const WhitelistInstantiatePage: NextPage = () => { ] || [wallet.address], admins_mutable: adminsMutable, } + + const flexMsg = { + members: whitelistFlexArray, + start_time: (startDate.getTime() * 1_000_000).toString(), + end_time: (endDate.getTime() * 1_000_000).toString(), + mint_price: coin(String(Number(unitPriceState.value) * 1000000), 'ustars'), + whale_cap: whaleCapState.value || undefined, + 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), + contract.instantiate( + whitelistType === 'standard' ? WHITELIST_CODE_ID : WHITELIST_FLEX_CODE_ID, + whitelistType === 'standard' ? standardMsg : flexMsg, + whitelistType === 'standard' ? 'Stargaze Whitelist Contract' : 'Stargaze Whitelist Flex Contract', + wallet.address, + ), { loading: 'Instantiating contract...', error: 'Instantiation failed!', @@ -112,9 +145,18 @@ const WhitelistInstantiatePage: NextPage = () => { ) const whitelistFileOnChange = (whitelistData: string[]) => { - setWhitelistArray(whitelistData) + setWhitelistStandardArray(whitelistData) } + const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => { + setWhitelistFlexArray(whitelistData) + } + + useEffect(() => { + setWhitelistStandardArray([]) + setWhitelistFlexArray([]) + }, [whitelistType]) + return (
@@ -125,6 +167,48 @@ const WhitelistInstantiatePage: NextPage = () => { /> +
+
+ { + setWhitelistType('standard') + }} + type="radio" + value="nft-storage" + /> + +
+ +
+ { + setWhitelistType('flex') + }} + type="radio" + value="flex" + /> + +
+
+ Instantiate success! Here is the transaction result containing the contract address and the transaction @@ -134,7 +218,7 @@ const WhitelistInstantiatePage: NextPage = () => {
-
+
- - 0}> - + + + 0}> + + + + + + 0}> + + - + + + + + + setStartDate(date)} value={startDate} /> diff --git a/utils/constants.ts b/utils/constants.ts index ec36024..06e66aa 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,9 +1,12 @@ export const SG721_CODE_ID = parseInt(process.env.NEXT_PUBLIC_SG721_CODE_ID, 10) export const SG721_UPDATABLE_CODE_ID = parseInt(process.env.NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID, 10) export const WHITELIST_CODE_ID = parseInt(process.env.NEXT_PUBLIC_WHITELIST_CODE_ID, 10) +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 VENDING_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_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 BASE_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_ADDRESS export const BASE_FACTORY_UPDATABLE_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS export const SG721_NAME_ADDRESS = process.env.NEXT_PUBLIC_SG721_NAME_ADDRESS diff --git a/utils/csvToFlexList.ts b/utils/csvToFlexList.ts new file mode 100644 index 0000000..490a8d4 --- /dev/null +++ b/utils/csvToFlexList.ts @@ -0,0 +1,31 @@ +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' + +export const csvToFlexList = (str: string, delimiter = ',') => { + let newline = '\n' + if (str.includes('\r')) newline = '\r' + if (str.includes('\r\n')) newline = '\r\n' + + const headers = str.slice(0, str.indexOf(newline)).split(delimiter) + if (headers.length !== 2) { + throw new Error('Invalid whitelist-flex file.') + } + if (headers[0] !== 'address' || headers[1] !== 'mint_count') { + throw new Error('Invalid whitelist-flex file. Headers must be "address" and "mint_count".') + } + + const rows = str.slice(str.indexOf('\n') + 1).split(newline) + + const arr = rows + .filter((row) => row !== '') + .map((row) => { + const values = row.split(delimiter) + const el = headers.reduce((object, header, index) => { + // @ts-expect-error assume object as Record + object[header] = values[index] + return object + }, {}) + return el + }) + + return arr as WhitelistFlexMember[] +} diff --git a/utils/isValidFlexListFile.ts b/utils/isValidFlexListFile.ts new file mode 100644 index 0000000..1a216f4 --- /dev/null +++ b/utils/isValidFlexListFile.ts @@ -0,0 +1,52 @@ +import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload' +import { toast } from 'react-hot-toast' + +import { isValidAddress } from './isValidAddress' + +export const isValidFlexListFile = (file: WhitelistFlexMember[]) => { + let sumOfAmounts = 0 + file.forEach((allocation) => { + sumOfAmounts += Number(allocation.mint_count) + }) + if (sumOfAmounts > 10000) { + toast.error(`Total mint count should be less than 10000 tokens (current count: ${sumOfAmounts}))`) + return false + } + + const checks = file.map((account) => { + // Check if address is valid bech32 address + if (account.address.trim().startsWith('stars')) { + if (!isValidAddress(account.address.trim())) { + return { address: false } + } + } + // Check if address start with stars + if (!account.address.trim().startsWith('stars') && !account.address.trim().endsWith('.stars')) { + return { address: false } + } + // Check if amount is valid + if (!Number.isInteger(Number(account.mint_count)) || !(Number(account.mint_count) > 0)) { + return { mint_count: false } + } + return null + }) + + const isStargazeAddresses = file.every( + (account) => account.address.trim().startsWith('stars') || account.address.trim().endsWith('.stars'), + ) + if (!isStargazeAddresses) { + toast.error('All accounts must be on the Stargaze network') + return false + } + + if (checks.filter((check) => check?.address === false).length > 0) { + toast.error('Invalid address in file') + return false + } + if (checks.filter((check) => check?.mint_count === false).length > 0) { + toast.error('Invalid mint count in file. Mint count must be a positive integer.') + return false + } + + return true +}