Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 008e9c395a | |||
| 70cce640aa | |||
| 0f8a764a44 | |||
| d66490b6be |
@@ -36,8 +36,8 @@ by `MainWindow` on desktop and as the single-view root on Android.
|
||||
|
||||
## Commands
|
||||
|
||||
.NET 10 SDK lives in `~/.dotnet`: in non-interactive shells, before any `dotnet` command run
|
||||
`export PATH="$HOME/.dotnet:$PATH" DOTNET_ROOT="$HOME/.dotnet"`.
|
||||
.NET 10 SDK lives in `~/.dotnet10`: in non-interactive shells, before any `dotnet` command run
|
||||
`export PATH="$HOME/.dotnet10:$PATH" DOTNET_ROOT="$HOME/.dotnet10"`.
|
||||
|
||||
- Build: `dotnet build`
|
||||
- Tests (headless, the primary verification layer): `dotnet test` — single: `dotnet test --filter "FullyQualifiedName~TestName"`
|
||||
|
||||
@@ -281,6 +281,8 @@ public sealed class Loc
|
||||
"As duas senhas não coincidem.",
|
||||
"Die beiden Passwörter stimmen nicht überein."],
|
||||
["msg.wrongpassword"] = ["Password errata.", "Wrong password.", "Contraseña incorrecta.", "Mot de passe incorrect.", "Senha incorreta.", "Falsches Passwort."],
|
||||
["msg.wallet.locked"] = ["Wallet già aperto in un'altra istanza dell'applicazione.", "Wallet already open in another instance of the application.", "El wallet ya está abierto en otra instancia de la aplicación.", "Le wallet est déjà ouvert dans une autre instance de l'application.", "A carteira já está aberta em outra instância do aplicativo.", "Wallet ist bereits in einer anderen Instanz der Anwendung geöffnet."],
|
||||
["msg.wallet.noaccess"] = ["Impossibile accedere al file del wallet: verificare i permessi.", "Cannot access the wallet file: check file permissions.", "No se puede acceder al archivo del wallet: verifique los permisos.", "Impossible d'accéder au fichier du wallet : vérifiez les autorisations.", "Não é possível acessar o arquivo da carteira: verifique as permissões.", "Zugriff auf die Wallet-Datei nicht möglich: Berechtigungen prüfen."],
|
||||
["msg.opened"] = ["Wallet aperto: connessione al server…", "Wallet opened: connecting to server…", "Wallet abierto: conectando al servidor…", "Wallet ouvert : connexion au serveur…", "Carteira aberta: conectando ao servidor…", "Wallet geöffnet: Verbindung zum Server…"],
|
||||
["msg.synced"] = ["Sincronizzato", "Synchronized", "Sincronizado", "Synchronisé", "Sincronizado", "Synchronisiert"],
|
||||
["msg.synced.detail"] = [
|
||||
|
||||
@@ -57,6 +57,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
private HdAccount? _account;
|
||||
private string? _walletPath;
|
||||
private string? _password;
|
||||
private WalletLock? _walletLock;
|
||||
private ElectrumClient? _client;
|
||||
private WalletSynchronizer? _synchronizer;
|
||||
private IReadOnlyDictionary<string, Transaction>? _lastTransactions;
|
||||
@@ -896,6 +897,10 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.wrongpassword");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.wallet.noaccess");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Errore: {ex.Message}";
|
||||
@@ -923,6 +928,10 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
SetupStep = StepOpen;
|
||||
StatusMessage = "";
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.wallet.noaccess");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Errore: {ex.Message}";
|
||||
@@ -939,8 +948,24 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
private bool TryAcquireWalletLock(string path)
|
||||
{
|
||||
var acquired = WalletLock.TryAcquire(path);
|
||||
if (acquired is null)
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.wallet.locked");
|
||||
return false;
|
||||
}
|
||||
_walletLock?.Dispose();
|
||||
_walletLock = acquired;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OpenLoaded(WalletDocument doc, HdAccount account, string path, string? password)
|
||||
{
|
||||
if (!TryAcquireWalletLock(path))
|
||||
return;
|
||||
|
||||
// La rete del wallet comanda (registry, pin TLS, indirizzi).
|
||||
SelectedNetwork = doc.Network;
|
||||
_doc = doc;
|
||||
@@ -1246,6 +1271,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void CloseWallet()
|
||||
{
|
||||
_walletLock?.Dispose();
|
||||
_walletLock = null;
|
||||
_ = _client?.DisposeAsync().AsTask();
|
||||
_client = null;
|
||||
_synchronizer = null;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Exclusive lock on a wallet file held for the lifetime of the session.
|
||||
/// The real lock is the open FileStream with FileShare.None — the .lock file
|
||||
/// is just its vessel. The OS releases it automatically on process exit or crash.
|
||||
/// </summary>
|
||||
public sealed class WalletLock : IDisposable
|
||||
{
|
||||
private readonly string _lockPath;
|
||||
private FileStream? _stream;
|
||||
|
||||
private WalletLock(string lockPath, FileStream stream)
|
||||
{
|
||||
_lockPath = lockPath;
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to acquire an exclusive lock for <paramref name="walletPath"/>.
|
||||
/// Returns null if another process already holds the lock (IOException).
|
||||
/// Lets UnauthorizedAccessException propagate so callers can show a distinct message.
|
||||
/// </summary>
|
||||
public static WalletLock? TryAcquire(string walletPath)
|
||||
{
|
||||
var lockPath = walletPath + ".lock";
|
||||
try
|
||||
{
|
||||
var stream = new FileStream(
|
||||
lockPath,
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.ReadWrite,
|
||||
FileShare.None);
|
||||
return new WalletLock(lockPath, stream);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
try { File.Delete(_lockPath); } catch { }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -92,4 +92,59 @@ public class StorageTests
|
||||
doc.Mnemonic = null;
|
||||
Assert.True(WalletDocument.FromJson(doc.ToJson()).IsWatchOnly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalletLock_acquisisce_e_rilascia()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"plm-lock-{Guid.NewGuid()}.wallet.json");
|
||||
try
|
||||
{
|
||||
using var lock1 = WalletLock.TryAcquire(path);
|
||||
Assert.NotNull(lock1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
File.Delete(path + ".lock");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalletLock_seconda_istanza_restituisce_null()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"plm-lock-{Guid.NewGuid()}.wallet.json");
|
||||
try
|
||||
{
|
||||
using var lock1 = WalletLock.TryAcquire(path);
|
||||
Assert.NotNull(lock1);
|
||||
|
||||
var lock2 = WalletLock.TryAcquire(path);
|
||||
Assert.Null(lock2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
File.Delete(path + ".lock");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalletLock_riacquisibile_dopo_rilascio()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"plm-lock-{Guid.NewGuid()}.wallet.json");
|
||||
try
|
||||
{
|
||||
var lock1 = WalletLock.TryAcquire(path);
|
||||
Assert.NotNull(lock1);
|
||||
lock1!.Dispose();
|
||||
|
||||
using var lock2 = WalletLock.TryAcquire(path);
|
||||
Assert.NotNull(lock2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
File.Delete(path + ".lock");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 _));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user