Page MenuHomePhabricator

D17484.id52024.diff
No OneTemporary

D17484.id52024.diff

diff --git a/cashtab/src/components/Common/Inputs.tsx b/cashtab/src/components/Common/Inputs.tsx
--- a/cashtab/src/components/Common/Inputs.tsx
+++ b/cashtab/src/components/Common/Inputs.tsx
@@ -585,7 +585,7 @@
`;
export const SliderLabel = styled.span`
color: ${props => props.theme.primaryText};
- width: 50%;
+ width: 25%;
text-align: right;
line-height: 14px;
`;
@@ -608,6 +608,7 @@
fixedWidth?: boolean;
allowTypedInput?: boolean;
label?: string;
+ disabled?: boolean;
}
export const Slider: React.FC<SliderProps> = ({
name,
@@ -620,6 +621,7 @@
fixedWidth,
allowTypedInput,
label,
+ disabled = false,
}) => {
return (
<CashtabInputWrapper>
@@ -634,6 +636,7 @@
onChange={handleSlide}
isInvalid={typeof error === 'string'}
fixedWidth={fixedWidth}
+ disabled={disabled}
/>
{allowTypedInput && (
<LabelAndInputFlex>
@@ -649,6 +652,7 @@
placeholder={typeof label === 'string' ? label : name}
invalid={typeof error === 'string'}
onChange={handleSlide}
+ disabled={disabled}
></SliderInput>
</LabelAndInputFlex>
)}
diff --git a/cashtab/src/components/Etokens/Token/index.tsx b/cashtab/src/components/Etokens/Token/index.tsx
--- a/cashtab/src/components/Etokens/Token/index.tsx
+++ b/cashtab/src/components/Etokens/Token/index.tsx
@@ -23,6 +23,7 @@
getAgoraPartialListPriceError,
NANOSAT_DECIMALS,
isValidTokenId,
+ getAgoraMinBuyError,
} from 'validation';
import BigNumber from 'bignumber.js';
import {
@@ -84,6 +85,8 @@
InputFlex,
ListPriceInput,
Slider,
+ LabelAndInputFlex,
+ SliderLabel,
} from 'components/Common/Inputs';
import { QuestionIcon } from 'components/Common/CustomIcons';
import Switch from 'components/Common/Switch';
@@ -617,36 +620,57 @@
}, [tokenId, cashtabCache.tokens.get(tokenId)]);
useEffect(() => {
- // when agoraPartialTokenQty changes, the min slider max changes
- // but its value does not necessarily change and may remain above agoraPartialTokenQty
-
- // If they are both zero, do nothing, we do not want to load the screen with validation errors
- if (agoraPartialMin === '0' && agoraPartialTokenQty === '0') {
+ if (
+ formData.tokenListPrice === null ||
+ formData.tokenListPrice === '' ||
+ agoraPartialTokenQty === '0'
+ ) {
+ // If we have no price or no offered qty, do nothing
+ // Min buy is the last field entered
return;
}
- // Check for this error condition on this event
- if (new BigNumber(agoraPartialMin).gt(agoraPartialTokenQty)) {
- // Set a validation error, this would create an unacceptable offer
- setAgoraPartialMinError(
- 'The min buy must be less than or equal to the offered quantity',
- );
- } else {
- // Normal validation, there may be some other reason agoraPartialMin is still invalid
- const isValidAmountOrErrorMsg = isValidTokenSendOrBurnAmount(
- agoraPartialMin,
- tokenBalance as string, // we do not render the slide without tokenBalance
- decimals as SlpDecimals,
- // Component does not render until token info is defined
- protocol as 'ALP' | 'SLP',
- );
- setAgoraPartialMinError(
- isValidAmountOrErrorMsg === true
- ? false
- : isValidAmountOrErrorMsg,
- );
+ let agoraPartialMinToValidate = agoraPartialMin;
+ // If the user has not yet entered the min quantity, enter THE min min qty as default
+ if (agoraPartialMin === '0') {
+ if (parseFloat(formData.tokenListPrice) === 0) {
+ // We can get here if the user is typing, e.g. 0.0001
+ // We do not want to setAgoraPartialMin(Infinity)
+ return;
+ }
+ // Calculate the min
+ const requiredMinBuyTokenQty = new BigNumber(
+ toXec(appConfig.dustSats),
+ )
+ .div(formData.tokenListPrice)
+ .decimalPlaces(decimals as SlpDecimals, BigNumber.ROUND_UP)
+ .toString();
+
+ // Set it as the min
+ // "return" as this change will trigger validation, we will re-enter this useEffect
+ // "should" always be valid tho
+ setAgoraPartialMin(requiredMinBuyTokenQty);
+
+ // We want to validate this here because the calculated min could be higher than the
+ // total offered qty
+ agoraPartialMinToValidate = requiredMinBuyTokenQty;
}
- }, [agoraPartialMin, agoraPartialTokenQty]);
+
+ // Normal validation, there may be some other reason agoraPartialMin is still invalid
+ const agoraMinBuyError = getAgoraMinBuyError(
+ formData.tokenListPrice as string,
+ selectedCurrency,
+ fiatPrice,
+ agoraPartialMinToValidate,
+ agoraPartialTokenQty,
+ decimals as SlpDecimals,
+ // Component does not render until token info is defined
+ protocol as 'ALP' | 'SLP',
+ tokenBalance as string, // we do not render the slide without tokenBalance
+ userLocale,
+ );
+ setAgoraPartialMinError(agoraMinBuyError);
+ }, [formData.tokenListPrice, agoraPartialTokenQty]);
const getNftOffer = async () => {
try {
@@ -919,31 +943,20 @@
const handleTokenMinSlide = (e: React.ChangeEvent<HTMLInputElement>) => {
const amount = e.target.value;
- const isValidAmountOrErrorMsg = isValidTokenSendOrBurnAmount(
+ const agoraMinBuyError = getAgoraMinBuyError(
+ formData.tokenListPrice as string,
+ selectedCurrency,
+ fiatPrice,
amount,
- tokenBalance as string, // we do not render the slide without tokenBalance
+ agoraPartialTokenQty,
decimals as SlpDecimals,
// Component does not render until token info is defined
protocol as 'ALP' | 'SLP',
+ tokenBalance as string, // we do not render the slide without tokenBalance
+ userLocale,
);
- setAgoraPartialMinError(
- isValidAmountOrErrorMsg === true ? false : isValidAmountOrErrorMsg,
- );
-
- // Also validate price input if it is non-zero
- // If the user has reduced min qty, the price may now be below dust
- if (formData.tokenListPrice !== null) {
- setTokenListPriceError(
- getAgoraPartialListPriceError(
- formData.tokenListPrice,
- selectedCurrency,
- fiatPrice,
- amount,
- decimals as SlpDecimals,
- ),
- );
- }
+ setAgoraPartialMinError(agoraMinBuyError);
setAgoraPartialMin(amount);
};
@@ -1312,7 +1325,6 @@
value,
selectedCurrency,
fiatPrice,
- agoraPartialMin,
decimals as SlpDecimals,
),
);
@@ -2148,7 +2160,7 @@
</AgoraPreviewRow>
<AgoraPreviewRow>
<AgoraPreviewLabel>
- Min buy:{' '}
+ Min qty:{' '}
</AgoraPreviewLabel>
<AgoraPreviewCol>
{decimalizedTokenQtyToLocaleFormat(
@@ -2764,13 +2776,60 @@
/>
</InputRow>
</SendTokenFormRow>
+ <SendTokenFormRow>
+ <InputRow>
+ <LabelAndInputFlex>
+ <SliderLabel>
+ Price:
+ </SliderLabel>
+ <ListPriceInput
+ name="tokenListPrice"
+ placeholder="Enter list price (per token)"
+ inputDisabled={
+ agoraPartialTokenQty ===
+ '0'
+ }
+ value={
+ formData.tokenListPrice
+ }
+ selectValue={
+ selectedCurrency
+ }
+ selectDisabled={
+ fiatPrice ===
+ null
+ }
+ fiatCode={settings.fiatCurrency.toUpperCase()}
+ error={
+ tokenListPriceError
+ }
+ handleInput={
+ handleTokenListPriceChange
+ }
+ handleSelect={
+ handleSelectedCurrencyChange
+ }
+ ></ListPriceInput>
+ </LabelAndInputFlex>
+ </InputRow>
+ </SendTokenFormRow>
<SendTokenFormRow>
<InputRow>
<Slider
name={
'agoraPartialMin'
}
- label={`Min buy`}
+ disabled={
+ formData.tokenListPrice ===
+ null ||
+ formData.tokenListPrice ===
+ '' ||
+ formData.tokenListPrice ===
+ '0' ||
+ tokenListPriceError !==
+ false
+ }
+ label={`Min qty`}
value={
agoraPartialMin
}
@@ -2792,38 +2851,6 @@
/>
</InputRow>
</SendTokenFormRow>
- <SendTokenFormRow>
- <InputRow>
- <ListPriceInput
- name="tokenListPrice"
- placeholder="Enter list price (per token)"
- inputDisabled={
- agoraPartialMin ===
- '0'
- }
- value={
- formData.tokenListPrice
- }
- selectValue={
- selectedCurrency
- }
- selectDisabled={
- fiatPrice ===
- null
- }
- fiatCode={settings.fiatCurrency.toUpperCase()}
- error={
- tokenListPriceError
- }
- handleInput={
- handleTokenListPriceChange
- }
- handleSelect={
- handleSelectedCurrencyChange
- }
- ></ListPriceInput>
- </InputRow>
- </SendTokenFormRow>
{!tokenListPriceError &&
formData.tokenListPrice !==
diff --git a/cashtab/src/components/Etokens/Token/styled.ts b/cashtab/src/components/Etokens/Token/styled.ts
--- a/cashtab/src/components/Etokens/Token/styled.ts
+++ b/cashtab/src/components/Etokens/Token/styled.ts
@@ -50,6 +50,12 @@
export const InputRow = styled.div`
width: 100%;
`;
+export const FlexLabeledInputWrapper = styled.div`
+ display: flex;
+`;
+export const FlexInputLabel = styled.div`
+ display: flex;
+`;
export const TokenStatsTable = styled.div`
display: flex;
diff --git a/cashtab/src/components/Etokens/__tests__/Token.test.js b/cashtab/src/components/Etokens/__tests__/Token.test.js
--- a/cashtab/src/components/Etokens/__tests__/Token.test.js
+++ b/cashtab/src/components/Etokens/__tests__/Token.test.js
@@ -129,15 +129,15 @@
);
const totalQtyInput = screen.getByPlaceholderText('Offered qty');
- const minQtyInput = screen.getByPlaceholderText('Min buy');
+ const minQtyInput = screen.getByPlaceholderText('Min qty');
// Input fields are rendered
expect(totalQtyInput).toBeInTheDocument();
expect(minQtyInput).toBeInTheDocument();
- // Qty inputs are not disabled
- expect(totalQtyInput).toHaveProperty('disabled', false);
- expect(minQtyInput).toHaveProperty('disabled', false);
+ // Only the total qty input is enabled
+ expect(totalQtyInput).toBeEnabled();
+ expect(minQtyInput).toBeDisabled();
// Price input is disabled as qty inputs are at 0 value
expect(
@@ -190,15 +190,15 @@
);
const totalQtyInput = screen.getByPlaceholderText('Offered qty');
- const minQtyInput = screen.getByPlaceholderText('Min buy');
+ const minQtyInput = screen.getByPlaceholderText('Min qty');
// Input fields are rendered
expect(totalQtyInput).toBeInTheDocument();
expect(minQtyInput).toBeInTheDocument();
- // Qty inputs are not disabled
- expect(totalQtyInput).toHaveProperty('disabled', false);
- expect(minQtyInput).toHaveProperty('disabled', false);
+ // Only total qty input is enabled
+ expect(totalQtyInput).toBeEnabled();
+ expect(minQtyInput).toBeDisabled();
// Price input is disabled as qty inputs are at 0 value
expect(
diff --git a/cashtab/src/components/Etokens/__tests__/TokenActions.test.js b/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
--- a/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
+++ b/cashtab/src/components/Etokens/__tests__/TokenActions.test.js
@@ -275,33 +275,48 @@
);
expect(priceInput).toHaveProperty('disabled', true);
+ const minQtyInput = screen.getByPlaceholderText('Min qty');
+
+ // Min qty input is disabled before we enter offered qty
+ expect(minQtyInput).toBeDisabled();
+
// Enter token balance as offered qty
await userEvent.type(screen.getByPlaceholderText('Offered qty'), '111');
- // Enter a min qty
- await userEvent.type(screen.getByPlaceholderText('Min buy'), '11');
-
// The price input is no longer disabled
expect(priceInput).toHaveProperty('disabled', false);
// We see expected error msg if we try to list the token at a price where the min buy would cost less than dust
await userEvent.type(priceInput, '0.001');
+ // The min qty input updates automatically when price is set to reflect the actual min qty
+ // i.e. what qty would sell for dust
+ expect(minQtyInput).toHaveValue('5460');
+
+ // But this is higher than our balance, so we get an error
expect(
screen.getByText(
- 'Minimum buy costs 0.011 XEC, must be at least 5.46 XEC',
+ 'The min buy must be less than or equal to the offered quantity',
),
).toBeInTheDocument();
- // The buy button is disabled with invalid price
- expect(listButton).toHaveProperty('disabled', true);
-
- // Increase the price to a valid one
+ // Ok, let's change the price
await userEvent.clear(priceInput);
await userEvent.type(priceInput, '0.5');
+ // Note that the min qty does not auto-update when price changes after the initial change
+ expect(minQtyInput).toHaveValue('5460');
+
+ // The buy button is disabled with invalid qty
+ expect(listButton).toBeDisabled();
+
+ // Let's lower the qty
+ expect(minQtyInput).toBeEnabled();
+ await userEvent.clear(minQtyInput);
+ await userEvent.type(minQtyInput, '11');
+
// The list button is no longer disabled
- expect(listButton).toHaveProperty('disabled', false);
+ expect(listButton).toBeEnabled();
// Lets bump the offered qty below the min qty using the slider
// get the agoraPartialTokenQty slider
@@ -1765,31 +1780,68 @@
// Enter token balance as offered qty
await userEvent.type(screen.getByPlaceholderText('Offered qty'), '100');
- // Enter a min qty
- await userEvent.type(screen.getByPlaceholderText('Min buy'), '1');
-
// The price input is no longer disabled
expect(priceInput).toBeEnabled();
- // We see expected error msg if we try to list the token at a price where the min buy would cost less than dust
+ // Enter a price
await userEvent.type(priceInput, '0.001');
+ const minQtyInput = screen.getByPlaceholderText('Min qty');
+
+ // The quantity updates automatically
+ expect(minQtyInput).toHaveValue('5460');
+
+ // But because this price is so low, now the min qty is actually higher than our token balance
+ // So we see an error
+ expect(
+ screen.getByText(
+ 'The min buy must be less than or equal to the offered quantity',
+ ),
+ ).toBeInTheDocument();
+
+ // Ok let's back off our min qty
+ await userEvent.clear(minQtyInput);
+ await userEvent.type(minQtyInput, '5');
+
+ // Now we have an error because the min qty is too low
expect(
screen.getByText(
- 'Minimum buy costs 0.001 XEC, must be at least 5.46 XEC',
+ 'Total cost of minimum buy below dust. Min offered qty must be at least 5,460.',
),
).toBeInTheDocument();
- // The buy button is disabled with invalid price
+ // The buy button is disabled with invalid qty
expect(listButton).toBeDisabled();
- // Increase the price to a valid one
+ // We'll need to raise the price because we don't have that many tokens
await userEvent.clear(priceInput);
- await userEvent.type(priceInput, '33');
+ await userEvent.type(priceInput, '1');
+
+ // But now still below dust
+ expect(
+ screen.getByText(
+ 'Total cost of minimum buy below dust. Min offered qty must be at least 5.46.',
+ ),
+ ).toBeInTheDocument();
+
+ // Ok well we can do that
+ await userEvent.clear(minQtyInput);
+ await userEvent.type(minQtyInput, '5.46');
+
+ // No more error
+ expect(
+ screen.queryByText(
+ 'Total cost of minimum buy below dust. Min offered qty must be at least 5.46.',
+ ),
+ ).not.toBeInTheDocument();
// The list button is no longer disabled
expect(listButton).toBeEnabled();
+ // Let's use a higher price though because that's what the test has mocks for
+ await userEvent.clear(priceInput);
+ await userEvent.type(priceInput, '33');
+
// The fiat price is previewed correctly
expect(
screen.getByText('33 XEC ($0.0009900 USD) per token'),
@@ -1821,6 +1873,10 @@
await screen.findByText('$0.0005 USD (16.67 XEC) per token'),
).toBeInTheDocument();
+ // We can have a lower min qty now since the price is higher
+ await userEvent.clear(minQtyInput);
+ await userEvent.type(minQtyInput, '1');
+
// Click the now-enabled list button
expect(listButton).toBeEnabled();
await userEvent.click(listButton);
@@ -1833,7 +1889,7 @@
// Offered qty (actual, calculated from AgoraOffer)
const actualOfferedQty = '99.9936';
expect(screen.getByText(actualOfferedQty)).toBeInTheDocument();
- // Min by (actual, calculated from AgoraOffer)
+ // Min buy (actual, calculated from AgoraOffer)
expect(screen.getByText('1.0240')).toBeInTheDocument();
// Actual price calculated from AgoraOffer
const actualPricePerTokenForMinBuy = '16.67 XEC';
@@ -1851,7 +1907,6 @@
// We change our mind and list it
await userEvent.click(listButton);
- // We wait for the preview to be calculated again
expect(await screen.findByText('List tCRD?')).toBeInTheDocument();
await userEvent.click(screen.getByText('OK'));
diff --git a/cashtab/src/validation/__tests__/index.test.js b/cashtab/src/validation/__tests__/index.test.js
--- a/cashtab/src/validation/__tests__/index.test.js
+++ b/cashtab/src/validation/__tests__/index.test.js
@@ -32,6 +32,7 @@
getXecListPriceError,
getAgoraPartialListPriceError,
getAgoraPartialAcceptTokenQtyError,
+ getAgoraMinBuyError,
} from 'validation';
import {
validXecAirdropExclusionList,
@@ -527,7 +528,6 @@
xecListPrice,
selectedCurrency,
fiatPrice,
- minBuyTokenQty,
tokenDecimals,
returned,
} = expectedReturn;
@@ -537,7 +537,6 @@
xecListPrice,
selectedCurrency,
fiatPrice,
- minBuyTokenQty,
tokenDecimals,
),
).toBe(returned);
@@ -569,4 +568,37 @@
});
});
});
+ describe('Gets error or false for min qty input', () => {
+ const { expectedReturns } = vectors.getAgoraMinBuyError;
+ expectedReturns.forEach(expectedReturn => {
+ const {
+ description,
+ xecListPrice,
+ selectedCurrency,
+ fiatPrice,
+ minBuyTokenQty,
+ offeredTokenQty,
+ tokenDecimals,
+ tokenProtocol,
+ tokenBalance,
+ userLocale,
+ returned,
+ } = expectedReturn;
+ it(`getAgoraMinBuyError: ${description}`, () => {
+ expect(
+ getAgoraMinBuyError(
+ xecListPrice,
+ selectedCurrency,
+ fiatPrice,
+ minBuyTokenQty,
+ offeredTokenQty,
+ tokenDecimals,
+ tokenProtocol,
+ tokenBalance,
+ userLocale,
+ ),
+ ).toBe(returned);
+ });
+ });
+ });
});
diff --git a/cashtab/src/validation/fixtures/vectors.js b/cashtab/src/validation/fixtures/vectors.js
--- a/cashtab/src/validation/fixtures/vectors.js
+++ b/cashtab/src/validation/fixtures/vectors.js
@@ -2618,14 +2618,13 @@
},
{
description:
- 'Rejects price if minimum token accept costs 1 nanosatoshi less than dust, xec price',
+ 'Accepts price if minimum token accept costs 1 nanosatoshi less than dust, xec price, because price validation does not depend on min buy qty, instead this is handled by qty validation',
xecListPrice: '5.45999999999',
selectedCurrency: 'XEC',
fiatPrice: null,
minBuyTokenQty: 1,
tokenDecimals: 0,
- returned:
- 'Minimum buy costs 5.45999999999 XEC, must be at least 5.46 XEC',
+ returned: false,
},
{
description:
@@ -2645,8 +2644,7 @@
fiatPrice: 1,
minBuyTokenQty: 1,
tokenDecimals: 0,
- returned:
- 'Minimum buy costs 5.45999999999 XEC, must be at least 5.46 XEC',
+ returned: false,
},
{
description: 'Accepts the lowest possible price for XEC input',
@@ -2912,4 +2910,78 @@
},
],
},
+ getAgoraMinBuyError: {
+ expectedReturns: [
+ {
+ description:
+ 'We reject a min qty that is higher than the offered qty',
+ xecListPrice: '1',
+ selectedCurrency: 'XEC',
+ fiatPrice: 1,
+ minBuyTokenQty: '2',
+ offeredTokenQty: '1',
+ tokenDecimals: 0,
+ tokenProtocol: 'ALP',
+ tokenBalance: '100',
+ userLocale: 'en-US',
+ returned:
+ 'The min buy must be less than or equal to the offered quantity',
+ },
+ {
+ description:
+ 'We can pass on an error from isValidTokenSendOrBurnAmount',
+ xecListPrice: '1',
+ selectedCurrency: 'XEC',
+ fiatPrice: 1,
+ minBuyTokenQty: '0', // 0 is invalid
+ offeredTokenQty: '1',
+ tokenDecimals: 0,
+ tokenProtocol: 'ALP',
+ tokenBalance: '100',
+ userLocale: 'en-US',
+ returned: 'Amount must be greater than 0',
+ },
+ {
+ description: 'We give the required min qty if input is too low',
+ xecListPrice: '1',
+ selectedCurrency: 'XEC',
+ fiatPrice: 1,
+ minBuyTokenQty: '1', // 0 is invalid
+ offeredTokenQty: '100',
+ tokenDecimals: 0,
+ tokenProtocol: 'ALP',
+ tokenBalance: '100',
+ userLocale: 'en-US',
+ returned: `Total cost of minimum buy below dust. Min offered qty must be at least 6.`,
+ },
+ {
+ description:
+ 'We give the required min qty if input is too low with decimals',
+ xecListPrice: '1',
+ selectedCurrency: 'XEC',
+ fiatPrice: 1,
+ minBuyTokenQty: '1', // 0 is invalid
+ offeredTokenQty: '100',
+ tokenDecimals: 9,
+ tokenProtocol: 'ALP',
+ tokenBalance: '100',
+ userLocale: 'en-US',
+ returned: `Total cost of minimum buy below dust. Min offered qty must be at least 5.46.`,
+ },
+ {
+ description:
+ 'We give the required min qty if input is too low for a locale that does not use a period for decimal places',
+ xecListPrice: '1',
+ selectedCurrency: 'XEC',
+ fiatPrice: 1,
+ minBuyTokenQty: '1', // 0 is invalid
+ offeredTokenQty: '100',
+ tokenDecimals: 9,
+ tokenProtocol: 'ALP',
+ tokenBalance: '100',
+ userLocale: 'fr-FR',
+ returned: `Total cost of minimum buy below dust. Min offered qty must be at least 5,46.`,
+ },
+ ],
+ },
};
diff --git a/cashtab/src/validation/index.ts b/cashtab/src/validation/index.ts
--- a/cashtab/src/validation/index.ts
+++ b/cashtab/src/validation/index.ts
@@ -9,6 +9,7 @@
xecToNanoSatoshis,
LegacyCashtabWallet,
SlpDecimals,
+ decimalizeTokenAmount,
} from 'wallet';
import { isValidCashAddress } from 'ecashaddrjs';
import * as bip39 from 'bip39';
@@ -1164,7 +1165,6 @@
xecListPrice: string,
selectedCurrency: string,
fiatPrice: null | number,
- minBuyTokenQty: string,
tokenDecimals: SlpDecimals,
): false | string => {
if (xecListPrice === '') {
@@ -1188,7 +1188,6 @@
}
// Get the price in XEC
-
const priceXec =
selectedCurrency !== 'XEC'
? new BigNumber(
@@ -1198,17 +1197,13 @@
)
: new BigNumber(xecListPrice);
- // Get the total price of the min buy amount
- const priceXecMinBuy = priceXec.times(new BigNumber(minBuyTokenQty));
-
- if (priceXecMinBuy.lt(toXec(appConfig.dustSats))) {
- // We cannot enforce an output to have less than dust satoshis
- return `Minimum buy costs ${priceXecMinBuy.toString()} XEC, must be at least 5.46 XEC`;
- }
-
// Get the price in nanosats per token satoshi
// this is the unit agora takes, 1 nanosat per 1 tokens at is the min
const priceNanoSatsPerDecimalizedToken = xecToNanoSatoshis(priceXec);
+ console.log(
+ `priceNanoSatsPerDecimalizedToken`,
+ priceNanoSatsPerDecimalizedToken,
+ );
if (priceNanoSatsPerDecimalizedToken < Math.pow(10, tokenDecimals)) {
return 'Price cannot be lower than 1 nanosatoshi per 1 token satoshi';
@@ -1218,6 +1213,69 @@
return false;
};
+export const getAgoraMinBuyError = (
+ xecListPrice: string,
+ selectedCurrency: string,
+ fiatPrice: null | number,
+ minBuyTokenQty: string,
+ offeredTokenQty: string,
+ tokenDecimals: SlpDecimals,
+ tokenProtocol: 'ALP' | 'SLP',
+ tokenBalance: string,
+ userLocale: string,
+) => {
+ // First, make sure the min does not exceed total offerd tokens
+ if (new BigNumber(minBuyTokenQty).gt(offeredTokenQty)) {
+ return 'The min buy must be less than or equal to the offered quantity';
+ }
+
+ // Next, make sure the qty is valid for this token
+ const isValidAmountOrErrorMsg = isValidTokenSendOrBurnAmount(
+ minBuyTokenQty,
+ tokenBalance,
+ tokenDecimals,
+ tokenProtocol,
+ );
+
+ if (typeof isValidAmountOrErrorMsg === 'string') {
+ // If not, this is the validation error
+ return isValidAmountOrErrorMsg;
+ }
+ // If the qty is valid per the token specs, see if it is valid on price
+ // We cannot list tokens if the price of a min buy is less than dust
+ // Note that we "could" do this, but it would mean users are overpaying
+ // Maintain accurate prices
+
+ // Get the price in XEC
+ const priceXec =
+ selectedCurrency !== 'XEC'
+ ? new BigNumber(
+ new BigNumber(xecListPrice)
+ .div(fiatPrice as number)
+ .toFixed(NANOSAT_DECIMALS),
+ )
+ : new BigNumber(xecListPrice);
+
+ // Get the total price of the min buy amount
+ const priceXecMinBuy = priceXec.times(new BigNumber(minBuyTokenQty));
+
+ if (priceXecMinBuy.lt(toXec(appConfig.dustSats))) {
+ // Determine what the min buy must be
+ const requiredMinBuyTokenQty = new BigNumber(toXec(appConfig.dustSats))
+ .div(priceXec)
+ .decimalPlaces(tokenDecimals, BigNumber.ROUND_UP);
+
+ const renderedMinBuyTokenQty = decimalizedTokenQtyToLocaleFormat(
+ requiredMinBuyTokenQty.toString(),
+ userLocale,
+ );
+
+ // We cannot enforce an output to have less than dust satoshis
+ return `Total cost of minimum buy below dust. Min offered qty must be at least ${renderedMinBuyTokenQty}.`;
+ }
+ return false;
+};
+
export const getAgoraPartialAcceptTokenQtyError = (
takeTokenDecimalizedQty: string,
decimalizedTokenQtyMin: string,

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 26, 11:31 (16 h, 40 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573411
Default Alt Text
D17484.id52024.diff (35 KB)

Event Timeline