From a13e0610e4f6c33eff6672b4efb04b0315750300 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Nov 2023 15:34:05 +0300 Subject: [PATCH 1/2] Add members query pagination & list export for whitelists --- contracts/whitelist/messages/query.ts | 6 +- pages/contracts/whitelist/query.tsx | 142 +++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/contracts/whitelist/messages/query.ts b/contracts/whitelist/messages/query.ts index a33ca61..5144c40 100644 --- a/contracts/whitelist/messages/query.ts +++ b/contracts/whitelist/messages/query.ts @@ -32,10 +32,12 @@ export interface DispatchQueryProps { messages: WhiteListInstance | undefined type: QueryType address: string + startAfter?: string + limit?: number } export const dispatchQuery = (props: DispatchQueryProps) => { - const { messages, type, address } = props + const { messages, type, address, startAfter, limit } = props switch (type) { case 'has_started': return messages?.hasStarted() @@ -44,7 +46,7 @@ export const dispatchQuery = (props: DispatchQueryProps) => { case 'is_active': return messages?.isActive() case 'members': - return messages?.members() + return messages?.members(startAfter, limit) case 'admin_list': return messages?.adminList() case 'has_member': diff --git a/pages/contracts/whitelist/query.tsx b/pages/contracts/whitelist/query.tsx index 3e847c3..8f9f3b7 100644 --- a/pages/contracts/whitelist/query.tsx +++ b/pages/contracts/whitelist/query.tsx @@ -1,9 +1,18 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ + +/* eslint-disable no-await-in-loop */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { toUtf8 } from '@cosmjs/encoding' import clsx from 'clsx' +import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' import { ContractPageHeader } from 'components/ContractPageHeader' import { FormControl } from 'components/FormControl' -import { AddressInput } from 'components/forms/FormInput' -import { useInputState } from 'components/forms/FormInput.hooks' +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 { whitelistLinkTabs } from 'components/LinkTabs.data' @@ -16,6 +25,7 @@ import { NextSeo } from 'next-seo' import { useEffect, useState } from 'react' import { toast } from 'react-hot-toast' import { useQuery } from 'react-query' +import { useDebounce } from 'utils/debounce' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' import { resolveAddress } from 'utils/resolveAddress' @@ -24,6 +34,7 @@ import { useWallet } from 'utils/wallet' const WhitelistQueryPage: NextPage = () => { const { whitelist: contract } = useContracts() const wallet = useWallet() + const [exporting, setExporting] = useState(false) const contractState = useInputState({ id: 'contract-address', @@ -41,20 +52,49 @@ const WhitelistQueryPage: NextPage = () => { }) const address = addressState.value + const limit = useNumberInputState({ + id: 'limit', + name: 'limit', + title: 'Limit', + subtitle: 'Maximum number of addresses to return', + defaultValue: 20, + }) + + const debouncedLimit = useDebounce(limit.value, 500) + + const startAfter = useInputState({ + id: 'start-after', + name: 'start-after', + title: 'Start After', + subtitle: 'Address to start after', + }) + + useEffect(() => { + if (debouncedLimit > 100) { + toast.success('Only 100 addresses can be returned at a time even if the limit is higher.', { + style: { maxWidth: 'none' }, + icon: '📝', + duration: 5000, + }) + } + }, [debouncedLimit]) + const [type, setType] = useState('config') const addressVisible = type === 'has_member' const { data: response } = useQuery( - [contractAddress, type, contract, wallet.address, address] as const, + [contractAddress, type, contract, wallet.address, address, startAfter.value, limit.value] as const, async ({ queryKey }) => { - const [_contractAddress, _type, _contract, _wallet, _address] = queryKey + const [_contractAddress, _type, _contract, _wallet, _address, _startAfter, _limit] = queryKey const messages = contract?.use(contractAddress) const res = await resolveAddress(_address, wallet).then(async (resolvedAddress) => { const result = await dispatchQuery({ messages, type, address: resolvedAddress, + startAfter: _startAfter || undefined, + limit: _limit, }) return result }) @@ -82,6 +122,88 @@ const WhitelistQueryPage: NextPage = () => { if (initial && initial.length > 0) contractState.onChange(initial) }, []) + const exportAllMembers = async () => { + if (wallet.isWalletDisconnected) { + toast.error('Please connect your wallet first.', { style: { maxWidth: 'none' } }) + setExporting(false) + return + } + try { + const messages = contract?.use(contractAddress) + + setExporting(true) + const contractInfoResponse = await (await wallet.getCosmWasmClient()) + .queryContractRaw( + contractAddress.trim(), + toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()), + ) + .catch((e) => { + if (e.message.includes('bech32')) throw new Error('Invalid whitelist contract address.') + console.log(e.message) + }) + const contractInfo = JSON.parse(new TextDecoder().decode(contractInfoResponse as Uint8Array)) + console.log('Contract Info: ', contractInfo.contract) + + if (contractInfo.contract.includes('flex')) { + let membersResponse = (await dispatchQuery({ messages, address, type: 'members', limit: 100 })) as any + let membersArray = [...membersResponse.members] + let lastMember = membersResponse.members[membersResponse.members.length - 1] + + while (membersResponse.members.length === 100) { + membersResponse = (await dispatchQuery({ + messages, + address, + type: 'members', + limit: 100, + startAfter: lastMember.address, + })) as any + lastMember = membersResponse.members[membersResponse.members.length - 1] + membersArray = [...membersArray, ...membersResponse.members] + } + + membersArray.unshift({ address: 'address', mint_count: 'mint_count' }) + const csv = membersArray.map((row) => Object.values(row).join(',')).join('\n') + const csvData = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) + const csvURL = window.URL.createObjectURL(csvData) + const tempLink = document.createElement('a') + tempLink.href = csvURL + tempLink.setAttribute('download', 'whitelist_flex_members.csv') + tempLink.click() + } else if (contractInfo.contract.includes('whitelist') && !contractInfo.contract.includes('flex')) { + let membersResponse = (await dispatchQuery({ messages, address, type: 'members', limit: 100 })) as any + let membersArray = [...membersResponse.members] + let lastMember = membersResponse.members[membersResponse.members.length - 1] + + while (membersResponse.members.length === 100) { + membersResponse = (await dispatchQuery({ + messages, + address, + type: 'members', + limit: 100, + startAfter: lastMember, + })) as any + lastMember = membersResponse.members[membersResponse.members.length - 1] + membersArray = [...membersArray, ...membersResponse.members] + } + + const txt = membersArray.map((member) => member).join('\n') + const txtData = new Blob([txt], { type: 'text/txt;charset=utf-8;' }) + const txtURL = window.URL.createObjectURL(txtData) + const tempLink = document.createElement('a') + tempLink.href = txtURL + tempLink.setAttribute('download', 'whitelist_members.txt') + tempLink.click() + } else { + toast.error('Invalid whitelist contract address.', { style: { maxWidth: 'none' } }) + } + setExporting(false) + } catch (e: any) { + console.log(e) + toast.error(e.message, { style: { maxWidth: 'none' } }) + setExporting(false) + } + } + return (
@@ -117,6 +239,18 @@ const WhitelistQueryPage: NextPage = () => { + + + + + From 2ef9e3ccb946532627eccd65e6e24d29b98283ec Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Nov 2023 15:34:56 +0300 Subject: [PATCH 2/2] Bump Studio version --- .env.example | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index b3e3279..00e05ce 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.8.0 +APP_VERSION=0.8.1 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=2595 diff --git a/package.json b/package.json index 7d21dd7..73b1f1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.8.0", + "version": "0.8.1", "workspaces": [ "packages/*" ],