From 008e9c395a3c59009f755369917b5b3f5acc0e29 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sat, 13 Jun 2026 20:39:05 +0200 Subject: [PATCH] fix(wallet): reject amounts with sub-satoshi precision Replace the silent (long) cast with an explicit fractional check: if value * factor has a non-zero remainder, TryParseIn/TryParseCoins return false. The caller already shows "Importo non valido." to the user, so no UI change is needed. Covers TryParseIn (all units) and TryParseCoins. Adds CoinAmountTests with valid and invalid cases including the triggering example (1.9 sat). --- src/Core/Wallet/CoinAmount.cs | 10 +++- .../Wallet/CoinAmountTests.cs | 56 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 tests/PalladiumWallet.Tests/Wallet/CoinAmountTests.cs diff --git a/src/Core/Wallet/CoinAmount.cs b/src/Core/Wallet/CoinAmount.cs index 598da2d..8f98d3a 100644 --- a/src/Core/Wallet/CoinAmount.cs +++ b/src/Core/Wallet/CoinAmount.cs @@ -46,7 +46,10 @@ public static class CoinAmount return false; try { - sats = (long)(value * factor); + var satsDecimal = value * factor; + if (satsDecimal % 1 != 0) + return false; + sats = (long)satsDecimal; } catch (OverflowException) { @@ -65,7 +68,10 @@ public static class CoinAmount return false; try { - sats = (long)(coins * SatsPerCoin); + var satsDecimal = coins * SatsPerCoin; + if (satsDecimal % 1 != 0) + return false; + sats = (long)satsDecimal; } catch (OverflowException) { diff --git a/tests/PalladiumWallet.Tests/Wallet/CoinAmountTests.cs b/tests/PalladiumWallet.Tests/Wallet/CoinAmountTests.cs new file mode 100644 index 0000000..b7b84f7 --- /dev/null +++ b/tests/PalladiumWallet.Tests/Wallet/CoinAmountTests.cs @@ -0,0 +1,56 @@ +using PalladiumWallet.Core.Wallet; + +namespace PalladiumWallet.Tests.Wallet; + +public class CoinAmountTests +{ + // --- importi validi --- + + [Theory] + [InlineData("1", "sat", 1)] + [InlineData("0", "sat", 0)] + [InlineData("1.5", "PLM", 150_000_000)] + [InlineData("0.00000001", "PLM", 1)] + [InlineData("1", "PLM", 100_000_000)] + [InlineData("1.00000", "mPLM", 100_000)] + [InlineData("1.00", "µPLM", 100)] + public void Importo_valido_viene_accettato(string input, string unit, long expectedSats) + { + Assert.True(CoinAmount.TryParseIn(input, unit, out var sats)); + Assert.Equal(expectedSats, sats); + } + + // --- importi con troppi decimali: devono essere rifiutati --- + + [Theory] + [InlineData("1.9", "sat")] // sat non è divisibile + [InlineData("0.001", "µPLM")] // 0.001 × 100 = 0.1 sat + [InlineData("1.500000001", "PLM")] // 9 decimali, uno di troppo + [InlineData("0.000000001", "PLM")] // sotto al satoshi + [InlineData("1.1", "sat")] + public void Importo_con_troppi_decimali_viene_rifiutato(string input, string unit) + { + Assert.False(CoinAmount.TryParseIn(input, unit, out _)); + } + + // --- TryParseCoins --- + + [Fact] + public void TryParseCoins_accetta_precisione_massima() + { + Assert.True(CoinAmount.TryParseCoins("0.00000001", out var sats)); + Assert.Equal(1L, sats); + } + + [Fact] + public void TryParseCoins_rifiuta_sotto_al_satoshi() + { + Assert.False(CoinAmount.TryParseCoins("0.000000001", out _)); + } + + [Fact] + public void TryParseCoins_rifiuta_importo_negativo() + { + Assert.False(CoinAmount.TryParseCoins("-1", out _)); + } +}