Skip to content

sashazykov/nostr-zap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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")
# => "Relay URL points to a private/reserved address: ws://localhost"

error = NostrZap::RelayUrlValidator.validate("ws://localhost:4848", check_private: false)
# => nil

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

error = NostrZap::RelayUrlValidator.validate("wss://relay.nostr.bg")
# => nil (hostnames that do not currently resolve are accepted)

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       # 99 specs
bundle exec rubocop

License

MIT

About

A Ruby toolkit for NOSTR Lightning Zaps (NIP-57)

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages