Merge pull request #156 from public-awesome/develop

Sync development > main
This commit is contained in:
Serkan Reis 2023-04-28 15:07:14 +03:00 committed by GitHub
commit cbbc6c5272
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 794 additions and 81 deletions

View File

@ -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_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_SG721_CODE_ID=1911 NEXT_PUBLIC_SG721_CODE_ID=1911
NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID=1912 NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID=1912
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=1909 NEXT_PUBLIC_VENDING_MINTER_CODE_ID=1909
NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID=2080
NEXT_PUBLIC_BASE_MINTER_CODE_ID=1910 NEXT_PUBLIC_BASE_MINTER_CODE_ID=1910
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1ynec878x5phexq3hj4zdgvp6r5ayfmxks38kvunwyjugqn3hqeqq3cgtuw" NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1ynec878x5phexq3hj4zdgvp6r5ayfmxks38kvunwyjugqn3hqeqq3cgtuw"
NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1fnfywcnzzwledr93at65qm8gf953tjxgh6u2u4r8n9vsdv7u75eqe7ecn3" NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1fnfywcnzzwledr93at65qm8gf953tjxgh6u2u4r8n9vsdv7u75eqe7ecn3"
NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS="stars1l5v0vgly8r9jw4exeajnftymr29kn70n23gpl2g5fylaww2pzkhq0rks7c"
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars10rmaxgnjvskuumgv7e2awqkhdqdcygkwrz8a8vvt88szj7fc7xlq5jcs3f" NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars10rmaxgnjvskuumgv7e2awqkhdqdcygkwrz8a8vvt88szj7fc7xlq5jcs3f"
NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS="stars13pw8r33dsnghlxfj2upaywf38z2fc6npuw9maq9e5cpet4v285sscgzjp2" NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS="stars13pw8r33dsnghlxfj2upaywf38z2fc6npuw9maq9e5cpet4v285sscgzjp2"
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr" NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
NEXT_PUBLIC_WHITELIST_CODE_ID=1913 NEXT_PUBLIC_WHITELIST_CODE_ID=1913
NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=2005
NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336 NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336
NEXT_PUBLIC_BADGE_HUB_ADDRESS="stars1dacun0xn7z73qzdcmq27q3xn6xuprg8e2ugj364784al2v27tklqynhuqa" NEXT_PUBLIC_BADGE_HUB_ADDRESS="stars1dacun0xn7z73qzdcmq27q3xn6xuprg8e2ugj364784al2v27tklqynhuqa"
NEXT_PUBLIC_BADGE_NFT_CODE_ID=1337 NEXT_PUBLIC_BADGE_NFT_CODE_ID=1337

View File

@ -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<WhitelistFlexMember[]>([])
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<HTMLInputElement>) => {
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<FileReader>) => {
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 (
<div
className={clsx(
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept=".csv"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="whitelist-flex-file"
onChange={onFileChange}
type="file"
/>
</div>
)
}

View File

@ -4,6 +4,8 @@ import { AddressList } from 'components/forms/AddressList'
import { useAddressListState } from 'components/forms/AddressList.hooks' import { useAddressListState } from 'components/forms/AddressList.hooks'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { isValidAddress } from 'utils/isValidAddress' import { isValidAddress } from 'utils/isValidAddress'
@ -17,9 +19,10 @@ interface WhitelistDetailsProps {
} }
export interface WhitelistDetailsDataProps { export interface WhitelistDetailsDataProps {
whitelistType: WhitelistState whitelistState: WhitelistState
whitelistType: WhitelistType
contractAddress?: string contractAddress?: string
members?: string[] members?: string[] | WhitelistFlexMember[]
unitPrice?: string unitPrice?: string
startTime?: string startTime?: string
endTime?: string endTime?: string
@ -31,11 +34,15 @@ export interface WhitelistDetailsDataProps {
type WhitelistState = 'none' | 'existing' | 'new' type WhitelistState = 'none' | 'existing' | 'new'
type WhitelistType = 'standard' | 'flex'
export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
const [whitelistState, setWhitelistState] = useState<WhitelistState>('none') const [whitelistState, setWhitelistState] = useState<WhitelistState>('none')
const [whitelistType, setWhitelistType] = useState<WhitelistType>('standard')
const [startDate, setStartDate] = useState<Date | undefined>(undefined) const [startDate, setStartDate] = useState<Date | undefined>(undefined)
const [endDate, setEndDate] = useState<Date | undefined>(undefined) const [endDate, setEndDate] = useState<Date | undefined>(undefined)
const [whitelistArray, setWhitelistArray] = useState<string[]>([]) const [whitelistStandardArray, setWhitelistStandardArray] = useState<string[]>([])
const [whitelistFlexArray, setWhitelistFlexArray] = useState<WhitelistFlexMember[]>([])
const [adminsMutable, setAdminsMutable] = useState<boolean>(true) const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
const whitelistAddressState = useInputState({ const whitelistAddressState = useInputState({
@ -72,19 +79,29 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
const addressListState = useAddressListState() const addressListState = useAddressListState()
const whitelistFileOnChange = (data: string[]) => { const whitelistFileOnChange = (data: string[]) => {
setWhitelistArray(data) setWhitelistStandardArray(data)
}
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
setWhitelistFlexArray(whitelistData)
} }
useEffect(() => {
setWhitelistStandardArray([])
setWhitelistFlexArray([])
}, [whitelistType])
useEffect(() => { useEffect(() => {
const data: WhitelistDetailsDataProps = { const data: WhitelistDetailsDataProps = {
whitelistType: whitelistState, whitelistState,
whitelistType,
contractAddress: whitelistAddressState.value contractAddress: whitelistAddressState.value
.toLowerCase() .toLowerCase()
.replace(/,/g, '') .replace(/,/g, '')
.replace(/"/g, '') .replace(/"/g, '')
.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() : '', unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : '',
startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '', startTime: startDate ? (startDate.getTime() * 1_000_000).toString() : '',
endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '', endTime: endDate ? (endDate.getTime() * 1_000_000).toString() : '',
@ -108,7 +125,8 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
perAddressLimitState.value, perAddressLimitState.value,
startDate, startDate,
endDate, endDate,
whitelistArray, whitelistStandardArray,
whitelistFlexArray,
whitelistState, whitelistState,
addressListState.values, addressListState.values,
adminsMutable, adminsMutable,
@ -125,6 +143,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
name="whitelistRadioOptions1" name="whitelistRadioOptions1"
onClick={() => { onClick={() => {
setWhitelistState('none') setWhitelistState('none')
setWhitelistType('standard')
}} }}
type="radio" type="radio"
value="None" value="None"
@ -181,11 +200,54 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
</Conditional> </Conditional>
<Conditional test={whitelistState === 'new'}> <Conditional test={whitelistState === 'new'}>
<div className="flex justify-between mb-5 ml-6 max-w-[300px] text-lg font-bold">
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'standard'}
className="peer sr-only"
id="inlineRadio7"
name="inlineRadioOptions7"
onClick={() => {
setWhitelistType('standard')
}}
type="radio"
value="nft-storage"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio7"
>
Standard Whitelist
</label>
</div>
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'flex'}
className="peer sr-only"
id="inlineRadio8"
name="inlineRadioOptions8"
onClick={() => {
setWhitelistType('flex')
}}
type="radio"
value="flex"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio8"
>
Whitelist Flex
</label>
</div>
</div>
<div className="grid grid-cols-2"> <div className="grid grid-cols-2">
<FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details"> <FormGroup subtitle="Information about your minting settings" title="Whitelist Minting Details">
<NumberInput isRequired {...unitPriceState} /> <NumberInput isRequired {...unitPriceState} />
<NumberInput isRequired {...memberLimitState} /> <NumberInput isRequired {...memberLimitState} />
<NumberInput isRequired {...perAddressLimitState} /> <Conditional test={whitelistType === 'standard'}>
<NumberInput isRequired {...perAddressLimitState} />
</Conditional>
<FormControl <FormControl
htmlId="start-date" htmlId="start-date"
isRequired isRequired
@ -226,11 +288,24 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
title="Administrator Addresses" title="Administrator Addresses"
/> />
</div> </div>
<FormGroup subtitle="TXT file that contains the whitelisted addresses" title="Whitelist File"> <Conditional test={whitelistType === 'standard'}>
<WhitelistUpload onChange={whitelistFileOnChange} /> <FormGroup subtitle="TXT file that contains the whitelisted addresses" title="Whitelist File">
</FormGroup> <WhitelistUpload onChange={whitelistFileOnChange} />
<Conditional test={whitelistArray.length > 0}> </FormGroup>
<JsonPreview content={whitelistArray} initialState title="File Contents" /> <Conditional test={whitelistStandardArray.length > 0}>
<JsonPreview content={whitelistStandardArray} initialState title="File Contents" />
</Conditional>
</Conditional>
<Conditional test={whitelistType === 'flex'}>
<FormGroup
subtitle="CSV file that contains the whitelisted addresses and their corresponding mint counts"
title="Whitelist File"
>
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
</FormGroup>
<Conditional test={whitelistFlexArray.length > 0}>
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
</Conditional>
</Conditional> </Conditional>
</div> </div>
</div> </div>

View File

@ -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<Record<string, WhitelistFlexMember>>(() => ({}))
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 }
}

View File

@ -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 (
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
{attributes.map(([id], i) => (
<FlexMemberAttribute
key={`ma-${id}`}
defaultAttribute={attributes[i][1]}
id={id}
isLast={i === attributes.length - 1}
onAdd={onAdd}
onChange={onChange}
onRemove={onRemove}
/>
))}
</FormControl>
)
}
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 (
<div className="grid relative md:grid-cols-[50%_43%_7%] lg:grid-cols-[65%_28%_7%] 2xl:grid-cols-[70%_23%_7%] 2xl:space-x-2">
<AddressInput {...addressState} />
<NumberInput className="ml-2" {...mintCountState} />
<div className="flex justify-end items-end pb-2 w-8">
<button
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
onClick={(e) => {
e.preventDefault()
isLast ? onAdd() : onRemove(id)
}}
type="button"
>
<Icon className="w-3 h-3" />
</button>
</div>
</div>
)
}

View File

@ -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 { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import type { Coin } from '@cosmjs/proto-signing' import type { Coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate' 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' import { VENDING_FACTORY_UPDATABLE_ADDRESS } from '../../utils/constants'
@ -23,11 +25,17 @@ export interface VendingFactoryInstance {
msg: Record<string, unknown>, msg: Record<string, unknown>,
funds: Coin[], funds: Coin[],
updatable?: boolean, updatable?: boolean,
flex?: boolean,
) => Promise<CreateVendingMinterResponse> ) => Promise<CreateVendingMinterResponse>
} }
export interface VendingFactoryMessages { export interface VendingFactoryMessages {
createVendingMinter: (msg: Record<string, unknown>, funds: Coin[], updatable?: boolean) => CreateVendingMinterMessage createVendingMinter: (
msg: Record<string, unknown>,
funds: Coin[],
updatable?: boolean,
flex?: boolean,
) => CreateVendingMinterMessage
} }
export interface CreateVendingMinterMessage { export interface CreateVendingMinterMessage {
@ -53,10 +61,11 @@ export const vendingFactory = (client: SigningCosmWasmClient, txSigner: string):
msg: Record<string, unknown>, msg: Record<string, unknown>,
funds: Coin[], funds: Coin[],
updatable?: boolean, updatable?: boolean,
flex?: boolean,
): Promise<CreateVendingMinterResponse> => { ): Promise<CreateVendingMinterResponse> => {
const result = await client.execute( const result = await client.execute(
senderAddress, senderAddress,
updatable ? VENDING_FACTORY_UPDATABLE_ADDRESS : VENDING_FACTORY_ADDRESS, flex ? VENDING_FACTORY_FLEX_ADDRESS : updatable ? VENDING_FACTORY_UPDATABLE_ADDRESS : VENDING_FACTORY_ADDRESS,
msg, msg,
'auto', 'auto',
'', '',
@ -82,6 +91,7 @@ export const vendingFactory = (client: SigningCosmWasmClient, txSigner: string):
msg: Record<string, unknown>, msg: Record<string, unknown>,
funds: Coin[], funds: Coin[],
updatable?: boolean, updatable?: boolean,
flex?: boolean,
): CreateVendingMinterMessage => { ): CreateVendingMinterMessage => {
return { return {
sender: txSigner, sender: txSigner,

View File

@ -11,6 +11,7 @@ export interface DispatchExecuteArgs {
msg: Record<string, unknown> msg: Record<string, unknown>
funds: Coin[] funds: Coin[]
updatable?: boolean updatable?: boolean
flex?: boolean
} }
export const dispatchExecute = async (args: DispatchExecuteArgs) => { export const dispatchExecute = async (args: DispatchExecuteArgs) => {
@ -18,12 +19,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
if (!messages) { if (!messages) {
throw new Error('cannot dispatch execute, messages is not defined') 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) => { export const previewExecutePayload = (args: DispatchExecuteArgs) => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const { messages } = useVendingFactoryContract() const { messages } = useVendingFactoryContract()
const { contract } = args 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)
} }

View File

@ -1,6 +1,7 @@
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import type { Coin } from '@cosmjs/proto-signing' import type { Coin } from '@cosmjs/proto-signing'
import { coin } from '@cosmjs/proto-signing' import { coin } from '@cosmjs/proto-signing'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
export interface InstantiateResponse { export interface InstantiateResponse {
readonly contractAddress: string readonly contractAddress: string
@ -30,7 +31,7 @@ export interface WhiteListInstance {
//Execute //Execute
updateStartTime: (startTime: string) => Promise<string> updateStartTime: (startTime: string) => Promise<string>
updateEndTime: (endTime: string) => Promise<string> updateEndTime: (endTime: string) => Promise<string>
addMembers: (memberList: string[]) => Promise<string> addMembers: (memberList: string[] | WhitelistFlexMember[]) => Promise<string>
removeMembers: (memberList: string[]) => Promise<string> removeMembers: (memberList: string[]) => Promise<string>
updatePerAddressLimit: (limit: number) => Promise<string> updatePerAddressLimit: (limit: number) => Promise<string>
increaseMemberLimit: (limit: number) => Promise<string> increaseMemberLimit: (limit: number) => Promise<string>
@ -41,7 +42,7 @@ export interface WhiteListInstance {
export interface WhitelistMessages { export interface WhitelistMessages {
updateStartTime: (startTime: string) => UpdateStartTimeMessage updateStartTime: (startTime: string) => UpdateStartTimeMessage
updateEndTime: (endTime: string) => UpdateEndTimeMessage updateEndTime: (endTime: string) => UpdateEndTimeMessage
addMembers: (memberList: string[]) => AddMembersMessage addMembers: (memberList: string[] | WhitelistFlexMember[]) => AddMembersMessage
removeMembers: (memberList: string[]) => RemoveMembersMessage removeMembers: (memberList: string[]) => RemoveMembersMessage
updatePerAddressLimit: (limit: number) => UpdatePerAddressLimitMessage updatePerAddressLimit: (limit: number) => UpdatePerAddressLimitMessage
increaseMemberLimit: (limit: number) => IncreaseMemberLimitMessage increaseMemberLimit: (limit: number) => IncreaseMemberLimitMessage
@ -86,7 +87,7 @@ export interface AddMembersMessage {
sender: string sender: string
contract: string contract: string
msg: { msg: {
add_members: { to_add: string[] } add_members: { to_add: string[] | WhitelistFlexMember[] }
} }
funds: Coin[] funds: Coin[]
} }
@ -182,7 +183,7 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit
return res.transactionHash return res.transactionHash
} }
const addMembers = async (memberList: string[]): Promise<string> => { const addMembers = async (memberList: string[] | WhitelistFlexMember[]): Promise<string> => {
const res = await client.execute( const res = await client.execute(
txSigner, txSigner,
contractAddress, contractAddress,
@ -307,7 +308,7 @@ export const WhiteList = (client: SigningCosmWasmClient, txSigner: string): Whit
} }
} }
const addMembers = (memberList: string[]) => { const addMembers = (memberList: string[] | WhitelistFlexMember[]) => {
return { return {
sender: txSigner, sender: txSigner,
contract: contractAddress, contract: contractAddress,

View File

@ -1,3 +1,4 @@
import type { WhitelistFlexMember } from '../../../components/WhitelistFlexUpload'
import type { WhiteListInstance } from '../index' import type { WhiteListInstance } from '../index'
import { useWhiteListContract } from '../index' import { useWhiteListContract } from '../index'
@ -68,23 +69,16 @@ export interface DispatchExecuteProps {
[k: string]: unknown [k: string]: unknown
} }
type Select<T extends ExecuteType> = T
/** @see {@link WhiteListInstance} */ /** @see {@link WhiteListInstance} */
export type DispatchExecuteArgs = { export interface DispatchExecuteArgs {
contract: string contract: string
messages?: WhiteListInstance messages?: WhiteListInstance
} & ( type: string | undefined
| { type: undefined } timestamp: string
| { type: Select<'update_start_time'>; timestamp: string } members: string[] | WhitelistFlexMember[]
| { type: Select<'update_end_time'>; timestamp: string } limit: number
| { type: Select<'add_members'>; members: string[] } admins: 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'> }
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => { export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { messages } = args const { messages } = args
@ -105,7 +99,7 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
return messages.addMembers(args.members) return messages.addMembers(args.members)
} }
case 'remove_members': { case 'remove_members': {
return messages.removeMembers(args.members) return messages.removeMembers(args.members as string[])
} }
case 'update_per_address_limit': { case 'update_per_address_limit': {
return messages.updatePerAddressLimit(args.limit) return messages.updatePerAddressLimit(args.limit)
@ -140,7 +134,7 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
return messages(contract)?.addMembers(args.members) return messages(contract)?.addMembers(args.members)
} }
case 'remove_members': { case 'remove_members': {
return messages(contract)?.removeMembers(args.members) return messages(contract)?.removeMembers(args.members as string[])
} }
case 'update_per_address_limit': { case 'update_per_address_limit': {
return messages(contract)?.updatePerAddressLimit(args.limit) return messages(contract)?.updatePerAddressLimit(args.limit)

3
env.d.ts vendored
View File

@ -17,9 +17,12 @@ declare namespace NodeJS {
readonly NEXT_PUBLIC_SG721_CODE_ID: string readonly NEXT_PUBLIC_SG721_CODE_ID: string
readonly NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID: string readonly NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID: string
readonly NEXT_PUBLIC_WHITELIST_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_CODE_ID: string
readonly NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID: string
readonly NEXT_PUBLIC_VENDING_FACTORY_ADDRESS: string readonly NEXT_PUBLIC_VENDING_FACTORY_ADDRESS: string
readonly NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_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_ADDRESS: string
readonly NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS: string readonly NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS: string
readonly NEXT_PUBLIC_SG721_NAME_ADDRESS: string readonly NEXT_PUBLIC_SG721_NAME_ADDRESS: string

View File

@ -1,6 +1,6 @@
{ {
"name": "stargaze-studio", "name": "stargaze-studio",
"version": "0.5.8", "version": "0.5.9",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],

View File

@ -145,7 +145,7 @@ const CollectionActionsPage: NextPage = () => {
}) })
.catch((err) => { .catch((err) => {
console.log(err) console.log(err)
setMinterType('base') setSg721Type('base')
console.log('Unable to retrieve contract type. Defaulting to "base".') console.log('Unable to retrieve contract type. Defaulting to "base".')
}) })
}, [debouncedSg721ContractState, wallet.client]) }, [debouncedSg721ContractState, wallet.client])

View File

@ -49,8 +49,10 @@ import {
SG721_UPDATABLE_CODE_ID, SG721_UPDATABLE_CODE_ID,
STARGAZE_URL, STARGAZE_URL,
VENDING_FACTORY_ADDRESS, VENDING_FACTORY_ADDRESS,
VENDING_FACTORY_FLEX_ADDRESS,
VENDING_FACTORY_UPDATABLE_ADDRESS, VENDING_FACTORY_UPDATABLE_ADDRESS,
WHITELIST_CODE_ID, WHITELIST_CODE_ID,
WHITELIST_FLEX_CODE_ID,
} from 'utils/constants' } from 'utils/constants'
import { withMetadata } from 'utils/layout' import { withMetadata } from 'utils/layout'
import { links } from 'utils/links' import { links } from 'utils/links'
@ -93,9 +95,11 @@ const CollectionCreationPage: NextPage = () => {
const [vendingMinterCreationFee, setVendingMinterCreationFee] = useState<string | null>(null) const [vendingMinterCreationFee, setVendingMinterCreationFee] = useState<string | null>(null)
const [baseMinterCreationFee, setBaseMinterCreationFee] = useState<string | null>(null) const [baseMinterCreationFee, setBaseMinterCreationFee] = useState<string | null>(null)
const [vendingMinterUpdatableCreationFee, setVendingMinterUpdatableCreationFee] = useState<string | null>(null) const [vendingMinterUpdatableCreationFee, setVendingMinterUpdatableCreationFee] = useState<string | null>(null)
const [vendingMinterFlexCreationFee, setVendingMinterFlexCreationFee] = useState<string | null>(null)
const [baseMinterUpdatableCreationFee, setBaseMinterUpdatableCreationFee] = useState<string | null>(null) const [baseMinterUpdatableCreationFee, setBaseMinterUpdatableCreationFee] = useState<string | null>(null)
const [minimumMintPrice, setMinimumMintPrice] = useState<string | null>('0') const [minimumMintPrice, setMinimumMintPrice] = useState<string | null>('0')
const [minimumUpdatableMintPrice, setMinimumUpdatableMintPrice] = useState<string | null>('0') const [minimumUpdatableMintPrice, setMinimumUpdatableMintPrice] = useState<string | null>('0')
const [minimumFlexMintPrice, setMinimumFlexMintPrice] = useState<string | null>('0')
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [isMintingComplete, setIsMintingComplete] = useState(false) const [isMintingComplete, setIsMintingComplete] = useState(false)
@ -221,8 +225,8 @@ const CollectionCreationPage: NextPage = () => {
setCoverImageUrl(coverImageUri) setCoverImageUrl(coverImageUri)
let whitelist: string | undefined let whitelist: string | undefined
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress if (whitelistDetails?.whitelistState === 'existing') whitelist = whitelistDetails.contractAddress
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() else if (whitelistDetails?.whitelistState === 'new') whitelist = await instantiateWhitelist()
setWhitelistContractAddress(whitelist as string) setWhitelistContractAddress(whitelist as string)
await instantiateVendingMinter(baseUri, coverImageUri, whitelist) await instantiateVendingMinter(baseUri, coverImageUri, whitelist)
@ -231,8 +235,8 @@ const CollectionCreationPage: NextPage = () => {
setCoverImageUrl(uploadDetails?.imageUrl as string) setCoverImageUrl(uploadDetails?.imageUrl as string)
let whitelist: string | undefined let whitelist: string | undefined
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress if (whitelistDetails?.whitelistState === 'existing') whitelist = whitelistDetails.contractAddress
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() else if (whitelistDetails?.whitelistState === 'new') whitelist = await instantiateWhitelist()
setWhitelistContractAddress(whitelist as string) setWhitelistContractAddress(whitelist as string)
await instantiateVendingMinter(baseTokenUri as string, coverImageUrl as string, whitelist) 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 (!wallet.initialized) throw new Error('Wallet not connected')
if (!whitelistContract) throw new Error('Contract not found') if (!whitelistContract) throw new Error('Contract not found')
const msg = { const standardMsg = {
members: whitelistDetails?.members, members: whitelistDetails?.members,
start_time: whitelistDetails?.startTime, start_time: whitelistDetails?.startTime,
end_time: whitelistDetails?.endTime, end_time: whitelistDetails?.endTime,
@ -412,9 +416,19 @@ const CollectionCreationPage: NextPage = () => {
admins_mutable: whitelistDetails?.adminsMutable, 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( const data = await whitelistContract.instantiate(
WHITELIST_CODE_ID, whitelistDetails?.whitelistType === 'standard' ? WHITELIST_CODE_ID : WHITELIST_FLEX_CODE_ID,
msg, whitelistDetails?.whitelistType === 'standard' ? standardMsg : flexMsg,
'Stargaze Whitelist Contract', 'Stargaze Whitelist Contract',
wallet.address, 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 = { 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, messages: vendingFactoryMessages,
txSigner: wallet.address, txSigner: wallet.address,
msg, msg,
funds: [ funds: [
coin( coin(
collectionDetails?.updatable whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex'
? (vendingMinterFlexCreationFee as string)
: collectionDetails?.updatable
? (vendingMinterUpdatableCreationFee as string) ? (vendingMinterUpdatableCreationFee as string)
: (vendingMinterCreationFee as string), : (vendingMinterCreationFee as string),
'ustars', 'ustars',
), ),
], ],
updatable: collectionDetails?.updatable, updatable: collectionDetails?.updatable,
flex: whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex',
} }
const data = await vendingFactoryDispatchExecute(payload) const data = await vendingFactoryDispatchExecute(payload)
setTransactionHash(data.transactionHash) setTransactionHash(data.transactionHash)
@ -786,7 +820,10 @@ const CollectionCreationPage: NextPage = () => {
const checkMintingDetails = () => { const checkMintingDetails = () => {
if (!mintingDetails) throw new Error('Please fill out the minting details') 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 (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)) if (Number(mintingDetails.unitPrice) < Number(minimumUpdatableMintPrice))
throw new Error( throw new Error(
`Invalid unit price: The minimum unit price is ${Number(minimumUpdatableMintPrice) / 1000000} STARS`, `Invalid unit price: The minimum unit price is ${Number(minimumUpdatableMintPrice) / 1000000} STARS`,
@ -822,12 +859,14 @@ const CollectionCreationPage: NextPage = () => {
const checkWhitelistDetails = async () => { const checkWhitelistDetails = async () => {
if (!whitelistDetails) throw new Error('Please fill out the whitelist details') 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') if (whitelistDetails.contractAddress === '') throw new Error('Whitelist contract address is required')
else { else {
const contract = whitelistContract?.use(whitelistDetails.contractAddress) const contract = whitelistContract?.use(whitelistDetails.contractAddress)
//check if the address belongs to a whitelist contract (see performChecks()) //check if the address belongs to a whitelist contract (see performChecks())
const config = await contract?.config() 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)) { if (Number(config?.start_time) !== Number(mintingDetails?.startTime)) {
const whitelistStartDate = new Date(Number(config?.start_time) / 1000000) const whitelistStartDate = new Date(Number(config?.start_time) / 1000000)
throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`) 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.members?.length === 0) throw new Error('Whitelist member list cannot be empty')
if (whitelistDetails.unitPrice === '') throw new Error('Whitelist unit price is required') if (whitelistDetails.unitPrice === '') throw new Error('Whitelist unit price is required')
if (Number(whitelistDetails.unitPrice) < 0) if (Number(whitelistDetails.unitPrice) < 0)
throw new Error('Invalid unit price: The unit price cannot be negative') throw new Error('Invalid unit price: The unit price cannot be negative')
if (whitelistDetails.startTime === '') throw new Error('Start time is required') if (whitelistDetails.startTime === '') throw new Error('Start time is required')
if (whitelistDetails.endTime === '') throw new Error('End 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') throw new Error('Per address limit is required')
if (!whitelistDetails.memberLimit || whitelistDetails.memberLimit === 0) if (!whitelistDetails.memberLimit || whitelistDetails.memberLimit === 0)
throw new Error('Member limit is required') throw new Error('Member limit is required')
@ -945,14 +987,25 @@ const CollectionCreationPage: NextPage = () => {
setVendingMinterUpdatableCreationFee(vendingFactoryUpdatableParameters?.params?.creation_fee?.amount) setVendingMinterUpdatableCreationFee(vendingFactoryUpdatableParameters?.params?.creation_fee?.amount)
setMinimumUpdatableMintPrice(vendingFactoryUpdatableParameters?.params?.min_mint_price?.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 = () => { const checkwalletBalance = () => {
if (!wallet.initialized) throw new Error('Wallet not connected.') 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 = const amountNeeded =
Math.ceil(Number(whitelistDetails.memberLimit) / 1000) * 100000000 + 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)) if (amountNeeded >= Number(wallet.balance[0].amount))
throw new Error( throw new Error(
`Insufficient wallet balance to instantiate the required contracts. Needed amount: ${( `Insufficient wallet balance to instantiate the required contracts. Needed amount: ${(
@ -962,7 +1015,9 @@ const CollectionCreationPage: NextPage = () => {
} else { } else {
const amountNeeded = const amountNeeded =
minterType === 'vending' minterType === 'vending'
? collectionDetails?.updatable ? whitelistDetails?.whitelistState === 'existing' && whitelistDetails.whitelistType === 'flex'
? Number(vendingMinterFlexCreationFee)
: collectionDetails?.updatable
? Number(vendingMinterUpdatableCreationFee) ? Number(vendingMinterUpdatableCreationFee)
: Number(vendingMinterCreationFee) : Number(vendingMinterCreationFee)
: collectionDetails?.updatable : collectionDetails?.updatable

View File

@ -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 { Alert } from 'components/Alert'
import { Button } from 'components/Button' import { Button } from 'components/Button'
import { Conditional } from 'components/Conditional' import { Conditional } from 'components/Conditional'
@ -7,6 +10,8 @@ import { useExecuteComboboxState } from 'components/contracts/whitelist/ExecuteC
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { AddressList } from 'components/forms/AddressList' import { AddressList } from 'components/forms/AddressList'
import { useAddressListState } from 'components/forms/AddressList.hooks' 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 { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
@ -14,6 +19,8 @@ import { JsonPreview } from 'components/JsonPreview'
import { LinkTabs } from 'components/LinkTabs' import { LinkTabs } from 'components/LinkTabs'
import { whitelistLinkTabs } from 'components/LinkTabs.data' import { whitelistLinkTabs } from 'components/LinkTabs.data'
import { TransactionHash } from 'components/TransactionHash' import { TransactionHash } from 'components/TransactionHash'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
import { WhitelistUpload } from 'components/WhitelistUpload' import { WhitelistUpload } from 'components/WhitelistUpload'
import { useContracts } from 'contexts/contracts' import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet' import { useWallet } from 'contexts/wallet'
@ -27,6 +34,7 @@ import { useEffect, useMemo, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa' import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query' import { useMutation } from 'react-query'
import { useDebounce } from 'utils/debounce'
import { isValidAddress } from 'utils/isValidAddress' import { isValidAddress } from 'utils/isValidAddress'
import { withMetadata } from 'utils/layout' import { withMetadata } from 'utils/layout'
import { links } from 'utils/links' import { links } from 'utils/links'
@ -37,6 +45,8 @@ const WhitelistExecutePage: NextPage = () => {
const [lastTx, setLastTx] = useState('') const [lastTx, setLastTx] = useState('')
const [memberList, setMemberList] = useState<string[]>([]) const [memberList, setMemberList] = useState<string[]>([])
const [flexMemberList, setFlexMemberList] = useState<WhitelistFlexMember[]>([])
const [whitelistType, setWhitelistType] = useState<'standard' | 'flex'>('standard')
const comboboxState = useExecuteComboboxState() const comboboxState = useExecuteComboboxState()
const type = comboboxState.value?.id const type = comboboxState.value?.id
@ -45,6 +55,8 @@ const WhitelistExecutePage: NextPage = () => {
const addressListState = useAddressListState() const addressListState = useAddressListState()
const flexAddressListState = useFlexMemberAttributesState()
const contractState = useInputState({ const contractState = useInputState({
id: 'contract-address', id: 'contract-address',
name: 'contract-address', name: 'contract-address',
@ -53,6 +65,8 @@ const WhitelistExecutePage: NextPage = () => {
}) })
const contractAddress = contractState.value const contractAddress = contractState.value
const debouncedWhitelistContractState = useDebounce(contractState.value, 300)
const limitState = useNumberInputState({ const limitState = useNumberInputState({
id: 'limit', id: 'limit',
name: 'limit', name: 'limit',
@ -63,7 +77,9 @@ const WhitelistExecutePage: NextPage = () => {
const showLimitState = isEitherType(type, ['update_per_address_limit', 'increase_member_limit']) const showLimitState = isEitherType(type, ['update_per_address_limit', 'increase_member_limit'])
const showTimestamp = isEitherType(type, ['update_start_time', 'update_end_time']) 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 showAdminList = isEitherType(type, ['update_admins'])
const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value]) const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value])
@ -73,14 +89,44 @@ const WhitelistExecutePage: NextPage = () => {
type, type,
limit: limitState.value, limit: limitState.value,
timestamp: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', timestamp: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
members: [ members:
...new Set( whitelistType === 'standard'
addressListState.values ? [
.map((a) => a.address.trim()) ...new Set(
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars')) addressListState.values
.concat(memberList), .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: [ admins: [
...new Set( ...new Set(
addressListState.values addressListState.values
@ -122,11 +168,55 @@ const WhitelistExecutePage: NextPage = () => {
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [contractAddress]) }, [contractAddress])
useEffect(() => { useEffect(() => {
const initial = new URL(document.URL).searchParams.get('contractAddress') const initial = new URL(document.URL).searchParams.get('contractAddress')
if (initial && initial.length > 0) contractState.onChange(initial) 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 ( return (
<section className="py-6 px-12 space-y-4"> <section className="py-6 px-12 space-y-4">
<NextSeo title="Execute Whitelist Contract" /> <NextSeo title="Execute Whitelist Contract" />
@ -154,7 +244,7 @@ const WhitelistExecutePage: NextPage = () => {
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} /> <InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl> </FormControl>
</Conditional> </Conditional>
<Conditional test={showMemberList || showAdminList}> <Conditional test={(whitelistType === 'standard' && showMemberList) || showAdminList || showRemoveMemberList}>
<AddressList <AddressList
entries={addressListState.entries} entries={addressListState.entries}
isRequired isRequired
@ -164,13 +254,30 @@ const WhitelistExecutePage: NextPage = () => {
subtitle={type === 'update_admins' ? 'Enter the admin addresses' : 'Enter the member addresses'} subtitle={type === 'update_admins' ? 'Enter the admin addresses' : 'Enter the member addresses'}
title="Addresses" title="Addresses"
/> />
<Conditional test={showMemberList}> <Conditional test={whitelistType === 'standard' && showMemberList}>
<Alert className="mt-8" type="info"> <Alert className="mt-8" type="info">
You may optionally choose a text file of additional member addresses. You may optionally choose a text file of additional member addresses.
</Alert> </Alert>
<WhitelistUpload onChange={setMemberList} /> <WhitelistUpload onChange={setMemberList} />
</Conditional> </Conditional>
</Conditional> </Conditional>
<Conditional test={whitelistType === 'flex' && showFlexMemberList}>
<FlexMemberAttributes
attributes={flexAddressListState.entries}
onAdd={flexAddressListState.add}
onChange={flexAddressListState.update}
onRemove={flexAddressListState.remove}
subtitle="Enter the member addresses and corresponding mint counts"
title="Members"
/>
<Conditional test={whitelistType === 'flex' && showFlexMemberList}>
<Alert className="mt-8" type="info">
You may optionally choose a .csv file of additional member addresses and mint counts.
</Alert>
<WhitelistFlexUpload onChange={setFlexMemberList} />
</Conditional>
</Conditional>
</div> </div>
<div className="space-y-8"> <div className="space-y-8">
<div className="relative"> <div className="relative">

View File

@ -13,13 +13,14 @@ import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview' import { JsonPreview } from 'components/JsonPreview'
import { LinkTabs } from 'components/LinkTabs' import { LinkTabs } from 'components/LinkTabs'
import { whitelistLinkTabs } from 'components/LinkTabs.data' import { whitelistLinkTabs } from 'components/LinkTabs.data'
import { type WhitelistFlexMember, WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
import { WhitelistUpload } from 'components/WhitelistUpload' import { WhitelistUpload } from 'components/WhitelistUpload'
import { useContracts } from 'contexts/contracts' import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet' import { useWallet } from 'contexts/wallet'
import type { InstantiateResponse } from 'contracts/sg721' import type { InstantiateResponse } from 'contracts/sg721'
import type { NextPage } from 'next' import type { NextPage } from 'next'
import { NextSeo } from 'next-seo' 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 { toast } from 'react-hot-toast'
import { FaAsterisk } from 'react-icons/fa' import { FaAsterisk } from 'react-icons/fa'
import { useMutation } from 'react-query' import { useMutation } from 'react-query'
@ -27,7 +28,7 @@ import { isValidAddress } from 'utils/isValidAddress'
import { withMetadata } from 'utils/layout' import { withMetadata } from 'utils/layout'
import { links } from 'utils/links' 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 WhitelistInstantiatePage: NextPage = () => {
const wallet = useWallet() const wallet = useWallet()
@ -36,8 +37,10 @@ const WhitelistInstantiatePage: NextPage = () => {
const [startDate, setStartDate] = useState<Date | undefined>(undefined) const [startDate, setStartDate] = useState<Date | undefined>(undefined)
const [endDate, setEndDate] = useState<Date | undefined>(undefined) const [endDate, setEndDate] = useState<Date | undefined>(undefined)
const [adminsMutable, setAdminsMutable] = useState<boolean>(true) const [adminsMutable, setAdminsMutable] = useState<boolean>(true)
const [whitelistType, setWhitelistType] = useState<'standard' | 'flex'>('standard')
const [whitelistArray, setWhitelistArray] = useState<string[]>([]) const [whitelistStandardArray, setWhitelistStandardArray] = useState<string[]>([])
const [whitelistFlexArray, setWhitelistFlexArray] = useState<WhitelistFlexMember[]>([])
const unitPriceState = useNumberInputState({ const unitPriceState = useNumberInputState({
id: 'unit-price', id: 'unit-price',
@ -63,6 +66,13 @@ const WhitelistInstantiatePage: NextPage = () => {
placeholder: '5', 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 addressListState = useAddressListState()
const { data, isLoading, mutate } = useMutation( const { data, isLoading, mutate } = useMutation(
@ -79,8 +89,8 @@ const WhitelistInstantiatePage: NextPage = () => {
throw new Error('End date is required') throw new Error('End date is required')
} }
const msg = { const standardMsg = {
members: whitelistArray, members: whitelistStandardArray,
start_time: (startDate.getTime() * 1_000_000).toString(), start_time: (startDate.getTime() * 1_000_000).toString(),
end_time: (endDate.getTime() * 1_000_000).toString(), end_time: (endDate.getTime() * 1_000_000).toString(),
mint_price: coin(String(Number(unitPriceState.value) * 1000000), 'ustars'), mint_price: coin(String(Number(unitPriceState.value) * 1000000), 'ustars'),
@ -95,8 +105,31 @@ const WhitelistInstantiatePage: NextPage = () => {
] || [wallet.address], ] || [wallet.address],
admins_mutable: adminsMutable, 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( 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...', loading: 'Instantiating contract...',
error: 'Instantiation failed!', error: 'Instantiation failed!',
@ -112,9 +145,18 @@ const WhitelistInstantiatePage: NextPage = () => {
) )
const whitelistFileOnChange = (whitelistData: string[]) => { const whitelistFileOnChange = (whitelistData: string[]) => {
setWhitelistArray(whitelistData) setWhitelistStandardArray(whitelistData)
} }
const whitelistFlexFileOnChange = (whitelistData: WhitelistFlexMember[]) => {
setWhitelistFlexArray(whitelistData)
}
useEffect(() => {
setWhitelistStandardArray([])
setWhitelistFlexArray([])
}, [whitelistType])
return ( return (
<form className="py-6 px-12 space-y-4" onSubmit={mutate}> <form className="py-6 px-12 space-y-4" onSubmit={mutate}>
<NextSeo title="Instantiate Whitelist Contract" /> <NextSeo title="Instantiate Whitelist Contract" />
@ -125,6 +167,48 @@ const WhitelistInstantiatePage: NextPage = () => {
/> />
<LinkTabs activeIndex={0} data={whitelistLinkTabs} /> <LinkTabs activeIndex={0} data={whitelistLinkTabs} />
<div className="flex justify-between mb-5 ml-6 max-w-[300px] text-lg font-bold">
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'standard'}
className="peer sr-only"
id="inlineRadio1"
name="inlineRadioOptions3"
onClick={() => {
setWhitelistType('standard')
}}
type="radio"
value="nft-storage"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio1"
>
Standard Whitelist
</label>
</div>
<div className="form-check form-check-inline">
<input
checked={whitelistType === 'flex'}
className="peer sr-only"
id="inlineRadio2"
name="inlineRadioOptions2"
onClick={() => {
setWhitelistType('flex')
}}
type="radio"
value="flex"
/>
<label
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio2"
>
Whitelist Flex
</label>
</div>
</div>
<Conditional test={Boolean(data)}> <Conditional test={Boolean(data)}>
<Alert type="info"> <Alert type="info">
<b>Instantiate success!</b> Here is the transaction result containing the contract address and the transaction <b>Instantiate success!</b> Here is the transaction result containing the contract address and the transaction
@ -134,7 +218,7 @@ const WhitelistInstantiatePage: NextPage = () => {
<br /> <br />
</Conditional> </Conditional>
<div className="mt-2 ml-3 w-full form-control"> <div className="mt-2 ml-3 w-1/3 form-control">
<label className="justify-start cursor-pointer label"> <label className="justify-start cursor-pointer label">
<span className="mr-4 font-bold">Mutable Administrator Addresses</span> <span className="mr-4 font-bold">Mutable Administrator Addresses</span>
<input <input
@ -158,16 +242,29 @@ const WhitelistInstantiatePage: NextPage = () => {
</div> </div>
<FormGroup subtitle="Your whitelisted addresses" title="Whitelist File"> <FormGroup subtitle="Your whitelisted addresses" title="Whitelist File">
<WhitelistUpload onChange={whitelistFileOnChange} /> <Conditional test={whitelistType === 'standard'}>
<Conditional test={whitelistArray.length > 0}> <WhitelistUpload onChange={whitelistFileOnChange} />
<JsonPreview content={whitelistArray} initialState={false} title="File Contents" /> <Conditional test={whitelistStandardArray.length > 0}>
<JsonPreview content={whitelistStandardArray} initialState={false} title="File Contents" />
</Conditional>
</Conditional>
<Conditional test={whitelistType === 'flex'}>
<WhitelistFlexUpload onChange={whitelistFlexFileOnChange} />
<Conditional test={whitelistFlexArray.length > 0}>
<JsonPreview content={whitelistFlexArray} initialState={false} title="File Contents" />
</Conditional>
</Conditional> </Conditional>
</FormGroup> </FormGroup>
<FormGroup subtitle="Information about your minting settings" title="Minting Details"> <FormGroup subtitle="Information about your minting settings" title="Minting Details">
<NumberInput isRequired {...unitPriceState} /> <NumberInput isRequired {...unitPriceState} />
<NumberInput isRequired {...memberLimitState} /> <NumberInput isRequired {...memberLimitState} />
<NumberInput isRequired {...perAddressLimitState} /> <Conditional test={whitelistType === 'standard'}>
<NumberInput isRequired {...perAddressLimitState} />
</Conditional>
<Conditional test={whitelistType === 'flex'}>
<NumberInput {...whaleCapState} />
</Conditional>
<FormControl htmlId="start-date" isRequired subtitle="Start time for the minting" title="Start Time"> <FormControl htmlId="start-date" isRequired subtitle="Start time for the minting" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setStartDate(date)} value={startDate} /> <InputDateTime minDate={new Date()} onChange={(date) => setStartDate(date)} value={startDate} />
</FormControl> </FormControl>

View File

@ -1,9 +1,12 @@
export const SG721_CODE_ID = parseInt(process.env.NEXT_PUBLIC_SG721_CODE_ID, 10) 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 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_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_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_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_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_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_ADDRESS
export const BASE_FACTORY_UPDATABLE_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_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 export const SG721_NAME_ADDRESS = process.env.NEXT_PUBLIC_SG721_NAME_ADDRESS

31
utils/csvToFlexList.ts Normal file
View File

@ -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<string, unknown>
object[header] = values[index]
return object
}, {})
return el
})
return arr as WhitelistFlexMember[]
}

View File

@ -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
}