Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
121 changes: 32 additions & 89 deletions docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -158,15 +93,19 @@ 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<RemarkBytes>
pub fn build_channel_create(
&self,
name: &samp::ChannelName,
description: &samp::ChannelDescription,
) -> Result<RemarkBytes>
```

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.

#### `build_channel_message`

```rust
pub fn build_channel_message(&self, channel_idx: usize, body: &str) -> Result<RemarkBytes>
pub fn build_channel_message(&self, channel_idx: usize, body: &MessageBody) -> Result<RemarkBytes>
```

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.
Expand Down Expand Up @@ -248,11 +187,11 @@ Events arrive on the `mpsc::Receiver<Event>` 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<u8>` | 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<u128>` | 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"). |
Expand All @@ -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
Expand Down Expand Up @@ -323,9 +262,9 @@ Wraps `Zeroizing<String>`.

| Method | Description |
|---|---|
| `generate()` | Random 12-word BIP-39 mnemonic |
| `generate() -> Result<Phrase, PhraseError>` | 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

Expand All @@ -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<T> = std::result::Result<T, SdkError>`.
The SDK uses `type Result<T> = std::result::Result<T, SdkError>`.

| 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

Expand Down Expand Up @@ -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` |
Expand Down
41 changes: 41 additions & 0 deletions examples/sdk_connected.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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(())
}
15 changes: 15 additions & 0 deletions examples/sdk_offline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use taolk::secret::{Phrase, Seed};
use taolk::types::MessageBody;

fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())
}
2 changes: 1 addition & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, _)| {
Expand Down
45 changes: 18 additions & 27 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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());
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/overlay/jump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/ui/overlay/palette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tests/compile_fail/password_display.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
fn main() {
let p = taolk::secret::Password::new("hunter2".to_string());
println!("{p}");
let _display: &dyn std::fmt::Display = &p;
}
10 changes: 4 additions & 6 deletions tests/compile_fail/password_display.stderr
Original file line number Diff line number Diff line change
@@ -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`
2 changes: 1 addition & 1 deletion tests/compile_fail/seed_debug.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
fn main() {
let seed = taolk::secret::Seed::from_bytes([0u8; 32]);
println!("{seed:?}");
let _debug: &dyn std::fmt::Debug = &seed;
}
9 changes: 4 additions & 5 deletions tests/compile_fail/seed_debug.stderr
Original file line number Diff line number Diff line change
@@ -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`
Loading