Build 78: honest transport labels, request decline/cancel, NIP-05 on requests, full localization

Settings now says "Manual transaction" and the privacy row reads "Messages &
lookups" opening a new Network privacy page that tells the truth: messages,
names, price and avatars ride the Nym mixnet; the grin node connects directly.
README and lander updated to match.

Requests are messages, payments are final: declining a request now sends the
requester a void control message (NIP-17), a requester can cancel a request they
sent (cancels the local invoice and notifies the payer), and incoming requests
resolve the sender's verified @username instead of a bare npub. The Requested
amount on the success screen is centered. New NostrDecline/NostrCancel tasks and
a goblin-action control message carry it, bound to the stored counterparty.

Localization: every Goblin-screen string moved to t!() keys (370 keys) and
translated into de/fr/ru/tr/zh-CN, guarded by a key/placeholder drift test.
System-locale auto-detect now matches region locales like zh-CN.
This commit is contained in:
2ro
2026-06-14 21:44:24 -04:00
parent 0644807f51
commit f0b854171c
19 changed files with 3610 additions and 578 deletions
+8
View File
@@ -450,4 +450,12 @@ pub enum WalletTask {
/// Republish our kind-0 profile (e.g. after toggling the incoming-requests
/// preference) so the change propagates to relays immediately.
NostrRepublishProfile,
/// Decline an incoming payment request: mark it declined and send the
/// requester a void control message so their side clears too.
/// * request id (rumor event id hex)
NostrDeclineRequest(String),
/// Cancel a request WE sent: cancel the local invoice tx and send the payer a
/// void control message so the pending card disappears on their side.
/// * slate id (uuid string)
NostrCancelOutgoing(String),
}
+53
View File
@@ -2364,6 +2364,59 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
service.republish_identity().await;
}
}
WalletTask::NostrDeclineRequest(rumor_id) => {
let Some(service) = w.nostr_service() else {
return;
};
let Some(mut request) = service.store.request(rumor_id) else {
error!("nostr decline: request not found");
return;
};
// Mark declined locally (idempotent) so the card stays gone, then tell
// the requester. Requests are messages; payments are final.
request.status = crate::nostr::RequestStatus::Declined;
service.store.save_request(&request);
if let Err(e) = service
.send_control_dm(&request.npub, &request.slate_id, &[])
.await
{
error!("nostr decline: control dispatch failed: {e}");
}
}
WalletTask::NostrCancelOutgoing(slate_id) => {
let Some(service) = w.nostr_service() else {
return;
};
let Some(meta) = service.store.tx_meta(slate_id) else {
error!("nostr cancel: no metadata for slate {slate_id}");
return;
};
if meta.direction != crate::nostr::NostrTxDirection::RequestedByUs {
error!("nostr cancel: slate {slate_id} is not an outgoing request");
return;
}
// Cancel the underlying grin invoice tx (an issued invoice locks no
// outputs, but cancelling keeps the wallet ledger tidy).
if let Some(tx_id) = w.get_data().and_then(|d| d.txs).and_then(|txs| {
txs.iter()
.find(|t| {
t.data.tx_slate_id.map(|u| u.to_string()).as_deref()
== Some(slate_id.as_str())
})
.map(|t| t.data.id)
}) {
if let Err(e) = w.cancel(tx_id) {
error!("nostr cancel: wallet cancel failed: {e}");
}
}
service
.store
.update_tx_status(slate_id, crate::nostr::NostrSendStatus::Cancelled);
if let Err(e) = service.send_control_dm(&meta.npub, slate_id, &[]).await {
error!("nostr cancel: control dispatch failed: {e}");
}
sync_wallet_data(&w, false);
}
WalletTask::NostrResend(id) => {
let Some(service) = w.nostr_service() else {
return;