onboarding + nostr: healthy default node, claim feedback, identity import, resolve payer name
Four field-reported issues from a fresh install + a friend payment: - Default node was grincoin.org, whose foreign API was returning "rpc call failed" — onboarding sync died with an un-retryable error. Lead the node list with the verified-healthy api.grin.money (external.rs) and use it as the onboarding default (was grincoin.org); grincoin.org stays in the list. - Claiming a username gave no feedback and the identity card kept showing the npub. The card now shows the @name + a seal check once claimed, and a clear "name is yours" success card replaces the claim form before Open wallet. - A returning user who restores a seed gets a fresh random nostr key, so their old @name couldn't come back. Offer "Import it" in the identity step: paste an nsec or pick a .backup file (reuses the wallet password just set) to keep the existing key + username. - The requester side of a request never resolved the payer's @username — the FinalizePost ingest arm skipped ensure_contact/resolve_contact_identity, so a completed request showed a bare npub for the payer. Resolve on finalize like every other ingest path. i18n: claimed_title/claimed_blurb + import_existing/import_title/import_blurb across all six locales; drift test green.
This commit is contained in:
@@ -719,8 +719,13 @@ goblin:
|
||||
claim_username: "Benutzernamen sichern"
|
||||
available_when_connected: "Verfügbar, sobald das mixnet verbindet — oder überspringen und später sichern."
|
||||
youre: "Du bist %{name}"
|
||||
claimed_title: "%{name} gehört dir"
|
||||
claimed_blurb: "Freunde können dich jetzt per Namen bezahlen. Alles bereit — öffne dein Wallet."
|
||||
open_wallet: "Mein Wallet öffnen"
|
||||
skip_for_now: "Vorerst überspringen"
|
||||
import_existing: "Schon eine Goblin-Identität? Importieren"
|
||||
import_title: "Identität importieren"
|
||||
import_blurb: "Füge deinen nsec ein oder wähle eine .backup-Datei, um deinen vorhandenen Schlüssel und Benutzernamen zu behalten statt diesen neuen."
|
||||
errors:
|
||||
cant_open: "Wallet konnte nicht geöffnet werden: %{err}"
|
||||
cant_create: "Wallet konnte nicht erstellt werden: %{err}"
|
||||
|
||||
@@ -719,8 +719,13 @@ goblin:
|
||||
claim_username: "Claim username"
|
||||
available_when_connected: "Available once the mixnet connects — or skip and claim later."
|
||||
youre: "You're %{name}"
|
||||
claimed_title: "%{name} is yours"
|
||||
claimed_blurb: "Friends can now pay you by name. You're all set — open your wallet."
|
||||
open_wallet: "Open my wallet"
|
||||
skip_for_now: "Skip for now"
|
||||
import_existing: "Already have a Goblin identity? Import it"
|
||||
import_title: "Import your identity"
|
||||
import_blurb: "Paste your nsec or pick a .backup file to keep your existing key and username instead of this new one."
|
||||
errors:
|
||||
cant_open: "Couldn't open the wallet: %{err}"
|
||||
cant_create: "Couldn't create the wallet: %{err}"
|
||||
|
||||
@@ -719,8 +719,13 @@ goblin:
|
||||
claim_username: "Réserver le nom d'utilisateur"
|
||||
available_when_connected: "Disponible une fois le mixnet connecté — ou passez et réservez plus tard."
|
||||
youre: "Vous êtes %{name}"
|
||||
claimed_title: "%{name} est à vous"
|
||||
claimed_blurb: "Vos amis peuvent désormais vous payer par votre nom. Tout est prêt — ouvrez votre portefeuille."
|
||||
open_wallet: "Ouvrir mon portefeuille"
|
||||
skip_for_now: "Passer pour l'instant"
|
||||
import_existing: "Vous avez déjà une identité Goblin ? Importez-la"
|
||||
import_title: "Importer votre identité"
|
||||
import_blurb: "Collez votre nsec ou choisissez un fichier .backup pour conserver votre clé et votre nom existants au lieu de ce nouveau."
|
||||
errors:
|
||||
cant_open: "Impossible d'ouvrir le portefeuille : %{err}"
|
||||
cant_create: "Impossible de créer le portefeuille : %{err}"
|
||||
|
||||
@@ -719,8 +719,13 @@ goblin:
|
||||
claim_username: "Занять имя"
|
||||
available_when_connected: "Доступно после подключения mixnet — или пропустите и займите позже."
|
||||
youre: "Вы %{name}"
|
||||
claimed_title: "%{name} теперь ваше"
|
||||
claimed_blurb: "Друзья теперь могут платить вам по имени. Всё готово — откройте кошелёк."
|
||||
open_wallet: "Открыть кошелёк"
|
||||
skip_for_now: "Пропустить пока"
|
||||
import_existing: "Уже есть личность Goblin? Импортируйте её"
|
||||
import_title: "Импорт личности"
|
||||
import_blurb: "Вставьте свой nsec или выберите файл .backup, чтобы сохранить существующий ключ и имя вместо нового."
|
||||
errors:
|
||||
cant_open: "Не удалось открыть кошелёк: %{err}"
|
||||
cant_create: "Не удалось создать кошелёк: %{err}"
|
||||
|
||||
@@ -719,8 +719,13 @@ goblin:
|
||||
claim_username: "Kullanıcı adı al"
|
||||
available_when_connected: "Mixnet bağlandığında müsait — ya da atla ve sonra al."
|
||||
youre: "Sen %{name}'sin"
|
||||
claimed_title: "%{name} artık senin"
|
||||
claimed_blurb: "Arkadaşların artık sana adınla ödeme yapabilir. Her şey hazır — cüzdanını aç."
|
||||
open_wallet: "Cüzdanımı aç"
|
||||
skip_for_now: "Şimdilik atla"
|
||||
import_existing: "Zaten bir Goblin kimliğin var mı? İçe aktar"
|
||||
import_title: "Kimliğini içe aktar"
|
||||
import_blurb: "Bu yeni anahtar yerine mevcut anahtarını ve kullanıcı adını korumak için nsec'ini yapıştır veya bir .backup dosyası seç."
|
||||
errors:
|
||||
cant_open: "Cüzdan açılamadı: %{err}"
|
||||
cant_create: "Cüzdan oluşturulamadı: %{err}"
|
||||
|
||||
@@ -719,8 +719,13 @@ goblin:
|
||||
claim_username: "注册用户名"
|
||||
available_when_connected: "mixnet 连接后可用 — 或跳过,稍后注册。"
|
||||
youre: "你是 %{name}"
|
||||
claimed_title: "%{name} 已归你所有"
|
||||
claimed_blurb: "朋友现在可以用你的用户名向你付款。一切就绪——打开钱包吧。"
|
||||
open_wallet: "打开我的钱包"
|
||||
skip_for_now: "暂时跳过"
|
||||
import_existing: "已有 Goblin 身份?导入它"
|
||||
import_title: "导入你的身份"
|
||||
import_blurb: "粘贴你的 nsec 或选择一个 .backup 文件,保留你现有的密钥和用户名,而不是这个新的。"
|
||||
errors:
|
||||
cant_open: "无法打开钱包:%{err}"
|
||||
cant_create: "无法创建钱包:%{err}"
|
||||
|
||||
@@ -28,6 +28,7 @@ use crate::gui::views::types::{ContentContainer, ModalPosition, QrScanResult};
|
||||
use crate::gui::views::wallets::creation::MnemonicSetup;
|
||||
use crate::gui::views::{CameraScanContent, Content, Modal, TextEdit, View};
|
||||
use crate::node::Node;
|
||||
use crate::nostr::NostrIdentity;
|
||||
use crate::wallet::types::{ConnectionMethod, PhraseMode, PhraseSize};
|
||||
use crate::wallet::{ConnectionsConfig, ExternalConnection, Wallet, WalletList};
|
||||
|
||||
@@ -70,6 +71,30 @@ pub struct OnboardingContent {
|
||||
wallet: Option<Wallet>,
|
||||
/// Optional username claim state (same machinery as Settings).
|
||||
claim: ClaimState,
|
||||
/// Optional "import an existing identity" sub-flow, opened from the identity
|
||||
/// step so a returning user can keep their old npub + username instead of the
|
||||
/// freshly-generated random key.
|
||||
import: Option<OnbImport>,
|
||||
}
|
||||
|
||||
/// Onboarding identity-import state. Reuses the wallet password the user just
|
||||
/// set, so it only needs the backup file / nsec (and the backup's own password
|
||||
/// when restoring a sealed `.backup`).
|
||||
#[derive(Default)]
|
||||
struct OnbImport {
|
||||
/// 0 = form, 1 = working, 2 = error.
|
||||
stage: u8,
|
||||
/// Pasted nsec or the read-in contents of a `.backup` / identity JSON file.
|
||||
nsec: String,
|
||||
/// Password the backup was sealed under (blank for a bare nsec, or when it
|
||||
/// matches this wallet's password).
|
||||
backup_password: String,
|
||||
/// Last import error, shown on stage 2.
|
||||
error: String,
|
||||
/// A native file pick is in flight (Android resolves the path asynchronously).
|
||||
picking: bool,
|
||||
/// Worker result: Ok(new npub) or Err(message).
|
||||
result: std::sync::Arc<std::sync::Mutex<Option<Result<String, String>>>>,
|
||||
}
|
||||
|
||||
impl Default for OnboardingContent {
|
||||
@@ -79,7 +104,7 @@ impl Default for OnboardingContent {
|
||||
// Default to the Instant path (connect to a public node) so a new
|
||||
// user is online immediately, with no chain-sync wait.
|
||||
integrated: false,
|
||||
ext_url: "https://grincoin.org".to_string(),
|
||||
ext_url: "https://api.grin.money".to_string(),
|
||||
restore: false,
|
||||
name: "Main wallet".to_string(),
|
||||
pass: String::new(),
|
||||
@@ -89,6 +114,7 @@ impl Default for OnboardingContent {
|
||||
scan_modal: None,
|
||||
wallet: None,
|
||||
claim: ClaimState::default(),
|
||||
import: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,6 +731,13 @@ impl OnboardingContent {
|
||||
.as_ref()
|
||||
.map(|s| (s.npub(), s.is_connected()))
|
||||
.unwrap_or((String::new(), false));
|
||||
// The claimed @name (bare), if any — so the identity card shows the name
|
||||
// instead of the npub once a username is registered.
|
||||
let claimed_name = service
|
||||
.as_ref()
|
||||
.and_then(|s| s.identity.read().nip05.clone())
|
||||
.and_then(|n| n.split('@').next().map(|s| s.to_string()))
|
||||
.filter(|n| !n.is_empty());
|
||||
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
@@ -719,18 +752,36 @@ impl OnboardingContent {
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
ui.vertical(|ui| {
|
||||
let short = if npub.len() > 20 {
|
||||
format!("{}…{}", &npub[..12], &npub[npub.len() - 6..])
|
||||
} else if npub.is_empty() {
|
||||
t!("goblin.onboarding.identity.key_being_made").to_string()
|
||||
// Once claimed, show the @name (with a check) instead of the npub
|
||||
// so the user can SEE the username applied.
|
||||
if let Some(name) = &claimed_name {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 5.0;
|
||||
ui.label(
|
||||
RichText::new(name)
|
||||
.font(FontId::new(16.0, fonts::bold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(crate::gui::icons::SEAL_CHECK)
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.pos),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
npub.clone()
|
||||
};
|
||||
ui.label(
|
||||
RichText::new(short)
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
let short = if npub.len() > 20 {
|
||||
format!("{}…{}", &npub[..12], &npub[npub.len() - 6..])
|
||||
} else if npub.is_empty() {
|
||||
t!("goblin.onboarding.identity.key_being_made").to_string()
|
||||
} else {
|
||||
npub.clone()
|
||||
};
|
||||
ui.label(
|
||||
RichText::new(short)
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
}
|
||||
ui.label(
|
||||
RichText::new(if connected {
|
||||
t!("goblin.onboarding.identity.connected_nym")
|
||||
@@ -792,7 +843,10 @@ impl OnboardingContent {
|
||||
.nostr_service()
|
||||
.map(|s| s.identity.read().nip05.is_some())
|
||||
.unwrap_or(false);
|
||||
if !registered {
|
||||
if self.import.is_some() {
|
||||
// Returning user is swapping the random key for their existing identity.
|
||||
self.import_ui(ui, &wallet, cb);
|
||||
} else if !registered {
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.label(
|
||||
@@ -870,9 +924,55 @@ impl OnboardingContent {
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
// Returning user? Let them restore their existing identity (nsec or a
|
||||
// .backup file) instead of claiming a fresh name on the random key.
|
||||
let import_resp = ui
|
||||
.add(
|
||||
egui::Label::new(
|
||||
RichText::new(t!("goblin.onboarding.identity.import_existing"))
|
||||
.font(FontId::new(13.0, fonts::semibold()))
|
||||
.color(t.accent),
|
||||
)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
if import_resp.clicked() {
|
||||
self.import = Some(OnbImport::default());
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
} else {
|
||||
ui.add_space(2.0);
|
||||
// Claimed: show a clear success confirmation so the user knows the
|
||||
// username stuck before they tap through to the wallet.
|
||||
let claimed = claimed_name.clone().unwrap_or_default();
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.0;
|
||||
ui.label(
|
||||
RichText::new(crate::gui::icons::SEAL_CHECK)
|
||||
.font(FontId::new(22.0, fonts::regular()))
|
||||
.color(t.pos),
|
||||
);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(
|
||||
RichText::new(t!(
|
||||
"goblin.onboarding.identity.claimed_title",
|
||||
name => &claimed
|
||||
))
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.onboarding.identity.claimed_blurb"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(16.0);
|
||||
}
|
||||
|
||||
if !connected {
|
||||
@@ -891,6 +991,199 @@ impl OnboardingContent {
|
||||
None
|
||||
}
|
||||
|
||||
/// Onboarding identity-import sub-flow: paste an nsec or pick a `.backup`
|
||||
/// file to swap the freshly-generated random key for the user's existing
|
||||
/// identity (keeping their npub and any claimed username). Reuses the wallet
|
||||
/// password the user just set; a sealed `.backup` may carry its own password.
|
||||
fn import_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
// Poll the worker first, WITHOUT holding a borrow across the reset below.
|
||||
if self.import.as_ref().map(|i| i.stage) == Some(1) {
|
||||
let res = self.import.as_ref().unwrap().result.lock().unwrap().take();
|
||||
if let Some(res) = res {
|
||||
match res {
|
||||
// Identity replaced: drop the sub-flow; the identity card and the
|
||||
// claim/success state re-render from the new service next frame.
|
||||
Ok(_) => {
|
||||
self.import = None;
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
let imp = self.import.as_mut().unwrap();
|
||||
imp.error = e;
|
||||
imp.stage = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let wallet_pass = self.pass.clone();
|
||||
let imp = self.import.as_mut().unwrap();
|
||||
let mut close = false;
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
match imp.stage {
|
||||
1 => {
|
||||
ui.horizontal(|ui| {
|
||||
View::small_loading_spinner(ui);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.settings.importing"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
});
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
2 => {
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.settings.import_failed"))
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.neg),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(&imp.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;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.onboarding.identity.import_title"))
|
||||
.font(FontId::new(15.0, fonts::semibold()))
|
||||
.color(t.surface_text),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.onboarding.identity.import_blurb"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
// Native ".backup file" picker. Desktop returns the path now;
|
||||
// Android resolves it asynchronously (poll picked_file()).
|
||||
if imp.picking {
|
||||
if let Some(path) = cb.picked_file() {
|
||||
imp.picking = false;
|
||||
if !path.is_empty() {
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(contents) => imp.nsec = contents.trim().to_string(),
|
||||
Err(_) => {
|
||||
imp.error =
|
||||
t!("goblin.settings.backup_read_failed").to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
if w::big_action_on_card(ui, &t!("goblin.settings.choose_backup_file"))
|
||||
.clicked()
|
||||
{
|
||||
imp.error.clear();
|
||||
match cb.pick_file() {
|
||||
Some(path) if !path.is_empty() => {
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(contents) => imp.nsec = contents.trim().to_string(),
|
||||
Err(_) => {
|
||||
imp.error =
|
||||
t!("goblin.settings.backup_read_failed").to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Empty string = Android async pick in flight.
|
||||
Some(_) => imp.picking = true,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
TextEdit::new(egui::Id::from("onb_import_nsec"))
|
||||
.focus(false)
|
||||
.hint_text(t!("goblin.settings.import_nsec_hint"))
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.ui(ui, &mut imp.nsec, cb);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
w::field_well(ui, |ui| {
|
||||
TextEdit::new(egui::Id::from("onb_import_bpw"))
|
||||
.focus(false)
|
||||
.hint_text(t!("goblin.settings.backup_password_hint"))
|
||||
.password()
|
||||
.text_color(t.surface_text)
|
||||
.body()
|
||||
.ui(ui, &mut imp.backup_password, cb);
|
||||
});
|
||||
if !imp.error.is_empty() {
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(&imp.error)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
let pasted = imp.nsec.trim();
|
||||
// Only an nsec paste or a sealed .backup file — nothing else.
|
||||
let armed =
|
||||
pasted.starts_with("nsec1") || NostrIdentity::is_encrypted_backup(pasted);
|
||||
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.import_btn"), false)
|
||||
.clicked()
|
||||
{
|
||||
imp.stage = 1;
|
||||
let slot = imp.result.clone();
|
||||
let nsec = std::mem::take(&mut imp.nsec);
|
||||
let bpw = std::mem::take(&mut imp.backup_password);
|
||||
let bpw = if bpw.is_empty() { None } else { Some(bpw) };
|
||||
let wallet = wallet.clone();
|
||||
let pass = wallet_pass.clone();
|
||||
std::thread::spawn(move || {
|
||||
let res = wallet.import_nostr_identity(nsec, pass, bpw);
|
||||
*slot.lock().unwrap() = Some(res);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if close {
|
||||
self.import = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recovery-phrase QR scan modal content.
|
||||
fn scan_modal_ui(&mut self, ui: &mut egui::Ui, _: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
if let Some(content) = self.scan_modal.as_mut() {
|
||||
|
||||
+38
-31
@@ -1363,39 +1363,46 @@ async fn handle_wrap(svc: &Arc<NostrService>, wallet: &Wallet, client: &Client,
|
||||
svc.store.mark_processed(&rumor_id);
|
||||
svc.store.mark_processed(&slate_marker);
|
||||
}
|
||||
IngestDecision::FinalizePost => match wallet.nostr_finalize_post(&slate) {
|
||||
Ok(true) => {
|
||||
svc.store
|
||||
.update_tx_status(&slate.id.to_string(), NostrSendStatus::Finalized);
|
||||
// Finalize+post committed; mark dedup before the sync tail so a
|
||||
// crash can't re-finalize on catch-up (grin rejects a second
|
||||
// finalize and the meta is now Finalized, which decide() drops —
|
||||
// this just avoids the redundant attempt).
|
||||
svc.store.mark_processed(&wrap_id);
|
||||
svc.store.mark_processed(&rumor_id);
|
||||
svc.store.mark_processed(&slate_marker);
|
||||
if let Some(mut contact) = svc.store.contact(&sender_hex) {
|
||||
contact.last_paid_at = Some(unix_time());
|
||||
svc.store.save_contact(&contact);
|
||||
IngestDecision::FinalizePost => {
|
||||
// The payer's reply is our first contact with their key on this side of
|
||||
// a request we sent — make sure they're a known contact and resolve their
|
||||
// @username so the completed request shows their name, not a bare npub.
|
||||
svc.ensure_contact(&sender_hex);
|
||||
svc.resolve_contact_identity(&sender_hex);
|
||||
match wallet.nostr_finalize_post(&slate) {
|
||||
Ok(true) => {
|
||||
svc.store
|
||||
.update_tx_status(&slate.id.to_string(), NostrSendStatus::Finalized);
|
||||
// Finalize+post committed; mark dedup before the sync tail so a
|
||||
// crash can't re-finalize on catch-up (grin rejects a second
|
||||
// finalize and the meta is now Finalized, which decide() drops —
|
||||
// this just avoids the redundant attempt).
|
||||
svc.store.mark_processed(&wrap_id);
|
||||
svc.store.mark_processed(&rumor_id);
|
||||
svc.store.mark_processed(&slate_marker);
|
||||
if let Some(mut contact) = svc.store.contact(&sender_hex) {
|
||||
contact.last_paid_at = Some(unix_time());
|
||||
svc.store.save_contact(&contact);
|
||||
}
|
||||
wallet.sync();
|
||||
}
|
||||
Ok(false) => {
|
||||
// The send was cancelled out-of-band (the meta usually already
|
||||
// reflects this and decide() drops the S2 before we get here; this
|
||||
// covers a tx-list cancel that left the meta untouched). Reconcile
|
||||
// the status and treat the reply as handled — never retry/re-post.
|
||||
svc.store
|
||||
.update_tx_status(&slate.id.to_string(), NostrSendStatus::Cancelled);
|
||||
svc.store.mark_processed(&wrap_id);
|
||||
svc.store.mark_processed(&rumor_id);
|
||||
svc.store.mark_processed(&slate_marker);
|
||||
info!("nostr: skipped finalize of cancelled slate {}", slate.id);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("nostr: finalize failed for slate {}: {:?}", slate.id, e);
|
||||
}
|
||||
wallet.sync();
|
||||
}
|
||||
Ok(false) => {
|
||||
// The send was cancelled out-of-band (the meta usually already
|
||||
// reflects this and decide() drops the S2 before we get here; this
|
||||
// covers a tx-list cancel that left the meta untouched). Reconcile
|
||||
// the status and treat the reply as handled — never retry/re-post.
|
||||
svc.store
|
||||
.update_tx_status(&slate.id.to_string(), NostrSendStatus::Cancelled);
|
||||
svc.store.mark_processed(&wrap_id);
|
||||
svc.store.mark_processed(&rumor_id);
|
||||
svc.store.mark_processed(&slate_marker);
|
||||
info!("nostr: skipped finalize of cancelled slate {}", slate.id);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("nostr: finalize failed for slate {}: {:?}", slate.id, e);
|
||||
}
|
||||
},
|
||||
}
|
||||
IngestDecision::Drop(reason) => {
|
||||
info!("nostr: dropped slate {}: {}", slate.id, reason);
|
||||
// A dropped slate is a permanent decision — don't re-evaluate it.
|
||||
|
||||
@@ -38,16 +38,16 @@ pub struct ExternalConnection {
|
||||
pub available: Option<bool>,
|
||||
}
|
||||
|
||||
/// Default external node URLs for main network. grincoin.org leads (main.gri.mw
|
||||
/// has intermittent issues); main.us-ea.st is the Goblin-run node. The rest are
|
||||
/// independent public nodes (healthy on grin.fail) so a single operator going
|
||||
/// down never strands the wallet.
|
||||
/// Default external node URLs for main network. api.grin.money leads (verified
|
||||
/// healthy; grincoin.org's node was returning "rpc call failed"); main.us-ea.st
|
||||
/// is the Goblin-run node. The rest are independent public nodes so a single
|
||||
/// operator going down never strands the wallet.
|
||||
const DEFAULT_MAIN_URLS: [&'static str; 6] = [
|
||||
"https://grincoin.org",
|
||||
"https://api.grin.money",
|
||||
"https://main.us-ea.st",
|
||||
"https://grincoin.org",
|
||||
"https://main.gri.mw",
|
||||
"https://mainnet.grinffindor.org",
|
||||
"https://api.grin.money",
|
||||
"https://main.grin.raubritter.org",
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user