Skip to content

Commit 26a7e09

Browse files
committed
feat: export wallet seed words
closes #28 adds a button to "Export Wallet Seed" to the Addresses screen. Clicking it opens a modal dialog with instructions, and a second button makes the seed words appear, after which the modal can be closed.
1 parent 194570c commit 26a7e09

File tree

8 files changed

+243
-53
lines changed

8 files changed

+243
-53
lines changed

Cargo.lock

Lines changed: 33 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ twenty-first = "1.0"
1313
dioxus = { version = "0.7.1", features = ["router", "logger"] }
1414
dioxus-logger = "0.7.1"
1515
getrandom = { version = "0.3", features = ["wasm_js"] }
16-
neptune-types = {git = "https://github.com/Neptune-Crypto/neptune-types.git", rev = "cb240441ec07b91f4dffaa8099895a62210f7220"}
16+
neptune-types = {git = "https://github.com/Neptune-Crypto/neptune-types.git", rev = "7175bf047067134a8383b689f0b3f8fe110086fb"}
1717
thiserror = "1.0"
1818
serde_json = "1.0.145"
1919
serde = { version = "1.0", features = ["derive"] }

api/src/lib.rs

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ use neptune_types::transaction_details::TransactionDetails;
3232
use neptune_types::transaction_kernel::TransactionKernel;
3333
use neptune_types::transaction_kernel_id::TransactionKernelId;
3434
use neptune_types::ui_utxo::UiUtxo;
35+
use neptune_types::wallet_file::WalletFile;
36+
use neptune_types::wallet_file_context::WalletFileContext;
37+
use neptune_types::secret_key_material::SecretKeyMaterial;
38+
3539
use prefs::user_prefs::UserPrefs;
3640
use price_map::PriceMap;
3741
use twenty_first::tip5::Digest;
@@ -49,50 +53,9 @@ pub async fn get_user_prefs() -> Result<UserPrefs, ApiError> {
4953

5054
#[post("/api/network")]
5155
pub async fn network() -> Result<Network, ApiError> {
52-
println!("DEBUG: [network] Called");
53-
54-
// 1. Connection
55-
println!("DEBUG: [network] calling rpc_client()...");
56-
let client_res = neptune_rpc::rpc_client().await;
57-
58-
let client = match client_res {
59-
Ok(c) => {
60-
println!("DEBUG: [network] rpc_client obtained successfully");
61-
c
62-
}
63-
Err(e) => {
64-
println!("DEBUG: [network] rpc_client failed: {:?}", e);
65-
// If this prints and then the frontend says "Shutdown",
66-
// it confirms the crash happens when returning this error.
67-
return Err(e);
68-
}
69-
};
70-
71-
// 2. Execution
72-
println!("DEBUG: [network] calling client.network(context)...");
73-
let result = client.network(tarpc::context::current()).await;
74-
75-
match result {
76-
Ok(Ok(n)) => {
77-
println!("DEBUG: [network] Success: {:?}", n);
78-
Ok(n)
79-
}
80-
Ok(Err(e)) => {
81-
println!("DEBUG: [network] Logic Error from Core: {:?}", e);
82-
Err(e.into())
83-
}
84-
Err(e) => {
85-
// This is the Tarpc Transport error (Shutdown/BrokenPipe)
86-
println!("DEBUG: [network] Transport Error: {:?}", e);
87-
Err(e.into())
88-
}
89-
}
56+
neptune_rpc::network().await
9057
}
9158

92-
// pub async fn network() -> Result<Network, ApiError> {
93-
// neptune_rpc::network().await
94-
// }
95-
9659
#[post("/api/wallet_balance")]
9760
pub async fn wallet_balance() -> Result<NativeCurrencyAmount, ApiError> {
9861
let client = neptune_rpc::rpc_client().await?;
@@ -264,6 +227,40 @@ pub async fn neptune_core_rpc_socket_addr() -> Result<SocketAddr, ApiError> {
264227
))
265228
}
266229

230+
/// Asynchronously retrieves the SecretKeyMaterial by reading the wallet.dat file.
231+
#[post("/api/get_wallet_secret_key")]
232+
pub async fn get_wallet_secret_key() -> Result<SecretKeyMaterial, ApiError> {
233+
use anyhow::Context;
234+
235+
let cookie_hint = neptune_rpc::cookie_hint().await?;
236+
237+
// Note: We use tokio::task::spawn_blocking for file I/O as it blocks the thread.
238+
// This is required for non-async I/O operations like WalletFile::read_from_file.
239+
tokio::task::spawn_blocking(move || {
240+
// 1. Get the wallet directory path
241+
let wallet_dir = cookie_hint.data_directory.wallet_directory_path();
242+
243+
// 2. Determine wallet file path and check existence
244+
let wallet_file = WalletFileContext::wallet_secret_path(&wallet_dir);
245+
246+
if !wallet_file.exists() {
247+
anyhow::bail!(
248+
"Wallet file not found at: {}. Please generate or import a wallet first.",
249+
wallet_file.display()
250+
);
251+
}
252+
253+
// 3. Read the WalletFile and extract the secret key
254+
let wallet_secret = WalletFile::read_from_file(&wallet_file)
255+
.context(format!(
256+
"Could not read WalletFile from disk at {}",
257+
wallet_file.display()
258+
))?;
259+
260+
Ok(wallet_secret.secret_key())
261+
}).await?
262+
}
263+
267264
#[cfg(not(target_arch = "wasm32"))]
268265
#[allow(dead_code)]
269266
mod neptune_rpc {

ui/src/components/empty_state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub fn EmptyState(props: EmptyStateProps) -> Element {
8080
color: var(--pico-primary-background);
8181
opacity: 0.8;
8282
",
83-
83+
8484
{icon}
8585
}
8686
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//=============================================================================
2+
// File: src/components/export_seed_phrase_modal.rs
3+
//=============================================================================
4+
use dioxus::prelude::*;
5+
use neptune_types::secret_key_material::SecretKeyMaterial;
6+
7+
use crate::components::pico::Button;
8+
use crate::components::pico::ButtonType;
9+
use crate::components::pico::NoTitleModal;
10+
11+
#[derive(Clone, Copy, Debug, PartialEq)]
12+
enum BackupStage {
13+
Instructions,
14+
DisplayingSeed,
15+
}
16+
17+
#[component]
18+
pub fn ExportSeedPhraseModal(is_open: Signal<bool>) -> Element {
19+
let mut stage = use_signal(|| BackupStage::Instructions);
20+
21+
// Resource to fetch the seed phrase.
22+
// This automatically re-runs when 'stage' changes because stage() is read inside.
23+
let mut seed_words_resource = use_resource(move || async move {
24+
if stage() == BackupStage::Instructions {
25+
return Ok(None::<SecretKeyMaterial>);
26+
}
27+
28+
match api::get_wallet_secret_key().await {
29+
Ok(secret) => Ok(Some(secret)),
30+
Err(e) => Err(e),
31+
}
32+
});
33+
34+
// Reset the stage automatically whenever the modal closes.
35+
// This catches "Esc" keys and backdrop clicks handled by NoTitleModal.
36+
use_effect(move || {
37+
if !is_open() {
38+
stage.set(BackupStage::Instructions);
39+
}
40+
});
41+
42+
let mut close_modal = move || {
43+
is_open.set(false);
44+
};
45+
46+
rsx! {
47+
NoTitleModal {
48+
is_open: is_open,
49+
50+
h5 {
51+
"⚠️ Export Seed Phrase"
52+
},
53+
54+
match stage() {
55+
BackupStage::Instructions => rsx! {
56+
div {
57+
p { "Your **Secret Recovery Phrase** is the master key to your funds." }
58+
p {
59+
strong { "1. Prepare: " }
60+
"Find a private location and something non-digital to write on (e.g., paper or metal)."
61+
}
62+
p {
63+
strong { "2. Write Down: " }
64+
"Write down the words in the exact order. Never type them into a computer."
65+
}
66+
p {
67+
strong { "3. Security: " }
68+
"Never share these words with anyone."
69+
}
70+
}
71+
},
72+
BackupStage::DisplayingSeed => rsx! {
73+
match &*seed_words_resource.read() {
74+
Some(Ok(Some(secret))) => rsx! {
75+
div {
76+
// card with 3 columns of seed words
77+
style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; padding: 1rem; border-radius: var(--pico-border-radius); background: var(--pico-card-background-color); color: var(--pico-color); box-shadow: var(--pico-card-box-shadow);",
78+
{
79+
secret.to_phrase().into_iter().enumerate().map(|(i, word)| {
80+
rsx! {
81+
div {
82+
key: "{i}",
83+
style: "text-align: left;",
84+
strong { "{i + 1}. " }
85+
"{word}"
86+
}
87+
}
88+
})
89+
}
90+
}
91+
small {
92+
style: "display: block; margin-top: 1rem; text-align: center; color: var(--pico-color-red-500); font-weight: bold;",
93+
"🚨 VIEW IN PRIVATE! WRITE DOWN AND CLOSE IMMEDIATELY! 🚨"
94+
}
95+
},
96+
Some(Err(e)) => rsx! {
97+
div {
98+
style: "color: var(--pico-color-red-500);",
99+
p { "Error retrieving wallet secret:" }
100+
pre { "{e}" }
101+
}
102+
},
103+
_ => rsx! {
104+
div {
105+
style: "text-align: center;",
106+
p { "Loading seed words..." }
107+
progress {}
108+
}
109+
}
110+
}
111+
}
112+
},
113+
114+
footer {
115+
div {
116+
style: "display: flex; justify-content: flex-end; gap: 1rem; margin-top: 1rem;",
117+
118+
Button {
119+
button_type: ButtonType::Secondary,
120+
outline: true,
121+
on_click: move |_| close_modal(),
122+
"Close"
123+
}
124+
125+
if stage() == BackupStage::Instructions {
126+
Button {
127+
button_type: ButtonType::Primary,
128+
on_click: move |_| {
129+
stage.set(BackupStage::DisplayingSeed);
130+
// The resource restart is triggered automatically because stage() is a dependency
131+
},
132+
"Display Seed Words"
133+
}
134+
}
135+
}
136+
}
137+
}
138+
}
139+
}

ui/src/components/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod currency_amount_input;
99
pub mod currency_chooser;
1010
pub mod digest_display;
1111
pub mod empty_state;
12+
pub mod export_seed_phrase_modal;
1213
pub mod pico;
1314
pub mod qr_code;
1415
pub mod qr_processor;

0 commit comments

Comments
 (0)