setIgnoreCustomAddressListError(false); // needs to be explicitly set in order to refresh the error state from prior invalidation
}
// commit the ignore list to state
setIgnoreCustomAddressesList(customAddressList);
};
let airdropCalcInputIsValid = tokenIdIsValid && totalAirdropIsValid;
// if the ignore min etoken balance and exclusion list options are in use, add the relevant validation to the total pre-calculation validation
if (ignoreMinEtokenBalance && ignoreCustomAddresses) {
// both enabled
airdropCalcInputIsValid =
ignoreMinEtokenBalanceAmountIsValid &&
tokenIdIsValid &&
totalAirdropIsValid &&
ignoreCustomAddressesListIsValid;
} else if (ignoreMinEtokenBalance && !ignoreCustomAddresses) {
// ignore minimum etoken balance option only
airdropCalcInputIsValid =
ignoreMinEtokenBalanceAmountIsValid &&
tokenIdIsValid &&
totalAirdropIsValid;
} else if (!ignoreMinEtokenBalance && ignoreCustomAddresses) {
// ignore custom addresses only
airdropCalcInputIsValid =
tokenIdIsValid &&
totalAirdropIsValid &&
ignoreCustomAddressesListIsValid;
}
return (
<>
<WalletInfoCtn>
<WalletLabel name={wallet.name}></WalletLabel>
{!balances.totalBalance ? (
<ZeroBalanceHeader>
You currently have 0 {currency.ticker}
<br />
Deposit some funds to use this feature
</ZeroBalanceHeader>
) : (
<>
<BalanceHeader
balance={balances.totalBalance}
ticker={currency.ticker}
/>
{fiatPrice !== null && (
<BalanceHeaderFiat
balance={balances.totalBalance}
settings={cashtabSettings}
fiatPrice={fiatPrice}
/>
)}
</>
)}
</WalletInfoCtn>
<StyledModal
title="Querying the eCash blockchain"
visible={isAirdropCalcModalVisible}
okButtonProps={{ style: { display: 'none' } }}
onCancel={handleAirdropCalcModalCancel}
>
<Spin indicator={CustomSpinner} />
<Progress percent={airdropCalcModalProgress} />
</StyledModal>
<br />
<SidePaddingCtn>
<Row type="flex">
<Col span={24}>
<CustomCollapseCtn
panelHeader="XEC Airdrop Calculator"
optionalDefaultActiveKey={
location &&
location.state &&
location.state.airdropEtokenId
? ['1']
: ['0']
}
optionalKey="1"
>
<Alert
message={`Please ensure the qualifying eToken transactions to airdrop recipients have at least one confirmation. The airdrop calculator will not detect unconfirmed token balances.`}
type="warning"
/>
<br />
<AntdFormWrapper>
<Form
style={{
width: 'auto',
}}
>
<Form.Item
validateStatus={
tokenIdIsValid === null ||
tokenIdIsValid
? ''
: 'error'
}
help={
tokenIdIsValid === null ||
tokenIdIsValid
? ''
: 'Invalid eToken ID'
}
>
<Input
addonBefore="eToken ID"
placeholder="Enter the eToken ID"
name="tokenId"
value={formData.tokenId}
onChange={e =>
handleTokenIdInput(e)
}
/>
</Form.Item>
<Form.Item
validateStatus={
totalAirdropIsValid === null ||
totalAirdropIsValid
? ''
: 'error'
}
help={
totalAirdropIsValid === null ||
totalAirdropIsValid
? ''
: 'Invalid total XEC airdrop'
}
>
<Input
addonBefore="Total XEC airdrop"
placeholder="Enter the total XEC airdrop"
name="totalAirdrop"
type="number"
value={formData.totalAirdrop}
onChange={e =>
handleTotalAirdropInput(e)
}
/>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreOwnAddress(
prev => !prev,
)
}
defaultunchecked="true"
checked={ignoreOwnAddress}
/>
 Ignore my own address
</AirdropOptions>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreRecipientBelowDust(
prev => !prev,
)
}
defaultunchecked="true"
checked={
ignoreRecipientsBelowDust
}
/>
 Ignore airdrops below min.
payment (
{fromSatoshisToXec(
currency.dustSats,
- )}{' '}
+ ).toString()}{' '}
XEC)
</AirdropOptions>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreMintAddress(
prev => !prev,
)
}
defaultunchecked="true"
checked={ignoreMintAddress}
/>
 Ignore eToken minter address
</AirdropOptions>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreMinEtokenBalanceAmt(
prev => !prev,
)
}
defaultunchecked="true"
checked={ignoreMinEtokenBalance}
style={{
marginBottom: '5px',
}}
/>
 Minimum eToken holder balance
{ignoreMinEtokenBalance && (
<InputAmountSingle
validateStatus={
ignoreMinEtokenBalanceAmountError
? 'error'
: ''
}
help={
ignoreMinEtokenBalanceAmountError
? ignoreMinEtokenBalanceAmountError
: ''
}
inputProps={{
placeholder:
'Minimum eToken balance',
onChange: e =>
handleMinEtokenBalanceChange(
e,
),
value: ignoreMinEtokenBalanceAmount,
}}
/>
)}
</AirdropOptions>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreCustomAddresses(
prev => !prev,
)
}
defaultunchecked="true"
checked={ignoreCustomAddresses}
style={{
marginBottom: '5px',
}}
/>
 Ignore custom addresses
{ignoreCustomAddresses && (
<DestinationAddressMulti
validateStatus={
ignoreCustomAddressListError
? 'error'
: ''
}
help={
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>
</Form.Item>
<Form.Item>
<SmartButton
onClick={() =>
calculateXecAirdrop()
}
disabled={
!airdropCalcInputIsValid ||
!tokenIdIsValid
}
>
Calculate Airdrop
</SmartButton>
</Form.Item>
{showAirdropOutputs && (
<>
{!ignoreRecipientsBelowDust &&
!airdropOutputIsValid &&
etokenHolders > 0 && (
<>
<Alert
description={
'At least one airdrop is below the minimum ' +
fromSatoshisToXec(
currency.dustSats,
- ) +
+ ).toString() +
' XEC dust. Please increase the total XEC airdrop.'
}
type="error"
showIcon
/>
<br />
</>
)}
<Form.Item>
One to Many Airdrop Payment
Outputs
<TextArea
name="airdropRecipients"
placeholder="Please input parameters above."
value={airdropRecipients}
rows="10"
readOnly
/>
</Form.Item>
<Form.Item>
<AirdropActions>
<Link
type="text"
to={{
pathname: `/send`,
state: {
airdropRecipients:
airdropRecipients,
airdropTokenId:
formData.tokenId,
},
}}
disabled={
!airdropRecipients
}
>
Copy to Send screen
</Link>
<CopyToClipboard
data={airdropRecipients}
optionalOnCopyNotification={{
title: 'Copied',
msg: 'Airdrop recipients copied to clipboard',
}}
>
<Link
type="text"
disabled={
!airdropRecipients
}
to={'#'}
>
Copy to Clipboard
</Link>
</CopyToClipboard>
</AirdropActions>
</Form.Item>
</>
)}
</Form>
</AntdFormWrapper>
</CustomCollapseCtn>
</Col>
</Row>
</SidePaddingCtn>
</>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in Airdrop.test.js
status => {console.log(status)} is an arbitrary stub function
function handleSendXecError(errorObj, oneToManyFlag) {
// Set loading to false here as well, as balance may not change depending on where error occured in try loop
passLoadingStatus(false);
let message;
if (!errorObj.error && !errorObj.message) {
message = `Transaction failed: no response from ${getRestUrl()}.`;
} else if (
/Could not communicate with full node or other external service/.test(
errorObj.error,
)
) {
message = 'Could not communicate with API. Please try again.';
} else if (
errorObj.error &&
errorObj.error.includes(
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)',
)
) {
message = `The ${currency.ticker} you are trying to send has too many unconfirmed ancestors to send (limit 50). Sending will be possible after a block confirmation. Try again in about 10 minutes.`;
? `are you sure you want to send the following One to Many transaction?
${formData.address}`
: `Are you sure you want to send ${formData.value}${' '}
${selectedCurrency} to ${formData.address}?`}
</p>
</Modal>
<WalletInfoCtn>
<WalletLabel name={wallet.name}></WalletLabel>
{!balances.totalBalance ? (
<ZeroBalanceHeader>
You currently have 0 {currency.ticker}
<br />
Deposit some funds to use this feature
</ZeroBalanceHeader>
) : (
<>
<BalanceHeader
balance={balances.totalBalance}
ticker={currency.ticker}
/>
<BalanceHeaderFiat
balance={balances.totalBalance}
settings={cashtabSettings}
fiatPrice={fiatPrice}
/>
</>
)}
</WalletInfoCtn>
<SidePaddingCtn>
<Row type="flex">
<Col span={24}>
<Form
style={{
width: 'auto',
marginTop: '40px',
}}
>
{!isOneToManyXECSend ? (
<SendInputCtn>
<FormLabel>Send to</FormLabel>
<DestinationAddressSingle
style={{ marginBottom: '0px' }}
loadWithCameraOpen={
location &&
location.state &&
location.state.replyAddress
? false
: scannerSupported
}
validateStatus={
sendBchAddressError ? 'error' : ''
}
help={
sendBchAddressError
? sendBchAddressError
: ''
}
onScan={result =>
handleAddressChange({
target: {
name: 'address',
value: result,
},
})
}
inputProps={{
placeholder: `${currency.ticker} Address`,
name: 'address',
onChange: e =>
handleAddressChange(e),
required: true,
value: formData.address,
}}
></DestinationAddressSingle>
<FormLabel>Amount</FormLabel>
<SendBchInput
activeFiatCode={
cashtabSettings &&
cashtabSettings.fiatCurrency
? cashtabSettings.fiatCurrency.toUpperCase()
: 'USD'
}
validateStatus={
sendBchAmountError ? 'error' : ''
}
help={
sendBchAmountError
? sendBchAmountError
: ''
}
onMax={onMax}
inputProps={{
name: 'value',
dollar:
selectedCurrency === 'USD'
? 1
: 0,
placeholder: 'Amount',
onChange: e =>
handleBchAmountChange(e),
required: true,
value: formData.value,
disabled: priceApiError,
}}
selectProps={{
value: selectedCurrency,
disabled: queryStringText !== null,
onChange: e =>
handleSelectedCurrencyChange(e),
}}
></SendBchInput>
{priceApiError && (
<AlertMsg>
Error fetching fiat price. Setting
send by{' '}
{currency.fiatCurrencies[
cashtabSettings.fiatCurrency
].slug.toUpperCase()}{' '}
disabled
</AlertMsg>
)}
</SendInputCtn>
) : (
<>
<FormLabel>Send to</FormLabel>
<DestinationAddressMulti
validateStatus={
sendBchAddressError ? 'error' : ''
}
help={
sendBchAddressError
? sendBchAddressError
: ''
}
inputProps={{
placeholder: `One XEC address & value per line, separated by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500 \necash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700`,
name: 'address',
onChange: e =>
handleMultiAddressChange(e),
required: true,
value: formData.address,
}}
></DestinationAddressMulti>
</>
)}
{!priceApiError && !isOneToManyXECSend && (
<AmountPreviewCtn>
<LocaleFormattedValue>
{formatBalance(
formData.value,
userLocale,
)}{' '}
{selectedCurrency}
</LocaleFormattedValue>
<ConvertAmount>
{fiatPriceString !== '' && '='}{' '}
{fiatPriceString}
</ConvertAmount>
</AmountPreviewCtn>
)}
{queryStringText && (
<Alert
message={`You are sending a transaction to an address including query parameters "${queryStringText}." Only the "amount" parameter, in units of ${currency.ticker} satoshis, is currently supported.`}
type="warning"
/>
)}
<div
style={{
paddingTop: '12px',
}}
>
{!balances.totalBalance ||
apiError ||
sendBchAmountError ||
sendBchAddressError ||
priceApiError ? (
<DisabledButton>Send</DisabledButton>
) : (
<>
{txInfoFromUrl ? (
<PrimaryButton
onClick={() =>
checkForConfirmationBeforeSendXec()
}
>
Send
</PrimaryButton>
) : (
<PrimaryButton
onClick={() => {
checkForConfirmationBeforeSendXec();
}}
>
Send
</PrimaryButton>
)}
</>
)}
</div>
<CustomCollapseCtn
panelHeader="Advanced"
optionalDefaultActiveKey={
location &&
location.state &&
location.state.replyAddress
? ['1']
: ['0']
}
optionalKey="1"
>
<AntdFormWrapper
style={{
marginBottom: '20px',
}}
>
<TextAreaLabel>
Multiple Recipients:
<Switch
defaultunchecked="true"
checked={isOneToManyXECSend}
onChange={() => {
setIsOneToManyXECSend(
!isOneToManyXECSend,
);
setIsEncryptedOptionalOpReturnMsg(
false,
);
}}
style={{
marginBottom: '7px',
}}
/>
</TextAreaLabel>
<TextAreaLabel>
Message:
<Switch
disabled={isOneToManyXECSend}
style={{
marginBottom: '7px',
}}
checkedChildren="Private"
unCheckedChildren="Public"
defaultunchecked="true"
checked={
isEncryptedOptionalOpReturnMsg
}
onChange={() => {
setIsEncryptedOptionalOpReturnMsg(
prev => !prev,
);
setIsOneToManyXECSend(false);
}}
/>
</TextAreaLabel>
{isEncryptedOptionalOpReturnMsg ? (
<Alert
style={{
marginBottom: '10px',
}}
description="Please note encrypted messages can only be sent to wallets with at least 1 outgoing transaction."
type="warning"
showIcon
/>
) : (
<Alert
style={{
marginBottom: '10px',
}}
description="Please note this message will be public."
it(`Accepts a cachedWalletState that has not preserved BigNumber object types, and returns the same wallet state with BigNumber type re-instituted`, () => {
test('areAllUtxosIncludedInIncrementallyHydratedUtxos correctly identifies all utxos are in incrementally hydrated utxos [template]', async () => {
expect(
areAllUtxosIncludedInIncrementallyHydratedUtxos(
incrementalUtxosTemplate,
incrementallyHydratedUtxosTemplate,
),
).toBe(true);
});
test('areAllUtxosIncludedInIncrementallyHydratedUtxos returns false if a utxo in the utxo set is not in incrementally hydrated utxos [template]', async () => {
expect(
areAllUtxosIncludedInIncrementallyHydratedUtxos(
incrementalUtxosTemplate,
incrementallyHydratedUtxosTemplateMissing,
),
).toBe(false);
});
test('areAllUtxosIncludedInIncrementallyHydratedUtxos correctly identifies all utxos are in incrementally hydrated utxos', async () => {
expect(
areAllUtxosIncludedInIncrementallyHydratedUtxos(
utxosAfterSentTxIncremental,
incrementallyHydratedUtxosAfterProcessing,
),
).toBe(true);
});
test('areAllUtxosIncludedInIncrementallyHydratedUtxos returns false if a utxo in the utxo set is not in incrementally hydrated utxos', async () => {
// based on ecies-lite's encryption logic, the encryption buffer is concatenated as follows:
// [ epk + iv + ct + mac ] whereby:
// - The first 32 or 64 chars of the encryptionBuffer is the epk
// - Both iv and ct params are 16 chars each, hence their combined substring is 32 chars from the end of the epk string
// - within this combined iv/ct substring, the first 16 chars is the iv param, and ct param being the later half
// - The mac param is appended to the end of the encryption buffer
// validate input buffer
if (!encryptionBuffer) {
throw new Error(
'cashmethods.convertToEncryptStruct() error: input must be a buffer',
);
}
try {
// variable tracking the starting char position for string extraction purposes
let startOfBuf = 0;
// *** epk param extraction ***
// The first char of the encryptionBuffer indicates the type of the public key
// If the first char is 4, then the public key is 64 chars
// If the first char is 3 or 2, then the public key is 32 chars
// Otherwise this is not a valid encryption buffer compatible with the ecies-lite library
let publicKey;
switch (encryptionBuffer[0]) {
case 4:
publicKey = encryptionBuffer.slice(0, 65); // extract first 64 chars as public key
break;
case 3:
case 2:
publicKey = encryptionBuffer.slice(0, 33); // extract first 32 chars as public key
break;
default:
throw new Error(`Invalid type: ${encryptionBuffer[0]}`);
}
// *** iv and ct param extraction ***
startOfBuf += publicKey.length; // sets the starting char position to the end of the public key (epk) in order to extract subsequent iv and ct substrings
const encryptionTagLength = 32; // the length of the encryption tag (i.e. mac param) computed from each block of ciphertext, and is used to verify no one has tampered with the encrypted data
const ivCtSubstring = encryptionBuffer.slice(
startOfBuf,
encryptionBuffer.length - encryptionTagLength,
); // extract the substring containing both iv and ct params, which is after the public key but before the mac param i.e. the 'encryption tag'
const ivbufParam = ivCtSubstring.slice(0, 16); // extract the first 16 chars of substring as the iv param
const ctbufParam = ivCtSubstring.slice(16); // extract the last 16 chars as substring the ct param
// *** mac param extraction ***
const macParam = encryptionBuffer.slice(
encryptionBuffer.length - encryptionTagLength,
encryptionBuffer.length,
); // extract the mac param appended to the end of the buffer
Both utxos and hydratedUtxoDetails.slpUtxos are build like so
[
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
]
We want a function that quickly determines how many utxos are here
*/
// First, validate that you are getting a valid bch-api utxo set
// if you are not, then return false -- which would cause areAllUtxosIncludedInIncrementallyHydratedUtxos to return false and calculate utxo set the legacy way