+
+ {/* //TODO Fix the conditional */}
+
+
+ {status === WalletStatus.WALLET_CONNECTED ? terraAddress : 'Terra Station Wallet not connected'}
+
+
-
+
-
+
} onClick={addToken}>
diff --git a/pages/airdrops/create.tsx b/pages/airdrops/create.tsx
index 271d2668..c5923cf4 100644
--- a/pages/airdrops/create.tsx
+++ b/pages/airdrops/create.tsx
@@ -25,7 +25,7 @@ import { uploadObject } from 'services/s3'
import { CW20_MERKLE_DROP_CODE_ID, NETWORK } from 'utils/constants'
import { csvToArray } from 'utils/csvToArray'
import type { AccountProps } from 'utils/isValidAccountsFile'
-import { isValidAccountsFile } from 'utils/isValidAccountsFile'
+import { isTerraAccounts, isValidAccountsFile } from 'utils/isValidAccountsFile'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
@@ -43,7 +43,7 @@ const START_RADIO_VALUES = [
{
id: 'timestamp',
title: 'Timestamp',
- subtitle: 'Specific a calendar date and time of day.',
+ subtitle: 'Specify a calendar date and time of day.',
},
]
@@ -56,12 +56,12 @@ const END_RADIO_VALUES = [
{
id: 'height',
title: 'Block Height',
- subtitle: 'Choose a specific block height for this airdrop to begin.',
+ subtitle: 'Choose a specific block height for this airdrop to end.',
},
{
id: 'timestamp',
title: 'Timestamp',
- subtitle: 'Specific a calendar date and time of day.',
+ subtitle: 'Specify a calendar date and time of day.',
},
]
@@ -210,6 +210,9 @@ const CreateAirdropPage: NextPage = () => {
const totalAmount = getTotalAirdropAmount(fileContents)
+ // This will be used to differentiate between juno and terra addresses
+ const isTerraAirdrop = isTerraAccounts(fileContents)
+
const startData = (() => {
switch (startType) {
case 'height':
@@ -224,7 +227,7 @@ const CreateAirdropPage: NextPage = () => {
const expirationData = (() => {
switch (expirationType) {
case 'height':
- return Number(start)
+ return Number(expiration)
case 'timestamp':
return expirationDate ? Math.floor(expirationDate.getTime() / 1000) : null
default:
@@ -245,6 +248,7 @@ const CreateAirdropPage: NextPage = () => {
totalAmount,
contractAddress,
stage,
+ isTerraAirdrop,
}
toast('Uploading your airdrop file')
diff --git a/pages/airdrops/manage.tsx b/pages/airdrops/manage.tsx
index 083db3fd..ec907433 100644
--- a/pages/airdrops/manage.tsx
+++ b/pages/airdrops/manage.tsx
@@ -1 +1,358 @@
-export { default } from './create'
+import axios from 'axios'
+import { AirdropsStepper } from 'components/AirdropsStepper'
+import { AirdropStatus } from 'components/AirdropStatus'
+import { Alert } from 'components/Alert'
+import { Anchor } from 'components/Anchor'
+import { Button } from 'components/Button'
+import { Conditional } from 'components/Conditional'
+import { FormControl } from 'components/FormControl'
+import { Input } from 'components/Input'
+import { JsonPreview } from 'components/JsonPreview'
+import { Stats } from 'components/Stats'
+import { useContracts } from 'contexts/contracts'
+import { useWallet } from 'contexts/wallet'
+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 { FaAsterisk } from 'react-icons/fa'
+import type { AirdropProps } from 'utils/constants'
+import { convertDenomToReadable } from 'utils/convertDenomToReadable'
+import { useDebounce } from 'utils/debounce'
+import { withMetadata } from 'utils/layout'
+
+const ManageAirdropPage: NextPage = () => {
+ const router = useRouter()
+ const wallet = useWallet()
+ const contract = useContracts().cw20Base
+ const merkleAirdropContract = useContracts().cw20MerkleAirdrop
+ const client = wallet.getClient()
+
+ const [loading, setLoading] = useState(false)
+ const [airdrop, setAirdrop] = useState(null)
+ const [amount, setAmount] = useState('0')
+ const [contractAddress, setContractAddress] = useState(
+ typeof router.query.contractAddress === 'string' ? router.query.contractAddress : '',
+ )
+ const [balance, setBalance] = useState(null)
+ const [target, setTarget] = useState(null)
+ const [denom, setDenom] = useState(null)
+ const [recipientAddress, setRecipientAddress] = useState(null)
+ const [isExpired, setIsExpired] = useState(false)
+
+ const contractAddressDebounce = useDebounce(contractAddress, 500)
+
+ const burnMessage: any = airdrop ? merkleAirdropContract?.messages()?.burn(airdrop.contractAddress, 1) : null
+ const withdrawMessage: any = airdrop
+ ? merkleAirdropContract
+ ?.messages()
+ ?.withdraw(airdrop.contractAddress, 1, recipientAddress ? recipientAddress : wallet.address)
+ : null
+
+ const getBalances = () => {
+ if (contractAddress !== '') {
+ setBalance(null)
+ setTarget(null)
+ setDenom(null)
+ setAirdrop(null)
+ axios
+ .get(`${process.env.NEXT_PUBLIC_API_URL}/airdrops/status/${contractAddress}/balance`)
+ .then(({ data }) => {
+ const _balance = data.balance
+ const _target = data.target
+ const _denom = data.denom
+
+ const needed = _target - _balance
+
+ setBalance(_balance)
+ setTarget(_target)
+ setAmount(needed < 0 ? '0' : needed.toString())
+ setDenom(_denom)
+ })
+ .catch((err: any) => {
+ toast.error(err.message, {
+ style: { maxWidth: 'none' },
+ })
+ })
+ } else {
+ setBalance(null)
+ setTarget(null)
+ setDenom(null)
+ setAirdrop(null)
+ }
+ }
+
+ const getAirdrop = () => {
+ if (contractAddress !== '') {
+ axios
+ .get(`${process.env.NEXT_PUBLIC_API_URL}/airdrops/status/${contractAddress}`)
+ .then(({ data }) => {
+ setAirdrop(data.airdrop)
+ })
+ .catch((err: any) => {
+ toast.error(err.message, {
+ style: { maxWidth: 'none' },
+ })
+ })
+ } else setAirdrop(null)
+ }
+
+ const getAirdropAndBalances = () => {
+ getBalances()
+ getAirdrop()
+ }
+
+ useEffect(() => {
+ getAirdropAndBalances()
+ }, [contractAddressDebounce])
+
+ useEffect(() => {
+ if (router.query.contractAddress && typeof router.query.contractAddress === 'string')
+ setContractAddress(router.query.contractAddress)
+ }, [router.query])
+
+ useEffect(() => {
+ if (contractAddress === '') return
+ getCurrentBlockHeight()
+ .then((blockHeight) => isAirdropExpired(blockHeight))
+ .catch((err) => toast.error(err.message, { style: { maxWidth: 'none' } }))
+ }, [contractAddressDebounce, airdrop?.expiration])
+
+ const contractAddressOnChange = (value: string) => {
+ setContractAddress(value)
+ window.history.replaceState(null, '', `?contractAddress=${value}`)
+ }
+
+ const getCurrentBlockHeight = async () => {
+ const blockInfo = await client.getBlock()
+ return blockInfo.header.height || 0
+ }
+
+ const isAirdropExpired = (blockHeight: number) => {
+ if (airdrop?.expirationType === null) setIsExpired(false)
+ else if (airdrop?.expiration && airdrop.expirationType === 'timestamp')
+ setIsExpired(airdrop.expiration * 1000 < Date.now())
+ else if (airdrop?.expirationType === 'height' && blockHeight)
+ setIsExpired(airdrop.expiration ? airdrop.expiration < blockHeight : false)
+ }
+
+ const burn = async () => {
+ try {
+ if (!wallet.initialized) return toast.error('Please connect your wallet!')
+ if (!contract || !merkleAirdropContract) return toast.error('Could not connect to smart contract')
+ if (!airdrop) return
+ if (airdrop.processing) return toast.error('Airdrop is being processed.\n Check back later!')
+
+ const contractMessages = contract.use(airdrop.cw20TokenAddress)
+ const merkleAirdropContractMessages = merkleAirdropContract.use(airdrop.contractAddress)
+
+ if (!contractMessages || !merkleAirdropContractMessages || !burnMessage)
+ return toast.error('Could not connect to smart contract')
+
+ setLoading(true)
+ await merkleAirdropContractMessages.burn(burnMessage.msg.burn.stage)
+ setLoading(false)
+
+ toast.success('The remaining funds are burnt!', {
+ style: { maxWidth: 'none' },
+ })
+
+ getAirdropAndBalances()
+ } catch (err: any) {
+ setLoading(false)
+ toast.error(err.message, { style: { maxWidth: 'none' } })
+ }
+ }
+
+ const withdraw = async () => {
+ try {
+ if (!wallet.initialized) return toast.error('Please connect your wallet!')
+ if (!contract || !merkleAirdropContract) return toast.error('Could not connect to smart contract')
+ if (!airdrop) return
+ if (airdrop.processing) return toast.error('Airdrop is being processed.\n Check back later!')
+
+ const contractMessages = contract.use(airdrop.cw20TokenAddress)
+ const merkleAirdropContractMessages = merkleAirdropContract.use(airdrop.contractAddress)
+
+ if (!contractMessages || !merkleAirdropContractMessages || !withdrawMessage)
+ return toast.error('Could not connect to smart contract')
+
+ setLoading(true)
+ await merkleAirdropContractMessages.withdraw(
+ withdrawMessage.msg.withdraw.stage,
+ withdrawMessage.msg.withdraw.address,
+ )
+ setLoading(false)
+
+ toast.success('The remaining funds are withdrawn!', {
+ style: { maxWidth: 'none' },
+ })
+
+ getAirdropAndBalances()
+ } catch (err: any) {
+ setLoading(false)
+ toast.error(err.message, { style: { maxWidth: 'none' } })
+ }
+ }
+
+ return (
+
+
+
+
+
Manage Airdrop
+
+
+
+
Manage the funds for your airdrop
+
+
+
+
+ {airdrop?.escrow && (
+
+ Current airdrop is not eligible to be managed.
+
+ To continue,{' '}
+
+ click here to complete your escrow deposit at the airdrop escrow step
+
+ .
+
+
+ )}
+
+ {airdrop && !airdrop.escrow && airdrop.expirationType === null && (
+
+ The airdrop does not have an expiration time and will stay active until all the funds are claimed. Remaining
+ funds cannot be burnt or withdrawn.
+
+ )}
+
+ {airdrop && !airdrop.escrow && !isExpired && airdrop.expirationType !== null && (
+
+ The airdrop is not yet expired. Remaining funds cannot be burnt or withdrawn before the airdrop expires.
+
+ )}
+
+