diff --git a/src/components/send/SendForm.schema.ts b/src/components/send/SendForm.schema.ts index 12436c31..eaddcf9a 100644 --- a/src/components/send/SendForm.schema.ts +++ b/src/components/send/SendForm.schema.ts @@ -1,10 +1,12 @@ import { getAddressInfo, validate as isValidBitcoinAddress, Network } from 'bitcoin-address-validation' import type { TFunction } from 'i18next' import * as yup from 'yup' +import { TOTAL_COIN_SUPPLY } from '@/constants/jam' import type { AddressSummary, Jar } from '@/context/JamWalletInfoContext' import { TX_FEE_UNITS } from '@/lib/feeConfig' import type { JamFeeConfigValues } from '@/lib/feeConfig' import { pseudoRandomInteger } from '@/lib/utils' +import type { AmountSats } from '@/types/global' import { toTxFeeFormDefaultValues, createTxFeeFormSchema } from './TxFeeForm.schema' import type { SendFormValues } from './types' @@ -18,6 +20,10 @@ export const initialNumberOfCollaborators = (minValue: number): number => { const MAX_NUM_COLLABORATORS = 99 +// min amount joinmarket is able to send (without errorsing, last check with v0.9.11 on 2026-04-26) +export const MIN_SEND_AMOUNT: AmountSats = 292 +export const MAX_SEND_AMOUNT: AmountSats = TOTAL_COIN_SUPPLY + const FORM_INPUT_DEFAULT_VALUES: Partial = { source: undefined, destination: undefined, @@ -96,12 +102,7 @@ export const createSendFormSchema = ( isSweep: yup.boolean().default(false).required(), sweepAmount: yup.number().when('isSweep', { is: (val: boolean) => val === true, - then: (schema) => - schema - .integer() - .min(1) - .max(21_000_000 * 100_000_000) - .required(), + then: (schema) => schema.integer().min(1).max(MAX_SEND_AMOUNT).required(), otherwise: (schema) => schema .transform(() => null) @@ -120,8 +121,8 @@ export const createSendFormSchema = ( .integer(t('send.feedback_invalid_amount')) .transform((value) => (Number.isSafeInteger(value) ? Number(value) : null)) .nonNullable(t('send.feedback_invalid_amount')) - .min(1, t('send.feedback_invalid_amount')) - .max(21_000_000 * 100_000_000, t('send.feedback_invalid_amount')) + .min(MIN_SEND_AMOUNT, t('send.feedback_invalid_amount')) + .max(MAX_SEND_AMOUNT, t('send.feedback_invalid_amount')) .required(t('send.feedback_invalid_amount')), }), }) diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx index 49590011..9952808a 100644 --- a/src/components/send/SendForm.tsx +++ b/src/components/send/SendForm.tsx @@ -37,7 +37,13 @@ import { Spinner } from '../ui/spinner' import { Switch } from '../ui/switch' import JarSelectorDialog from './JarSelectorDialog' import { SendCoinjoinPreconditionAlert } from './SendCoinjoinPreconditionAlert' -import { createSendFormSchema, initialNumberOfCollaborators, toSendFormDefaultValues } from './SendForm.schema' +import { + createSendFormSchema, + initialNumberOfCollaborators, + MAX_SEND_AMOUNT, + MIN_SEND_AMOUNT, + toSendFormDefaultValues, +} from './SendForm.schema' import { TxFeeForm } from './TxFeeForm' import { estimateMaxCollaboratorFee } from './feeEstimate' import type { SendFormValues } from './types' @@ -451,6 +457,8 @@ export function SendForm({ required: false, disabled, })} + min={MIN_SEND_AMOUNT} + max={MAX_SEND_AMOUNT} type="number" className="h-auto pl-9" placeholder={t('send.placeholder_amount_input')} diff --git a/src/constants/jam.ts b/src/constants/jam.ts index 0d9bdf25..15c30c30 100644 --- a/src/constants/jam.ts +++ b/src/constants/jam.ts @@ -106,3 +106,27 @@ export const JAM_JM_WEBSOCKET_RECONNECT_INTERVAL_MAX: Milliseconds = Math.max( * the `/display` endpoint taking more than ~15s. */ export const GAPLIMIT_WARN_THRESHOLD = 250 + +export function totalCoinSupply(): AmountSats { + const SATS_PER_COIN = 100_000_000 + const initialSubsidy = 50 * SATS_PER_COIN + const blockPerHalving = 210_000 + + let subsidy = initialSubsidy + let total = 0 + let halving = 0 + + // Continue until subsidy becomes zero + while (subsidy > 0n) { + total += blockPerHalving * subsidy + // Prepare next halving: integer division by 2 (floor) + subsidy = subsidy / 2 + halving += 1 + // Safety: break if loop would be infinite (not needed here but good practice) + if (halving > 1000) break + } + + return total +} + +export const TOTAL_COIN_SUPPLY: AmountSats = totalCoinSupply()