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:
+73
-16
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user