Changeset View
Changeset View
Standalone View
Standalone View
cashtab/src/components/Airdrop/Airdrop.js
// Copyright (c) 2024 The Bitcoin developers | // Copyright (c) 2024 The Bitcoin developers | ||||
// Distributed under the MIT software license, see the accompanying | // Distributed under the MIT software license, see the accompanying | ||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php. | // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
import React, { useState, useEffect } from 'react'; | import React, { useState, useEffect } from 'react'; | ||||
import { useLocation } from 'react-router-dom'; | import { useLocation } from 'react-router-dom'; | ||||
import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||
import { BN } from 'slp-mdm'; | import { BN } from 'slp-mdm'; | ||||
import styled from 'styled-components'; | import styled from 'styled-components'; | ||||
import { WalletContext } from 'wallet/context'; | import { WalletContext } from 'wallet/context'; | ||||
import { | import PrimaryButton, { SecondaryLink } from 'components/Common/PrimaryButton'; | ||||
AntdFormWrapper, | |||||
DestinationAddressMulti, | |||||
InputAmountSingle, | |||||
} from 'components/Common/EnhancedInputs'; | |||||
import { Form, Input } from 'antd'; | |||||
import { Switch } from 'antd'; | |||||
import PrimaryButton from 'components/Common/PrimaryButton'; | |||||
import CopyToClipboard from 'components/Common/CopyToClipboard'; | import CopyToClipboard from 'components/Common/CopyToClipboard'; | ||||
import { getMintAddress } from 'chronik'; | import { getMintAddress } from 'chronik'; | ||||
import { | import { | ||||
isValidTokenId, | isValidTokenId, | ||||
isValidXecAirdrop, | isValidXecAirdrop, | ||||
isValidAirdropExclusionArray, | isValidAirdropExclusionArray, | ||||
} from 'validation'; | } from 'validation'; | ||||
import { SidePaddingCtn } from 'components/Common/Atoms'; | import { SidePaddingCtn } from 'components/Common/Atoms'; | ||||
import { Link } from 'react-router-dom'; | |||||
import { getAirdropTx, getEqualAirdropTx } from 'airdrop'; | import { getAirdropTx, getEqualAirdropTx } from 'airdrop'; | ||||
import Communist from 'assets/communist.png'; | import Communist from 'assets/communist.png'; | ||||
import { toast } from 'react-toastify'; | import { toast } from 'react-toastify'; | ||||
import CashtabSwitch from 'components/Common/Switch'; | |||||
import { Input, TextArea, InputFlex } from 'components/Common/Inputs'; | |||||
import { ThemedCopySolid } from 'components/Common/CustomIcons'; | |||||
const { TextArea } = Input; | const AirdropForm = styled.div` | ||||
const Gulag = styled.img` | display: flex; | ||||
width: 20px; | flex-direction: column; | ||||
height: 20px; | gap: 12px; | ||||
`; | `; | ||||
const SwitchContentHolder = styled.div` | const FormRow = styled.div` | ||||
display: flex; | display: flex; | ||||
align-items: center; | flex-direction: column; | ||||
gap: 12px; | |||||
color: ${props => props.theme.contrast}; | |||||
`; | `; | ||||
const AirdropActions = styled.div` | const SwitchHolder = styled.div` | ||||
text-align: center; | |||||
width: 100%; | |||||
padding: 10px; | |||||
border-radius: 5px; | |||||
display: flex; | display: flex; | ||||
align-content: center; | |||||
gap: 12px; | |||||
`; | |||||
const AirdropTitle = styled.div` | |||||
display: flex; | |||||
align-items: flex-start; | |||||
gap: 12px; | |||||
text-align: center; | |||||
justify-content: center; | justify-content: center; | ||||
a { | |||||
color: ${props => props.theme.contrast}; | |||||
margin: 0; | |||||
font-size: 11px; | |||||
border: 1px solid ${props => props.theme.contrast}; | |||||
border-radius: 5px; | |||||
padding: 2px 10px; | |||||
opacity: 0.6; | |||||
} | |||||
a:hover { | |||||
opacity: 1; | |||||
border-color: ${props => props.theme.eCashBlue}; | |||||
color: ${props => props.theme.contrast}; | |||||
background: ${props => props.theme.eCashBlue}; | |||||
} | |||||
${({ received, ...props }) => | |||||
received && | |||||
` | |||||
text-align: left; | |||||
background: ${props.theme.receivedMessage}; | |||||
`} | |||||
`; | `; | ||||
const SwitchLabel = styled.div` | |||||
const AirdropOptions = styled.div` | |||||
text-align: left; | |||||
color: ${props => props.theme.contrast}; | color: ${props => props.theme.contrast}; | ||||
font-size: 18px; | |||||
`; | `; | ||||
const Airdrop = ({ passLoadingStatus }) => { | const Airdrop = ({ passLoadingStatus }) => { | ||||
const ContextValue = React.useContext(WalletContext); | const ContextValue = React.useContext(WalletContext); | ||||
const { chronik, cashtabState } = ContextValue; | const { chronik, cashtabState } = ContextValue; | ||||
const { wallets, cashtabCache } = cashtabState; | const { wallets, cashtabCache } = cashtabState; | ||||
const wallet = wallets.length > 0 ? wallets[0] : false; | const wallet = wallets.length > 0 ? wallets[0] : false; | ||||
const location = useLocation(); | const location = useLocation(); | ||||
Show All 23 Lines | const Airdrop = ({ passLoadingStatus }) => { | ||||
const [showAirdropOutputs, setShowAirdropOutputs] = useState(false); | const [showAirdropOutputs, setShowAirdropOutputs] = useState(false); | ||||
const [ignoreOwnAddress, setIgnoreOwnAddress] = useState(false); | const [ignoreOwnAddress, setIgnoreOwnAddress] = useState(false); | ||||
const [ignoreMintAddress, setIgnoreMintAddress] = useState(false); | const [ignoreMintAddress, setIgnoreMintAddress] = useState(false); | ||||
// flag to reflect the exclusion list checkbox | // flag to reflect the exclusion list checkbox | ||||
const [ignoreCustomAddresses, setIgnoreCustomAddresses] = useState(false); | const [ignoreCustomAddresses, setIgnoreCustomAddresses] = useState(false); | ||||
// the exclusion list values | // the exclusion list values | ||||
const [ignoreCustomAddressesList, setIgnoreCustomAddressesList] = | const [ignoreCustomAddressesList, setIgnoreCustomAddressesList] = | ||||
useState(false); | useState(''); | ||||
const [ | const [ | ||||
ignoreCustomAddressesListIsValid, | ignoreCustomAddressesListIsValid, | ||||
setIgnoreCustomAddressesListIsValid, | setIgnoreCustomAddressesListIsValid, | ||||
] = useState(false); | ] = useState(false); | ||||
const [ignoreCustomAddressListError, setIgnoreCustomAddressListError] = | const [ignoreCustomAddressListError, setIgnoreCustomAddressListError] = | ||||
useState(false); | useState(false); | ||||
// flag to reflect the ignore minimum etoken balance switch | // flag to reflect the ignore minimum etoken balance switch | ||||
▲ Show 20 Lines • Show All 51 Lines • ▼ Show 20 Lines | const calculateXecAirdrop = async () => { | ||||
setShowAirdropOutputs(false); | setShowAirdropOutputs(false); | ||||
let tokenUtxos; | let tokenUtxos; | ||||
try { | try { | ||||
tokenUtxos = await chronik.tokenId(formData.tokenId).utxos(); | tokenUtxos = await chronik.tokenId(formData.tokenId).utxos(); | ||||
} catch (err) { | } catch (err) { | ||||
console.log(`Error getting token utxos from chronik`, err); | console.log(`Error getting token utxos from chronik`, err); | ||||
toast.error('Error retrieving airdrop recipients'); | toast.error('Error retrieving airdrop recipients'); | ||||
// Clear result field from earlier calc, if present, on any error | |||||
setAirdropRecipients(''); | |||||
return passLoadingStatus(false); | return passLoadingStatus(false); | ||||
} | } | ||||
const excludedAddresses = []; | const excludedAddresses = []; | ||||
if (ignoreOwnAddress) { | if (ignoreOwnAddress) { | ||||
excludedAddresses.push(wallet.paths.get(1899).address); | excludedAddresses.push(wallet.paths.get(1899).address); | ||||
} | } | ||||
if (ignoreMintAddress) { | if (ignoreMintAddress) { | ||||
let mintAddress; | let mintAddress; | ||||
try { | try { | ||||
mintAddress = await getMintAddress(chronik, formData.tokenId); | mintAddress = await getMintAddress(chronik, formData.tokenId); | ||||
excludedAddresses.push(mintAddress); | excludedAddresses.push(mintAddress); | ||||
} catch (err) { | } catch (err) { | ||||
console.log(`Error getting mint address from chronik`, err); | console.log(`Error getting mint address from chronik`, err); | ||||
toast.error( | toast.error( | ||||
`Error determining mint address for ${formData.tokenId}`, | `Error determining mint address for ${formData.tokenId}`, | ||||
); | ); | ||||
// Clear result field from earlier calc, if present, on any error | |||||
setAirdropRecipients(''); | |||||
return passLoadingStatus(false); | return passLoadingStatus(false); | ||||
} | } | ||||
} | } | ||||
if (ignoreCustomAddresses && ignoreCustomAddressesListIsValid) { | if (ignoreCustomAddresses && ignoreCustomAddressesListIsValid) { | ||||
const addressStringArray = ignoreCustomAddressesList.split(','); | const addressStringArray = ignoreCustomAddressesList.split(','); | ||||
for (const address of addressStringArray) { | for (const address of addressStringArray) { | ||||
excludedAddresses.push(address); | excludedAddresses.push(address); | ||||
} | } | ||||
Show All 14 Lines | const calculateXecAirdrop = async () => { | ||||
) | ) | ||||
.times(10 ** tokenInfo.genesisInfo.decimals) | .times(10 ** tokenInfo.genesisInfo.decimals) | ||||
.toString(); | .toString(); | ||||
} catch (err) { | } catch (err) { | ||||
console.log(`Error getting token utxos from chronik`, err); | console.log(`Error getting token utxos from chronik`, err); | ||||
toast.error( | toast.error( | ||||
`Error determining mint address for ${formData.tokenId}`, | `Error determining mint address for ${formData.tokenId}`, | ||||
); | ); | ||||
// Clear result field from earlier calc, if present, on any error | |||||
setAirdropRecipients(''); | |||||
return passLoadingStatus(false); | return passLoadingStatus(false); | ||||
} | } | ||||
} else { | } else { | ||||
// get it from cache if available | // get it from cache if available | ||||
undecimalizedMinTokenAmount = new BN(ignoreMinEtokenBalanceAmount) | undecimalizedMinTokenAmount = new BN(ignoreMinEtokenBalanceAmount) | ||||
.times(10 ** tokenInfo.decimals) | .times(10 ** tokenInfo.decimals) | ||||
.toString(); | .toString(); | ||||
} | } | ||||
Show All 14 Lines | const calculateXecAirdrop = async () => { | ||||
excludedAddresses, | excludedAddresses, | ||||
formData.totalAirdrop, | formData.totalAirdrop, | ||||
undecimalizedMinTokenAmount, | undecimalizedMinTokenAmount, | ||||
); | ); | ||||
setAirdropRecipients(csv); | setAirdropRecipients(csv); | ||||
// display the airdrop outputs TextArea | // display the airdrop outputs TextArea | ||||
setShowAirdropOutputs(true); | setShowAirdropOutputs(true); | ||||
} catch (err) { | } catch (err) { | ||||
// Clear result field from earlier calc, if present, on any error | |||||
setAirdropRecipients(''); | |||||
toast.error(`${err}`); | toast.error(`${err}`); | ||||
} | } | ||||
return passLoadingStatus(false); | return passLoadingStatus(false); | ||||
}; | }; | ||||
const handleIgnoreMinEtokenBalanceAmt = e => { | const handleIgnoreMinEtokenBalanceAmt = e => { | ||||
setIgnoreMinEtokenBalance(e); | setIgnoreMinEtokenBalance(e); | ||||
// Also reset the balance amount | // Also reset the balance amount | ||||
Show All 15 Lines | const Airdrop = ({ passLoadingStatus }) => { | ||||
const handleIgnoreCustomAddressesList = e => { | const handleIgnoreCustomAddressesList = e => { | ||||
// if the checkbox is not checked then skip the input validation | // if the checkbox is not checked then skip the input validation | ||||
if (!ignoreCustomAddresses) { | if (!ignoreCustomAddresses) { | ||||
return; | return; | ||||
} | } | ||||
let customAddressList = e.target.value; | let customAddressList = e.target.value; | ||||
// remove all whitespaces via regex | |||||
customAddressList = customAddressList.replace(/ /g, ''); | |||||
// validate the exclusion list input | // validate the exclusion list input | ||||
const addressListIsValid = | const addressListIsValid = | ||||
isValidAirdropExclusionArray(customAddressList); | isValidAirdropExclusionArray(customAddressList); | ||||
setIgnoreCustomAddressesListIsValid(addressListIsValid); | setIgnoreCustomAddressesListIsValid(addressListIsValid); | ||||
if (!addressListIsValid) { | if (!addressListIsValid) { | ||||
setIgnoreCustomAddressListError( | setIgnoreCustomAddressListError( | ||||
'Invalid address detected in ignore list', | 'Must be a comma-separated list of valid ecash-prefixed addresses with no spaces', | ||||
); | ); | ||||
} else { | } else { | ||||
setIgnoreCustomAddressListError(false); // needs to be explicitly set in order to refresh the error state from prior invalidation | setIgnoreCustomAddressListError(false); // needs to be explicitly set in order to refresh the error state from prior invalidation | ||||
} | } | ||||
// commit the ignore list to state | // commit the ignore list to state | ||||
setIgnoreCustomAddressesList(customAddressList); | setIgnoreCustomAddressesList(customAddressList); | ||||
}; | }; | ||||
Show All 21 Lines | if (ignoreMinEtokenBalance && ignoreCustomAddresses) { | ||||
totalAirdropIsValid && | totalAirdropIsValid && | ||||
ignoreCustomAddressesListIsValid; | ignoreCustomAddressesListIsValid; | ||||
} | } | ||||
return ( | return ( | ||||
<> | <> | ||||
<br /> | <br /> | ||||
<SidePaddingCtn data-testid="airdrop-ctn"> | <SidePaddingCtn data-testid="airdrop-ctn"> | ||||
<AntdFormWrapper> | <AirdropForm> | ||||
<Form | <FormRow> | ||||
style={{ | <InputFlex> | ||||
width: 'auto', | |||||
}} | |||||
> | |||||
<Form.Item | |||||
validateStatus={ | |||||
tokenIdIsValid === null || tokenIdIsValid | |||||
? '' | |||||
: 'error' | |||||
} | |||||
help={ | |||||
tokenIdIsValid === null || tokenIdIsValid | |||||
? '' | |||||
: 'Invalid eToken ID' | |||||
} | |||||
> | |||||
<Input | <Input | ||||
addonBefore="eToken ID" | |||||
placeholder="Enter the eToken ID" | placeholder="Enter the eToken ID" | ||||
name="tokenId" | name="tokenId" | ||||
value={formData.tokenId} | value={formData.tokenId} | ||||
onChange={e => handleTokenIdInput(e)} | handleInput={handleTokenIdInput} | ||||
/> | error={ | ||||
</Form.Item> | tokenIdIsValid === false | ||||
<Form.Item | ? 'Invalid eToken ID' | ||||
validateStatus={ | : false | ||||
totalAirdropIsValid === null || | |||||
totalAirdropIsValid | |||||
? '' | |||||
: 'error' | |||||
} | |||||
help={ | |||||
totalAirdropIsValid === null || | |||||
totalAirdropIsValid | |||||
? '' | |||||
: 'Invalid total XEC airdrop' | |||||
} | } | ||||
> | /> | ||||
</InputFlex> | |||||
</FormRow> | |||||
<FormRow> | |||||
<InputFlex> | |||||
<Input | <Input | ||||
addonBefore="Total XEC airdrop" | |||||
placeholder="Enter the total XEC airdrop" | placeholder="Enter the total XEC airdrop" | ||||
name="totalAirdrop" | name="totalAirdrop" | ||||
type="number" | type="number" | ||||
value={formData.totalAirdrop} | value={formData.totalAirdrop} | ||||
onChange={e => handleTotalAirdropInput(e)} | handleInput={handleTotalAirdropInput} | ||||
/> | error={ | ||||
</Form.Item> | totalAirdropIsValid === false | ||||
<Form.Item> | ? 'Invalid total XEC airdrop' | ||||
<AirdropOptions> | : false | ||||
<Switch | |||||
data-testid="communist-airdrop" | |||||
checkedChildren={ | |||||
<> | |||||
<SwitchContentHolder> | |||||
"Equal"{' '} | |||||
<Gulag src={Communist} /> | |||||
</SwitchContentHolder> | |||||
</> | |||||
} | } | ||||
unCheckedChildren="Pro-Rata" | /> | ||||
defaultunchecked="true" | </InputFlex> | ||||
checked={equalDistributionRatio} | </FormRow> | ||||
onChange={() => { | |||||
setEqualDistributionRatio( | <FormRow> | ||||
prev => !prev, | <SwitchHolder> | ||||
); | <CashtabSwitch | ||||
name="communist-airdrop" | |||||
on="Pro-Rata" | |||||
width={120} | |||||
right={86} | |||||
bgImageOff={Communist} | |||||
checked={!equalDistributionRatio} | |||||
handleToggle={() => { | |||||
setEqualDistributionRatio(prev => !prev); | |||||
}} | }} | ||||
/> | /> | ||||
<SwitchLabel> | |||||
{equalDistributionRatio | {equalDistributionRatio | ||||
? ` Airdrop qty | ? ` Airdrop | ||||
the same for everyone` | the same for everyone` | ||||
: ` Airdrop qty | : ` Airdrop | ||||
scaled to token balance`} | scaled to token balance`} | ||||
</AirdropOptions> | </SwitchLabel> | ||||
</Form.Item> | </SwitchHolder> | ||||
<Form.Item> | </FormRow> | ||||
<AirdropOptions> | <FormRow> | ||||
<Switch | <SwitchHolder> | ||||
onChange={() => | <CashtabSwitch | ||||
name="ignoreOwnAddress" | |||||
checked={ignoreOwnAddress} | |||||
handleToggle={() => | |||||
handleIgnoreOwnAddress(prev => !prev) | handleIgnoreOwnAddress(prev => !prev) | ||||
} | } | ||||
defaultunchecked="true" | |||||
checked={ignoreOwnAddress} | |||||
/> | /> | ||||
 Ignore my own address | <SwitchLabel>Ignore my own address</SwitchLabel> | ||||
</AirdropOptions> | </SwitchHolder> | ||||
</Form.Item> | </FormRow> | ||||
<Form.Item> | <FormRow> | ||||
<AirdropOptions> | <SwitchHolder> | ||||
<Switch | <CashtabSwitch | ||||
data-testid="ignore-mint-address" | name="ignore-mint-address" | ||||
onChange={() => | checked={ignoreMintAddress} | ||||
handleToggle={() => | |||||
handleIgnoreMintAddress(prev => !prev) | handleIgnoreMintAddress(prev => !prev) | ||||
} | } | ||||
defaultunchecked="true" | |||||
checked={ignoreMintAddress} | |||||
/> | /> | ||||
 Ignore eToken minter address | <SwitchLabel> | ||||
</AirdropOptions> | Ignore eToken minter address | ||||
</Form.Item> | </SwitchLabel> | ||||
<Form.Item> | </SwitchHolder> | ||||
<AirdropOptions> | </FormRow> | ||||
<Switch | <FormRow> | ||||
data-testid="minimum-etoken-holder-balance" | <SwitchHolder> | ||||
onChange={() => | <CashtabSwitch | ||||
name="minimum-etoken-holder-balance" | |||||
checked={ignoreMinEtokenBalance} | |||||
handleToggle={() => | |||||
handleIgnoreMinEtokenBalanceAmt( | handleIgnoreMinEtokenBalanceAmt( | ||||
prev => !prev, | prev => !prev, | ||||
) | ) | ||||
} | } | ||||
defaultunchecked="true" | |||||
checked={ignoreMinEtokenBalance} | |||||
style={{ | |||||
marginBottom: '5px', | |||||
}} | |||||
/> | /> | ||||
 Minimum eToken holder balance | <SwitchLabel> | ||||
Minimum eToken holder balance | |||||
</SwitchLabel> | |||||
</SwitchHolder> | |||||
{ignoreMinEtokenBalance && ( | {ignoreMinEtokenBalance && ( | ||||
<InputAmountSingle | <Input | ||||
validateStatus={ | error={ignoreMinEtokenBalanceAmountError} | ||||
ignoreMinEtokenBalanceAmountError | placeholder="Minimum eToken balance" | ||||
? 'error' | handleInput={handleMinEtokenBalanceChange} | ||||
: '' | value={ignoreMinEtokenBalanceAmount} | ||||
} | |||||
help={ | |||||
ignoreMinEtokenBalanceAmountError | |||||
? ignoreMinEtokenBalanceAmountError | |||||
: '' | |||||
} | |||||
inputProps={{ | |||||
placeholder: | |||||
'Minimum eToken balance', | |||||
onChange: e => | |||||
handleMinEtokenBalanceChange(e), | |||||
value: ignoreMinEtokenBalanceAmount, | |||||
}} | |||||
/> | /> | ||||
)} | )} | ||||
</AirdropOptions> | </FormRow> | ||||
</Form.Item> | <FormRow> | ||||
<Form.Item> | <SwitchHolder> | ||||
<AirdropOptions> | <CashtabSwitch | ||||
<Switch | name="ignore-custom-addresses" | ||||
data-testid="ignore-custom-addresses" | |||||
onChange={() => | |||||
handleIgnoreCustomAddresses( | |||||
prev => !prev, | |||||
) | |||||
} | |||||
defaultunchecked="true" | |||||
checked={ignoreCustomAddresses} | checked={ignoreCustomAddresses} | ||||
style={{ | handleToggle={() => | ||||
marginBottom: '5px', | handleIgnoreCustomAddresses(prev => !prev) | ||||
}} | } | ||||
/> | /> | ||||
 Ignore custom addresses | <SwitchLabel>Ignore custom addresses</SwitchLabel> | ||||
</SwitchHolder> | |||||
{ignoreCustomAddresses && ( | {ignoreCustomAddresses && ( | ||||
<DestinationAddressMulti | <TextArea | ||||
validateStatus={ | placeholder={`If more than one XEC address, separate them by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed`} | ||||
ignoreCustomAddressListError | error={ignoreCustomAddressListError} | ||||
? 'error' | value={ignoreCustomAddressesList} | ||||
: '' | name="address" | ||||
} | handleInput={handleIgnoreCustomAddressesList} | ||||
help={ | disabled={!ignoreCustomAddresses} | ||||
ignoreCustomAddressListError | |||||
? ignoreCustomAddressListError | |||||
: '' | |||||
} | |||||
inputProps={{ | |||||
placeholder: `If more than one XEC address, separate them by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed`, | |||||
name: 'address', | |||||
onChange: e => | |||||
handleIgnoreCustomAddressesList( | |||||
e, | |||||
), | |||||
required: ignoreCustomAddresses, | |||||
disabled: !ignoreCustomAddresses, | |||||
}} | |||||
/> | /> | ||||
)} | )} | ||||
</AirdropOptions> | </FormRow> | ||||
</Form.Item> | <FormRow> | ||||
<Form.Item> | |||||
<PrimaryButton | <PrimaryButton | ||||
onClick={() => calculateXecAirdrop()} | onClick={() => calculateXecAirdrop()} | ||||
disabled={ | disabled={ | ||||
!airdropCalcInputIsValid || !tokenIdIsValid | !airdropCalcInputIsValid || !tokenIdIsValid | ||||
} | } | ||||
> | > | ||||
Calculate Airdrop | Calculate Airdrop | ||||
</PrimaryButton> | </PrimaryButton> | ||||
</Form.Item> | </FormRow> | ||||
{showAirdropOutputs && ( | {showAirdropOutputs && ( | ||||
<> | <> | ||||
<Form.Item> | <FormRow> | ||||
<AirdropTitle> | |||||
One to Many Airdrop Payment Outputs | One to Many Airdrop Payment Outputs | ||||
<CopyToClipboard | |||||
data={airdropRecipients} | |||||
showToast | |||||
customMsg={ | |||||
'Airdrop recipients copied to clipboard' | |||||
} | |||||
> | |||||
<ThemedCopySolid /> | |||||
</CopyToClipboard> | |||||
</AirdropTitle> | |||||
<TextArea | <TextArea | ||||
name="airdropRecipients" | name="airdropRecipients" | ||||
placeholder="Please input parameters above." | placeholder="Please input parameters above." | ||||
value={airdropRecipients} | value={airdropRecipients} | ||||
rows="10" | rows="10" | ||||
readOnly | readOnly | ||||
/> | /> | ||||
</Form.Item> | </FormRow> | ||||
<Form.Item> | <FormRow> | ||||
<AirdropActions> | <SecondaryLink | ||||
<Link | |||||
type="text" | type="text" | ||||
to="/send" | to="/send" | ||||
state={{ | state={{ | ||||
airdropRecipients: | airdropRecipients: airdropRecipients, | ||||
airdropRecipients, | airdropTokenId: formData.tokenId, | ||||
airdropTokenId: | |||||
formData.tokenId, | |||||
}} | }} | ||||
disabled={!airdropRecipients} | disabled={!airdropRecipients} | ||||
> | > | ||||
Copy to Send screen | Copy to Send screen | ||||
</Link> | </SecondaryLink> | ||||
| </FormRow> | ||||
<CopyToClipboard | |||||
data={airdropRecipients} | |||||
showToast | |||||
customMsg={ | |||||
'Airdrop recipients copied to clipboard' | |||||
} | |||||
> | |||||
<Link | |||||
type="text" | |||||
disabled={!airdropRecipients} | |||||
to={'#'} | |||||
> | |||||
Copy to Clipboard | |||||
</Link> | |||||
</CopyToClipboard> | |||||
</AirdropActions> | |||||
</Form.Item> | |||||
</> | </> | ||||
)} | )} | ||||
</Form> | </AirdropForm> | ||||
</AntdFormWrapper> | |||||
</SidePaddingCtn> | </SidePaddingCtn> | ||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
/* | /* | ||||
passLoadingStatus must receive a default prop that is a function | passLoadingStatus must receive a default prop that is a function | ||||
in order to pass the rendering unit test in Airdrop.test.js | in order to pass the rendering unit test in Airdrop.test.js | ||||
Show All 15 Lines |