neco-secp 0.1.1

minimum dependency secp256k1 and Nostr signing core
Documentation
use crate::{
    EventId, NAddr, NEvent, NProfile, NRelay, Nip19, SecpError, SecretKey, XOnlyPublicKey,
};
use bech32::{self, FromBase32, ToBase32, Variant};

pub fn encode_npub(pubkey: &XOnlyPublicKey) -> Result<String, SecpError> {
    bech32::encode("npub", pubkey.to_bytes().to_base32(), Variant::Bech32)
        .map_err(|_| SecpError::InvalidNip19("failed to encode npub"))
}

pub fn encode_nsec(secret: &SecretKey) -> Result<String, SecpError> {
    bech32::encode("nsec", secret.to_bytes().to_base32(), Variant::Bech32)
        .map_err(|_| SecpError::InvalidNip19("failed to encode nsec"))
}

pub fn encode_note(id: &EventId) -> Result<String, SecpError> {
    bech32::encode("note", id.to_bytes().to_base32(), Variant::Bech32)
        .map_err(|_| SecpError::InvalidNip19("failed to encode note"))
}

pub fn encode_nprofile(profile: &NProfile) -> Result<String, SecpError> {
    encode_tlv_entity(
        "nprofile",
        &[
            (0, vec![profile.pubkey.to_bytes().to_vec()]),
            (
                1,
                profile
                    .relays
                    .iter()
                    .map(|relay| relay.as_bytes().to_vec())
                    .collect(),
            ),
        ],
    )
}

pub fn encode_nevent(event: &NEvent) -> Result<String, SecpError> {
    let mut fields = vec![
        (0, vec![event.id.to_bytes().to_vec()]),
        (
            1,
            event
                .relays
                .iter()
                .map(|relay| relay.as_bytes().to_vec())
                .collect(),
        ),
    ];

    if let Some(author) = event.author {
        fields.push((2, vec![author.to_bytes().to_vec()]));
    }
    if let Some(kind) = event.kind {
        fields.push((3, vec![kind.to_be_bytes().to_vec()]));
    }

    encode_tlv_entity("nevent", &fields)
}

pub fn encode_naddr(addr: &NAddr) -> Result<String, SecpError> {
    encode_tlv_entity(
        "naddr",
        &[
            (0, vec![addr.identifier.as_bytes().to_vec()]),
            (
                1,
                addr.relays
                    .iter()
                    .map(|relay| relay.as_bytes().to_vec())
                    .collect(),
            ),
            (2, vec![addr.author.to_bytes().to_vec()]),
            (3, vec![addr.kind.to_be_bytes().to_vec()]),
        ],
    )
}

pub fn encode_nrelay(relay: &NRelay) -> Result<String, SecpError> {
    encode_tlv_entity("nrelay", &[(0, vec![relay.relay.as_bytes().to_vec()])])
}

pub fn decode(s: &str) -> Result<Nip19, SecpError> {
    let (hrp, data, variant) =
        bech32::decode(s).map_err(|_| SecpError::InvalidNip19("invalid bech32 string"))?;
    if variant != Variant::Bech32 {
        return Err(SecpError::InvalidNip19("unexpected bech32 variant"));
    }

    let bytes = Vec::<u8>::from_base32(&data)
        .map_err(|_| SecpError::InvalidNip19("invalid bech32 payload"))?;

    match hrp.as_str() {
        "npub" => {
            let payload: [u8; 32] = bytes
                .as_slice()
                .try_into()
                .map_err(|_| SecpError::InvalidNip19("expected 32-byte payload"))?;
            Ok(Nip19::Npub(XOnlyPublicKey::from_bytes(payload)?))
        }
        "nsec" => {
            let payload: [u8; 32] = bytes
                .as_slice()
                .try_into()
                .map_err(|_| SecpError::InvalidNip19("expected 32-byte payload"))?;
            Ok(Nip19::Nsec(SecretKey::from_bytes(payload)?))
        }
        "note" => {
            let payload: [u8; 32] = bytes
                .as_slice()
                .try_into()
                .map_err(|_| SecpError::InvalidNip19("expected 32-byte payload"))?;
            Ok(Nip19::Note(EventId::from_bytes(payload)))
        }
        "nprofile" => decode_nprofile(&bytes),
        "nevent" => decode_nevent(&bytes),
        "naddr" => decode_naddr(&bytes),
        "nrelay" => decode_nrelay(&bytes),
        _ => Err(SecpError::InvalidNip19("unsupported nip19 prefix")),
    }
}

fn decode_nprofile(bytes: &[u8]) -> Result<Nip19, SecpError> {
    let tlv = parse_tlv(bytes)?;
    let pubkey = required_bytes32(&tlv, 0, "nprofile")?;
    let relays = utf8_entries(&tlv, 1, "nprofile")?;
    Ok(Nip19::NProfile(NProfile {
        pubkey: XOnlyPublicKey::from_bytes(pubkey)?,
        relays,
    }))
}

fn decode_nevent(bytes: &[u8]) -> Result<Nip19, SecpError> {
    let tlv = parse_tlv(bytes)?;
    let id = required_bytes32(&tlv, 0, "nevent")?;
    let relays = utf8_entries(&tlv, 1, "nevent")?;
    let author = optional_bytes32(&tlv, 2, "nevent")?
        .map(XOnlyPublicKey::from_bytes)
        .transpose()?;
    let kind = optional_u32(&tlv, 3, "nevent")?;

    Ok(Nip19::NEvent(NEvent {
        id: EventId::from_bytes(id),
        relays,
        author,
        kind,
    }))
}

fn decode_naddr(bytes: &[u8]) -> Result<Nip19, SecpError> {
    let tlv = parse_tlv(bytes)?;
    let identifier = required_utf8(&tlv, 0, "naddr")?;
    let relays = utf8_entries(&tlv, 1, "naddr")?;
    let author = required_bytes32(&tlv, 2, "naddr")?;
    let kind = required_u32(&tlv, 3, "naddr")?;

    Ok(Nip19::NAddr(NAddr {
        identifier,
        relays,
        author: XOnlyPublicKey::from_bytes(author)?,
        kind,
    }))
}

fn decode_nrelay(bytes: &[u8]) -> Result<Nip19, SecpError> {
    let tlv = parse_tlv(bytes)?;
    let relay = required_utf8(&tlv, 0, "nrelay")?;
    Ok(Nip19::NRelay(NRelay { relay }))
}

fn encode_tlv_entity(prefix: &str, fields: &[(u8, Vec<Vec<u8>>)]) -> Result<String, SecpError> {
    let tlv = encode_tlv(fields)?;
    bech32::encode(prefix, tlv.to_base32(), Variant::Bech32)
        .map_err(|_| SecpError::InvalidNip19("failed to encode tlv entity"))
}

fn encode_tlv(fields: &[(u8, Vec<Vec<u8>>)]) -> Result<Vec<u8>, SecpError> {
    let mut out = Vec::new();
    for (tag, values) in fields.iter().rev() {
        for value in values {
            let len: u8 = value
                .len()
                .try_into()
                .map_err(|_| SecpError::InvalidNip19("tlv value too long"))?;
            out.push(*tag);
            out.push(len);
            out.extend_from_slice(value);
        }
    }
    Ok(out)
}

fn parse_tlv(bytes: &[u8]) -> Result<Vec<Vec<Vec<u8>>>, SecpError> {
    let mut tlv = vec![Vec::new(); 256];
    let mut offset = 0usize;
    while offset < bytes.len() {
        if offset + 2 > bytes.len() {
            return Err(SecpError::InvalidNip19("truncated tlv header"));
        }
        let tag = bytes[offset] as usize;
        let len = bytes[offset + 1] as usize;
        offset += 2;
        if offset + len > bytes.len() {
            return Err(SecpError::InvalidNip19("not enough data for tlv entry"));
        }
        tlv[tag].push(bytes[offset..offset + len].to_vec());
        offset += len;
    }
    Ok(tlv)
}

fn required_bytes32(
    tlv: &[Vec<Vec<u8>>],
    tag: usize,
    entity: &'static str,
) -> Result<[u8; 32], SecpError> {
    let value = tlv[tag]
        .first()
        .ok_or(SecpError::InvalidNip19(missing_required_field(entity, tag)))?;
    value
        .as_slice()
        .try_into()
        .map_err(|_| SecpError::InvalidNip19(expected_length(entity, tag, 32)))
}

fn optional_bytes32(
    tlv: &[Vec<Vec<u8>>],
    tag: usize,
    entity: &'static str,
) -> Result<Option<[u8; 32]>, SecpError> {
    tlv[tag]
        .first()
        .map(|value| {
            value
                .as_slice()
                .try_into()
                .map_err(|_| SecpError::InvalidNip19(expected_length(entity, tag, 32)))
        })
        .transpose()
}

fn required_u32(tlv: &[Vec<Vec<u8>>], tag: usize, entity: &'static str) -> Result<u32, SecpError> {
    optional_u32(tlv, tag, entity)?
        .ok_or(SecpError::InvalidNip19(missing_required_field(entity, tag)))
}

fn optional_u32(
    tlv: &[Vec<Vec<u8>>],
    tag: usize,
    entity: &'static str,
) -> Result<Option<u32>, SecpError> {
    tlv[tag]
        .first()
        .map(|value| {
            let bytes: [u8; 4] = value
                .as_slice()
                .try_into()
                .map_err(|_| SecpError::InvalidNip19(expected_length(entity, tag, 4)))?;
            Ok(u32::from_be_bytes(bytes))
        })
        .transpose()
}

fn required_utf8(
    tlv: &[Vec<Vec<u8>>],
    tag: usize,
    entity: &'static str,
) -> Result<String, SecpError> {
    let value = tlv[tag]
        .first()
        .ok_or(SecpError::InvalidNip19(missing_required_field(entity, tag)))?;
    String::from_utf8(value.clone()).map_err(|_| SecpError::InvalidNip19("invalid utf-8 payload"))
}

fn utf8_entries(
    tlv: &[Vec<Vec<u8>>],
    tag: usize,
    _entity: &'static str,
) -> Result<Vec<String>, SecpError> {
    tlv[tag]
        .iter()
        .map(|value| {
            String::from_utf8(value.clone())
                .map_err(|_| SecpError::InvalidNip19("invalid utf-8 payload"))
        })
        .collect()
}

fn missing_required_field(entity: &'static str, tag: usize) -> &'static str {
    match (entity, tag) {
        ("nprofile", 0) => "missing TLV 0 for nprofile",
        ("nevent", 0) => "missing TLV 0 for nevent",
        ("naddr", 0) => "missing TLV 0 for naddr",
        ("naddr", 2) => "missing TLV 2 for naddr",
        ("naddr", 3) => "missing TLV 3 for naddr",
        ("nrelay", 0) => "missing TLV 0 for nrelay",
        _ => "missing required tlv field",
    }
}

fn expected_length(entity: &'static str, tag: usize, len: usize) -> &'static str {
    match (entity, tag, len) {
        ("nprofile", 0, 32) => "TLV 0 should be 32 bytes",
        ("nevent", 0, 32) => "TLV 0 should be 32 bytes",
        ("nevent", 2, 32) => "TLV 2 should be 32 bytes",
        ("nevent", 3, 4) => "TLV 3 should be 4 bytes",
        ("naddr", 2, 32) => "TLV 2 should be 32 bytes",
        ("naddr", 3, 4) => "TLV 3 should be 4 bytes",
        _ => "invalid tlv length",
    }
}