pub mod client;
pub mod tx;
#[cfg(test)]
mod tests;
pub use client::{Client, Status, TxStatus};
pub use tx::{Kind, Tx};
use mudra::SigningKey;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
Encoding(String),
Rule(String),
Key(String),
Http(String),
Rejected(String),
Timeout(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Encoding(m) => write!(f, "encoding: {m}"),
Error::Rule(m) => write!(f, "rule: {m}"),
Error::Key(m) => write!(f, "key: {m}"),
Error::Http(m) => write!(f, "http: {m}"),
Error::Rejected(m) => write!(f, "rejected: {m}"),
Error::Timeout(m) => write!(f, "timeout: {m}"),
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedTx {
pub chain: [u8; 32],
pub bytes: Vec<u8>,
pub txid: [u8; 32],
pub tx: Tx,
}
impl SignedTx {
pub fn txid_hex(&self) -> String {
hex(&self.txid)
}
}
#[derive(Clone)]
pub struct Signer {
key: SigningKey,
pub pubkey: [u8; 33],
pub neuron: [u8; 32],
pub account_id: [u8; 20],
}
impl Signer {
pub fn from_key(key: SigningKey) -> Self {
let pubkey = mudra::cosmos::compressed(key.verifying_key());
let neuron = tx::neuron_of(&pubkey);
let account_id = mudra::cosmos::account_id(&pubkey);
Self { key, pubkey, neuron, account_id }
}
pub fn from_secret(secret: &[u8; 32]) -> Result<Self, Error> {
SigningKey::from_bytes(secret.into())
.map(Self::from_key)
.map_err(|e| Error::Key(e.to_string()))
}
pub fn from_spell(spell: &str, passphrase: &str) -> Result<Self, Error> {
mudra::spell::cosmos_key(spell.trim(), passphrase)
.map(Self::from_key)
.map_err(|e| Error::Key(e.to_string()))
}
pub fn generate_spell() -> Result<String, Error> {
mudra::spell::generate().map_err(|e| Error::Key(e.to_string()))
}
pub fn address(&self, hrp: &str) -> Result<String, Error> {
mudra::cosmos::address(&self.pubkey, hrp).map_err(|e| Error::Key(e.to_string()))
}
pub fn neuron_hex(&self) -> String {
hex(&self.neuron)
}
pub fn legacy_owner(&self, hrp: &str) -> [u8; 32] {
tx::legacy_owner(hrp, &self.account_id)
}
pub fn sign(&self, chain: &[u8; 32], tx: Tx) -> Result<SignedTx, Error> {
tx.check_rules()?;
let subject = match &tx {
Tx::Claim { .. } => self.neuron,
Tx::Coinbase { .. } => return Err(Error::Rule("a coinbase is not signed".into())),
other => other.from().expect("signed kinds 1, 2, 6, 7 carry from"),
};
let mut bytes = tx.encode_unsigned();
let statement = tx::statement(chain, &bytes);
let sig = mudra::neuron::sign(&self.key, subject, statement)
.map_err(|e| Error::Key(e.to_string()))?;
bytes.extend_from_slice(&sig);
let txid = tx::txid(chain, &bytes);
Ok(SignedTx { chain: *chain, bytes, txid, tx })
}
pub fn transfer(
&self,
chain: &[u8; 32],
nonce: u64,
token: [u8; 32],
amount: u64,
to: [u8; 32],
fee: Option<u64>,
) -> Result<SignedTx, Error> {
let fee = fee.unwrap_or(Kind::Transfer.min_fee());
self.sign(chain, Tx::Transfer { from: self.neuron, nonce, token, amount, to, fee })
}
#[allow(clippy::too_many_arguments)]
pub fn cyberlink(
&self,
chain: &[u8; 32],
nonce: u64,
p: [u8; 32],
q: [u8; 32],
valence: i8,
token: [u8; 32],
amount: u64,
fee: Option<u64>,
) -> Result<SignedTx, Error> {
let fee = fee.unwrap_or(Kind::Cyberlink.min_fee());
self.sign(
chain,
Tx::Cyberlink { from: self.neuron, nonce, token, amount, p, q, valence, fee },
)
}
pub fn unlock(&self, chain: &[u8; 32], nonce: u64, p: [u8; 32], q: [u8; 32], amount: u64, fee: Option<u64>) -> Result<SignedTx, Error> {
let fee = fee.unwrap_or(Kind::Unlock.min_fee());
self.sign(chain, Tx::Unlock { from: self.neuron, nonce, p, q, amount, fee })
}
pub fn payout(&self, chain: &[u8; 32], nonce: u64, p: [u8; 32], q: [u8; 32], payout: [u8; 32], fee: Option<u64>) -> Result<SignedTx, Error> {
let fee = fee.unwrap_or(Kind::Payout.min_fee());
self.sign(chain, Tx::Payout { from: self.neuron, nonce, p, q, payout, fee })
}
pub fn claim(&self, chain: &[u8; 32], nonce: u64, fee: Option<u64>) -> Result<SignedTx, Error> {
let fee = fee.unwrap_or(Kind::Claim.min_fee());
self.sign(chain, Tx::Claim { account_id: self.account_id, nonce, fee })
}
}
pub fn verify(chain: &[u8; 32], bytes: &[u8]) -> Result<(Tx, Option<[u8; 32]>), Error> {
let (tx, sig) = Tx::decode(bytes)?;
tx.check_rules()?;
let Some(sig) = sig else {
return Ok((tx, None));
};
let unsigned = &bytes[..bytes.len() - tx::SIG_BYTES];
let statement = tx::statement(chain, unsigned);
let mut pubkey = [0u8; 33];
pubkey.copy_from_slice(&sig[5..38]);
let signer = tx::neuron_of(&pubkey);
let subject = match &tx {
Tx::Transfer { from, .. } | Tx::Cyberlink { from, .. } | Tx::Unlock { from, .. } | Tx::Payout { from, .. } => *from,
Tx::Claim { account_id, .. } => {
if mudra::cosmos::account_id(&pubkey) != *account_id {
return Err(Error::Key("claim account id is not derived by the signing key".into()));
}
signer
}
Tx::Coinbase { .. } => unreachable!("coinbase carries no signature"),
};
if !mudra::neuron::verify_statement(subject, statement, &sig) {
return Err(Error::Key("NSIG1 signature does not verify for this chain and subject".into()));
}
Ok((tx, Some(signer)))
}
pub fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn parse32(s: &str) -> Result<[u8; 32], Error> {
let s = s.strip_prefix("0x").unwrap_or(s);
if s.len() != 64 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(Error::Encoding(format!("expected 64 hex digits, got {s:?}")));
}
let mut out = [0u8; 32];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("hex digits");
}
Ok(out)
}
pub fn card(text: &str) -> [u8; 32] {
parse32(text).unwrap_or_else(|_| *hemera::hash(text.as_bytes()).as_bytes())
}
pub fn token_arg(text: &str) -> [u8; 32] {
parse32(text).unwrap_or_else(|_| tx::token(text))
}