Project

nostr-zap

0.0
The project is in a healthy, maintained state
A Ruby toolkit for NOSTR NIP-57 Lightning Zaps. Includes BIP-340 key management, event signing/verification, zap request validation, zap receipt construction, relay publishing via WebSocket, and SSRF-safe relay URL validation. No Rails dependency required.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

nostr-zap

CI Gem Version

A Ruby toolkit for NOSTR NIP-57 Lightning Zaps. Validate zap requests, build zap receipts, publish to relays.

No Rails dependency required.

Installation

Add to your Gemfile:

gem 'nostr-zap'

Components

Class Purpose
NostrZap::Crypto Stateless BIP-340 Schnorr cryptography (sign, verify, key derivation)
NostrZap::KeyPair Holds a private/public key pair, convenience signing
NostrZap::Event NIP-01 event: build, sign, verify, serialize
NostrZap::Zap::RequestValidator Validate kind-9734 zap requests (NIP-57)
NostrZap::Zap::ReceiptBuilder Build kind-9735 zap receipts (NIP-57)
NostrZap::RelayClient Publish events to relays via WebSocket (concurrent)
NostrZap::RelayUrlValidator SSRF-safe relay URL validation

Usage

Key Management

# Generate a new key pair
key_pair = NostrZap::KeyPair.generate
key_pair.public_key_hex  # => "abc123..."

# From an existing private key
key_pair = NostrZap::KeyPair.new("deadbeef" * 8)

# Low-level crypto
NostrZap::Crypto.generate_private_key       # => "64-char hex"
NostrZap::Crypto.derive_public_key(priv)    # => "64-char hex"
NostrZap::Crypto.valid_private_key?(hex)    # => true/false

Building and Signing Events

event = NostrZap::Event.new(kind: 1, content: "hello", tags: [])
event.sign(key_pair)

event.to_h
# => { "id" => "...", "pubkey" => "...", "created_at" => 1234567890,
#      "kind" => 1, "tags" => [], "content" => "hello", "sig" => "..." }

Verifying Events

event = NostrZap::Event.from_h(parsed_hash)
event.id_valid?        # recomputes ID from payload
event.signature_valid? # verifies BIP-340 signature

Validating Zap Requests

validator = NostrZap::Zap::RequestValidator.new(
  zap_request_json,
  verify_signatures: true  # set false to skip sig check (e.g. in tests)
)

if validator.valid?
  validator.recipient_pubkey  # => "hex pubkey"
  validator.relay_urls        # => ["wss://relay.example.com"]
  validator.amount_msats      # => 1000
  validator.zapped_event_id   # => "hex event id" or nil
else
  validator.errors  # => ["Invalid kind: expected integer 9734, got 1"]
end

Building Zap Receipts

builder = NostrZap::Zap::ReceiptBuilder.new(
  zap_request_json: original_zap_request_json,
  bolt11_invoice:   "lnbc10n1...",
  key_pair:         key_pair,
  paid_at:          Time.now,  # optional, defaults to now
  preimage:         "abc..."   # optional
)

event = builder.build       # => NostrZap::Event (kind 9735, signed)
relays = builder.relay_urls # => relay URLs from the original zap request

Publishing to Relays

client = NostrZap::RelayClient.new(
  timeout: 10,          # connection timeout in seconds (default: 10)
  logger:  Rails.logger  # optional, any object responding to #error
)

# Publish to multiple relays concurrently
results = client.publish_event(
  event:      event.to_h,
  relay_urls: ["wss://relay1.example.com", "wss://relay2.example.com"]
)
# => { "wss://relay1.example.com" => { success: true, message: "Published" },
#      "wss://relay2.example.com" => { success: false, message: "rate-limited" } }

# Publish to a single relay
result = client.publish_to_relay(
  event:     event.to_h,
  relay_url: "wss://relay.example.com"
)

Relay URL Validation

error = NostrZap::RelayUrlValidator.validate("wss://relay.example.com")
# => nil (valid)

error = NostrZap::RelayUrlValidator.validate("ws://localhost")
# => "Invalid relay URL scheme 'ws' (only wss is allowed): ws://localhost"

error = NostrZap::RelayUrlValidator.validate("wss://192.168.1.1")
# => "Relay URL points to a private/reserved address: wss://192.168.1.1"

Blocked IP ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 0.0.0.0/8, ::1, fc00::/7, fe80::/10.

Error Classes

NostrZap::Error              # base
  NostrZap::ConfigurationError
  NostrZap::InvalidKeyError
  NostrZap::SigningError
  NostrZap::RelayClient::PublishError

Dependencies

  • bip-schnorr ~> 0.7 -- BIP-340 Schnorr signatures
  • websocket ~> 1.0 -- WebSocket handshake and framing

Development

cd gems/nostr-zap
bundle install
bundle exec rspec       # 86 specs
bundle exec rubocop

License

MIT