diff --git a/packages/shared/components/inputs/AssetAmountInput.svelte b/packages/shared/components/inputs/AssetAmountInput.svelte index 64793ed9054..102377f86fc 100644 --- a/packages/shared/components/inputs/AssetAmountInput.svelte +++ b/packages/shared/components/inputs/AssetAmountInput.svelte @@ -8,6 +8,7 @@ convertToRawAmount, formatTokenAmountBestMatch, formatTokenAmountDefault, + getRequiredStorageDepositForMinimalBasicOutput, getUnitFromTokenMetadata, visibleSelectedAccountAssets, } from '@core/wallet' @@ -51,9 +52,14 @@ unit = undefined } - export function validate(allowZeroOrNull = false): Promise { + export async function validate(allowZeroOrNull = false): Promise { const amountAsFloat = parseCurrency(amount) const isAmountZeroOrNull = !Number(amountAsFloat) + const standard = asset?.metadata?.standard + const remainderBalance = Number(Big(availableBalance)?.minus(bigAmount)) + // Calculate the minimum required storage deposit for a minimal basic output + // This is used to check if the user is leaving dust behind that cant cover the storage deposit + const minRequiredStorageDeposit = await getRequiredStorageDepositForMinimalBasicOutput() // Zero value transactions can still contain metadata/tags error = '' if (allowZeroOrNull && isAmountZeroOrNull) { @@ -62,7 +68,7 @@ } else if (isAmountZeroOrNull) { error = localize('error.send.amountInvalidFormat') } else if ( - ((asset?.metadata?.standard === TokenStandard.BaseToken && unit === asset?.metadata?.subunit) || + ((standard === TokenStandard.BaseToken && unit === asset?.metadata?.subunit) || (unit === getUnitFromTokenMetadata(asset?.metadata) && asset?.metadata?.decimals === 0)) && Number.parseInt(amount, 10).toString() !== amount ) { @@ -73,6 +79,17 @@ error = localize('error.send.amountZero') } else if (!bigAmount.mod(1).eq(Big(0))) { error = localize('error.send.amountSmallerThanSubunit') + } else if ( + standard === TokenStandard.BaseToken && + remainderBalance !== 0 && + remainderBalance < minRequiredStorageDeposit + ) { + // don't allow leaving dust(amount less than minimum required storage deposit) for base token + error = localize('error.send.leavingDust', { + values: { + minRequiredStorageDeposit: formatTokenAmountBestMatch(minRequiredStorageDeposit, asset?.metadata), + }, + }) } if (error) { diff --git a/packages/shared/lib/core/wallet/utils/outputs/getRequiredStorageDepositForMinimalBasicOutput.ts b/packages/shared/lib/core/wallet/utils/outputs/getRequiredStorageDepositForMinimalBasicOutput.ts new file mode 100644 index 00000000000..699b08f35a7 --- /dev/null +++ b/packages/shared/lib/core/wallet/utils/outputs/getRequiredStorageDepositForMinimalBasicOutput.ts @@ -0,0 +1,41 @@ +import { getClient } from '@core/profile-manager/api/getClient' +import { EMPTY_HEX_ID } from '@core/wallet' +import { + AddressType, + AddressUnlockCondition, + BasicOutputBuilderParams, + Ed25519Address, + UnlockConditionType, +} from '@iota/sdk/out/types' +import { plainToInstance } from 'class-transformer' + +const MOCK_BASIC_OUTPUT_AMOUNT = '10' + +/** + * Calculate minimum storage deposit required for the most minimal basic output with a single address unlock condition. + * @returns The minimum storage deposit. + */ +export async function getRequiredStorageDepositForMinimalBasicOutput(): Promise { + const address = { + type: AddressType.Ed25519, + pubKeyHash: EMPTY_HEX_ID, + } + const ed25519Address = plainToInstance(Ed25519Address, address) + + const unlockCondition = { + type: UnlockConditionType.Address, + address: ed25519Address, + } + const addressUnlockCondition = plainToInstance(AddressUnlockCondition, unlockCondition) + + const params: BasicOutputBuilderParams = { + amount: MOCK_BASIC_OUTPUT_AMOUNT, + unlockConditions: [addressUnlockCondition], + } + + const client = await getClient() + const basicOutput = await client.buildBasicOutput(params) + const minimumRequiredStorageDeposit = await client.minimumRequiredStorageDeposit(basicOutput) + + return minimumRequiredStorageDeposit +} diff --git a/packages/shared/lib/core/wallet/utils/outputs/index.ts b/packages/shared/lib/core/wallet/utils/outputs/index.ts index 3cf0d8ba021..3bfdcf7b34d 100644 --- a/packages/shared/lib/core/wallet/utils/outputs/index.ts +++ b/packages/shared/lib/core/wallet/utils/outputs/index.ts @@ -15,3 +15,4 @@ export * from './isOutputAsync' export * from './preprocessGroupedOutputs' export * from './preprocessTransaction' export * from './getOutputIdFromTransactionIdAndIndex' +export * from './getRequiredStorageDepositForMinimalBasicOutput' diff --git a/packages/shared/locales/en.json b/packages/shared/locales/en.json index d967df9f22e..3909730d608 100644 --- a/packages/shared/locales/en.json +++ b/packages/shared/locales/en.json @@ -1874,7 +1874,7 @@ "cannotClaimTwice": "Output has been already claimed", "noToAccount": "You have not selected a wallet to send the funds to.", "sendingDust": "You cannot send less than 1 Mi.", - "leavingDust": "You cannot leave less than 1 Mi on your address.", + "leavingDust": "You cannot leave less than the minimum required storage deposit ({minRequiredStorageDeposit}) on your address.", "cancelled": "The transaction was cancelled.", "transaction": "There was an error sending your transaction. Please try again.", "invalidExpirationDateTime": "The chosen expiration date/time is invalid.",