Add splits contract dashboard & mint revenue payment address
This commit is contained in:
parent
1a076026e0
commit
7a21240bed
@ -1,4 +1,4 @@
|
|||||||
APP_VERSION=0.4.8
|
APP_VERSION=0.4.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=1702
|
NEXT_PUBLIC_SG721_CODE_ID=1702
|
||||||
@ -12,6 +12,8 @@ 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
|
||||||
NEXT_PUBLIC_BADGE_NFT_ADDRESS="stars1vlw4y54dyzt3zg7phj8yey9fg4zj49czknssngwmgrnwymyktztstalg7t"
|
NEXT_PUBLIC_BADGE_NFT_ADDRESS="stars1vlw4y54dyzt3zg7phj8yey9fg4zj49czknssngwmgrnwymyktztstalg7t"
|
||||||
|
NEXT_PUBLIC_SPLITS_CODE_ID=1904
|
||||||
|
NEXT_PUBLIC_CW4_GROUP_CODE_ID=1905
|
||||||
|
|
||||||
|
|
||||||
NEXT_PUBLIC_API_URL=https://nft-api.elgafar-1.stargaze-apis.com
|
NEXT_PUBLIC_API_URL=https://nft-api.elgafar-1.stargaze-apis.com
|
||||||
|
@ -104,3 +104,26 @@ export const badgeHubLinkTabs: LinkTabProps[] = [
|
|||||||
href: '/contracts/badgeHub/migrate',
|
href: '/contracts/badgeHub/migrate',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const splitsLinkTabs: LinkTabProps[] = [
|
||||||
|
{
|
||||||
|
title: 'Instantiate',
|
||||||
|
description: `Initialize a new Splits contract`,
|
||||||
|
href: '/contracts/splits/instantiate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Query',
|
||||||
|
description: `Dispatch queries for your Splits contract`,
|
||||||
|
href: '/contracts/splits/query',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Execute',
|
||||||
|
description: `Execute Splits contract actions`,
|
||||||
|
href: '/contracts/splits/execute',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Migrate',
|
||||||
|
description: `Migrate Splits contract`,
|
||||||
|
href: '/contracts/splits/migrate',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
@ -190,6 +190,15 @@ export const Sidebar = () => {
|
|||||||
<Link href="/contracts/badgeHub/">Badge Hub Contract</Link>
|
<Link href="/contracts/badgeHub/">Badge Hub Contract</Link>
|
||||||
</li>
|
</li>
|
||||||
</Conditional>
|
</Conditional>
|
||||||
|
<li
|
||||||
|
className={clsx(
|
||||||
|
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||||
|
router.asPath.includes('/contracts/splits/') ? 'text-white' : 'text-gray',
|
||||||
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<Link href="/contracts/splits/">Splits Contract</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -119,9 +119,9 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
|
|||||||
<TextInput className="mt-2" {...symbolState} isRequired />
|
<TextInput className="mt-2" {...symbolState} isRequired />
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(minterType === 'base' ? 'ml-10' : '')}>
|
<div className={clsx(minterType === 'base' ? 'ml-10' : '')}>
|
||||||
<TextInput {...externalLinkState} />
|
<TextInput className="mt-2" {...externalLinkState} />
|
||||||
<FormControl
|
<FormControl
|
||||||
className={clsx(minterType === 'base' ? 'mt-12' : '')}
|
className={clsx(minterType === 'base' ? 'mt-12' : 'mt-2')}
|
||||||
htmlId="timestamp"
|
htmlId="timestamp"
|
||||||
subtitle="Trading start time offset will be set as 2 weeks by default."
|
subtitle="Trading start time offset will be set as 2 weeks by default."
|
||||||
title="Trading Start Time (optional)"
|
title="Trading Start Time (optional)"
|
||||||
@ -140,6 +140,8 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
|
|||||||
<input
|
<input
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
minterType === 'base' ? 'w-1/2' : 'w-full',
|
||||||
|
'p-[13px] rounded border-2 border-white/20 border-dashed cursor-pointer h-18',
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||||
'before:hover:bg-white/5 before:transition',
|
'before:hover:bg-white/5 before:transition',
|
||||||
)}
|
)}
|
||||||
@ -173,7 +175,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<div className={clsx(minterType === 'base' ? 'flex flex-col -ml-16 space-y-2' : 'flex flex-col space-y-2')}>
|
<div className={clsx(minterType === 'base' ? 'flex flex-col -ml-16 space-y-2' : 'flex flex-col space-y-2')}>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex">
|
<div className="flex mt-4">
|
||||||
<span className="mt-1 text-sm first-letter:capitalize">
|
<span className="mt-1 text-sm first-letter:capitalize">
|
||||||
Does the collection contain explicit content?
|
Does the collection contain explicit content?
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { FormControl } from 'components/FormControl'
|
import { FormControl } from 'components/FormControl'
|
||||||
import { FormGroup } from 'components/FormGroup'
|
import { FormGroup } from 'components/FormGroup'
|
||||||
import { 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 React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { resolveAddress } from 'utils/resolveAddress'
|
||||||
|
|
||||||
import { NumberInput } from '../../forms/FormInput'
|
import { useWallet } from '../../../contexts/wallet'
|
||||||
|
import { NumberInput, TextInput } from '../../forms/FormInput'
|
||||||
import type { UploadMethod } from './UploadDetails'
|
import type { UploadMethod } from './UploadDetails'
|
||||||
|
|
||||||
interface MintingDetailsProps {
|
interface MintingDetailsProps {
|
||||||
@ -18,9 +20,12 @@ export interface MintingDetailsDataProps {
|
|||||||
unitPrice: string
|
unitPrice: string
|
||||||
perAddressLimit: number
|
perAddressLimit: number
|
||||||
startTime: string
|
startTime: string
|
||||||
|
paymentAddress?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: MintingDetailsProps) => {
|
export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: MintingDetailsProps) => {
|
||||||
|
const wallet = useWallet()
|
||||||
|
|
||||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
||||||
|
|
||||||
const numberOfTokensState = useNumberInputState({
|
const numberOfTokensState = useNumberInputState({
|
||||||
@ -47,6 +52,24 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
|
|||||||
placeholder: '1',
|
placeholder: '1',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const paymentAddressState = useInputState({
|
||||||
|
id: 'payment-address',
|
||||||
|
name: 'paymentAddress',
|
||||||
|
title: 'Payment Address (optional)',
|
||||||
|
subtitle: 'Address to receive minting revenues (defaults to current wallet address)',
|
||||||
|
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvePaymentAddress = async () => {
|
||||||
|
await resolveAddress(paymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
|
||||||
|
paymentAddressState.onChange(resolvedAddress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void resolvePaymentAddress()
|
||||||
|
}, [paymentAddressState.value])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens)
|
if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens)
|
||||||
const data: MintingDetailsDataProps = {
|
const data: MintingDetailsDataProps = {
|
||||||
@ -54,10 +77,18 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
|
|||||||
unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : '',
|
unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : '',
|
||||||
perAddressLimit: perAddressLimitState.value,
|
perAddressLimit: perAddressLimitState.value,
|
||||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||||
|
paymentAddress: paymentAddressState.value,
|
||||||
}
|
}
|
||||||
onChange(data)
|
onChange(data)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [numberOfTokens, numberOfTokensState.value, unitPriceState.value, perAddressLimitState.value, timestamp])
|
}, [
|
||||||
|
numberOfTokens,
|
||||||
|
numberOfTokensState.value,
|
||||||
|
unitPriceState.value,
|
||||||
|
perAddressLimitState.value,
|
||||||
|
timestamp,
|
||||||
|
paymentAddressState.value,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -74,6 +105,7 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
|
|||||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<TextInput className="p-4 mt-5" {...paymentAddressState} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
7
components/contracts/splits/ExecuteCombobox.hooks.ts
Normal file
7
components/contracts/splits/ExecuteCombobox.hooks.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { ExecuteListItem } from 'contracts/splits/messages/execute'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export const useExecuteComboboxState = () => {
|
||||||
|
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||||
|
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||||
|
}
|
93
components/contracts/splits/ExecuteCombobox.tsx
Normal file
93
components/contracts/splits/ExecuteCombobox.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { Combobox, Transition } from '@headlessui/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { matchSorter } from 'match-sorter'
|
||||||
|
import { Fragment, useState } from 'react'
|
||||||
|
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||||
|
|
||||||
|
import type { ExecuteListItem } from '../../../contracts/splits/messages/execute'
|
||||||
|
import { EXECUTE_LIST } from '../../../contracts/splits/messages/execute'
|
||||||
|
|
||||||
|
export interface ExecuteComboboxProps {
|
||||||
|
value: ExecuteListItem | null
|
||||||
|
onChange: (item: ExecuteListItem) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
|
const filtered =
|
||||||
|
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
as={FormControl}
|
||||||
|
htmlId="message-type"
|
||||||
|
labelAs={Combobox.Label}
|
||||||
|
onChange={onChange}
|
||||||
|
subtitle="Contract execute message type"
|
||||||
|
title="Message Type"
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Combobox.Input
|
||||||
|
className={clsx(
|
||||||
|
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||||
|
'placeholder:text-white/50',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
)}
|
||||||
|
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
||||||
|
id="message-type"
|
||||||
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
|
placeholder="Select message type"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Combobox.Button
|
||||||
|
className={clsx(
|
||||||
|
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||||
|
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||||
|
</Combobox.Button>
|
||||||
|
|
||||||
|
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||||
|
<Combobox.Options
|
||||||
|
className={clsx(
|
||||||
|
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||||
|
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||||
|
'divide-y divide-stone-500/50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{filtered.length < 1 && (
|
||||||
|
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||||
|
Message type not found.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{filtered.map((entry) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={entry.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||||
|
}
|
||||||
|
value={entry}
|
||||||
|
>
|
||||||
|
<span className="font-bold">{entry.name}</span>
|
||||||
|
<span className="max-w-md text-sm">{entry.description}</span>
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{value && (
|
||||||
|
<div className="flex space-x-2 text-white/50">
|
||||||
|
<div className="mt-1">
|
||||||
|
<FaInfoCircle className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">{value.description}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
)
|
||||||
|
}
|
33
components/forms/MemberAttributes.hooks.ts
Normal file
33
components/forms/MemberAttributes.hooks.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { uid } from 'utils/random'
|
||||||
|
|
||||||
|
import type { Attribute } from './MemberAttributes'
|
||||||
|
|
||||||
|
export function useMemberAttributesState() {
|
||||||
|
const [record, setRecord] = useState<Record<string, Attribute>>(() => ({}))
|
||||||
|
|
||||||
|
const entries = useMemo(() => Object.entries(record), [record])
|
||||||
|
const values = useMemo(() => Object.values(record), [record])
|
||||||
|
|
||||||
|
function add(attribute: Attribute = { address: '', weight: 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 }
|
||||||
|
}
|
94
components/forms/MemberAttributes.tsx
Normal file
94
components/forms/MemberAttributes.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||||
|
import { useEffect, useId, useMemo } from 'react'
|
||||||
|
import { FaMinus, FaPlus } from 'react-icons/fa'
|
||||||
|
|
||||||
|
import { useInputState, useNumberInputState } from './FormInput.hooks'
|
||||||
|
|
||||||
|
export interface Attribute {
|
||||||
|
address: string
|
||||||
|
weight: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemberAttributesProps {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
isRequired?: boolean
|
||||||
|
attributes: [string, Attribute][]
|
||||||
|
onAdd: () => void
|
||||||
|
onChange: (key: string, attribute: Attribute) => void
|
||||||
|
onRemove: (key: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MemberAttributes(props: MemberAttributesProps) {
|
||||||
|
const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl isRequired={isRequired} subtitle={subtitle} title={title}>
|
||||||
|
{attributes.map(([id], i) => (
|
||||||
|
<MemberAttribute
|
||||||
|
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: MemberAttributesProps['onAdd']
|
||||||
|
onChange: MemberAttributesProps['onChange']
|
||||||
|
onRemove: MemberAttributesProps['onRemove']
|
||||||
|
defaultAttribute: Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MemberAttribute({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: MemberAttributeProps) {
|
||||||
|
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 weightState = useNumberInputState({
|
||||||
|
id: `ma-weight-${htmlId}`,
|
||||||
|
name: `ma-weight-${htmlId}`,
|
||||||
|
title: `Weight`,
|
||||||
|
defaultValue: defaultAttribute.weight,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange(id, { address: addressState.value, weight: weightState.value })
|
||||||
|
}, [addressState.value, weightState.value, id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid relative lg:grid-cols-[70%_20%_10%] 2xl:space-x-2">
|
||||||
|
<AddressInput {...addressState} />
|
||||||
|
<NumberInput {...weightState} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
@ -17,6 +17,9 @@ import { Fragment, useEffect } from 'react'
|
|||||||
import type { State } from 'zustand'
|
import type { State } from 'zustand'
|
||||||
import create from 'zustand'
|
import create from 'zustand'
|
||||||
|
|
||||||
|
import type { UseSplitsContractProps } from '../contracts/splits/useContract'
|
||||||
|
import { useSplitsContract } from '../contracts/splits/useContract'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contracts store type definitions
|
* Contracts store type definitions
|
||||||
*/
|
*/
|
||||||
@ -28,6 +31,7 @@ export interface ContractsStore extends State {
|
|||||||
vendingFactory: UseVendingFactoryContractProps | null
|
vendingFactory: UseVendingFactoryContractProps | null
|
||||||
baseFactory: UseBaseFactoryContractProps | null
|
baseFactory: UseBaseFactoryContractProps | null
|
||||||
badgeHub: UseBadgeHubContractProps | null
|
badgeHub: UseBadgeHubContractProps | null
|
||||||
|
splits: UseSplitsContractProps | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,6 +45,7 @@ export const defaultValues: ContractsStore = {
|
|||||||
vendingFactory: null,
|
vendingFactory: null,
|
||||||
baseFactory: null,
|
baseFactory: null,
|
||||||
badgeHub: null,
|
badgeHub: null,
|
||||||
|
splits: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,6 +76,7 @@ const ContractsSubscription: VFC = () => {
|
|||||||
const vendingFactory = useVendingFactoryContract()
|
const vendingFactory = useVendingFactoryContract()
|
||||||
const baseFactory = useBaseFactoryContract()
|
const baseFactory = useBaseFactoryContract()
|
||||||
const badgeHub = useBadgeHubContract()
|
const badgeHub = useBadgeHubContract()
|
||||||
|
const splits = useSplitsContract()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
useContracts.setState({
|
useContracts.setState({
|
||||||
@ -81,8 +87,9 @@ const ContractsSubscription: VFC = () => {
|
|||||||
vendingFactory,
|
vendingFactory,
|
||||||
baseFactory,
|
baseFactory,
|
||||||
badgeHub,
|
badgeHub,
|
||||||
|
splits,
|
||||||
})
|
})
|
||||||
}, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub])
|
}, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub, splits])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
191
contracts/splits/contract.ts
Normal file
191
contracts/splits/contract.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||||
|
import type { Coin } from '@cosmjs/proto-signing'
|
||||||
|
import type { logs } from '@cosmjs/stargate'
|
||||||
|
|
||||||
|
export interface InstantiateResponse {
|
||||||
|
readonly contractAddress: string
|
||||||
|
readonly transactionHash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MigrateResponse {
|
||||||
|
readonly transactionHash: string
|
||||||
|
readonly logs: readonly logs.Log[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitsInstance {
|
||||||
|
readonly contractAddress: string
|
||||||
|
//Query
|
||||||
|
getAdmin: () => Promise<string>
|
||||||
|
getMemberWeight: (member: string) => Promise<string>
|
||||||
|
listMembers: (startAfter?: string, limit?: number) => Promise<string[]>
|
||||||
|
getGroup: () => Promise<string>
|
||||||
|
|
||||||
|
//Execute
|
||||||
|
updateAdmin: (admin: string) => Promise<string>
|
||||||
|
distribute: () => Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitsMessages {
|
||||||
|
updateAdmin: (admin: string) => UpdateAdminMessage
|
||||||
|
distribute: () => DistributeMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateAdminMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: {
|
||||||
|
update_admin: { admin: string }
|
||||||
|
}
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributeMessage {
|
||||||
|
sender: string
|
||||||
|
contract: string
|
||||||
|
msg: { distribute: Record<string, never> }
|
||||||
|
funds: Coin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitsContract {
|
||||||
|
instantiate: (
|
||||||
|
codeId: number,
|
||||||
|
initMsg: Record<string, unknown>,
|
||||||
|
label: string,
|
||||||
|
admin?: string,
|
||||||
|
) => Promise<InstantiateResponse>
|
||||||
|
|
||||||
|
use: (contractAddress: string) => SplitsInstance
|
||||||
|
|
||||||
|
migrate: (
|
||||||
|
senderAddress: string,
|
||||||
|
contractAddress: string,
|
||||||
|
codeId: number,
|
||||||
|
migrateMsg: Record<string, unknown>,
|
||||||
|
) => Promise<MigrateResponse>
|
||||||
|
|
||||||
|
messages: (contractAddress: string) => SplitsMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Splits = (client: SigningCosmWasmClient, txSigner: string): SplitsContract => {
|
||||||
|
const use = (contractAddress: string): SplitsInstance => {
|
||||||
|
///QUERY
|
||||||
|
const listMembers = async (startAfter?: string, limit?: number): Promise<string[]> => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
list_members: { start_after: startAfter ? startAfter : undefined, limit },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMemberWeight = async (address: string): Promise<string> => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
member: { address },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAdmin = async (): Promise<string> => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
admin: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGroup = async (): Promise<string> => {
|
||||||
|
return client.queryContractSmart(contractAddress, {
|
||||||
|
group: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// EXECUTE
|
||||||
|
const updateAdmin = async (admin: string): Promise<string> => {
|
||||||
|
const res = await client.execute(
|
||||||
|
txSigner,
|
||||||
|
contractAddress,
|
||||||
|
{
|
||||||
|
update_admin: {
|
||||||
|
admin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'auto',
|
||||||
|
)
|
||||||
|
return res.transactionHash
|
||||||
|
}
|
||||||
|
|
||||||
|
const distribute = async (): Promise<string> => {
|
||||||
|
const res = await client.execute(
|
||||||
|
txSigner,
|
||||||
|
contractAddress,
|
||||||
|
{
|
||||||
|
distribute: {},
|
||||||
|
},
|
||||||
|
'auto',
|
||||||
|
)
|
||||||
|
return res.transactionHash
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contractAddress,
|
||||||
|
updateAdmin,
|
||||||
|
distribute,
|
||||||
|
getMemberWeight,
|
||||||
|
getAdmin,
|
||||||
|
listMembers,
|
||||||
|
getGroup,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instantiate = async (
|
||||||
|
codeId: number,
|
||||||
|
initMsg: Record<string, unknown>,
|
||||||
|
label: string,
|
||||||
|
admin?: string,
|
||||||
|
): Promise<InstantiateResponse> => {
|
||||||
|
const result = await client.instantiate(txSigner, codeId, initMsg, label, 'auto', {
|
||||||
|
admin,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
contractAddress: result.contractAddress,
|
||||||
|
transactionHash: result.transactionHash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrate = async (
|
||||||
|
senderAddress: string,
|
||||||
|
contractAddress: string,
|
||||||
|
codeId: number,
|
||||||
|
migrateMsg: Record<string, unknown>,
|
||||||
|
): Promise<MigrateResponse> => {
|
||||||
|
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
|
||||||
|
return {
|
||||||
|
transactionHash: result.transactionHash,
|
||||||
|
logs: result.logs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = (contractAddress: string) => {
|
||||||
|
const updateAdmin = (admin: string) => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
update_admin: { admin },
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const distribute = () => {
|
||||||
|
return {
|
||||||
|
sender: txSigner,
|
||||||
|
contract: contractAddress,
|
||||||
|
msg: {
|
||||||
|
distribute: {},
|
||||||
|
},
|
||||||
|
funds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateAdmin,
|
||||||
|
distribute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { use, instantiate, migrate, messages }
|
||||||
|
}
|
2
contracts/splits/index.ts
Normal file
2
contracts/splits/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './contract'
|
||||||
|
export * from './useContract'
|
79
contracts/splits/messages/execute.ts
Normal file
79
contracts/splits/messages/execute.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||||
|
|
||||||
|
import type { SplitsInstance } from '../index'
|
||||||
|
import { useSplitsContract } from '../index'
|
||||||
|
|
||||||
|
export type ExecuteType = typeof EXECUTE_TYPES[number]
|
||||||
|
|
||||||
|
export const EXECUTE_TYPES = ['update_admin', 'distribute'] as const
|
||||||
|
|
||||||
|
export interface ExecuteListItem {
|
||||||
|
id: ExecuteType
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||||
|
{
|
||||||
|
id: 'update_admin',
|
||||||
|
name: 'Update Admin',
|
||||||
|
description: `Update the splits contract admin`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'distribute',
|
||||||
|
name: 'Distribute',
|
||||||
|
description: `Distribute the revenue to the group members`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface DispatchExecuteProps {
|
||||||
|
type: ExecuteType
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type Select<T extends ExecuteType> = T
|
||||||
|
|
||||||
|
/** @see {@link SplitsInstance} */
|
||||||
|
export type DispatchExecuteArgs = {
|
||||||
|
contract: string
|
||||||
|
messages?: SplitsInstance
|
||||||
|
} & ({ type: Select<'update_admin'>; admin: string } | { type: Select<'distribute'> | undefined })
|
||||||
|
|
||||||
|
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||||
|
const { messages } = args
|
||||||
|
if (!messages) {
|
||||||
|
throw new Error('Cannot dispatch execute, messages are not defined')
|
||||||
|
}
|
||||||
|
switch (args.type) {
|
||||||
|
case 'update_admin': {
|
||||||
|
return messages.updateAdmin(args.admin)
|
||||||
|
}
|
||||||
|
case 'distribute': {
|
||||||
|
return messages.distribute()
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error('Unknown execution type')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const { messages } = useSplitsContract()
|
||||||
|
const { contract } = args
|
||||||
|
switch (args.type) {
|
||||||
|
case 'update_admin': {
|
||||||
|
return messages(contract)?.updateAdmin(args.admin)
|
||||||
|
}
|
||||||
|
case 'distribute': {
|
||||||
|
return messages(contract)?.distribute()
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
|
||||||
|
return arr.some((val) => type === val)
|
||||||
|
}
|
43
contracts/splits/messages/query.ts
Normal file
43
contracts/splits/messages/query.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import type { SplitsInstance } from '../contract'
|
||||||
|
|
||||||
|
export type QueryType = typeof QUERY_TYPES[number]
|
||||||
|
|
||||||
|
export const QUERY_TYPES = ['admin', 'group', 'member', 'list_members'] as const
|
||||||
|
|
||||||
|
export interface QueryListItem {
|
||||||
|
id: QueryType
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QUERY_LIST: QueryListItem[] = [
|
||||||
|
{ id: 'list_members', name: 'Query Members', description: 'View the group members' },
|
||||||
|
{ id: 'member', name: 'Query Member Weight', description: 'Query the weight of a member in the group' },
|
||||||
|
{ id: 'admin', name: 'Query Admin', description: 'View the splits contract admin' },
|
||||||
|
{ id: 'group', name: 'Query Group Contract Address', description: 'View the group contract address' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface DispatchQueryProps {
|
||||||
|
messages: SplitsInstance | undefined
|
||||||
|
type: QueryType
|
||||||
|
address: string
|
||||||
|
startAfter?: string
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dispatchQuery = (props: DispatchQueryProps) => {
|
||||||
|
const { messages, type, address, startAfter, limit } = props
|
||||||
|
switch (type) {
|
||||||
|
case 'list_members':
|
||||||
|
return messages?.listMembers(startAfter, limit)
|
||||||
|
case 'admin':
|
||||||
|
return messages?.getAdmin()
|
||||||
|
case 'member':
|
||||||
|
return messages?.getMemberWeight(address)
|
||||||
|
case 'group':
|
||||||
|
return messages?.getGroup()
|
||||||
|
default: {
|
||||||
|
throw new Error('unknown query type')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
contracts/splits/useContract.ts
Normal file
93
contracts/splits/useContract.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||||
|
|
||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import type { InstantiateResponse, MigrateResponse, SplitsContract, SplitsInstance, SplitsMessages } from './contract'
|
||||||
|
import { Splits as initContract } from './contract'
|
||||||
|
|
||||||
|
export interface UseSplitsContractProps {
|
||||||
|
instantiate: (
|
||||||
|
codeId: number,
|
||||||
|
initMsg: Record<string, unknown>,
|
||||||
|
label: string,
|
||||||
|
admin?: string,
|
||||||
|
) => Promise<InstantiateResponse>
|
||||||
|
|
||||||
|
migrate: (contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>) => Promise<MigrateResponse>
|
||||||
|
|
||||||
|
use: (customAddress?: string) => SplitsInstance | undefined
|
||||||
|
|
||||||
|
updateContractAddress: (contractAddress: string) => void
|
||||||
|
|
||||||
|
messages: (contractAddress: string) => SplitsMessages | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSplitsContract(): UseSplitsContractProps {
|
||||||
|
const wallet = useWallet()
|
||||||
|
|
||||||
|
const [address, setAddress] = useState<string>('')
|
||||||
|
const [splits, setSplits] = useState<SplitsContract>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAddress(localStorage.getItem('contract_address') || '')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const splitsContract = initContract(wallet.getClient(), wallet.address)
|
||||||
|
setSplits(splitsContract)
|
||||||
|
}, [wallet])
|
||||||
|
|
||||||
|
const updateContractAddress = (contractAddress: string) => {
|
||||||
|
setAddress(contractAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const instantiate = useCallback(
|
||||||
|
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!splits) {
|
||||||
|
reject(new Error('Contract is not initialized.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
splits.instantiate(codeId, initMsg, label, admin).then(resolve).catch(reject)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[splits],
|
||||||
|
)
|
||||||
|
|
||||||
|
const migrate = useCallback(
|
||||||
|
(contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>): Promise<MigrateResponse> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!splits) {
|
||||||
|
reject(new Error('Contract is not initialized.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(wallet.address, contractAddress, codeId)
|
||||||
|
splits.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[splits, wallet],
|
||||||
|
)
|
||||||
|
|
||||||
|
const use = useCallback(
|
||||||
|
(customAddress = ''): SplitsInstance | undefined => {
|
||||||
|
return splits?.use(address || customAddress)
|
||||||
|
},
|
||||||
|
[splits, address],
|
||||||
|
)
|
||||||
|
|
||||||
|
const messages = useCallback(
|
||||||
|
(customAddress = ''): SplitsMessages | undefined => {
|
||||||
|
return splits?.messages(address || customAddress)
|
||||||
|
},
|
||||||
|
[splits, address],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
instantiate,
|
||||||
|
migrate,
|
||||||
|
use,
|
||||||
|
updateContractAddress,
|
||||||
|
messages,
|
||||||
|
}
|
||||||
|
}
|
2
env.d.ts
vendored
2
env.d.ts
vendored
@ -25,6 +25,8 @@ declare namespace NodeJS {
|
|||||||
readonly NEXT_PUBLIC_BADGE_HUB_ADDRESS: string
|
readonly NEXT_PUBLIC_BADGE_HUB_ADDRESS: string
|
||||||
readonly NEXT_PUBLIC_BADGE_NFT_CODE_ID: string
|
readonly NEXT_PUBLIC_BADGE_NFT_CODE_ID: string
|
||||||
readonly NEXT_PUBLIC_BADGE_NFT_ADDRESS: string
|
readonly NEXT_PUBLIC_BADGE_NFT_ADDRESS: string
|
||||||
|
readonly NEXT_PUBLIC_SPLITS_CODE_ID: string
|
||||||
|
readonly NEXT_PUBLIC_CW4_GROUP_CODE_ID: string
|
||||||
|
|
||||||
readonly NEXT_PUBLIC_PINATA_ENDPOINT_URL: string
|
readonly NEXT_PUBLIC_PINATA_ENDPOINT_URL: string
|
||||||
readonly NEXT_PUBLIC_API_URL: string
|
readonly NEXT_PUBLIC_API_URL: string
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stargaze-studio",
|
"name": "stargaze-studio",
|
||||||
"version": "0.4.8",
|
"version": "0.4.9",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
|
@ -408,6 +408,7 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}` : `${baseUri}`}`,
|
base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}` : `${baseUri}`}`,
|
||||||
start_time: mintingDetails?.startTime,
|
start_time: mintingDetails?.startTime,
|
||||||
num_tokens: mintingDetails?.numTokens,
|
num_tokens: mintingDetails?.numTokens,
|
||||||
|
payment_address: mintingDetails?.paymentAddress ? mintingDetails.paymentAddress.trim() : undefined,
|
||||||
mint_price: {
|
mint_price: {
|
||||||
amount: mintingDetails?.unitPrice,
|
amount: mintingDetails?.unitPrice,
|
||||||
denom: 'ustars',
|
denom: 'ustars',
|
||||||
@ -761,6 +762,12 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
)
|
)
|
||||||
if (mintingDetails.startTime === '') throw new Error('Start time is required')
|
if (mintingDetails.startTime === '') throw new Error('Start time is required')
|
||||||
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
|
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
|
||||||
|
if (
|
||||||
|
mintingDetails.paymentAddress &&
|
||||||
|
(!isValidAddress(mintingDetails.paymentAddress.trim()) ||
|
||||||
|
!mintingDetails.paymentAddress.trim().startsWith('stars1'))
|
||||||
|
)
|
||||||
|
throw new Error('Invalid payment address')
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkWhitelistDetails = async () => {
|
const checkWhitelistDetails = async () => {
|
||||||
@ -851,11 +858,19 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
if (minterType === 'vending' && whitelistDetails?.whitelistType === 'new' && whitelistDetails.memberLimit) {
|
if (minterType === 'vending' && whitelistDetails?.whitelistType === 'new' && whitelistDetails.memberLimit) {
|
||||||
const amountNeeded = Math.ceil(Number(whitelistDetails.memberLimit) / 1000) * 100000000 + 3000000000
|
const amountNeeded = Math.ceil(Number(whitelistDetails.memberLimit) / 1000) * 100000000 + 3000000000
|
||||||
if (amountNeeded >= Number(wallet.balance[0].amount))
|
if (amountNeeded >= Number(wallet.balance[0].amount))
|
||||||
throw new Error('Insufficient wallet balance to instantiate the required contracts.')
|
throw new Error(
|
||||||
|
`Insufficient wallet balance to instantiate the required contracts. Needed amount: ${(
|
||||||
|
amountNeeded / 1000000
|
||||||
|
).toString()} STARS`,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const amountNeeded = minterType === 'vending' ? 3000000000 : 1000000000
|
const amountNeeded = minterType === 'vending' ? 3000000000 : 1000000000
|
||||||
if (amountNeeded >= Number(wallet.balance[0].amount))
|
if (amountNeeded >= Number(wallet.balance[0].amount))
|
||||||
throw new Error('Insufficient wallet balance to instantiate the required contracts.')
|
throw new Error(
|
||||||
|
`Insufficient wallet balance to instantiate the required contracts. Needed amount: ${(
|
||||||
|
amountNeeded / 1000000
|
||||||
|
).toString()} STARS`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -58,6 +58,9 @@ const HomePage: NextPage = () => {
|
|||||||
Execute messages and run queries on the Badge Hub contract designed for event organizers.
|
Execute messages and run queries on the Badge Hub contract designed for event organizers.
|
||||||
</HomeCard>
|
</HomeCard>
|
||||||
</Conditional>
|
</Conditional>
|
||||||
|
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/contracts/splits" title="Splits Contract">
|
||||||
|
Execute messages and run queries on the Splits contract designed for revenue distribution.
|
||||||
|
</HomeCard>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
135
pages/contracts/splits/execute.tsx
Normal file
135
pages/contracts/splits/execute.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { Button } from 'components/Button'
|
||||||
|
import { Conditional } from 'components/Conditional'
|
||||||
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
|
import { ExecuteCombobox } from 'components/contracts/splits/ExecuteCombobox'
|
||||||
|
import { useExecuteComboboxState } from 'components/contracts/splits/ExecuteCombobox.hooks'
|
||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { AddressInput } from 'components/forms/FormInput'
|
||||||
|
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||||
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
|
import { LinkTabs } from 'components/LinkTabs'
|
||||||
|
import { splitsLinkTabs } from 'components/LinkTabs.data'
|
||||||
|
import { TransactionHash } from 'components/TransactionHash'
|
||||||
|
import { useContracts } from 'contexts/contracts'
|
||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import type { DispatchExecuteArgs } from 'contracts/splits/messages/execute'
|
||||||
|
import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/splits/messages/execute'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { NextSeo } from 'next-seo'
|
||||||
|
import type { FormEvent } from 'react'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { FaArrowRight } from 'react-icons/fa'
|
||||||
|
import { useMutation } from 'react-query'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
|
||||||
|
const SplitsExecutePage: NextPage = () => {
|
||||||
|
const { splits: contract } = useContracts()
|
||||||
|
const wallet = useWallet()
|
||||||
|
|
||||||
|
const [lastTx, setLastTx] = useState('')
|
||||||
|
|
||||||
|
const comboboxState = useExecuteComboboxState()
|
||||||
|
const type = comboboxState.value?.id
|
||||||
|
|
||||||
|
const contractState = useInputState({
|
||||||
|
id: 'contract-address',
|
||||||
|
name: 'contract-address',
|
||||||
|
title: 'Splits Address',
|
||||||
|
subtitle: 'Address of the Splits contract',
|
||||||
|
})
|
||||||
|
const contractAddress = contractState.value
|
||||||
|
|
||||||
|
const adminAddressState = useInputState({
|
||||||
|
id: 'admin-address',
|
||||||
|
name: 'admin-address',
|
||||||
|
title: 'Admin Address',
|
||||||
|
subtitle: 'Address of the new administrator',
|
||||||
|
})
|
||||||
|
|
||||||
|
const showAdminAddress = isEitherType(type, ['update_admin'])
|
||||||
|
|
||||||
|
const messages = useMemo(() => contract?.use(contractState.value), [contract, contractState.value])
|
||||||
|
const payload: DispatchExecuteArgs = {
|
||||||
|
contract: contractState.value,
|
||||||
|
messages,
|
||||||
|
type,
|
||||||
|
admin: adminAddressState.value.trim(),
|
||||||
|
}
|
||||||
|
const { isLoading, mutate } = useMutation(
|
||||||
|
async (event: FormEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!type) {
|
||||||
|
throw new Error('Please select message type!')
|
||||||
|
}
|
||||||
|
if (!wallet.initialized) {
|
||||||
|
throw new Error('Please connect your wallet.')
|
||||||
|
}
|
||||||
|
const txHash = await toast.promise(dispatchExecute(payload), {
|
||||||
|
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
|
||||||
|
loading: 'Executing message...',
|
||||||
|
success: (tx) => `Transaction ${tx} success!`,
|
||||||
|
})
|
||||||
|
if (txHash) {
|
||||||
|
setLastTx(txHash)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(String(error), { style: { maxWidth: 'none' } })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contractAddress.length > 0) {
|
||||||
|
void router.replace({ query: { contractAddress } })
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-6 px-12 space-y-4">
|
||||||
|
<NextSeo title="Execute Splits Contract" />
|
||||||
|
<ContractPageHeader
|
||||||
|
description="Splits contract distributes funds to a cw4-group based on member weights."
|
||||||
|
link={links.Documentation}
|
||||||
|
title="Splits Contract"
|
||||||
|
/>
|
||||||
|
<LinkTabs activeIndex={2} data={splitsLinkTabs} />
|
||||||
|
|
||||||
|
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<AddressInput {...contractState} />
|
||||||
|
<ExecuteCombobox {...comboboxState} />
|
||||||
|
<Conditional test={showAdminAddress}>
|
||||||
|
<AddressInput {...adminAddressState} />
|
||||||
|
</Conditional>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="relative">
|
||||||
|
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
|
||||||
|
Execute
|
||||||
|
</Button>
|
||||||
|
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
|
||||||
|
<TransactionHash hash={lastTx} />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormControl subtitle="View current message to be sent" title="Payload Preview">
|
||||||
|
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(SplitsExecutePage, { center: false })
|
1
pages/contracts/splits/index.tsx
Normal file
1
pages/contracts/splits/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './instantiate'
|
237
pages/contracts/splits/instantiate.tsx
Normal file
237
pages/contracts/splits/instantiate.tsx
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { toBase64, toUtf8 } from '@cosmjs/encoding'
|
||||||
|
import { Alert } from 'components/Alert'
|
||||||
|
import { Button } from 'components/Button'
|
||||||
|
import { Conditional } from 'components/Conditional'
|
||||||
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
|
import { AddressInput } from 'components/forms/FormInput'
|
||||||
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
|
import { LinkTabs } from 'components/LinkTabs'
|
||||||
|
import { splitsLinkTabs } from 'components/LinkTabs.data'
|
||||||
|
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, useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { FaAsterisk } from 'react-icons/fa'
|
||||||
|
import { useMutation } from 'react-query'
|
||||||
|
import { isValidAddress } from 'utils/isValidAddress'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
|
||||||
|
import { useInputState } from '../../../components/forms/FormInput.hooks'
|
||||||
|
import type { Attribute } from '../../../components/forms/MemberAttributes'
|
||||||
|
import { MemberAttributes } from '../../../components/forms/MemberAttributes'
|
||||||
|
import { useMemberAttributesState } from '../../../components/forms/MemberAttributes.hooks'
|
||||||
|
import { CW4_GROUP_CODE_ID, SPLITS_CODE_ID } from '../../../utils/constants'
|
||||||
|
import { resolveAddress } from '../../../utils/resolveAddress'
|
||||||
|
|
||||||
|
export type CW4Method = 'new' | 'existing'
|
||||||
|
|
||||||
|
const SplitsInstantiatePage: NextPage = () => {
|
||||||
|
const wallet = useWallet()
|
||||||
|
const { splits: contract } = useContracts()
|
||||||
|
const [members, setMembers] = useState<Attribute[]>([])
|
||||||
|
const [cw4Method, setCw4Method] = useState<CW4Method>('new')
|
||||||
|
|
||||||
|
const cw4GroupAddressState = useInputState({
|
||||||
|
id: 'cw4-group-address',
|
||||||
|
name: 'cw4-group-address',
|
||||||
|
title: 'CW4 Group Address',
|
||||||
|
subtitle: 'Address of the CW4 Group contract',
|
||||||
|
placeholder: 'stars1...',
|
||||||
|
})
|
||||||
|
|
||||||
|
const splitsAdminState = useInputState({
|
||||||
|
id: 'splits-admin',
|
||||||
|
name: 'splits-admin',
|
||||||
|
title: 'Splits Contract Admin',
|
||||||
|
subtitle: 'Address of the Splits Contract administrator',
|
||||||
|
defaultValue: wallet.address,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cw4GroupAdminState = useInputState({
|
||||||
|
id: 'cw4-group-admin',
|
||||||
|
name: 'cw4-group-admin',
|
||||||
|
title: 'CW4 Group Admin',
|
||||||
|
subtitle: 'Address of the CW4 Group administrator',
|
||||||
|
defaultValue: wallet.address,
|
||||||
|
})
|
||||||
|
|
||||||
|
const memberListState = useMemberAttributesState()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
memberListState.reset()
|
||||||
|
memberListState.add({
|
||||||
|
address: '',
|
||||||
|
weight: 0,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { data, isLoading, mutate } = useMutation(
|
||||||
|
async (event: FormEvent): Promise<InstantiateResponse | null> => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!contract) {
|
||||||
|
throw new Error('Smart contract connection failed')
|
||||||
|
}
|
||||||
|
const msg =
|
||||||
|
cw4Method === 'existing'
|
||||||
|
? {
|
||||||
|
admin: splitsAdminState.value ? splitsAdminState.value : undefined,
|
||||||
|
group: { cw4_address: cw4GroupAddressState.value },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
admin: splitsAdminState.value ? splitsAdminState.value : undefined,
|
||||||
|
group: {
|
||||||
|
cw4_instantiate: {
|
||||||
|
code_id: CW4_GROUP_CODE_ID,
|
||||||
|
label: 'cw4-group',
|
||||||
|
msg: toBase64(
|
||||||
|
toUtf8(
|
||||||
|
JSON.stringify({
|
||||||
|
admin: cw4GroupAdminState.value ? cw4GroupAdminState.value : undefined,
|
||||||
|
members: [
|
||||||
|
...new Set(
|
||||||
|
members
|
||||||
|
.filter(
|
||||||
|
(member) =>
|
||||||
|
member.address !== '' &&
|
||||||
|
member.weight > 0 &&
|
||||||
|
isValidAddress(member.address) &&
|
||||||
|
member.address.startsWith('stars'),
|
||||||
|
)
|
||||||
|
.map((member) => ({ addr: member.address, weight: member.weight })),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return toast.promise(contract.instantiate(SPLITS_CODE_ID, msg, 'Stargaze Splits Contract', wallet.address), {
|
||||||
|
loading: 'Instantiating contract...',
|
||||||
|
error: 'Instantiation failed!',
|
||||||
|
success: 'Instantiation success!',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(String(error), { style: { maxWidth: 'none' } })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolveMemberAddresses = () => {
|
||||||
|
const tempMembers: Attribute[] = []
|
||||||
|
memberListState.values.map(async (member) => {
|
||||||
|
await resolveAddress(member.address.trim(), wallet).then((resolvedAddress) => {
|
||||||
|
tempMembers.push({ address: resolvedAddress, weight: member.weight })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setMembers(tempMembers)
|
||||||
|
console.log('Members:', members)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resolveMemberAddresses()
|
||||||
|
}, [memberListState.values])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="py-6 px-12 space-y-4" onSubmit={mutate}>
|
||||||
|
<NextSeo title="Instantiate Splits Contract" />
|
||||||
|
<ContractPageHeader
|
||||||
|
description="Splits contract distributes funds to a cw4-group based on member weights."
|
||||||
|
link={links.Documentation}
|
||||||
|
title="Splits Contract"
|
||||||
|
/>
|
||||||
|
<LinkTabs activeIndex={0} data={splitsLinkTabs} />
|
||||||
|
|
||||||
|
<Conditional test={Boolean(data)}>
|
||||||
|
<Alert type="info">
|
||||||
|
<b>Instantiate success!</b> Here is the transaction result containing the contract address and the transaction
|
||||||
|
hash.
|
||||||
|
</Alert>
|
||||||
|
<JsonPreview content={data} title="Transaction Result" />
|
||||||
|
<br />
|
||||||
|
</Conditional>
|
||||||
|
|
||||||
|
<div className="justify-items-start mb-3 flex-column">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
checked={cw4Method === 'new'}
|
||||||
|
className="peer sr-only"
|
||||||
|
id="inlineRadio2"
|
||||||
|
name="inlineRadioOptions2"
|
||||||
|
onClick={() => {
|
||||||
|
setCw4Method('new')
|
||||||
|
}}
|
||||||
|
type="radio"
|
||||||
|
value="New"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||||
|
htmlFor="inlineRadio2"
|
||||||
|
>
|
||||||
|
New CW4 Group Contract
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
checked={cw4Method === 'existing'}
|
||||||
|
className="peer sr-only"
|
||||||
|
id="inlineRadio1"
|
||||||
|
name="inlineRadioOptions1"
|
||||||
|
onClick={() => {
|
||||||
|
setCw4Method('existing')
|
||||||
|
}}
|
||||||
|
type="radio"
|
||||||
|
value="Existing"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||||
|
htmlFor="inlineRadio1"
|
||||||
|
>
|
||||||
|
Use an existing CW4 Group Contract
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<AddressInput className="mt-2 ml-4 w-full" {...splitsAdminState} />
|
||||||
|
<Conditional test={cw4Method === 'new'}>
|
||||||
|
<AddressInput className="mt-2 ml-4 w-full" {...cw4GroupAdminState} />
|
||||||
|
</Conditional>
|
||||||
|
<Conditional test={cw4Method === 'existing'}>
|
||||||
|
<AddressInput className="mt-2 ml-4 w-full" {...cw4GroupAddressState} />
|
||||||
|
</Conditional>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Conditional test={cw4Method === 'new'}>
|
||||||
|
<div className="ml-4 w-full">
|
||||||
|
<MemberAttributes
|
||||||
|
attributes={memberListState.entries}
|
||||||
|
onAdd={memberListState.add}
|
||||||
|
onChange={memberListState.update}
|
||||||
|
onRemove={memberListState.remove}
|
||||||
|
title="Members"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Conditional>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center p-4">
|
||||||
|
<div className="flex-grow" />
|
||||||
|
<Button isLoading={isLoading} isWide rightIcon={<FaAsterisk />} type="submit">
|
||||||
|
Instantiate Contract
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(SplitsInstantiatePage, { center: false })
|
133
pages/contracts/splits/migrate.tsx
Normal file
133
pages/contracts/splits/migrate.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { Button } from 'components/Button'
|
||||||
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
|
import { useExecuteComboboxState } from 'components/contracts/splits/ExecuteCombobox.hooks'
|
||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||||
|
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||||
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
|
import { LinkTabs } from 'components/LinkTabs'
|
||||||
|
import { splitsLinkTabs } from 'components/LinkTabs.data'
|
||||||
|
import { TransactionHash } from 'components/TransactionHash'
|
||||||
|
import { useContracts } from 'contexts/contracts'
|
||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import type { MigrateResponse } from 'contracts/splits'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { NextSeo } from 'next-seo'
|
||||||
|
import type { FormEvent } from 'react'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { FaArrowRight } from 'react-icons/fa'
|
||||||
|
import { useMutation } from 'react-query'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
|
||||||
|
const SplitsMigratePage: NextPage = () => {
|
||||||
|
const { splits: contract } = useContracts()
|
||||||
|
const wallet = useWallet()
|
||||||
|
|
||||||
|
const [lastTx, setLastTx] = useState('')
|
||||||
|
|
||||||
|
const comboboxState = useExecuteComboboxState()
|
||||||
|
const type = comboboxState.value?.id
|
||||||
|
const codeIdState = useNumberInputState({
|
||||||
|
id: 'code-id',
|
||||||
|
name: 'code-id',
|
||||||
|
title: 'Code ID',
|
||||||
|
subtitle: 'Code ID of the New Splits contract',
|
||||||
|
placeholder: '1',
|
||||||
|
})
|
||||||
|
|
||||||
|
const contractState = useInputState({
|
||||||
|
id: 'contract-address',
|
||||||
|
name: 'contract-address',
|
||||||
|
title: 'Splits Address',
|
||||||
|
subtitle: 'Address of the Splits contract',
|
||||||
|
})
|
||||||
|
const contractAddress = contractState.value
|
||||||
|
|
||||||
|
const { data, isLoading, mutate } = useMutation(
|
||||||
|
async (event: FormEvent): Promise<MigrateResponse | null> => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!contract) {
|
||||||
|
throw new Error('Smart contract connection failed')
|
||||||
|
}
|
||||||
|
if (!wallet.initialized) {
|
||||||
|
throw new Error('Please connect your wallet.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrateMsg = {}
|
||||||
|
|
||||||
|
return toast.promise(contract.migrate(contractAddress, codeIdState.value, migrateMsg), {
|
||||||
|
error: `Migration failed!`,
|
||||||
|
loading: 'Executing message...',
|
||||||
|
success: (tx) => {
|
||||||
|
if (tx) {
|
||||||
|
setLastTx(tx.transactionHash)
|
||||||
|
}
|
||||||
|
return `Transaction success!`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(String(error), { style: { maxWidth: 'none' } })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contractAddress.length > 0) {
|
||||||
|
void router.replace({ query: { contractAddress } })
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-6 px-12 space-y-4">
|
||||||
|
<NextSeo title="Migrate Splits Contract" />
|
||||||
|
<ContractPageHeader
|
||||||
|
description="Splits contract distributes funds to a cw4-group based on member weights."
|
||||||
|
link={links.Documentation}
|
||||||
|
title="Splits Contract"
|
||||||
|
/>
|
||||||
|
<LinkTabs activeIndex={3} data={splitsLinkTabs} />
|
||||||
|
|
||||||
|
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<AddressInput {...contractState} />
|
||||||
|
<NumberInput isRequired {...codeIdState} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="relative">
|
||||||
|
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
|
||||||
|
Execute
|
||||||
|
</Button>
|
||||||
|
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
|
||||||
|
<TransactionHash hash={lastTx} />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormControl subtitle="View current message to be sent" title="Payload Preview">
|
||||||
|
<JsonPreview
|
||||||
|
content={{
|
||||||
|
sender: wallet.address,
|
||||||
|
contract: contractAddress,
|
||||||
|
code_id: codeIdState.value,
|
||||||
|
msg: {},
|
||||||
|
}}
|
||||||
|
isCopyable
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(SplitsMigratePage, { center: false })
|
156
pages/contracts/splits/query.tsx
Normal file
156
pages/contracts/splits/query.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { Conditional } from 'components/Conditional'
|
||||||
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
|
import { FormControl } from 'components/FormControl'
|
||||||
|
import { AddressInput, NumberInput, TextInput } from 'components/forms/FormInput'
|
||||||
|
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||||
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
|
import { LinkTabs } from 'components/LinkTabs'
|
||||||
|
import { splitsLinkTabs } from 'components/LinkTabs.data'
|
||||||
|
import { useContracts } from 'contexts/contracts'
|
||||||
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import type { QueryType } from 'contracts/splits/messages/query'
|
||||||
|
import { dispatchQuery, QUERY_LIST } from 'contracts/splits/messages/query'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { NextSeo } from 'next-seo'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { useQuery } from 'react-query'
|
||||||
|
import { withMetadata } from 'utils/layout'
|
||||||
|
import { links } from 'utils/links'
|
||||||
|
import { resolveAddress } from 'utils/resolveAddress'
|
||||||
|
|
||||||
|
const SplitsQueryPage: NextPage = () => {
|
||||||
|
const { splits: contract } = useContracts()
|
||||||
|
const wallet = useWallet()
|
||||||
|
|
||||||
|
const contractState = useInputState({
|
||||||
|
id: 'contract-address',
|
||||||
|
name: 'contract-address',
|
||||||
|
title: 'Splits Address',
|
||||||
|
subtitle: 'Address of the Splits contract',
|
||||||
|
})
|
||||||
|
const contractAddress = contractState.value
|
||||||
|
|
||||||
|
const memberAddressState = useInputState({
|
||||||
|
id: 'member-address',
|
||||||
|
name: 'member-address',
|
||||||
|
title: 'Member Address',
|
||||||
|
subtitle: 'Member address to query the weight for',
|
||||||
|
})
|
||||||
|
|
||||||
|
const memberAddress = memberAddressState.value
|
||||||
|
|
||||||
|
const startAfterStringState = useInputState({
|
||||||
|
id: 'start-after-string',
|
||||||
|
name: 'start-after-string',
|
||||||
|
title: 'Start After (optional)',
|
||||||
|
subtitle: 'The member address to start the pagination after',
|
||||||
|
})
|
||||||
|
|
||||||
|
const paginationLimitState = useNumberInputState({
|
||||||
|
id: 'pagination-limit',
|
||||||
|
name: 'pagination-limit',
|
||||||
|
title: 'Pagination Limit (optional)',
|
||||||
|
subtitle: 'The number of items to return (max: 30)',
|
||||||
|
defaultValue: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [type, setType] = useState<QueryType>('list_members')
|
||||||
|
|
||||||
|
const { data: response } = useQuery(
|
||||||
|
[
|
||||||
|
contractAddress,
|
||||||
|
type,
|
||||||
|
contract,
|
||||||
|
wallet,
|
||||||
|
memberAddress,
|
||||||
|
startAfterStringState.value,
|
||||||
|
paginationLimitState.value,
|
||||||
|
] as const,
|
||||||
|
async ({ queryKey }) => {
|
||||||
|
const [_contractAddress, _type, _contract, _wallet, _memberAddress, startAfter, limit] = queryKey
|
||||||
|
const messages = contract?.use(contractAddress)
|
||||||
|
const res = await resolveAddress(_memberAddress, wallet).then(async (resolvedAddress) => {
|
||||||
|
const result = await dispatchQuery({
|
||||||
|
messages,
|
||||||
|
type,
|
||||||
|
address: resolvedAddress,
|
||||||
|
startAfter: startAfter.length > 0 ? startAfter : undefined,
|
||||||
|
limit: limit > 0 ? limit : undefined,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: null,
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||||
|
},
|
||||||
|
enabled: Boolean(contractAddress && contract && wallet),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contractAddress.length > 0) {
|
||||||
|
void router.replace({ query: { contractAddress } })
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-6 px-12 space-y-4">
|
||||||
|
<NextSeo title="Query Splits Contract" />
|
||||||
|
<ContractPageHeader
|
||||||
|
description="Splits contract distributes funds to a cw4-group based on member weights."
|
||||||
|
link={links.Documentation}
|
||||||
|
title="Splits Contract"
|
||||||
|
/>
|
||||||
|
<LinkTabs activeIndex={1} data={splitsLinkTabs} />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 p-4 space-x-8">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<AddressInput {...contractState} />
|
||||||
|
<FormControl htmlId="contract-query-type" subtitle="Type of query to be dispatched" title="Query Type">
|
||||||
|
<select
|
||||||
|
className={clsx(
|
||||||
|
'bg-white/10 rounded border-2 border-white/20 form-select',
|
||||||
|
'placeholder:text-white/50',
|
||||||
|
'focus:ring focus:ring-plumbus-20',
|
||||||
|
)}
|
||||||
|
defaultValue="config"
|
||||||
|
id="contract-query-type"
|
||||||
|
name="query-type"
|
||||||
|
onChange={(e) => setType(e.target.value as QueryType)}
|
||||||
|
>
|
||||||
|
{QUERY_LIST.map(({ id, name }) => (
|
||||||
|
<option key={`query-${id}`} className="mt-2 text-lg bg-[#1A1A1A]" value={id}>
|
||||||
|
{name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormControl>
|
||||||
|
<Conditional test={type === 'member'}>
|
||||||
|
<AddressInput {...memberAddressState} />
|
||||||
|
</Conditional>
|
||||||
|
|
||||||
|
<Conditional test={type === 'list_members'}>
|
||||||
|
<TextInput {...startAfterStringState} />
|
||||||
|
<NumberInput {...paginationLimitState} />
|
||||||
|
</Conditional>
|
||||||
|
</div>
|
||||||
|
<JsonPreview content={contractAddress ? { type, response } : null} title="Query Response" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMetadata(SplitsQueryPage, { center: false })
|
@ -9,6 +9,8 @@ export const BADGE_HUB_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_HUB_CODE
|
|||||||
export const BADGE_HUB_ADDRESS = process.env.NEXT_PUBLIC_BADGE_HUB_ADDRESS
|
export const BADGE_HUB_ADDRESS = process.env.NEXT_PUBLIC_BADGE_HUB_ADDRESS
|
||||||
export const BADGE_NFT_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_NFT_CODE_ID, 10)
|
export const BADGE_NFT_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_NFT_CODE_ID, 10)
|
||||||
export const BADGE_NFT_ADDRESS = process.env.NEXT_PUBLIC_BADGE_NFT_ADDRESS
|
export const BADGE_NFT_ADDRESS = process.env.NEXT_PUBLIC_BADGE_NFT_ADDRESS
|
||||||
|
export const SPLITS_CODE_ID = parseInt(process.env.NEXT_PUBLIC_SPLITS_CODE_ID, 10)
|
||||||
|
export const CW4_GROUP_CODE_ID = parseInt(process.env.NEXT_PUBLIC_CW4_GROUP_CODE_ID, 10)
|
||||||
|
|
||||||
export const PINATA_ENDPOINT_URL = process.env.NEXT_PUBLIC_PINATA_ENDPOINT_URL
|
export const PINATA_ENDPOINT_URL = process.env.NEXT_PUBLIC_PINATA_ENDPOINT_URL
|
||||||
export const NETWORK = process.env.NEXT_PUBLIC_NETWORK
|
export const NETWORK = process.env.NEXT_PUBLIC_NETWORK
|
||||||
|
Loading…
Reference in New Issue
Block a user