M8: bundled relay — RelayMode::Bundled runs a co-located nostr-rs-relay
Make bundled mode actually self-contained: resolve() now leads the relay set with GP_BUNDLED_RELAY_URL (default ws://127.0.0.1:7777), which the checkout nprofile advertises, so a merchant needs no third-party relay. External mode uses only GP_RELAYS. Ship the relay as a vendored, unmodified nostr-rs-relay config (deploy/relay/nostr-rs-relay.toml); the compose service arrives with the deploy pipeline. Fix the stale "bundled is a later milestone" comment and reconcile the GP_NYM=off wording to a supported server-side-clearnet posture.
This commit is contained in:
Generated
+1
-1
@@ -5733,7 +5733,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nip44"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chacha20 0.9.1",
|
||||
|
||||
@@ -29,6 +29,14 @@ pub const DEFAULT_DATA_DIR: &str = "./gp-data";
|
||||
/// Nostr gift-wrap layer in gp-nostr).
|
||||
pub const DEFAULT_NODE_URL: &str = "https://main.gri.mw";
|
||||
|
||||
/// Default URL of the bundled relay in `bundled` relay mode: the co-located
|
||||
/// relay GoblinPay ships in `deploy/docker-compose.yml` (a vendored
|
||||
/// nostr-rs-relay), so a merchant needs no third-party relay. Override with
|
||||
/// `GP_BUNDLED_RELAY_URL`. In a public deployment set this to the relay's
|
||||
/// publicly reachable `wss://<domain>` URL, because the same value is both
|
||||
/// dialed by the server AND advertised to payers in the checkout `nprofile`.
|
||||
pub const DEFAULT_BUNDLED_RELAY: &str = "ws://127.0.0.1:7777";
|
||||
|
||||
/// TLS mode for the HTTP server.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -53,7 +61,11 @@ pub enum Chain {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RelayMode {
|
||||
/// GoblinPay supervises its own relay (default; see module design 3).
|
||||
/// GoblinPay talks to its own co-located relay (default): the bundled
|
||||
/// nostr-rs-relay from `deploy/docker-compose.yml`, reached at
|
||||
/// `GP_BUNDLED_RELAY_URL`. That relay is what the checkout `nprofile`
|
||||
/// advertises, so a merchant needs no third-party relay. Any `GP_RELAYS`
|
||||
/// are added alongside it for redundancy.
|
||||
Bundled,
|
||||
/// Only external relays from `GP_RELAYS` are used.
|
||||
External,
|
||||
@@ -130,8 +142,14 @@ pub struct Config {
|
||||
pub relay_mode: RelayMode,
|
||||
/// External relays (`GP_RELAYS`, comma separated).
|
||||
pub relays: Vec<String>,
|
||||
/// URL of the bundled relay used in `bundled` relay mode
|
||||
/// (`GP_BUNDLED_RELAY_URL`, default `ws://127.0.0.1:7777`). Both dialed by
|
||||
/// the ingest service and advertised to payers in the checkout `nprofile`.
|
||||
pub bundled_relay_url: String,
|
||||
/// Route Nostr traffic over the Nym mixnet (`GP_NYM`: `on` or `off`,
|
||||
/// default on; clearnet is a debugging escape hatch only).
|
||||
/// default on). Production may deliberately run `off` (server-side
|
||||
/// clearnet): the payer's Goblin Wallet still provides sender privacy over
|
||||
/// its own mixnet, and the payload is gift-wrapped end to end regardless.
|
||||
pub nym: bool,
|
||||
/// Run the Nostr ingest service (`GP_INGEST`: `on` or `off`, default on).
|
||||
/// When on, the wallet and identity secrets are required at boot.
|
||||
@@ -235,6 +253,7 @@ impl Default for Config {
|
||||
chain: Chain::Mainnet,
|
||||
relay_mode: RelayMode::Bundled,
|
||||
relays: Vec::new(),
|
||||
bundled_relay_url: DEFAULT_BUNDLED_RELAY.into(),
|
||||
nym: true,
|
||||
ingest: true,
|
||||
checkout_nostr: true,
|
||||
@@ -320,6 +339,10 @@ impl Config {
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
let bundled_relay_url = get("GP_BUNDLED_RELAY_URL")
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or(defaults.bundled_relay_url);
|
||||
|
||||
let nym = match get("GP_NYM").as_deref().unwrap_or("on") {
|
||||
"on" => true,
|
||||
@@ -406,6 +429,7 @@ impl Config {
|
||||
chain,
|
||||
relay_mode,
|
||||
relays,
|
||||
bundled_relay_url,
|
||||
nym,
|
||||
ingest,
|
||||
checkout_nostr,
|
||||
@@ -473,6 +497,9 @@ impl Config {
|
||||
if self.relay_mode == RelayMode::External && self.relays.is_empty() {
|
||||
return Err("GP_RELAY_MODE=external requires GP_RELAYS".into());
|
||||
}
|
||||
if self.relay_mode == RelayMode::Bundled && self.bundled_relay_url.trim().is_empty() {
|
||||
return Err("GP_RELAY_MODE=bundled requires a non-empty GP_BUNDLED_RELAY_URL".into());
|
||||
}
|
||||
if self.nsec.is_some() && self.ncryptsec.is_some() {
|
||||
return Err("set only one of GP_NSEC and GP_NCRYPTSEC".into());
|
||||
}
|
||||
@@ -507,7 +534,8 @@ impl Config {
|
||||
let set = |o: bool| if o { "set" } else { "unset" };
|
||||
format!(
|
||||
"bind={} tls={} db={} data_dir={} node={} chain={:?} relay_mode={:?} \
|
||||
relays={:?} nym={} ingest={} checkout_methods={} match_mode={:?} mnemonic={} \
|
||||
relays={:?} bundled_relay={} nym={} ingest={} checkout_methods={} match_mode={:?} \
|
||||
mnemonic={} \
|
||||
wallet_password={} \
|
||||
nsec={} ncryptsec={} public_url={} api_token={} admin_token={} webhook_url={} \
|
||||
webhook_secret={} qr_logo={} merchant_npub={} notify_merchant_dm={} \
|
||||
@@ -525,6 +553,7 @@ impl Config {
|
||||
self.chain,
|
||||
self.relay_mode,
|
||||
self.relays,
|
||||
self.bundled_relay_url,
|
||||
if self.nym { "on" } else { "off" },
|
||||
if self.ingest { "on" } else { "off" },
|
||||
self.checkout_methods_str(),
|
||||
@@ -657,6 +686,7 @@ mod tests {
|
||||
assert_eq!(cfg.chain, Chain::Mainnet);
|
||||
assert_eq!(cfg.relay_mode, RelayMode::Bundled);
|
||||
assert!(cfg.relays.is_empty());
|
||||
assert_eq!(cfg.bundled_relay_url, DEFAULT_BUNDLED_RELAY);
|
||||
assert!(cfg.nym);
|
||||
assert!(cfg.ingest);
|
||||
assert_eq!(cfg.match_mode, MatchMode::Memo);
|
||||
@@ -676,6 +706,7 @@ mod tests {
|
||||
("GP_CHAIN", "testnet"),
|
||||
("GP_RELAY_MODE", "external"),
|
||||
("GP_RELAYS", "wss://relay.example, wss://relay2.example ,"),
|
||||
("GP_BUNDLED_RELAY_URL", "wss://relay.mystore.example"),
|
||||
("GP_NYM", "off"),
|
||||
("GP_INGEST", "off"),
|
||||
("GP_MATCH_MODE", "derived"),
|
||||
@@ -691,6 +722,7 @@ mod tests {
|
||||
cfg.relays,
|
||||
vec!["wss://relay.example", "wss://relay2.example"]
|
||||
);
|
||||
assert_eq!(cfg.bundled_relay_url, "wss://relay.mystore.example");
|
||||
assert!(!cfg.nym);
|
||||
assert!(!cfg.ingest);
|
||||
assert_eq!(cfg.match_mode, MatchMode::Derived);
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
//! Default relay set and helpers (mirrors `goblin/src/nostr/relays.rs`).
|
||||
//! Relay set resolution.
|
||||
//!
|
||||
//! GoblinPay runs in one of two relay modes (`GP_RELAY_MODE`, see
|
||||
//! [`gp_core::config::RelayMode`]):
|
||||
//!
|
||||
//! - `bundled` (default): GoblinPay talks to its own co-located relay, the
|
||||
//! nostr-rs-relay shipped as the `relay` service in
|
||||
//! `deploy/docker-compose.yml`. Its URL is `GP_BUNDLED_RELAY_URL` (default
|
||||
//! `ws://127.0.0.1:7777`). Because the resolved set is exactly what the
|
||||
//! checkout `nprofile` advertises to payers, a merchant needs no third-party
|
||||
//! relay: the payer's Goblin Wallet is told to deliver the gift-wrapped
|
||||
//! slatepack to the merchant's own relay. Extra relays listed in `GP_RELAYS`
|
||||
//! are appended for redundancy (and advertised alongside the bundled one).
|
||||
//! - `external`: only the relays listed in `GP_RELAYS` are used (no bundled
|
||||
//! relay); config validation requires at least one.
|
||||
//!
|
||||
//! The bundled relay is a vendored, unmodified nostr-rs-relay (config only, no
|
||||
//! fork) rather than a relay written from scratch: it is a small, SQLite-backed
|
||||
//! Rust relay that fits a single-merchant till, and reusing it keeps the money
|
||||
//! path off any third-party infrastructure.
|
||||
|
||||
/// Default DM relays: the Goblin relay plus large public relays for
|
||||
/// redundancy. Used when `GP_RELAYS` is unset (the bundled relay is a later
|
||||
/// milestone; until then `bundled` mode serves this set too).
|
||||
pub const DEFAULT_RELAYS: &[&str] = &[
|
||||
"wss://relay.goblin.st",
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
];
|
||||
use gp_core::config::RelayMode;
|
||||
|
||||
/// Maximum relays published in the kind 10050 DM relay list (NIP-17
|
||||
/// guidance) and read from a payer's list.
|
||||
pub const MAX_DM_RELAYS: usize = 3;
|
||||
|
||||
/// The relay set to run with: the configured external list, else defaults.
|
||||
pub fn resolve(configured: &[String]) -> Vec<String> {
|
||||
if configured.is_empty() {
|
||||
DEFAULT_RELAYS.iter().map(|s| s.to_string()).collect()
|
||||
} else {
|
||||
configured.to_vec()
|
||||
/// The relay set to listen on, publish to, and advertise in the `nprofile`.
|
||||
///
|
||||
/// In `bundled` mode the co-located `bundled_url` comes first (so it heads the
|
||||
/// advertised kind 10050 / `nprofile` hints), followed by any `configured`
|
||||
/// redundancy relays, de-duplicated. In `external` mode only the `configured`
|
||||
/// relays are used.
|
||||
pub fn resolve(mode: RelayMode, bundled_url: &str, configured: &[String]) -> Vec<String> {
|
||||
match mode {
|
||||
RelayMode::Bundled => {
|
||||
let mut relays = vec![bundled_url.to_string()];
|
||||
for relay in configured {
|
||||
if !relays.iter().any(|r| r == relay) {
|
||||
relays.push(relay.clone());
|
||||
}
|
||||
}
|
||||
relays
|
||||
}
|
||||
RelayMode::External => configured.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +51,42 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolves_defaults_and_overrides() {
|
||||
assert_eq!(resolve(&[]), DEFAULT_RELAYS.to_vec());
|
||||
fn bundled_leads_with_the_bundled_relay() {
|
||||
// No extras: just the bundled relay, so the nprofile advertises it and
|
||||
// nothing third-party is involved.
|
||||
assert_eq!(
|
||||
resolve(RelayMode::Bundled, "ws://127.0.0.1:7777", &[]),
|
||||
vec!["ws://127.0.0.1:7777".to_string()]
|
||||
);
|
||||
// Extras are appended for redundancy; the bundled relay stays first.
|
||||
let extras = vec!["wss://relay.damus.io".to_string()];
|
||||
assert_eq!(
|
||||
resolve(RelayMode::Bundled, "ws://127.0.0.1:7777", &extras),
|
||||
vec![
|
||||
"ws://127.0.0.1:7777".to_string(),
|
||||
"wss://relay.damus.io".to_string(),
|
||||
]
|
||||
);
|
||||
// A configured relay equal to the bundled one is not added twice.
|
||||
let dup = vec![
|
||||
"ws://127.0.0.1:7777".to_string(),
|
||||
"wss://r.example".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
resolve(RelayMode::Bundled, "ws://127.0.0.1:7777", &dup),
|
||||
vec![
|
||||
"ws://127.0.0.1:7777".to_string(),
|
||||
"wss://r.example".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_uses_only_configured() {
|
||||
let own = vec!["wss://relay.example".to_string()];
|
||||
assert_eq!(resolve(&own), own);
|
||||
assert_eq!(
|
||||
resolve(RelayMode::External, "ws://127.0.0.1:7777", &own),
|
||||
own
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,9 @@ const NYM_WARM_WAIT: Duration = Duration::from_secs(30);
|
||||
pub struct ServiceOptions {
|
||||
/// Relay set to listen on and publish to.
|
||||
pub relays: Vec<String>,
|
||||
/// Route everything over the Nym mixnet (default on; clearnet is a
|
||||
/// debugging escape hatch only).
|
||||
/// Route everything over the Nym mixnet (default on). `off` is a supported
|
||||
/// production posture (server-side clearnet): the payer's Goblin Wallet
|
||||
/// still rides its own mixnet, and the payload is gift-wrapped end to end.
|
||||
pub nym: bool,
|
||||
/// Optional NIP-17 payment DMs (milestone 6, all off by default).
|
||||
pub notify: NotifyOptions,
|
||||
@@ -141,7 +142,10 @@ pub async fn run<R: SlatepackReceiver>(
|
||||
.websocket_transport(NymWebSocketTransport)
|
||||
.build()
|
||||
} else {
|
||||
warn!("nostr: GP_NYM=off — relay traffic goes CLEARNET (debugging only)");
|
||||
warn!(
|
||||
"nostr: GP_NYM=off — this server's relay traffic goes CLEARNET (supported: the \
|
||||
payer's wallet still provides sender privacy; the payload stays gift-wrapped)"
|
||||
);
|
||||
Client::builder().build()
|
||||
};
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ async fn dashboard(
|
||||
match_mode: format!("{:?}", cfg.match_mode).to_lowercase(),
|
||||
nym: cfg.nym,
|
||||
ingest: cfg.ingest,
|
||||
relay_count: gp_nostr::relays::resolve(&cfg.relays).len(),
|
||||
relay_count: gp_nostr::relays::resolve(cfg.relay_mode, &cfg.bundled_relay_url, &cfg.relays)
|
||||
.len(),
|
||||
webhook_configured: cfg.webhook_url.is_some(),
|
||||
pending_webhooks,
|
||||
rotate_interval: cfg.endpub_rotate_interval,
|
||||
@@ -206,7 +207,7 @@ struct CreateUserBody {
|
||||
}
|
||||
|
||||
fn endpub_json(cfg: &Config, user_id: &str, epoch: i64, pubkey: &str) -> serde_json::Value {
|
||||
let relays = gp_nostr::relays::resolve(&cfg.relays);
|
||||
let relays = gp_nostr::relays::resolve(cfg.relay_mode, &cfg.bundled_relay_url, &cfg.relays);
|
||||
let (npub, nprofile, qr) = match PublicKey::from_hex(pubkey) {
|
||||
Ok(pk) => (
|
||||
gp_nostr::npub_of(pk),
|
||||
|
||||
@@ -56,7 +56,7 @@ pub struct CheckoutInfo {
|
||||
/// caller does not surface the Slatepack option (e.g. the JSON connector API),
|
||||
/// in which case no Slatepack address or QR is produced.
|
||||
pub fn build_info(inv: &Invoice, cfg: &Config, slatepack_addr: Option<&str>) -> CheckoutInfo {
|
||||
let relays = gp_nostr::relays::resolve(&cfg.relays);
|
||||
let relays = gp_nostr::relays::resolve(cfg.relay_mode, &cfg.bundled_relay_url, &cfg.relays);
|
||||
let recipient_pubkey = inv.recipient_pubkey.clone().unwrap_or_default();
|
||||
// The Nostr (Goblin Wallet) method is only surfaced when the operator has it
|
||||
// enabled (`GP_CHECKOUT_METHODS`). Disabled, the nprofile/npub/QR are left
|
||||
|
||||
@@ -132,7 +132,7 @@ async fn start_ingest(cfg: &Config, pool: sqlx::SqlitePool) -> (Keys, GpWallet)
|
||||
eprintln!("warning: GP_NOTIFY_MERCHANT_DM=on but GP_MERCHANT_NPUB is unset/invalid");
|
||||
}
|
||||
let opts = gp_nostr::service::ServiceOptions {
|
||||
relays: gp_nostr::relays::resolve(&cfg.relays),
|
||||
relays: gp_nostr::relays::resolve(cfg.relay_mode, &cfg.bundled_relay_url, &cfg.relays),
|
||||
nym: cfg.nym,
|
||||
notify: gp_nostr::service::NotifyOptions {
|
||||
merchant,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Configuration for the BUNDLED GoblinPay relay: a stock, unmodified
|
||||
# nostr-rs-relay (https://github.com/scsibug/nostr-rs-relay) run as the `relay`
|
||||
# service in docker-compose.yml. This is the self-contained relay that
|
||||
# `GP_RELAY_MODE=bundled` (the default) points at, so a merchant needs no
|
||||
# third-party relay: GoblinPay dials it, and the checkout `nprofile` advertises
|
||||
# it to payers, who deliver their gift-wrapped slatepack straight to the
|
||||
# merchant's own relay.
|
||||
#
|
||||
# nostr-rs-relay is a small, SQLite-backed Rust relay: a good fit for a
|
||||
# single-merchant till, and vendored as-is (config only, no fork).
|
||||
|
||||
[info]
|
||||
# Set this to the relay's PUBLIC wss URL (the same value you put in
|
||||
# GP_BUNDLED_RELAY_URL). Payers connect here.
|
||||
relay_url = "wss://pay.example/"
|
||||
name = "GoblinPay bundled relay"
|
||||
description = "Co-located Nostr relay for a GoblinPay merchant till."
|
||||
|
||||
[database]
|
||||
data_directory = "/usr/src/app/db"
|
||||
|
||||
[network]
|
||||
# Inside the container. Caddy terminates TLS and proxies wss -> here.
|
||||
address = "0.0.0.0"
|
||||
port = 7777
|
||||
|
||||
[limits]
|
||||
# Bound the footprint so an unauthenticated ingest/subscription flood cannot
|
||||
# starve the till (mirrors the reasoning behind the bundled strfry limits in
|
||||
# goblin-nip05d). Payers publish NIP-59 gift wraps from random ephemeral keys,
|
||||
# so a pubkey allowlist is intentionally NOT used (it would block payments).
|
||||
messages_per_sec = 10
|
||||
subscriptions_per_min = 60
|
||||
max_event_bytes = 131072
|
||||
max_ws_message_bytes = 262144
|
||||
max_subscriptions = 20
|
||||
|
||||
[options]
|
||||
reject_future_seconds = 1800
|
||||
Reference in New Issue
Block a user