Skip to content
Closed
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
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,14 @@ serde_json = "1"
# the library itself still compile with the gates off.
[[example]]
name = "basic"
required-features = ["btc", "evm", "solana", "tron", "keccak"]
required-features = ["btc", "evm", "solana", "tron", "nano", "keccak"]

[[test]]
name = "public_api"
required-features = ["btc", "evm", "solana", "tron", "keccak"]

[features]
default = ["btc", "evm", "solana", "tron", "keccak", "net", "key", "asset", "client", "tx", "x402", "wire", "eip712", "abi"]
default = ["btc", "evm", "solana", "tron", "nano", "keccak", "net", "key", "asset", "client", "tx", "x402", "wire", "eip712", "abi"]
# Every gate below that names a module this crate no longer owns forwards to the
# crate that does. The names are unchanged on purpose: a consumer that asked for
# `tinywallet/btc` before the split gets exactly what it got before, from a
Expand All @@ -172,6 +172,8 @@ evm = ["tinywallet-crypto/evm"]
solana = ["tinywallet-crypto/solana"]
# Tron base58check address validation and hex conversion.
tron = ["tinywallet-crypto/tron"]
# Nano address validation and encoding.
nano = ["tinywallet-crypto/nano"]
# EIP-55 mixed-case checksum support for EVM addresses. `dep:sha3` is this
# crate's own, for the addresses `key` derivation produces.
keccak = ["evm", "dep:sha3", "tinywallet-crypto/keccak"]
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Agent-friendly multi-chain wallet primitives in Rust.

`tinywallet` owns the parts of wallet handling that are pure: address formats,
their validation, and the conversions between their encodings. Bitcoin, EVM
chains, Solana, and Tron each get a module, and `address::validate` dispatches
across them for chain-generic callers.
chains, Solana, Tron, and Nano each get a module, and `address::validate`
dispatches across them for chain-generic callers.

```rust
use tinywallet::{address, Chain};
Expand Down Expand Up @@ -40,6 +40,7 @@ because it is easy to assume otherwise:
| --- | --- | --- |
| Bitcoin | yes (base58check / bech32) | caught |
| Tron | yes (base58check) | caught |
| Nano | yes (blake2b, 40 bits) | caught |
| EVM | optional (EIP-55, only if mixed-case) | usually **not** caught |
| Solana | none | **not reliably** caught |

Expand Down Expand Up @@ -75,6 +76,7 @@ not pay for the others' parsers.
| `evm` | on | EVM addresses | — (dependency-free) |
| `solana` | on | Solana addresses | `bs58` |
| `tron` | on | Tron addresses | `bs58`, `hex` |
| `nano` | on | Nano addresses (`nano_` / legacy `xrb_`) | `blake2` |
| `keccak` | on | EIP-55 checksums for EVM | `sha3` |

With a chain's gate off, `address::validate` returns `Error::ChainNotCompiled`
Expand Down Expand Up @@ -170,7 +172,7 @@ must error, never validate):
```sh
cargo clippy --all-targets --no-default-features -- -D warnings
cargo test --lib --no-default-features
for f in btc evm solana tron keccak; do
for f in btc evm solana tron nano keccak; do
cargo check --lib --no-default-features --features "$f"
cargo test --lib --no-default-features --features "$f"
done
Expand Down
8 changes: 6 additions & 2 deletions crates/tinywallet-bus/src/chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@ pub enum Chain {
Solana,
/// Tron (mainnet).
Tron,
/// Nano (mainnet).
Nano,
}

impl Chain {
/// Every chain this crate knows, in declaration order.
///
/// Useful for a host enumerating supported chains. This is the full set
/// regardless of which feature gates are enabled — see the module docs.
pub const ALL: &'static [Self] = &[Self::Btc, Self::Evm, Self::Solana, Self::Tron];
pub const ALL: &'static [Self] = &[Self::Btc, Self::Evm, Self::Solana, Self::Tron, Self::Nano];

/// The chain's lowercase machine-readable name (`"btc"`, `"evm"`,
/// `"solana"`, `"tron"`).
/// `"solana"`, `"tron"`, `"nano"`).
///
/// This is the form [`Chain::from_str`] parses, so `chain.as_str()` always
/// round-trips.
Expand All @@ -59,6 +61,7 @@ impl Chain {
Self::Evm => "evm",
Self::Solana => "solana",
Self::Tron => "tron",
Self::Nano => "nano",
}
}
}
Expand Down Expand Up @@ -95,6 +98,7 @@ impl FromStr for Chain {
"evm" | "eth" | "ethereum" => Ok(Self::Evm),
"solana" | "sol" => Ok(Self::Solana),
"tron" | "trx" => Ok(Self::Tron),
"nano" | "xno" => Ok(Self::Nano),
other => Err(UnknownChain(other.to_string())),
}
}
Expand Down
13 changes: 13 additions & 0 deletions crates/tinywallet-bus/src/chain/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,16 @@ fn an_unknown_name_is_reported_with_the_input() {
UnknownChain("dogecoin".to_string())
);
}

#[test]
fn nano_parses_with_its_aliases() {
for input in ["nano", "xno", "NANO"] {
assert_eq!(
Chain::from_str(input).unwrap(),
Chain::Nano,
"alias {input}"
);
}
assert_eq!(Chain::Nano.as_str(), "nano");
assert!(Chain::ALL.contains(&Chain::Nano));
}
6 changes: 5 additions & 1 deletion crates/tinywallet-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ hex = { version = "0.4", optional = true }
bech32 = { version = "0.12", optional = true }
# SHA-256: Bitcoin's HASH160, Tron's base58check checksum, and the Tron txid.
sha2 = { version = "0.11", optional = true }
# Blake2b-40, the checksum on a Nano address; see `src/address/nano.rs`.
blake2 = { version = "0.11", default-features = false, optional = true }
# Keccak-256 for EIP-55 checksums on EVM addresses.
#
# Pinned to 0.12. The root crate's `coins-bip32` still depends on `sha3` 0.10,
Expand All @@ -57,7 +59,7 @@ serde_json = "1"

[features]
# Every feature the bus exposed before the split, so `default` matches it.
default = ["btc", "evm", "solana", "tron", "keccak", "net", "asset", "tx-codec"]
default = ["btc", "evm", "solana", "tron", "nano", "keccak", "net", "asset", "tx-codec"]
# Bitcoin address parsing and validation. Dependency-light on purpose: base58
# via `bs58` (already here for Solana and Tron) and segwit via `bech32`.
btc = ["dep:bs58", "dep:bech32", "dep:sha2"]
Expand All @@ -72,6 +74,8 @@ evm = []
solana = ["dep:bs58"]
# Tron base58check address validation and hex conversion.
tron = ["dep:bs58", "dep:hex"]
# Nano address validation and encoding.
nano = ["dep:blake2"]
# EIP-55 mixed-case checksum support for EVM addresses.
keccak = ["evm", "dep:sha3"]
# The `Transport` network seam (`tinywallet_crypto::rpc`). The one module that
Expand Down
15 changes: 13 additions & 2 deletions crates/tinywallet-crypto/src/address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! Each submodule owns one chain's address format and exposes the same core
//! shape: a `validate` returning the trimmed address, plus whatever
//! chain-specific conversions are genuinely useful (`solana::decode`,
//! `tron::to_hex`, `btc::validate_sender`).
//! `tron::to_hex`, `btc::validate_sender`, `nano::decode`).
//!
//! [`validate`] dispatches across all of them for chain-generic callers.
//!
Expand All @@ -20,6 +20,7 @@
//! | --- | --- | --- |
//! | Bitcoin | yes (base58check / bech32) | caught |
//! | Tron | yes (base58check) | caught |
//! | Nano | yes (blake2b, 40 bits) | caught |
//! | EVM | optional (EIP-55, only if mixed-case) | usually *not* caught |
//! | Solana | none | *not reliably* caught |
//!
Expand All @@ -34,6 +35,8 @@ use crate::chain::Chain;
pub mod btc;
#[cfg(feature = "evm")]
pub mod evm;
#[cfg(feature = "nano")]
pub mod nano;
#[cfg(feature = "solana")]
pub mod solana;
#[cfg(feature = "tron")]
Expand Down Expand Up @@ -72,7 +75,13 @@ pub mod tron;
// `address` goes unread. That build is legal (a host may depend on this crate
// purely for `Chain`), so the unused binding is expected rather than a bug.
#[cfg_attr(
not(any(feature = "btc", feature = "evm", feature = "solana", feature = "tron")),
not(any(
feature = "btc",
feature = "evm",
feature = "solana",
feature = "tron",
feature = "nano"
)),
allow(unused_variables)
)]
pub fn validate(chain: Chain, address: &str) -> Result<String> {
Expand All @@ -85,6 +94,8 @@ pub fn validate(chain: Chain, address: &str) -> Result<String> {
Chain::Solana => Some(solana::validate),
#[cfg(feature = "tron")]
Chain::Tron => Some(tron::validate),
#[cfg(feature = "nano")]
Chain::Nano => Some(nano::validate),
_ => None,
};
let validator = validator.ok_or(crate::Error::ChainNotCompiled { chain })?;
Expand Down
8 changes: 7 additions & 1 deletion crates/tinywallet-crypto/src/address/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ use super::validate;
use crate::{Chain, Error};

/// One valid mainnet address per chain.
const FIXTURES: [(Chain, &str); 4] = [
const FIXTURES: [(Chain, &str); 5] = [
(Chain::Btc, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"),
(Chain::Evm, "0x52908400098527886E0F7030069857D2E4169EE7"),
(Chain::Solana, "11111111111111111111111111111111"),
(Chain::Tron, "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
(
Chain::Nano,
"nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3",
),
];

/// Whether the feature gate behind `chain` is on in this build.
Expand All @@ -28,6 +32,8 @@ const fn chain_enabled(chain: Chain) -> bool {
Chain::Solana => true,
#[cfg(feature = "tron")]
Chain::Tron => true,
#[cfg(feature = "nano")]
Chain::Nano => true,
_ => false,
}
}
Expand Down
Loading
Loading