Merge pull request #169 from public-awesome/airdrop-specific-tokens
Feature: Airdrop specific tokens to multiple addresses
This commit is contained in:
commit
9b126c38ce
@ -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
|
||||
|
@ -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,
|
||||
})),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -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} />
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stargaze-studio",
|
||||
"version": "0.6.3",
|
||||
"version": "0.6.4",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
|
@ -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')
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user