diff --git a/components/LinkTabs.data.ts b/components/LinkTabs.data.ts index e5664b7..70c22d6 100644 --- a/components/LinkTabs.data.ts +++ b/components/LinkTabs.data.ts @@ -171,3 +171,16 @@ export const authzLinkTabs: LinkTabProps[] = [ href: '/authz/revoke', }, ] + +export const snapshotLinkTabs: LinkTabProps[] = [ + { + title: 'Collection Holders', + description: `Take a snapshot of collection holders`, + href: '/snapshots/holders', + }, + { + title: 'Chain Snapshots', + description: `Export a list of users fulfilling a given condition`, + href: '/snapshots/chain', + }, +] diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index f3d322d..f11f11f 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -1903,6 +1903,13 @@ const CollectionCreationPage: NextPage = () => { Setting the unit price as 0 for public minting may render the collection vulnerable for bot attacks. Please consider creating a whitelist of addresses that can mint for free instead. + + + You may export a list of active Stargaze addresses using + + Snapshots + + diff --git a/pages/snapshots/chain.tsx b/pages/snapshots/chain.tsx new file mode 100644 index 0000000..a7030ab --- /dev/null +++ b/pages/snapshots/chain.tsx @@ -0,0 +1,98 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable tailwindcss/classnames-order */ + +import { Button } from 'components/Button' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { LinkTabs } from 'components/LinkTabs' +import { snapshotLinkTabs } from 'components/LinkTabs.data' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import { useState } from 'react' +import toast from 'react-hot-toast' +// import Brand from 'public/brand/brand.svg' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +export interface ChainDataType { + type: 'active-users' + endpoint: string +} + +const Chain: NextPage = () => { + const activeUsersEndpoint = `https://metabase.constellations.zone/public/question/cc17fce5-3cc4-4b03-b100-81bdf982f391.json` + const [chainDataType, setChainDataType] = useState({ + type: 'active-users', + endpoint: activeUsersEndpoint, + }) + const [isLoading, setIsLoading] = useState(false) + + const download = (content: string, fileName: string, contentType: string) => { + const a = document.createElement('a') + const file = new Blob([content], { type: contentType }) + a.href = URL.createObjectURL(file) + a.download = fileName + a.click() + } + + return ( + + + + + + Chain Snapshot Type + setChainDataType(JSON.parse(e.target.value))} + > + + Active Stargaze Users + + + + + { + setIsLoading(true) + fetch(chainDataType.endpoint) + .then((response) => response.json()) + .then((data) => { + if (data.length === 0) { + toast.error('Could not fetch snapshot data for the given collection address.', { + style: { maxWidth: 'none' }, + }) + setIsLoading(false) + return + } + if (chainDataType.type === 'active-users') { + const addresses = data.map((item: any) => item.address) + download(addresses.join('\n'), 'active-users.txt', 'text/plain') + } + setIsLoading(false) + }) + .catch((err) => { + setIsLoading(false) + toast.error(`Could not fetch chain data: ${err}`, { + style: { maxWidth: 'none' }, + }) + console.error('Could not fetch chain data: ', err) + }) + }} + > + {' '} + Export List + + + ) +} + +export default withMetadata(Chain, { center: false }) diff --git a/pages/snapshots/holders.tsx b/pages/snapshots/holders.tsx new file mode 100644 index 0000000..f011a16 --- /dev/null +++ b/pages/snapshots/holders.tsx @@ -0,0 +1,165 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable tailwindcss/classnames-order */ + +import { Button } from 'components/Button' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { AddressInput } from 'components/forms/FormInput' +import { useInputState } from 'components/forms/FormInput.hooks' +import { LinkTabs } from 'components/LinkTabs' +import { snapshotLinkTabs } from 'components/LinkTabs.data' +import { SelectCollection } from 'components/SelectCollection' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import { useEffect, useState } from 'react' +import toast from 'react-hot-toast' +// import Brand from 'public/brand/brand.svg' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const Holders: NextPage = () => { + const [collectionAddress, setCollectionAddress] = useState('') + const collectionAddressState = useInputState({ + id: 'collection-address', + name: 'collection-address', + title: 'Collection Address', + defaultValue: '', + }) + + const [includeStaked, setIncludeStaked] = useState(true) + const [includeListed, setIncludeListed] = useState(true) + const [exportIndividualTokens, setExportIndividualTokens] = useState(false) + const [isLoading, setIsLoading] = useState(false) + + const snapshotEndpoint = `https://metabase.constellations.zone/api/public/card/4cf9550e-5eb7-4fe7-bd3b-dc33229f53dc/query/json?parameters=%5B%7B%22type%22%3A%22category%22%2C%22value%22%3A%22${collectionAddressState.value}%22%2C%22id%22%3A%22cb34b7a8-70cf-ba86-8d9c-360b5b2fedd3%22%2C%22target%22%3A%5B%22variable%22%2C%5B%22template-tag%22%2C%22collection_addr%22%5D%5D%7D%5D` + + const download = (content: string, fileName: string, contentType: string) => { + const a = document.createElement('a') + const file = new Blob([content], { type: contentType }) + a.href = URL.createObjectURL(file) + a.download = fileName + a.click() + } + + useEffect(() => { + collectionAddressState.onChange(collectionAddress) + }, [collectionAddress]) + + return ( + + + + + + + + Snapshot Options + + + Include tokens listed on Marketplace + { + setIncludeListed(!includeListed) + }} + type="checkbox" + /> + + + Include tokens staked on DAOs + { + setIncludeStaked(!includeStaked) + }} + type="checkbox" + /> + + + Export by Token ID + { + setExportIndividualTokens(!exportIndividualTokens) + }} + type="checkbox" + /> + + + + + { + if (collectionAddressState.value.length === 0) { + toast.error('Please select a collection or enter a valid collection address.', { + style: { maxWidth: 'none' }, + }) + return + } + setIsLoading(true) + fetch(snapshotEndpoint) + .then((response) => response.json()) + .then((data) => { + if (data.length === 0) { + toast.error('Could not fetch snapshot data for the given collection address.', { + style: { maxWidth: 'none' }, + }) + return + } + if (exportIndividualTokens) { + const csv = `address,tokenId\n${data + ?.map((row: any) => { + if (!includeListed && row.is_listed) return '' + if (!includeStaked && row.is_staked) return '' + return `${row.owner_addr},${row.token_id}\n` + }) + .join('')}` + download(csv, 'snapshot.csv', 'text/csv') + setIsLoading(false) + return + } + const aggregatedData: any[] = [] + data.forEach((row: any) => { + if (!includeListed && row.is_listed) return + if (!includeStaked && row.is_staked) return + const existingRow = aggregatedData.find((r) => r.address === row.owner_addr) + if (existingRow) { + existingRow.amount += 1 + } else { + aggregatedData.push({ address: row.owner_addr, amount: 1 }) + } + }) + + aggregatedData.sort((a, b) => b.amount - a.amount) + const csv = `address,amount\n${aggregatedData.map((row: any) => Object.values(row).join(',')).join('\n')}` + download(csv, 'snapshot.csv', 'text/csv') + setIsLoading(false) + }) + .catch((err) => { + setIsLoading(false) + toast.error(`Could not fetch snapshot data: ${err}`, { + style: { maxWidth: 'none' }, + }) + console.error('Could not fetch snapshot data: ', err) + }) + }} + > + {' '} + Export Snapshot + + + ) +} + +export default withMetadata(Holders, { center: false }) diff --git a/pages/snapshots/index.tsx b/pages/snapshots/index.tsx index ffdccf6..0647478 100644 --- a/pages/snapshots/index.tsx +++ b/pages/snapshots/index.tsx @@ -1 +1 @@ -export { default } from './snapshot' +export { default } from './holders' diff --git a/pages/snapshots/snapshot.tsx b/pages/snapshots/snapshot.tsx deleted file mode 100644 index 63c8590..0000000 --- a/pages/snapshots/snapshot.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* eslint-disable eslint-comments/disable-enable-pair */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable tailwindcss/classnames-order */ -/* eslint-disable react/button-has-type */ - -import { ContractPageHeader } from 'components/ContractPageHeader' -import { AddressInput } from 'components/forms/FormInput' -import { useInputState } from 'components/forms/FormInput.hooks' -import { SelectCollection } from 'components/SelectCollection' -import type { NextPage } from 'next' -import { NextSeo } from 'next-seo' -import { useEffect, useState } from 'react' -import toast from 'react-hot-toast' -// import Brand from 'public/brand/brand.svg' -import { withMetadata } from 'utils/layout' -import { links } from 'utils/links' - -const Snapshots: NextPage = () => { - const [collectionAddress, setCollectionAddress] = useState('') - const collectionAddressState = useInputState({ - id: 'collection-address', - name: 'collection-address', - title: 'Collection Address', - defaultValue: '', - }) - - const snapshotEndpoint = `https://metabase.constellations.zone/api/public/card/b5764fb2-9a23-4ecf-866b-dec79c4c461e/query/json?parameters=%5B%7B%22type%22%3A%22category%22%2C%22value%22%3A%22${collectionAddressState.value}%22%2C%22id%22%3A%22cb34b7a8-70cf-ba86-8d9c-360b5b2fedd3%22%2C%22target%22%3A%5B%22variable%22%2C%5B%22template-tag%22%2C%22collection_addr%22%5D%5D%7D%5D` - // function to download .json from the endpoint - const download = (content: string, fileName: string, contentType: string) => { - const a = document.createElement('a') - const file = new Blob([content], { type: contentType }) - a.href = URL.createObjectURL(file) - a.download = fileName - a.click() - } - - useEffect(() => { - collectionAddressState.onChange(collectionAddress) - }, [collectionAddress]) - - return ( - - - - - - - { - fetch(snapshotEndpoint) - .then((response) => response.json()) - .then((data) => { - if (data.length === 0) { - toast.error('Could not fetch snapshot data for the given collection address.', { - style: { maxWidth: 'none' }, - }) - return - } - const csv = `address,amount\n${data?.map((row: any) => Object.values(row).join(',')).join('\n')}` - download(csv, 'snapshot.csv', 'text/csv') - }) - .catch((err) => { - toast.error(`Could not fetch snapshot data: ${err}`, { - style: { maxWidth: 'none' }, - }) - console.error('Could not fetch snapshot data: ', err) - }) - }} - > - {' '} - Export Snapshot - - - ) -} - -export default withMetadata(Snapshots, { center: false })