diff --git a/docs/cli.md b/docs/cli.md index 1b8649e..990330e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -116,9 +116,9 @@ If you mistype a key name, taolk suggests the closest match (Levenshtein distanc | `security.lock_timeout` | integer | `300` | Auto-lock timeout in seconds (0 disables) | | `security.require_password_per_send` | bool | `false` | Require password confirmation before each send | | `notifications.enabled` | bool | `true` | Play notification sounds | -| `notifications.volume` | integer | `100` | Volume 0-100 | +| `notifications.volume` | integer | `70` | Volume 0-100 | | `notifications.dm` | bool | `true` | Sound on direct messages | -| `notifications.ambient` | bool | `false` | Sound on channel messages | +| `notifications.ambient` | bool | `true` | Sound on channel messages | | `notifications.mention` | bool | `true` | Sound on mentions | | `ui.sidebar_width` | integer | `28` | Sidebar width in columns | | `ui.mouse` | boolean | `true` | Enable mouse support | diff --git a/docs/sdk.md b/docs/sdk.md index 28c29f4..10cb6fb 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -11,82 +11,17 @@ taolk = { version = "2", default-features = false } The `tui` feature gates `ratatui`, `crossterm`, `rpassword`, and `clap`. Without it, you get the core SDK: session management, wallet operations, SAMP encoding, chain submission. -## Hello World: Offline +## Offline Example -Create a wallet, derive keys, and encode a SAMP public remark -- no chain connection needed. +Create keys and encode a SAMP public remark without connecting to a node: -```rust -use taolk::secret::{Password, Phrase, Seed}; -use taolk::wallet; - -fn main() { - // Generate a fresh mnemonic and derive a seed - let phrase = Phrase::generate(); - println!("Recovery phrase: {}", phrase.words().join(" ")); - - let seed = Seed::from_phrase(&phrase); - let signing_key = seed.derive_signing_key(); - let pubkey = signing_key.public_key(); - println!("Public key: {pubkey:?}"); - - // Persist the seed in an encrypted wallet file - let password = Password::new("hunter2".into()); - wallet::create("demo", &password, &seed).expect("create wallet"); - - // Re-open the wallet to verify - let recovered = wallet::open("demo", &password).expect("open wallet"); - assert!(seed.ct_eq(&recovered)); - println!("Wallet round-trip OK"); -} -``` +See `examples/sdk_offline.rs`. It is compile-checked with `cargo test --example sdk_offline`. -## Hello World: Connected +## Connected Example -Open a wallet, start a session, send an encrypted message, and listen for events. +Open a wallet, start a session, send an encrypted message, and listen for events: -```rust -use taolk::secret::Password; -use taolk::{wallet, Event, Pubkey, Session}; - -#[tokio::main] -async fn main() { - // Open the wallet - let password = Password::new("hunter2".into()); - let seed = wallet::open("demo", &password).expect("open wallet"); - - // Start a session (keep_seed = true so we can encrypt later) - let (session, rx) = Session::start( - seed.as_bytes(), - "wss://entrypoint-finney.opentensor.ai:443", - "demo", - true, - ) - .await - .expect("start session"); - - println!("Running as {}", session.ss58()); - - // Send an encrypted message - let recipient = Pubkey([0xab; 32]); // replace with real pubkey - let body = taolk::MessageBody::from("hello"); - let remark = session - .build_encrypted_message(seed.as_bytes(), &recipient, &body) - .expect("build message"); - session.submit(&remark).await.expect("submit"); - - // Listen for events - while let Ok(event) = rx.recv() { - match event { - Event::MessageSent => println!("Message confirmed on-chain"), - Event::NewMessage { decrypted_body: Some(body), sender, .. } => { - println!("From {sender:?}: {body}"); - } - Event::Error(e) => eprintln!("Error: {e}"), - _ => {} - } - } -} -``` +See `examples/sdk_connected.rs`. It is compile-checked with `cargo test --example sdk_connected`. ## Session @@ -158,7 +93,11 @@ Appends an encrypted reply to the thread at index `thread_idx` in `session.threa #### `build_channel_create` ```rust -pub fn build_channel_create(&self, name: &str, description: &str) -> Result +pub fn build_channel_create( + &self, + name: &samp::ChannelName, + description: &samp::ChannelDescription, +) -> Result ``` Encodes a channel creation remark. `name` and `description` are plaintext and visible on-chain. The resulting extrinsic's `BlockRef` becomes the channel's permanent identifier. @@ -166,7 +105,7 @@ Encodes a channel creation remark. `name` and `description` are plaintext and vi #### `build_channel_message` ```rust -pub fn build_channel_message(&self, channel_idx: usize, body: &str) -> Result +pub fn build_channel_message(&self, channel_idx: usize, body: &MessageBody) -> Result ``` Encodes a plaintext message to the channel at index `channel_idx` in `session.channels`. Includes reply-to and continues refs for ordering. Returns `SdkError::NotFound` if the index is invalid. @@ -248,11 +187,11 @@ Events arrive on the `mpsc::Receiver` returned by `Session::start`. All ` | `BlockUpdate` | `u64` | New block number observed. | | `FetchBlock` | `block_ref: BlockRef` | Request to fetch a specific block (gap fill). | | `FetchChannelMirror` | `channel_ref: BlockRef` | Request to fetch channel history from a mirror. | -| `SubmitRemark` | `remark: Vec` | Internal: a remark needs to be submitted (used by mirror sync). | +| `SubmitRemark` | `remark: samp::RemarkBytes` | Internal: a remark needs to be submitted (used by mirror sync). | | `GapsRefreshed` | (none) | Thread/channel gap detection completed. | | `FeeEstimated` | `fee_display: String`, `fee_raw: Option` | Result of a fee estimation. | | `BalanceUpdated` | `u128` | Account balance changed. | -| `ChainSnapshotRefreshed` | `info: String`, `token_symbol: String`, `token_decimals: u32` | Chain metadata refreshed (occurs on connect and reconnect). | +| `ChainSnapshotRefreshed` | `info: ChainInfo`, `token_symbol: String`, `token_decimals: u32` | Chain metadata refreshed (occurs on connect and reconnect). | | `GenesisMismatch` | (none) | Connected node's genesis hash does not match the expected chain. | | `ConnectionStatus` | `ConnState` | Connection state change. `ConnState::Connected` or `ConnState::Reconnecting { in_secs }`. | | `Status` | `String` | Human-readable status message (e.g. "Connected to node"). | @@ -276,8 +215,8 @@ A 32-byte SR25519 public key, re-exported from the `samp` crate. Construct from bytes: ```rust -let pk = Pubkey([0xab; 32]); -let pk2 = Pubkey::from(some_bytes); +let pk = Pubkey::from_bytes([0xab; 32]); +let pk2 = Pubkey::from_bytes(some_bytes); ``` ### BlockRef @@ -323,9 +262,9 @@ Wraps `Zeroizing`. | Method | Description | |---|---| -| `generate()` | Random 12-word BIP-39 mnemonic | +| `generate() -> Result` | Random 12-word BIP-39 mnemonic | | `parse(&str)` | Parse and validate a mnemonic | -| `words() -> Vec<&str>` | Split into word list | +| `words() -> &str` | Borrow the mnemonic words | #### SigningKey @@ -342,27 +281,31 @@ Wraps a schnorrkel `Keypair`. pub enum SdkError { Encryption(String), Decryption(String), - InvalidAddress(String), - Chain(String), - NotFound(String), + Address(AddressError), + Chain(ChainError), + Wallet(WalletError), + Config(ConfigError), + Metadata(samp::metadata::Error), Database(String), - Wallet(String), + NotFound(String), Other(String), } ``` -All variants carry a `String` message. The SDK uses `type Result = std::result::Result`. +The SDK uses `type Result = std::result::Result`. | Variant | When it occurs | |---|---| | `Encryption` | `build_encrypted_message`, `build_thread_root`, `build_thread_reply`, `build_group_create`, `build_group_message` -- invalid recipient key or internal crypto failure. | | `Decryption` | Decryption of an incoming message failed. | -| `InvalidAddress` | An SS58 address could not be parsed. | +| `Address` | An SS58 address could not be parsed. | | `Chain` | RPC call to the subtensor node failed (`submit`, `fetch_balance`, `estimate_fee`, `start`). | | `NotFound` | Index out of bounds for `build_thread_reply`, `build_channel_message`, `build_group_message`. | | `Database` | SQLite error during `start` or persistence operations. | -| `Wallet` | Invalid seed bytes passed to `start` or `start_with_mirrors`. | -| `Other` | Everything else (e.g. `build_channel_create` encoding failure). | +| `Wallet` | Wallet open/create errors. | +| `Config` | Invalid or unwritable configuration. | +| `Metadata` | Runtime metadata parse/layout errors. | +| `Other` | Everything else. | ## Wallet @@ -414,9 +357,9 @@ Available keys: | `security.lock_timeout` | u64 | `300` | | `security.require_password_per_send` | bool | `false` | | `notifications.enabled` | bool | `true` | -| `notifications.volume` | u8 | `100` | +| `notifications.volume` | u8 | `70` | | `notifications.dm` | bool | `true` | -| `notifications.ambient` | bool | `false` | +| `notifications.ambient` | bool | `true` | | `notifications.mention` | bool | `true` | | `ui.sidebar_width` | u16 | `28` | | `ui.mouse` | bool | `true` | diff --git a/examples/sdk_connected.rs b/examples/sdk_connected.rs new file mode 100644 index 0000000..a80af7f --- /dev/null +++ b/examples/sdk_connected.rs @@ -0,0 +1,41 @@ +use taolk::event::Event; +use taolk::secret::Password; +use taolk::session::Session; +use taolk::types::{MessageBody, Pubkey}; +use taolk::wallet; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let password = Password::new("hunter2".into()); + let seed = wallet::open("demo", &password)?; + + let (session, rx) = Session::start( + seed.as_bytes(), + "wss://entrypoint-finney.opentensor.ai:443", + "demo", + true, + ) + .await?; + + println!("Running as {}", session.ss58()); + + let recipient = Pubkey::from_bytes([0xab; 32]); + let body = MessageBody::parse("hello")?; + let remark = session.build_encrypted_message(seed.as_bytes(), &recipient, &body)?; + session.submit(&remark).await?; + + while let Ok(event) = rx.recv() { + match event { + Event::MessageSent => println!("Message confirmed on-chain"), + Event::NewMessage { + decrypted_body: Some(body), + sender, + .. + } => println!("From {sender:?}: {body}"), + Event::Error(e) => eprintln!("Error: {e}"), + _ => {} + } + } + + Ok(()) +} diff --git a/examples/sdk_offline.rs b/examples/sdk_offline.rs new file mode 100644 index 0000000..ad4b8d8 --- /dev/null +++ b/examples/sdk_offline.rs @@ -0,0 +1,15 @@ +use taolk::secret::{Phrase, Seed}; +use taolk::types::MessageBody; + +fn main() -> Result<(), Box> { + let phrase = Phrase::generate()?; + let seed = Seed::from_phrase(&phrase); + let signing_key = seed.derive_signing_key(); + let recipient = signing_key.public_key(); + + let body = MessageBody::parse("hello from the SDK")?; + let remark = samp::encode_public(&recipient, body.as_str()); + assert!(samp::is_samp_remark(remark.as_bytes())); + + Ok(()) +} diff --git a/src/app.rs b/src/app.rs index 4ebbcbd..491e18e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -513,7 +513,7 @@ impl App { View::ChannelDir => {} } let mut entries: Vec<(String, BlockRef)> = last_seen.into_iter().collect(); - entries.sort_by(|a, b| b.1.cmp(&a.1)); + entries.sort_by_key(|(_, at)| std::cmp::Reverse(*at)); entries .into_iter() .map(|(ss58, _)| { diff --git a/src/main.rs b/src/main.rs index 8a73c2f..3576c31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,18 +50,13 @@ impl TuiEventHandler { std::thread::spawn(move || { loop { if term_event::poll(tick_rate).unwrap_or(false) { - match term_event::read() { - Ok(TermEvent::Key(key)) => { - if poll_tx.send(TuiEvent::Key(key)).is_err() { - return; - } - } - Ok(TermEvent::Mouse(mouse)) => { - if poll_tx.send(TuiEvent::Mouse(mouse)).is_err() { - return; - } - } - _ => {} + let event = match term_event::read() { + Ok(TermEvent::Key(key)) => Some(TuiEvent::Key(key)), + Ok(TermEvent::Mouse(mouse)) => Some(TuiEvent::Mouse(mouse)), + Ok(_) | Err(_) => None, + }; + if event.is_some_and(|event| poll_tx.send(event).is_err()) { + return; } } if poll_tx.send(TuiEvent::Tick).is_err() { @@ -2303,26 +2298,22 @@ fn handle_search_key(app: &mut App, key: crossterm::event::KeyEvent) { fn handle_sender_picker_key(app: &mut App, key: crossterm::event::KeyEvent) { let len = app.picker_senders.len(); - match key.code { - KeyCode::Esc => { + match (key.code, len) { + (KeyCode::Esc, _) => { app.picker_senders.clear(); app.close_overlay(); } - KeyCode::Up | KeyCode::Char('k') => { - if len > 0 { - app.contact_idx = if app.contact_idx == 0 { - len - 1 - } else { - app.contact_idx - 1 - }; - } + (KeyCode::Up | KeyCode::Char('k'), 1..) => { + app.contact_idx = if app.contact_idx == 0 { + len - 1 + } else { + app.contact_idx - 1 + }; } - KeyCode::Down | KeyCode::Char('j') | KeyCode::Tab => { - if len > 0 { - app.contact_idx = (app.contact_idx + 1) % len; - } + (KeyCode::Down | KeyCode::Char('j') | KeyCode::Tab, 1..) => { + app.contact_idx = (app.contact_idx + 1) % len; } - KeyCode::Enter => { + (KeyCode::Enter, _) => { if let Some((short, pk)) = app.picker_senders.get(app.contact_idx).cloned() { copy_sender(app, &short, pk.as_ref()); } diff --git a/src/ui/overlay/jump.rs b/src/ui/overlay/jump.rs index 7f78638..8a4350a 100644 --- a/src/ui/overlay/jump.rs +++ b/src/ui/overlay/jump.rs @@ -138,7 +138,7 @@ impl JumpState { ranked.push(RankedTarget { idx, score }); } } - ranked.sort_by(|a, b| b.score.cmp(&a.score)); + ranked.sort_by_key(|target| std::cmp::Reverse(target.score)); self.ranking = ranked; if self.cursor >= self.ranking.len() { self.cursor = self.ranking.len().saturating_sub(1); diff --git a/src/ui/overlay/palette.rs b/src/ui/overlay/palette.rs index 750ecb9..b364fd3 100644 --- a/src/ui/overlay/palette.rs +++ b/src/ui/overlay/palette.rs @@ -88,7 +88,7 @@ impl PaletteState { ranked.push(RankedCommand { idx, score }); } } - ranked.sort_by(|a, b| b.score.cmp(&a.score)); + ranked.sort_by_key(|command| std::cmp::Reverse(command.score)); self.ranking = ranked; if self.cursor >= self.ranking.len() { self.cursor = self.ranking.len().saturating_sub(1); diff --git a/tests/compile_fail/password_display.rs b/tests/compile_fail/password_display.rs index 9fd1f8f..3e89767 100644 --- a/tests/compile_fail/password_display.rs +++ b/tests/compile_fail/password_display.rs @@ -1,4 +1,4 @@ fn main() { let p = taolk::secret::Password::new("hunter2".to_string()); - println!("{p}"); + let _display: &dyn std::fmt::Display = &p; } diff --git a/tests/compile_fail/password_display.stderr b/tests/compile_fail/password_display.stderr index b0cf98e..d78d9bf 100644 --- a/tests/compile_fail/password_display.stderr +++ b/tests/compile_fail/password_display.stderr @@ -1,9 +1,7 @@ error[E0277]: `Password` doesn't implement `std::fmt::Display` - --> tests/compile_fail/password_display.rs:3:15 + --> tests/compile_fail/password_display.rs:3:44 | -3 | println!("{p}"); - | ^^^ `Password` cannot be formatted with the default formatter +3 | let _display: &dyn std::fmt::Display = &p; + | ^^ the trait `std::fmt::Display` is not implemented for `Password` | - = help: the trait `std::fmt::Display` is not implemented for `Password` - = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead - = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: required for the cast from `&Password` to `&dyn std::fmt::Display` diff --git a/tests/compile_fail/seed_debug.rs b/tests/compile_fail/seed_debug.rs index baa235d..8c53e99 100644 --- a/tests/compile_fail/seed_debug.rs +++ b/tests/compile_fail/seed_debug.rs @@ -1,4 +1,4 @@ fn main() { let seed = taolk::secret::Seed::from_bytes([0u8; 32]); - println!("{seed:?}"); + let _debug: &dyn std::fmt::Debug = &seed; } diff --git a/tests/compile_fail/seed_debug.stderr b/tests/compile_fail/seed_debug.stderr index ffc4e87..cec209f 100644 --- a/tests/compile_fail/seed_debug.stderr +++ b/tests/compile_fail/seed_debug.stderr @@ -1,8 +1,7 @@ error[E0277]: `Seed` doesn't implement `Debug` - --> tests/compile_fail/seed_debug.rs:3:15 + --> tests/compile_fail/seed_debug.rs:3:40 | -3 | println!("{seed:?}"); - | ^^^^^^^^ `Seed` cannot be formatted using `{:?}` because it doesn't implement `Debug` +3 | let _debug: &dyn std::fmt::Debug = &seed; + | ^^^^^ the trait `Debug` is not implemented for `Seed` | - = help: the trait `Debug` is not implemented for `Seed` - = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: required for the cast from `&Seed` to `&dyn Debug`