mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
529 lines
21 KiB
Rust
529 lines
21 KiB
Rust
//! NIP-11 relay information document.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[cfg(test)]
|
|
use crate::config::DEFAULT_MAX_FRAME_BYTES;
|
|
|
|
/// NIPs unconditionally supported by this relay, advertised in the NIP-11
|
|
/// document. Kept as a module-level constant so tests can verify it without
|
|
/// constructing a full `Config` (which reads env vars and races with
|
|
/// config.rs tests).
|
|
///
|
|
/// NIP-43 (relay membership) is advertised separately by [`RelayInfo::build`]
|
|
/// only when membership enforcement is actually enabled — see that function.
|
|
pub(crate) const SUPPORTED_NIPS: &[u32] = &[1, 2, 10, 11, 16, 17, 23, 25, 29, 33, 38, 42, 50, 56];
|
|
|
|
/// NIP-43 (relay membership). Advertised only when the relay actually
|
|
/// enforces membership (`BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`) AND has a
|
|
/// stable signing key — both are required for kind 13534/8000/8001 events
|
|
/// to be verifiable by clients.
|
|
pub(crate) const NIP_RELAY_MEMBERSHIP: u32 = 43;
|
|
|
|
/// Relay information document served at `GET /` with `Accept: application/nostr+json`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RelayInfo {
|
|
/// Human-readable relay name.
|
|
pub name: String,
|
|
/// Human-readable relay description.
|
|
pub description: String,
|
|
/// Workspace icon URL (NIP-11 `icon`), per-community, set by relay
|
|
/// admins/owners via the kind:9033 command. Omitted when no icon is set.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub icon: Option<String>,
|
|
/// Relay operator's public key (hex), if published.
|
|
pub pubkey: Option<String>,
|
|
/// Contact address for the relay operator.
|
|
pub contact: Option<String>,
|
|
/// NIPs supported by this relay.
|
|
pub supported_nips: Vec<u32>,
|
|
/// Draft/extension protocol identifiers supported by this relay.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub supported_extensions: Option<Vec<String>>,
|
|
/// NIP-PL executor descriptor. Present only when push delivery is configured.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub push: Option<serde_json::Value>,
|
|
/// URL of the relay software repository.
|
|
pub software: String,
|
|
/// Relay software version string.
|
|
pub version: String,
|
|
/// Protocol and resource limits advertised to clients.
|
|
pub limitation: Option<RelayLimitation>,
|
|
/// Public WebSocket URL of the dedicated NIP-AB device-pairing relay.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub pairing_relay_url: Option<String>,
|
|
/// Relay's own signing pubkey (NIP-11 `self` field, NIP-43).
|
|
#[serde(rename = "self", skip_serializing_if = "Option::is_none")]
|
|
pub relay_self: Option<String>,
|
|
}
|
|
|
|
/// Protocol and resource limits advertised in the NIP-11 document.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RelayLimitation {
|
|
/// Maximum WebSocket frame size in bytes.
|
|
pub max_message_length: Option<u64>,
|
|
/// Maximum number of concurrent subscriptions per connection.
|
|
pub max_subscriptions: Option<u32>,
|
|
/// Maximum number of filters per subscription.
|
|
pub max_filters: Option<u32>,
|
|
/// Maximum value of the `limit` field in a filter.
|
|
pub max_limit: Option<u32>,
|
|
/// Maximum length of a subscription ID string.
|
|
pub max_subid_length: Option<u32>,
|
|
/// Minimum proof-of-work difficulty required for events.
|
|
pub min_pow_difficulty: Option<u32>,
|
|
/// Whether NIP-42 authentication is required before subscribing or
|
|
/// publishing events.
|
|
pub auth_required: bool,
|
|
/// Whether payment is required to use the relay.
|
|
pub payment_required: bool,
|
|
/// Whether writes are restricted to authorized pubkeys.
|
|
pub restricted_writes: bool,
|
|
/// NIP-ER: how the relay delivers due reminders ("push" or "lazy").
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub due_delivery_mode: Option<String>,
|
|
/// NIP-ER: maximum allowed `not_before` horizon in seconds from now.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub max_not_before_delta: Option<u64>,
|
|
}
|
|
|
|
/// Canonical `RelayLimitation` advertised by this relay.
|
|
///
|
|
/// `max_limit` is [`buzz_db::DEFAULT_MAX_PAGE_LIMIT`], the same constant the
|
|
/// REQ path clamps filter limits to, so the advertised ceiling and the
|
|
/// enforced one cannot drift (see
|
|
/// `handlers::req::tests::req_filter_limit_clamps_to_advertised_nip11_max_limit`).
|
|
///
|
|
/// `auth_required` is always `true`: the REQ, EVENT, and COUNT handlers
|
|
/// unconditionally reject connections that are not in
|
|
/// `AuthState::Authenticated`. This is independent of the REST API token
|
|
/// toggle (`config.require_auth_token`).
|
|
fn relay_limitation(max_message_length: usize) -> RelayLimitation {
|
|
let max_not_before_delta: u64 = std::env::var("SPROUT_MAX_NOT_BEFORE_DELTA")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(31_536_000); // 1 year default
|
|
|
|
RelayLimitation {
|
|
max_message_length: Some(max_message_length as u64),
|
|
max_subscriptions: Some(1024),
|
|
max_filters: Some(10),
|
|
max_limit: Some(buzz_db::DEFAULT_MAX_PAGE_LIMIT as u32),
|
|
max_subid_length: Some(256),
|
|
min_pow_difficulty: None,
|
|
auth_required: true,
|
|
payment_required: false,
|
|
restricted_writes: true,
|
|
due_delivery_mode: Some("push".to_string()),
|
|
max_not_before_delta: Some(max_not_before_delta),
|
|
}
|
|
}
|
|
|
|
impl RelayInfo {
|
|
/// Builds the relay's NIP-11 information document.
|
|
///
|
|
/// `relay_self` is the relay's own signing pubkey (hex), advertised as the
|
|
/// NIP-11 `self` field. NIP-11 defines `self` generically as the relay's
|
|
/// identity key; other NIPs reference it. Notably NIP-29 (group metadata
|
|
/// kinds 39000/39001/39002, which Buzz signs with `state.relay_keypair`
|
|
/// unconditionally) requires clients to verify those events against
|
|
/// `self`. Pass `Some` whenever the relay has a stable signing key.
|
|
///
|
|
/// `icon` is the community's workspace icon (see
|
|
/// [`workspace_icon_for_host`]) — a host-scoped scalar, pre-fetched by
|
|
/// the caller so `build` itself stays static-input.
|
|
///
|
|
/// `advertise_nip43` controls whether NIP-43 (relay membership) is added
|
|
/// to `supported_nips`. Set `true` only when the relay actually emits and
|
|
/// gates on NIP-43 events — i.e. has a stable key AND enforces
|
|
/// membership. NIP-43 events are verified against `self`, so it is a
|
|
/// programmer error to advertise NIP-43 without a `relay_self`.
|
|
pub fn build(
|
|
relay_self: Option<&str>,
|
|
icon: Option<&str>,
|
|
advertise_nip43: bool,
|
|
max_message_length: usize,
|
|
pairing_relay_url: Option<&str>,
|
|
) -> Self {
|
|
debug_assert!(
|
|
!advertise_nip43 || relay_self.is_some(),
|
|
"advertise_nip43=true requires relay_self=Some — NIP-43 events are verified against `self`"
|
|
);
|
|
|
|
let mut supported_nips = SUPPORTED_NIPS.to_vec();
|
|
if advertise_nip43 {
|
|
supported_nips.push(NIP_RELAY_MEMBERSHIP);
|
|
}
|
|
|
|
Self {
|
|
name: "Buzz Relay".to_string(),
|
|
description: "Buzz — private team communication relay".to_string(),
|
|
icon: icon.filter(|s| !s.is_empty()).map(|s| s.to_string()),
|
|
pubkey: None,
|
|
contact: None,
|
|
supported_nips,
|
|
supported_extensions: Some(vec!["nip-er".to_string()]),
|
|
push: None,
|
|
software: "https://github.com/block/buzz".to_string(),
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
limitation: Some(relay_limitation(max_message_length)),
|
|
pairing_relay_url: pairing_relay_url.map(str::to_string),
|
|
relay_self: relay_self.map(|s| s.to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Axum handler that returns the NIP-11 relay information document as JSON.
|
|
pub async fn relay_info_handler(
|
|
axum::extract::State(state): axum::extract::State<std::sync::Arc<crate::state::AppState>>,
|
|
headers: axum::http::HeaderMap,
|
|
) -> axum::response::Json<RelayInfo> {
|
|
let raw_host = headers
|
|
.get(axum::http::header::HOST)
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("");
|
|
axum::response::Json(nip11_document(&state, raw_host).await)
|
|
}
|
|
|
|
fn push_descriptor(
|
|
push_configured: bool,
|
|
relay_url: &str,
|
|
executor_key_id: &str,
|
|
relay_keypair: &nostr::Keys,
|
|
tenant_host: Option<&str>,
|
|
) -> Option<serde_json::Value> {
|
|
let host = tenant_host?;
|
|
push_configured.then_some(())?;
|
|
let scheme = if relay_url.starts_with("wss://") {
|
|
"wss"
|
|
} else {
|
|
"ws"
|
|
};
|
|
Some(serde_json::json!({
|
|
"origin": format!("{scheme}://{host}"),
|
|
"keys": [{
|
|
"id": executor_key_id,
|
|
"pubkey": relay_keypair.public_key().to_hex(),
|
|
"current": true
|
|
}],
|
|
"app_profiles": [
|
|
{"id": "buzz-ios-dogfood", "transport": "apns"},
|
|
{"id": "buzz-ios-app-store", "transport": "apns"}
|
|
],
|
|
"push_kinds": crate::handlers::push_lease::PUSH_KINDS,
|
|
"urgent_kinds": crate::handlers::push_lease::URGENT_KINDS,
|
|
"h_grammar": "uuid-v4-lowercase",
|
|
"class_support": {"apns": ["silent", "default", "time_sensitive"]},
|
|
"limitation": {
|
|
"max_lease_ttl": 2592000,
|
|
"max_leases_per_pubkey": 16,
|
|
"max_subscriptions_per_lease": 16,
|
|
"max_kinds": 16,
|
|
"max_authors": 20,
|
|
"max_h": 50,
|
|
"max_tag_values": 20,
|
|
"max_ignore": 8,
|
|
"max_content_len": 65536,
|
|
"max_plaintext_len": 32768,
|
|
"max_endpoint_len": 4096,
|
|
"max_string_len": 512
|
|
}
|
|
}))
|
|
}
|
|
|
|
/// Builds the served NIP-11 document for a request arriving on `raw_host`.
|
|
///
|
|
/// Centralised so the content-negotiated root handler and the dedicated
|
|
/// `/info` endpoint can't drift apart. Every input to `RelayInfo::build`
|
|
/// stays a pre-derived scalar: [`nip11_facts`] (config + keypair) plus the
|
|
/// host-scoped workspace icon.
|
|
pub(crate) async fn nip11_document(state: &crate::state::AppState, raw_host: &str) -> RelayInfo {
|
|
let (relay_self, advertise_nip43) = nip11_facts(state);
|
|
let icon = workspace_icon_for_host(state, raw_host).await;
|
|
let mut info = RelayInfo::build(
|
|
relay_self.as_deref(),
|
|
icon.as_deref(),
|
|
advertise_nip43,
|
|
state.config.max_frame_bytes,
|
|
state.config.pairing_relay_url.as_deref(),
|
|
);
|
|
let tenant_host = if state.config.push_enabled {
|
|
crate::tenant::bind_community(&state.db, raw_host)
|
|
.await
|
|
.ok()
|
|
.map(|tenant| tenant.host().to_owned())
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(push) = push_descriptor(
|
|
state.config.push_enabled,
|
|
&state.config.relay_url,
|
|
&state.config.push_executor_key_id,
|
|
&state.relay_keypair,
|
|
tenant_host.as_deref(),
|
|
) {
|
|
info.supported_extensions
|
|
.get_or_insert_default()
|
|
.push("nip-pl".to_string());
|
|
info.push = Some(push);
|
|
}
|
|
info
|
|
}
|
|
|
|
/// Fetches the workspace icon for the community bound to `raw_host`, as the
|
|
/// host-scoped scalar consumed by [`RelayInfo::build`].
|
|
///
|
|
/// The icon is per-community state (`communities.icon`, set by relay
|
|
/// admins/owners via the kind:9033 command) served in the standard NIP-11
|
|
/// `icon` field. The lookup is scoped through
|
|
/// [`crate::tenant::bind_community`] — never an unscoped query. Fails open to
|
|
/// `None` (no `icon` field): NIP-11 is intentionally served to unmapped hosts
|
|
/// too, and an icon lookup failure must not break that.
|
|
async fn workspace_icon_for_host(state: &crate::state::AppState, raw_host: &str) -> Option<String> {
|
|
let tenant = crate::tenant::bind_community(&state.db, raw_host)
|
|
.await
|
|
.ok()?;
|
|
state
|
|
.db
|
|
.get_community_icon(tenant.community())
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
}
|
|
|
|
/// Derives the two NIP-11 facts that depend on runtime config:
|
|
///
|
|
/// - `relay_self`: the NIP-11 `self` pubkey, set whenever the relay has a
|
|
/// stable signing key. Consumed by NIP-29 (group metadata verification)
|
|
/// and NIP-43, among others. Ephemeral keys are excluded because they
|
|
/// change on restart, leaving previously-signed events unverifiable.
|
|
/// - `advertise_nip43`: whether to list NIP-43 in `supported_nips`. True
|
|
/// only when membership is actually enforced AND we have a stable key
|
|
/// (NIP-43 events must be verifiable against `self`).
|
|
///
|
|
/// Centralised so the content-negotiated root handler and the dedicated
|
|
/// `/info` endpoint can't drift apart.
|
|
pub(crate) fn nip11_facts(state: &crate::state::AppState) -> (Option<String>, bool) {
|
|
let has_stable_key = state.config.relay_private_key.is_some();
|
|
let relay_self = has_stable_key.then(|| state.relay_keypair.public_key().to_hex());
|
|
let advertise_nip43 = has_stable_key && state.config.require_relay_membership;
|
|
(relay_self, advertise_nip43)
|
|
}
|
|
|
|
/// Multi-tenant conformance static-input fence (surface row "NIP-11 relay info
|
|
/// and relay `self`").
|
|
///
|
|
/// The conformance obligation: `RelayInfo::build` "must not grow unscoped
|
|
/// DB/search/audit inputs", so an unauthenticated NIP-11 read can never become
|
|
/// an enumeration oracle for *other* communities. `build` takes only static
|
|
/// and scalar inputs — the per-deployment facts arrive pre-derived through
|
|
/// [`nip11_facts`] (config + relay keypair), and the one host-scoped fact
|
|
/// (the workspace `icon`) arrives as a scalar from
|
|
/// [`workspace_icon_for_host`], whose DB lookup is scoped through
|
|
/// [`crate::tenant::bind_community`] and can therefore only ever surface the
|
|
/// requesting host's own community state.
|
|
///
|
|
/// This const binds `RelayInfo::build` to its **exact** allowed signature. The
|
|
/// moment someone adds a `&Db`, `&AppState`, a search handle, an audit handle,
|
|
/// or any other unscoped input, the function pointer's type stops matching and
|
|
/// **this file fails to compile** — turning a silent cross-tenant leak into a
|
|
/// hard build break, the same way a deny-lint would. If you must change this
|
|
/// signature, you are changing the conformance contract: update the conformance
|
|
/// doc and prove the new input is host-scoped, not unscoped, first.
|
|
#[allow(clippy::type_complexity)]
|
|
const _RELAY_INFO_BUILD_STATIC_INPUT_FENCE: fn(
|
|
Option<&str>,
|
|
Option<&str>,
|
|
bool,
|
|
usize,
|
|
Option<&str>,
|
|
) -> RelayInfo = RelayInfo::build;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn push_descriptor_is_gated_by_gateway_configuration_and_tenant_binding() {
|
|
let keys = nostr::Keys::generate();
|
|
assert!(
|
|
push_descriptor(false, "ws://relay", "key", &keys, Some("tenant.example")).is_none()
|
|
);
|
|
assert!(push_descriptor(true, "ws://relay", "key", &keys, None).is_none());
|
|
let descriptor = push_descriptor(true, "ws://relay", "key", &keys, Some("tenant.example"))
|
|
.expect("configured push descriptor");
|
|
assert_eq!(descriptor["origin"], "ws://tenant.example");
|
|
assert_eq!(
|
|
descriptor["push_kinds"],
|
|
serde_json::json!(crate::handlers::push_lease::PUSH_KINDS)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn supported_nips_includes_nip23_and_nip33() {
|
|
// Tests the production SUPPORTED_NIPS constant directly — no Config::from_env()
|
|
// needed, avoiding the env-var race with config.rs tests.
|
|
assert!(
|
|
SUPPORTED_NIPS.contains(&23),
|
|
"NIP-23 (long-form content) must be advertised"
|
|
);
|
|
assert!(
|
|
SUPPORTED_NIPS.contains(&33),
|
|
"NIP-33 (parameterized replaceable) must be advertised"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn supported_nips_includes_nip38() {
|
|
assert!(
|
|
SUPPORTED_NIPS.contains(&38),
|
|
"NIP-38 (user statuses) must be advertised"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn supported_nips_includes_nip56() {
|
|
assert!(
|
|
SUPPORTED_NIPS.contains(&56),
|
|
"NIP-56 (reporting) must be advertised — kind:1984 ingest is live"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn build_advertises_buzz_repository_url() {
|
|
let info = RelayInfo::build(None, None, false, DEFAULT_MAX_FRAME_BYTES, None);
|
|
assert_eq!(info.software, "https://github.com/block/buzz");
|
|
}
|
|
|
|
#[test]
|
|
fn configured_pairing_relay_is_advertised_and_unset_value_is_omitted() {
|
|
let info = RelayInfo::build(
|
|
None,
|
|
None,
|
|
false,
|
|
DEFAULT_MAX_FRAME_BYTES,
|
|
Some("wss://pairing.buzz.xyz"),
|
|
);
|
|
let json = serde_json::to_value(&info).expect("serialize");
|
|
assert_eq!(
|
|
json.get("pairing_relay_url")
|
|
.and_then(|value| value.as_str()),
|
|
Some("wss://pairing.buzz.xyz")
|
|
);
|
|
|
|
let info = RelayInfo::build(None, None, false, DEFAULT_MAX_FRAME_BYTES, None);
|
|
let json = serde_json::to_value(&info).expect("serialize");
|
|
assert!(json.get("pairing_relay_url").is_none());
|
|
}
|
|
|
|
/// NIP-WP → NIP-11 mirror: a set workspace icon is served in the standard
|
|
/// `icon` field; no icon (or a cleared, empty icon) omits the field
|
|
/// entirely so the JSON matches pre-icon documents byte-for-byte.
|
|
#[test]
|
|
fn icon_is_mirrored_and_empty_or_absent_is_omitted() {
|
|
let info = RelayInfo::build(
|
|
None,
|
|
Some("data:image/webp;base64,UklGRg=="),
|
|
false,
|
|
DEFAULT_MAX_FRAME_BYTES,
|
|
None,
|
|
);
|
|
assert_eq!(
|
|
info.icon.as_deref(),
|
|
Some("data:image/webp;base64,UklGRg==")
|
|
);
|
|
let json = serde_json::to_value(&info).expect("serialize");
|
|
assert_eq!(
|
|
json.get("icon").and_then(|v| v.as_str()),
|
|
Some("data:image/webp;base64,UklGRg==")
|
|
);
|
|
|
|
for icon in [None, Some("")] {
|
|
let info = RelayInfo::build(None, icon, false, DEFAULT_MAX_FRAME_BYTES, None);
|
|
assert!(info.icon.is_none());
|
|
let json = serde_json::to_value(&info).expect("serialize");
|
|
assert!(
|
|
json.get("icon").is_none(),
|
|
"unset/cleared icon must omit the `icon` field, not serialize null/empty"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn auth_required_is_advertised_true() {
|
|
// REQ, EVENT, and COUNT all unconditionally require
|
|
// `AuthState::Authenticated` (see `crates/buzz-relay/src/handlers/`),
|
|
// so the NIP-11 doc must advertise it.
|
|
assert!(relay_limitation(DEFAULT_MAX_FRAME_BYTES).auth_required);
|
|
}
|
|
|
|
#[test]
|
|
fn max_message_length_uses_configured_frame_limit() {
|
|
let info = RelayInfo::build(None, None, false, 262_144, None);
|
|
let limitation = info.limitation.expect("limitation");
|
|
assert_eq!(limitation.max_message_length, Some(262_144));
|
|
}
|
|
|
|
#[test]
|
|
fn supported_nips_are_sorted() {
|
|
let mut sorted = SUPPORTED_NIPS.to_vec();
|
|
sorted.sort();
|
|
assert_eq!(
|
|
SUPPORTED_NIPS,
|
|
&sorted[..],
|
|
"supported_nips should be sorted"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn nip43_not_in_static_supported_nips() {
|
|
// NIP-43 advertisement is conditional on runtime config (stable signing
|
|
// key + membership enforcement) and must NOT live in the static list.
|
|
// The desktop pairing probe keys off this NIP — advertising it on
|
|
// open relays misroutes pairing peers to a non-existent /pair sidecar.
|
|
assert!(
|
|
!SUPPORTED_NIPS.contains(&NIP_RELAY_MEMBERSHIP),
|
|
"NIP-43 must be advertised only when advertise_nip43=true is passed to RelayInfo::build"
|
|
);
|
|
}
|
|
|
|
/// Open relay, ephemeral key — both `self` and NIP-43 are absent.
|
|
#[test]
|
|
fn build_open_relay_ephemeral_key_omits_self_and_nip43() {
|
|
let info = RelayInfo::build(None, None, false, DEFAULT_MAX_FRAME_BYTES, None);
|
|
assert!(info.relay_self.is_none());
|
|
assert!(!info.supported_nips.contains(&NIP_RELAY_MEMBERSHIP));
|
|
}
|
|
|
|
/// Open relay with a stable signing key (e.g. for NIP-29 group metadata
|
|
/// signing): `self` MUST be advertised so clients can verify those
|
|
/// events; NIP-43 must NOT be, because the relay isn't enforcing
|
|
/// membership. This is the staging-default shape — the bug we're
|
|
/// fixing — and the regression we must not reintroduce.
|
|
#[test]
|
|
fn build_open_relay_stable_key_advertises_self_but_not_nip43() {
|
|
let pk = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
let info = RelayInfo::build(Some(pk), None, false, DEFAULT_MAX_FRAME_BYTES, None);
|
|
assert_eq!(info.relay_self.as_deref(), Some(pk));
|
|
assert!(!info.supported_nips.contains(&NIP_RELAY_MEMBERSHIP));
|
|
}
|
|
|
|
/// Membership-enforcing relay: both `self` and NIP-43 advertised.
|
|
#[test]
|
|
fn build_membership_relay_advertises_self_and_nip43() {
|
|
let pk = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
let info = RelayInfo::build(Some(pk), None, true, DEFAULT_MAX_FRAME_BYTES, None);
|
|
assert_eq!(info.relay_self.as_deref(), Some(pk));
|
|
assert!(info.supported_nips.contains(&NIP_RELAY_MEMBERSHIP));
|
|
}
|
|
|
|
/// NIP-43 events are verified against `self`; advertising NIP-43 without
|
|
/// `self` would give clients no way to verify membership events. The
|
|
/// debug_assert in `build` catches this in tests/debug builds.
|
|
#[test]
|
|
#[should_panic(expected = "advertise_nip43=true requires relay_self=Some")]
|
|
fn build_nip43_without_self_panics_in_debug() {
|
|
let _ = RelayInfo::build(None, None, true, DEFAULT_MAX_FRAME_BYTES, None);
|
|
}
|
|
}
|