Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { COW } from '@cowprotocol/common-const'
import { SupportedChainId } from '@cowprotocol/cow-sdk'

import { AirdropOption } from './types'

export const AIRDROP_OPTIONS = [
{
name: 'COW',
dataBaseUrl: 'https://raw.githubusercontent.com/bleu/cow-airdrop-contract-deployer/example/mock-airdrop-data/',
addressesMapping: {
[SupportedChainId.SEPOLIA]: '0xD1fB81659c434DDebC8468713E482134be0D85C0',
},
tokenMapping: {
[SupportedChainId.SEPOLIA]: {
...COW[SupportedChainId.SEPOLIA],
address: '0x5fe27bf718937ca1c4a7818d246cd4e755c7470c',
},
},

decimals: 18,
},
] as AirdropOption[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { HookDappInternal, HookDappType } from 'modules/hooksStore/types/hooks'

import { AirdropHookApp } from './index'

export const PRE_AIRDROP: HookDappInternal = {
name: 'TODO',
description: 'TODO',
descriptionShort: 'TODO',
type: HookDappType.INTERNAL,
image:
'https://static.vecteezy.com/system/resources/previews/017/317/302/original/an-icon-of-medical-airdrop-editable-and-easy-to-use-vector.jpg',
component: (props) => <AirdropHookApp {...props} />,
version: '0.1.0',
website: 'TODO',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Airdrop, AirdropAbi } from '@cowprotocol/abis'
import { useWalletInfo } from '@cowprotocol/wallet'

import useSWR from 'swr'

import { useContract } from 'common/hooks/useContract'

import { AirdropDataInfo, IClaimData, AirdropOption } from '../types'

type IntervalsType = { [key: string]: string }

type ChunkDataType = { [key: string]: AirdropDataInfo[] }

export interface PreviewClaimableTokensParams {
dataBaseUrl: string
address: string
}

export const AIRDROP_PREVIEW_ERRORS = {
NO_CLAIMABLE_TOKENS: "You don't have claimable tokens",
ERROR_FETCHING_DATA: 'There was an error trying to load claimable tokens',
NO_CLAIMABLE_AIRDROPS: 'You possibly have other items to claim, but not Airdrops',
UNEXPECTED_WRONG_FORMAT_DATA: 'Unexpected error fetching data: wrong format data',
}

/*
function to check if a name is inside a interval
intervals is in the format: {
"name1":"name2",
"name3":"name4",
...
}
name4 > name3 > name2 > name1

returns the interval key if the condition is checked, else undefined
*/
export function findIntervalKey(name: string, intervals: IntervalsType): string | undefined {
const keys = Object.keys(intervals)

if (keys.length === 0) {
return
}

for (let i = 0; i < keys.length; i++) {
const key = keys[i]

if (key <= name && intervals[key] >= name) {
return key
}

// Quit at once when verifying that name will not be in the intervals
// Imagine searching for "Albert" in a phone list, but you've finished the "A" section
if (key > name) {
return
}

if (i === keys.length - 1) {
return
}
}

return
}

const fetchIntervals = async (dataBaseUrl: string): Promise<IntervalsType> => {
const response = await fetch(dataBaseUrl + 'mapping.json')
const intervals = await response.json()
return intervals
}

const fetchChunk = async (dataBaseUrl: string, intervalKey: string): Promise<ChunkDataType> => {
const response = await fetch(`${dataBaseUrl}chunks/${intervalKey}.json`)
const chunkData = await response.json()
return chunkData
}

const fetchAddressIsEligible = async ({
dataBaseUrl,
address,
}: PreviewClaimableTokensParams): Promise<AirdropDataInfo | undefined> => {
const intervals = await fetchIntervals(dataBaseUrl)

const intervalKey = findIntervalKey(address, intervals)

// Interval key is undefined (user address is not in intervals)
if (!intervalKey) throw new Error(AIRDROP_PREVIEW_ERRORS.NO_CLAIMABLE_TOKENS)

const chunkData = await fetchChunk(dataBaseUrl, intervalKey)

const addressLowerCase = address.toLowerCase()

// The user address is not listed in chunk
if (!(addressLowerCase in chunkData)) throw new Error(AIRDROP_PREVIEW_ERRORS.NO_CLAIMABLE_TOKENS)

const airDropData = chunkData[addressLowerCase]
// The user has other kind of tokens, but not airdrops
if (airDropData.length < 1) throw new Error(AIRDROP_PREVIEW_ERRORS.NO_CLAIMABLE_AIRDROPS)

return airDropData[0]
}

export const useClaimData = (selectedAirdrop?: AirdropOption) => {
const { account, chainId } = useWalletInfo()
const airdropContract = useContract<Airdrop>(selectedAirdrop?.addressesMapping, AirdropAbi)

const fetchPreviewClaimableTokens = async ({
dataBaseUrl,
address,
}: PreviewClaimableTokensParams): Promise<IClaimData> => {
const isEligibleData = await fetchAddressIsEligible({ dataBaseUrl, address })
if (!isEligibleData || !airdropContract || !isEligibleData.index || !selectedAirdrop || !account)
throw new Error(AIRDROP_PREVIEW_ERRORS.ERROR_FETCHING_DATA)

const isClaimed = await airdropContract?.isClaimed(isEligibleData.index)

const callData = airdropContract.interface.encodeFunctionData('claim', [
isEligibleData.index, //index
account, //claimant
isEligibleData.amount, //claimableAmount
isEligibleData.proof, //merkleProof
])

return {
...isEligibleData,
isClaimed,
callData,
contract: airdropContract,
token: selectedAirdrop.tokenMapping[chainId],
}
}

return useSWR<IClaimData | undefined, Error>(
selectedAirdrop && account
? {
dataBaseUrl: selectedAirdrop.dataBaseUrl,
address: account.toLowerCase(),
}
: null,
fetchPreviewClaimableTokens,
{ errorRetryCount: 0 }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useCallback, useContext, useState } from 'react'

import { formatTokenAmount } from '@cowprotocol/common-utils'
import { HookDappInternal, HookDappType } from '@cowprotocol/types'
import { ButtonPrimary } from '@cowprotocol/ui'
import { Fraction } from '@uniswap/sdk-core'
import { CurrencyAmount } from '@uniswap/sdk-core'

import { useGasLimit } from 'modules/hooksStore/hooks/useGasLimitHooks'

import { AIRDROP_OPTIONS } from './constants'
import { useClaimData } from './hooks/useClaimData'
import { ContentWrapper } from './styled/ContentWrapper'
import { DropDownMenu } from './styled/DropDown'
import { Header } from './styled/Header'
import { Link } from './styled/Link'
import { Row } from './styled/Row'
import { Wrapper } from './styled/Wrapper'
import { AirdropOption, IClaimData } from './types'
import { HookDappProps } from 'modules/hooksStore/types/hooks'

const NAME = 'Airdrop'
const DESCRIPTION = 'Claim an aidrop before swapping!'
const IMAGE_URL =
'https://static.vecteezy.com/system/resources/previews/017/317/302/original/an-icon-of-medical-airdrop-editable-and-easy-to-use-vector.jpg'

export function AirdropHookApp({ context }: HookDappProps) {
const [selectedAirdrop, setSelectedAirdrop] = useState<AirdropOption>()
const { data: claimData, isValidating, error } = useClaimData(selectedAirdrop)
const gasLimit = useGasLimit(claimData?.contract.address, claimData?.callData)

const clickOnAddHook = useCallback(async () => {
if (!context || !claimData || !gasLimit) return

context.addHook({
hook: {
target: claimData.contract.address,
callData: claimData.callData,
gasLimit,
},
})
}, [context, claimData, gasLimit])

const canClaim = claimData?.amount && !claimData?.isClaimed

return (
<Wrapper>
<Header>
<img src={IMAGE_URL} alt={NAME} width="120" />
<p>{DESCRIPTION}</p>
</Header>
<ContentWrapper>
<Row>
<DropDownMenu airdropOptions={AIRDROP_OPTIONS} setSelectedAirdrop={setSelectedAirdrop} />
</Row>
{selectedAirdrop && <AirdropMessage claimData={claimData} error={error} isValidating={isValidating} />}
</ContentWrapper>
<ButtonPrimary disabled={!canClaim || isValidating} onClick={clickOnAddHook}>
+Add Pre-hook
</ButtonPrimary>
<Link
onClick={(e) => {
e.preventDefault()
hookDappContext?.close()
}}
>
Close
</Link>
</Wrapper>
)
}

function AirdropMessage({
claimData,
error,
isValidating,
}: {
claimData?: IClaimData
error?: Error
isValidating?: boolean
}) {
if (isValidating) {
return <Row>Loading...</Row>
}

if (error) {
return <Row>{error.message}</Row>
}

if (!claimData?.amount) {
return <Row>You are not eligible for this airdrop</Row>
}

const tokenAmount = formatTokenAmount(new Fraction(claimData.amount, 10 ** 18))
const message = claimData?.isClaimed
? `You have already claimed this airdrop`
: `You have ${tokenAmount} ${claimData?.token.symbol} to claim`
return <Row>{message}</Row>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import styled from 'styled-components/macro'

export const ContentWrapper = styled.div`
flex-grow: 1;
justify-content: center;
align-items: center;
flex-flow: column wrap;

display: flex;
justify-content: center;
align-items: center;

padding: 1em;
text-align: center;
`
Loading