Merge pull request #169 from public-awesome/airdrop-specific-tokens

Feature: Airdrop specific tokens to multiple addresses
This commit is contained in:
Adnan Deniz corlu 2023-06-08 12:10:53 +03:00 committed by GitHub
commit 9b126c38ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 98 additions and 10 deletions

View File

@ -1,4 +1,4 @@
APP_VERSION=0.6.3
APP_VERSION=0.6.4
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_SG721_CODE_ID=2092

View File

@ -38,7 +38,7 @@ export const AirdropUpload = ({ onChange }: AirdropUploadProps) => {
.then((res) => {
const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri
if (tokenUri && isValidAddress(tokenUri))
resolvedAllocationData.push({ address: tokenUri, amount: data.amount })
resolvedAllocationData.push({ address: tokenUri, amount: data.amount, tokenId: data.tokenId })
else toast.error(`Resolved address is empty or invalid for the name: ${data.address}`)
})
.catch((e) => {
@ -80,8 +80,15 @@ export const AirdropUpload = ({ onChange }: AirdropUploadProps) => {
.map((data) => ({
address: data.address.trim(),
amount: data.amount,
tokenId: data.tokenId,
}))
.concat(resolvedAllocationData),
.concat(
resolvedAllocationData.map((data) => ({
address: data.address,
amount: data.amount,
tokenId: data.tokenId,
})),
),
)
},
)

View File

@ -185,7 +185,7 @@ export const CollectionActions = ({
'batch_transfer',
'batch_mint_for',
])
const showAirdropFileField = type === 'airdrop'
const showAirdropFileField = isEitherType(type, ['airdrop', 'airdrop_specific'])
const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
const showDescriptionField = type === 'update_collection_info'
const showImageField = type === 'update_collection_info'
@ -211,6 +211,7 @@ export const CollectionActions = ({
sg721Messages,
recipient: resolvedRecipientAddress,
recipients: airdropArray,
tokenRecipients: airdropAllocationArray,
txSigner: wallet.address,
type,
price: priceState.value.toString(),
@ -477,7 +478,11 @@ export const CollectionActions = ({
)}
{showAirdropFileField && (
<FormGroup
subtitle="CSV file that contains the airdrop addresses and the amount of tokens allocated for each address. Should start with the following header row: address,amount"
subtitle={`CSV file that contains the airdrop addresses and the ${
type === 'airdrop' ? 'amount of tokens' : 'token ID'
} allocated for each address. Should start with the following header row: ${
type === 'airdrop' ? 'address,amount' : 'address,tokenId'
}`}
title="Airdrop File"
>
<AirdropUpload onChange={airdropFileOnChange} />

View File

@ -5,6 +5,7 @@ import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
import { useSG721Contract } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { useVendingMinterContract } from 'contracts/vendingMinter'
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
import type { BaseMinterInstance } from '../../../contracts/baseMinter/contract'
@ -31,6 +32,7 @@ export const ACTION_TYPES = [
'batch_mint_for',
'shuffle',
'airdrop',
'airdrop_specific',
'burn_remaining',
'update_token_metadata',
'batch_update_token_metadata',
@ -183,6 +185,11 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
name: 'Airdrop Tokens',
description: 'Airdrop tokens to given addresses',
},
{
id: 'airdrop_specific',
name: 'Airdrop Specific Tokens',
description: 'Airdrop specific tokens to given addresses',
},
{
id: 'burn_remaining',
name: 'Burn Remaining Tokens',
@ -237,6 +244,7 @@ export interface DispatchExecuteArgs {
limit: number
tokenIds: string
recipients: string[]
tokenRecipients: AirdropAllocation[]
collectionInfo: CollectionInfo | undefined
baseUri: string
}
@ -319,6 +327,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'airdrop': {
return vendingMinterMessages.airdrop(txSigner, args.recipients)
}
case 'airdrop_specific': {
return vendingMinterMessages.airdropSpecificTokens(txSigner, args.tokenRecipients)
}
case 'burn_remaining': {
return vendingMinterMessages.burnRemaining(txSigner)
}
@ -409,6 +420,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'airdrop': {
return vendingMinterMessages(minterContract)?.airdrop(args.recipients)
}
case 'airdrop_specific': {
return vendingMinterMessages(minterContract)?.airdropSpecificTokens(args.tokenRecipients)
}
case 'burn_remaining': {
return vendingMinterMessages(minterContract)?.burnRemaining()
}

View File

@ -5,6 +5,7 @@ import { coin } from '@cosmjs/proto-signing'
import type { logs } from '@cosmjs/stargate'
import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types'
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
export interface InstantiateResponse {
readonly contractAddress: string
@ -47,6 +48,7 @@ export interface VendingMinterInstance {
shuffle: (senderAddress: string) => Promise<string>
withdraw: (senderAddress: string) => Promise<string>
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
airdropSpecificTokens: (senderAddress: string, tokenRecipients: AirdropAllocation[]) => Promise<string>
burnRemaining: (senderAddress: string) => Promise<string>
updateDiscountPrice: (senderAddress: string, price: string) => Promise<string>
removeDiscountPrice: (senderAddress: string) => Promise<string>
@ -67,6 +69,7 @@ export interface VendingMinterMessages {
shuffle: () => ShuffleMessage
withdraw: () => WithdrawMessage
airdrop: (recipients: string[]) => CustomMessage
airdropSpecificTokens: (recipients: AirdropAllocation[]) => CustomMessage
burnRemaining: () => BurnRemainingMessage
updateDiscountPrice: (price: string) => UpdateDiscountPriceMessage
removeDiscountPrice: () => RemoveDiscountPriceMessage
@ -553,6 +556,29 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
return res.transactionHash
}
const airdropSpecificTokens = async (senderAddress: string, recipients: AirdropAllocation[]): Promise<string> => {
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
for (let i = 0; i < recipients.length; i++) {
const msg = {
mint_for: { recipient: recipients[i].address, token_id: Number(recipients[i].tokenId) },
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: senderAddress,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
}),
}
executeContractMsgs.push(executeContractMsg)
}
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop_specific_tokens')
return res.transactionHash
}
const shuffle = async (senderAddress: string): Promise<string> => {
const res = await client.execute(
senderAddress,
@ -617,6 +643,7 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
batchMintFor,
batchMint,
airdrop,
airdropSpecificTokens,
shuffle,
withdraw,
burnRemaining,
@ -838,6 +865,19 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
}
}
const airdropSpecificTokens = (recipients: AirdropAllocation[]): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < recipients.length; i++) {
msg.push({ mint_for: { recipient: recipients[i].address, token_id: recipients[i].tokenId } })
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
const shuffle = (): ShuffleMessage => {
return {
sender: txSigner,
@ -886,6 +926,7 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
batchMintFor,
batchMint,
airdrop,
airdropSpecificTokens,
shuffle,
withdraw,
burnRemaining,

View File

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

View File

@ -5,11 +5,11 @@ export const csvToArray = (str: string, delimiter = ',') => {
if (str.includes('\r')) newline = '\r'
if (str.includes('\r\n')) newline = '\r\n'
const headers = str.slice(0, str.indexOf(newline)).split(delimiter)
const headers = str.trim().slice(0, str.indexOf(newline)).split(delimiter)
if (headers.length !== 2) {
throw new Error('Invalid accounts file')
}
if (headers[0] !== 'address' || headers[1] !== 'amount') {
if (headers[0] !== 'address' || (headers[1] !== 'amount' && headers[1] !== 'tokenId')) {
throw new Error('Invalid accounts file')
}

View File

@ -4,7 +4,8 @@ import { isValidAddress } from './isValidAddress'
export interface AirdropAllocation {
address: string
amount: string
amount?: string
tokenId?: string
}
export const isValidAccountsFile = (file: AirdropAllocation[]) => {
@ -28,10 +29,19 @@ export const isValidAccountsFile = (file: AirdropAllocation[]) => {
if (!account.address.trim().startsWith('stars') && !account.address.trim().endsWith('.stars')) {
return { address: false }
}
if (!account.amount && !account.tokenId) {
return { amount: false, tokenId: false }
}
// Check if amount is valid
if (!Number.isInteger(Number(account.amount)) || !(Number(account.amount) > 0)) {
if (account.amount && (!Number.isInteger(Number(account.amount)) || !(Number(account.amount) > 0))) {
return { amount: false }
}
// Check if tokenId is valid
if (account.tokenId && (!Number.isInteger(Number(account.tokenId)) || !(Number(account.tokenId) > 0))) {
return { tokenId: false }
}
return null
})
@ -47,11 +57,22 @@ export const isValidAccountsFile = (file: AirdropAllocation[]) => {
toast.error('Invalid address in file')
return false
}
if (checks.filter((check) => check?.amount === false && check.tokenId === false).length > 0) {
toast.error('No amount or token ID found in the file. Please check the header.')
return false
}
if (checks.filter((check) => check?.amount === false).length > 0) {
toast.error('Invalid amount in file. Amount must be a positive integer.')
return false
}
if (checks.filter((check) => check?.tokenId === false).length > 0) {
toast.error('Invalid token ID in file. Token ID must be a positive integer.')
return false
}
// if (duplicateCheck.length > 0) {
// toast.error('The file contains duplicate addresses.')
// return false