1
0
forked from GRIN/grim

wallet: contacts on send, request-approve states, .backup create/import

- Create/refresh a contact when you SEND (not just receive) so people you
  pay show up under Suggested and resolve their @name.
- Approve-request: set SENT on the Standard1 success path and fail_send on
  the error + non-payable paths, so the button never sticks greyed.
- create_nostr_backup() + import of the encrypted .backup envelope.
This commit is contained in:
2ro
2026-06-16 00:31:50 -04:00
parent 222f149fc2
commit 313a14b82c
+73 -16
View File
@@ -558,16 +558,37 @@ impl Wallet {
old.unlock(&password)
.map_err(|_| "Wrong password".to_string())?;
let input = input.trim();
let (mut new_identity, new_keys) = if input.starts_with('{') {
// Identity backup JSON: decrypt with the password it was exported
// under (may differ on a new device), then RE-ENCRYPT under this
// wallet's password so future unlocks use the current one.
let bpw = backup_password
.as_deref()
.filter(|s| !s.is_empty())
.unwrap_or(&password);
let (mut new_identity, new_keys) = if NostrIdentity::is_encrypted_backup(input) {
// A GOBLIN-*.backup file: fully sealed. Open it with the password it
// was created under (may differ on a new device), then RE-ENCRYPT
// under this wallet's password so future unlocks use the current one.
let (backup, keys) =
NostrIdentity::from_encrypted_backup(input, bpw).map_err(|_| {
"Couldn't open the backup — wrong password? If it was made on \
another device, enter that wallet's password in the \
backup-password field."
.to_string()
})?;
let mut ident = NostrIdentity::from_unlocked_keys(
&keys,
&password,
backup.source,
backup.derivation_account,
)
.map_err(|e| format!("re-encryption failed: {e}"))?;
ident.nip05 = backup.nip05.clone();
ident.anonymous = backup.anonymous;
ident.prev_npubs = backup.prev_npubs.clone();
(ident, keys)
} else if input.starts_with('{') {
// Legacy plaintext identity-backup JSON (pre-.backup-file): decrypt
// with its password, then re-encrypt under this wallet's password.
let backup: NostrIdentity =
serde_json::from_str(input).map_err(|_| "Invalid identity backup".to_string())?;
let bpw = backup_password
.as_deref()
.filter(|s| !s.is_empty())
.unwrap_or(&password);
let keys = backup.unlock(bpw).map_err(|_| {
"Couldn't decrypt the backup — if it was exported on another \
device, enter that wallet's password in the backup-password \
@@ -618,6 +639,21 @@ impl Wallet {
Ok(new_npub)
}
/// Build the contents of a `GOBLIN-*.backup` file: the whole nostr identity,
/// fully sealed under the wallet password. Verifies the password first.
pub fn create_nostr_backup(&self, password: &str) -> Result<String, String> {
let svc = self
.nostr_service()
.ok_or_else(|| "nostr is not running".to_string())?;
let identity = svc.identity.read().clone();
let keys = identity
.unlock(password)
.map_err(|_| "Wrong password".to_string())?;
identity
.to_encrypted_backup(&keys)
.map_err(|e| format!("backup failed: {e}"))
}
/// Get keychain mask [`SecretKey`].
pub fn keychain_mask(&self) -> Option<SecretKey> {
let r_key = self.keychain_mask.read();
@@ -2314,14 +2350,32 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
meta.updated_at = crate::nostr::unix_time();
service.store.save_tx_meta(&meta);
}
// Update contact last paid time.
if let Some(mut contact) = service.store.contact(receiver) {
contact.last_paid_at = Some(crate::nostr::unix_time());
contact.unknown = false;
service.store.save_contact(&contact);
}
// Resolve the recipient's @username so activity shows
// their name, not a bare npub.
// Record/refresh the contact so someone you PAY shows up
// under Suggested (sends used to create no contact — only
// incoming payments did — so a person you paid first never
// appeared). Create on first pay, then stamp last_paid_at.
let mut contact =
service.store.contact(receiver).unwrap_or_else(|| {
crate::nostr::Contact {
ver: 1,
npub: receiver.clone(),
petname: None,
nip05: None,
nip05_verified_at: None,
relays: relay_hints.clone(),
hue: crate::gui::views::goblin::data::hue_of(receiver)
as u8,
unknown: true,
added_at: crate::nostr::unix_time(),
last_paid_at: None,
blocked: false,
}
});
contact.last_paid_at = Some(crate::nostr::unix_time());
contact.unknown = false;
service.store.save_contact(&contact);
// Resolve the recipient's @username so activity + Suggested
// show their name, not a bare npub.
service.resolve_contact_identity(receiver);
service.set_send_phase(crate::nostr::send_phase::SENT);
}
@@ -2695,15 +2749,18 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
}
request.status = crate::nostr::RequestStatus::Approved;
service.store.save_request(&request);
service.set_send_phase(crate::nostr::send_phase::SENT);
sync_wallet_data(&w, false);
}
Err(e) => {
error!("nostr accept failed: {:?}", e);
service.fail_send(friendly_send_error(&e));
}
}
}
_ => {
error!("nostr pay: stored slatepack is not payable");
service.fail_send("This request is no longer payable.".to_string());
request.status = crate::nostr::RequestStatus::Expired;
service.store.save_request(&request);
}