├── .gitignore ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── src ├── client │ ├── auth.rs │ ├── connection.rs │ └── mod.rs ├── error.rs ├── lib.rs ├── nip46 │ ├── mod.rs │ ├── params.rs │ ├── prebunk.rs │ ├── request.rs │ └── response.rs ├── types │ ├── client_message.rs │ ├── content.rs │ ├── delegation.rs │ ├── event.rs │ ├── event_kind.rs │ ├── event_reference.rs │ ├── file_metadata.rs │ ├── filter.rs │ ├── hll8.rs │ ├── id.rs │ ├── identity.rs │ ├── key_signer.rs │ ├── metadata.rs │ ├── mod.rs │ ├── naddr.rs │ ├── nevent.rs │ ├── nip05.rs │ ├── nostr_url.rs │ ├── pay_request_data.rs │ ├── private_key │ │ ├── content_encryption.rs │ │ ├── encrypted_private_key.rs │ │ └── mod.rs │ ├── profile.rs │ ├── public_key.rs │ ├── relay_information_document.rs │ ├── relay_list.rs │ ├── relay_message.rs │ ├── relay_usage.rs │ ├── satoshi.rs │ ├── signature.rs │ ├── signer.rs │ ├── simple_relay_list.rs │ ├── subscription_id.rs │ ├── tag.rs │ ├── tagval.rs │ ├── unixtime.rs │ └── url.rs └── versioned │ ├── event3.rs │ ├── filter1.rs │ ├── filter2.rs │ ├── metadata1.rs │ ├── metadata2.rs │ ├── mod.rs │ ├── nip05.rs │ ├── relay_information_document1.rs │ ├── relay_information_document2.rs │ ├── relay_list.rs │ ├── tag3.rs │ └── zap_data.rs └── versioning-plan.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-types" 3 | version = "0.8.0-unstable" 4 | edition = "2021" 5 | authors = ["Mike Dilger "] 6 | description = "Types for nostr protocol handling" 7 | repository = "https://github.com/mikedilger/nostr-types" 8 | documentation = "https://docs.rs/nostr-types" 9 | readme = "README.md" 10 | keywords = [ "nostr" ] 11 | license = "MIT" 12 | 13 | [features] 14 | default = [] 15 | nip46 = [ 16 | "client" 17 | ] 18 | client = [ 19 | "futures-util", 20 | "http", 21 | "reqwest", 22 | "textnonce", 23 | "tokio", 24 | "tokio-tungstenite", 25 | "tungstenite", 26 | # Note that you must also select one of the 3 TLS choices below. 27 | # Cargo makes it hard to specify that, but without any of them, 28 | # you won't get TLS support. 29 | ] 30 | 31 | # Use Native TLS code and native root certs 32 | native-tls = [ 33 | "reqwest/native-tls", 34 | "tungstenite/native-tls", 35 | "tokio-tungstenite/native-tls" 36 | ] 37 | 38 | # Use Rust TLS code with WebPKI compiled-in root certs 39 | rustls-tls = [ 40 | "reqwest/rustls-tls-webpki-roots", 41 | "tungstenite/rustls-tls-webpki-roots", 42 | "tokio-tungstenite/rustls-tls-webpki-roots" 43 | ] 44 | 45 | # Use Rust TLS code with native root certs 46 | rustls-tls-native = [ 47 | "reqwest/rustls-tls-native-roots", 48 | "tungstenite/rustls-tls-native-roots", 49 | "tokio-tungstenite/rustls-tls-native-roots" 50 | ] 51 | 52 | [dependencies] 53 | aes = "0.8" 54 | aho-corasick = "1.1" 55 | async-trait = "0.1" 56 | base64 = "0.22" 57 | bech32 = "0.11" 58 | cbc = { version = "0.1", features = [ "std" ] } 59 | chacha20poly1305 = "0.10" 60 | chacha20 = "0.9" 61 | core-net = "0.1" 62 | derive_more = "0.99" 63 | futures-util = { version = "0.3", optional = true, features = [ "sink" ] } 64 | hex = "0.4" 65 | hmac = "0.12" 66 | http = { version = "1.1", optional = true } 67 | lazy_static = "1.4" 68 | lightning-invoice = { git = "https://github.com/mikedilger/rust-lightning", rev = "7a62cb4106d449bc4d1724920b73918d501bb3a9" } 69 | linkify = "0.10" 70 | nip44 = { git = "https://github.com/mikedilger/nip44", rev="a55cd3850634d7e462c107a37a068f829670d6a2" } 71 | num_cpus = "1" 72 | pbkdf2 = { version = "0.12", default-features = false, features = [ "hmac", "sha2", "std" ] } 73 | rand_core = "0.6" 74 | rand = "0.8" 75 | regex = "1.10" 76 | reqwest = { version = "0.12", default-features=false, features = ["brotli", "deflate", "gzip", "json", "stream"], optional = true } 77 | scrypt = "0.11" 78 | secp256k1 = { version = "0.29", features = [ "hashes", "global-context", "rand-std", "serde" ] } 79 | serde = { version = "1.0", features = [ "derive", "rc" ] } 80 | serde_json = "1.0" 81 | sha2 = "0.10" 82 | speedy = { version = "0.8.7", optional = true } 83 | textnonce = { version = "1", optional = true } 84 | thiserror = "1.0" 85 | thread-priority = "1.0" 86 | tokio = { version = "1", features = ["full"], optional = true } 87 | tokio-tungstenite = { version = "0.26", default-features = false, features = [ "connect", "handshake" ], optional = true } 88 | tracing = "0.1" 89 | tungstenite = { version = "0.26", default-features = false, optional = true } 90 | unicode-normalization = "0.1" 91 | url = "2.5" 92 | zeroize = "1.7" 93 | 94 | [dev-dependencies] 95 | tokio = { version = "1", features = ["full"] } 96 | 97 | # Force scrypt to build with release-like speed even in dev mode 98 | [profile.dev.package.scrypt] 99 | opt-level = 3 100 | debug-assertions = false 101 | overflow-checks = false 102 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Michael Dilger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nostr-types 2 | 3 | [![Crates.io][crates-badge]][crates-url] 4 | [![MIT licensed][mit-badge]][mit-url] 5 | [![Stable Docs][doc-badge]][doc-url] 6 | [![Master Docs][doc2-badge]][doc2-url] 7 | 8 | [crates-badge]: https://img.shields.io/crates/v/nostr-types.svg 9 | [crates-url]: https://crates.io/crates/nostr-types 10 | [doc-badge]: https://img.shields.io/badge/docs-stable-green.svg 11 | [doc-url]: https://docs.rs/nostr-types 12 | [doc2-badge]: https://img.shields.io/badge/docs-master-yellow.svg 13 | [doc2-url]: https://mikedilger.com/docs/nostr-types/nostr_types/ 14 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 15 | [mit-url]: https://github.com/mikedilger/nostr-types/blob/master/LICENSE-MIT 16 | 17 | nostr-types is a crate defining types useful for the nostr protocol. 18 | 19 | We wrap all basic types. An `i64` may or may not be a `Unixtime`. A `&str` might 20 | be a hex encoded private key, or it might be somebody's name. By using types for 21 | everything, common mistakes can be avoided. 22 | 23 | We have extensive serde implementations for all types which are not simple to serialize 24 | such as Tag. 25 | 26 | Private keys remember if you've seen them or imported them and set themselves to `Weak` if 27 | you have. Generated private keys start out as `Medium`. We don't support `Strong` yet 28 | which will require a hardware token. (Note: there are ways to leak a private key without 29 | it knowing, so if it says `Medium` that is the maximum security, not a guaranteed level 30 | of security). Private keys can be imported and exported in a password-keyed encrypted form 31 | without weakening their security. 32 | 33 | ## License 34 | 35 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 36 | 37 | ### Contribution 38 | 39 | Unless you explicitly state otherwise, any contribution intentionally submitted 40 | for inclusion in the work by you, shall be licensed as above, without any additional 41 | terms or conditions. 42 | -------------------------------------------------------------------------------- /src/client/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::Id; 2 | 3 | /// The state of authentication to the relay 4 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 5 | pub enum AuthState { 6 | /// AUTH has not been requested by the relay 7 | #[default] 8 | NotYetRequested, 9 | 10 | /// AUTH has been requested 11 | Challenged(String), 12 | 13 | /// AUTH is in progress, we have sent the event 14 | InProgress(Id), 15 | 16 | /// AUTH succeeded 17 | Success, 18 | 19 | /// AUTH failed 20 | Failure(String), 21 | } 22 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Errors that can occur in the nostr-proto crate 4 | #[derive(Error, Debug)] 5 | pub enum Error { 6 | /// Assertion failed 7 | #[error("Assertion failed: {0}")] 8 | AssertionFailed(String), 9 | 10 | /// Bad NIP-46 Bunker URL 11 | #[error("Bad NIP-46 Bunker URL")] 12 | BadBunkerUrl, 13 | 14 | /// Bad Encrypted Message 15 | #[error("Bad Encrypted Message")] 16 | BadEncryptedMessage, 17 | 18 | /// Bad Encrypted Message due to bad Base64 19 | #[error("Bad Encrypted Message due to invalid base64")] 20 | BadEncryptedMessageBase64(base64::DecodeError), 21 | 22 | /// Base64 error 23 | #[error("Base64 Decoding Error: {0}")] 24 | Base64(#[from] base64::DecodeError), 25 | 26 | /// Bech32 decode error 27 | #[error("Bech32 Error: {0}")] 28 | Bech32Decode(#[from] bech32::DecodeError), 29 | 30 | /// Bech32 encode error 31 | #[error("Bech32 Error: {0}")] 32 | Bech32Encode(#[from] bech32::EncodeError), 33 | 34 | /// Bech32 HRP error 35 | #[error("Bech32 Error: {0}")] 36 | Bech32Hrp(#[from] bech32::primitives::hrp::Error), 37 | 38 | /// Crypto error 39 | #[error("Crypto Error: {0}")] 40 | Crypto(#[from] nip44::Error), 41 | 42 | /// Disconnected 43 | #[cfg(feature = "client")] 44 | #[error("Disconnected")] 45 | Disconnected, 46 | 47 | /// Encryption/Decryption Error 48 | #[error("Private Key Encryption/Decryption Error")] 49 | PrivateKeyEncryption, 50 | 51 | /// From utf8 Error 52 | #[error("From UTF-8 Error")] 53 | FromUtf8(#[from] std::string::FromUtf8Error), 54 | 55 | /// Bech32 error 56 | #[error("Wrong Bech32 Kind: Expected {0} found {0}")] 57 | WrongBech32(String, String), 58 | 59 | /// Key or Signature error 60 | #[error("Key or Signature Error: {0}")] 61 | KeyOrSignature(#[from] secp256k1::Error), 62 | 63 | /// Event is in the future 64 | #[error("Event is in the future")] 65 | EventInFuture, 66 | 67 | /// Formatting error 68 | #[error("Formatting Error: {0}")] 69 | Fmt(#[from] std::fmt::Error), 70 | 71 | /// A hash mismatch verification error 72 | #[error("Hash Mismatch")] 73 | HashMismatch, 74 | 75 | /// Hex string decoding error 76 | #[error("Hex Decode Error: {0}")] 77 | HexDecode(#[from] hex::FromHexError), 78 | 79 | /// HTTP error 80 | #[cfg(feature = "client")] 81 | #[error("HTTP: {0}")] 82 | Http(#[from] http::Error), 83 | 84 | /// Invalid encrypted private key 85 | #[error("Invalid Encrypted Private Key")] 86 | InvalidEncryptedPrivateKey, 87 | 88 | /// Invalid encrypted event 89 | #[error("Invalid Encrypted Event")] 90 | InvalidEncryptedEvent, 91 | 92 | /// Invalid HyperLogLog data 93 | #[error("Invalid HLL data")] 94 | InvalidHll, 95 | 96 | /// Invalid event Id 97 | #[error("Invalid event Id")] 98 | InvalidId, 99 | 100 | /// Invalid event Id Prefix 101 | #[error("Invalid event Id Prefix")] 102 | InvalidIdPrefix, 103 | 104 | /// Invalid digest length 105 | #[error("Invalid digest length")] 106 | InvalidLength(#[from] hmac::digest::InvalidLength), 107 | 108 | /// Invalid NAddr 109 | #[error("Invalid naddr")] 110 | InvalidNAddr, 111 | 112 | /// Invalid NEvent 113 | #[error("Invalid nevent")] 114 | InvalidNEvent, 115 | 116 | /// Invalid Operation 117 | #[error("Invalid Operation")] 118 | InvalidOperation, 119 | 120 | /// Invalid Private Key 121 | #[error("Invalid Private Key")] 122 | InvalidPrivateKey, 123 | 124 | /// Invalid Profile 125 | #[error("Invalid Profile")] 126 | InvalidProfile, 127 | 128 | /// Invalid public key 129 | #[error("Invalid Public Key")] 130 | InvalidPublicKey, 131 | 132 | /// Invalid public key prefix 133 | #[error("Invalid Public Key Prefix")] 134 | InvalidPublicKeyPrefix, 135 | 136 | /// Invalid recipient 137 | #[error("Invalid Recipient")] 138 | InvalidRecipient, 139 | 140 | /// Invalid state 141 | #[error("Invalid state: \"{0}\"")] 142 | InvalidState(String), 143 | 144 | /// Invalid URL 145 | #[error("Invalid URL: \"{0}\"")] 146 | InvalidUrl(#[from] url::ParseError), 147 | 148 | /// Invalid URI 149 | #[cfg(feature = "client")] 150 | #[error("Invalid URI: {0}")] 151 | InvalidUri(#[from] http::uri::InvalidUri), 152 | 153 | /// Invalid URL TLV encoding 154 | #[error("Invalid URL TLV encoding")] 155 | InvalidUrlTlv, 156 | 157 | /// Invalid URL Host 158 | #[error("Invalid URL Host: \"{0}\"")] 159 | InvalidUrlHost(String), 160 | 161 | /// Invalid URL Scheme 162 | #[error("Invalid URL Scheme: \"{0}\"")] 163 | InvalidUrlScheme(String), 164 | 165 | /// Missing URL Authority 166 | #[error("Missing URL Authority")] 167 | InvalidUrlMissingAuthority, 168 | 169 | /// NIP-46 error 170 | #[cfg(feature = "nip46")] 171 | #[error("NIP-46 error: {0}")] 172 | Nip46Error(String), 173 | 174 | /// NIP-46 failed to post 175 | #[cfg(feature = "nip46")] 176 | #[error("NIP-46 failed to post: {0}")] 177 | Nip46FailedToPost(String), 178 | 179 | /// NIP-46 failed to post 180 | #[cfg(feature = "nip46")] 181 | #[error("NIP-46 no response")] 182 | Nip46NoResponse, 183 | 184 | /// Addr to a non-replaceable event kind 185 | #[error("Event kind is not replaceable")] 186 | NonReplaceableAddr, 187 | 188 | /// No Private Key 189 | #[error("No private key")] 190 | NoPrivateKey, 191 | 192 | /// No Public Key 193 | #[error("No public key")] 194 | NoPublicKey, 195 | 196 | /// Out of Range 197 | #[error("Out of Range")] 198 | OutOfRange(usize), 199 | 200 | /// Parse integer error 201 | #[error("Parse integer error")] 202 | ParseInt(#[from] std::num::ParseIntError), 203 | 204 | /// Relay did not AUTH 205 | #[error("Relay (broken) says auth-required before challenging with AUTH")] 206 | RelayDidNotAuth, 207 | 208 | /// Relay forgot that we successfully AUTHed 209 | #[error("Relay (broken) forgot that we already successfully AUTHed")] 210 | RelayForgotAuth, 211 | 212 | /// Relay rejected our post 213 | #[error("Relay rejected our post")] 214 | RelayRejectedPost, 215 | 216 | /// Relay rejected our AUTH 217 | #[error("Relay rejected our AUTH")] 218 | RelayRejectedAuth, 219 | 220 | /// Relay requires AUTH but we aren't supplying it 221 | #[error("Relay requires AUTH")] 222 | RelayRequiresAuth, 223 | 224 | /// HTTP request eror 225 | #[cfg(feature = "client")] 226 | #[error("HTTP error: {0}")] 227 | Reqwest(#[from] reqwest::Error), 228 | 229 | /// Scrypt error 230 | #[error("Scrypt invalid output length")] 231 | Scrypt, 232 | 233 | /// Serialization error 234 | #[error("JSON (de)serialization error: {0}")] 235 | SerdeJson(#[from] serde_json::Error), 236 | 237 | /// Signer is locked 238 | #[error("Signer is locked")] 239 | SignerIsLocked, 240 | 241 | /// Try from slice error 242 | #[error("Try From Slice error: {0}")] 243 | Slice(#[from] std::array::TryFromSliceError), 244 | 245 | /// Speedy error 246 | #[cfg(feature = "speedy")] 247 | #[error("Speedy (de)serialization error: {0}")] 248 | Speedy(#[from] speedy::Error), 249 | 250 | /// Tag mismatch 251 | #[error("Tag mismatch")] 252 | TagMismatch, 253 | 254 | /// Timeout 255 | #[cfg(feature = "client")] 256 | #[error("Timeout")] 257 | TimedOut, 258 | 259 | /// Timeout 260 | #[cfg(feature = "client")] 261 | #[error("Timeout: {0}")] 262 | Timeout(#[from] tokio::time::error::Elapsed), 263 | 264 | /// Unknown event kind 265 | #[error("Unknown event kind = {0}")] 266 | UnknownEventKind(u32), 267 | 268 | /// Unknown Key Security 269 | #[error("Unknown key security = {0}")] 270 | UnknownKeySecurity(u8), 271 | 272 | /// Unknown Cipher Version 273 | #[error("Unknown cipher version = {0}")] 274 | UnknownCipherVersion(u8), 275 | 276 | /// Unpad error 277 | #[error("Decryption error: {0}")] 278 | Unpad(#[from] aes::cipher::block_padding::UnpadError), 279 | 280 | /// Unsupported Algorithm 281 | #[error("Unsupported algorithm")] 282 | UnsupportedAlgorithm, 283 | 284 | /// Url Error 285 | #[error("Not a valid nostr relay url: {0}")] 286 | Url(String), 287 | 288 | /// UTF-8 error 289 | #[error("UTF-8 Error: {0}")] 290 | Utf8Error(#[from] std::str::Utf8Error), 291 | 292 | /// Websocket error 293 | #[cfg(feature = "client")] 294 | #[error("Websocket error: {0}")] 295 | Websocket(#[from] tungstenite::Error), 296 | 297 | /// Websocket Connection Failed 298 | #[cfg(feature = "client")] 299 | #[error("Websocket connection failed: {0}")] 300 | WebsocketConnectionFailed(http::StatusCode), 301 | 302 | /// Wrong event kind 303 | #[error("Wrong event kind")] 304 | WrongEventKind, 305 | 306 | /// Wrong length hex string 307 | #[error("Wrong length hex string")] 308 | WrongLengthHexString, 309 | 310 | /// Wrong length bytes for event kind 311 | #[error("Wrong length bytes for event kind")] 312 | WrongLengthKindBytes, 313 | 314 | /// Wrong Decryption Password 315 | #[error("Wrong decryption password")] 316 | WrongDecryptionPassword, 317 | 318 | /// Zap Receipt issue 319 | #[error("Invalid Zap Receipt: {0}")] 320 | ZapReceipt(String), 321 | } 322 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2020 nostr-proto Developers 2 | // Licensed under the MIT license 3 | // This file may not be copied, modified, or distributed except according to those terms. 4 | 5 | //! This crate provides types for nostr protocol handling. 6 | 7 | #![deny( 8 | missing_debug_implementations, 9 | trivial_casts, 10 | trivial_numeric_casts, 11 | unused_import_braces, 12 | //unused_qualifications, 13 | unused_results, 14 | unused_lifetimes, 15 | unused_labels, 16 | unused_extern_crates, 17 | non_ascii_idents, 18 | keyword_idents, 19 | deprecated_in_future, 20 | unstable_features, 21 | single_use_lifetimes, 22 | //unsafe_code, 23 | unreachable_pub, 24 | missing_docs, 25 | missing_copy_implementations 26 | )] 27 | #![deny(clippy::string_slice)] 28 | 29 | mod error; 30 | pub use error::Error; 31 | 32 | #[cfg(test)] 33 | macro_rules! test_serde { 34 | ($t:ty, $fnname:ident) => { 35 | #[test] 36 | fn $fnname() { 37 | let a = <$t>::mock(); 38 | let x = serde_json::to_string(&a).unwrap(); 39 | println!("{}", x); 40 | let b = serde_json::from_str(&x).unwrap(); 41 | assert_eq!(a, b); 42 | } 43 | }; 44 | } 45 | 46 | #[cfg(test)] 47 | macro_rules! test_serde_async { 48 | ($t:ty, $fnname:ident) => { 49 | #[tokio::test] 50 | async fn $fnname() { 51 | let a = <$t>::mock().await; 52 | let x = serde_json::to_string(&a).unwrap(); 53 | println!("{}", x); 54 | let b = serde_json::from_str(&x).unwrap(); 55 | assert_eq!(a, b); 56 | } 57 | }; 58 | } 59 | 60 | #[cfg(test)] 61 | macro_rules! test_serde_val { 62 | ($fnname:ident, $val:expr) => { 63 | #[test] 64 | fn $fnname() { 65 | let a = $val; 66 | let x = serde_json::to_string(&a).unwrap(); 67 | println!("{}", x); 68 | let b = serde_json::from_str(&x).unwrap(); 69 | assert_eq!(a, b); 70 | } 71 | }; 72 | } 73 | 74 | #[cfg(test)] 75 | macro_rules! test_serde_val_async { 76 | ($fnname:ident, $val:expr) => { 77 | #[tokio::test] 78 | async fn $fnname() { 79 | let a = $val; 80 | let x = serde_json::to_string(&a).unwrap(); 81 | println!("{}", x); 82 | let b = serde_json::from_str(&x).unwrap(); 83 | assert_eq!(a, b); 84 | } 85 | }; 86 | } 87 | 88 | /// A basic nostr client 89 | #[cfg(feature = "client")] 90 | pub mod client; 91 | 92 | /// NIP-46 nsec bunker related types and functions 93 | #[cfg(feature = "nip46")] 94 | pub mod nip46; 95 | 96 | mod types; 97 | pub use types::{ 98 | find_nostr_bech32_pos, find_nostr_url_pos, ClientMessage, ContentEncryptionAlgorithm, 99 | ContentSegment, CountResult, DelegationConditions, EncryptedPrivateKey, Event, EventDelegation, 100 | EventKind, EventKindIterator, EventKindOrRange, EventReference, ExportableSigner, Fee, 101 | FileMetadata, Filter, Hll8, Id, IdHex, Identity, KeySecurity, KeySigner, LockableSigner, 102 | Metadata, MilliSatoshi, MutExportableSigner, NAddr, NEvent, Nip05, NostrBech32, NostrUrl, 103 | ParsedTag, PayRequestData, PreEvent, PrivateKey, Profile, PublicKey, PublicKeyHex, RelayFees, 104 | RelayInformationDocument, RelayLimitation, RelayList, RelayListUsage, RelayMessage, 105 | RelayOrigin, RelayRetention, RelayUrl, RelayUsage, RelayUsageSet, Rumor, ShatteredContent, 106 | Signature, SignatureHex, Signer, SignerExt, SimpleRelayList, SimpleRelayUsage, Span, 107 | SubscriptionId, Tag, UncheckedUrl, Unixtime, Url, Why, XOnlyPublicKey, ZapData, 108 | }; 109 | 110 | mod versioned; 111 | pub use versioned::{ 112 | EventV3, FeeV1, FilterV1, FilterV2, MetadataV1, MetadataV2, Nip05V1, PreEventV3, RelayFeesV1, 113 | RelayInformationDocumentV1, RelayInformationDocumentV2, RelayLimitationV1, RelayLimitationV2, 114 | RelayRetentionV1, RumorV3, TagV3, ZapDataV1, ZapDataV2, 115 | }; 116 | 117 | #[inline] 118 | pub(crate) fn get_leading_zero_bits(bytes: &[u8]) -> u8 { 119 | let mut res = 0_u8; 120 | for b in bytes { 121 | if *b == 0 { 122 | res += 8; 123 | } else { 124 | res += b.leading_zeros() as u8; 125 | return res; 126 | } 127 | } 128 | res 129 | } 130 | 131 | trait IntoVec { 132 | fn into_vec(self) -> Vec; 133 | } 134 | 135 | impl IntoVec for Option { 136 | fn into_vec(self) -> Vec { 137 | match self { 138 | None => vec![], 139 | Some(t) => vec![t], 140 | } 141 | } 142 | } 143 | 144 | use bech32::Hrp; 145 | lazy_static::lazy_static! { 146 | static ref HRP_LNURL: Hrp = Hrp::parse("lnurl").expect("HRP error on lnurl"); 147 | static ref HRP_NADDR: Hrp = Hrp::parse("naddr").expect("HRP error on naddr"); 148 | static ref HRP_NCRYPTSEC: Hrp = Hrp::parse("ncryptsec").expect("HRP error on ncryptsec"); 149 | static ref HRP_NEVENT: Hrp = Hrp::parse("nevent").expect("HRP error on nevent"); 150 | static ref HRP_NOTE: Hrp = Hrp::parse("note").expect("HRP error on note"); 151 | static ref HRP_NPROFILE: Hrp = Hrp::parse("nprofile").expect("HRP error on nprofile"); 152 | static ref HRP_NPUB: Hrp = Hrp::parse("npub").expect("HRP error on npub"); 153 | static ref HRP_NRELAY: Hrp = Hrp::parse("nrelay").expect("HRP error on nrelay"); 154 | static ref HRP_NSEC: Hrp = Hrp::parse("nsec").expect("HRP error on nsec"); 155 | } 156 | 157 | /// Add a 'p' pubkey tag to a set of tags if it doesn't already exist 158 | pub fn add_pubkey_to_tags( 159 | existing_tags: &mut Vec, 160 | new_pubkey: PublicKey, 161 | new_hint: Option, 162 | ) -> usize { 163 | let index = existing_tags.iter().position(|existing_tag| { 164 | if let Ok(ParsedTag::Pubkey { pubkey, .. }) = existing_tag.parse() { 165 | pubkey == new_pubkey 166 | } else { 167 | false 168 | } 169 | }); 170 | 171 | if let Some(idx) = index { 172 | // force additional data to match 173 | existing_tags[idx].set_index( 174 | 2, 175 | match new_hint { 176 | Some(u) => u.as_str().to_owned(), 177 | None => "".to_owned(), 178 | }, 179 | ); 180 | existing_tags[idx].trim(); 181 | idx 182 | } else { 183 | existing_tags.push( 184 | ParsedTag::Pubkey { 185 | pubkey: new_pubkey, 186 | recommended_relay_url: new_hint, 187 | petname: None, 188 | } 189 | .into_tag(), 190 | ); 191 | existing_tags.len() - 1 192 | } 193 | } 194 | 195 | /// Add an 'e' id tag to a set of tags if it doesn't already exist 196 | pub fn add_event_to_tags( 197 | existing_tags: &mut Vec, 198 | new_id: Id, 199 | new_hint: Option, 200 | new_marker: &str, 201 | new_pubkey: Option, 202 | use_quote: bool, 203 | ) -> usize { 204 | // NIP-18: "Quote reposts are kind 1 events with an embedded q tag..." 205 | if new_marker == "mention" && use_quote { 206 | let index = existing_tags.iter().position(|existing_tag| { 207 | if let Ok(ParsedTag::Quote { id, .. }) = existing_tag.parse() { 208 | id == new_id 209 | } else { 210 | false 211 | } 212 | }); 213 | 214 | if let Some(idx) = index { 215 | // force additional data to match 216 | existing_tags[idx].set_index( 217 | 2, 218 | match new_hint { 219 | Some(u) => u.as_str().to_owned(), 220 | None => "".to_owned(), 221 | }, 222 | ); 223 | existing_tags[idx].set_index( 224 | 3, 225 | match new_pubkey { 226 | Some(pk) => pk.as_hex_string(), 227 | None => "".to_owned(), 228 | }, 229 | ); 230 | existing_tags[idx].trim(); 231 | idx 232 | } else { 233 | let newtag = ParsedTag::Quote { 234 | id: new_id, 235 | recommended_relay_url: new_hint, 236 | author_pubkey: new_pubkey, 237 | } 238 | .into_tag(); 239 | existing_tags.push(newtag); 240 | existing_tags.len() - 1 241 | } 242 | } else { 243 | let index = existing_tags.iter().position(|existing_tag| { 244 | if let Ok(ParsedTag::Event { id, .. }) = existing_tag.parse() { 245 | id == new_id 246 | } else { 247 | false 248 | } 249 | }); 250 | 251 | if let Some(idx) = index { 252 | // force additional data to match 253 | existing_tags[idx].set_index( 254 | 2, 255 | match new_hint { 256 | Some(u) => u.as_str().to_owned(), 257 | None => "".to_owned(), 258 | }, 259 | ); 260 | existing_tags[idx].set_index(3, new_marker.to_owned()); 261 | existing_tags[idx].set_index( 262 | 4, 263 | match new_pubkey { 264 | Some(pk) => pk.as_hex_string(), 265 | None => "".to_owned(), 266 | }, 267 | ); 268 | existing_tags[idx].trim(); 269 | idx 270 | } else { 271 | let newtag = ParsedTag::Event { 272 | id: new_id, 273 | recommended_relay_url: new_hint, 274 | marker: Some(new_marker.to_string()), 275 | author_pubkey: new_pubkey, 276 | } 277 | .into_tag(); 278 | existing_tags.push(newtag); 279 | existing_tags.len() - 1 280 | } 281 | } 282 | } 283 | 284 | /// Add an 'a' addr tag to a set of tags if it doesn't already exist 285 | pub fn add_addr_to_tags( 286 | existing_tags: &mut Vec, 287 | new_addr: &NAddr, 288 | new_marker: Option, 289 | ) -> usize { 290 | let index = existing_tags.iter().position(|existing_tag| { 291 | if let Ok(ParsedTag::Address { address, .. }) = existing_tag.parse() { 292 | address.kind == new_addr.kind 293 | && address.author == new_addr.author 294 | && address.d == new_addr.d 295 | } else { 296 | false 297 | } 298 | }); 299 | 300 | if let Some(idx) = index { 301 | // force additional data to match 302 | existing_tags[idx].set_index( 303 | 2, 304 | match new_marker { 305 | Some(s) => s, 306 | None => "".to_owned(), 307 | }, 308 | ); 309 | existing_tags[idx].trim(); 310 | idx 311 | } else { 312 | existing_tags.push( 313 | ParsedTag::Address { 314 | address: new_addr.clone(), 315 | marker: new_marker, 316 | } 317 | .into_tag(), 318 | ); 319 | existing_tags.len() - 1 320 | } 321 | } 322 | 323 | /// Add an 'subject' tag to a set of tags if it doesn't already exist 324 | pub fn add_subject_to_tags_if_missing(existing_tags: &mut Vec, subject: String) { 325 | if !existing_tags.iter().any(|t| t.tagname() == "subject") { 326 | existing_tags.push(ParsedTag::Subject(subject).into_tag()); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/nip46/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client, ContentEncryptionAlgorithm, EncryptedPrivateKey, Error, Event, EventKind, Filter, 3 | KeySecurity, KeySigner, LockableSigner, PreEvent, PublicKey, RelayUrl, Signer, 4 | }; 5 | use async_trait::async_trait; 6 | use std::sync::Arc; 7 | use std::time::Duration; 8 | 9 | mod request; 10 | pub use request::Nip46Request; 11 | 12 | mod response; 13 | pub use response::Nip46Response; 14 | 15 | mod params; 16 | pub use params::Nip46ConnectionParameters; 17 | 18 | mod prebunk; 19 | pub use prebunk::PreBunkerClient; 20 | 21 | /// This is a NIP-46 Bunker client 22 | #[derive(Debug)] 23 | pub struct BunkerClient { 24 | /// The pubkey of the bunker 25 | pub remote_signer_pubkey: PublicKey, 26 | 27 | /// The relay the bunker is listening at 28 | pub relay_url: RelayUrl, 29 | 30 | /// Our local identity 31 | pub local_signer: Arc, 32 | 33 | /// User Public Key 34 | pub public_key: PublicKey, 35 | 36 | /// Timeout 37 | pub timeout: Duration, 38 | 39 | /// Client 40 | pub client: client::Client, 41 | } 42 | 43 | impl BunkerClient { 44 | /// Create a new BunkerClient from stored data. This will be in the locked state. 45 | pub async fn from_stored_data( 46 | remote_signer_pubkey: PublicKey, 47 | relay_url: RelayUrl, 48 | keysigner: KeySigner, 49 | public_key: PublicKey, 50 | timeout: Duration, 51 | ) -> BunkerClient { 52 | let client = client::Client::new(relay_url.as_str()); 53 | BunkerClient { 54 | remote_signer_pubkey, 55 | relay_url, 56 | local_signer: Arc::new(keysigner), 57 | public_key, 58 | timeout, 59 | client, 60 | } 61 | } 62 | 63 | /// Is the signer locked? 64 | pub fn is_locked(&self) -> bool { 65 | self.local_signer.is_locked() 66 | } 67 | 68 | /// Unlock (if locked) 69 | pub fn unlock(&self, password: &str) -> Result<(), Error> { 70 | self.local_signer.unlock(password) 71 | } 72 | 73 | /// Lock 74 | pub fn lock(&self) { 75 | self.local_signer.lock() 76 | } 77 | 78 | /// Change passphrase 79 | pub fn change_passphrase(&self, old: &str, new: &str, log_n: u8) -> Result<(), Error> { 80 | self.local_signer.change_passphrase(old, new, log_n) 81 | } 82 | 83 | /// Send a `Nip46Request` and wait for a `Nip46Response` (up to our timeout) 84 | pub async fn call(&self, request: Nip46Request) -> Result { 85 | // Subscribe first 86 | let mut filter = Filter::new(); 87 | filter.add_author(self.remote_signer_pubkey); 88 | filter.add_event_kind(EventKind::NostrConnect); 89 | filter.add_tag_value('p', self.local_signer.public_key().as_hex_string()); 90 | filter.limit = Some(1); 91 | let sub_id = self.client.subscribe(filter.clone(), self.timeout).await?; 92 | let event = request 93 | .to_event(self.remote_signer_pubkey, self.local_signer.clone()) 94 | .await?; 95 | 96 | // Post event to server and wait for OK 97 | let event_id = event.id; 98 | self.client.post_event(event, self.timeout).await?; 99 | let (ok, msg) = self.client.wait_for_ok(event_id, self.timeout).await?; 100 | if !ok { 101 | return Err(Error::Nip46FailedToPost(msg)); 102 | } 103 | 104 | // Wait for a response 105 | let event = self 106 | .client 107 | .wait_for_subscribed_event(sub_id.clone(), self.timeout) 108 | .await?; 109 | 110 | let contents = self.local_signer.decrypt_event_contents(&event).await?; 111 | 112 | // Convert into a response 113 | let response: Nip46Response = serde_json::from_str(&contents)?; 114 | 115 | // Close the subscription 116 | self.client.close_subscription(sub_id).await?; 117 | 118 | Ok(response) 119 | } 120 | 121 | /// Disconnect from the relay 122 | pub async fn disconnect(&self) -> Result<(), Error> { 123 | self.client.disconnect().await 124 | } 125 | 126 | /// Disconnect from the relay and lock 127 | pub async fn disconnect_and_lock(&self) -> Result<(), Error> { 128 | self.client.disconnect().await?; 129 | self.local_signer.lock(); 130 | Ok(()) 131 | } 132 | } 133 | 134 | #[async_trait] 135 | impl Signer for BunkerClient { 136 | fn public_key(&self) -> PublicKey { 137 | self.public_key 138 | } 139 | 140 | fn encrypted_private_key(&self) -> Option { 141 | // NIP-46 does not offer an export function (yet) 142 | None 143 | } 144 | 145 | async fn sign_event(&self, pre_event: PreEvent) -> Result { 146 | if self.is_locked() { 147 | return Err(Error::SignerIsLocked); 148 | } 149 | 150 | let pre_event_string = serde_json::to_string(&pre_event)?; 151 | let request = Nip46Request::new("sign_event".to_owned(), vec![pre_event_string]); 152 | let response = self.call(request).await?; 153 | if let Some(error) = response.error { 154 | if !error.is_empty() { 155 | return Err(Error::Nip46Error(error)); 156 | } 157 | } 158 | let event: Event = serde_json::from_str(&response.result)?; 159 | Ok(event) 160 | } 161 | 162 | async fn encrypt( 163 | &self, 164 | other: &PublicKey, 165 | plaintext: &str, 166 | algo: ContentEncryptionAlgorithm, 167 | ) -> Result { 168 | if self.is_locked() { 169 | return Err(Error::SignerIsLocked); 170 | } 171 | 172 | let cmd = match algo { 173 | ContentEncryptionAlgorithm::Nip04 => "nip04_encrypt", 174 | ContentEncryptionAlgorithm::Nip44v1Unpadded => return Err(Error::UnsupportedAlgorithm), 175 | ContentEncryptionAlgorithm::Nip44v1Padded => return Err(Error::UnsupportedAlgorithm), 176 | ContentEncryptionAlgorithm::Nip44v2 => "nip44_encrypt", 177 | }; 178 | 179 | let request = Nip46Request::new( 180 | cmd.to_owned(), 181 | vec![other.as_hex_string(), plaintext.to_owned()], 182 | ); 183 | 184 | let response = self.call(request).await?; 185 | if let Some(error) = response.error { 186 | if !error.is_empty() { 187 | return Err(Error::Nip46Error(error)); 188 | } 189 | } 190 | 191 | Ok(response.result) 192 | } 193 | 194 | async fn decrypt(&self, other: &PublicKey, ciphertext: &str) -> Result { 195 | if self.is_locked() { 196 | return Err(Error::SignerIsLocked); 197 | } 198 | 199 | let cmd = if ciphertext.contains("?iv=") { 200 | "nip04_decrypt" 201 | } else { 202 | "nip44_decrypt" 203 | }; 204 | 205 | let request = Nip46Request::new( 206 | cmd.to_owned(), 207 | vec![other.as_hex_string(), ciphertext.to_owned()], 208 | ); 209 | 210 | let response = self.call(request).await?; 211 | if let Some(error) = response.error { 212 | if !error.is_empty() { 213 | return Err(Error::Nip46Error(error)); 214 | } 215 | } 216 | 217 | Ok(response.result) 218 | } 219 | 220 | fn key_security(&self) -> Result { 221 | Ok(KeySecurity::NotTracked) 222 | } 223 | } 224 | 225 | use serde::de::Error as DeError; 226 | use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor}; 227 | use serde::ser::{Serialize, SerializeSeq, Serializer}; 228 | use std::fmt; 229 | 230 | impl Serialize for BunkerClient { 231 | fn serialize(&self, serializer: S) -> Result 232 | where 233 | S: Serializer, 234 | { 235 | let mut seq = serializer.serialize_seq(Some(5))?; 236 | seq.serialize_element(&self.remote_signer_pubkey)?; 237 | seq.serialize_element(&self.relay_url)?; 238 | seq.serialize_element(&self.local_signer)?; 239 | seq.serialize_element(&self.public_key)?; 240 | seq.serialize_element(&self.timeout)?; 241 | seq.end() 242 | } 243 | } 244 | 245 | impl<'de> Deserialize<'de> for BunkerClient { 246 | fn deserialize(deserializer: D) -> Result 247 | where 248 | D: Deserializer<'de>, 249 | { 250 | deserializer.deserialize_seq(BunkerClientVisitor) 251 | } 252 | } 253 | 254 | struct BunkerClientVisitor; 255 | 256 | impl<'de> Visitor<'de> for BunkerClientVisitor { 257 | type Value = BunkerClient; 258 | 259 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 260 | write!(f, "a serialized BunkerClient as a sequence") 261 | } 262 | 263 | fn visit_seq(self, mut access: A) -> Result 264 | where 265 | A: SeqAccess<'de>, 266 | { 267 | let remote_signer_pubkey = access 268 | .next_element::()? 269 | .ok_or_else(|| DeError::custom("Missing remote_signer_pubkey"))?; 270 | let relay_url = access 271 | .next_element::()? 272 | .ok_or_else(|| DeError::custom("Missing relay_url"))?; 273 | let local_signer = access 274 | .next_element::>()? 275 | .ok_or_else(|| DeError::custom("Missing local_signer"))?; 276 | let public_key = access 277 | .next_element::()? 278 | .ok_or_else(|| DeError::custom("Missing public_key"))?; 279 | let timeout = access 280 | .next_element::()? 281 | .ok_or_else(|| DeError::custom("Missing timeout"))?; 282 | let client = client::Client::new(relay_url.as_str()); 283 | Ok(BunkerClient { 284 | remote_signer_pubkey, 285 | relay_url, 286 | local_signer, 287 | public_key, 288 | timeout, 289 | client, 290 | }) 291 | } 292 | } 293 | 294 | #[cfg(test)] 295 | mod test { 296 | use super::*; 297 | use crate::PrivateKey; 298 | 299 | #[test] 300 | fn test_bunker_client_serde() { 301 | let prebunk = PreBunkerClient::new( 302 | PrivateKey::generate().public_key(), 303 | RelayUrl::try_from_str("wss://relay.example/").unwrap(), 304 | None, 305 | "password", 306 | ) 307 | .unwrap(); 308 | 309 | let s = serde_json::to_string(&prebunk).unwrap(); 310 | println!("{s}"); 311 | let prebunk2: PreBunkerClient = serde_json::from_str(&*s).unwrap(); 312 | assert_eq!(prebunk.remote_signer_pubkey, prebunk2.remote_signer_pubkey); 313 | assert_eq!(prebunk.relay_url, prebunk2.relay_url); 314 | assert_eq!(prebunk.connect_secret, prebunk2.connect_secret); 315 | assert_eq!( 316 | prebunk.local_signer.encrypted_private_key(), 317 | prebunk2.local_signer.encrypted_private_key() 318 | ); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/nip46/params.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, PublicKey, RelayUrl}; 2 | use lazy_static::lazy_static; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// The connection parameters provided by an nsec bunker for a client connecting to it 6 | /// usually as a `bunker://` url 7 | #[derive(Clone, Debug, Serialize, Deserialize)] 8 | pub struct Nip46ConnectionParameters { 9 | /// The public key of the remote signer 10 | pub remote_signer_pubkey: PublicKey, 11 | 12 | /// The relays to contact the remote signer on 13 | pub relays: Vec, 14 | 15 | /// A secret to provide in the connect request to prove this client is authorized 16 | pub secret: Option, 17 | } 18 | 19 | impl Nip46ConnectionParameters { 20 | /// Parse a `bunker://` url into `Nip46ConnectionParameters` 21 | #[allow(clippy::should_implement_trait)] 22 | pub fn from_str(s: &str) -> Result { 23 | // "bunker://{pk}?{relay_part}&secret={secret}" 24 | use regex::Regex; 25 | lazy_static! { 26 | static ref BUNKER_RE: Regex = 27 | Regex::new(r#"^bunker://(.+)\?(.+)$"#).expect("Could not compile bunker regex"); 28 | } 29 | 30 | let mut relays: Vec = Vec::new(); 31 | let mut secret: Option = None; 32 | 33 | let captures = match BUNKER_RE.captures(s) { 34 | Some(c) => c, 35 | None => return Err(Error::BadBunkerUrl), 36 | }; 37 | 38 | let public_key = if let Some(pk_part) = captures.get(1) { 39 | PublicKey::try_from_hex_string(pk_part.as_str(), true)? 40 | } else { 41 | return Err(Error::BadBunkerUrl); 42 | }; 43 | 44 | if let Some(param_part) = captures.get(2) { 45 | let assignments = param_part.as_str().split('&'); 46 | for assignment in assignments { 47 | let halfs: Vec<&str> = assignment.split('=').collect(); 48 | if halfs.len() != 2 { 49 | return Err(Error::BadBunkerUrl); 50 | } 51 | let var = halfs[0]; 52 | let val = halfs[1]; 53 | match var { 54 | "relay" => relays.push(RelayUrl::try_from_str(val)?), 55 | "secret" => secret = Some(val.to_owned()), 56 | _ => continue, // ignore other terms 57 | } 58 | } 59 | } else { 60 | return Err(Error::BadBunkerUrl); 61 | } 62 | 63 | if relays.is_empty() { 64 | return Err(Error::BadBunkerUrl); 65 | } 66 | 67 | Ok(Nip46ConnectionParameters { 68 | remote_signer_pubkey: public_key, 69 | relays, 70 | secret, 71 | }) 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod test { 77 | use super::*; 78 | 79 | #[test] 80 | fn test_nip46_connection_params() { 81 | let params = Nip46ConnectionParameters::from_str( 82 | "bunker://ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49?relay=wss://chorus.mikedilger.com:444/&secret=5ijGGB0AGmgAAAAAgbaYUxymvgMQnrQh" 83 | ).unwrap(); 84 | 85 | assert_eq!(params.relays.len(), 1); 86 | assert_eq!( 87 | params.relays[0].as_str(), 88 | "wss://chorus.mikedilger.com:444/" 89 | ); 90 | assert_eq!( 91 | params.remote_signer_pubkey, 92 | PublicKey::try_from_hex_string( 93 | "ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49", 94 | true 95 | ) 96 | .unwrap() 97 | ); 98 | assert_eq!( 99 | params.secret, 100 | Some("5ijGGB0AGmgAAAAAgbaYUxymvgMQnrQh".to_string()) 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/nip46/prebunk.rs: -------------------------------------------------------------------------------- 1 | use crate::client::Client; 2 | use crate::nip46::{BunkerClient, Nip46ConnectionParameters, Nip46Request, Nip46Response}; 3 | use crate::{Error, EventKind, Filter, KeySigner, LockableSigner, PublicKey, RelayUrl, Signer}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::sync::Arc; 6 | use std::time::Duration; 7 | use tracing::{event, span, Level}; 8 | 9 | /// This is a Remote Signer setup that has not yet discovered the user's PublicKey 10 | /// As a result, it cannot implement Signer yet. 11 | #[derive(Debug, Serialize, Deserialize)] 12 | pub struct PreBunkerClient { 13 | /// The pubkey of the bunker 14 | pub remote_signer_pubkey: PublicKey, 15 | 16 | /// The relay the bunker is listening at 17 | pub relay_url: RelayUrl, 18 | 19 | /// The connect secret 20 | pub connect_secret: Option, 21 | 22 | /// Our local identity 23 | pub local_signer: Arc, 24 | 25 | /// Timeout 26 | pub timeout: Duration, 27 | } 28 | 29 | impl PreBunkerClient { 30 | /// Create a new BunkerClient, generating a fresh local identity 31 | pub fn new( 32 | remote_signer_pubkey: PublicKey, 33 | relay_url: RelayUrl, 34 | connect_secret: Option, 35 | new_password: &str, 36 | timeout: Duration, 37 | ) -> Result { 38 | let local_signer = Arc::new(KeySigner::generate(new_password, 18)?); 39 | 40 | Ok(PreBunkerClient { 41 | remote_signer_pubkey, 42 | relay_url, 43 | connect_secret, 44 | local_signer, 45 | timeout, 46 | }) 47 | } 48 | 49 | /// Create a new nip46 client from a URL. 50 | /// 51 | /// This connects to the relay, but does not contact the bunker yet. Use `connect()` to 52 | /// initiate contact with the bunker. 53 | pub fn new_from_url( 54 | url: &str, 55 | new_password: &str, 56 | timeout: Duration, 57 | ) -> Result { 58 | let Nip46ConnectionParameters { 59 | remote_signer_pubkey, 60 | relays, 61 | secret, 62 | } = Nip46ConnectionParameters::from_str(url)?; 63 | 64 | let local_signer = Arc::new(KeySigner::generate(new_password, 18)?); 65 | 66 | Ok(PreBunkerClient { 67 | remote_signer_pubkey, 68 | relay_url: relays[0].clone(), 69 | connect_secret: secret, 70 | local_signer, 71 | timeout, 72 | }) 73 | } 74 | 75 | /// Is the signer locked? 76 | pub fn is_locked(&self) -> bool { 77 | self.local_signer.is_locked() 78 | } 79 | 80 | /// Unlock (if locked) 81 | pub fn unlock(&mut self, password: &str) -> Result<(), Error> { 82 | self.local_signer.unlock(password) 83 | } 84 | 85 | /// Connect to the relay and bunker, learn our user's PublicKey, 86 | /// and return a full BunkerClient which impl Signer 87 | pub async fn initialize(&mut self) -> Result { 88 | let span = span!(Level::DEBUG, "nip46 Prebunk initializing"); 89 | let _enter = span.enter(); 90 | 91 | let client = Client::new(self.relay_url.as_str()); 92 | 93 | let connect_response = { 94 | let connect_request = { 95 | let params = { 96 | let mut params = vec![self.remote_signer_pubkey.as_hex_string()]; 97 | if let Some(secret) = &self.connect_secret { 98 | params.push(secret.to_owned()); 99 | } 100 | params 101 | }; 102 | 103 | Nip46Request::new("connect".to_string(), params) 104 | }; 105 | 106 | event!(Level::DEBUG, "Calling with connect request event"); 107 | self.call(&client, connect_request).await? 108 | }; 109 | 110 | if let Some(error) = connect_response.error { 111 | if !error.is_empty() { 112 | return Err(Error::Nip46Error(error)); 113 | } 114 | } 115 | 116 | // Ask for our pubkey 117 | let pubkey_response = { 118 | let pubkey_request = { 119 | let params = vec![]; 120 | Nip46Request::new("get_public_key".to_string(), params) 121 | }; 122 | 123 | event!(Level::DEBUG, "Calling with pubkey request event"); 124 | self.call(&client, pubkey_request).await? 125 | }; 126 | 127 | // Verify there is no error 128 | if let Some(error) = pubkey_response.error { 129 | if !error.is_empty() { 130 | return Err(Error::Nip46Error(error)); 131 | } 132 | } 133 | 134 | let public_key = PublicKey::try_from_hex_string(&pubkey_response.result, true)?; 135 | 136 | Ok(BunkerClient { 137 | remote_signer_pubkey: self.remote_signer_pubkey, 138 | relay_url: self.relay_url.clone(), 139 | local_signer: self.local_signer.clone(), 140 | public_key, 141 | timeout: self.timeout, 142 | client, 143 | }) 144 | } 145 | 146 | async fn call(&self, client: &Client, request: Nip46Request) -> Result { 147 | let span = span!(Level::DEBUG, "nip46 Prebunk callfn"); 148 | let _enter = span.enter(); 149 | 150 | let event = request 151 | .to_event(self.remote_signer_pubkey, self.local_signer.clone()) 152 | .await?; 153 | 154 | // Subscribe 155 | let mut filter = Filter::new(); 156 | filter.add_author(self.remote_signer_pubkey); 157 | filter.add_event_kind(EventKind::NostrConnect); 158 | filter.add_tag_value('p', self.local_signer.public_key().as_hex_string()); 159 | filter.limit = Some(1); 160 | event!( 161 | Level::DEBUG, 162 | "calling client subscribe to subscribe to responses from the remote signer" 163 | ); 164 | let sub_id = client.subscribe(filter.clone(), self.timeout).await?; 165 | 166 | // Post event to server 167 | let event_id = event.id; 168 | event!(Level::DEBUG, "posting our event"); 169 | client.post_event(event, self.timeout).await?; 170 | event!(Level::DEBUG, "waiting for OK response"); 171 | let (ok, msg) = client.wait_for_ok(event_id, self.timeout).await?; 172 | if !ok { 173 | return Err(Error::Nip46FailedToPost(msg)); 174 | } 175 | 176 | // Wait for a response on the subscription 177 | event!( 178 | Level::DEBUG, 179 | "waiting for a response event on the remote signer subscription" 180 | ); 181 | let event = client 182 | .wait_for_subscribed_event(sub_id, self.timeout) 183 | .await?; 184 | let contents = self.local_signer.decrypt_event_contents(&event).await?; 185 | 186 | // Convert into a response 187 | let response: Nip46Response = serde_json::from_str(&contents)?; 188 | 189 | Ok(response) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/nip46/request.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | ContentEncryptionAlgorithm, Error, Event, EventKind, ParsedTag, PreEvent, PublicKey, Signer, 3 | Unixtime, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | use std::sync::Arc; 7 | 8 | /// A NIP-46 request, found stringified in the content of a kind 24133 event 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | pub struct Nip46Request { 11 | /// The Request ID 12 | pub id: String, 13 | 14 | /// The Request Method (See NIP-46) 15 | pub method: String, 16 | 17 | /// The Request parameters 18 | pub params: Vec, 19 | } 20 | 21 | impl Nip46Request { 22 | /// Create a new request object 23 | pub fn new(method: String, params: Vec) -> Nip46Request { 24 | Nip46Request { 25 | id: textnonce::TextNonce::new().into_string(), 26 | method, 27 | params, 28 | } 29 | } 30 | 31 | /// Create a NIP-46 request event from this request 32 | pub async fn to_event( 33 | &self, 34 | bunker_pubkey: PublicKey, 35 | signer: Arc, 36 | ) -> Result { 37 | let request_string = serde_json::to_string(self)?; 38 | 39 | let content = signer 40 | .encrypt( 41 | &bunker_pubkey, 42 | request_string.as_str(), 43 | ContentEncryptionAlgorithm::Nip44v2, 44 | ) 45 | .await?; 46 | 47 | let pre_event = PreEvent { 48 | pubkey: signer.public_key(), 49 | created_at: Unixtime::now(), 50 | kind: EventKind::NostrConnect, 51 | tags: vec![ParsedTag::Pubkey { 52 | pubkey: bunker_pubkey, 53 | recommended_relay_url: None, 54 | petname: None, 55 | } 56 | .into_tag()], 57 | content, 58 | }; 59 | 60 | let event = signer.sign_event(pre_event).await?; 61 | 62 | Ok(event) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/nip46/response.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// A NIP-46 request, found stringified in the content of a kind 24133 event 4 | #[derive(Clone, Debug, Serialize, Deserialize)] 5 | pub struct Nip46Response { 6 | /// The Request Id being responded to 7 | pub id: String, 8 | 9 | /// The result, either a string or a stringified JSON object 10 | pub result: String, 11 | 12 | /// Optionally an error (in which case result is usually empty) 13 | pub error: Option, 14 | } 15 | -------------------------------------------------------------------------------- /src/types/client_message.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{Event, Filter, SubscriptionId}; 2 | use serde::de::Error as DeError; 3 | use serde::de::{Deserialize, Deserializer, IgnoredAny, SeqAccess, Visitor}; 4 | use serde::ser::{Serialize, SerializeSeq, Serializer}; 5 | use std::fmt; 6 | 7 | /// A message from a client to a relay 8 | #[derive(Clone, Debug, Eq, PartialEq)] 9 | pub enum ClientMessage { 10 | /// An event 11 | Event(Box), 12 | 13 | /// A subscription request 14 | Req(SubscriptionId, Filter), 15 | 16 | /// A request to close a subscription 17 | Close(SubscriptionId), 18 | 19 | /// Used to send authentication events 20 | Auth(Box), 21 | 22 | /// Count 23 | Count(SubscriptionId, Filter), 24 | 25 | /// Negentropy Initiation 26 | NegOpen(SubscriptionId, Filter, String), 27 | 28 | /// Negentropy Message 29 | NegMsg(SubscriptionId, String), 30 | 31 | /// Negentropy Close 32 | NegClose(SubscriptionId), 33 | } 34 | 35 | impl ClientMessage { 36 | // Mock data for testing 37 | #[allow(dead_code)] 38 | pub(crate) async fn mock() -> ClientMessage { 39 | ClientMessage::Event(Box::new(Event::mock().await)) 40 | } 41 | } 42 | 43 | impl Serialize for ClientMessage { 44 | fn serialize(&self, serializer: S) -> Result 45 | where 46 | S: Serializer, 47 | { 48 | match self { 49 | ClientMessage::Event(event) => { 50 | let mut seq = serializer.serialize_seq(Some(2))?; 51 | seq.serialize_element("EVENT")?; 52 | seq.serialize_element(&event)?; 53 | seq.end() 54 | } 55 | ClientMessage::Req(id, filter) => { 56 | let mut seq = serializer.serialize_seq(Some(3))?; 57 | seq.serialize_element("REQ")?; 58 | seq.serialize_element(&id)?; 59 | seq.serialize_element(&filter)?; 60 | seq.end() 61 | } 62 | ClientMessage::Close(id) => { 63 | let mut seq = serializer.serialize_seq(Some(2))?; 64 | seq.serialize_element("CLOSE")?; 65 | seq.serialize_element(&id)?; 66 | seq.end() 67 | } 68 | ClientMessage::Auth(event) => { 69 | let mut seq = serializer.serialize_seq(Some(2))?; 70 | seq.serialize_element("AUTH")?; 71 | seq.serialize_element(&event)?; 72 | seq.end() 73 | } 74 | ClientMessage::Count(id, filter) => { 75 | let mut seq = serializer.serialize_seq(Some(3))?; 76 | seq.serialize_element("COUNT")?; 77 | seq.serialize_element(&id)?; 78 | seq.serialize_element(&filter)?; 79 | seq.end() 80 | } 81 | ClientMessage::NegOpen(subid, filter, msg) => { 82 | let mut seq = serializer.serialize_seq(Some(4))?; 83 | seq.serialize_element("NEG-OPEN")?; 84 | seq.serialize_element(&subid)?; 85 | seq.serialize_element(&filter)?; 86 | seq.serialize_element(&msg)?; 87 | seq.end() 88 | } 89 | ClientMessage::NegMsg(subid, msg) => { 90 | let mut seq = serializer.serialize_seq(Some(3))?; 91 | seq.serialize_element("NEG-MSG")?; 92 | seq.serialize_element(&subid)?; 93 | seq.serialize_element(&msg)?; 94 | seq.end() 95 | } 96 | ClientMessage::NegClose(subid) => { 97 | let mut seq = serializer.serialize_seq(Some(2))?; 98 | seq.serialize_element("NEG-CLOSE")?; 99 | seq.serialize_element(&subid)?; 100 | seq.end() 101 | } 102 | } 103 | } 104 | } 105 | 106 | impl<'de> Deserialize<'de> for ClientMessage { 107 | fn deserialize(deserializer: D) -> Result 108 | where 109 | D: Deserializer<'de>, 110 | { 111 | deserializer.deserialize_seq(ClientMessageVisitor) 112 | } 113 | } 114 | 115 | struct ClientMessageVisitor; 116 | 117 | impl<'de> Visitor<'de> for ClientMessageVisitor { 118 | type Value = ClientMessage; 119 | 120 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 121 | write!(f, "a sequence of strings") 122 | } 123 | 124 | fn visit_seq(self, mut seq: A) -> Result 125 | where 126 | A: SeqAccess<'de>, 127 | { 128 | let word: &str = seq 129 | .next_element()? 130 | .ok_or_else(|| DeError::custom("Message missing initial string field"))?; 131 | let mut output: Option = None; 132 | if word == "EVENT" { 133 | let event: Event = seq 134 | .next_element()? 135 | .ok_or_else(|| DeError::custom("Message missing event field"))?; 136 | output = Some(ClientMessage::Event(Box::new(event))) 137 | } else if word == "REQ" { 138 | let id: SubscriptionId = seq 139 | .next_element()? 140 | .ok_or_else(|| DeError::custom("Message missing id field"))?; 141 | let filter: Filter = seq 142 | .next_element()? 143 | .ok_or_else(|| DeError::custom("Message missing filter field"))?; 144 | output = Some(ClientMessage::Req(id, filter)) 145 | } else if word == "COUNT" { 146 | let id: SubscriptionId = seq 147 | .next_element()? 148 | .ok_or_else(|| DeError::custom("Message missing filter field"))?; 149 | let filter: Filter = seq 150 | .next_element()? 151 | .ok_or_else(|| DeError::custom("Message missing filter field"))?; 152 | output = Some(ClientMessage::Count(id, filter)) 153 | } else if word == "CLOSE" { 154 | let id: SubscriptionId = seq 155 | .next_element()? 156 | .ok_or_else(|| DeError::custom("Message missing id field"))?; 157 | output = Some(ClientMessage::Close(id)) 158 | } else if word == "AUTH" { 159 | let event: Event = seq 160 | .next_element()? 161 | .ok_or_else(|| DeError::custom("Message missing event field"))?; 162 | output = Some(ClientMessage::Auth(Box::new(event))) 163 | } else if word == "NEG-OPEN" { 164 | let id: SubscriptionId = seq 165 | .next_element()? 166 | .ok_or_else(|| DeError::custom("Message missing id field"))?; 167 | let filter: Filter = seq 168 | .next_element()? 169 | .ok_or_else(|| DeError::custom("Message missing filter"))?; 170 | let msg: String = seq 171 | .next_element()? 172 | .ok_or_else(|| DeError::custom("Message missing message"))?; 173 | output = Some(ClientMessage::NegOpen(id, filter, msg)) 174 | } else if word == "NEG-MSG" { 175 | let id: SubscriptionId = seq 176 | .next_element()? 177 | .ok_or_else(|| DeError::custom("Message missing id field"))?; 178 | let msg: String = seq 179 | .next_element()? 180 | .ok_or_else(|| DeError::custom("Message missing message"))?; 181 | output = Some(ClientMessage::NegMsg(id, msg)) 182 | } else if word == "NEG-CLOSE" { 183 | let id: SubscriptionId = seq 184 | .next_element()? 185 | .ok_or_else(|| DeError::custom("Message missing id field"))?; 186 | output = Some(ClientMessage::NegClose(id)) 187 | } 188 | 189 | // Consume any trailing fields 190 | while let Some(_ignored) = seq.next_element::()? {} 191 | 192 | match output { 193 | Some(cm) => Ok(cm), 194 | None => Err(DeError::custom(format!("Unknown Message: {word}"))), 195 | } 196 | } 197 | } 198 | 199 | #[cfg(test)] 200 | mod test { 201 | use super::*; 202 | use crate::Event; 203 | 204 | test_serde_async! {ClientMessage, test_client_message_serde} 205 | 206 | test_serde_val_async! { 207 | test_client_message_serde_event, 208 | ClientMessage::Event(Box::new(Event::mock().await)) 209 | } 210 | test_serde_val! { 211 | test_client_message_serde_req, 212 | ClientMessage::Req(SubscriptionId::mock(), Filter::mock()) 213 | } 214 | test_serde_val! { 215 | test_client_message_serde_close, 216 | ClientMessage::Close(SubscriptionId::mock()) 217 | } 218 | test_serde_val_async! { 219 | test_client_message_serde_auth, 220 | ClientMessage::Auth(Box::new(Event::mock().await)) 221 | } 222 | test_serde_val! { 223 | test_client_message_serde_negopen, 224 | ClientMessage::NegOpen(SubscriptionId::mock(), Filter::mock(), "dummy".to_string()) 225 | } 226 | test_serde_val! { 227 | test_client_message_serde_negmsg, 228 | ClientMessage::NegMsg(SubscriptionId::mock(), "dummy".to_string()) 229 | } 230 | test_serde_val! { 231 | test_client_message_serde_negclose, 232 | ClientMessage::NegClose(SubscriptionId::mock()) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/types/delegation.rs: -------------------------------------------------------------------------------- 1 | use super::{EventKind, PublicKey, Signature, Unixtime}; 2 | use crate::Error; 3 | use serde::de::Error as DeError; 4 | use serde::de::{Deserialize, Deserializer, Visitor}; 5 | use serde::ser::{Serialize, Serializer}; 6 | #[cfg(feature = "speedy")] 7 | use speedy::{Readable, Writable}; 8 | use std::fmt; 9 | 10 | /// Delegation information for an Event 11 | #[derive(Clone, Debug, PartialEq, Eq)] 12 | pub enum EventDelegation { 13 | /// The event was not delegated 14 | NotDelegated, 15 | 16 | /// The delegation was invalid (with reason) 17 | InvalidDelegation(String), 18 | 19 | /// The event was delegated and is valid (with pubkey of delegator) 20 | DelegatedBy(PublicKey), 21 | } 22 | 23 | /// Conditions of delegation 24 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 25 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 26 | pub struct DelegationConditions { 27 | /// If the delegation is only for a given event kind 28 | pub kind: Option, 29 | 30 | /// If the delegation is only for events created after a certain time 31 | pub created_after: Option, 32 | 33 | /// If the delegation is only for events created before a certain time 34 | pub created_before: Option, 35 | 36 | /// Optional full string form, in case it was parsed from string 37 | pub full_string: Option, 38 | } 39 | 40 | impl DelegationConditions { 41 | /// Return in conmpiled string form. If full form is stored, it is returned, otherwise it is compiled from parts. 42 | pub fn as_string(&self) -> String { 43 | match &self.full_string { 44 | Some(fs) => fs.clone(), 45 | None => self.compile_full_string(), 46 | } 47 | } 48 | 49 | /// Compile full string from parts. 50 | fn compile_full_string(&self) -> String { 51 | let mut parts: Vec = Vec::new(); 52 | if let Some(kind) = self.kind { 53 | parts.push(format!("kind={}", u32::from(kind))); 54 | } 55 | if let Some(created_after) = self.created_after { 56 | parts.push(format!("created_at>{}", created_after.0)); 57 | } 58 | if let Some(created_before) = self.created_before { 59 | parts.push(format!("created_at<{}", created_before.0)); 60 | } 61 | parts.join("&") 62 | } 63 | 64 | #[allow(dead_code)] 65 | fn update_full_string(&mut self) { 66 | self.full_string = Some(self.compile_full_string()) 67 | } 68 | 69 | /// Convert from string from 70 | pub fn try_from_str(s: &str) -> Result { 71 | let mut output: DelegationConditions = Default::default(); 72 | 73 | let parts = s.split('&'); 74 | for part in parts { 75 | if let Some(kindstr) = part.strip_prefix("kind=") { 76 | let event_num = kindstr.parse::()?; 77 | let event_kind: EventKind = From::from(event_num); 78 | output.kind = Some(event_kind); 79 | } 80 | if let Some(timestr) = part.strip_prefix("created_at>") { 81 | let time = timestr.parse::()?; 82 | output.created_after = Some(Unixtime(time)); 83 | } 84 | if let Some(timestr) = part.strip_prefix("created_at<") { 85 | let time = timestr.parse::()?; 86 | output.created_before = Some(Unixtime(time)); 87 | } 88 | } 89 | // store orignal string 90 | output.full_string = Some(s.to_string()); 91 | 92 | Ok(output) 93 | } 94 | 95 | #[allow(dead_code)] 96 | pub(crate) fn mock() -> DelegationConditions { 97 | let mut dc = DelegationConditions { 98 | kind: Some(EventKind::Repost), 99 | created_after: Some(Unixtime(1677700000)), 100 | created_before: None, 101 | full_string: None, 102 | }; 103 | dc.update_full_string(); 104 | dc 105 | } 106 | 107 | /// Verify the signature part of a Delegation tag 108 | pub fn verify_signature( 109 | &self, 110 | pubkey_delegater: &PublicKey, 111 | pubkey_delegatee: &PublicKey, 112 | signature: &Signature, 113 | ) -> Result<(), Error> { 114 | let input = format!( 115 | "nostr:delegation:{}:{}", 116 | pubkey_delegatee.as_hex_string(), 117 | self.as_string() 118 | ); 119 | pubkey_delegater.verify(input.as_bytes(), signature) 120 | } 121 | } 122 | 123 | impl Serialize for DelegationConditions { 124 | fn serialize(&self, serializer: S) -> Result 125 | where 126 | S: Serializer, 127 | { 128 | serializer.serialize_str(&self.as_string()) 129 | } 130 | } 131 | 132 | impl<'de> Deserialize<'de> for DelegationConditions { 133 | fn deserialize(deserializer: D) -> Result 134 | where 135 | D: Deserializer<'de>, 136 | { 137 | deserializer.deserialize_str(DelegationConditionsVisitor) 138 | } 139 | } 140 | 141 | struct DelegationConditionsVisitor; 142 | 143 | impl Visitor<'_> for DelegationConditionsVisitor { 144 | type Value = DelegationConditions; 145 | 146 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 147 | write!(f, "A string") 148 | } 149 | 150 | fn visit_str(self, v: &str) -> Result 151 | where 152 | E: DeError, 153 | { 154 | DelegationConditions::try_from_str(v).map_err(|e| E::custom(format!("{e}"))) 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod test { 160 | use super::*; 161 | use crate::{KeySigner, ParsedTag, PrivateKey, SignerExt, Tag}; 162 | 163 | test_serde! {DelegationConditions, test_delegation_conditions_serde} 164 | 165 | #[tokio::test] 166 | async fn test_sign_delegation_verify_delegation_signature() { 167 | let delegator_private_key = PrivateKey::try_from_hex_string( 168 | "ee35e8bb71131c02c1d7e73231daa48e9953d329a4b701f7133c8f46dd21139c", 169 | ) 170 | .unwrap(); 171 | let delegator_public_key = delegator_private_key.public_key(); 172 | 173 | let signer = KeySigner::from_private_key(delegator_private_key, "lockme", 16).unwrap(); 174 | 175 | let delegatee_public_key = PublicKey::try_from_hex_string( 176 | "477318cfb5427b9cfc66a9fa376150c1ddbc62115ae27cef72417eb959691396", 177 | true, 178 | ) 179 | .unwrap(); 180 | 181 | let dc = DelegationConditions::try_from_str( 182 | "kind=1&created_at>1674834236&created_at<1677426236", 183 | ) 184 | .unwrap(); 185 | 186 | let sig = signer 187 | .generate_delegation_signature(delegatee_public_key, &dc) 188 | .await 189 | .unwrap(); 190 | 191 | let verify_result = dc.verify_signature(&delegator_public_key, &delegatee_public_key, &sig); 192 | assert!(verify_result.is_ok()); 193 | } 194 | 195 | #[test] 196 | fn test_delegation_tag_parse_and_verify() { 197 | let tag_str = "[\"delegation\",\"1a459a8a6aa6441d480ba665fb8fb21a4cfe8bcacb7d87300f8046a558a3fce4\",\"kind=1&created_at>1676067553&created_at<1678659553\",\"369aed09c1ad52fceb77ecd6c16f2433eac4a3803fc41c58876a5b60f4f36b9493d5115e5ec5a0ce6c3668ffe5b58d47f2cbc97233833bb7e908f66dbbbd9d36\"]"; 198 | let dt = serde_json::from_str::(tag_str).unwrap(); 199 | if let Ok(ParsedTag::Delegation { 200 | pubkey, 201 | conditions, 202 | sig, 203 | }) = dt.parse() 204 | { 205 | assert_eq!( 206 | conditions.as_string(), 207 | "kind=1&created_at>1676067553&created_at<1678659553" 208 | ); 209 | 210 | let delegatee_public_key = PublicKey::try_from_hex_string( 211 | "bea8aeb6c1657e33db5ac75a83910f77e8ec6145157e476b5b88c6e85b1fab34", 212 | true, 213 | ) 214 | .unwrap(); 215 | 216 | let verify_result = conditions.verify_signature(&pubkey, &delegatee_public_key, &sig); 217 | assert!(verify_result.is_ok()); 218 | } else { 219 | panic!("Incorrect tag type") 220 | } 221 | } 222 | 223 | #[test] 224 | fn test_delegation_tag_parse_and_verify_alt_order() { 225 | // Clauses in the condition string are not in the canonical order, but this should not matter 226 | let tag_str = "[\"delegation\",\"05bc52a6117c57f99b73f5315f3105b21cecdcd2c6825dee8d508bd7d972ad6a\",\"kind=1&created_at<1686078180&created_at>1680807780\",\"1016d2f4284cdb4e6dc6eaa4e61dff87b9f4138786154d070d36e9434f817bd623abed2133bb62b9dcfb2fbf54b42e16bcd44cfc23907f8eb5b45c011caaa47c\"]"; 227 | let dt = serde_json::from_str::(tag_str).unwrap(); 228 | if let Ok(ParsedTag::Delegation { 229 | pubkey, 230 | conditions, 231 | sig, 232 | }) = dt.parse() 233 | { 234 | assert_eq!( 235 | conditions.as_string(), 236 | "kind=1&created_at<1686078180&created_at>1680807780" 237 | ); 238 | 239 | let delegatee_public_key = PublicKey::try_from_hex_string( 240 | "111c02821806b046068dffc4d8e4de4a56bc99d3015c335b8929d900928fa317", 241 | true, 242 | ) 243 | .unwrap(); 244 | 245 | let verify_result = conditions.verify_signature(&pubkey, &delegatee_public_key, &sig); 246 | assert!(verify_result.is_ok()); 247 | } else { 248 | panic!("Incorrect tag type") 249 | } 250 | } 251 | 252 | #[test] 253 | fn test_from_str() { 254 | let str = "kind=1&created_at>1000000&created_at<2000000"; 255 | let dc = DelegationConditions::try_from_str(str).unwrap(); 256 | assert_eq!(dc.as_string(), str); 257 | } 258 | 259 | #[test] 260 | fn test_from_str_alt_order() { 261 | // Even with alternative order, as_string() should return the same 262 | let str = "created_at<2000000&created_at>1000000&kind=1"; 263 | let dc = DelegationConditions::try_from_str(str).unwrap(); 264 | assert_eq!(dc.as_string(), str); 265 | } 266 | 267 | #[test] 268 | fn test_as_string() { 269 | let dc = DelegationConditions { 270 | kind: Some(EventKind::TextNote), 271 | created_before: Some(Unixtime(2000000)), 272 | created_after: Some(Unixtime(1000000)), 273 | full_string: None, 274 | }; 275 | assert_eq!( 276 | dc.as_string(), 277 | "kind=1&created_at>1000000&created_at<2000000" 278 | ); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/types/event.rs: -------------------------------------------------------------------------------- 1 | use crate::versioned::event3::{EventV3, PreEventV3, RumorV3}; 2 | use crate::versioned::zap_data::ZapDataV2; 3 | 4 | /// The main event type 5 | pub type Event = EventV3; 6 | 7 | /// Data used to construct an event 8 | pub type PreEvent = PreEventV3; 9 | 10 | /// A Rumor is an Event without a signature 11 | pub type Rumor = RumorV3; 12 | 13 | /// Data about a Zap 14 | pub type ZapData = ZapDataV2; 15 | -------------------------------------------------------------------------------- /src/types/event_reference.rs: -------------------------------------------------------------------------------- 1 | use super::{Id, NAddr, PublicKey, RelayUrl}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::hash::{Hash, Hasher}; 4 | 5 | /// A reference to another event, either by `Id` (often coming from an 'e' tag), 6 | /// or by `NAddr` (often coming from an 'a' tag). 7 | #[derive(Clone, Debug, Serialize, Deserialize)] 8 | pub enum EventReference { 9 | /// Refer to a specific event by Id 10 | Id { 11 | /// The event id 12 | id: Id, 13 | 14 | /// Optionally include author (to find via their relay list) 15 | author: Option, 16 | 17 | /// Optionally include relays (to find the event) 18 | relays: Vec, 19 | 20 | /// Optional marker, if this came from an event tag 21 | marker: Option, 22 | }, 23 | 24 | /// Refer to a replaceable event by NAddr 25 | Addr(NAddr), 26 | } 27 | 28 | impl EventReference { 29 | /// Get the author 30 | pub fn author(&self) -> Option { 31 | match self { 32 | EventReference::Id { author, .. } => *author, 33 | EventReference::Addr(naddr) => Some(naddr.author), 34 | } 35 | } 36 | 37 | /// Set the author 38 | pub fn set_author(&mut self, new_author: PublicKey) { 39 | match self { 40 | EventReference::Id { ref mut author, .. } => *author = Some(new_author), 41 | EventReference::Addr(ref mut naddr) => naddr.author = new_author, 42 | } 43 | } 44 | 45 | /// Copy the relays 46 | pub fn copy_relays(&self) -> Vec { 47 | match self { 48 | EventReference::Id { relays, .. } => relays.clone(), 49 | EventReference::Addr(naddr) => naddr 50 | .relays 51 | .iter() 52 | .filter_map(|r| RelayUrl::try_from_unchecked_url(r).ok()) 53 | .collect(), 54 | } 55 | } 56 | 57 | /// Extend relays 58 | pub fn extend_relays(&mut self, relays: Vec) { 59 | let mut new_relays = self.copy_relays(); 60 | new_relays.extend(relays); 61 | 62 | match self { 63 | EventReference::Id { ref mut relays, .. } => *relays = new_relays, 64 | EventReference::Addr(ref mut naddr) => { 65 | naddr.relays = new_relays.iter().map(|r| r.to_unchecked_url()).collect() 66 | } 67 | } 68 | } 69 | } 70 | 71 | impl PartialEq for EventReference { 72 | fn eq(&self, other: &Self) -> bool { 73 | match self { 74 | EventReference::Id { id: id1, .. } => { 75 | match other { 76 | EventReference::Id { id: id2, .. } => { 77 | // We don't compare the other fields which are only helpers, 78 | // not definitive identity 79 | id1 == id2 80 | } 81 | _ => false, 82 | } 83 | } 84 | EventReference::Addr(addr1) => match other { 85 | EventReference::Addr(addr2) => addr1 == addr2, 86 | _ => false, 87 | }, 88 | } 89 | } 90 | } 91 | 92 | impl Eq for EventReference {} 93 | 94 | impl Hash for EventReference { 95 | fn hash(&self, state: &mut H) { 96 | match self { 97 | EventReference::Id { id, .. } => { 98 | // We do not hash the other fields which are only helpers, 99 | // not definitive identity 100 | id.hash(state); 101 | } 102 | EventReference::Addr(addr) => { 103 | addr.hash(state); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/types/file_metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::{Event, EventKind, PreEvent, PublicKey, Tag, UncheckedUrl, Unixtime}; 2 | 3 | /// NIP-92/94 File Metadata 4 | #[derive(Clone, Debug, Hash, PartialEq)] 5 | pub struct FileMetadata { 6 | /// The URL this metadata applies to 7 | pub url: UncheckedUrl, 8 | 9 | /// Mime type (lowercase), see https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types 10 | pub m: Option, 11 | 12 | /// SHA-256 hex-encoded hash 13 | pub x: Option, 14 | 15 | /// original SHA-256 hex-encoded hash prior to transformations 16 | pub ox: Option, 17 | 18 | /// Size of file in bytes 19 | pub size: Option, 20 | 21 | /// Dimensions of the image 22 | pub dim: Option<(usize, usize)>, 23 | 24 | /// Magnet URI 25 | pub magnet: Option, 26 | 27 | /// Torrent infohash 28 | pub i: Option, 29 | 30 | /// Blurhash 31 | pub blurhash: Option, 32 | 33 | /// Thumbnail URL 34 | pub thumb: Option, 35 | 36 | /// Preview image (same dimensions) 37 | pub image: Option, 38 | 39 | /// Summary text 40 | pub summary: Option, 41 | 42 | /// Alt description 43 | pub alt: Option, 44 | 45 | /// Fallback URLs 46 | pub fallback: Vec, 47 | 48 | /// Service 49 | pub service: Option, 50 | } 51 | 52 | impl FileMetadata { 53 | /// Create a new empty (except the URL) FileMetadata 54 | pub fn new(url: UncheckedUrl) -> FileMetadata { 55 | FileMetadata { 56 | url, 57 | m: None, 58 | x: None, 59 | ox: None, 60 | size: None, 61 | dim: None, 62 | magnet: None, 63 | i: None, 64 | blurhash: None, 65 | thumb: None, 66 | image: None, 67 | summary: None, 68 | alt: None, 69 | fallback: vec![], 70 | service: None, 71 | } 72 | } 73 | 74 | /// Create a NIP-94 FileMetadata PreEvent from this FileMetadata 75 | pub fn to_nip94_preevent(&self, pubkey: PublicKey) -> PreEvent { 76 | let mut tags = vec![Tag::new(&["url", &self.url.0])]; 77 | 78 | if let Some(m) = &self.m { 79 | tags.push(Tag::new(&["m", m])); 80 | } 81 | 82 | if let Some(x) = &self.x { 83 | tags.push(Tag::new(&["x", x])); 84 | } 85 | 86 | if let Some(ox) = &self.ox { 87 | tags.push(Tag::new(&["ox", ox])); 88 | } 89 | 90 | if let Some(size) = self.size { 91 | tags.push(Tag::new(&["size", &format!("{size}")])); 92 | } 93 | 94 | if let Some(dim) = self.dim { 95 | tags.push(Tag::new(&["dim", &format!("{}x{}", dim.0, dim.1)])); 96 | } 97 | 98 | if let Some(magnet) = &self.magnet { 99 | tags.push(Tag::new(&["magnet", &magnet.0])); 100 | } 101 | 102 | if let Some(i) = &self.i { 103 | tags.push(Tag::new(&["i", i])); 104 | } 105 | 106 | if let Some(blurhash) = &self.blurhash { 107 | tags.push(Tag::new(&["blurhash", blurhash])); 108 | } 109 | 110 | if let Some(thumb) = &self.thumb { 111 | tags.push(Tag::new(&["thumb", &thumb.0])); 112 | } 113 | 114 | if let Some(image) = &self.image { 115 | tags.push(Tag::new(&["image", &image.0])); 116 | } 117 | 118 | if let Some(summary) = &self.summary { 119 | tags.push(Tag::new(&["summary", summary])); 120 | } 121 | 122 | if let Some(alt) = &self.alt { 123 | tags.push(Tag::new(&["alt", alt])); 124 | } 125 | 126 | for fallback in &self.fallback { 127 | tags.push(Tag::new(&["fallback", &fallback.0])); 128 | } 129 | 130 | if let Some(service) = &self.service { 131 | tags.push(Tag::new(&["service", service])); 132 | } 133 | 134 | PreEvent { 135 | pubkey, 136 | created_at: Unixtime::now(), 137 | kind: EventKind::FileMetadata, 138 | content: "".to_owned(), 139 | tags, 140 | } 141 | } 142 | 143 | /// Turn a kind-1063 (FileMetadata) event into a FileMetadata structure 144 | pub fn from_nip94_event(event: &Event) -> Option { 145 | if event.kind != EventKind::FileMetadata { 146 | return None; 147 | } 148 | 149 | let mut fm = FileMetadata::new(UncheckedUrl("".to_owned())); 150 | 151 | for tag in &event.tags { 152 | match tag.tagname() { 153 | "url" => fm.url = UncheckedUrl(tag.value().to_owned()), 154 | "m" => fm.m = Some(tag.value().to_owned()), 155 | "x" => fm.x = Some(tag.value().to_owned()), 156 | "ox" => fm.ox = Some(tag.value().to_owned()), 157 | "size" => { 158 | if let Ok(u) = tag.value().parse::() { 159 | fm.size = Some(u); 160 | } 161 | } 162 | "dim" => { 163 | let parts: Vec<&str> = tag.value().split('x').collect(); 164 | if parts.len() == 2 { 165 | if let Ok(w) = parts[0].parse::() { 166 | if let Ok(h) = parts[1].parse::() { 167 | fm.dim = Some((w, h)); 168 | } 169 | } 170 | } 171 | } 172 | "magnet" => fm.magnet = Some(UncheckedUrl(tag.value().to_owned())), 173 | "i" => fm.i = Some(tag.value().to_owned()), 174 | "blurhash" => fm.blurhash = Some(tag.value().to_owned()), 175 | "thumb" => fm.thumb = Some(UncheckedUrl(tag.value().to_owned())), 176 | "image" => fm.image = Some(UncheckedUrl(tag.value().to_owned())), 177 | "summary" => fm.summary = Some(tag.value().to_owned()), 178 | "alt" => fm.alt = Some(tag.value().to_owned()), 179 | "fallback" => fm.fallback.push(UncheckedUrl(tag.value().to_owned())), 180 | "service" => fm.service = Some(tag.value().to_owned()), 181 | _ => continue, 182 | } 183 | } 184 | 185 | if !fm.url.0.is_empty() { 186 | Some(fm) 187 | } else { 188 | None 189 | } 190 | } 191 | 192 | /// Convert into an 'imeta' tag 193 | pub fn to_imeta_tag(&self) -> Tag { 194 | let mut tag = Tag::new(&["imeta"]); 195 | 196 | tag.push_value(format!("url {}", self.url)); 197 | 198 | if let Some(m) = &self.m { 199 | tag.push_value(format!("m {}", m)); 200 | } 201 | 202 | if let Some(x) = &self.x { 203 | tag.push_value(format!("x {}", x)); 204 | } 205 | 206 | if let Some(ox) = &self.ox { 207 | tag.push_value(format!("ox {}", ox)); 208 | } 209 | 210 | if let Some(size) = &self.size { 211 | tag.push_value(format!("size {}", size)); 212 | } 213 | 214 | if let Some(dim) = &self.dim { 215 | tag.push_value(format!("dim {}x{}", dim.0, dim.1)); 216 | } 217 | 218 | if let Some(magnet) = &self.magnet { 219 | tag.push_value(format!("magnet {}", magnet)); 220 | } 221 | 222 | if let Some(i) = &self.i { 223 | tag.push_value(format!("i {}", i)); 224 | } 225 | 226 | if let Some(blurhash) = &self.blurhash { 227 | tag.push_value(format!("blurhash {}", blurhash)); 228 | } 229 | 230 | if let Some(thumb) = &self.thumb { 231 | tag.push_value(format!("thumb {}", thumb)); 232 | } 233 | 234 | if let Some(image) = &self.image { 235 | tag.push_value(format!("image {}", image)); 236 | } 237 | 238 | if let Some(summary) = &self.summary { 239 | tag.push_value(format!("summary {}", summary)); 240 | } 241 | 242 | if let Some(alt) = &self.alt { 243 | tag.push_value(format!("alt {}", alt)); 244 | } 245 | 246 | for fallback in &self.fallback { 247 | tag.push_value(format!("fallback {}", fallback)); 248 | } 249 | 250 | if let Some(service) = &self.service { 251 | tag.push_value(format!("service {}", service)); 252 | } 253 | 254 | tag 255 | } 256 | 257 | /// Import from an 'imeta' tag 258 | pub fn from_imeta_tag(tag: &Tag) -> Option { 259 | let mut fm = FileMetadata::new(UncheckedUrl("".to_owned())); 260 | 261 | for i in 0..tag.len() { 262 | let parts: Vec<&str> = tag.get_index(i).splitn(2, ' ').collect(); 263 | if parts.len() < 2 { 264 | continue; 265 | } 266 | match parts[0] { 267 | "url" => fm.url = UncheckedUrl(parts[1].to_owned()), 268 | "m" => fm.m = Some(parts[1].to_owned()), 269 | "x" => fm.x = Some(parts[1].to_owned()), 270 | "ox" => fm.ox = Some(parts[1].to_owned()), 271 | "size" => { 272 | if let Ok(u) = parts[1].parse::() { 273 | fm.size = Some(u); 274 | } 275 | } 276 | "dim" => { 277 | let parts: Vec<&str> = parts[1].split('x').collect(); 278 | if parts.len() == 2 { 279 | if let Ok(w) = parts[0].parse::() { 280 | if let Ok(h) = parts[1].parse::() { 281 | fm.dim = Some((w, h)); 282 | } 283 | } 284 | } 285 | } 286 | "magnet" => fm.magnet = Some(UncheckedUrl(parts[1].to_owned())), 287 | "i" => fm.i = Some(parts[1].to_owned()), 288 | "blurhash" => fm.blurhash = Some(parts[1].to_owned()), 289 | "thumb" => fm.thumb = Some(UncheckedUrl(parts[1].to_owned())), 290 | "image" => fm.image = Some(UncheckedUrl(parts[1].to_owned())), 291 | "summary" => fm.summary = Some(parts[1].to_owned()), 292 | "alt" => fm.alt = Some(parts[1].to_owned()), 293 | "fallback" => fm.fallback.push(UncheckedUrl(parts[1].to_owned())), 294 | "service" => fm.service = Some(parts[1].to_owned()), 295 | _ => continue, 296 | } 297 | } 298 | 299 | if !fm.url.0.is_empty() { 300 | Some(fm) 301 | } else { 302 | None 303 | } 304 | } 305 | } 306 | 307 | #[cfg(test)] 308 | mod test { 309 | use super::*; 310 | 311 | #[tokio::test] 312 | async fn test_nip94_event() { 313 | let mut fm = FileMetadata::new(UncheckedUrl("https://nostr.build/blahblahblah".to_owned())); 314 | fm.x = Some("12345".to_owned()); 315 | fm.service = Some("http".to_owned()); 316 | fm.size = Some(10124); 317 | fm.alt = Some("a crackerjack".to_owned()); 318 | 319 | use crate::{PrivateKey, Signer}; 320 | let private_key = PrivateKey::generate(); 321 | let public_key = private_key.public_key(); 322 | 323 | let pre_event = fm.to_nip94_preevent(public_key); 324 | let event = private_key.sign_event(pre_event).await.unwrap(); 325 | let fm2 = FileMetadata::from_nip94_event(&event).unwrap(); 326 | 327 | assert_eq!(fm, fm2); 328 | } 329 | 330 | #[test] 331 | fn test_imeta_tag() { 332 | let mut fm = FileMetadata::new(UncheckedUrl("https://nostr.build/blahblahblah".to_owned())); 333 | fm.x = Some("12345".to_owned()); 334 | fm.service = Some("http".to_owned()); 335 | fm.size = Some(10124); 336 | fm.alt = Some("a crackerjack".to_owned()); 337 | 338 | let tag = fm.to_imeta_tag(); 339 | let fm2 = FileMetadata::from_imeta_tag(&tag).unwrap(); 340 | assert_eq!(fm, fm2); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/types/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::versioned::filter2::FilterV2; 2 | 3 | /// The main filter type 4 | pub type Filter = FilterV2; 5 | -------------------------------------------------------------------------------- /src/types/hll8.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use std::ops::AddAssign; 3 | 4 | /// HyperLogLog approximate counting mechanism 5 | /// 6 | /// This uses a fixed set of 256 buckets (k=8, M=256), each holding a count up to 255. 7 | /// https://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf 8 | /// 9 | /// This is used for NIP-45 PR #1561 10 | #[derive(Debug, Clone, Copy)] 11 | pub struct Hll8([u8; 256]); 12 | 13 | impl Default for Hll8 { 14 | fn default() -> Self { 15 | Self::new() 16 | } 17 | } 18 | 19 | impl Hll8 { 20 | /// Create a new Hll8 21 | pub fn new() -> Hll8 { 22 | Hll8([0; 256]) 23 | } 24 | 25 | /// Import from a (hex) string 26 | pub fn from_hex_string(s: &str) -> Result { 27 | let vec: Vec = hex::decode(s)?; 28 | let arr: [u8; 256] = vec.try_into().map_err(|_| Error::InvalidHll)?; 29 | Ok(Hll8(arr)) 30 | } 31 | 32 | /// Export to a (hex) string 33 | pub fn to_hex_string(&self) -> String { 34 | hex::encode(self.0) 35 | } 36 | 37 | /// Clear to zero counts 38 | pub fn clear(&mut self) { 39 | for i in 0..=255 { 40 | self.0[i] = 0 41 | } 42 | } 43 | 44 | /// Add an element to the count by value 45 | /// Returns false on error (e.g. offset is out of range) 46 | pub fn add_element(&mut self, input: &[u8; 32], offset: usize) -> Result<(), Error> { 47 | if offset >= 24 { 48 | return Err(Error::OutOfRange(offset)); 49 | } 50 | 51 | // Use the byte at that offset as the bucket 52 | let index = input[offset]; 53 | 54 | // Count zeros after that offset 55 | let zeros = { 56 | let mut zeros: u8 = 0; 57 | #[allow(clippy::needless_range_loop)] 58 | for i in offset + 1..=31 { 59 | let leading = input[i].leading_zeros(); 60 | zeros += leading as u8; 61 | if leading < 8 { 62 | break; 63 | } 64 | } 65 | zeros 66 | }; 67 | 68 | let rho = zeros + 1; 69 | self.add_element_inner(index, rho); 70 | 71 | Ok(()) 72 | } 73 | 74 | /// Add an element to the count by index and rho (position of the first 1) 75 | pub fn add_element_inner(&mut self, index: u8, rho: u8) { 76 | if rho > self.0[index as usize] { 77 | self.0[index as usize] = rho; 78 | } 79 | } 80 | 81 | /// Compute the approximate count 82 | pub fn estimate_count(&self) -> usize { 83 | // 2007 paper calls this 'V'; 2016 paper calls this 'z' 84 | let zero_count = self.0.iter().filter(|&c| *c == 0).count(); 85 | 86 | // Sum over the reciprocals (SUM of 2^-m) 87 | let mut sum: f64 = 0.0; 88 | for i in 0..=255 { 89 | let power: usize = 1 << self.0[i]; 90 | sum += 1.0 / (power as f64); 91 | } 92 | 93 | let estimate = estimate_hyperloglog(sum, zero_count); 94 | // estimate_fiatjaf(sum, zero_count); 95 | 96 | estimate.round() as usize 97 | } 98 | } 99 | 100 | impl AddAssign for Hll8 { 101 | fn add_assign(&mut self, other: Self) { 102 | for i in 0..=255 { 103 | if other.0[i] > self.0[i] { 104 | self.0[i] = other.0[i]; 105 | } 106 | } 107 | } 108 | } 109 | 110 | // The number of buckets we use = 2**k ('m' from the papers) 111 | const M: f64 = 256.0; 112 | 113 | // The correction factor, 'α' from the paper for k=8,M=256 114 | const ALPHA: f64 = 0.7213 / (1.0 + 1.079 / M); 115 | 116 | // 2^32 as a floating point number 117 | const TWO32: f64 = 4_294_967_296.0; 118 | 119 | // HyperLogLog++ Threshold, when to switch from linear counting for M=256 (k=8) 120 | #[allow(dead_code)] 121 | const THRESHOLD: f64 = 220.0; 122 | 123 | fn estimate_hyperloglog(sum: f64, zero_count: usize) -> f64 { 124 | let mut estimate = ALPHA * (M * M) / sum; 125 | if estimate <= (5.0 / 2.0) * M { 126 | // 640 127 | if zero_count != 0 { 128 | estimate = M * (M / (zero_count as f64)).ln(); // linear 129 | } 130 | } else if estimate > (1.0 / 30.0) * TWO32 { 131 | // 143165576 132 | estimate = -TWO32 * (1.0 - estimate / TWO32).log2(); 133 | }; 134 | estimate 135 | } 136 | 137 | #[allow(dead_code)] 138 | fn estimate_fiatjaf(sum: f64, zero_count: usize) -> f64 { 139 | let estimate = ALPHA * M * M / sum; 140 | if zero_count == 0 { 141 | return estimate; 142 | } 143 | let linear = M * (M / zero_count as f64).ln(); 144 | if linear <= THRESHOLD { 145 | linear 146 | } else if estimate < 256.0 * 3.0 { 147 | // 768 148 | linear 149 | } else { 150 | estimate 151 | } 152 | } 153 | 154 | #[cfg(test)] 155 | mod test { 156 | use super::*; 157 | 158 | #[test] 159 | fn test_hll8() { 160 | use rand::seq::SliceRandom; 161 | use rand_core::{OsRng, RngCore}; 162 | let mut rng = rand::thread_rng(); 163 | 164 | // Create 2500 well known different keys 165 | let mut input: Vec<[u8; 32]> = Vec::new(); 166 | for _ in 0..2500 { 167 | let mut key = [0u8; 32]; 168 | OsRng.fill_bytes(&mut key); 169 | input.push(key); 170 | } 171 | 172 | for numkeys in [ 173 | 1, 2, 3, 5, 9, 15, 33, 50, 89, 115, 150, 195, 260, 420, 1000, 2500, 174 | ] { 175 | // Test Hll8 using these keys 176 | let mut h = Hll8::new(); 177 | for _ in 0..=100_000 { 178 | let random_key = input[0..numkeys].choose(&mut rng).unwrap(); 179 | h.add_element(random_key, 16).unwrap(); 180 | } 181 | 182 | // Even though we added 100,000 elements, there are only numkey distinct ones. 183 | 184 | println!("Actual: {} Estimate: {}", numkeys, h.estimate_count()); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/types/id.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use derive_more::{AsMut, AsRef, Deref, Display, From, FromStr, Into}; 3 | use serde::de::{Deserializer, Visitor}; 4 | use serde::ser::Serializer; 5 | use serde::{Deserialize, Serialize}; 6 | #[cfg(feature = "speedy")] 7 | use speedy::{Readable, Writable}; 8 | use std::fmt; 9 | 10 | /// An event identifier, constructed as a SHA256 hash of the event fields according to NIP-01 11 | #[derive( 12 | AsMut, AsRef, Clone, Copy, Debug, Deref, Eq, From, Hash, Into, Ord, PartialEq, PartialOrd, 13 | )] 14 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 15 | pub struct Id(pub [u8; 32]); 16 | 17 | impl Id { 18 | /// Render into a hexadecimal string 19 | /// 20 | /// Consider converting `.into()` an `IdHex` which is a wrapped type rather than a naked `String` 21 | pub fn as_hex_string(&self) -> String { 22 | hex::encode(self.0) 23 | } 24 | 25 | /// Create from a hexadecimal string 26 | pub fn try_from_hex_string(v: &str) -> Result { 27 | let vec: Vec = hex::decode(v)?; 28 | Ok(Id(vec 29 | .try_into() 30 | .map_err(|_| Error::WrongLengthHexString)?)) 31 | } 32 | 33 | /// Export as a bech32 encoded string ("note") 34 | pub fn as_bech32_string(&self) -> String { 35 | bech32::encode::(*crate::HRP_NOTE, &self.0).unwrap() 36 | } 37 | 38 | /// Import from a bech32 encoded string ("note") 39 | pub fn try_from_bech32_string(s: &str) -> Result { 40 | let data = bech32::decode(s)?; 41 | if data.0 != *crate::HRP_NOTE { 42 | Err(Error::WrongBech32( 43 | crate::HRP_NOTE.to_lowercase(), 44 | data.0.to_lowercase(), 45 | )) 46 | } else if data.1.len() != 32 { 47 | Err(Error::InvalidId) 48 | } else { 49 | match <[u8; 32]>::try_from(data.1) { 50 | Ok(array) => Ok(Id(array)), 51 | _ => Err(Error::InvalidId), 52 | } 53 | } 54 | } 55 | 56 | // Mock data for testing 57 | #[allow(dead_code)] 58 | pub(crate) fn mock() -> Id { 59 | Id::try_from_hex_string("5df64b33303d62afc799bdc36d178c07b2e1f0d824f31b7dc812219440affab6") 60 | .unwrap() 61 | } 62 | } 63 | 64 | impl Serialize for Id { 65 | fn serialize(&self, serializer: S) -> Result 66 | where 67 | S: Serializer, 68 | { 69 | serializer.serialize_str(&hex::encode(self.0)) 70 | } 71 | } 72 | 73 | impl<'de> Deserialize<'de> for Id { 74 | fn deserialize(deserializer: D) -> Result 75 | where 76 | D: Deserializer<'de>, 77 | { 78 | deserializer.deserialize_str(IdVisitor) 79 | } 80 | } 81 | 82 | struct IdVisitor; 83 | 84 | impl Visitor<'_> for IdVisitor { 85 | type Value = Id; 86 | 87 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 88 | write!(f, "a lowercase hexadecimal string representing 32 bytes") 89 | } 90 | 91 | fn visit_str(self, v: &str) -> Result 92 | where 93 | E: serde::de::Error, 94 | { 95 | let vec: Vec = hex::decode(v).map_err(|e| serde::de::Error::custom(format!("{e}")))?; 96 | 97 | Ok(Id(vec.try_into().map_err(|e: Vec| { 98 | E::custom(format!( 99 | "Id is not 32 bytes long. Was {} bytes long", 100 | e.len() 101 | )) 102 | })?)) 103 | } 104 | } 105 | 106 | /// An event identifier, constructed as a SHA256 hash of the event fields according to NIP-01, as a hex string 107 | /// 108 | /// You can convert from an `Id` into this with `From`/`Into`. You can convert this back to an `Id` with `TryFrom`/`TryInto`. 109 | #[derive( 110 | AsMut, 111 | AsRef, 112 | Clone, 113 | Debug, 114 | Deref, 115 | Display, 116 | Eq, 117 | From, 118 | FromStr, 119 | Hash, 120 | Into, 121 | Ord, 122 | PartialEq, 123 | PartialOrd, 124 | )] 125 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 126 | pub struct IdHex(String); 127 | 128 | impl IdHex { 129 | // Mock data for testing 130 | #[allow(dead_code)] 131 | pub(crate) fn mock() -> IdHex { 132 | From::from(Id::mock()) 133 | } 134 | 135 | /// Try from &str 136 | pub fn try_from_str(s: &str) -> Result { 137 | Self::try_from_string(s.to_owned()) 138 | } 139 | 140 | /// Try from String 141 | pub fn try_from_string(s: String) -> Result { 142 | if s.len() != 64 { 143 | return Err(Error::InvalidId); 144 | } 145 | let vec: Vec = hex::decode(&s)?; 146 | if vec.len() != 32 { 147 | return Err(Error::InvalidId); 148 | } 149 | Ok(IdHex(s)) 150 | } 151 | 152 | /// As &str 153 | pub fn as_str(&self) -> &str { 154 | &self.0 155 | } 156 | 157 | /// Into String 158 | pub fn into_string(self) -> String { 159 | self.0 160 | } 161 | } 162 | 163 | impl TryFrom<&str> for IdHex { 164 | type Error = Error; 165 | 166 | fn try_from(s: &str) -> Result { 167 | IdHex::try_from_str(s) 168 | } 169 | } 170 | 171 | impl From for IdHex { 172 | fn from(i: Id) -> IdHex { 173 | IdHex(i.as_hex_string()) 174 | } 175 | } 176 | 177 | impl From for Id { 178 | fn from(h: IdHex) -> Id { 179 | // could only fail if IdHex is invalid 180 | Id::try_from_hex_string(&h.0).unwrap() 181 | } 182 | } 183 | 184 | impl Serialize for IdHex { 185 | fn serialize(&self, serializer: S) -> Result 186 | where 187 | S: Serializer, 188 | { 189 | serializer.serialize_str(&self.0) 190 | } 191 | } 192 | 193 | impl<'de> Deserialize<'de> for IdHex { 194 | fn deserialize(deserializer: D) -> Result 195 | where 196 | D: Deserializer<'de>, 197 | { 198 | deserializer.deserialize_str(IdHexVisitor) 199 | } 200 | } 201 | 202 | struct IdHexVisitor; 203 | 204 | impl Visitor<'_> for IdHexVisitor { 205 | type Value = IdHex; 206 | 207 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 208 | write!(f, "a lowercase hexadecimal string representing 32 bytes") 209 | } 210 | 211 | fn visit_str(self, v: &str) -> Result 212 | where 213 | E: serde::de::Error, 214 | { 215 | if v.len() != 64 { 216 | return Err(serde::de::Error::custom("IdHex is not 64 characters long")); 217 | } 218 | 219 | let vec: Vec = hex::decode(v).map_err(|e| serde::de::Error::custom(format!("{e}")))?; 220 | if vec.len() != 32 { 221 | return Err(serde::de::Error::custom("Invalid IdHex")); 222 | } 223 | 224 | Ok(IdHex(v.to_owned())) 225 | } 226 | } 227 | 228 | #[cfg(test)] 229 | mod test { 230 | use super::*; 231 | 232 | test_serde! {Id, test_id_serde} 233 | test_serde! {IdHex, test_id_hex_serde} 234 | 235 | #[test] 236 | fn test_id_bech32() { 237 | let bech32 = Id::mock().as_bech32_string(); 238 | println!("{bech32}"); 239 | assert_eq!(Id::mock(), Id::try_from_bech32_string(&bech32).unwrap()); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/types/key_signer.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | ContentEncryptionAlgorithm, EncryptedPrivateKey, Error, Event, ExportableSigner, Id, 3 | KeySecurity, LockableSigner, PreEvent, PrivateKey, PublicKey, Signature, Signer, SignerExt, 4 | }; 5 | use async_trait::async_trait; 6 | use serde::de::Error as DeError; 7 | use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor}; 8 | use serde::ser::{Serialize, SerializeSeq, Serializer}; 9 | use std::fmt; 10 | use std::sync::RwLock; 11 | 12 | /// Signer with a local private key (and public key) 13 | pub struct KeySigner { 14 | public_key: PublicKey, 15 | encrypted_private_key: RwLock, 16 | private_key: RwLock>, 17 | } 18 | 19 | impl fmt::Debug for KeySigner { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 21 | f.debug_struct("KeySigner") 22 | .field( 23 | "encrypted_private_key", 24 | &*self.encrypted_private_key.read().unwrap(), 25 | ) 26 | .field("public_key", &self.public_key) 27 | .finish() 28 | } 29 | } 30 | 31 | impl KeySigner { 32 | /// Create a Signer from an `EncryptedPrivateKey` 33 | pub fn from_locked_parts(epk: EncryptedPrivateKey, pk: PublicKey) -> Self { 34 | Self { 35 | public_key: pk, 36 | encrypted_private_key: RwLock::new(epk), 37 | private_key: RwLock::new(None), 38 | } 39 | } 40 | 41 | /// Create a Signer from a `PrivateKey` 42 | pub fn from_private_key(privk: PrivateKey, password: &str, log_n: u8) -> Result { 43 | let epk = privk.export_encrypted(password, log_n)?; 44 | Ok(Self { 45 | public_key: privk.public_key(), 46 | encrypted_private_key: RwLock::new(epk), 47 | private_key: RwLock::new(Some(privk)), 48 | }) 49 | } 50 | 51 | /// Create a Signer from an `EncryptedPrivateKey` and a password to unlock it 52 | pub fn from_encrypted_private_key(epk: EncryptedPrivateKey, pass: &str) -> Result { 53 | let priv_key = epk.decrypt(pass)?; 54 | let pub_key = priv_key.public_key(); 55 | Ok(Self { 56 | encrypted_private_key: RwLock::new(epk), 57 | public_key: pub_key, 58 | private_key: RwLock::new(Some(priv_key)), 59 | }) 60 | } 61 | 62 | /// Create a Signer by generating a new `PrivateKey` 63 | pub fn generate(password: &str, log_n: u8) -> Result { 64 | let privk = PrivateKey::generate(); 65 | let epk = privk.export_encrypted(password, log_n)?; 66 | Ok(Self { 67 | public_key: privk.public_key(), 68 | encrypted_private_key: RwLock::new(epk), 69 | private_key: RwLock::new(Some(privk)), 70 | }) 71 | } 72 | } 73 | 74 | #[async_trait] 75 | impl Signer for KeySigner { 76 | fn public_key(&self) -> PublicKey { 77 | self.public_key 78 | } 79 | 80 | fn encrypted_private_key(&self) -> Option { 81 | Some(self.encrypted_private_key.read().unwrap().clone()) 82 | } 83 | 84 | async fn sign_event(&self, input: PreEvent) -> Result { 85 | // Verify the pubkey matches 86 | if input.pubkey != self.public_key() { 87 | return Err(Error::InvalidPrivateKey); 88 | } 89 | 90 | // Generate Id 91 | let id = input.hash()?; 92 | 93 | // Generate Signature 94 | let signature = self.sign_id(id).await?; 95 | 96 | Ok(Event { 97 | id, 98 | pubkey: input.pubkey, 99 | created_at: input.created_at, 100 | kind: input.kind, 101 | tags: input.tags, 102 | content: input.content, 103 | sig: signature, 104 | }) 105 | } 106 | 107 | async fn encrypt( 108 | &self, 109 | other: &PublicKey, 110 | plaintext: &str, 111 | algo: ContentEncryptionAlgorithm, 112 | ) -> Result { 113 | match &*self.private_key.read().unwrap() { 114 | Some(pk) => pk.encrypt(other, plaintext, algo), 115 | None => Err(Error::SignerIsLocked), 116 | } 117 | } 118 | 119 | async fn decrypt(&self, other: &PublicKey, ciphertext: &str) -> Result { 120 | match &*self.private_key.read().unwrap() { 121 | Some(pk) => pk.decrypt(other, ciphertext), 122 | None => Err(Error::SignerIsLocked), 123 | } 124 | } 125 | 126 | fn key_security(&self) -> Result { 127 | match &*self.private_key.read().unwrap() { 128 | Some(pk) => Ok(pk.key_security()), 129 | None => Err(Error::SignerIsLocked), 130 | } 131 | } 132 | } 133 | 134 | #[async_trait] 135 | impl SignerExt for KeySigner { 136 | async fn sign_id(&self, id: Id) -> Result { 137 | let Some(pk) = self.private_key.read().unwrap().clone() else { 138 | return Err(Error::SignerIsLocked); 139 | }; 140 | pk.sign_id(id).await 141 | } 142 | 143 | async fn sign(&self, message: &[u8]) -> Result { 144 | let Some(pk) = self.private_key.read().unwrap().clone() else { 145 | return Err(Error::SignerIsLocked); 146 | }; 147 | pk.sign(message).await 148 | } 149 | 150 | async fn nip44_conversation_key(&self, other: &PublicKey) -> Result<[u8; 32], Error> { 151 | let xpub = other.as_xonly_public_key(); 152 | match &*self.private_key.read().unwrap() { 153 | Some(pk) => Ok(nip44::get_conversation_key(pk.as_secret_key(), xpub)), 154 | None => Err(Error::SignerIsLocked), 155 | } 156 | } 157 | } 158 | 159 | impl LockableSigner for KeySigner { 160 | fn is_locked(&self) -> bool { 161 | self.private_key.read().unwrap().is_none() 162 | } 163 | 164 | fn unlock(&self, password: &str) -> Result<(), Error> { 165 | if !self.is_locked() { 166 | return Ok(()); 167 | } 168 | 169 | let private_key = self 170 | .encrypted_private_key 171 | .read() 172 | .unwrap() 173 | .decrypt(password)?; 174 | 175 | *self.private_key.write().unwrap() = Some(private_key); 176 | 177 | Ok(()) 178 | } 179 | 180 | fn lock(&self) { 181 | *self.private_key.write().unwrap() = None; 182 | } 183 | 184 | fn change_passphrase(&self, old: &str, new: &str, log_n: u8) -> Result<(), Error> { 185 | let private_key = self.encrypted_private_key.read().unwrap().decrypt(old)?; 186 | *self.encrypted_private_key.write().unwrap() = private_key.export_encrypted(new, log_n)?; 187 | *self.private_key.write().unwrap() = Some(private_key); 188 | Ok(()) 189 | } 190 | 191 | fn upgrade(&self, pass: &str, log_n: u8) -> Result<(), Error> { 192 | let private_key = self.encrypted_private_key.read().unwrap().decrypt(pass)?; 193 | *self.encrypted_private_key.write().unwrap() = private_key.export_encrypted(pass, log_n)?; 194 | Ok(()) 195 | } 196 | } 197 | 198 | #[async_trait] 199 | impl ExportableSigner for KeySigner { 200 | async fn export_private_key_in_hex( 201 | &self, 202 | pass: &str, 203 | log_n: u8, 204 | ) -> Result<(String, bool), Error> { 205 | if let Some(ref mut pk) = *self.private_key.write().unwrap() { 206 | // Test password and check key security 207 | let pkcheck = self.encrypted_private_key.read().unwrap().decrypt(pass)?; 208 | 209 | // side effect: this may downgrade the key security of self.private_key 210 | let output = pk.as_hex_string(); 211 | 212 | // If key security changed, re-export 213 | let mut downgraded = false; 214 | if pk.key_security() != pkcheck.key_security() { 215 | downgraded = true; 216 | *self.encrypted_private_key.write().unwrap() = pk.export_encrypted(pass, log_n)?; 217 | } 218 | Ok((output, downgraded)) 219 | } else { 220 | Err(Error::SignerIsLocked) 221 | } 222 | } 223 | 224 | async fn export_private_key_in_bech32( 225 | &self, 226 | pass: &str, 227 | log_n: u8, 228 | ) -> Result<(String, bool), Error> { 229 | if let Some(ref mut pk) = *self.private_key.write().unwrap() { 230 | // Test password and check key security 231 | let pkcheck = self.encrypted_private_key.read().unwrap().decrypt(pass)?; 232 | 233 | // side effect: this may downgrade the key security of self.private_key 234 | let output = pk.as_bech32_string(); 235 | 236 | // If key security changed, re-export 237 | let mut downgraded = false; 238 | if pk.key_security() != pkcheck.key_security() { 239 | downgraded = true; 240 | *self.encrypted_private_key.write().unwrap() = pk.export_encrypted(pass, log_n)?; 241 | } 242 | 243 | Ok((output, downgraded)) 244 | } else { 245 | Err(Error::SignerIsLocked) 246 | } 247 | } 248 | } 249 | 250 | impl Serialize for KeySigner { 251 | fn serialize(&self, serializer: S) -> Result 252 | where 253 | S: Serializer, 254 | { 255 | let mut seq = serializer.serialize_seq(Some(2))?; 256 | seq.serialize_element(&self.public_key)?; 257 | seq.serialize_element(&*self.encrypted_private_key.read().unwrap())?; 258 | seq.end() 259 | } 260 | } 261 | 262 | impl<'de> Deserialize<'de> for KeySigner { 263 | fn deserialize(deserializer: D) -> Result 264 | where 265 | D: Deserializer<'de>, 266 | { 267 | deserializer.deserialize_seq(KeySignerVisitor) 268 | } 269 | } 270 | 271 | struct KeySignerVisitor; 272 | 273 | impl<'de> Visitor<'de> for KeySignerVisitor { 274 | type Value = KeySigner; 275 | 276 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 277 | write!(f, "a key signer structure as a sequence") 278 | } 279 | 280 | fn visit_seq(self, mut access: A) -> Result 281 | where 282 | A: SeqAccess<'de>, 283 | { 284 | let public_key = access 285 | .next_element::()? 286 | .ok_or_else(|| DeError::custom("Missing or invalid pubkey"))?; 287 | let epk = access 288 | .next_element::()? 289 | .ok_or_else(|| DeError::custom("Missing or invalid epk"))?; 290 | 291 | Ok(KeySigner { 292 | public_key, 293 | encrypted_private_key: RwLock::new(epk), 294 | private_key: RwLock::new(None), 295 | }) 296 | } 297 | } 298 | 299 | #[cfg(test)] 300 | mod test { 301 | use super::*; 302 | 303 | #[test] 304 | fn test_key_signer_serde() { 305 | let ks = KeySigner::generate("password", 16).unwrap(); 306 | let s = serde_json::to_string(&ks).unwrap(); 307 | println!("{s}"); 308 | let ks2: KeySigner = serde_json::from_str(&s).unwrap(); 309 | assert_eq!(ks.public_key, ks2.public_key); 310 | assert_eq!( 311 | *ks.encrypted_private_key.read().unwrap(), 312 | *ks2.encrypted_private_key.read().unwrap() 313 | ); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/types/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::versioned::metadata1::MetadataV1; 2 | 3 | /// Metadata about a user 4 | /// 5 | /// Note: the value is an Option because some real-world data has been found to 6 | /// contain JSON nulls as values, and we don't want deserialization of those 7 | /// events to fail. We treat these in our get() function the same as if the key 8 | /// did not exist. 9 | pub type Metadata = MetadataV1; 10 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | mod client_message; 2 | pub use client_message::ClientMessage; 3 | 4 | mod content; 5 | pub use content::{ContentSegment, ShatteredContent, Span}; 6 | 7 | mod delegation; 8 | pub use delegation::{DelegationConditions, EventDelegation}; 9 | 10 | mod event; 11 | pub use event::{Event, PreEvent, Rumor, ZapData}; 12 | 13 | mod event_kind; 14 | pub use event_kind::{EventKind, EventKindIterator, EventKindOrRange}; 15 | 16 | mod event_reference; 17 | pub use event_reference::EventReference; 18 | 19 | mod file_metadata; 20 | pub use file_metadata::FileMetadata; 21 | 22 | mod filter; 23 | pub use filter::Filter; 24 | 25 | mod id; 26 | pub use id::{Id, IdHex}; 27 | 28 | mod identity; 29 | pub use identity::Identity; 30 | 31 | mod key_signer; 32 | pub use key_signer::KeySigner; 33 | 34 | mod hll8; 35 | pub use hll8::Hll8; 36 | 37 | mod metadata; 38 | pub use metadata::Metadata; 39 | 40 | mod naddr; 41 | pub use naddr::NAddr; 42 | 43 | mod nevent; 44 | pub use nevent::NEvent; 45 | 46 | mod nip05; 47 | pub use nip05::Nip05; 48 | 49 | mod nostr_url; 50 | pub use nostr_url::{find_nostr_bech32_pos, find_nostr_url_pos, NostrBech32, NostrUrl}; 51 | 52 | mod pay_request_data; 53 | pub use pay_request_data::PayRequestData; 54 | 55 | mod private_key; 56 | pub use private_key::{ContentEncryptionAlgorithm, EncryptedPrivateKey, KeySecurity, PrivateKey}; 57 | 58 | mod profile; 59 | pub use profile::Profile; 60 | 61 | mod public_key; 62 | pub use public_key::{PublicKey, PublicKeyHex, XOnlyPublicKey}; 63 | 64 | mod relay_information_document; 65 | pub use relay_information_document::{ 66 | Fee, RelayFees, RelayInformationDocument, RelayLimitation, RelayRetention, 67 | }; 68 | 69 | mod relay_list; 70 | pub use relay_list::{RelayList, RelayListUsage}; 71 | 72 | mod relay_message; 73 | pub use relay_message::{CountResult, RelayMessage, Why}; 74 | 75 | mod relay_usage; 76 | pub use relay_usage::{RelayUsage, RelayUsageSet}; 77 | 78 | mod satoshi; 79 | pub use satoshi::MilliSatoshi; 80 | 81 | mod signature; 82 | pub use signature::{Signature, SignatureHex}; 83 | 84 | mod signer; 85 | pub use signer::{ExportableSigner, LockableSigner, MutExportableSigner, Signer, SignerExt}; 86 | 87 | mod simple_relay_list; 88 | pub use simple_relay_list::{SimpleRelayList, SimpleRelayUsage}; 89 | 90 | mod subscription_id; 91 | pub use subscription_id::SubscriptionId; 92 | 93 | mod tag; 94 | pub use tag::{ParsedTag, Tag}; 95 | 96 | mod unixtime; 97 | pub use unixtime::Unixtime; 98 | 99 | mod url; 100 | pub use self::url::{RelayOrigin, RelayUrl, UncheckedUrl, Url}; 101 | 102 | #[cfg(test)] 103 | mod test { 104 | use crate::*; 105 | 106 | #[test] 107 | fn test_real_messages() { 108 | let wire = r#"["EVENT","j5happy-fiatjaf",{"id":"75468b04a0e03633a40f1c8d7e1a0cad1363ecc514ecbcde22093874e04e8166","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1668011201,"kind":1,"tags":[["e","247baa8ed5db8097b16d9594a3a27fd2b64c030fa9e68ce7d6106df4a499700d","","reply"],["p","6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964","","reply"]],"content":"you're not allowed to pronounce these words, traitor","sig":"588577ccd5ad6be8f61d93e4738799dede9b169ad150ee3ee6a1c4bb80adfbee27bb4e302e0ea173637c189d6664f1dc82ad3590b5524240bf492fa0b754432c"}]"#; 109 | let message: RelayMessage = serde_json::from_str(wire).unwrap(); 110 | match message { 111 | RelayMessage::Event(_subid, event) => { 112 | event.verify(None).unwrap(); 113 | println!("{}", event.content); 114 | } 115 | _ => panic!("Wrong message type"), 116 | } 117 | 118 | let wire = r#"["EVENT","j5happy-fiatjaf",{"id":"267660849149c7226a4a4f7c75f359f3995965c05d25451f13c907bf0b158178","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1668011264,"kind":1,"tags":[["e","8a128cd11c6a56554b8201635a19c97258504060464cec4f3e5f0500814339cf","","reply"],["p","000000000652e452ee68a01187fb08c899496cb46cb51d1aa0803d063acedba7","","reply"]],"content":"this is quite nice, specially the part where you say it was written in Rust.","sig":"1c49b4f4d2b86077ae4c1f7f8dc212d6c040dfdff7864eac2154fe7df1baceb162cf658d78634b803b964f920aeb861014ed30df113ed0857aaf1854e3c572a3"}]"#; 119 | let message: RelayMessage = serde_json::from_str(wire).unwrap(); 120 | match message { 121 | RelayMessage::Event(_subid, event) => { 122 | event.verify(None).unwrap(); 123 | println!("{}", event.as_ref().content); 124 | } 125 | _ => panic!("Wrong message type"), 126 | } 127 | 128 | let wire = r#"["EVENT","j5happy-fiatjaf",{"id":"fe0cfc6d2be988f46f849535518c3e43a509ea8a016ccd8b83a3ffd79575fd33","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1668011340,"kind":1,"tags":[["e","b1a2a2e55f1b6f1f6756e6e4c1c4ecbce0123ede048423413228134143fd84ac","","root"],["e","c758d9d467bf925923f57bb6b47db870fad50ba9629bc086f573f3d4ff278c84","","reply"],["p","9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437","","root"],["p","32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245","","reply"]],"content":"they are definitely annoying in Go, but we already have them anyway because of the `[\"EVENT\", {}]` message so this doesn't make any difference in my case at least.","sig":"23b1eed3087a72f2e940c1c95541b22b3434390926780ed055abf5dd77a3aa16e1c5c3965382ec7343c0da3ece31e05945f910d684f3196e81e05765a5b1e631"}]"#; 129 | let message: RelayMessage = serde_json::from_str(wire).unwrap(); 130 | match message { 131 | RelayMessage::Event(_subid, event) => { 132 | event.verify(None).unwrap(); 133 | println!("{}", event.content); 134 | } 135 | _ => panic!("Wrong message type"), 136 | } 137 | 138 | let wire = r#"["EVENT","j5happy-fiatjaf",{"id":"adf038ca047260a20f70b7863c3a8ef7afdac455cd9fcb785950b86ebb104911","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1668011516,"kind":1,"tags":[["e","c0138298e2ac89078e206aea1e16f1d9a37257c8400f48aba781dd890bc9f35b","","root"],["e","24b757dfc938d9d29d7be40ac91424bfecd8c0016929ac911447a2f785519d97","","reply"],["p","3235036bd0957dfb27ccda02d452d7c763be40c91a1ac082ba6983b25238388c","","root"],["p","46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d","","reply"]],"content":"when I started writing branle a million years ago I thought it would be so much simpler too, I guess that explains why twitter has 800 developers on its payroll","sig":"0f7d1cfbcc38bb861f51538cb8e4a5268e2bdca13969eaba8d0993e19fa8469d9ebcc60081523d075ca63c7ab55270e2a3de2373db605cde081b82357907af1f"}]"#; 139 | let message: RelayMessage = serde_json::from_str(wire).unwrap(); 140 | match message { 141 | RelayMessage::Event(_subid, event) => { 142 | event.verify(None).unwrap(); 143 | println!("{}", event.content); 144 | } 145 | _ => panic!("Wrong message type"), 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/types/naddr.rs: -------------------------------------------------------------------------------- 1 | use super::{EventKind, PublicKey, UncheckedUrl}; 2 | use crate::Error; 3 | use serde::{Deserialize, Serialize}; 4 | #[cfg(feature = "speedy")] 5 | use speedy::{Readable, Writable}; 6 | use std::hash::{Hash, Hasher}; 7 | 8 | /// An 'naddr': data to address a possibly parameterized replaceable event (d-tag, kind, author, and relays) 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 11 | pub struct NAddr { 12 | /// the 'd' tag of the Event, or an empty string if the kind is not parameterized 13 | pub d: String, 14 | 15 | /// Some of the relays where this could be found 16 | pub relays: Vec, 17 | 18 | /// Kind 19 | pub kind: EventKind, 20 | 21 | /// Author 22 | pub author: PublicKey, 23 | } 24 | 25 | impl NAddr { 26 | /// Export as a bech32 encoded string ("naddr") 27 | pub fn as_bech32_string(&self) -> String { 28 | // Compose 29 | let mut tlv: Vec = Vec::new(); 30 | 31 | // Push d tag 32 | tlv.push(0); // the special value, in this case the 'd' tag 33 | let len = self.d.len() as u8; 34 | tlv.push(len); // the length of the d tag 35 | tlv.extend(&self.d.as_bytes()[..len as usize]); 36 | 37 | // Push relays 38 | for relay in &self.relays { 39 | tlv.push(1); // type 'relay' 40 | let len = relay.0.len() as u8; 41 | tlv.push(len); // the length of the string 42 | tlv.extend(&relay.0.as_bytes()[..len as usize]); 43 | } 44 | 45 | // Push kind 46 | let kindnum: u32 = From::from(self.kind); 47 | let bytes = kindnum.to_be_bytes(); 48 | tlv.push(3); // type 'kind' 49 | tlv.push(bytes.len() as u8); // '4' 50 | tlv.extend(bytes); 51 | 52 | // Push author 53 | tlv.push(2); // type 'author' 54 | tlv.push(32); // the length of the value (always 32 for public key) 55 | tlv.extend(self.author.as_bytes()); 56 | 57 | bech32::encode::(*crate::HRP_NADDR, &tlv).unwrap() 58 | } 59 | 60 | /// Import from a bech32 encoded string ("naddr") 61 | pub fn try_from_bech32_string(s: &str) -> Result { 62 | let data = bech32::decode(s)?; 63 | if data.0 != *crate::HRP_NADDR { 64 | Err(Error::WrongBech32( 65 | crate::HRP_NADDR.to_lowercase(), 66 | data.0.to_lowercase(), 67 | )) 68 | } else { 69 | let mut maybe_d: Option = None; 70 | let mut relays: Vec = Vec::new(); 71 | let mut maybe_kind: Option = None; 72 | let mut maybe_author: Option = None; 73 | 74 | let tlv = data.1; 75 | let mut pos = 0; 76 | loop { 77 | // we need at least 2 more characters for anything meaningful 78 | if pos > tlv.len() - 2 { 79 | break; 80 | } 81 | let ty = tlv[pos]; 82 | let len = tlv[pos + 1] as usize; 83 | pos += 2; 84 | if pos + len > tlv.len() { 85 | return Err(Error::InvalidProfile); 86 | } 87 | let raw = &tlv[pos..pos + len]; 88 | match ty { 89 | 0 => { 90 | // special (bytes of d tag) 91 | maybe_d = Some(std::str::from_utf8(raw)?.to_string()); 92 | } 93 | 1 => { 94 | // relay 95 | let relay_str = std::str::from_utf8(raw)?; 96 | let relay = UncheckedUrl::from_str(relay_str); 97 | relays.push(relay); 98 | } 99 | 2 => { 100 | // author 101 | // 102 | // Don't fail if the pubkey is bad, just don't include it. 103 | // Some client is generating these, and we want to tolerate it 104 | // as much as we can. 105 | if let Ok(pk) = PublicKey::from_bytes(raw, true) { 106 | maybe_author = Some(pk); 107 | } 108 | } 109 | 3 => { 110 | // kind 111 | let kindnum = u32::from_be_bytes( 112 | raw.try_into().map_err(|_| Error::WrongLengthKindBytes)?, 113 | ); 114 | maybe_kind = Some(kindnum.into()); 115 | } 116 | _ => {} // unhandled type for nprofile 117 | } 118 | pos += len; 119 | } 120 | 121 | match (maybe_d, maybe_kind, maybe_author) { 122 | (Some(d), Some(kind), Some(author)) => { 123 | if !kind.is_replaceable() { 124 | Err(Error::NonReplaceableAddr) 125 | } else { 126 | Ok(NAddr { 127 | d, 128 | relays, 129 | kind, 130 | author, 131 | }) 132 | } 133 | } 134 | _ => Err(Error::InvalidNAddr), 135 | } 136 | } 137 | } 138 | 139 | // Mock data for testing 140 | #[allow(dead_code)] 141 | pub(crate) fn mock() -> NAddr { 142 | let d = "Test D Indentifier 1lkjf23".to_string(); 143 | 144 | NAddr { 145 | d, 146 | relays: vec![ 147 | UncheckedUrl::from_str("wss://relay.example.com"), 148 | UncheckedUrl::from_str("wss://relay2.example.com"), 149 | ], 150 | kind: EventKind::LongFormContent, 151 | author: PublicKey::mock_deterministic(), 152 | } 153 | } 154 | } 155 | 156 | impl PartialEq for NAddr { 157 | fn eq(&self, other: &Self) -> bool { 158 | self.d == other.d && self.kind == other.kind && self.author == other.author 159 | // We do not compare the relays field! 160 | } 161 | } 162 | 163 | impl Eq for NAddr {} 164 | 165 | impl Hash for NAddr { 166 | fn hash(&self, state: &mut H) { 167 | self.d.hash(state); 168 | self.kind.hash(state); 169 | self.author.hash(state); 170 | // We do not hash relays field! 171 | } 172 | } 173 | 174 | #[cfg(test)] 175 | mod test { 176 | use super::*; 177 | 178 | test_serde! {NAddr, test_naddr_serde} 179 | 180 | #[test] 181 | fn test_profile_bech32() { 182 | let bech32 = NAddr::mock().as_bech32_string(); 183 | println!("{bech32}"); 184 | assert_eq!( 185 | NAddr::mock(), 186 | NAddr::try_from_bech32_string(&bech32).unwrap() 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/types/nevent.rs: -------------------------------------------------------------------------------- 1 | use super::{EventKind, Id, PublicKey, UncheckedUrl}; 2 | use crate::Error; 3 | use serde::{Deserialize, Serialize}; 4 | #[cfg(feature = "speedy")] 5 | use speedy::{Readable, Writable}; 6 | 7 | /// An 'nevent': event id along with some relays in which that event may be found. 8 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 9 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 10 | pub struct NEvent { 11 | /// Event id 12 | pub id: Id, 13 | 14 | /// Some of the relays where this could be in 15 | pub relays: Vec, 16 | 17 | /// Kind (optional) 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | #[serde(default)] 20 | pub kind: Option, 21 | 22 | /// Author (optional) 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | #[serde(default)] 25 | pub author: Option, 26 | } 27 | 28 | impl NEvent { 29 | /// Export as a bech32 encoded string ("nevent") 30 | pub fn as_bech32_string(&self) -> String { 31 | // Compose 32 | let mut tlv: Vec = Vec::new(); 33 | 34 | // Push Id 35 | tlv.push(0); // the special value, in this case the id 36 | tlv.push(32); // the length of the value (always 32 for id) 37 | tlv.extend(self.id.0); 38 | 39 | // Push relays 40 | for relay in &self.relays { 41 | tlv.push(1); // type 'relay' 42 | let len = relay.0.len() as u8; 43 | tlv.push(len); // the length of the string 44 | tlv.extend(&relay.0.as_bytes()[..len as usize]); 45 | } 46 | 47 | // Maybe Push kind 48 | if let Some(kind) = self.kind { 49 | let kindnum: u32 = From::from(kind); 50 | let bytes = kindnum.to_be_bytes(); 51 | tlv.push(3); // type 'kind' 52 | tlv.push(bytes.len() as u8); // '4' 53 | tlv.extend(bytes); 54 | } 55 | 56 | // Maybe Push author 57 | if let Some(pubkey) = self.author { 58 | tlv.push(2); // type 'author' 59 | tlv.push(32); // the length of the value (always 32 for public key) 60 | tlv.extend(pubkey.as_bytes()); 61 | } 62 | 63 | bech32::encode::(*crate::HRP_NEVENT, &tlv).unwrap() 64 | } 65 | 66 | /// Import from a bech32 encoded string ("nevent") 67 | pub fn try_from_bech32_string(s: &str) -> Result { 68 | let data = bech32::decode(s)?; 69 | if data.0 != *crate::HRP_NEVENT { 70 | Err(Error::WrongBech32( 71 | crate::HRP_NEVENT.to_lowercase(), 72 | data.0.to_lowercase(), 73 | )) 74 | } else { 75 | let mut relays: Vec = Vec::new(); 76 | let mut id: Option = None; 77 | let mut kind: Option = None; 78 | let mut author: Option = None; 79 | 80 | let tlv = data.1; 81 | let mut pos = 0; 82 | loop { 83 | // we need at least 2 more characters for anything meaningful 84 | if pos > tlv.len() - 2 { 85 | break; 86 | } 87 | let ty = tlv[pos]; 88 | let len = tlv[pos + 1] as usize; 89 | pos += 2; 90 | if pos + len > tlv.len() { 91 | return Err(Error::InvalidProfile); 92 | } 93 | let raw = &tlv[pos..pos + len]; 94 | match ty { 95 | 0 => { 96 | // special (32 bytes of id) 97 | if len != 32 { 98 | return Err(Error::InvalidNEvent); 99 | } 100 | id = Some(Id(raw 101 | .try_into() 102 | .map_err(|_| Error::WrongLengthHexString)?)); 103 | } 104 | 1 => { 105 | // relay 106 | let relay_str = std::str::from_utf8(raw)?; 107 | let relay = UncheckedUrl::from_str(relay_str); 108 | relays.push(relay); 109 | } 110 | 2 => { 111 | // author 112 | // 113 | // Don't fail if the pubkey is bad, just don't include it. 114 | // Some client is generating these, and we want to tolerate it 115 | // as much as we can. 116 | if let Ok(pk) = PublicKey::from_bytes(raw, true) { 117 | author = Some(pk); 118 | } 119 | } 120 | 3 => { 121 | // kind 122 | let kindnum = u32::from_be_bytes( 123 | raw.try_into().map_err(|_| Error::WrongLengthKindBytes)?, 124 | ); 125 | kind = Some(kindnum.into()); 126 | } 127 | _ => {} // unhandled type for nprofile 128 | } 129 | pos += len; 130 | } 131 | if let Some(id) = id { 132 | Ok(NEvent { 133 | id, 134 | relays, 135 | kind, 136 | author, 137 | }) 138 | } else { 139 | Err(Error::InvalidNEvent) 140 | } 141 | } 142 | } 143 | 144 | // Mock data for testing 145 | #[allow(dead_code)] 146 | pub(crate) fn mock() -> NEvent { 147 | let id = Id::try_from_hex_string( 148 | "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", 149 | ) 150 | .unwrap(); 151 | 152 | NEvent { 153 | id, 154 | relays: vec![ 155 | UncheckedUrl::from_str("wss://relay.example.com"), 156 | UncheckedUrl::from_str("wss://relay2.example.com"), 157 | ], 158 | kind: None, 159 | author: None, 160 | } 161 | } 162 | } 163 | 164 | #[cfg(test)] 165 | mod test { 166 | use super::*; 167 | 168 | test_serde! {NEvent, test_nevent_serde} 169 | 170 | #[test] 171 | fn test_profile_bech32() { 172 | let bech32 = NEvent::mock().as_bech32_string(); 173 | println!("{bech32}"); 174 | assert_eq!( 175 | NEvent::mock(), 176 | NEvent::try_from_bech32_string(&bech32).unwrap() 177 | ); 178 | } 179 | 180 | #[test] 181 | fn test_nip19_example() { 182 | let nevent = NEvent { 183 | id: Id::try_from_hex_string( 184 | "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", 185 | ) 186 | .unwrap(), 187 | relays: vec![ 188 | UncheckedUrl::from_str("wss://r.x.com"), 189 | UncheckedUrl::from_str("wss://djbas.sadkb.com"), 190 | ], 191 | kind: None, 192 | author: None, 193 | }; 194 | 195 | // As serialized by us (not necessarily in the order others would do it) 196 | let bech32 = "nevent1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaks343fay"; 197 | 198 | // Try converting profile to bech32 199 | assert_eq!(nevent.as_bech32_string(), bech32); 200 | 201 | // Try converting bech32 to profile 202 | assert_eq!(nevent, NEvent::try_from_bech32_string(bech32).unwrap()); 203 | 204 | // Try this one that used to fail 205 | let bech32 = 206 | "nevent1qqstxx3lk7zqfyn8cyyptvujfxq9w6mad4205x54772tdkmyqaay9scrqsqqqpp8x4vwhf"; 207 | let _ = NEvent::try_from_bech32_string(bech32).unwrap(); 208 | // it won't be equal, but should have the basics and should not error. 209 | } 210 | 211 | #[test] 212 | fn test_nevent_alt_fields() { 213 | let nevent = NEvent { 214 | id: Id::try_from_hex_string( 215 | "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", 216 | ) 217 | .unwrap(), 218 | relays: vec![ 219 | UncheckedUrl::from_str("wss://r.x.com"), 220 | UncheckedUrl::from_str("wss://djbas.sadkb.com"), 221 | ], 222 | kind: Some(EventKind::TextNote), 223 | author: Some( 224 | PublicKey::try_from_hex_string( 225 | "000000000332c7831d9c5a99f183afc2813a6f69a16edda7f6fc0ed8110566e6", 226 | true, 227 | ) 228 | .unwrap(), 229 | ), 230 | }; 231 | 232 | // As serialized by us (not necessarily in the order others would do it) 233 | let bech32 = "nevent1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksxpqqqqqqzq3qqqqqqqqrxtrcx8vut2vlrqa0c2qn5mmf59hdmflkls8dsyg9vmnqu25v0j"; 234 | 235 | // Try converting profile to bech32 236 | assert_eq!(nevent.as_bech32_string(), bech32); 237 | 238 | // Try converting bech32 to profile 239 | assert_eq!(nevent, NEvent::try_from_bech32_string(bech32).unwrap()); 240 | } 241 | 242 | #[test] 243 | fn test_ones_that_were_failing() { 244 | let bech32 = "nevent1qqswrqr63ddwk8l3zfqrgdxh2lxh2jlcxl36k3h33g25gtchzchx8agpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3yamnwvaz7tm0venxx6rpd9hzuur4vgpyqdmyxs6rzdmyx4jxvdpnx4snjdmz8pnr2dtr8pnryefhv5ex2e34xvek2v3nxuckxef4v5ckxenxvs6njdtrxymnjcfnv4skvvekvs6qfe99uy"; 245 | 246 | let _ne = NEvent::try_from_bech32_string(bech32).unwrap(); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/types/nip05.rs: -------------------------------------------------------------------------------- 1 | use crate::versioned::nip05::Nip05V1; 2 | 3 | /// The content of a webserver's /.well-known/nostr.json file used in NIP-05 and NIP-35 4 | /// This allows lookup and verification of a nostr user via a `user@domain` style identifier. 5 | pub type Nip05 = Nip05V1; 6 | -------------------------------------------------------------------------------- /src/types/pay_request_data.rs: -------------------------------------------------------------------------------- 1 | use super::{PublicKeyHex, UncheckedUrl}; 2 | use serde::de::Error as DeError; 3 | use serde::de::{Deserialize, Deserializer, MapAccess, Visitor}; 4 | use serde::ser::{Serialize, SerializeMap, Serializer}; 5 | use serde_json::{json, Map, Value}; 6 | use std::fmt; 7 | 8 | /// This is a response from a zapper lnurl 9 | #[derive(Clone, Debug, Eq, PartialEq)] 10 | pub struct PayRequestData { 11 | /// The URL to make the pay request to with a kind 9374 event 12 | pub callback: UncheckedUrl, 13 | 14 | /// Metadata 15 | pub metadata: Vec<(String, String)>, 16 | 17 | /// Whether the lnurl supports nostr zaps 18 | pub allows_nostr: Option, 19 | 20 | /// The nostr public key of the zapper 21 | pub nostr_pubkey: Option, 22 | 23 | /// Other fields such as: 24 | /// 25 | /// "maxSendable": 100000000000, 26 | /// "minSendable": 1000, 27 | /// "commentAllowed": 32 28 | /// "tag": "payRequest" 29 | pub other: Map, 30 | } 31 | 32 | impl Default for PayRequestData { 33 | fn default() -> Self { 34 | PayRequestData { 35 | callback: UncheckedUrl("".to_owned()), 36 | metadata: vec![], 37 | allows_nostr: None, 38 | nostr_pubkey: None, 39 | other: Map::new(), 40 | } 41 | } 42 | } 43 | 44 | impl PayRequestData { 45 | #[allow(dead_code)] 46 | pub(crate) fn mock() -> PayRequestData { 47 | let mut map = Map::new(); 48 | let _ = map.insert("tag".to_string(), Value::String("payRequest".to_owned())); 49 | let _ = map.insert( 50 | "maxSendable".to_string(), 51 | Value::Number(100000000000_u64.into()), 52 | ); 53 | let _ = map.insert("minSendable".to_string(), Value::Number(1000.into())); 54 | let _ = map.insert("commentAllowed".to_string(), Value::Number(32.into())); 55 | PayRequestData { 56 | callback: UncheckedUrl("https://livingroomofsatoshi.com/api/v1/lnurl/payreq/f16bacaa-8e5f-4038-bdea-4c9e796f913c".to_string()), 57 | metadata: vec![ 58 | ("text/plain".to_owned(), 59 | "Pay to Wallet of Satoshi user: decentbun13".to_owned()), 60 | ("text/identifier".to_owned(), 61 | "decentbun13@walletofsatoshi.com".to_owned()), 62 | ], 63 | allows_nostr: Some(true), 64 | nostr_pubkey: Some(PublicKeyHex::try_from_str("be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479").unwrap()), 65 | other: map, 66 | } 67 | } 68 | } 69 | 70 | impl Serialize for PayRequestData { 71 | fn serialize(&self, serializer: S) -> Result 72 | where 73 | S: Serializer, 74 | { 75 | let mut map = serializer.serialize_map(Some(4 + self.other.len()))?; 76 | map.serialize_entry("callback", &json!(&self.callback))?; 77 | map.serialize_entry("metadata", &json!(&self.metadata))?; 78 | map.serialize_entry("allowsNostr", &json!(&self.allows_nostr))?; 79 | map.serialize_entry("nostrPubkey", &json!(&self.nostr_pubkey))?; 80 | for (k, v) in &self.other { 81 | map.serialize_entry(&k, &v)?; 82 | } 83 | map.end() 84 | } 85 | } 86 | 87 | impl<'de> Deserialize<'de> for PayRequestData { 88 | fn deserialize(deserializer: D) -> Result 89 | where 90 | D: Deserializer<'de>, 91 | { 92 | deserializer.deserialize_map(PayRequestDataVisitor) 93 | } 94 | } 95 | 96 | struct PayRequestDataVisitor; 97 | 98 | impl<'de> Visitor<'de> for PayRequestDataVisitor { 99 | type Value = PayRequestData; 100 | 101 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 102 | write!(f, "A JSON object") 103 | } 104 | 105 | fn visit_map(self, mut access: M) -> Result 106 | where 107 | M: MapAccess<'de>, 108 | { 109 | let mut map: Map = Map::new(); 110 | while let Some((key, value)) = access.next_entry::()? { 111 | let _ = map.insert(key, value); 112 | } 113 | 114 | let mut m: PayRequestData = Default::default(); 115 | 116 | if let Some(Value::String(s)) = map.remove("callback") { 117 | m.callback = UncheckedUrl(s) 118 | } else { 119 | return Err(DeError::custom("Missing callback url".to_owned())); 120 | } 121 | 122 | if let Some(Value::Array(a)) = map.remove("metadata") { 123 | for elem in a.iter() { 124 | if let Value::Array(a2) = elem { 125 | if a2.len() == 2 { 126 | if let Value::String(key) = &a2[0] { 127 | if let Value::String(val) = &a2[1] { 128 | m.metadata.push((key.to_owned(), val.to_owned())); 129 | } 130 | } 131 | } else { 132 | return Err(DeError::custom("Metadata entry not a pair".to_owned())); 133 | } 134 | } else { 135 | return Err(DeError::custom("Metadata entry not recognized".to_owned())); 136 | } 137 | } 138 | } 139 | 140 | if let Some(Value::Bool(b)) = map.remove("allowsNostr") { 141 | m.allows_nostr = Some(b); 142 | } else { 143 | m.allows_nostr = None; 144 | } 145 | 146 | if let Some(Value::String(s)) = map.remove("nostrPubkey") { 147 | m.nostr_pubkey = match PublicKeyHex::try_from_string(s) { 148 | Ok(pkh) => Some(pkh), 149 | Err(e) => return Err(DeError::custom(format!("{e}"))), 150 | }; 151 | } 152 | 153 | m.other = map; 154 | 155 | Ok(m) 156 | } 157 | } 158 | 159 | #[cfg(test)] 160 | mod test { 161 | use super::*; 162 | 163 | test_serde! {PayRequestData, test_pay_request_data_serde} 164 | } 165 | -------------------------------------------------------------------------------- /src/types/private_key/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Error, Event, Id, MutExportableSigner, PreEvent, PublicKey, Signature, Signer, SignerExt, 3 | }; 4 | use async_trait::async_trait; 5 | use rand_core::OsRng; 6 | use std::convert::TryFrom; 7 | use std::fmt; 8 | 9 | mod encrypted_private_key; 10 | pub use encrypted_private_key::*; 11 | 12 | mod content_encryption; 13 | pub use content_encryption::*; 14 | 15 | /// This indicates the security of the key by keeping track of whether the 16 | /// secret key material was handled carefully. If the secret is exposed in any 17 | /// way, or leaked and the memory not zeroed, the key security drops to Weak. 18 | /// 19 | /// This is a Best Effort tag. There are ways to leak the key and still have this 20 | /// tag claim the key is Medium security. So Medium really means it might not 21 | /// have leaked, whereas Weak means we know that it definately did leak. 22 | /// 23 | /// We offer no Strong security via the PrivateKey structure. If we support 24 | /// hardware tokens in the future, it will probably be via a different structure. 25 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 26 | #[repr(u8)] 27 | pub enum KeySecurity { 28 | /// This means that the key was exposed in a way such that this library 29 | /// cannot ensure it's secrecy, usually either by being exported as a hex string, 30 | /// or by being imported from the same. Often in these cases it is displayed 31 | /// on the screen or left in the cut buffer or in freed memory that was not 32 | /// subsequently zeroed. 33 | Weak = 0, 34 | 35 | /// This means that the key might not have been directly exposed. But it still 36 | /// might have as there are numerous ways you can leak it such as exporting it 37 | /// and then decrypting the exported key, using unsafe rust, transmuting it into 38 | /// a different type that doesn't protect it, or using a privileged process to 39 | /// scan memory. Additionally, more advanced techniques can get at your key such 40 | /// as hardware attacks like spectre, rowhammer, and power analysis. 41 | Medium = 1, 42 | 43 | /// Not tracked 44 | NotTracked = 2, 45 | } 46 | 47 | impl TryFrom for KeySecurity { 48 | type Error = Error; 49 | 50 | fn try_from(i: u8) -> Result { 51 | if i == 0 { 52 | Ok(KeySecurity::Weak) 53 | } else if i == 1 { 54 | Ok(KeySecurity::Medium) 55 | } else if i == 2 { 56 | Ok(KeySecurity::NotTracked) 57 | } else { 58 | Err(Error::UnknownKeySecurity(i)) 59 | } 60 | } 61 | } 62 | 63 | /// This is a private key which is to be kept secret and is used to prove identity 64 | #[allow(missing_debug_implementations)] 65 | #[derive(Clone)] 66 | pub struct PrivateKey(secp256k1::SecretKey, KeySecurity); 67 | 68 | impl Default for PrivateKey { 69 | fn default() -> Self { 70 | Self::new() 71 | } 72 | } 73 | 74 | impl fmt::Debug for PrivateKey { 75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 76 | write!(f, "PRIVATE-KEY-ELIDED") 77 | } 78 | } 79 | 80 | impl PrivateKey { 81 | /// Generate a new `PrivateKey` (which can be used to get the `PublicKey`) 82 | #[inline] 83 | pub fn new() -> PrivateKey { 84 | Self::generate() 85 | } 86 | 87 | /// Generate a new `PrivateKey` (which can be used to get the `PublicKey`) 88 | pub fn generate() -> PrivateKey { 89 | let mut secret_key; 90 | loop { 91 | secret_key = secp256k1::SecretKey::new(&mut OsRng); 92 | let (_, parity) = secret_key.x_only_public_key(secp256k1::SECP256K1); 93 | if parity == secp256k1::Parity::Even { 94 | break; 95 | } 96 | } 97 | 98 | PrivateKey(secret_key, KeySecurity::Medium) 99 | } 100 | 101 | /// Get the PublicKey matching this PrivateKey 102 | pub fn public_key(&self) -> PublicKey { 103 | let (xopk, _parity) = self.0.x_only_public_key(secp256k1::SECP256K1); 104 | PublicKey::from_bytes(&xopk.serialize(), false).unwrap() 105 | } 106 | 107 | /// Get the security level of the private key 108 | pub fn key_security(&self) -> KeySecurity { 109 | self.1 110 | } 111 | 112 | /// Render into a hexadecimal string 113 | /// 114 | /// WARNING: This weakens the security of your key. Your key will be marked 115 | /// with `KeySecurity::Weak` if you execute this. 116 | pub fn as_hex_string(&mut self) -> String { 117 | self.1 = KeySecurity::Weak; 118 | hex::encode(self.0.secret_bytes()) 119 | } 120 | 121 | /// Create from a hexadecimal string 122 | /// 123 | /// This creates a key with `KeySecurity::Weak`. Use `generate()` or 124 | /// `import_encrypted()` for `KeySecurity::Medium` 125 | pub fn try_from_hex_string(v: &str) -> Result { 126 | let vec: Vec = hex::decode(v)?; 127 | Ok(PrivateKey( 128 | secp256k1::SecretKey::from_slice(&vec)?, 129 | KeySecurity::Weak, 130 | )) 131 | } 132 | 133 | /// Export as a bech32 encoded string 134 | /// 135 | /// WARNING: This weakens the security of your key. Your key will be marked 136 | /// with `KeySecurity::Weak` if you execute this. 137 | pub fn as_bech32_string(&mut self) -> String { 138 | self.1 = KeySecurity::Weak; 139 | bech32::encode::(*crate::HRP_NSEC, self.0.secret_bytes().as_slice()) 140 | .unwrap() 141 | } 142 | 143 | /// Import from a bech32 encoded string 144 | /// 145 | /// This creates a key with `KeySecurity::Weak`. Use `generate()` or 146 | /// `import_encrypted()` for `KeySecurity::Medium` 147 | pub fn try_from_bech32_string(s: &str) -> Result { 148 | let data = bech32::decode(s)?; 149 | if data.0 != *crate::HRP_NSEC { 150 | Err(Error::WrongBech32( 151 | crate::HRP_NSEC.to_lowercase(), 152 | data.0.to_lowercase(), 153 | )) 154 | } else { 155 | Ok(PrivateKey( 156 | secp256k1::SecretKey::from_slice(&data.1)?, 157 | KeySecurity::Weak, 158 | )) 159 | } 160 | } 161 | 162 | /// As a `secp256k1::SecretKey` 163 | pub fn as_secret_key(&self) -> secp256k1::SecretKey { 164 | self.0 165 | } 166 | 167 | // Mock data for testing 168 | #[allow(dead_code)] 169 | pub(crate) fn mock() -> PrivateKey { 170 | PrivateKey::generate() 171 | } 172 | } 173 | 174 | impl Drop for PrivateKey { 175 | fn drop(&mut self) { 176 | self.0.non_secure_erase(); 177 | } 178 | } 179 | 180 | #[async_trait] 181 | impl Signer for PrivateKey { 182 | fn public_key(&self) -> PublicKey { 183 | self.public_key() 184 | } 185 | 186 | fn encrypted_private_key(&self) -> Option { 187 | None 188 | } 189 | 190 | async fn sign_event(&self, input: PreEvent) -> Result { 191 | // Verify the pubkey matches 192 | if input.pubkey != self.public_key() { 193 | return Err(Error::InvalidPrivateKey); 194 | } 195 | 196 | // Generate Id 197 | let id = input.hash()?; 198 | 199 | // Generate Signature 200 | let signature = self.sign_id(id).await?; 201 | 202 | Ok(Event { 203 | id, 204 | pubkey: input.pubkey, 205 | created_at: input.created_at, 206 | kind: input.kind, 207 | tags: input.tags, 208 | content: input.content, 209 | sig: signature, 210 | }) 211 | } 212 | 213 | async fn encrypt( 214 | &self, 215 | other: &PublicKey, 216 | plaintext: &str, 217 | algo: ContentEncryptionAlgorithm, 218 | ) -> Result { 219 | self.encrypt(other, plaintext, algo) 220 | } 221 | 222 | /// Decrypt NIP-44 223 | async fn decrypt(&self, other: &PublicKey, ciphertext: &str) -> Result { 224 | self.decrypt(other, ciphertext) 225 | } 226 | 227 | fn key_security(&self) -> Result { 228 | Ok(KeySecurity::NotTracked) 229 | } 230 | } 231 | 232 | #[async_trait] 233 | impl SignerExt for PrivateKey { 234 | async fn sign_id(&self, id: Id) -> Result { 235 | let keypair = secp256k1::Keypair::from_secret_key(secp256k1::SECP256K1, &self.0); 236 | let message = secp256k1::Message::from_digest_slice(id.0.as_slice())?; 237 | Ok(Signature(keypair.sign_schnorr(message))) 238 | } 239 | 240 | async fn sign(&self, message: &[u8]) -> Result { 241 | use secp256k1::hashes::{sha256, Hash}; 242 | let keypair = secp256k1::Keypair::from_secret_key(secp256k1::SECP256K1, &self.0); 243 | let hash = sha256::Hash::hash(message).to_byte_array(); 244 | let message = secp256k1::Message::from_digest(hash); 245 | Ok(Signature(keypair.sign_schnorr(message))) 246 | } 247 | 248 | async fn nip44_conversation_key(&self, other: &PublicKey) -> Result<[u8; 32], Error> { 249 | Ok(nip44::get_conversation_key( 250 | self.0, 251 | other.as_xonly_public_key(), 252 | )) 253 | } 254 | } 255 | 256 | #[async_trait] 257 | impl MutExportableSigner for PrivateKey { 258 | async fn export_private_key_in_hex( 259 | &mut self, 260 | _pass: &str, 261 | _log_n: u8, 262 | ) -> Result<(String, bool), Error> { 263 | Ok((self.as_hex_string(), false)) 264 | } 265 | 266 | async fn export_private_key_in_bech32( 267 | &mut self, 268 | _pass: &str, 269 | _log_n: u8, 270 | ) -> Result<(String, bool), Error> { 271 | Ok((self.as_bech32_string(), false)) 272 | } 273 | } 274 | 275 | fn base64flex() -> base64::engine::GeneralPurpose { 276 | let config = base64::engine::GeneralPurposeConfig::new() 277 | .with_decode_allow_trailing_bits(true) 278 | .with_encode_padding(true) 279 | .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent); 280 | base64::engine::GeneralPurpose::new(&base64::alphabet::STANDARD, config) 281 | } 282 | 283 | #[cfg(test)] 284 | mod test { 285 | use super::*; 286 | 287 | #[test] 288 | fn test_privkey_bech32() { 289 | let mut pk = PrivateKey::mock(); 290 | 291 | let encoded = pk.as_bech32_string(); 292 | println!("bech32: {encoded}"); 293 | 294 | let decoded = PrivateKey::try_from_bech32_string(&encoded).unwrap(); 295 | 296 | assert_eq!(pk.0.secret_bytes(), decoded.0.secret_bytes()); 297 | assert_eq!(decoded.1, KeySecurity::Weak); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/types/profile.rs: -------------------------------------------------------------------------------- 1 | use super::{PublicKey, UncheckedUrl}; 2 | use crate::Error; 3 | use serde::{Deserialize, Serialize}; 4 | #[cfg(feature = "speedy")] 5 | use speedy::{Readable, Writable}; 6 | 7 | /// A person's profile on nostr which consists of the data needed in order to follow someone. 8 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 9 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 10 | pub struct Profile { 11 | /// Their public key 12 | pub pubkey: PublicKey, 13 | 14 | /// Some of the relays they post to (when the profile was created) 15 | pub relays: Vec, 16 | } 17 | 18 | impl Profile { 19 | /// Export as a bech32 encoded string ("nprofile") 20 | pub fn as_bech32_string(&self) -> String { 21 | // Compose 22 | let mut tlv: Vec = Vec::new(); 23 | 24 | // Push Public Key 25 | tlv.push(0); // the special value, in this case the public key 26 | tlv.push(32); // the length of the value (always 32 for public key) 27 | tlv.extend(self.pubkey.as_slice()); 28 | 29 | // Push relays 30 | for relay in &self.relays { 31 | tlv.push(1); // type 'relay' 32 | let len = relay.0.len() as u8; 33 | tlv.push(len); // the length of the string 34 | tlv.extend(&relay.0.as_bytes()[..len as usize]); 35 | } 36 | 37 | bech32::encode::(*crate::HRP_NPROFILE, &tlv).unwrap() 38 | } 39 | 40 | /// Import from a bech32 encoded string ("nprofile") 41 | /// 42 | /// If verify is true, will verify that it works as a secp256k1::XOnlyPublicKey. This 43 | /// has a performance cost. 44 | pub fn try_from_bech32_string(s: &str, verify: bool) -> Result { 45 | let data = bech32::decode(s)?; 46 | if data.0 != *crate::HRP_NPROFILE { 47 | Err(Error::WrongBech32( 48 | crate::HRP_NPROFILE.to_lowercase(), 49 | data.0.to_lowercase(), 50 | )) 51 | } else { 52 | let mut relays: Vec = Vec::new(); 53 | let mut pubkey: Option = None; 54 | let tlv = data.1; 55 | let mut pos = 0; 56 | loop { 57 | // we need at least 2 more characters for anything meaningful 58 | if pos > tlv.len() - 2 { 59 | break; 60 | } 61 | let ty = tlv[pos]; 62 | let len = tlv[pos + 1] as usize; 63 | pos += 2; 64 | if pos + len > tlv.len() { 65 | return Err(Error::InvalidProfile); 66 | } 67 | match ty { 68 | 0 => { 69 | // special, 32 bytes of the public key 70 | if len != 32 { 71 | return Err(Error::InvalidProfile); 72 | } 73 | pubkey = Some(PublicKey::from_bytes(&tlv[pos..pos + len], verify)?); 74 | } 75 | 1 => { 76 | // relay 77 | let relay_bytes = &tlv[pos..pos + len]; 78 | let relay_str = std::str::from_utf8(relay_bytes)?; 79 | let relay = UncheckedUrl::from_str(relay_str); 80 | relays.push(relay); 81 | } 82 | _ => {} // unhandled type for nprofile 83 | } 84 | pos += len; 85 | } 86 | if let Some(pubkey) = pubkey { 87 | Ok(Profile { pubkey, relays }) 88 | } else { 89 | Err(Error::InvalidProfile) 90 | } 91 | } 92 | } 93 | 94 | // Mock data for testing 95 | #[allow(dead_code)] 96 | pub(crate) fn mock() -> Profile { 97 | let pubkey = PublicKey::try_from_hex_string( 98 | "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", 99 | true, 100 | ) 101 | .unwrap(); 102 | 103 | Profile { 104 | pubkey, 105 | relays: vec![ 106 | UncheckedUrl::from_str("wss://relay.example.com"), 107 | UncheckedUrl::from_str("wss://relay2.example.com"), 108 | ], 109 | } 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod test { 115 | use super::*; 116 | 117 | test_serde! {Profile, test_profile_serde} 118 | 119 | #[test] 120 | fn test_profile_bech32() { 121 | let bech32 = Profile::mock().as_bech32_string(); 122 | println!("{bech32}"); 123 | assert_eq!( 124 | Profile::mock(), 125 | Profile::try_from_bech32_string(&bech32, true).unwrap() 126 | ); 127 | } 128 | 129 | #[test] 130 | fn test_nip19_example() { 131 | let profile = Profile { 132 | pubkey: PublicKey::try_from_hex_string( 133 | "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", 134 | true, 135 | ) 136 | .unwrap(), 137 | relays: vec![ 138 | UncheckedUrl::from_str("wss://r.x.com"), 139 | UncheckedUrl::from_str("wss://djbas.sadkb.com"), 140 | ], 141 | }; 142 | 143 | let bech32 = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"; 144 | 145 | // Try converting profile to bech32 146 | assert_eq!(profile.as_bech32_string(), bech32); 147 | 148 | // Try converting bech32 to profile 149 | assert_eq!( 150 | profile, 151 | Profile::try_from_bech32_string(bech32, true).unwrap() 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/types/relay_information_document.rs: -------------------------------------------------------------------------------- 1 | use crate::versioned::relay_information_document1::{FeeV1, RelayFeesV1, RelayRetentionV1}; 2 | use crate::versioned::relay_information_document2::{ 3 | RelayInformationDocumentV2, RelayLimitationV2, 4 | }; 5 | 6 | /// Relay limitations 7 | pub type RelayLimitation = RelayLimitationV2; 8 | 9 | /// Relay retention 10 | pub type RelayRetention = RelayRetentionV1; 11 | 12 | /// Fee 13 | pub type Fee = FeeV1; 14 | 15 | /// Relay fees 16 | pub type RelayFees = RelayFeesV1; 17 | 18 | /// Relay information document as described in NIP-11, supplied by a relay 19 | pub type RelayInformationDocument = RelayInformationDocumentV2; 20 | -------------------------------------------------------------------------------- /src/types/relay_list.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{Event, ParsedTag, RelayUrl, Tag}; 2 | use std::collections::HashMap; 3 | 4 | /// Relay Usage 5 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] 6 | pub enum RelayListUsage { 7 | /// The relay is used as an inbox (called 'read' in kind-10002) 8 | Inbox, 9 | 10 | /// The relay is used as an outbox (called 'write' in kind-10002) 11 | Outbox, 12 | 13 | /// The relay is used both as an inbox and an outbox 14 | #[default] 15 | Both, 16 | } 17 | 18 | impl RelayListUsage { 19 | /// A string marker used in a kind-10002 RelayList event for the variant 20 | pub fn marker(&self) -> Option<&'static str> { 21 | match self { 22 | RelayListUsage::Inbox => Some("read"), 23 | RelayListUsage::Outbox => Some("write"), 24 | RelayListUsage::Both => None, 25 | } 26 | } 27 | } 28 | 29 | /// A relay list, indicating usage for each relay, which can be used to 30 | /// represent the data found in a kind 10002 RelayListMetadata event. 31 | #[derive(Clone, Debug, Default, Eq, PartialEq)] 32 | pub struct RelayList(pub HashMap); 33 | 34 | impl RelayList { 35 | /// Parse a kind-10002 RelayList event into a RelayList 36 | /// 37 | /// This does not check the event kind, that is left up to the caller. 38 | pub fn from_event(event: &Event) -> RelayList { 39 | let mut relay_list: RelayList = Default::default(); 40 | 41 | for tag in event.tags.iter() { 42 | if let Ok(ParsedTag::RelayUsage { url, usage }) = tag.parse() { 43 | if let Ok(relay_url) = RelayUrl::try_from_unchecked_url(&url) { 44 | if let Some(m) = usage { 45 | match &*m.trim().to_lowercase() { 46 | "read" => { 47 | let _ = relay_list.0.insert(relay_url, RelayListUsage::Inbox); 48 | } 49 | "write" => { 50 | let _ = relay_list.0.insert(relay_url, RelayListUsage::Outbox); 51 | } 52 | _ => {} // ignore unknown marker 53 | } 54 | } else { 55 | let _ = relay_list.0.insert(relay_url, RelayListUsage::Both); 56 | } 57 | } 58 | } 59 | } 60 | 61 | relay_list 62 | } 63 | 64 | /// Create a `Vec` appropriate for forming a kind-10002 RelayList event 65 | pub fn to_event_tags(&self) -> Vec { 66 | let mut tags: Vec = Vec::new(); 67 | for (relay_url, usage) in self.0.iter() { 68 | tags.push( 69 | ParsedTag::RelayUsage { 70 | url: relay_url.to_unchecked_url(), 71 | usage: usage.marker().map(|s| s.to_owned()), 72 | } 73 | .into_tag(), 74 | ); 75 | } 76 | tags 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/types/relay_message.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{Event, Id, SubscriptionId}; 2 | use serde::de::Error as DeError; 3 | use serde::de::{Deserialize, Deserializer, IgnoredAny, SeqAccess, Visitor}; 4 | use serde::ser::{Serialize, SerializeSeq, Serializer}; 5 | use std::fmt; 6 | 7 | /// A message from a relay to a client 8 | #[derive(Clone, Debug, Eq, PartialEq)] 9 | pub enum RelayMessage { 10 | /// Used to send authentication challenges 11 | Auth(String), 12 | 13 | /// Used to indicate that a subscription was ended on the server side 14 | /// Every ClientMessage::Req _may_ trigger a RelayMessage::Closed response 15 | /// The last parameter may have a colon-terminated machine-readable prefix of: 16 | /// duplicate, pow, blocked, rate-limited, invalid, auth-required, 17 | /// restricted, or error 18 | Closed(SubscriptionId, String), 19 | 20 | /// End of subscribed events notification 21 | Eose(SubscriptionId), 22 | 23 | /// An event matching a subscription 24 | Event(SubscriptionId, Box), 25 | 26 | /// A human readable notice for errors and other information 27 | Notice(String), 28 | 29 | /// A human readable notice for the end user 30 | Notify(String), 31 | 32 | /// Used to notify clients if an event was successuful 33 | /// Every ClientMessage::Event will trigger a RelayMessage::OK response 34 | /// The last parameter may have a colon-terminated machine-readable prefix of: 35 | /// duplicate, pow, blocked, rate-limited, invalid, auth-required, 36 | /// restricted or error 37 | Ok(Id, bool, String), 38 | 39 | /// The results of a COUNT command 40 | Count(SubscriptionId, CountResult), 41 | } 42 | 43 | /// The count results 44 | #[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] 45 | pub struct CountResult { 46 | /// The count 47 | pub count: usize, 48 | 49 | /// Whether the count is approximate 50 | #[serde(default)] 51 | pub approximate: bool, 52 | 53 | /// Optional HyperLogLog data 54 | #[serde(default)] 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub hll: Option, 57 | } 58 | 59 | /// The reason why a relay issued an OK or CLOSED message 60 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 61 | pub enum Why { 62 | /// Authentication is required 63 | AuthRequired, 64 | 65 | /// You have been blocked from this relay 66 | Blocked, 67 | 68 | /// Your request is a duplicate 69 | Duplicate, 70 | 71 | /// Other error 72 | Error, 73 | 74 | /// Your request is invalid 75 | Invalid, 76 | 77 | /// Proof-of-work is required 78 | Pow, 79 | 80 | /// Rejected due to rate limiting 81 | RateLimited, 82 | 83 | /// The action you requested is restricted to your identity 84 | Restricted, 85 | } 86 | 87 | impl RelayMessage { 88 | /// Translate the machine-readable prefix from the message 89 | pub fn why(&self) -> Option { 90 | let s = match *self { 91 | RelayMessage::Closed(_, ref s) => s, 92 | RelayMessage::Ok(_, _, ref s) => s, 93 | _ => return None, 94 | }; 95 | 96 | match s.split(':').next() { 97 | Some("auth-required") => Some(Why::AuthRequired), 98 | Some("blocked") => Some(Why::Blocked), 99 | Some("duplicate") => Some(Why::Duplicate), 100 | Some("error") => Some(Why::Error), 101 | Some("invalid") => Some(Why::Invalid), 102 | Some("pow") => Some(Why::Pow), 103 | Some("rate-limited") => Some(Why::RateLimited), 104 | Some("restricted") => Some(Why::Restricted), 105 | _ => None, 106 | } 107 | } 108 | 109 | // Mock data for testing 110 | #[allow(dead_code)] 111 | pub(crate) async fn mock() -> RelayMessage { 112 | RelayMessage::Event(SubscriptionId::mock(), Box::new(Event::mock().await)) 113 | } 114 | } 115 | 116 | impl Serialize for RelayMessage { 117 | fn serialize(&self, serializer: S) -> Result 118 | where 119 | S: Serializer, 120 | { 121 | match self { 122 | RelayMessage::Auth(challenge) => { 123 | let mut seq = serializer.serialize_seq(Some(2))?; 124 | seq.serialize_element("AUTH")?; 125 | seq.serialize_element(&challenge)?; 126 | seq.end() 127 | } 128 | RelayMessage::Closed(id, message) => { 129 | let mut seq = serializer.serialize_seq(Some(3))?; 130 | seq.serialize_element("CLOSED")?; 131 | seq.serialize_element(&id)?; 132 | seq.serialize_element(&message)?; 133 | seq.end() 134 | } 135 | RelayMessage::Eose(id) => { 136 | let mut seq = serializer.serialize_seq(Some(2))?; 137 | seq.serialize_element("EOSE")?; 138 | seq.serialize_element(&id)?; 139 | seq.end() 140 | } 141 | RelayMessage::Event(id, event) => { 142 | let mut seq = serializer.serialize_seq(Some(3))?; 143 | seq.serialize_element("EVENT")?; 144 | seq.serialize_element(&id)?; 145 | seq.serialize_element(&event)?; 146 | seq.end() 147 | } 148 | RelayMessage::Notice(s) => { 149 | let mut seq = serializer.serialize_seq(Some(2))?; 150 | seq.serialize_element("NOTICE")?; 151 | seq.serialize_element(&s)?; 152 | seq.end() 153 | } 154 | RelayMessage::Notify(s) => { 155 | let mut seq = serializer.serialize_seq(Some(2))?; 156 | seq.serialize_element("NOTIFY")?; 157 | seq.serialize_element(&s)?; 158 | seq.end() 159 | } 160 | RelayMessage::Ok(id, ok, message) => { 161 | let mut seq = serializer.serialize_seq(Some(4))?; 162 | seq.serialize_element("OK")?; 163 | seq.serialize_element(&id)?; 164 | seq.serialize_element(&ok)?; 165 | seq.serialize_element(&message)?; 166 | seq.end() 167 | } 168 | RelayMessage::Count(sub, result) => { 169 | let mut seq = serializer.serialize_seq(Some(3))?; 170 | seq.serialize_element("COUNT")?; 171 | seq.serialize_element(&sub)?; 172 | seq.serialize_element(&result)?; 173 | seq.end() 174 | } 175 | } 176 | } 177 | } 178 | 179 | impl<'de> Deserialize<'de> for RelayMessage { 180 | fn deserialize(deserializer: D) -> Result 181 | where 182 | D: Deserializer<'de>, 183 | { 184 | deserializer.deserialize_seq(RelayMessageVisitor) 185 | } 186 | } 187 | 188 | struct RelayMessageVisitor; 189 | 190 | impl<'de> Visitor<'de> for RelayMessageVisitor { 191 | type Value = RelayMessage; 192 | 193 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 194 | write!(f, "a sequence of strings") 195 | } 196 | 197 | fn visit_seq(self, mut seq: A) -> Result 198 | where 199 | A: SeqAccess<'de>, 200 | { 201 | let word: &str = seq 202 | .next_element()? 203 | .ok_or_else(|| DeError::custom("Message missing initial string field"))?; 204 | let mut output: Option = None; 205 | if word == "EVENT" { 206 | let id: SubscriptionId = seq 207 | .next_element()? 208 | .ok_or_else(|| DeError::custom("Message missing id field"))?; 209 | let event: Event = seq 210 | .next_element()? 211 | .ok_or_else(|| DeError::custom("Message missing event field"))?; 212 | output = Some(RelayMessage::Event(id, Box::new(event))); 213 | } else if word == "NOTICE" { 214 | let s: String = seq 215 | .next_element()? 216 | .ok_or_else(|| DeError::custom("Message missing string field"))?; 217 | output = Some(RelayMessage::Notice(s)); 218 | } else if word == "NOTIFY" { 219 | let s: String = seq 220 | .next_element()? 221 | .ok_or_else(|| DeError::custom("Message missing string field"))?; 222 | output = Some(RelayMessage::Notify(s)); 223 | } else if word == "EOSE" { 224 | let id: SubscriptionId = seq 225 | .next_element()? 226 | .ok_or_else(|| DeError::custom("Message missing id field"))?; 227 | output = Some(RelayMessage::Eose(id)) 228 | } else if word == "OK" { 229 | let id: Id = seq 230 | .next_element()? 231 | .ok_or_else(|| DeError::custom("Message missing id field"))?; 232 | let ok: bool = seq 233 | .next_element()? 234 | .ok_or_else(|| DeError::custom("Message missing ok field"))?; 235 | let message: String = seq 236 | .next_element()? 237 | .ok_or_else(|| DeError::custom("Message missing string field"))?; 238 | output = Some(RelayMessage::Ok(id, ok, message)); 239 | } else if word == "AUTH" { 240 | let challenge: String = seq 241 | .next_element()? 242 | .ok_or_else(|| DeError::custom("Message missing challenge field"))?; 243 | output = Some(RelayMessage::Auth(challenge)); 244 | } else if word == "CLOSED" { 245 | let id: SubscriptionId = seq 246 | .next_element()? 247 | .ok_or_else(|| DeError::custom("Message messing id field"))?; 248 | let message: String = seq 249 | .next_element()? 250 | .ok_or_else(|| DeError::custom("Message missing string field"))?; 251 | output = Some(RelayMessage::Closed(id, message)); 252 | } else if word == "COUNT" { 253 | let id: SubscriptionId = seq 254 | .next_element()? 255 | .ok_or_else(|| DeError::custom("Message messing id field"))?; 256 | let count_result: CountResult = seq 257 | .next_element()? 258 | .ok_or_else(|| DeError::custom("Message messing count result object field"))?; 259 | output = Some(RelayMessage::Count(id, count_result)); 260 | } 261 | 262 | // Consume any trailing fields 263 | while let Some(_ignored) = seq.next_element::()? {} 264 | 265 | match output { 266 | Some(rm) => Ok(rm), 267 | None => Err(DeError::custom(format!("Unknown Message: {word}"))), 268 | } 269 | } 270 | } 271 | 272 | #[cfg(test)] 273 | mod test { 274 | use super::*; 275 | 276 | test_serde_async! {RelayMessage, test_relay_message_serde} 277 | } 278 | -------------------------------------------------------------------------------- /src/types/relay_usage.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | /// A way that a user uses a Relay 4 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] 5 | #[repr(u32)] 6 | pub enum RelayUsage { 7 | /// User seeks events here if they are not otherwise found 8 | FallbackRead = 1 << 0, 9 | 10 | /// User writes here but does not advertise it 11 | Archive = 1 << 1, 12 | 13 | // was a relay usage flag in gossip, but was retired 14 | // Advertise = 1 << 2, 15 | /// User accepts posts here from the public that tag them 16 | Inbox = 1 << 3, 17 | 18 | /// User posts here for the public 19 | Outbox = 1 << 4, 20 | 21 | /// User seeks relay lists here (index, discover) 22 | Directory = 1 << 5, 23 | 24 | // is used as SPAMSAFE bit in gossip so reserved, but isn't a relay usage 25 | // ReservedSpamsafe = 1 << 6, 26 | /// User accepts DMs here 27 | Dm = 1 << 7, 28 | 29 | /// user stores and reads back their own configurations here 30 | Config = 1 << 8, 31 | 32 | /// User does NIP-50 SEARCH here 33 | Search = 1 << 9, 34 | } 35 | 36 | impl TryFrom for RelayUsage { 37 | type Error = (); 38 | 39 | fn try_from(u: u32) -> Result { 40 | match u { 41 | 1 => Ok(RelayUsage::FallbackRead), 42 | 2 => Ok(RelayUsage::Archive), 43 | 8 => Ok(RelayUsage::Inbox), 44 | 16 => Ok(RelayUsage::Outbox), 45 | 32 => Ok(RelayUsage::Directory), 46 | 128 => Ok(RelayUsage::Dm), 47 | 256 => Ok(RelayUsage::Config), 48 | 512 => Ok(RelayUsage::Search), 49 | _ => Err(()), 50 | } 51 | } 52 | } 53 | 54 | /// The ways that a user uses a Relay 55 | /// 56 | // See also https://github.com/mikedilger/gossip/blob/master/gossip-lib/src/storage/types/relay3.rs 57 | // See also https://github.com/nostr-protocol/nips/issues/1282 for possible future entries 58 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] 59 | pub struct RelayUsageSet(u32); 60 | 61 | impl RelayUsageSet { 62 | const MASK: u32 = RelayUsage::FallbackRead as u32 63 | | RelayUsage::Archive as u32 64 | | RelayUsage::Inbox as u32 65 | | RelayUsage::Outbox as u32 66 | | RelayUsage::Directory as u32 67 | | RelayUsage::Dm as u32 68 | | RelayUsage::Config as u32 69 | | RelayUsage::Search as u32; 70 | 71 | /// Create a new empty RelayUsageSet 72 | pub const fn new_empty() -> Self { 73 | RelayUsageSet(0) 74 | } 75 | 76 | /// Create a new RelayUsageSet with all usages 77 | pub const fn new_all() -> Self { 78 | Self(Self::MASK) 79 | } 80 | 81 | /// Get the u32 bitflag representation 82 | pub const fn bits(&self) -> u32 { 83 | self.0 84 | } 85 | 86 | /// Set from a u32 bitflag representation. If any unknown bits are set, 87 | /// this will return None 88 | pub const fn from_bits(bits: u32) -> Option { 89 | if bits & !Self::MASK != 0 { 90 | None 91 | } else { 92 | Some(RelayUsageSet(bits)) 93 | } 94 | } 95 | 96 | /// Set from a u32 bitflag representation. If any unknown bits are set, 97 | /// they will be cleared 98 | pub const fn from_bits_truncate(bits: u32) -> RelayUsageSet { 99 | RelayUsageSet(bits & Self::MASK) 100 | } 101 | 102 | /// Whether all bits are unset 103 | pub const fn is_empty(&self) -> bool { 104 | self.0 == 0 105 | } 106 | 107 | /// Whether all defined bits are set 108 | pub const fn is_all(&self) -> bool { 109 | self.0 & Self::MASK == Self::MASK 110 | } 111 | 112 | /// Whether any usage in other is also in Self 113 | pub const fn intersects(&self, other: Self) -> bool { 114 | self.0 & other.0 != 0 115 | } 116 | 117 | /// Whether all usages in other are in Self 118 | pub const fn contains(&self, other: Self) -> bool { 119 | self.0 & other.0 == other.0 120 | } 121 | 122 | /// Has a RelayUsage set 123 | pub fn has_usage(&mut self, ru: RelayUsage) -> bool { 124 | self.0 & ru as u32 == ru as u32 125 | } 126 | 127 | /// Add a RelayUsage to Self 128 | pub fn add_usage(&mut self, ru: RelayUsage) { 129 | self.0 |= ru as u32 130 | } 131 | 132 | /// Remove a RelayUsage to Self 133 | pub fn remove_usage(&mut self, ru: RelayUsage) { 134 | self.0 &= !(ru as u32) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/types/satoshi.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{AsMut, AsRef, Deref, Display, From, Into}; 2 | use serde::{Deserialize, Serialize}; 3 | #[cfg(feature = "speedy")] 4 | use speedy::{Readable, Writable}; 5 | use std::ops::Add; 6 | 7 | /// Bitcoin amount measured in millisatoshi 8 | #[derive( 9 | AsMut, 10 | AsRef, 11 | Clone, 12 | Copy, 13 | Debug, 14 | Deref, 15 | Deserialize, 16 | Display, 17 | Eq, 18 | From, 19 | Into, 20 | Ord, 21 | PartialEq, 22 | PartialOrd, 23 | Serialize, 24 | )] 25 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 26 | pub struct MilliSatoshi(pub u64); 27 | 28 | impl MilliSatoshi { 29 | // Mock data for testing 30 | #[allow(dead_code)] 31 | pub(crate) fn mock() -> MilliSatoshi { 32 | MilliSatoshi(15423000) 33 | } 34 | } 35 | 36 | impl Add for MilliSatoshi { 37 | type Output = Self; 38 | 39 | fn add(self, rhs: MilliSatoshi) -> Self::Output { 40 | MilliSatoshi(self.0 + rhs.0) 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod test { 46 | use super::*; 47 | 48 | test_serde! {MilliSatoshi, test_millisatoshi_serde} 49 | 50 | #[test] 51 | fn test_millisatoshi_math() { 52 | let a = MilliSatoshi(15000); 53 | let b = MilliSatoshi(3000); 54 | let c = a + b; 55 | assert_eq!(c.0, 18000); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/types/signature.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, Event}; 2 | use derive_more::{AsMut, AsRef, Deref, Display, From, FromStr, Into}; 3 | use serde::{Deserialize, Serialize}; 4 | #[cfg(feature = "speedy")] 5 | use speedy::{Context, Readable, Reader, Writable, Writer}; 6 | 7 | /// A Schnorr signature that signs an Event, taken on the Event Id field 8 | #[derive( 9 | AsMut, AsRef, Clone, Copy, Debug, Deref, Eq, From, Into, PartialEq, Serialize, Deserialize, 10 | )] 11 | pub struct Signature(pub secp256k1::schnorr::Signature); 12 | 13 | impl Signature { 14 | /// Render into a hexadecimal string 15 | pub fn as_hex_string(&self) -> String { 16 | hex::encode(self.0.as_ref()) 17 | } 18 | 19 | /// Create from a hexadecimal string 20 | pub fn try_from_hex_string(v: &str) -> Result { 21 | let vec: Vec = hex::decode(v)?; 22 | Ok(Signature(secp256k1::schnorr::Signature::from_slice(&vec)?)) 23 | } 24 | 25 | /// A dummy signature of all zeroes 26 | pub fn zeroes() -> Signature { 27 | Signature(secp256k1::schnorr::Signature::from_slice(&[0; 64]).unwrap()) 28 | } 29 | 30 | // Mock data for testing 31 | #[allow(dead_code)] 32 | pub(crate) async fn mock() -> Signature { 33 | let event = Event::mock().await; 34 | event.sig 35 | } 36 | } 37 | 38 | #[cfg(feature = "speedy")] 39 | impl<'a, C: Context> Readable<'a, C> for Signature { 40 | #[inline] 41 | fn read_from>(reader: &mut R) -> Result { 42 | let bytes: Vec = reader.read_vec(64)?; 43 | let sig = 44 | secp256k1::schnorr::Signature::from_slice(&bytes[..]).map_err(speedy::Error::custom)?; 45 | Ok(Signature(sig)) 46 | } 47 | 48 | #[inline] 49 | fn minimum_bytes_needed() -> usize { 50 | 64 51 | } 52 | } 53 | 54 | #[cfg(feature = "speedy")] 55 | impl Writable for Signature { 56 | #[inline] 57 | fn write_to>(&self, writer: &mut T) -> Result<(), C::Error> { 58 | let bytes = self.0.as_ref(); 59 | assert_eq!(bytes.as_slice().len(), 64); 60 | writer.write_bytes(bytes.as_slice()) 61 | } 62 | 63 | #[inline] 64 | fn bytes_needed(&self) -> Result { 65 | Ok(64) 66 | } 67 | } 68 | 69 | /// A Schnorr signature that signs an Event, taken on the Event Id field, as a hex string 70 | #[derive( 71 | AsMut, 72 | AsRef, 73 | Clone, 74 | Debug, 75 | Deref, 76 | Deserialize, 77 | Display, 78 | Eq, 79 | From, 80 | FromStr, 81 | Hash, 82 | Into, 83 | PartialEq, 84 | Serialize, 85 | )] 86 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 87 | pub struct SignatureHex(pub String); 88 | 89 | impl SignatureHex { 90 | // Mock data for testing 91 | #[allow(dead_code)] 92 | pub(crate) async fn mock() -> SignatureHex { 93 | From::from(Signature::mock().await) 94 | } 95 | } 96 | 97 | impl From for SignatureHex { 98 | fn from(s: Signature) -> SignatureHex { 99 | SignatureHex(s.as_hex_string()) 100 | } 101 | } 102 | 103 | impl TryFrom for Signature { 104 | type Error = Error; 105 | 106 | fn try_from(sh: SignatureHex) -> Result { 107 | Signature::try_from_hex_string(&sh.0) 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod test { 113 | use super::*; 114 | 115 | test_serde_async! {Signature, test_signature_serde} 116 | 117 | #[cfg(feature = "speedy")] 118 | #[tokio::test] 119 | async fn test_speedy_signature() { 120 | let sig = Signature::mock().await; 121 | let bytes = sig.write_to_vec().unwrap(); 122 | let sig2 = Signature::read_from_buffer(&bytes).unwrap(); 123 | assert_eq!(sig, sig2); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/types/simple_relay_list.rs: -------------------------------------------------------------------------------- 1 | use crate::types::UncheckedUrl; 2 | use serde::de::{Deserializer, MapAccess, Visitor}; 3 | use serde::ser::{SerializeMap, Serializer}; 4 | use serde::{Deserialize, Serialize}; 5 | #[cfg(feature = "speedy")] 6 | use speedy::{Readable, Writable}; 7 | use std::collections::HashMap; 8 | use std::fmt; 9 | 10 | /// When and how to use a Relay 11 | /// 12 | /// This is used only for `SimpleRelayList`. 13 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] 14 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 15 | pub struct SimpleRelayUsage { 16 | /// Whether to write to this relay 17 | pub write: bool, 18 | 19 | /// Whether to read from this relay 20 | pub read: bool, 21 | } 22 | 23 | impl Default for SimpleRelayUsage { 24 | fn default() -> SimpleRelayUsage { 25 | SimpleRelayUsage { 26 | write: false, 27 | read: true, 28 | } 29 | } 30 | } 31 | 32 | /// A list of relays with SimpleRelayUsage 33 | /// 34 | /// This is only used for handling the contents of a kind-3 contact list. 35 | /// For normal relay lists, consider using `RelayList` instead. 36 | #[derive(Clone, Debug, Default, Eq, PartialEq)] 37 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 38 | pub struct SimpleRelayList(pub HashMap); 39 | 40 | impl SimpleRelayList { 41 | #[allow(dead_code)] 42 | pub(crate) fn mock() -> SimpleRelayList { 43 | let mut map: HashMap = HashMap::new(); 44 | let _ = map.insert( 45 | UncheckedUrl::from_str("wss://nostr.oxtr.dev"), 46 | SimpleRelayUsage { 47 | write: true, 48 | read: true, 49 | }, 50 | ); 51 | let _ = map.insert( 52 | UncheckedUrl::from_str("wss://nostr-relay.wlvs.space"), 53 | SimpleRelayUsage { 54 | write: false, 55 | read: true, 56 | }, 57 | ); 58 | SimpleRelayList(map) 59 | } 60 | } 61 | 62 | impl Serialize for SimpleRelayList { 63 | fn serialize(&self, serializer: S) -> Result 64 | where 65 | S: Serializer, 66 | { 67 | let mut map = serializer.serialize_map(Some(self.0.len()))?; 68 | for (k, v) in &self.0 { 69 | map.serialize_entry(&k, &v)?; 70 | } 71 | map.end() 72 | } 73 | } 74 | 75 | impl<'de> Deserialize<'de> for SimpleRelayList { 76 | fn deserialize(deserializer: D) -> Result 77 | where 78 | D: Deserializer<'de>, 79 | { 80 | deserializer.deserialize_map(SimpleRelayListVisitor) 81 | } 82 | } 83 | 84 | struct SimpleRelayListVisitor; 85 | 86 | impl<'de> Visitor<'de> for SimpleRelayListVisitor { 87 | type Value = SimpleRelayList; 88 | 89 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 90 | write!(f, "A JSON object") 91 | } 92 | 93 | fn visit_map(self, mut access: M) -> Result 94 | where 95 | M: MapAccess<'de>, 96 | { 97 | let mut map: HashMap = HashMap::new(); 98 | while let Some((key, value)) = access.next_entry::()? { 99 | let _ = map.insert(key, value); 100 | } 101 | Ok(SimpleRelayList(map)) 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod test { 107 | use super::*; 108 | 109 | test_serde! {SimpleRelayList, test_simple_relay_list_serde} 110 | 111 | #[test] 112 | fn test_simple_relay_list_json() { 113 | let serialized = r#"{"wss://nostr.oxtr.dev":{"write":true,"read":true},"wss://relay.damus.io":{"write":true,"read":true},"wss://nostr.fmt.wiz.biz":{"write":true,"read":true},"wss://nostr-relay.wlvs.space":{"write":true,"read":true}}"#; 114 | let _simple_relay_list: SimpleRelayList = serde_json::from_str(serialized).unwrap(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/types/subscription_id.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{AsMut, AsRef, Deref, From, FromStr, Into}; 2 | use serde::{Deserialize, Serialize}; 3 | #[cfg(feature = "speedy")] 4 | use speedy::{Readable, Writable}; 5 | 6 | /// A random client-chosen string used to refer to a subscription 7 | #[derive( 8 | AsMut, AsRef, Clone, Debug, Deref, Deserialize, Eq, From, FromStr, Into, PartialEq, Serialize, 9 | )] 10 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 11 | pub struct SubscriptionId(pub String); 12 | 13 | impl SubscriptionId { 14 | // Mock data for testing 15 | #[allow(dead_code)] 16 | pub(crate) fn mock() -> SubscriptionId { 17 | SubscriptionId("lk234js09".to_owned()) 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod test { 23 | use super::*; 24 | 25 | test_serde! {SubscriptionId, test_subscription_id_serde} 26 | } 27 | -------------------------------------------------------------------------------- /src/types/tagval.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikedilger/nostr-types/a5474784e860c193cac2ee2280d66417f9d2e389/src/types/tagval.rs -------------------------------------------------------------------------------- /src/types/unixtime.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{AsMut, AsRef, Deref, Display, From, Into}; 2 | use serde::{Deserialize, Serialize}; 3 | #[cfg(feature = "speedy")] 4 | use speedy::{Readable, Writable}; 5 | use std::ops::{Add, Sub}; 6 | use std::time::Duration; 7 | 8 | /// An integer count of the number of seconds from 1st January 1970. 9 | /// This does not count any of the leap seconds that have occurred, it 10 | /// simply presumes UTC never had leap seconds; yet it is well known 11 | /// and well understood. 12 | #[derive( 13 | AsMut, 14 | AsRef, 15 | Clone, 16 | Copy, 17 | Debug, 18 | Deref, 19 | Deserialize, 20 | Display, 21 | Eq, 22 | From, 23 | Into, 24 | Ord, 25 | PartialEq, 26 | PartialOrd, 27 | Serialize, 28 | )] 29 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 30 | pub struct Unixtime(pub i64); 31 | 32 | impl Unixtime { 33 | /// Get the current unixtime (depends on the system clock being accurate) 34 | pub fn now() -> Unixtime { 35 | Unixtime(std::time::UNIX_EPOCH.elapsed().unwrap().as_secs() as i64) 36 | } 37 | 38 | // Mock data for testing 39 | #[allow(dead_code)] 40 | pub(crate) fn mock() -> Unixtime { 41 | Unixtime(1668572286) 42 | } 43 | } 44 | 45 | impl Add for Unixtime { 46 | type Output = Self; 47 | 48 | fn add(self, rhs: Duration) -> Self::Output { 49 | Unixtime(self.0 + rhs.as_secs() as i64) 50 | } 51 | } 52 | 53 | impl Sub for Unixtime { 54 | type Output = Self; 55 | 56 | fn sub(self, rhs: Duration) -> Self::Output { 57 | Unixtime(self.0 - rhs.as_secs() as i64) 58 | } 59 | } 60 | 61 | impl Sub for Unixtime { 62 | type Output = Duration; 63 | 64 | fn sub(self, rhs: Unixtime) -> Self::Output { 65 | Duration::from_secs((self.0 - rhs.0).unsigned_abs()) 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod test { 71 | use super::*; 72 | 73 | test_serde! {Unixtime, test_unixtime_serde} 74 | 75 | #[test] 76 | fn test_print_now() { 77 | println!("NOW: {}", Unixtime::now()); 78 | } 79 | 80 | #[test] 81 | fn test_unixtime_math() { 82 | let now = Unixtime::now(); 83 | let fut = now + Duration::from_secs(70); 84 | assert!(fut > now); 85 | assert_eq!(fut.0 - now.0, 70); 86 | let back = fut - Duration::from_secs(70); 87 | assert_eq!(now, back); 88 | assert_eq!(now - back, Duration::ZERO); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/types/url.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use serde::{Deserialize, Serialize}; 3 | #[cfg(feature = "speedy")] 4 | use speedy::{Readable, Writable}; 5 | use std::fmt; 6 | 7 | /// A string that is supposed to represent a URL but which might be invalid 8 | #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize, Ord)] 9 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 10 | pub struct UncheckedUrl(pub String); 11 | 12 | impl fmt::Display for UncheckedUrl { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | write!(f, "{}", self.0) 15 | } 16 | } 17 | 18 | impl UncheckedUrl { 19 | /// Create an UncheckedUrl from a &str 20 | // note - this from_str cannot error, so we don't impl std::str::FromStr which by 21 | // all rights should be called TryFromStr anyway 22 | #[allow(clippy::should_implement_trait)] 23 | pub fn from_str(s: &str) -> UncheckedUrl { 24 | UncheckedUrl(s.to_owned()) 25 | } 26 | 27 | /// Create an UncheckedUrl from a String 28 | pub fn from_string(s: String) -> UncheckedUrl { 29 | UncheckedUrl(s) 30 | } 31 | 32 | /// As &str 33 | pub fn as_str(&self) -> &str { 34 | &self.0 35 | } 36 | 37 | /// As nrelay 38 | pub fn as_bech32_string(&self) -> String { 39 | bech32::encode::(*crate::HRP_NRELAY, self.0.as_bytes()).unwrap() 40 | } 41 | 42 | /// Import from a bech32 encoded string ("nrelay") 43 | pub fn try_from_bech32_string(s: &str) -> Result { 44 | let data = bech32::decode(s)?; 45 | if data.0 != *crate::HRP_NRELAY { 46 | Err(Error::WrongBech32( 47 | crate::HRP_NRELAY.to_lowercase(), 48 | data.0.to_lowercase(), 49 | )) 50 | } else { 51 | let s = std::str::from_utf8(&data.1)?.to_owned(); 52 | Ok(UncheckedUrl(s)) 53 | } 54 | } 55 | 56 | // Mock data for testing 57 | #[allow(dead_code)] 58 | pub(crate) fn mock() -> UncheckedUrl { 59 | UncheckedUrl("/home/user/file.txt".to_string()) 60 | } 61 | } 62 | 63 | /// A String representing a valid URL with an authority present including an 64 | /// Internet based host. 65 | /// 66 | /// We don't serialize/deserialize these directly, see `UncheckedUrl` for that 67 | #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 68 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 69 | pub struct Url(String); 70 | 71 | impl fmt::Display for Url { 72 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 73 | write!(f, "{}", self.0) 74 | } 75 | } 76 | 77 | impl Url { 78 | /// Create a new Url from an UncheckedUrl 79 | pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result { 80 | Url::try_from_str(&u.0) 81 | } 82 | 83 | /// Create a new Url from a string 84 | pub fn try_from_str(s: &str) -> Result { 85 | // We use the url crate to parse and normalize 86 | let url = url::Url::parse(s.trim())?; 87 | 88 | if !url.has_authority() { 89 | return Err(Error::InvalidUrlMissingAuthority); 90 | } 91 | 92 | if let Some(host) = url.host() { 93 | match host { 94 | url::Host::Domain(_) => { 95 | // Strange that we can't access as a string 96 | let s = format!("{host}"); 97 | if s != s.trim() || s.starts_with("localhost") { 98 | return Err(Error::InvalidUrlHost(s)); 99 | } 100 | } 101 | url::Host::Ipv4(addr) => { 102 | let addrx = core_net::Ipv4Addr::from(addr.octets()); 103 | if !addrx.is_global() { 104 | return Err(Error::InvalidUrlHost(format!("{host}"))); 105 | } 106 | } 107 | url::Host::Ipv6(addr) => { 108 | let addrx = core_net::Ipv6Addr::from(addr.octets()); 109 | if !addrx.is_global() { 110 | return Err(Error::InvalidUrlHost(format!("{host}"))); 111 | } 112 | } 113 | } 114 | } else { 115 | return Err(Error::InvalidUrlHost("".to_string())); 116 | } 117 | 118 | Ok(Url(url.as_str().to_owned())) 119 | } 120 | 121 | /// Convert into a UncheckedUrl 122 | pub fn to_unchecked_url(&self) -> UncheckedUrl { 123 | UncheckedUrl(self.0.clone()) 124 | } 125 | 126 | /// As &str 127 | pub fn as_str(&self) -> &str { 128 | &self.0 129 | } 130 | 131 | /// Into String 132 | pub fn into_string(self) -> String { 133 | self.0 134 | } 135 | 136 | /// As url crate Url 137 | pub fn as_url_crate_url(&self) -> url::Url { 138 | url::Url::parse(&self.0).unwrap() 139 | } 140 | 141 | // Mock data for testing 142 | #[allow(dead_code)] 143 | pub(crate) fn mock() -> Url { 144 | Url("http://example.com/avatar.png".to_string()) 145 | } 146 | } 147 | 148 | /// A Url validated as a nostr relay url in canonical form 149 | /// We don't serialize/deserialize these directly, see `UncheckedUrl` for that 150 | #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 151 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 152 | pub struct RelayUrl(String); 153 | 154 | impl fmt::Display for RelayUrl { 155 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 156 | write!(f, "{}", self.0) 157 | } 158 | } 159 | 160 | impl RelayUrl { 161 | /// Create a new RelayUrl from a Url 162 | pub fn try_from_url(u: &Url) -> Result { 163 | // Verify we aren't looking at a comma-separated-list of URLs 164 | // (technically they might be valid URLs but just about 100% of the time 165 | // it's somebody else's bad data) 166 | if u.0.contains(",wss://") || u.0.contains(",ws://") { 167 | return Err(Error::Url(format!( 168 | "URL appears to be a list of multiple URLs: {}", 169 | u.0 170 | ))); 171 | } 172 | 173 | let url = url::Url::parse(&u.0)?; 174 | 175 | // Verify the scheme is websockets 176 | if url.scheme() != "wss" && url.scheme() != "ws" { 177 | return Err(Error::InvalidUrlScheme(url.scheme().to_owned())); 178 | } 179 | 180 | // Verify host is some 181 | if !url.has_host() { 182 | return Err(Error::Url(format!("URL has no host: {}", u.0))); 183 | } 184 | 185 | Ok(RelayUrl(url.as_str().to_owned())) 186 | } 187 | 188 | /// Create a new RelayUrl from an UncheckedUrl 189 | pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result { 190 | Self::try_from_str(&u.0) 191 | } 192 | 193 | /// Construct a new RelayUrl from a Url 194 | pub fn try_from_str(s: &str) -> Result { 195 | let url = Url::try_from_str(s)?; 196 | RelayUrl::try_from_url(&url) 197 | } 198 | 199 | /// Convert into a Url 200 | // fixme should be 'as_url' 201 | pub fn to_url(&self) -> Url { 202 | Url(self.0.clone()) 203 | } 204 | 205 | /// As url crate Url 206 | pub fn as_url_crate_url(&self) -> url::Url { 207 | url::Url::parse(&self.0).unwrap() 208 | } 209 | 210 | /// As nrelay 211 | pub fn as_bech32_string(&self) -> String { 212 | bech32::encode::(*crate::HRP_NRELAY, self.0.as_bytes()).unwrap() 213 | } 214 | 215 | /// Convert into a UncheckedUrl 216 | pub fn to_unchecked_url(&self) -> UncheckedUrl { 217 | UncheckedUrl(self.0.clone()) 218 | } 219 | 220 | /// Host 221 | pub fn host(&self) -> String { 222 | self.as_url_crate_url().host_str().unwrap().to_owned() 223 | } 224 | 225 | /// As &str 226 | pub fn as_str(&self) -> &str { 227 | &self.0 228 | } 229 | 230 | /// Into String 231 | pub fn into_string(self) -> String { 232 | self.0 233 | } 234 | 235 | // Mock data for testing 236 | #[allow(dead_code)] 237 | pub(crate) fn mock() -> Url { 238 | Url("wss://example.com".to_string()) 239 | } 240 | } 241 | 242 | impl TryFrom for RelayUrl { 243 | type Error = Error; 244 | 245 | fn try_from(u: Url) -> Result { 246 | RelayUrl::try_from_url(&u) 247 | } 248 | } 249 | 250 | impl TryFrom<&Url> for RelayUrl { 251 | type Error = Error; 252 | 253 | fn try_from(u: &Url) -> Result { 254 | RelayUrl::try_from_url(u) 255 | } 256 | } 257 | 258 | impl From for Url { 259 | fn from(ru: RelayUrl) -> Url { 260 | ru.to_url() 261 | } 262 | } 263 | 264 | /// A canonical URL representing just a relay's origin 265 | /// (without path/query/fragment or username/password) 266 | #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize, Ord)] 267 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 268 | pub struct RelayOrigin(String); 269 | 270 | impl RelayOrigin { 271 | /// Convert a RelayUrl into a RelayOrigin 272 | pub fn from_relay_url(url: RelayUrl) -> RelayOrigin { 273 | let mut xurl = url::Url::parse(url.as_str()).unwrap(); 274 | xurl.set_fragment(None); 275 | xurl.set_query(None); 276 | xurl.set_path("/"); 277 | let _ = xurl.set_username(""); 278 | let _ = xurl.set_password(None); 279 | RelayOrigin(xurl.into()) 280 | } 281 | 282 | /// Construct a new RelayOrigin from a string 283 | pub fn try_from_str(s: &str) -> Result { 284 | let url = RelayUrl::try_from_str(s)?; 285 | Ok(RelayOrigin::from_relay_url(url)) 286 | } 287 | 288 | /// Create a new Url from an UncheckedUrl 289 | pub fn try_from_unchecked_url(u: &UncheckedUrl) -> Result { 290 | let relay_url = RelayUrl::try_from_str(&u.0)?; 291 | Ok(relay_url.into()) 292 | } 293 | 294 | /// Convert this RelayOrigin into a RelayUrl 295 | pub fn into_relay_url(self) -> RelayUrl { 296 | RelayUrl(self.0) 297 | } 298 | 299 | /// Get a RelayUrl matching this RelayOrigin 300 | pub fn as_relay_url(&self) -> RelayUrl { 301 | RelayUrl(self.0.clone()) 302 | } 303 | 304 | /// Convert into a UncheckedUrl 305 | pub fn to_unchecked_url(&self) -> UncheckedUrl { 306 | UncheckedUrl(self.0.clone()) 307 | } 308 | 309 | /// As &str 310 | pub fn as_str(&self) -> &str { 311 | &self.0 312 | } 313 | 314 | /// Into String 315 | pub fn into_string(self) -> String { 316 | self.0 317 | } 318 | } 319 | 320 | impl fmt::Display for RelayOrigin { 321 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 322 | write!(f, "{}", self.0) 323 | } 324 | } 325 | 326 | impl From for RelayOrigin { 327 | fn from(ru: RelayUrl) -> RelayOrigin { 328 | RelayOrigin::from_relay_url(ru) 329 | } 330 | } 331 | 332 | impl From for RelayUrl { 333 | fn from(ru: RelayOrigin) -> RelayUrl { 334 | ru.into_relay_url() 335 | } 336 | } 337 | 338 | #[cfg(test)] 339 | mod test { 340 | use super::*; 341 | 342 | test_serde! {UncheckedUrl, test_unchecked_url_serde} 343 | 344 | #[test] 345 | fn test_url_case() { 346 | let url = Url::try_from_str("Wss://MyRelay.example.COM/PATH?Query").unwrap(); 347 | assert_eq!(url.as_str(), "wss://myrelay.example.com/PATH?Query"); 348 | } 349 | 350 | #[test] 351 | fn test_relay_url_slash() { 352 | let input = "Wss://MyRelay.example.COM"; 353 | let url = RelayUrl::try_from_str(input).unwrap(); 354 | assert_eq!(url.as_str(), "wss://myrelay.example.com/"); 355 | } 356 | 357 | #[test] 358 | fn test_relay_origin() { 359 | let input = "wss://user:pass@filter.nostr.wine:444/npub1234?x=y#z"; 360 | let relay_url = RelayUrl::try_from_str(input).unwrap(); 361 | let origin: RelayOrigin = relay_url.into(); 362 | assert_eq!(origin.as_str(), "wss://filter.nostr.wine:444/"); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/versioned/metadata1.rs: -------------------------------------------------------------------------------- 1 | use serde::de::{Deserialize, Deserializer, MapAccess, Visitor}; 2 | use serde::ser::{Serialize, SerializeMap, Serializer}; 3 | use serde_json::{json, Map, Value}; 4 | use std::fmt; 5 | 6 | /// Metadata about a user 7 | /// 8 | /// Note: the value is an Option because some real-world data has been found to 9 | /// contain JSON nulls as values, and we don't want deserialization of those 10 | /// events to fail. We treat these in our get() function the same as if the key 11 | /// did not exist. 12 | #[derive(Clone, Debug, Eq, PartialEq)] 13 | pub struct MetadataV1 { 14 | /// username 15 | pub name: Option, 16 | 17 | /// about 18 | pub about: Option, 19 | 20 | /// picture URL 21 | pub picture: Option, 22 | 23 | /// nip05 dns id 24 | pub nip05: Option, 25 | 26 | /// Additional fields not specified in NIP-01 or NIP-05 27 | pub other: Map, 28 | } 29 | 30 | impl Default for MetadataV1 { 31 | fn default() -> Self { 32 | MetadataV1 { 33 | name: None, 34 | about: None, 35 | picture: None, 36 | nip05: None, 37 | other: Map::new(), 38 | } 39 | } 40 | } 41 | 42 | impl MetadataV1 { 43 | /// Create new empty Metadata 44 | pub fn new() -> MetadataV1 { 45 | MetadataV1::default() 46 | } 47 | 48 | #[allow(dead_code)] 49 | pub(crate) fn mock() -> MetadataV1 { 50 | let mut map = Map::new(); 51 | let _ = map.insert( 52 | "display_name".to_string(), 53 | Value::String("William Caserin".to_string()), 54 | ); 55 | MetadataV1 { 56 | name: Some("jb55".to_owned()), 57 | about: None, 58 | picture: None, 59 | nip05: Some("jb55.com".to_owned()), 60 | other: map, 61 | } 62 | } 63 | 64 | /// Get the lnurl for the user, if available via lud06 or lud16 65 | pub fn lnurl(&self) -> Option { 66 | if let Some(Value::String(lud06)) = self.other.get("lud06") { 67 | if let Ok(data) = bech32::decode(lud06) { 68 | if data.0 == *crate::HRP_LNURL { 69 | return Some(String::from_utf8_lossy(&data.1).to_string()); 70 | } 71 | } 72 | } 73 | 74 | if let Some(Value::String(lud16)) = self.other.get("lud16") { 75 | let vec: Vec<&str> = lud16.split('@').collect(); 76 | if vec.len() == 2 { 77 | let user = &vec[0]; 78 | let domain = &vec[1]; 79 | return Some(format!("https://{domain}/.well-known/lnurlp/{user}")); 80 | } 81 | } 82 | 83 | None 84 | } 85 | } 86 | 87 | impl Serialize for MetadataV1 { 88 | fn serialize(&self, serializer: S) -> Result 89 | where 90 | S: Serializer, 91 | { 92 | let mut map = serializer.serialize_map(Some(4 + self.other.len()))?; 93 | map.serialize_entry("name", &json!(&self.name))?; 94 | map.serialize_entry("about", &json!(&self.about))?; 95 | map.serialize_entry("picture", &json!(&self.picture))?; 96 | map.serialize_entry("nip05", &json!(&self.nip05))?; 97 | for (k, v) in &self.other { 98 | map.serialize_entry(&k, &v)?; 99 | } 100 | map.end() 101 | } 102 | } 103 | 104 | impl<'de> Deserialize<'de> for MetadataV1 { 105 | fn deserialize(deserializer: D) -> Result 106 | where 107 | D: Deserializer<'de>, 108 | { 109 | deserializer.deserialize_map(MetadataV1Visitor) 110 | } 111 | } 112 | 113 | struct MetadataV1Visitor; 114 | 115 | impl<'de> Visitor<'de> for MetadataV1Visitor { 116 | type Value = MetadataV1; 117 | 118 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 119 | write!(f, "A JSON object") 120 | } 121 | 122 | fn visit_map(self, mut access: M) -> Result 123 | where 124 | M: MapAccess<'de>, 125 | { 126 | let mut map: Map = Map::new(); 127 | while let Some((key, value)) = access.next_entry::()? { 128 | let _ = map.insert(key, value); 129 | } 130 | 131 | let mut m: MetadataV1 = Default::default(); 132 | 133 | if let Some(Value::String(s)) = map.remove("name") { 134 | m.name = Some(s); 135 | } 136 | if let Some(Value::String(s)) = map.remove("about") { 137 | m.about = Some(s); 138 | } 139 | if let Some(Value::String(s)) = map.remove("picture") { 140 | m.picture = Some(s); 141 | } 142 | if let Some(Value::String(s)) = map.remove("nip05") { 143 | m.nip05 = Some(s); 144 | } 145 | 146 | m.other = map; 147 | 148 | Ok(m) 149 | } 150 | } 151 | 152 | #[cfg(test)] 153 | mod test { 154 | use super::*; 155 | 156 | test_serde! {MetadataV1, test_metadata_serde} 157 | 158 | #[test] 159 | fn test_metadata_print_json() { 160 | // I want to see if JSON serialized metadata is network appropriate 161 | let m = MetadataV1::mock(); 162 | println!("{}", serde_json::to_string(&m).unwrap()); 163 | } 164 | 165 | #[test] 166 | fn test_tolerate_nulls() { 167 | let json = r##"{"name":"monlovesmango","picture":"https://astral.ninja/aura/monlovesmango.svg","about":"building on nostr","nip05":"monlovesmango@astral.ninja","lud06":null,"testing":"123"}"##; 168 | let m: MetadataV1 = serde_json::from_str(json).unwrap(); 169 | assert_eq!(m.name, Some("monlovesmango".to_owned())); 170 | assert_eq!(m.other.get("lud06"), Some(&Value::Null)); 171 | assert_eq!( 172 | m.other.get("testing"), 173 | Some(&Value::String("123".to_owned())) 174 | ); 175 | } 176 | 177 | #[test] 178 | fn test_metadata_lnurls() { 179 | // test lud06 180 | let json = r##"{"name":"mikedilger","about":"Author of Gossip client: https://github.com/mikedilger/gossip\nexpat American living in New Zealand","picture":"https://avatars.githubusercontent.com/u/1669069","nip05":"_@mikedilger.com","banner":"https://mikedilger.com/banner.jpg","display_name":"Michael Dilger","location":"New Zealand","lud06":"lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkgetrv4h8gcn4dccnxv563ep","website":"https://mikedilger.com"}"##; 181 | let m: MetadataV1 = serde_json::from_str(json).unwrap(); 182 | assert_eq!( 183 | m.lnurl().as_deref(), 184 | Some("https://walletofsatoshi.com/.well-known/lnurlp/decentbun13") 185 | ); 186 | 187 | // test lud16 188 | let json = r##"{"name":"mikedilger","about":"Author of Gossip client: https://github.com/mikedilger/gossip\nexpat American living in New Zealand","picture":"https://avatars.githubusercontent.com/u/1669069","nip05":"_@mikedilger.com","banner":"https://mikedilger.com/banner.jpg","display_name":"Michael Dilger","location":"New Zealand","lud16":"decentbun13@walletofsatoshi.com","website":"https://mikedilger.com"}"##; 189 | let m: MetadataV1 = serde_json::from_str(json).unwrap(); 190 | assert_eq!( 191 | m.lnurl().as_deref(), 192 | Some("https://walletofsatoshi.com/.well-known/lnurlp/decentbun13") 193 | ); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/versioned/metadata2.rs: -------------------------------------------------------------------------------- 1 | use serde::de::{Deserialize, Deserializer, MapAccess, Visitor}; 2 | use serde::ser::{Serialize, SerializeMap, Serializer}; 3 | use serde_json::{json, Map, Value}; 4 | use std::fmt; 5 | 6 | /// Metadata about a user 7 | /// 8 | /// Note: the value is an Option because some real-world data has been found to 9 | /// contain JSON nulls as values, and we don't want deserialization of those 10 | /// events to fail. We treat these in our get() function the same as if the key 11 | /// did not exist. 12 | #[derive(Clone, Debug, Eq, PartialEq)] 13 | pub struct MetadataV2 { 14 | /// username 15 | pub name: Option, 16 | 17 | /// about 18 | pub about: Option, 19 | 20 | /// picture URL 21 | pub picture: Option, 22 | 23 | /// nip05 dns id 24 | pub nip05: Option, 25 | 26 | /// fields 27 | pub fields: Vec<(String, String)>, 28 | 29 | /// Additional fields not specified in NIP-01 or NIP-05 30 | pub other: Map, 31 | } 32 | 33 | impl Default for MetadataV2 { 34 | fn default() -> Self { 35 | MetadataV2 { 36 | name: None, 37 | about: None, 38 | picture: None, 39 | nip05: None, 40 | fields: Vec::new(), 41 | other: Map::new(), 42 | } 43 | } 44 | } 45 | 46 | impl MetadataV2 { 47 | /// Create new empty Metadata 48 | pub fn new() -> MetadataV2 { 49 | MetadataV2::default() 50 | } 51 | 52 | #[allow(dead_code)] 53 | pub(crate) fn mock() -> MetadataV2 { 54 | let mut map = Map::new(); 55 | let _ = map.insert( 56 | "display_name".to_string(), 57 | Value::String("William Caserin".to_string()), 58 | ); 59 | MetadataV2 { 60 | name: Some("jb55".to_owned()), 61 | about: None, 62 | picture: None, 63 | nip05: Some("jb55.com".to_owned()), 64 | fields: vec![("Pronouns".to_owned(), "ye/haw".to_owned())], 65 | other: map, 66 | } 67 | } 68 | 69 | /// Get the lnurl for the user, if available via lud06 or lud16 70 | pub fn lnurl(&self) -> Option { 71 | if let Some(Value::String(lud06)) = self.other.get("lud06") { 72 | if let Ok(data) = bech32::decode(lud06) { 73 | if data.0 == *crate::HRP_LNURL { 74 | return Some(String::from_utf8_lossy(&data.1).to_string()); 75 | } 76 | } 77 | } 78 | 79 | if let Some(Value::String(lud16)) = self.other.get("lud16") { 80 | let vec: Vec<&str> = lud16.split('@').collect(); 81 | if vec.len() == 2 { 82 | let user = &vec[0]; 83 | let domain = &vec[1]; 84 | return Some(format!("https://{domain}/.well-known/lnurlp/{user}")); 85 | } 86 | } 87 | 88 | None 89 | } 90 | } 91 | 92 | impl Serialize for MetadataV2 { 93 | fn serialize(&self, serializer: S) -> Result 94 | where 95 | S: Serializer, 96 | { 97 | let mut map = serializer.serialize_map(Some(5 + self.other.len()))?; 98 | map.serialize_entry("name", &json!(&self.name))?; 99 | map.serialize_entry("about", &json!(&self.about))?; 100 | map.serialize_entry("picture", &json!(&self.picture))?; 101 | map.serialize_entry("nip05", &json!(&self.nip05))?; 102 | 103 | let mut fields_as_vector: Vec> = Vec::new(); 104 | for pair in &self.fields { 105 | fields_as_vector.push(vec![pair.0.clone(), pair.1.clone()]); 106 | } 107 | map.serialize_entry("fields", &json!(&fields_as_vector))?; 108 | 109 | for (k, v) in &self.other { 110 | map.serialize_entry(&k, &v)?; 111 | } 112 | map.end() 113 | } 114 | } 115 | 116 | impl<'de> Deserialize<'de> for MetadataV2 { 117 | fn deserialize(deserializer: D) -> Result 118 | where 119 | D: Deserializer<'de>, 120 | { 121 | deserializer.deserialize_map(MetadataV2Visitor) 122 | } 123 | } 124 | 125 | struct MetadataV2Visitor; 126 | 127 | impl<'de> Visitor<'de> for MetadataV2Visitor { 128 | type Value = MetadataV2; 129 | 130 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 131 | write!(f, "A JSON object") 132 | } 133 | 134 | fn visit_map(self, mut access: M) -> Result 135 | where 136 | M: MapAccess<'de>, 137 | { 138 | let mut map: Map = Map::new(); 139 | while let Some((key, value)) = access.next_entry::()? { 140 | let _ = map.insert(key, value); 141 | } 142 | 143 | let mut m: MetadataV2 = Default::default(); 144 | 145 | if let Some(Value::String(s)) = map.remove("name") { 146 | m.name = Some(s); 147 | } 148 | if let Some(Value::String(s)) = map.remove("about") { 149 | m.about = Some(s); 150 | } 151 | if let Some(Value::String(s)) = map.remove("picture") { 152 | m.picture = Some(s); 153 | } 154 | if let Some(Value::String(s)) = map.remove("nip05") { 155 | m.nip05 = Some(s); 156 | } 157 | if let Some(Value::Array(v)) = map.remove("fields") { 158 | for elem in v { 159 | if let Value::Array(v2) = elem { 160 | if v2.len() == 2 { 161 | if let (Value::String(s1), Value::String(s2)) = (&v2[0], &v2[1]) { 162 | m.fields.push((s1.to_owned(), s2.to_owned())); 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | m.other = map; 170 | 171 | Ok(m) 172 | } 173 | } 174 | 175 | #[cfg(test)] 176 | mod test { 177 | use super::*; 178 | 179 | test_serde! {MetadataV2, test_metadata_serde} 180 | 181 | #[test] 182 | fn test_metadata_print_json() { 183 | // I want to see if JSON serialized metadata is network appropriate 184 | let m = MetadataV2::mock(); 185 | println!("{}", serde_json::to_string(&m).unwrap()); 186 | } 187 | 188 | #[test] 189 | fn test_tolerate_nulls() { 190 | let json = r##"{"name":"monlovesmango","picture":"https://astral.ninja/aura/monlovesmango.svg","about":"building on nostr","nip05":"monlovesmango@astral.ninja","lud06":null,"testing":"123"}"##; 191 | let m: MetadataV2 = serde_json::from_str(json).unwrap(); 192 | assert_eq!(m.name, Some("monlovesmango".to_owned())); 193 | assert_eq!(m.other.get("lud06"), Some(&Value::Null)); 194 | assert_eq!( 195 | m.other.get("testing"), 196 | Some(&Value::String("123".to_owned())) 197 | ); 198 | } 199 | 200 | #[test] 201 | fn test_metadata_lnurls() { 202 | // test lud06 203 | let json = r##"{"name":"mikedilger","about":"Author of Gossip client: https://github.com/mikedilger/gossip\nexpat American living in New Zealand","picture":"https://avatars.githubusercontent.com/u/1669069","nip05":"_@mikedilger.com","banner":"https://mikedilger.com/banner.jpg","display_name":"Michael Dilger","location":"New Zealand","lud06":"lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkgetrv4h8gcn4dccnxv563ep","website":"https://mikedilger.com"}"##; 204 | let m: MetadataV2 = serde_json::from_str(json).unwrap(); 205 | assert_eq!( 206 | m.lnurl().as_deref(), 207 | Some("https://walletofsatoshi.com/.well-known/lnurlp/decentbun13") 208 | ); 209 | 210 | // test lud16 211 | let json = r##"{"name":"mikedilger","about":"Author of Gossip client: https://github.com/mikedilger/gossip\nexpat American living in New Zealand","picture":"https://avatars.githubusercontent.com/u/1669069","nip05":"_@mikedilger.com","banner":"https://mikedilger.com/banner.jpg","display_name":"Michael Dilger","location":"New Zealand","lud16":"decentbun13@walletofsatoshi.com","website":"https://mikedilger.com"}"##; 212 | let m: MetadataV2 = serde_json::from_str(json).unwrap(); 213 | assert_eq!( 214 | m.lnurl().as_deref(), 215 | Some("https://walletofsatoshi.com/.well-known/lnurlp/decentbun13") 216 | ); 217 | } 218 | 219 | #[test] 220 | fn test_metadata_fields() { 221 | let json = r##"{ 222 | "name": "Alex", 223 | "picture": "https://...", 224 | "fields": [ 225 | ["Pronouns", "ye/haw"], 226 | ["Lifestyle", "vegan"], 227 | ["Color", "green"] 228 | ] 229 | }"##; 230 | 231 | let m: MetadataV2 = serde_json::from_str(json).unwrap(); 232 | println!("{:?}", m); 233 | assert_eq!(m.fields[0], ("Pronouns".to_string(), "ye/haw".to_string())); 234 | assert_eq!(m.fields[2], ("Color".to_string(), "green".to_string())); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/versioned/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod event3; 2 | pub use event3::{EventV3, PreEventV3, RumorV3}; 3 | 4 | pub(crate) mod filter1; 5 | pub use filter1::FilterV1; 6 | 7 | pub(crate) mod filter2; 8 | pub use filter2::FilterV2; 9 | 10 | pub(crate) mod metadata1; 11 | pub use metadata1::MetadataV1; 12 | 13 | pub(crate) mod metadata2; 14 | pub use metadata2::MetadataV2; 15 | 16 | pub(crate) mod nip05; 17 | pub use nip05::Nip05V1; 18 | 19 | pub(crate) mod relay_information_document1; 20 | pub use relay_information_document1::{ 21 | FeeV1, RelayFeesV1, RelayInformationDocumentV1, RelayLimitationV1, RelayRetentionV1, 22 | }; 23 | pub(crate) mod relay_information_document2; 24 | pub use relay_information_document2::{RelayInformationDocumentV2, RelayLimitationV2}; 25 | 26 | pub(crate) mod tag3; 27 | pub use tag3::TagV3; 28 | 29 | pub(crate) mod zap_data; 30 | pub use zap_data::{ZapDataV1, ZapDataV2}; 31 | -------------------------------------------------------------------------------- /src/versioned/nip05.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{PublicKeyHex, UncheckedUrl}; 2 | use serde::{Deserialize, Serialize}; 3 | #[cfg(feature = "speedy")] 4 | use speedy::{Readable, Writable}; 5 | use std::collections::HashMap; 6 | 7 | /// The content of a webserver's /.well-known/nostr.json file used in NIP-05 and NIP-35 8 | /// This allows lookup and verification of a nostr user via a `user@domain` style identifier. 9 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 10 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 11 | pub struct Nip05V1 { 12 | /// DNS names mapped to public keys 13 | pub names: HashMap, 14 | 15 | /// Public keys mapped to arrays of relays where they post 16 | #[serde(skip_serializing_if = "HashMap::is_empty")] 17 | #[serde(default)] 18 | pub relays: HashMap>, 19 | } 20 | 21 | impl Nip05V1 { 22 | // Mock data for testing 23 | #[allow(dead_code)] 24 | pub(crate) fn mock() -> Nip05V1 { 25 | let pubkey = PublicKeyHex::try_from_str( 26 | "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", 27 | ) 28 | .unwrap(); 29 | 30 | let mut names: HashMap = HashMap::new(); 31 | let _ = names.insert("bob".to_string(), pubkey.clone()); 32 | 33 | let mut relays: HashMap> = HashMap::new(); 34 | let _ = relays.insert( 35 | pubkey, 36 | vec![ 37 | UncheckedUrl::from_str("wss://relay.example.com"), 38 | UncheckedUrl::from_str("wss://relay2.example.com"), 39 | ], 40 | ); 41 | 42 | Nip05V1 { names, relays } 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod test { 48 | use super::*; 49 | 50 | test_serde! {Nip05V1, test_nip05_serde} 51 | 52 | #[test] 53 | fn test_nip05_example() { 54 | let body = r#"{ 55 | "names": { 56 | "bob": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9" 57 | }, 58 | "relays": { 59 | "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9": [ "wss://relay.example.com", "wss://relay2.example.com" ] 60 | } 61 | }"#; 62 | 63 | let nip05: Nip05V1 = serde_json::from_str(body).unwrap(); 64 | 65 | let bobs_pk: PublicKeyHex = nip05.names.get("bob").unwrap().clone(); 66 | assert_eq!( 67 | bobs_pk.as_str(), 68 | "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9" 69 | ); 70 | 71 | let bobs_relays: Vec = nip05.relays.get(&bobs_pk).unwrap().to_owned(); 72 | 73 | assert_eq!( 74 | bobs_relays, 75 | vec![ 76 | UncheckedUrl::from_str("wss://relay.example.com"), 77 | UncheckedUrl::from_str("wss://relay2.example.com") 78 | ] 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/versioned/relay_list.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikedilger/nostr-types/a5474784e860c193cac2ee2280d66417f9d2e389/src/versioned/relay_list.rs -------------------------------------------------------------------------------- /src/versioned/tag3.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{ParsedTag, UncheckedUrl}; 2 | use crate::Error; 3 | use serde::{Deserialize, Serialize}; 4 | #[cfg(feature = "speedy")] 5 | use speedy::{Readable, Writable}; 6 | 7 | /// A tag on an Event 8 | #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] 9 | #[cfg_attr(feature = "speedy", derive(Readable, Writable))] 10 | pub struct TagV3(Vec); 11 | 12 | impl TagV3 { 13 | const EMPTY_STRING: &'static str = ""; 14 | 15 | /// Create a new tag 16 | pub fn new(fields: &[&str]) -> TagV3 { 17 | TagV3(fields.iter().map(|f| (*f).to_owned()).collect()) 18 | } 19 | 20 | /// Create a new tag without copying 21 | pub fn from_strings(fields: Vec) -> TagV3 { 22 | TagV3(fields) 23 | } 24 | 25 | /// Remove empty fields from the end 26 | pub fn trim(&mut self) { 27 | while self.0[self.len() - 1].is_empty() { 28 | let _ = self.0.pop(); 29 | } 30 | } 31 | 32 | /// Into a `Vec` 33 | pub fn into_inner(self) -> Vec { 34 | self.0 35 | } 36 | 37 | /// Number of string fields in the tag 38 | pub fn len(&self) -> usize { 39 | self.0.len() 40 | } 41 | 42 | /// Is the tag empty 43 | pub fn is_empty(&self) -> bool { 44 | self.len() == 0 45 | } 46 | 47 | /// Get the string at the given index 48 | pub fn get_index(&self, index: usize) -> &str { 49 | if self.len() > index { 50 | &self.0[index] 51 | } else { 52 | Self::EMPTY_STRING 53 | } 54 | } 55 | 56 | /// Get the string at the given index, None if beyond length or empty 57 | pub fn get_opt_index(&self, i: usize) -> Option<&str> { 58 | if self.0.len() <= i { 59 | None 60 | } else { 61 | let s = self.get_index(i); 62 | if s.is_empty() { 63 | None 64 | } else { 65 | Some(s) 66 | } 67 | } 68 | } 69 | 70 | /// Set the string at the given index 71 | pub fn set_index(&mut self, index: usize, value: String) { 72 | while self.len() <= index { 73 | self.0.push("".to_owned()); 74 | } 75 | self.0[index] = value; 76 | } 77 | 78 | /// Push another values onto the tag 79 | pub fn push_value(&mut self, value: String) { 80 | self.0.push(value); 81 | } 82 | 83 | /// Push more values onto the tag 84 | pub fn push_values(&mut self, mut values: Vec) { 85 | for value in values.drain(..) { 86 | self.0.push(value); 87 | } 88 | } 89 | 90 | /// Get the tag name for the tag (the first string in the array) 91 | pub fn tagname(&self) -> &str { 92 | self.get_index(0) 93 | } 94 | 95 | /// Get the tag value (index 1, after the tag name) 96 | pub fn value(&self) -> &str { 97 | self.get_index(1) 98 | } 99 | 100 | /// Get the marker (if relevant), else "" 101 | pub fn marker(&self) -> &str { 102 | if self.tagname() == "e" || self.tagname() == "a" { 103 | self.get_index(3) 104 | } else { 105 | Self::EMPTY_STRING 106 | } 107 | } 108 | 109 | // Mock data for testing 110 | #[allow(dead_code)] 111 | pub(crate) fn mock() -> TagV3 { 112 | TagV3(vec!["e".to_string(), UncheckedUrl::mock().0]) 113 | } 114 | 115 | /// Parse into a ParsedTag 116 | pub fn parse(&self) -> Result { 117 | ParsedTag::parse(self) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/versioned/zap_data.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{EventReference, Id, MilliSatoshi, PublicKey}; 2 | 3 | /// Data about a Zap 4 | #[derive(Clone, Debug)] 5 | pub struct ZapDataV2 { 6 | /// The event that was zapped. If missing we can't use the zap receipt event. 7 | pub zapped_event: EventReference, 8 | 9 | /// The amount that the event was zapped 10 | pub amount: MilliSatoshi, 11 | 12 | /// The public key of the person who received the zap 13 | pub payee: PublicKey, 14 | 15 | /// The public key of the person who paid the zap, if it was in the receipt 16 | pub payer: PublicKey, 17 | 18 | /// The public key of the zap provider, for verification purposes 19 | pub provider_pubkey: PublicKey, 20 | } 21 | 22 | /// Data about a Zap 23 | #[derive(Clone, Debug, Copy)] 24 | pub struct ZapDataV1 { 25 | /// The event that was zapped 26 | pub id: Id, 27 | 28 | /// The amount that the event was zapped 29 | pub amount: MilliSatoshi, 30 | 31 | /// The public key of the person who provided the zap 32 | pub pubkey: PublicKey, 33 | 34 | /// The public key of the zap provider, for verification purposes 35 | pub provider_pubkey: PublicKey, 36 | } 37 | -------------------------------------------------------------------------------- /versioning-plan.txt: -------------------------------------------------------------------------------- 1 | So simple they will never change 2 | UncheckedUrl 3 | Url 4 | RelayUrl 5 | Unixtime 6 | SubscriptionId 7 | MilliSatoshi 8 | 9 | Very unlikely to need versioning due to being fixed by the protocol 10 | Signature (uses a type that is the raw sequence of bytes) 11 | PublicKey (is the raw bytes) 12 | PublicKeyHex 13 | Id (bytes) 14 | IdHex 15 | EventKind (just a u32) 16 | 17 | Never serialized 18 | Span 19 | ContentSegment 20 | ShatteredContent 21 | EventDelegation 22 | ZapData 23 | PayRequestData 24 | EventKindOrRange 25 | 26 | Dont version right now 27 | Filter (defined structure that only uses types that aren't versioned) 28 | EventAddr 29 | EventReference 30 | EventPointer 31 | Profile 32 | NostrBech32 33 | NostrUrl 34 | DelegationConditions 35 | 36 | Versioned: 37 | RelayLimitation 38 | RelayRetention 39 | Fee 40 | RelayFees 41 | RelayInformationDocument 42 | SimpleRelayUsage 43 | SimpleRelayList 44 | Nip05 45 | Metadata 46 | Tag (due to changing fields in enum values) 47 | Rumor (due to Tag)- might not be serialized, but has speedy 48 | PreEvent (due to Tag)- might not be serialized, but has speedy 49 | Event (due to Tag) 50 | RelayMessage 51 | ClientMessage 52 | --------------------------------------------------------------------------------