'SLP 1 fungible token. Token may be of fixed supply if no mint batons exist. If you have a mint baton, you can mint more of this token at any time. May have up to 9 decimal places.';
isSupportedToken = true;
break;
}
case 'SLP_TOKEN_TYPE_NFT1_GROUP': {
renderedTokenType = 'NFT Collection';
renderedTokenDescription =
'The parent tokens for an NFT collection. Can be used to mint NFTs. No decimal places. The supply of this token is the potential quantity of NFTs which could be minted. If no mint batons exist, the supply is fixed.';
isSupportedToken = true;
isNftParent = true;
break;
}
case 'SLP_TOKEN_TYPE_NFT1_CHILD': {
renderedTokenType = 'NFT';
renderedTokenDescription =
'eCash NFT. NFT supply is always 1. This NFT may belong to an NFT collection.';
isSupportedToken = true;
isNftChild = true;
break;
}
+ case 'SLP_TOKEN_TYPE_MINT_VAULT': {
+ renderedTokenType = 'SLP 2';
+ renderedTokenDescription =
+ 'SLP 2 mint vault token. Any utxo at the mint vault address may mint additional supply.';
+ isSupportedToken = true;
+ break;
+ }
default: {
// leave renderedTokenType and renderedTokenDescription as defaults
break;
}
}
break;
}
case 'ALP': {
renderedTokenType = 'ALP';
switch (tokenType?.type) {
case 'ALP_TOKEN_TYPE_STANDARD': {
renderedTokenType = 'ALP';
renderedTokenDescription =
'ALP v1 fungible token. Token may be of fixed or variable supply. If you have a mint baton, you can mint more of this token at any time. May have up to 9 decimal places. ALP tokens use EMPP technology, which supports more token actions compared to SLP and more complex combinations of token and app actions. ALP token txs may have up to 127 outputs, though current OP_RETURN size de facto limits a single tx to 29 outputs.';
isSupportedToken = true;
isAlp = true;
break;
}
default: {
// leave renderedTokenType and renderedTokenDescription as defaults
break;
}
}
break;
}
default: {
// leave renderedTokenType and renderedTokenDescription as defaults
description={`List tokens for sale with Agora Partial offers. Decide how many tokens you would like to sell, the minimum amount a user must buy to accept an offer, and the price per token. Due to encoding, input values here are approximate. The actual offer may have slightly different parameters. Price can be set lower than 1 XEC per token (no lower than 1 nanosat per 1 token satoshi). To ensure accurate pricing, the minimum buy should be set to at least 0.1% of the total tokens offered.`}
placeholder={`Type "burn ${tokenTicker}" to confirm`}
name="etokenToBeBurnt"
value={confirmationOfEtokenToBeBurnt}
error={burnConfirmationError}
handleInput={handleBurnConfirmationInput}
/>
</Modal>
)}
{showConfirmListNft &&
formData.nftListPrice !== '' &&
formData.nftListPrice !== null && (
<Modal
title={`List ${tokenTicker} for ${
selectedCurrency === appConfig.ticker
? `${parseFloat(
formData.nftListPrice,
).toLocaleString(userLocale)}
XEC ${getFormattedFiatPrice(
settings.fiatCurrency,
userLocale,
formData.nftListPrice,
fiatPrice,
)}?`
: `${
supportedFiatCurrencies[
settings.fiatCurrency
].symbol
}${parseFloat(
formData.nftListPrice,
).toLocaleString(userLocale)} ${
settings && settings.fiatCurrency
? settings.fiatCurrency.toUpperCase()
: 'USD'
} (${(
parseFloat(
formData.nftListPrice,
) / (fiatPrice as number)
).toLocaleString(userLocale, {
minimumFractionDigits:
appConfig.cashDecimals,
maximumFractionDigits:
appConfig.cashDecimals,
})}
XEC)?`
}`}
handleOk={listNft}
handleCancel={() =>
setShowConfirmListNft(false)
}
showCancelButton
description={`This will create a sell offer. Your NFT is only transferred if your full price is paid. The price is fixed in XEC. If your NFT is not purchased, you can cancel or renew your listing at any time.`}
// We can click an info icon to learn more about this token type
await userEvent.click(
await screen.findByRole('button', {
name: 'Click for more info about this token type',
}),
);
expect(
screen.getByText(
`SLP 1 fungible token. Token may be of fixed supply if no mint batons exist. If you have a mint baton, you can mint more of this token at any time. May have up to 9 decimal places.`,
// We can click an info icon to learn more about this token type
await userEvent.click(
await screen.findByRole('button', {
name: 'Click for more info about this token type',
}),
);
expect(
screen.getByText(
`SLP 1 fungible token. Token may be of fixed supply if no mint batons exist. If you have a mint baton, you can mint more of this token at any time. May have up to 9 decimal places.`,
// We can click an info icon to learn more about this token type
await userEvent.click(
await screen.findByRole('button', {
name: 'Click for more info about this token type',
}),
);
expect(
screen.getByText(
`The parent tokens for an NFT collection. Can be used to mint NFTs. No decimal places. The supply of this token is the potential quantity of NFTs which could be minted. If no mint batons exist, the supply is fixed.`,
// We can click an info icon to learn more about this token type
await userEvent.click(
await screen.findByRole('button', {
name: 'Click for more info about this token type',
}),
);
expect(
screen.getByText(
`The parent tokens for an NFT collection. Can be used to mint NFTs. No decimal places. The supply of this token is the potential quantity of NFTs which could be minted. If no mint batons exist, the supply is fixed.`,
// We can click an info icon to learn more about this token type
await userEvent.click(
await screen.findByRole('button', {
name: 'Click for more info about this token type',
}),
);
expect(
screen.getByText(
`The parent tokens for an NFT collection. Can be used to mint NFTs. No decimal places. The supply of this token is the potential quantity of NFTs which could be minted. If no mint batons exist, the supply is fixed.`,
),
).toBeInTheDocument();
// Close out of the info modal
await userEvent.click(screen.getByText('OK'));
// The wallet balance of this token is correctly rendered
it('SLP1 NFT page will update cashtab token cache for the NFT if it does not include groupTokenId, and for its parent if it is not in cache', async () => {
// Use wallet with nft utxo as only utxo
// Preset a cache without groupTokenId
// Use existing tx and token mocks
// We need to use a unique mockedChronik for this test, with a minted NFT utxo but no parent utxo
// The user actions available for the child NFTs depend on whether or not the NFTs exist in the user's wallet
// We can click an info icon to learn more about this token type
await userEvent.click(
await screen.findByRole('button', {
name: 'Click for more info about this token type',
}),
);
expect(
screen.getByText(
'ALP v1 fungible token. Token may be of fixed or variable supply. If you have a mint baton, you can mint more of this token at any time. May have up to 9 decimal places. ALP tokens use EMPP technology, which supports more token actions compared to SLP and more complex combinations of token and app actions. ALP token txs may have up to 127 outputs, though current OP_RETURN size de facto limits a single tx to 29 outputs.',
'⚠️ XECX redemption larger than hot wallet balance of 10k XEC. Execution may take up to 24 hours.',
),
).toBeInTheDocument();
});
it('We DO NOT see expected alert in XECX redemption workflow for hot wallet balance if there is some error determining the hot wallet balance', async () => {
// Mock Math.random()
jest.spyOn(global.Math, 'random').mockReturnValue(0.5); // set a fixed value
paramKey as keyof Omit<CashtabTxInfo, 'parseAllAsBip21'>
] = paramKeyValue[1];
}
}
}
// Only set txInfoFromUrl if you have valid legacy params or bip21
const validUrlParams =
(parseAllAsBip21 && 'bip21' in txInfo) ||
// Good if we have both address and value
('address' in txInfo && 'value' in txInfo) ||
// Good if we have address and no value
('address' in txInfo && !('value' in txInfo));
// If we 'value' key with no address, no good
// Note: because only the address and value keys are handled below,
// it's not an issue if we get all kinds of other garbage params
if (validUrlParams) {
// This is a tx request from the URL
// Save this flag in state var so it can be parsed in useEffect
txInfo.parseAllAsBip21 = parseAllAsBip21;
setTxInfoFromUrl(txInfo);
}
}, []);
useEffect(() => {
if (txInfoFromUrl === false) {
return;
}
if (txInfoFromUrl.parseAllAsBip21) {
handleAddressChange({
target: {
name: 'address',
value: txInfoFromUrl.bip21,
},
} as React.ChangeEvent<HTMLInputElement>);
} else {
// Enter address into input field and trigger handleAddressChange for validation
handleAddressChange({
target: {
name: 'address',
value: txInfoFromUrl.address,
},
} as React.ChangeEvent<HTMLInputElement>);
if (
typeof txInfoFromUrl.value !== 'undefined' &&
!Number.isNaN(parseFloat(txInfoFromUrl.value))
) {
// Only update the amount field if txInfo.value is a good input
// Sometimes we want this field to be adjusted by the user, e.g. a donation amount
// Do not populate the field if the value param is not parseable as a number
// the strings 'undefined' and 'null', which PayButton passes to signify 'no amount', fail this test
// TODO deprecate this support once PayButton and cashtab-components do not require it
handleAmountChange({
target: {
name: 'amount',
value: txInfoFromUrl.value,
},
} as React.ChangeEvent<HTMLInputElement>);
}
}
// We re-run this when balanceSats changes because validation of send amounts depends on balanceSats
}, [txInfoFromUrl, balanceSats]);
interface XecSendError {
error?: string;
message?: string;
}
function handleSendXecError(errorObj: XecSendError) {
let message;
if (
errorObj.error &&
errorObj.error.includes(
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)',
)
) {
message = `The ${appConfig.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.`;
it('We will throw expected insufficient funds error if we have enough utxos to cover target send amount but not enough to cover the fee', async () => {
/**
* We send 2000 satoshis with utxos of 1000, and 1001
* Expected behavior
* sendXec will build and attempt to broadcast the tx with total inputs of 2001 satoshis,
* as 2001 > 2000
* This will fail because tx fee is greater than 1 satoshi
* Cashtab will try to add another input, but no other inputs are available