use core::fmt;
use core::str::FromStr;
use bitcoin::secp256k1::SecretKey;
use sidestr_nostr::event::{Event, UnsignedEvent};
use crate::account::{account_of, check_sats, js_trim, sats};
use crate::deposit::is_x;
use crate::error::{Error, Result};
use crate::REQUEST_KIND;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Op {
Join,
Withdraw,
Transfer,
}
impl Op {
pub fn as_str(self) -> &'static str {
match self {
Op::Join => "join",
Op::Withdraw => "withdraw",
Op::Transfer => "transfer",
}
}
}
impl fmt::Display for Op {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Op {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"join" => Ok(Op::Join),
"withdraw" => Ok(Op::Withdraw),
"transfer" => Ok(Op::Transfer),
_ => Err(Error::Op),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RequestParams<'a> {
pub ledger_hash: &'a str,
pub op: Op,
pub amount: Option<u64>,
pub to: Option<&'a str>,
pub id: &'a str,
}
impl<'a> RequestParams<'a> {
pub fn join(ledger_hash: &'a str, id: &'a str) -> Self {
Self {
ledger_hash,
op: Op::Join,
amount: None,
to: None,
id,
}
}
pub fn withdraw(ledger_hash: &'a str, amount: u64, to: &'a str, id: &'a str) -> Self {
Self {
ledger_hash,
op: Op::Withdraw,
amount: Some(amount),
to: Some(to),
id,
}
}
pub fn transfer(ledger_hash: &'a str, amount: u64, to: &'a str, id: &'a str) -> Self {
Self {
ledger_hash,
op: Op::Transfer,
amount: Some(amount),
to: Some(to),
id,
}
}
}
pub fn request_id(random: [u8; 16]) -> String {
hex::encode(random)
}
pub fn request_tags(p: &RequestParams<'_>) -> Result<Vec<Vec<String>>> {
if !is_x(p.ledger_hash) {
return Err(Error::RequestLedgerHash);
}
let mut tags = vec![
vec!["ledger".to_owned(), p.ledger_hash.to_owned()],
vec!["op".to_owned(), p.op.as_str().to_owned()],
vec!["id".to_owned(), p.id.to_owned()],
];
if p.op != Op::Join {
let amount = check_sats(p.amount.ok_or(Error::Amount)?)?;
tags.push(vec!["amount".to_owned(), amount.to_string()]);
let to = p.to.filter(|t| !t.is_empty()).ok_or(Error::NoDestination)?;
let to = if p.op == Op::Transfer {
account_of(to)?
} else {
js_trim(to).to_owned()
};
tags.push(vec!["to".to_owned(), to]);
}
Ok(tags)
}
pub fn request_template(p: &RequestParams<'_>, created_at: u64) -> Result<UnsignedEvent> {
Ok(UnsignedEvent {
pubkey: String::new(),
created_at,
kind: REQUEST_KIND,
tags: request_tags(p)?,
content: String::new(),
})
}
pub fn request_event(
account_secret: &SecretKey,
p: &RequestParams<'_>,
created_at: u64,
) -> Result<Event> {
crate::sign::sign_event(account_secret, request_template(p, created_at)?)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
pub id: String,
pub op: Op,
pub account: String,
pub created_at: u64,
pub amount: Option<u64>,
pub to: Option<String>,
pub event: Event,
}
pub fn parse_request(ev: &Event, ledger_hash: &str) -> Result<Request> {
if ev.kind != REQUEST_KIND {
return Err(Error::NotARequest);
}
if ev.verify().is_err() {
return Err(Error::Unverified);
}
let tag = |n: &str| {
ev.tags
.iter()
.find(|t| t.first().map(String::as_str) == Some(n))
.and_then(|t| t.get(1))
.map(String::as_str)
};
if tag("ledger") != Some(ledger_hash) {
return Err(Error::OtherLedger);
}
let op = tag("op").and_then(|o| o.parse::<Op>().ok());
let id = tag("id").filter(|id| {
(8..=64).contains(&id.len()) && id.bytes().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f'))
});
let (Some(op), Some(id)) = (op, id) else {
return Err(Error::OpAndId);
};
let mut r = Request {
id: id.to_owned(),
op,
account: format!("did:nostr:{}", ev.pubkey),
created_at: ev.created_at,
amount: None,
to: None,
event: ev.clone(),
};
if op != Op::Join {
r.amount = Some(sats(tag("amount").ok_or(Error::Amount)?)?);
let to = tag("to")
.filter(|t| !t.is_empty())
.ok_or(Error::NoDestination)?;
r.to = Some(if op == Op::Transfer {
account_of(to)?
} else {
to.to_owned()
});
}
Ok(r)
}