fix(app): validate contact addresses when saving
Adding a contact now checks the address against the wallet's network and rejects duplicates; whitespace in a pasted address is removed. Before, any string was stored and the error only surfaced at send time.
This commit is contained in:
1 parent
06d1f59c2b
commit
eca4d4ac0c
5 files changed
+117
-5
No files matched your search
@@ -17,6 +17,10 @@ release and covers the full history from the initial commit.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Adding a contact now validates the address for the wallet's network and rejects duplicates
|
||||
(case-insensitive); whitespace and line breaks in a pasted address are removed. Before, any
|
||||
string was saved and the error only appeared at send time. Contacts already stored with an
|
||||
invalid address still load and display.
|
||||
- The wallet now verifies the chain identity of a server right after connecting: the
|
||||
`genesis_hash` from `server.features` must match the profile and, on mainnet (which shares
|
||||
Bitcoin's genesis), the header at the highest checkpoint not above the server's tip must match
|
||||
|
||||
+5
-3
@@ -508,9 +508,11 @@ them is normal.
|
||||
|
||||
A simple per-wallet address book, on its own tab:
|
||||
|
||||
- **Add**: name + address, both required (whitespace is trimmed). Note: the address format
|
||||
is **not validated when saving** — it is only validated when you actually send to it. Copy
|
||||
addresses carefully.
|
||||
- **Add**: name + address, both required. The address is **validated when saving** against
|
||||
the wallet's network: an invalid or wrong-network address, or one already in the list, is
|
||||
refused with a message. Spaces and line breaks in a pasted address are removed. Contacts
|
||||
saved by an older version without validation are still shown; the send form validates the
|
||||
destination again when you use them.
|
||||
- **Remove selected**: deletes the highlighted contact. There is no edit function — remove
|
||||
and re-add.
|
||||
- Contacts are used by the **From contacts** dropdown on the Send tab.
|
||||
|
||||
@@ -477,6 +477,22 @@ public sealed class Loc
|
||||
"服务器 {0} 不在 Palladium 链上(创世块或检查点不同)。请选择其他服务器。"],
|
||||
|
||||
// Contacts
|
||||
["msg.contact.address.invalid"] = [
|
||||
"Indirizzo non valido per questa rete: il contatto non è stato salvato.",
|
||||
"Invalid address for this network: the contact was not saved.",
|
||||
"Dirección no válida para esta red: el contacto no se guardó.",
|
||||
"Adresse invalide pour ce réseau : le contact n'a pas été enregistré.",
|
||||
"Endereço inválido para esta rede: o contato não foi salvo.",
|
||||
"Ungültige Adresse für dieses Netzwerk: Der Kontakt wurde nicht gespeichert.",
|
||||
"该网络的地址无效:联系人未保存。"],
|
||||
["msg.contact.duplicate"] = [
|
||||
"Esiste già un contatto con questo indirizzo.",
|
||||
"A contact with this address already exists.",
|
||||
"Ya existe un contacto con esta dirección.",
|
||||
"Un contact avec cette adresse existe déjà.",
|
||||
"Já existe um contato com este endereço.",
|
||||
"Ein Kontakt mit dieser Adresse existiert bereits.",
|
||||
"已存在相同地址的联系人。"],
|
||||
["contacts.name"] = ["Nome", "Name", "Nombre", "Nom", "Nome", "Name", "姓名"],
|
||||
["contacts.address"] = ["Indirizzo", "Address", "Dirección", "Adresse", "Endereço", "Adresse", "地址"],
|
||||
["contacts.name.ph"] = ["Nome contatto", "Contact name", "Nombre del contacto", "Nom du contact", "Nome do contato", "Kontaktname", "联系人姓名"],
|
||||
|
||||
@@ -2,6 +2,9 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NBitcoin;
|
||||
using PalladiumWallet.App.Localization;
|
||||
using PalladiumWallet.Core.Chain;
|
||||
using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
@@ -33,8 +36,23 @@ public partial class MainWindowViewModel
|
||||
private void AddContact()
|
||||
{
|
||||
var name = NewContactName.Trim();
|
||||
var addr = NewContactAddress.Trim();
|
||||
// Pasted addresses often carry spaces or line breaks anywhere: drop all whitespace.
|
||||
var addr = string.Concat(NewContactAddress.Where(c => !char.IsWhiteSpace(c)));
|
||||
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(addr)) return;
|
||||
try
|
||||
{
|
||||
BitcoinAddress.Create(addr, PalladiumNetworks.For(Net));
|
||||
}
|
||||
catch (System.FormatException)
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.contact.address.invalid");
|
||||
return;
|
||||
}
|
||||
if (Contacts.Any(c => string.Equals(c.Address, addr, System.StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.contact.duplicate");
|
||||
return;
|
||||
}
|
||||
Contacts.Add(new ContactEntry(name, addr));
|
||||
NewContactName = NewContactAddress = "";
|
||||
PersistContacts();
|
||||
|
||||
@@ -291,7 +291,8 @@ public sealed class MainWindowViewModelTests : VmTestBase
|
||||
var vm = new MainWindowViewModel();
|
||||
RestoreFlow(vm, Mnemonic, null, "c");
|
||||
|
||||
vm.NewContactName = "Alice"; vm.NewContactAddress = "plm1qexample";
|
||||
var alice = Address(1);
|
||||
vm.NewContactName = "Alice"; vm.NewContactAddress = alice;
|
||||
vm.AddContactCommand.Execute(null);
|
||||
vm.NewContactName = ""; vm.NewContactAddress = "ignored";
|
||||
vm.AddContactCommand.Execute(null); // blank name → ignored
|
||||
@@ -306,6 +307,77 @@ public sealed class MainWindowViewModelTests : VmTestBase
|
||||
Assert.Empty(WalletStore.Load(Assert.Single(AppPaths.WalletFiles(NetKind.Mainnet))).Contacts);
|
||||
});
|
||||
|
||||
private static string Address(int index) =>
|
||||
HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Mainnet)
|
||||
.GetReceiveAddress(index).ToString();
|
||||
|
||||
[Theory]
|
||||
[InlineData("plm1qexample")] // garbage
|
||||
[InlineData("not an address")]
|
||||
[InlineData("tplm1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx")] // wrong network / bad checksum
|
||||
public Task Contacts_RejectInvalidAddresses(string bad) => UiSession.Run(() =>
|
||||
{
|
||||
var vm = new MainWindowViewModel();
|
||||
RestoreFlow(vm, Mnemonic, null, "c1");
|
||||
|
||||
vm.NewContactName = "Bob"; vm.NewContactAddress = bad;
|
||||
vm.AddContactCommand.Execute(null);
|
||||
|
||||
Assert.Empty(vm.Contacts);
|
||||
Assert.Equal(Loc.Tr("msg.contact.address.invalid"), vm.StatusMessage);
|
||||
Assert.Empty(WalletStore.Load(Assert.Single(AppPaths.WalletFiles(NetKind.Mainnet))).Contacts);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task Contacts_RejectAnAddressFromAnotherNetwork() => UiSession.Run(() =>
|
||||
{
|
||||
var vm = new MainWindowViewModel();
|
||||
RestoreFlow(vm, Mnemonic, null, "c2");
|
||||
var regtest = HdAccount.FromMnemonic(ParseMnemonic(), null, ScriptKind.NativeSegwit, ChainProfiles.Regtest)
|
||||
.GetReceiveAddress(0).ToString();
|
||||
|
||||
vm.NewContactName = "Eve"; vm.NewContactAddress = regtest;
|
||||
vm.AddContactCommand.Execute(null);
|
||||
|
||||
Assert.Empty(vm.Contacts);
|
||||
Assert.Equal(Loc.Tr("msg.contact.address.invalid"), vm.StatusMessage);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task Contacts_RejectDuplicatesAndCleanPastedWhitespace() => UiSession.Run(() =>
|
||||
{
|
||||
var vm = new MainWindowViewModel();
|
||||
RestoreFlow(vm, Mnemonic, null, "c3");
|
||||
var a = Address(2);
|
||||
|
||||
vm.NewContactName = " Carol "; vm.NewContactAddress = $" {a[..10]}\n{a[10..]}\r\n";
|
||||
vm.AddContactCommand.Execute(null);
|
||||
Assert.Equal(new ContactEntry("Carol", a), Assert.Single(vm.Contacts));
|
||||
|
||||
vm.NewContactName = "Carol again"; vm.NewContactAddress = a.ToUpperInvariant();
|
||||
vm.AddContactCommand.Execute(null);
|
||||
Assert.Single(vm.Contacts);
|
||||
Assert.Equal(Loc.Tr("msg.contact.duplicate"), vm.StatusMessage);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task Contacts_AlreadyStoredWithAnInvalidAddress_StillLoadAndDisplay() => UiSession.Run(() =>
|
||||
{
|
||||
var vm = new MainWindowViewModel();
|
||||
RestoreFlow(vm, Mnemonic, null, "c4");
|
||||
var path = Assert.Single(AppPaths.WalletFiles(NetKind.Mainnet));
|
||||
var doc = WalletStore.Load(path);
|
||||
doc.Contacts = [new StoredContact { Name = "Legacy", Address = "whatever" }];
|
||||
WalletStore.Save(doc, path, null);
|
||||
vm.CloseWalletCommand.Execute(null);
|
||||
|
||||
vm.WizardStartOpenCommand.Execute(null);
|
||||
vm.OpenExistingCommand.Execute(null);
|
||||
|
||||
Assert.True(vm.IsWalletOpen);
|
||||
Assert.Equal(new ContactEntry("Legacy", "whatever"), Assert.Single(vm.Contacts));
|
||||
});
|
||||
|
||||
// ---- sync + send against the fake server ----
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in new issue
Block a user