1
0
forked from GRIN/grim

goblin: remove the rotate-nostr-identity feature

Rotation just does Keys::generate (IdentitySource::Random) — the old
seed-derived path was dropped in Build 8 — so it is functionally a random
add-identity-and-switch, and it does NOT carry the username or profile
over (it RELEASES the old name and hands the new key a nameless identity).
It therefore adds nothing over the identity switcher, so remove it: the
Settings entry row, the inline rotate flow (RotateState + rotate_ui) and
the now-unreachable Wallet::rotate_nostr_identity. Shared helpers
(unlock_all_identities, create_random, nip05::unregister) are untouched.
Drops the fifteen unused goblin.settings.rotate_* / key_rotated /
new_npub / backup_new_key / copy_new_nsec / rotation_failed keys from all
eight locales; drift + call-site tests green.
This commit is contained in:
2ro
2026-07-07 00:37:49 -04:00
parent 44f9363ecf
commit 811eff825c
10 changed files with 3 additions and 452 deletions
-269
View File
@@ -75,8 +75,6 @@ pub struct GoblinWalletView {
wallet_id: Option<String>,
/// Inline username-claim state for the Me tab.
claim: Option<ClaimState>,
/// Inline key-rotation state for the Me tab.
rotate: Option<RotateState>,
/// Inline nsec-import state for the Me tab.
import_nsec: Option<ImportState>,
/// Inline "back up identity to a file" flow state.
@@ -300,7 +298,6 @@ impl Default for GoblinWalletView {
request_error: None,
wallet_id: None,
claim: None,
rotate: None,
import_nsec: None,
backup: None,
identity_switch: IdentitySwitchState::default(),
@@ -340,31 +337,6 @@ impl Default for GoblinWalletView {
}
}
/// Inline key-rotation flow state (two warnings + typed confirmation).
struct RotateState {
/// 1 = warning, 2 = confirm (RESET + password), 3 = working,
/// 4 = done (new npub), 5 = error (message).
stage: u8,
reset_input: String,
password: String,
new_npub: String,
error: String,
result: std::sync::Arc<std::sync::Mutex<Option<Result<String, String>>>>,
}
impl Default for RotateState {
fn default() -> Self {
Self {
stage: 1,
reset_input: String::new(),
password: String::new(),
new_npub: String::new(),
error: String::new(),
result: std::sync::Arc::new(std::sync::Mutex::new(None)),
}
}
}
/// Inline nsec-import flow state (restore path for the random-key model).
struct ImportState {
/// 1 = form, 3 = working, 4 = done, 5 = error.
@@ -816,7 +788,6 @@ impl GoblinWalletView {
self.receipt = None;
self.profile = None;
self.claim = None;
self.rotate = None;
self.import_nsec = None;
self.approve_review = None;
self.approve_hold = w::HoldToSend::default();
@@ -3666,14 +3637,6 @@ impl GoblinWalletView {
{
self.backup = Some(BackupState::default());
}
if settings_row_danger(
ui,
&t!("goblin.settings.rotate_key"),
crate::gui::icons::ARROWS_CLOCKWISE,
) && self.rotate.is_none()
{
self.rotate = Some(RotateState::default());
}
if settings_row_btn(
ui,
&t!("goblin.settings.import_identity"),
@@ -3754,10 +3717,6 @@ impl GoblinWalletView {
ui.add_space(8.0);
self.backup_ui(ui, wallet, cb);
}
if self.rotate.is_some() {
ui.add_space(8.0);
self.rotate_ui(ui, wallet, cb);
}
if self.import_nsec.is_some() {
ui.add_space(8.0);
self.import_nsec_ui(ui, wallet, cb);
@@ -5357,234 +5316,6 @@ impl GoblinWalletView {
});
}
/// Inline key-rotation flow: warning → typed RESET + password → result.
fn rotate_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
let t = theme::tokens();
let rotate = self.rotate.as_mut().unwrap();
// Poll the worker result.
if rotate.stage == 3 {
if let Some(res) = rotate.result.lock().unwrap().take() {
match res {
Ok(npub) => {
rotate.new_npub = npub;
rotate.stage = 4;
}
Err(e) => {
rotate.error = e;
rotate.stage = 5;
}
}
}
}
let mut close = false;
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
match rotate.stage {
1 => {
ui.label(
RichText::new(t!("goblin.settings.rotate_key"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.neg),
);
ui.add_space(6.0);
for line in [
t!("goblin.settings.rotate_line1"),
t!("goblin.settings.rotate_line2"),
t!("goblin.settings.rotate_line3"),
t!("goblin.settings.rotate_line4"),
t!("goblin.settings.rotate_line5"),
] {
ui.label(
RichText::new(line)
.font(FontId::new(13.0, fonts::regular()))
.color(t.surface_text_dim),
);
ui.add_space(4.0);
}
ui.add_space(8.0);
ui.horizontal(|ui| {
let half = (ui.available_width() - 10.0) / 2.0;
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(half, 44.0),
)),
|ui| {
if w::big_action_on_card(ui, &t!("goblin.settings.cancel"))
.clicked()
{
close = true;
}
},
);
ui.add_space(10.0);
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(half, 44.0),
)),
|ui| {
if w::big_action(ui, &t!("goblin.settings.continue"), false)
.clicked()
{
rotate.stage = 2;
}
},
);
});
}
2 => {
ui.label(
RichText::new(t!("goblin.settings.final_confirmation"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.neg),
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("goblin.settings.rotate_confirm_blurb"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.surface_text_dim),
);
ui.add_space(10.0);
w::field_well(ui, |ui| {
TextEdit::new(egui::Id::from("rotate_reset"))
.focus(false)
.hint_text(t!("goblin.settings.type_reset"))
.text_color(t.surface_text)
.body()
.ui(ui, &mut rotate.reset_input, cb);
});
ui.add_space(8.0);
w::field_well(ui, |ui| {
TextEdit::new(egui::Id::from("rotate_pass"))
.focus(false)
.hint_text(t!("goblin.settings.wallet_password"))
.password()
.text_color(t.surface_text)
.body()
.ui(ui, &mut rotate.password, cb);
});
ui.add_space(10.0);
let armed = rotate.reset_input.trim() == "RESET" && !rotate.password.is_empty();
ui.horizontal(|ui| {
let half = (ui.available_width() - 10.0) / 2.0;
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(half, 44.0),
)),
|ui| {
if w::big_action_on_card(ui, &t!("goblin.settings.cancel"))
.clicked()
{
close = true;
}
},
);
ui.add_space(10.0);
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(half, 44.0),
)),
|ui| {
ui.add_enabled_ui(armed, |ui| {
if w::big_action(
ui,
&t!("goblin.settings.rotate_key_btn"),
false,
)
.clicked()
{
rotate.stage = 3;
let slot = rotate.result.clone();
let password = std::mem::take(&mut rotate.password);
rotate.reset_input.clear();
let wallet = wallet.clone();
std::thread::spawn(move || {
let res = wallet.rotate_nostr_identity(password);
*slot.lock().unwrap() = Some(res);
});
}
});
},
);
});
}
3 => {
ui.horizontal(|ui| {
View::small_loading_spinner(ui);
ui.add_space(8.0);
ui.label(
RichText::new(t!("goblin.settings.rotating_key"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.surface_text_dim),
);
});
ui.ctx().request_repaint();
}
4 => {
ui.label(
RichText::new(t!("goblin.settings.key_rotated"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.pos),
);
ui.add_space(4.0);
let npub = &rotate.new_npub;
let short = if npub.len() > 18 {
format!("{}{}", &npub[..12], &npub[npub.len() - 6..])
} else {
npub.clone()
};
ui.label(
RichText::new(t!("goblin.settings.new_npub", npub => short))
.font(FontId::new(13.0, fonts::mono()))
.color(t.surface_text_dim),
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("goblin.settings.backup_new_key"))
.font(FontId::new(13.0, fonts::semibold()))
.color(t.neg),
);
ui.add_space(10.0);
if w::big_action_on_card(ui, &t!("goblin.settings.copy_new_nsec")).clicked() {
if let Some(nsec) = wallet.nostr_service().and_then(|s| s.nsec()) {
// Secret: auto-clears from the clipboard after a delay
// (compare-then-clear) so it does not linger there.
cb.copy_secret_to_buffer(nsec);
cb.vibrate_copy();
}
}
ui.add_space(8.0);
if w::big_action(ui, &t!("goblin.settings.done"), false).clicked() {
close = true;
}
}
_ => {
ui.label(
RichText::new(t!("goblin.settings.rotation_failed"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.neg),
);
ui.add_space(4.0);
ui.label(
RichText::new(&rotate.error)
.font(FontId::new(13.0, fonts::regular()))
.color(t.surface_text_dim),
);
ui.add_space(10.0);
if w::big_action_on_card(ui, &t!("goblin.settings.close")).clicked() {
close = true;
}
}
}
});
if close {
self.rotate = None;
}
}
/// Inline "back up identity to a file" flow: ask for the wallet password,
/// seal the identity, and write a GOBLIN-*.backup file via the native picker.
fn backup_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
+3 -71
View File
@@ -604,74 +604,6 @@ impl Wallet {
.map_err(|e| format!("nsec encode failed: {e}"))
}
pub fn rotate_nostr_identity(&self, password: String) -> Result<String, String> {
let svc = self
.nostr_service()
.ok_or_else(|| "nostr is not running".to_string())?;
// Snapshot the old identity and prove the password by unlocking it
// (NIP-49 decryption fails on a wrong password).
let old = svc.identity.read().clone();
let old_keys = old
.unlock(&password)
.map_err(|_| "Wrong password".to_string())?;
// Generate the replacement identity.
let (mut new_identity, _new_keys) = NostrIdentity::create_random(&password)
.map_err(|e| format!("key generation failed: {e}"))?;
// Release the username first (the server also deletes its avatar);
// abort the rotation if that fails so the user never ends up with a
// burned key still welded to a public name. After rotation the name
// is up for grabs — by the new key or anyone else.
if let Some(nip05) = old.nip05.clone() {
let name = nip05.split('@').next().unwrap_or_default().to_string();
let server = { svc.config.read().nip05_server() };
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| e.to_string())?;
rt.block_on(async { crate::nostr::nip05::unregister(&server, &name, &old_keys).await })
.map_err(|e| format!("Couldn't release @{name}: {e} — rotation cancelled"))?;
}
new_identity.prev_npubs = {
let mut v = old.prev_npubs.clone();
v.push(old.npub.clone());
v
};
// Persist, then swap the running service for one bound to the new key.
let config = self.get_config();
let nostr_dir = config.get_nostr_path();
new_identity
.save(&nostr_dir)
.map_err(|e| format!("identity save failed: {e}"))?;
svc.stop();
for _ in 0..100 {
if !svc.is_running() {
break;
}
thread::sleep(Duration::from_millis(100));
}
let wallet_dir = PathBuf::from(config.get_data_path());
let nostr_config = NostrConfig::load(wallet_dir);
let store = NostrStore::new(config.get_nostr_db_path());
let new_npub = new_identity.npub.clone();
// Rebuild the service holding ALL held identities (the primary entry now
// resolves to the new identity), with the new one active.
let (recv, _) = self
.unlock_all_identities(&nostr_dir, &password)
.ok_or_else(|| "identity unlock failed".to_string())?;
let active_hex = new_identity.pubkey_hex().unwrap_or_default();
let new_svc = NostrService::new(recv, &active_hex, nostr_config, store, nostr_dir);
{
let mut w_nostr = self.nostr.write();
*w_nostr = Some(new_svc.clone());
}
new_svc.start(self.clone());
info!("nostr: identity rotated to {}", new_npub);
Ok(new_npub)
}
/// Replace the nostr identity with an imported key — either a bare nsec
/// or an exported identity-backup JSON (which restores the username and
/// rotation history too). The current identity is overwritten; its npub
@@ -791,9 +723,9 @@ impl Wallet {
// identities and present a different one at will. Exactly one is ACTIVE: it
// drives the single live gift-wrap subscription and all display, and every
// identity redeems into the SAME shared grin balance. Only the active nsec is
// ever decrypted into memory; the rest rest as ncryptsec on disk. Switching is
// mechanically identical to `rotate_nostr_identity` (stop the service, rebuild
// it on the target key, restart), plus a per-identity catch-up so payments that
// ever decrypted into memory; the rest rest as ncryptsec on disk. Switching
// stops the service and rebuilds it on the target key, then restarts, plus a
// per-identity catch-up so payments that
// arrived while an identity was dormant are redeemed on switch-in.
/// The held identities for the identity switcher (no secrets). Empty if nostr