Skip to content

Commit

Permalink
Sync development to testnet (#221)
Browse files Browse the repository at this point in the history
* Amount validity check update

* Airdrop fund and register subtitle fix (#219)

* Fix subtitles for airdrop register & fund

* Typo fix

* Native token airdrop support (#220)

* Add token selection UI to airdrop create page

* Fix native token logic on airdrop create

* Add new method for funding in merkle airdrop contract

* Update airdrop props

* Use correct funding method based on airdrop token

* Implement native token logic on claim page

* Fix client error on website load

* Update instantiate message with native token

* Fix balance error on website load

Co-authored-by: Serkan Reis <serkanreis@gmail.com>
  • Loading branch information
findolor and MightOfOaks authored Jun 24, 2022
1 parent 1c84b7b commit 7a54362
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 34 deletions.
50 changes: 50 additions & 0 deletions contracts/cw20/merkleAirdrop/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface CW20MerkleAirdropInstance {
stage: number,
) => Promise<ExecuteWithSignDataResponse>
depositEscrow: () => Promise<ExecuteWithSignDataResponse>
fundWithSend: (amount: string) => Promise<ExecuteWithSignDataResponse>
}

export interface CW20MerkleAirdropMessages {
Expand All @@ -77,6 +78,7 @@ export interface CW20MerkleAirdropMessages {
) => [RegisterMessage, ReleaseEscrowMessage]
depositEscrow: (airdropAddress: string) => DepositEscrowMessage
claim: (airdropAddress: string, stage: number, amount: string, proof: string[]) => ClaimMessage
fundWithSend: (recipient: string, amount: string) => FundWithSendMessage
}

export interface InstantiateMessage {
Expand Down Expand Up @@ -137,6 +139,12 @@ export interface ClaimMessage {
funds: Coin[]
}

export interface FundWithSendMessage {
from_address: string
to_address: string
amount: Coin[]
}

export interface CW20MerkleAirdropContract {
instantiate: (
senderAddress: string,
Expand Down Expand Up @@ -333,6 +341,38 @@ export const CW20MerkleAirdrop = (client: SigningCosmWasmClient, txSigner: strin
}
}

const fundWithSend = async (amount: string): Promise<ExecuteWithSignDataResponse> => {
const config = getNetworkConfig(NETWORK)
const signed = await client.sign(
txSigner,
[
{
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
value: {
fromAddress: txSigner,
toAddress: contractAddress,
amount: [coin(amount, config.feeToken)],
},
},
],
fee,
'',
)
const result = await client.broadcastTx(TxRaw.encode(signed).finish())
if (isDeliverTxFailure(result)) {
throw new Error(
[
`Error when broadcasting tx ${result.transactionHash} at height ${result.height}.`,
`Code: ${result.code}; Raw log: ${result.rawLog ?? ''}`,
].join(' '),
)
}
return {
signed,
txHash: result.transactionHash,
}
}

return {
contractAddress,
getConfig,
Expand All @@ -346,6 +386,7 @@ export const CW20MerkleAirdrop = (client: SigningCosmWasmClient, txSigner: strin
burn,
registerAndReleaseEscrow,
depositEscrow,
fundWithSend,
}
}

Expand Down Expand Up @@ -443,11 +484,20 @@ export const CW20MerkleAirdrop = (client: SigningCosmWasmClient, txSigner: strin
}
}

const fundWithSend = (recipient: string, amount: string): FundWithSendMessage => {
return {
from_address: txSigner,
to_address: recipient,
amount: [coin(amount, getNetworkConfig(NETWORK).feeToken)],
}
}

return {
instantiate,
registerAndReleaseEscrow,
depositEscrow,
claim,
fundWithSend,
}
}

Expand Down
4 changes: 3 additions & 1 deletion contracts/cw20/merkleAirdrop/useContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export function useCW20MerkleAirdropContract(): UseCW20MerkleAirdropContractProp
}, [])

useEffect(() => {
const cw20MerkleAirdropContract = initContract(wallet.getClient(), wallet.address)
const client = wallet.getClient()
if (!client) return
const cw20MerkleAirdropContract = initContract(client, wallet.address)
setCW20MerkleAirdrop(cw20MerkleAirdropContract)
}, [wallet])

Expand Down
23 changes: 18 additions & 5 deletions pages/airdrops/[address]/claim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ const ClaimAirdropPage: NextPage = () => {
const [name, setName] = useState('')
const [cw20TokenAddress, setCW20TokenAddress] = useState('')
const [balance, setBalance] = useState(0)
const [cw20TokenInfo, setCW20TokenInfo] = useState<TokenInfoResponse | null>(null)
const [cw20TokenInfo, setCW20TokenInfo] = useState<TokenInfoResponse | null>({
name: 'Juno Native Token',
decimals: 6,
symbol: getConfig(NETWORK).feeToken.slice(1).toUpperCase(),
total_supply: '',
})
const [stage, setStage] = useState(0)

const [airdropState, setAirdropState] = useState<ClaimState>('loading')
Expand Down Expand Up @@ -82,6 +87,10 @@ const ClaimAirdropPage: NextPage = () => {
void getAirdropInfo()
}, [contractAddress, wallet.address, wallet.initialized])

useEffect(() => {
setBalance(Number(wallet.balance[0]?.amount))
}, [wallet.balance])

useEffect(() => {
if (!cw20BaseContract || !cw20TokenAddress) return

Expand Down Expand Up @@ -190,7 +199,9 @@ const ClaimAirdropPage: NextPage = () => {
<StackedList.Item name="Airdrop Contract Address">{contractAddress}</StackedList.Item>
<StackedList.Item name="Token Name">{cw20TokenInfo?.name}</StackedList.Item>
<StackedList.Item name="Token Symbol">{cw20TokenInfo?.symbol}</StackedList.Item>
<StackedList.Item name="Token Address">{cw20TokenAddress}</StackedList.Item>
<Conditional test={Boolean(cw20TokenAddress)}>
<StackedList.Item name="Token Address">{cw20TokenAddress}</StackedList.Item>
</Conditional>
<StackedList.Item name="Claim Amount">
{convertDenomToReadable(amount)} {cw20TokenInfo?.symbol}
</StackedList.Item>
Expand All @@ -210,9 +221,11 @@ const ClaimAirdropPage: NextPage = () => {

<Conditional test={wallet.initialized && airdropState !== 'no_allocation'}>
<div className="flex justify-end pb-6 space-x-4">
<Button isWide leftIcon={<BiCoinStack />} onClick={addToken}>
Add Token to Keplr
</Button>
<Conditional test={Boolean(cw20TokenAddress)}>
<Button isWide leftIcon={<BiCoinStack />} onClick={addToken}>
Add Token to Keplr
</Button>
</Conditional>
<Button
className={clsx('px-8', {
'bg-green-500': airdropState === 'claimed',
Expand Down
74 changes: 58 additions & 16 deletions pages/airdrops/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Input } from 'components/Input'
import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview'
import { Radio } from 'components/Radio'
import { getConfig } from 'config'
import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next'
Expand All @@ -21,7 +22,7 @@ import { toast } from 'react-hot-toast'
import { FaAsterisk } from 'react-icons/fa'
import { IoCloseSharp } from 'react-icons/io5'
import { uploadObject } from 'services/s3'
import { CW20_MERKLE_DROP_CODE_ID } from 'utils/constants'
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'
Expand Down Expand Up @@ -64,8 +65,23 @@ const END_RADIO_VALUES = [
},
]

const TOKEN_VALUES = [
{
id: 'native',
title: 'Native Token',
subtitle: `This airdrop will use ${getConfig(NETWORK).feeToken.slice(1)} as the token.`,
},
{
id: 'cw20',
title: 'CW20 Token',
subtitle: 'This airdrop will use a custom cw20 token address.',
},
]

type StartEndValue = 'null' | 'height' | 'timestamp'

type TokenValue = 'native' | 'cw20'

const getTotalAirdropAmount = (accounts: AccountProps[]) => {
return accounts.reduce((acc: number, curr: AccountProps) => acc + parseInt(curr.amount), 0)
}
Expand All @@ -80,13 +96,14 @@ const CreateAirdropPage: NextPage = () => {
const [accountsFile, setAccountsFile] = useState<File | null>(null)
const [fileContents, setFileContents] = useState<any>(null)
const [projectName, setProjectName] = useState('')
const [cw20TokenAddress, setCW20TokenAddress] = useState('')
const [cw20TokenAddress, setCW20TokenAddress] = useState<string | null>('')
const [start, setStart] = useState('')
const [startDate, setStartDate] = useState<Date | null>(null)
const [startType, setStartType] = useState<StartEndValue>('null')
const [expiration, setExpiration] = useState('')
const [expirationDate, setExpirationDate] = useState<Date | null>(null)
const [expirationType, setExpirationType] = useState<StartEndValue>('null')
const [tokenType, setTokenType] = useState<TokenValue>('cw20')

const inputFile = useRef<HTMLInputElement>(null)

Expand All @@ -95,6 +112,7 @@ const CreateAirdropPage: NextPage = () => {
?.instantiate(CW20_MERKLE_DROP_CODE_ID, `${projectName} Airdrop`, {
owner: wallet.address,
cw20_token_address: cw20TokenAddress,
native_token: tokenType === 'native' ? getConfig(NETWORK).feeToken : null,
})

const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -157,7 +175,7 @@ const CreateAirdropPage: NextPage = () => {
toast.error('Please enter a project name')
return false
}
if (cw20TokenAddress.trim() === '') {
if (tokenType === 'cw20' && cw20TokenAddress?.trim() === '') {
toast.error('Please enter a cw20 token address')
return false
}
Expand All @@ -183,8 +201,10 @@ const CreateAirdropPage: NextPage = () => {

setLoading(true)

toast('Validating your cw20 token address')
await isCW20TokenValid(cw20TokenAddress)
if (tokenType === 'cw20') {
toast('Validating your cw20 token address')
await isCW20TokenValid(cw20TokenAddress as string)
}

const contractAddress = await instantiate()

Expand Down Expand Up @@ -281,7 +301,13 @@ const CreateAirdropPage: NextPage = () => {
setExpirationDate(null)
}

const isValidToCreate = projectName !== '' && accountsFile !== null && cw20TokenAddress !== ''
const tokenTypeOnChange = (value: string) => {
setTokenType(value as TokenValue)
setCW20TokenAddress(value === 'native' ? null : '')
}

const isValidToCreate =
projectName !== '' && accountsFile !== null && tokenType === 'native' ? true : cw20TokenAddress !== ''

return (
<div className="relative py-6 px-12 space-y-8">
Expand Down Expand Up @@ -323,17 +349,33 @@ const CreateAirdropPage: NextPage = () => {
{/* CW20 token address */}
<FormControl
htmlId="airdrop-cw20"
subtitle=" Address of the CW20 token that will be airdropped."
title="CW20 Address"
subtitle="Type and address of the airdropped token."
title="Airdropped Token"
>
<Input
id="airdrop-cw20"
name="cw20"
onChange={(e) => setCW20TokenAddress(e.target.value)}
placeholder="juno1234567890abcdefghijklmnopqrstuvwxyz..."
type="text"
value={cw20TokenAddress}
/>
<fieldset className="p-4 space-y-4 rounded border-2 border-white/25">
{TOKEN_VALUES.map(({ id, title, subtitle }) => (
<Radio
key={`token-${id}`}
checked={tokenType === id}
htmlFor="token"
id={id}
onChange={() => tokenTypeOnChange(id)}
subtitle={subtitle}
title={title}
>
{tokenType === 'cw20' && (
<Input
id="airdrop-cw20"
name="cw20"
onChange={(e) => setCW20TokenAddress(e.target.value)}
placeholder="juno1234567890abcdefghijklmnopqrstuvwxyz..."
type="text"
value={cw20TokenAddress as string}
/>
)}
</Radio>
))}
</fieldset>
</FormControl>

{/* start type */}
Expand Down
25 changes: 16 additions & 9 deletions pages/airdrops/fund.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const FundAirdropPage: NextPage = () => {
const router = useRouter()
const wallet = useWallet()
const contract = useContracts().cw20Base
const merkleAirdropContract = useContracts().cw20MerkleAirdrop

const [loading, setLoading] = useState(false)
const [airdrop, setAirdrop] = useState<AirdropProps | null>(null)
Expand All @@ -54,7 +55,9 @@ const FundAirdropPage: NextPage = () => {

const contractAddressDebounce = useDebounce(contractAddress, 500)

const transactionMessage = contract?.messages()?.mint(airdrop?.cw20TokenAddress || '', contractAddress, amount)
const transactionMessage: any = airdrop?.isNative
? merkleAirdropContract?.messages()?.fundWithSend(airdrop.contractAddress, amount)
: contract?.messages()?.mint(airdrop?.cw20TokenAddress || '', contractAddress, amount)

useEffect(() => {
if (contractAddress !== '') {
Expand Down Expand Up @@ -112,20 +115,24 @@ const FundAirdropPage: NextPage = () => {
const fund = async (executeType: string) => {
try {
if (!wallet.initialized) return toast.error('Please connect your wallet!')
if (!contract) return toast.error('Could not connect to smart contract')
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 || !transactionMessage) return toast.error('Could not connect to smart contract')
if (!contractMessages || !merkleAirdropContractMessages || !transactionMessage)
return toast.error('Could not connect to smart contract')

setLoading(true)

const result = await contractMessages.mint(
transactionMessage.msg.mint.recipient,
transactionMessage.msg.mint.amount,
)
let result

if (airdrop.isNative)
result = await merkleAirdropContractMessages.fundWithSend(transactionMessage.amount[0].amount)
else
result = await contractMessages.mint(transactionMessage.msg.mint.recipient, transactionMessage.msg.mint.amount)

setLoading(false)
toast.success('Airdrop funded!', {
Expand Down Expand Up @@ -168,7 +175,7 @@ const FundAirdropPage: NextPage = () => {
<div className="space-y-8">
<FormControl
htmlId="airdrop-cw20"
subtitle="Address of the CW20 token that will be funded"
subtitle="Address of the airdrop contract that will be funded."
title="Airdrop contract address"
>
<Input
Expand Down Expand Up @@ -235,7 +242,7 @@ const FundAirdropPage: NextPage = () => {
</FormControl>
)}

<Conditional test={Boolean(airdrop && !airdrop.escrow && !airdrop.processing && denom)}>
<Conditional test={Boolean(airdrop && !airdrop.escrow && !airdrop.processing && !airdrop.isNative && denom)}>
<FormControl subtitle="Please select which method you would like to use" title="Airdrop fund method">
<fieldset className="p-4 space-y-4 rounded border-2 border-white/25">
{FUND_RADIO_VALUES.map(({ id, title, subtitle }) => (
Expand Down
2 changes: 1 addition & 1 deletion pages/airdrops/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ const RegisterAirdropPage: NextPage = () => {
<div className="space-y-8">
<FormControl
htmlId="airdrop-cw20"
subtitle="Address of the CW20 token that will be airdropped."
subtitle="Address of the airdrop contract that will be registered."
title="Airdrop contract address"
>
<Input
Expand Down
1 change: 1 addition & 0 deletions utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ export interface AirdropProps {
escrowStatus?: string
status?: string
accountsSize?: number
isNative?: boolean
}
Loading

0 comments on commit 7a54362

Please sign in to comment.