use crate::error::{QuantusError, Result};
use serde::{Deserialize, Serialize};
pub const SIGN_REQUEST_VERSION: u8 = 1;
const MAX_PAYLOAD_BYTES: usize = 8 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignRequest {
pub signer: String,
pub payload: Vec<u8>,
}
#[derive(Serialize, Deserialize)]
struct Wire {
v: u8,
signer: String,
payload: String,
}
impl SignRequest {
pub fn new(signer: impl Into<String>, payload: Vec<u8>) -> Self {
Self { signer: signer.into(), payload }
}
pub fn encode(&self) -> Vec<u8> {
let wire = Wire {
v: SIGN_REQUEST_VERSION,
signer: self.signer.clone(),
payload: format!("0x{}", hex::encode(&self.payload)),
};
serde_json::to_vec(&wire).expect("sign request serialises")
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
let wire: Wire = serde_json::from_slice(bytes).map_err(|e| {
QuantusError::Generic(format!(
"Not a signing request. A wallet built before the request envelope sends a bare \
payload, which cannot say which account it is for ({e})"
))
})?;
if wire.v != SIGN_REQUEST_VERSION {
return Err(QuantusError::Generic(format!(
"Unsupported signing request version: {} (this build reads {SIGN_REQUEST_VERSION})",
wire.v
)));
}
let hex_payload = wire.payload.strip_prefix("0x").ok_or_else(|| {
QuantusError::Generic("Signing request payload is not 0x hex".to_string())
})?;
let payload = hex::decode(hex_payload).map_err(|e| {
QuantusError::Generic(format!("Signing request payload is not hex: {e}"))
})?;
if payload.is_empty() {
return Err(QuantusError::Generic("Signing request payload is empty".to_string()));
}
if payload.len() > MAX_PAYLOAD_BYTES {
return Err(QuantusError::Generic(format!(
"Signing request payload too large: {} bytes",
payload.len()
)));
}
Ok(Self { signer: wire.signer, payload })
}
}
#[cfg(test)]
mod tests {
use super::*;
const ADDRESS: &str = "qznQKhufTDfU3szAzfgCny7wMhxUN3qjEqneiRUNgC7MjSDyG";
#[test]
fn round_trips_through_the_wire_format() {
let request = SignRequest::new(ADDRESS, vec![0x02, 0x00, 0xff]);
assert_eq!(SignRequest::decode(&request.encode()).unwrap(), request);
}
#[test]
fn carries_exactly_the_keys_the_wallets_read() {
let encoded = SignRequest::new(ADDRESS, vec![0xab]).encode();
let json: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
assert_eq!(json["v"], 1);
assert_eq!(json["signer"], ADDRESS);
assert_eq!(json["payload"], "0xab");
assert_eq!(json.as_object().unwrap().len(), 3, "a wallet refuses any other key set");
}
#[test]
fn refuses_a_bare_payload() {
let bare = vec![0x02, 0x00, 0x01, 0x02];
assert!(SignRequest::decode(&bare).is_err());
}
#[test]
fn refuses_a_version_it_does_not_read() {
let wire = serde_json::json!({ "v": 2, "signer": ADDRESS, "payload": "0xab" });
let error = SignRequest::decode(wire.to_string().as_bytes()).unwrap_err().to_string();
assert!(error.contains("version"), "unexpected error: {error}");
}
#[test]
fn refuses_a_payload_that_is_not_hex_bytes() {
for payload in ["", "0x", "abcd", "0xnothex"] {
let wire = serde_json::json!({ "v": 1, "signer": ADDRESS, "payload": payload });
assert!(
SignRequest::decode(wire.to_string().as_bytes()).is_err(),
"payload {payload:?} was accepted"
);
}
}
#[test]
fn refuses_a_payload_past_the_size_a_wallet_reads() {
let wire = serde_json::json!({
"v": 1,
"signer": ADDRESS,
"payload": format!("0x{}", hex::encode(vec![0u8; MAX_PAYLOAD_BYTES + 1])),
});
assert!(SignRequest::decode(wire.to_string().as_bytes()).is_err());
}
}