lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

nip17_conformance.rs (10950B)


      1 #![cfg(feature = "nip17")]
      2 
      3 mod support;
      4 
      5 use nostr::{Keys, SecretKey};
      6 use radroots_event::social::message::{Message, MessageRecipient};
      7 use radroots_event::social::message_file::{MessageFile, MessageFileDimensions};
      8 use radroots_nostr::nip17::{
      9     Rumor, WrapOptions, unwrap_gift_wrap, wrap_message, wrap_message_file,
     10 };
     11 use serde::Deserialize;
     12 use serde_json::Value;
     13 use std::{borrow::Cow, fs, path::Path};
     14 use support::{
     15     FIXTURE_ALICE_PUBLIC_KEY_HEX, FIXTURE_ALICE_SECRET_KEY_HEX, FIXTURE_BOB_PUBLIC_KEY_HEX,
     16     FIXTURE_BOB_SECRET_KEY_HEX,
     17 };
     18 
     19 const PACKAGED_VECTORS: &str = include_str!("fixtures/nip17_adapter.v1.json");
     20 const WORKSPACE_VECTOR_PATH: &str = "../../contracts/conformance/vectors/nip17/adapter.v1.json";
     21 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml";
     22 
     23 #[derive(Debug, Deserialize)]
     24 struct Suite {
     25     suite: String,
     26     contract_version: String,
     27     vectors: Vec<Vector>,
     28 }
     29 
     30 #[derive(Debug, Deserialize)]
     31 struct Vector {
     32     id: String,
     33     kind: String,
     34     input: Value,
     35     expected: Value,
     36 }
     37 
     38 #[tokio::test]
     39 async fn checked_in_nip17_vectors_execute_against_public_api() {
     40     let vectors = conformance_vectors();
     41     let suite: Suite = serde_json::from_str(&vectors).expect("NIP-17 vectors must parse");
     42     assert_eq!(suite.suite, "nip17_adapter");
     43     assert_eq!(suite.contract_version, "1.0.0");
     44     assert!(!suite.vectors.is_empty());
     45 
     46     for vector in &suite.vectors {
     47         execute_vector(vector).await;
     48     }
     49 }
     50 
     51 fn conformance_vectors() -> Cow<'static, str> {
     52     let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH);
     53     match fs::read_to_string(&workspace_path) {
     54         Ok(canonical) => {
     55             assert_eq!(
     56                 canonical,
     57                 PACKAGED_VECTORS,
     58                 "packaged NIP-17 vectors must match {}",
     59                 workspace_path.display()
     60             );
     61             Cow::Owned(canonical)
     62         }
     63         Err(error)
     64             if error.kind() == std::io::ErrorKind::NotFound
     65                 && !Path::new(env!("CARGO_MANIFEST_DIR"))
     66                     .join(WORKSPACE_CONTRACT_MARKER_PATH)
     67                     .is_file() =>
     68         {
     69             Cow::Borrowed(PACKAGED_VECTORS)
     70         }
     71         Err(error) => panic!("failed to read {}: {error}", workspace_path.display()),
     72     }
     73 }
     74 
     75 async fn execute_vector(vector: &Vector) {
     76     match vector.kind.as_str() {
     77         "nip17.message.roundtrip" => execute_message_roundtrip(vector).await,
     78         "nip17.message_file.roundtrip" => execute_message_file_roundtrip(vector).await,
     79         "nip17.message.invalid_recipient" => execute_invalid_recipient(vector).await,
     80         kind => panic!("{} uses unsupported vector kind {kind}", vector.id),
     81     }
     82 }
     83 
     84 async fn execute_message_roundtrip(vector: &Vector) {
     85     assert_eq!(
     86         input_str(vector, "recipient_public_key"),
     87         FIXTURE_BOB_PUBLIC_KEY_HEX
     88     );
     89     let sender = sender_keys();
     90     let receiver = receiver_keys();
     91     let message = Message {
     92         recipients: vec![MessageRecipient {
     93             public_key: input_str(vector, "recipient_public_key").to_owned(),
     94             relay_url: None,
     95         }],
     96         content: input_str(vector, "content").to_owned(),
     97         reply_to: None,
     98         subject: Some(input_str(vector, "subject").to_owned()),
     99     };
    100     let events = wrap_message(&sender, &message, options(vector))
    101         .await
    102         .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    103     assert_eq!(events.len(), expected_u64(vector, "event_count") as usize);
    104 
    105     let rumor = unwrap_gift_wrap(&receiver, &events[0])
    106         .await
    107         .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    108     let rendered = format!("{rumor:?}");
    109     for private_value in [
    110         message.content.as_str(),
    111         FIXTURE_ALICE_PUBLIC_KEY_HEX,
    112         FIXTURE_BOB_PUBLIC_KEY_HEX,
    113     ] {
    114         assert!(!rendered.contains(private_value), "{}", vector.id);
    115     }
    116     match rumor {
    117         Rumor::Message(metadata) => {
    118             assert_eq!(
    119                 metadata.author,
    120                 expected_str(vector, "author"),
    121                 "{}",
    122                 vector.id
    123             );
    124             assert_eq!(
    125                 metadata.published_at,
    126                 expected_u64(vector, "published_at"),
    127                 "{}",
    128                 vector.id
    129             );
    130             assert_eq!(
    131                 metadata.data.content,
    132                 expected_str(vector, "content"),
    133                 "{}",
    134                 vector.id
    135             );
    136             assert_eq!(
    137                 metadata.data.recipients.len(),
    138                 expected_u64(vector, "recipient_count") as usize,
    139                 "{}",
    140                 vector.id
    141             );
    142             assert_eq!(
    143                 metadata.data.subject.as_deref(),
    144                 Some(expected_str(vector, "subject")),
    145                 "{}",
    146                 vector.id
    147             );
    148         }
    149         other => panic!("{} expected a message rumor, got {other:?}", vector.id),
    150     }
    151 }
    152 
    153 async fn execute_message_file_roundtrip(vector: &Vector) {
    154     assert_eq!(
    155         input_str(vector, "recipient_public_key"),
    156         FIXTURE_BOB_PUBLIC_KEY_HEX
    157     );
    158     let sender = sender_keys();
    159     let receiver = receiver_keys();
    160     let message = MessageFile {
    161         recipients: vec![MessageRecipient {
    162             public_key: input_str(vector, "recipient_public_key").to_owned(),
    163             relay_url: None,
    164         }],
    165         file_url: input_str(vector, "file_url").to_owned(),
    166         reply_to: None,
    167         subject: None,
    168         file_type: input_str(vector, "file_type").to_owned(),
    169         encryption_algorithm: input_str(vector, "encryption_algorithm").to_owned(),
    170         decryption_key: input_str(vector, "decryption_key").to_owned(),
    171         decryption_nonce: input_str(vector, "decryption_nonce").to_owned(),
    172         encrypted_hash: input_str(vector, "encrypted_hash").to_owned(),
    173         original_hash: None,
    174         size: Some(input_u64(vector, "size")),
    175         dimensions: Some(MessageFileDimensions {
    176             w: input_u64(vector, "width") as u32,
    177             h: input_u64(vector, "height") as u32,
    178         }),
    179         blurhash: None,
    180         thumb: None,
    181         fallbacks: Vec::new(),
    182     };
    183     let events = wrap_message_file(&sender, &message, options(vector))
    184         .await
    185         .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    186     assert_eq!(events.len(), expected_u64(vector, "event_count") as usize);
    187 
    188     let rumor = unwrap_gift_wrap(&receiver, &events[0])
    189         .await
    190         .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    191     let rendered = format!("{rumor:?}");
    192     for private_value in [
    193         message.file_url.as_str(),
    194         message.decryption_key.as_str(),
    195         message.decryption_nonce.as_str(),
    196         message.encrypted_hash.as_str(),
    197         FIXTURE_ALICE_PUBLIC_KEY_HEX,
    198         FIXTURE_BOB_PUBLIC_KEY_HEX,
    199     ] {
    200         assert!(!rendered.contains(private_value), "{}", vector.id);
    201     }
    202     match rumor {
    203         Rumor::MessageFile(metadata) => {
    204             assert_eq!(
    205                 metadata.author,
    206                 expected_str(vector, "author"),
    207                 "{}",
    208                 vector.id
    209             );
    210             assert_eq!(
    211                 metadata.published_at,
    212                 expected_u64(vector, "published_at"),
    213                 "{}",
    214                 vector.id
    215             );
    216             assert_eq!(
    217                 metadata.data.file_url,
    218                 expected_str(vector, "file_url"),
    219                 "{}",
    220                 vector.id
    221             );
    222             assert_eq!(
    223                 metadata.data.file_type,
    224                 expected_str(vector, "file_type"),
    225                 "{}",
    226                 vector.id
    227             );
    228             assert_eq!(
    229                 metadata.data.encrypted_hash,
    230                 expected_str(vector, "encrypted_hash"),
    231                 "{}",
    232                 vector.id
    233             );
    234             assert_eq!(
    235                 metadata.data.size,
    236                 Some(expected_u64(vector, "size")),
    237                 "{}",
    238                 vector.id
    239             );
    240             assert_eq!(
    241                 metadata.data.dimensions,
    242                 Some(MessageFileDimensions {
    243                     w: expected_u64(vector, "width") as u32,
    244                     h: expected_u64(vector, "height") as u32,
    245                 }),
    246                 "{}",
    247                 vector.id
    248             );
    249         }
    250         other => panic!("{} expected a message-file rumor, got {other:?}", vector.id),
    251     }
    252 }
    253 
    254 async fn execute_invalid_recipient(vector: &Vector) {
    255     let sender = sender_keys();
    256     let recipient = input_str(vector, "recipient_public_key");
    257     let message = Message {
    258         recipients: vec![MessageRecipient {
    259             public_key: recipient.to_owned(),
    260             relay_url: None,
    261         }],
    262         content: input_str(vector, "content").to_owned(),
    263         reply_to: None,
    264         subject: None,
    265     };
    266     let error = wrap_message(&sender, &message, options(vector))
    267         .await
    268         .expect_err("invalid recipient vector must fail");
    269     assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    270     let rendered = format!("{error:?} {error}");
    271     assert!(!rendered.contains(recipient), "{}", vector.id);
    272     assert!(!rendered.contains(&message.content), "{}", vector.id);
    273 }
    274 
    275 fn options(vector: &Vector) -> WrapOptions {
    276     WrapOptions::default()
    277         .include_sender(false)
    278         .with_rumor_created_at(input_u64(vector, "created_at"))
    279 }
    280 
    281 fn sender_keys() -> Keys {
    282     let keys = Keys::new(
    283         SecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("approved sender fixture key"),
    284     );
    285     assert_eq!(keys.public_key().to_string(), FIXTURE_ALICE_PUBLIC_KEY_HEX);
    286     keys
    287 }
    288 
    289 fn receiver_keys() -> Keys {
    290     let keys = Keys::new(
    291         SecretKey::from_hex(FIXTURE_BOB_SECRET_KEY_HEX).expect("approved receiver fixture key"),
    292     );
    293     assert_eq!(keys.public_key().to_string(), FIXTURE_BOB_PUBLIC_KEY_HEX);
    294     keys
    295 }
    296 
    297 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
    298     vector
    299         .input
    300         .get(field)
    301         .and_then(Value::as_str)
    302         .unwrap_or_else(|| panic!("{} missing string input {field}", vector.id))
    303 }
    304 
    305 fn input_u64(vector: &Vector, field: &str) -> u64 {
    306     vector
    307         .input
    308         .get(field)
    309         .and_then(Value::as_u64)
    310         .unwrap_or_else(|| panic!("{} missing u64 input {field}", vector.id))
    311 }
    312 
    313 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
    314     vector
    315         .expected
    316         .get(field)
    317         .and_then(Value::as_str)
    318         .unwrap_or_else(|| panic!("{} missing string expectation {field}", vector.id))
    319 }
    320 
    321 fn expected_u64(vector: &Vector, field: &str) -> u64 {
    322     vector
    323         .expected
    324         .get(field)
    325         .and_then(Value::as_u64)
    326         .unwrap_or_else(|| panic!("{} missing u64 expectation {field}", vector.id))
    327 }