Compare commits
60 Commits
e2b650fb84
...
defeba1278
Author | SHA1 | Date |
---|---|---|
Wilson Silva | defeba1278 | |
Wilson Silva | 955b74054b | |
Wilson Silva | 90ab1a6149 | |
Wilson Silva | a0cf41bfb4 | |
Wilson Silva | 61eb0459d4 | |
Wilson Silva | 0497878d54 | |
Wilson Silva | 7946b82aaf | |
Wilson Silva | e114656166 | |
Wilson Silva | 04574cc836 | |
Wilson Silva | 1ff9611051 | |
Wilson Silva | 1d12363af1 | |
Wilson Silva | 9ae68542f1 | |
Wilson Silva | 6a5068a552 | |
Wilson Silva | 470a72d4de | |
Wilson Silva | 86cd6c6baa | |
Wilson Silva | 9d6d91e436 | |
Wilson Silva | 838a2db834 | |
Wilson Silva | 7c571d3b12 | |
Wilson Silva | 01010c763f | |
Wilson Silva | f8893f9b0e | |
Wilson Silva | 3788ba4ce5 | |
Wilson Silva | 0f83b8071a | |
Wilson Silva | c8d633dbea | |
Wilson Silva | df51354d3e | |
Wilson Silva | 3765b60a68 | |
Wilson Silva | 17cd2bf0f4 | |
Josua Schmid | 5497930dd5 | |
Wilson Silva | c064a8d5f0 | |
Wilson Silva | 8bc6b84f6f | |
Wilson Silva | 61a88981e6 | |
Wilson Silva | bba18d1bc0 | |
Wilson Silva | b45370e65f | |
Wilson Silva | 53069a3d0c | |
Wilson Silva | 3520cf8219 | |
Wilson Silva | 3fffcd1a4e | |
Wilson Silva | 2c44ae4ee8 | |
Wilson Silva | 4b630c678b | |
Wilson Silva | 3077aa67a7 | |
Wilson Silva | 2584967654 | |
Wilson Silva | b54147cfce | |
Wilson Silva | 0f9c0d3a3d | |
Wilson Silva | 904fe46a9d | |
Wilson Silva | 80c272f149 | |
Wilson Silva | be4c3e0e32 | |
Wilson Silva | 30eafa1203 | |
Wilson Silva | 4f13b22e51 | |
Wilson Silva | 6d9758a37a | |
Wilson Silva | 4d8803d769 | |
Wilson Silva | e9deab2fc2 | |
Wilson Silva | 35c7e6a76e | |
Wilson Silva | 8e1e3092c3 | |
Wilson Silva | eaa97e0018 | |
Wilson Silva | 1fb7e454ae | |
Wilson Silva | 82aacb70e7 | |
Wilson Silva | d49fac49b6 | |
Wilson Silva | b206f6504e | |
Wilson Silva | 6d81d07f8a | |
Wilson Silva | 3fbc523b18 | |
Wilson Silva | c27de6d506 | |
Wilson Silva | 1865858230 |
|
@ -6,6 +6,6 @@ max_line_length = 120
|
|||
trim_trailing_whitespace = true
|
||||
|
||||
# 2 space indentation
|
||||
[*.rb]
|
||||
[{*.rb, *.mjs}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
ruby:
|
||||
- '3.2.0'
|
||||
- '3.3.0'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
@ -18,3 +18,6 @@
|
|||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# A local copy of nostr nips to help Github Copilot suggestions
|
||||
/nips/
|
||||
|
|
27
.rubocop.yml
27
.rubocop.yml
|
@ -5,12 +5,23 @@ require:
|
|||
- rubocop-rspec
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 3.2
|
||||
TargetRubyVersion: 3.3
|
||||
DisplayCopNames: true
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
- docs/**/*
|
||||
|
||||
# ----------------------- Gemspec -----------------------
|
||||
|
||||
Gemspec/DevelopmentDependencies:
|
||||
Enabled: false
|
||||
|
||||
# ----------------------- Style -----------------------
|
||||
|
||||
Style/RaiseArgs:
|
||||
Exclude:
|
||||
- 'lib/nostr/key.rb'
|
||||
|
||||
Style/StringLiterals:
|
||||
Enabled: true
|
||||
EnforcedStyle: single_quotes
|
||||
|
@ -38,3 +49,17 @@ Metrics/ParameterLists:
|
|||
|
||||
RSpec/ExampleLength:
|
||||
Enabled: false
|
||||
|
||||
RSpec/FilePath:
|
||||
Exclude:
|
||||
- spec/nostr/errors/invalid_*
|
||||
|
||||
RSpec/SpecFilePathFormat:
|
||||
Exclude:
|
||||
- spec/nostr/errors/invalid_*
|
||||
|
||||
# ----------------------- Naming -----------------------
|
||||
|
||||
Naming/MemoizedInstanceVariableName:
|
||||
Exclude:
|
||||
- 'spec/nostr/key.rb'
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
# Offense count: 2
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes.
|
||||
Metrics/AbcSize:
|
||||
Max: 23
|
||||
Max: 24
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods.
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
ruby 3.2.0
|
||||
ruby 3.3.0
|
||||
bun 1.1.3
|
||||
|
|
98
CHANGELOG.md
98
CHANGELOG.md
|
@ -1,9 +1,102 @@
|
|||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.1/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.7.0] 2024-04-13
|
||||
|
||||
### Added
|
||||
|
||||
- Added the `Nostr::Client::Logger` class to log connection events, messages sent and received, errors, and connection
|
||||
closures.
|
||||
- Added the `Nostr::Client::ColorLogger` class to log events in color using ANSI escape codes.
|
||||
- Added the `Nostr::Client::PlainLogger` class to log events without color coding.
|
||||
- Added Architecture Decision Records (ADRs) to document the decision process for logging functionality.
|
||||
- Added a new common use case document for logging and debugging.
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the `Nostr::Client` class to use the `ColorLogger` by default for logging client interactions with relays.
|
||||
- Updated the `Nostr::Client#connect` method to emit a `:send` event when sending a message.
|
||||
- Updated the `Nostr::Client#on` method to pass the `relay` parameter to the `:connect` event handler.
|
||||
- Updated bun to version `1.1.3` (was `1.0.30`).
|
||||
- Updated the gem `json` to version `2.7` (was `2.6`).
|
||||
- Updated the gem `mermaid` to version `10.9` (was `10.6`).
|
||||
- Updated the gem `rubocop-rspec` to version `2.29` (was `2.27`).
|
||||
- Updated the gem `steep` to version `1.7.dev3` (was `1.6`).
|
||||
- Updated the gem `vitepress` to version `1.1` (was `1.0.0-rc.25`).
|
||||
- Updated the gem `vitepress-plugin-mermaid` to version `2.0.16` (was `2.0.15`).
|
||||
- Updated the error message in `Nostr::InvalidSignatureTypeError` to provide more detail.
|
||||
|
||||
### Fixed
|
||||
|
||||
Fixed a type-checking issue in `Nostr::Event#verify_signature` by removing a workaround after the steep gem author,
|
||||
[@soutaro](https://github.com/soutaro) resolved [the problem I reported](https://github.com/soutaro/steep/issues/1079).
|
||||
|
||||
## [0.6.0] 2024-03-15
|
||||
|
||||
### Added
|
||||
|
||||
- Added Architecture Decision Records (ADRs) to document architectural decisions
|
||||
- Added the `Signature` class to fix the primitive obsession with signatures and to make it easier to work with them
|
||||
- Added `valid_sig?` and `check_sig!` to the `Crypto` class to verify whether an event's signature is valid
|
||||
- Added `sign_message` to the `Crypto` class to sign a message
|
||||
- Added `verify_signature?` to the `Event` class to verify whether an event's signature is valid
|
||||
- Added `#to_ary` to the `KeyPair` class to enable keypair destructuring
|
||||
- Added RBS types for `schnorr`
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated the required Ruby version to `3.3.0` (was `3.2.0`)
|
||||
- Updated the gem `dotenv` to version `3.1` (was `2.8`)
|
||||
- Updated the gem `bip-schnorr` to version `0.7` (was `0.6`)
|
||||
- Updated the gem `overcommit` to version `0.63` (was `0.59`)
|
||||
- Updated the gem `rbs` to version `3.4` (was `3.3`)
|
||||
- Updated the gem `rspec` to version `3.13` (was `3.12`)
|
||||
- Updated the gem `rspec-rubocop` to version `2.27` (was `2.25`)
|
||||
- Updated the gem `rubocop` to version `1.62` (was `1.57`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a typo in the README (`generate_keypair` -> `generate_key_pair`)
|
||||
- Fixed a typo in the YARD documentation of `Nostr::Key#initialize` (`ValidationError` -> `KeyValidationError`)
|
||||
- Fixed YARD example rendering issues in `InvalidKeyFormatError#initialize`, `InvalidKeyLengthError#initialize`,
|
||||
`InvalidKeyTypeError#initialize`, `Event#initialize`, `EncryptedDirectMessage#initialize` and `Filter#to_h`
|
||||
|
||||
## [0.5.0] 2023-11-20
|
||||
|
||||
### Added
|
||||
|
||||
- Added relay message type enums `Nostr::RelayMessageType`
|
||||
- Compliance with [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) - bech32-formatted strings
|
||||
- Added `Nostr::PrivateKey` and `Nostr::PublicKey` to represent private and public keys, respectively
|
||||
- Added a validation of private and public keys
|
||||
- Added an ability to convert keys to and from Bech32 format
|
||||
- Added RBS types for `faye-websocket` and `bech32`
|
||||
|
||||
### Changed
|
||||
|
||||
- Set the gem's homepage to [`https://nostr-ruby.com/`](https://nostr-ruby.com/)
|
||||
- Updated the filter's documentation to reflect the removal of prefix matching
|
||||
- Updated the subscription's id documentation to reflect the changes in the protocol definition
|
||||
- Updated `Nostr::PrivateKey` and `Nostr::PublicKey` internally, instead of Strings
|
||||
- Updated the gem `bip-schnorr` to version `0.6` (was `0.4`)
|
||||
- Updated the gem `puma` to version `6.4` (was `6.3`)
|
||||
- Updated the gem `rake` to version `13.1` (was `13.0`)
|
||||
- Updated the gem `rbs` to version `3.3` (was `2.8`)
|
||||
- Updated the gem `rubocop` to version `1.57` (was `1.42`)
|
||||
- Updated the gem `rubocop-rspec` to version `2.25` (was `2.16`)
|
||||
- Updated the gem `steep` to version `1.6` (was `1.4`)
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fixed the RBS type of the constant `Nostr::Crypto::BN_BASE`
|
||||
- Fixed the return type of `Nostr::Crypto#decrypt_text` when given an invalid ciphertext
|
||||
- Fixed the RBS type of `Nostr::Filter#to_h`, `Nostr::Filter#e` and `Nostr::Filter#p`
|
||||
- Fixed the RBS types of `EventEmitter` and `EventMachine::Channel`
|
||||
- Fixed the generation of private keys
|
||||
|
||||
## [0.4.0] - 2023-02-25
|
||||
|
||||
### Removed
|
||||
|
@ -49,6 +142,9 @@ principles of immutability and was a major source of internal complexity as I ne
|
|||
|
||||
- Initial release
|
||||
|
||||
[0.7.0]: https://github.com/wilsonsilva/nostr/compare/v0.6.0...v0.7.0
|
||||
[0.6.0]: https://github.com/wilsonsilva/nostr/compare/v0.5.0...v0.6.0
|
||||
[0.5.0]: https://github.com/wilsonsilva/nostr/compare/v0.4.0...v0.5.0
|
||||
[0.4.0]: https://github.com/wilsonsilva/nostr/compare/v0.3.0...v0.4.0
|
||||
[0.3.0]: https://github.com/wilsonsilva/nostr/compare/v0.2.0...v0.3.0
|
||||
[0.2.0]: https://github.com/wilsonsilva/nostr/compare/v0.1.0...v0.2.0
|
||||
|
|
331
README.md
331
README.md
|
@ -1,34 +1,34 @@
|
|||
# Nostr
|
||||
|
||||
[![Gem Version](https://badge.fury.io/rb/nostr.svg)](https://badge.fury.io/rb/nostr)
|
||||
![Build](https://github.com/wilsonsilva/nostr/actions/workflows/main.yml/badge.svg)
|
||||
[![Maintainability](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/maintainability)](https://codeclimate.com/github/wilsonsilva/nostr/maintainability)
|
||||
[![Test Coverage](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/test_coverage)](https://codeclimate.com/github/wilsonsilva/nostr/test_coverage)
|
||||
|
||||
Asynchronous Nostr client. Please note that the API is likely to change as the gem is still in development and
|
||||
has not yet reached a stable release. Use with caution.
|
||||
Asynchronous Nostr client for Rubyists.
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
* [Requiring the gem](#requiring-the-gem)
|
||||
* [Generating a keypair](#generating-a-keypair)
|
||||
* [Generating a private key and a public key](#generating-a-private-key-and-a-public-key)
|
||||
* [Connecting to a Relay](#connecting-to-a-relay)
|
||||
* [WebSocket events](#websocket-events)
|
||||
* [Requesting for events / creating a subscription](#requesting-for-events--creating-a-subscription)
|
||||
* [Stop previous subscriptions](#stop-previous-subscriptions)
|
||||
* [Publishing an event](#publishing-an-event)
|
||||
* [Creating/updating your contact list](#creatingupdating-your-contact-list)
|
||||
* [Sending an encrypted direct message](#sending-an-encrypted-direct-message)
|
||||
- [Implemented NIPs](#implemented-nips)
|
||||
- [Development](#development)
|
||||
- [Key features](#-key-features)
|
||||
- [Installation](#-installation)
|
||||
- [Quickstart](#-quickstart)
|
||||
- [Documentation](#-documentation)
|
||||
- [Implemented NIPs](#-implemented-nips)
|
||||
- [Development](#-development)
|
||||
* [Type checking](#type-checking)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Contributing](#-contributing)
|
||||
- [License](#-license)
|
||||
- [Code of Conduct](#-code-of-conduct)
|
||||
|
||||
## Installation
|
||||
## 🔑 Key features
|
||||
|
||||
- Asynchronous
|
||||
- Easy to use
|
||||
- Fully documented
|
||||
- Fully tested
|
||||
- Fully typed
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
Install the gem and add to the application's Gemfile by executing:
|
||||
|
||||
|
@ -38,248 +38,106 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|||
|
||||
$ gem install nostr
|
||||
|
||||
## Usage
|
||||
## ⚡️ Quickstart
|
||||
|
||||
### Requiring the gem
|
||||
|
||||
All examples below assume that the gem has been required.
|
||||
Here is a quick example of how to use the gem. For more detailed documentation, please check the
|
||||
[documentation website](https://nostr-ruby.com).
|
||||
|
||||
```ruby
|
||||
# Require the gem
|
||||
require 'nostr'
|
||||
```
|
||||
|
||||
### Generating a keypair
|
||||
# Instantiate a client
|
||||
client = Nostr::Client.new
|
||||
|
||||
```ruby
|
||||
keygen = Nostr::Keygen.new
|
||||
# a) Use an existing keypair
|
||||
keypair = Nostr::KeyPair.new(
|
||||
private_key: Nostr::PrivateKey.new('add-your-hex-private-key-here'),
|
||||
public_key: Nostr::PublicKey.new('add-your-hex-public-key-here'),
|
||||
)
|
||||
|
||||
# b) Or build a keypair from a private key
|
||||
keygen = Nostr::Keygen.new
|
||||
keypair = keygen.get_key_pair_from_private_key(
|
||||
Nostr::PrivateKey.new('add-your-hex-private-key-here')
|
||||
)
|
||||
|
||||
# c) Or create a new keypair
|
||||
keygen = Nostr::Keygen.new
|
||||
keypair = keygen.generate_key_pair
|
||||
|
||||
keypair.private_key
|
||||
keypair.public_key
|
||||
```
|
||||
# Create a user with the keypair
|
||||
user = Nostr::User.new(keypair: keypair)
|
||||
|
||||
### Generating a private key and a public key
|
||||
|
||||
```ruby
|
||||
keygen = Nostr::Keygen.new
|
||||
|
||||
private_key = keygen.generate_private_key
|
||||
public_key = keygen.extract_public_key(private_key)
|
||||
```
|
||||
|
||||
### Connecting to a Relay
|
||||
|
||||
Clients can connect to multiple Relays. In this version, a Client can only connect to a single Relay at a time.
|
||||
|
||||
You may instantiate multiple Clients and multiple Relays.
|
||||
|
||||
```ruby
|
||||
client = Nostr::Client.new
|
||||
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
|
||||
# Create a signed event
|
||||
text_note_event = user.create_event(
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
|
||||
# Connect asynchronously to a relay
|
||||
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
|
||||
client.connect(relay)
|
||||
```
|
||||
|
||||
### WebSocket events
|
||||
# Listen asynchronously for the connect event
|
||||
client.on :connect do |relay|
|
||||
# Send the event to the Relay
|
||||
client.publish(text_note_event)
|
||||
|
||||
All communication between clients and relays happen in WebSockets.
|
||||
# Create a filter to receive the first 20 text notes
|
||||
# and encrypted direct messages from the relay that
|
||||
# were created in the previous hour
|
||||
filter = Nostr::Filter.new(
|
||||
kinds: [
|
||||
Nostr::EventKind::TEXT_NOTE,
|
||||
Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE
|
||||
],
|
||||
since: Time.now.to_i - 3600, # 1 hour ago
|
||||
until: Time.now.to_i,
|
||||
limit: 20,
|
||||
)
|
||||
|
||||
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
|
||||
# Subscribe to events matching conditions of a filter
|
||||
subscription = client.subscribe(filter: filter)
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
# all the code goes here
|
||||
end
|
||||
```
|
||||
|
||||
The `:close` event is fired when a connection with a WebSocket has been closed because of an error.
|
||||
|
||||
```ruby
|
||||
client.on :error do |error_message|
|
||||
puts error_message
|
||||
# Unsubscribe from events matching the filter above
|
||||
client.unsubscribe(subscription.id)
|
||||
end
|
||||
|
||||
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com'
|
||||
```
|
||||
|
||||
The `:message` event is fired when data is received through a WebSocket.
|
||||
|
||||
```ruby
|
||||
# Listen for incoming messages and print them
|
||||
client.on :message do |message|
|
||||
puts message
|
||||
end
|
||||
|
||||
# [
|
||||
# "EVENT",
|
||||
# "d34107357089bfc9882146d3bfab0386",
|
||||
# {
|
||||
# "content":"",
|
||||
# "created_at":1676456512,
|
||||
# "id":"18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
|
||||
# "kind":3,
|
||||
# "pubkey":"117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
|
||||
# "sig":"d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
|
||||
# "tags":[
|
||||
# ["p","1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
```
|
||||
# Listen for error messages
|
||||
client.on :error do |error_message|
|
||||
# Handle the error
|
||||
end
|
||||
|
||||
The `:close` event is fired when a connection with a WebSocket is closed.
|
||||
|
||||
```ruby
|
||||
# Listen for the close event
|
||||
client.on :close do |code, reason|
|
||||
# you may attempt to reconnect
|
||||
|
||||
client.connect(relay)
|
||||
# You may attempt to reconnect to the relay here
|
||||
end
|
||||
|
||||
# This line keeps the background client from exiting immediately.
|
||||
gets
|
||||
```
|
||||
|
||||
### Requesting for events / creating a subscription
|
||||
## 📚 Documentation
|
||||
|
||||
A client can request events and subscribe to new updates after it has established a connection with the Relay.
|
||||
I made a detailed documentation for this gem and it's usage. The code is also fully documented using YARD.
|
||||
|
||||
You may use a `Nostr::Filter` instance with as many attributes as you wish:
|
||||
- [Guide documentation](https://nostr-ruby.com)
|
||||
- [YARD documentation](https://rubydoc.info/gems/nostr)
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
filter = Nostr::Filter.new(
|
||||
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
|
||||
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
|
||||
kinds: [Nostr::EventKind::TEXT_NOTE],
|
||||
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
|
||||
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
|
||||
since: 1230981305,
|
||||
until: 1292190341,
|
||||
limit: 420,
|
||||
)
|
||||
## ✅ Implemented NIPs
|
||||
|
||||
subscription = client.subscribe('a_random_subscription_id', filter)
|
||||
end
|
||||
```
|
||||
- [x] [NIP-01 - Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
||||
- [x] [NIP-02 - Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md)
|
||||
- [x] [NIP-04 - Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
|
||||
- [x] [NIP-19 - Bech32-encoded entities](https://github.com/nostr-protocol/nips/blob/master/19.md)
|
||||
|
||||
With just a few:
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
|
||||
subscription = client.subscribe('a_random_subscription_id', filter)
|
||||
end
|
||||
```
|
||||
|
||||
Or omit the filter:
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
subscription = client.subscribe('a_random_subscription_id')
|
||||
end
|
||||
```
|
||||
|
||||
Or even omit the subscription id:
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
subscription = client.subscribe('a_random_subscription_id')
|
||||
end
|
||||
```
|
||||
|
||||
### Stop previous subscriptions
|
||||
|
||||
You can stop receiving messages from a subscription by calling `#unsubscribe`:
|
||||
|
||||
```ruby
|
||||
client.unsubscribe('your_subscription_id')
|
||||
```
|
||||
|
||||
### Publishing an event
|
||||
|
||||
To publish an event you need a keypair.
|
||||
|
||||
```ruby
|
||||
# Create a keypair
|
||||
keypair = Nostr::KeyPair.new(
|
||||
private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
|
||||
public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
|
||||
)
|
||||
|
||||
# Add the keypair to the user facade
|
||||
user = Nostr::User.new(keypair: keypair)
|
||||
|
||||
# Create a signed event
|
||||
event = user.create_event(
|
||||
created_at: 1667422587, # optional, defaults to the current time
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
tags: [], # optional, defaults to []
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
|
||||
# Send it to the Relay
|
||||
client.publish(event)
|
||||
```
|
||||
|
||||
### Creating/updating your contact list
|
||||
|
||||
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
|
||||
|
||||
```ruby
|
||||
# Creating a contact list event with 2 contacts
|
||||
update_contacts_event = user.create_event(
|
||||
kind: Nostr::EventKind::CONTACT_LIST,
|
||||
tags: [
|
||||
[
|
||||
"p", # mandatory
|
||||
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
|
||||
"wss://alicerelay.com/", # can be an empty string or can be omitted
|
||||
"alice" # can be an empty string or can be omitted
|
||||
],
|
||||
[
|
||||
"p", # mandatory
|
||||
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
|
||||
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
|
||||
"bob" # can be an empty string or can be omitted
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
# Send it to the Relay
|
||||
client.publish(update_contacts_event)
|
||||
```
|
||||
|
||||
### Sending an encrypted direct message
|
||||
|
||||
```ruby
|
||||
sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
|
||||
|
||||
encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
|
||||
sender_private_key: sender_private_key,
|
||||
recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
|
||||
plain_text: 'Your feedback is appreciated, now pay $8',
|
||||
previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
|
||||
)
|
||||
|
||||
encrypted_direct_message.sign(sender_private_key)
|
||||
|
||||
# #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
|
||||
# @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
|
||||
# @created_at=1676456512,
|
||||
# @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
|
||||
# @kind=4,
|
||||
# @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
|
||||
# @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
|
||||
# @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
|
||||
|
||||
# Send it to the Relay
|
||||
client.publish(encrypted_direct_message)
|
||||
````
|
||||
|
||||
## Implemented NIPs
|
||||
|
||||
- [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
||||
- [x] [NIP-02 - Client](https://github.com/nostr-protocol/nips/blob/master/02.md)
|
||||
- [x] [NIP-04 - Client](https://github.com/nostr-protocol/nips/blob/master/04.md)
|
||||
|
||||
## Development
|
||||
## 🔨 Development
|
||||
|
||||
After checking out the repo, run `bin/setup` to install dependencies.
|
||||
|
||||
|
@ -323,17 +181,22 @@ used to provide type checking and autocompletion in your editor. Run `bundle exe
|
|||
an RBS definition for the given Ruby file. And validate all definitions using [Steep](https://github.com/soutaro/steep)
|
||||
with the command `bundle exec steep check`.
|
||||
|
||||
## Contributing
|
||||
## 🐞 Issues & Bugs
|
||||
|
||||
If you find any issues or bugs, please report them [here](https://github.com/wilsonsilva/nostr/issues), I will be happy
|
||||
to have a look at them and fix them as soon as possible.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Bug reports and pull requests are welcome on GitHub at https://github.com/wilsonsilva/nostr.
|
||||
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere
|
||||
to the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
||||
## License
|
||||
## 📜 License
|
||||
|
||||
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
||||
|
||||
## Code of Conduct
|
||||
## 👔 Code of Conduct
|
||||
|
||||
Everyone interacting in the Nostr project's codebases, issue trackers, chat rooms and mailing lists is expected
|
||||
to follow the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md).
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# 1. Record architecture decisions
|
||||
|
||||
Date: 2024-03-13
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
We need to record the architectural decisions made on this project.
|
||||
|
||||
## Decision
|
||||
|
||||
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
|
||||
|
||||
## Consequences
|
||||
|
||||
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
|
|
@ -0,0 +1,27 @@
|
|||
# 2. introduction-of-signature-class
|
||||
|
||||
Date: 2024-03-14
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
I noticed significant overuse of primitive strings for signatures, which led to widespread and repetitive validation logic, increasing the potential for errors and making the system harder to manage and maintain.
|
||||
|
||||
## Decision
|
||||
|
||||
I introduced the Nostr::Signature class, choosing to subclass String to leverage string-like behavior while embedding specific validation rules for signatures. This move was aimed at streamlining validation, ensuring consistency, and maintaining the usability of strings.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- This design choice has made the codebase cleaner and more robust, reducing the chances of errors related to signature handling. It ensures that all signature instances are valid at creation, leveraging the familiarity and flexibility of string operations without sacrificing the integrity of the data. Moreover, it sets a strong foundation for extending signature-related functionality in the future.
|
||||
|
||||
### Negative
|
||||
|
||||
- __Performance Concerns:__ Subclassing String might introduce slight performance overheads due to the additional validation logic executed upon instantiation of a Signature object.
|
||||
- __Integration Challenges:__ Integrating this class into existing systems where strings were used indiscriminately for signatures requires careful refactoring to ensure compatibility. There's also the potential for issues when passing Nostr::Signature objects to libraries or APIs expecting plain strings without the additional constraints.
|
||||
- __Learning Curve:__ For new team members or contributors, understanding the necessity and functionality of the Nostr::Signature class adds to the learning curve, potentially slowing down initial development efforts as they familiarize themselves with the custom implementation.
|
|
@ -0,0 +1,122 @@
|
|||
# 3. Logging methods vs logger class
|
||||
|
||||
Date: 2024-03-19
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
I'm deciding between integrating logging directly into the main class or creating a dedicated logger class.
|
||||
|
||||
### Option 1: Logging methods
|
||||
|
||||
The first approach weaves logging actions into the operational code, resulting in a tight coupling of functionality and
|
||||
logging. Classes should be open for extension but closed for modification, and this strategy violates that principle.
|
||||
|
||||
```ruby
|
||||
class Client
|
||||
def connect(relay)
|
||||
execute_within_an_em_thread do
|
||||
client = build_websocket_client(relay.url)
|
||||
parent_to_child_channel.subscribe do |msg|
|
||||
client.send(msg)
|
||||
emit(:send, msg)
|
||||
log_send(msg) # <------ new code
|
||||
end
|
||||
|
||||
client.on :open do
|
||||
child_to_parent_channel.push(type: :open, relay:)
|
||||
log_connection_opened(relay) # <------ new code
|
||||
end
|
||||
|
||||
client.on :message do |event|
|
||||
child_to_parent_channel.push(type: :message, data: event.data)
|
||||
log_message_received(event.data) # <------ new code
|
||||
end
|
||||
|
||||
client.on :error do |event|
|
||||
child_to_parent_channel.push(type: :error, message: event.message)
|
||||
log_error(event.message) # <------ new code
|
||||
end
|
||||
|
||||
client.on :close do |event|
|
||||
child_to_parent_channel.push(type: :close, code: event.code, reason: event.reason)
|
||||
log_connection_closed(event.code, event.reason) # <------ new code
|
||||
end
|
||||
end
|
||||
|
||||
# ------ new code below ------
|
||||
|
||||
def log_send(msg)
|
||||
logger.info("Message sent: #{msg}")
|
||||
end
|
||||
|
||||
def log_connection_opened(relay)
|
||||
logger.info("Connection opened to #{relay.url}")
|
||||
end
|
||||
|
||||
def log_message_received(data)
|
||||
logger.info("Message received: #{data}")
|
||||
end
|
||||
|
||||
def log_error(message)
|
||||
logger.error("Error: #{message}")
|
||||
end
|
||||
|
||||
def log_connection_closed(code, reason)
|
||||
logger.info("Connection closed with code: #{code}, reason: #{reason}")
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Option 2: Logger class
|
||||
|
||||
The second strategy separates logging into its own class, promoting cleaner code and adherence to the Single
|
||||
Responsibility Principle. Client already exposes events that can be tapped into, so the logger class can listen to these
|
||||
events and log accordingly.
|
||||
|
||||
```ruby
|
||||
class ClientLogger
|
||||
def attach_to(client)
|
||||
logger_instance = self
|
||||
|
||||
client.on(:connect) { |relay| logger_instance.on_connect(relay) }
|
||||
client.on(:message) { |message| logger_instance.on_message(message) }
|
||||
client.on(:send) { |message| logger_instance.on_send(message) }
|
||||
client.on(:error) { |message| logger_instance.on_error(message) }
|
||||
client.on(:close) { |code, reason| logger_instance.on_close(code, reason) }
|
||||
end
|
||||
|
||||
def on_connect(relay); end
|
||||
def on_message(message); end
|
||||
def on_send(message); end
|
||||
def on_error(message); end
|
||||
def on_close(code, reason); end
|
||||
end
|
||||
|
||||
client = Nostr::Client.new
|
||||
logger = Nostr::ClientLogger.new
|
||||
logger.attach_to(client)
|
||||
```
|
||||
|
||||
This approach decouples logging from the main class, making it easier to maintain and extend the logging system without
|
||||
affecting the core logic.
|
||||
|
||||
## Decision
|
||||
|
||||
I've chosen the dedicated logger class route. This choice is driven by a desire for extensibility in the logging system.
|
||||
With a separate logger, I can easily modify logging behavior—like changing formats, adjusting verbosity levels,
|
||||
switching colors, or altering output destinations (files, networks, etc.) — without needing to rewrite any of the main
|
||||
operational code.
|
||||
|
||||
## Consequences
|
||||
|
||||
Adopting a dedicated logger class offers greater flexibility and simplifies maintenance, making it straightforward to
|
||||
adjust how and what I log independently of the core logic. This separation of concerns means that any future changes
|
||||
to logging preferences or requirements can be implemented quickly and without risk to the main class's functionality.
|
||||
However, it's important to manage the integration carefully to avoid introducing complexity, such as handling
|
||||
dependencies and ensuring seamless communication between the main operations and the logging system.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# 3. Default Logging Activation
|
||||
|
||||
Date: 2024-03-19
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Logging provides visibility into the gem's behavior and helps to diagnose issues. The decision centered on whether
|
||||
to enable logging by default or require manual activation.
|
||||
|
||||
### Option 1: On-demand Logging
|
||||
|
||||
```ruby
|
||||
client = Nostr::Client.new
|
||||
logger = Nostr::ClientLogger.new
|
||||
logger.attach_to(client)
|
||||
```
|
||||
|
||||
#### Advantages:
|
||||
|
||||
- Offers users flexibility and control over logging.
|
||||
- Conserves resources by logging only when needed.
|
||||
|
||||
#### Disadvantages:
|
||||
|
||||
- Potential to miss critical logs if not enabled.
|
||||
- Requires users to read and understand documentation to enable logging.
|
||||
- Requires users to manually activate and configure logging.
|
||||
|
||||
### Option 2: Automatic Logging
|
||||
|
||||
```ruby
|
||||
class Client
|
||||
def initialize(logger: ClientLogger.new)
|
||||
@logger = logger
|
||||
logger&.attach_to(self)
|
||||
end
|
||||
end
|
||||
|
||||
client = Nostr::Client.new
|
||||
```
|
||||
|
||||
#### Advantages:
|
||||
|
||||
- Ensures comprehensive logging without user intervention.
|
||||
- Simplifies debugging and monitoring.
|
||||
- Balances logging detail with performance impact.
|
||||
|
||||
#### Disadvantages:
|
||||
|
||||
- Needs additional steps to disable logging.
|
||||
|
||||
## Decision
|
||||
|
||||
Logging will be enabled by default. Users can disable logging by passing a `nil` logger to the client:
|
||||
|
||||
```ruby
|
||||
client = Nostr::Client.new(logger: nil)
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
Enabling logging by default favors ease of use and simplifies the developer's experience.
|
|
@ -0,0 +1,32 @@
|
|||
# 3. Logger Types
|
||||
|
||||
Date: 2024-04-01
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
In developing the `Nostr::Client` logging mechanism, I identified a need to cater to multiple development environments
|
||||
and developer preferences. The consideration was whether to implement a singular logger type or to introduce
|
||||
multiple, specialized loggers. The alternatives were:
|
||||
|
||||
- A single `ClientLogger` providing a one-size-fits-all solution.
|
||||
- Multiple logger types:
|
||||
- `Nostr::Client::Logger`: The base class for logging, providing core functionalities.
|
||||
- `Nostr::Client::ColorLogger`: An extension of the base logger, introducing color-coded outputs for enhanced readability in environments that support ANSI colors.
|
||||
- `Nostr::Client::PlainLogger`: A variation that produces logs without color coding, suitable for environments lacking color support or for users preferring plain text.
|
||||
|
||||
## Decision
|
||||
|
||||
I decided to implement the latter option: three specific kinds of loggers (`Nostr::Client::Logger`,
|
||||
`Nostr::Client::ColorLogger`, and `Nostr::Client::PlainLogger`). This approach is intended to offer flexibility and
|
||||
cater to the varied preferences and requirements of our users, recognizing the diverse environments in which our
|
||||
library might be used.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `Developer Choice`: Developers gain the ability to select the one that best matches their needs and environmental constraints, thereby enhancing the library's usability.
|
||||
- `Code Complexity`: While introducing multiple logger types increases the library's code complexity, this is offset by the significant gain in flexibility and user satisfaction.
|
||||
- `Broad Compatibility`: This decision ensures that the logging mechanism is adaptable to a wide range of operational environments, enhancing the library's overall robustness and accessibility.
|
|
@ -0,0 +1,4 @@
|
|||
/node_modules/
|
||||
.vitepress/dist
|
||||
.vitepress/temp
|
||||
.vitepress/cache
|
|
@ -0,0 +1,115 @@
|
|||
import { defineConfig } from 'vitepress'
|
||||
import { withMermaid } from "vitepress-plugin-mermaid";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
// https://www.npmjs.com/package/vitepress-plugin-mermaid
|
||||
export default defineConfig(withMermaid({
|
||||
title: "Nostr",
|
||||
description: "Documentation of the Nostr Ruby gem",
|
||||
// https://vitepress.dev/reference/site-config#head
|
||||
head: [
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-last-updated
|
||||
lastUpdated: true,
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Guide', link: '/getting-started/overview' }
|
||||
],
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-search
|
||||
search: {
|
||||
provider: 'local'
|
||||
},
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-sidebar
|
||||
sidebar: [
|
||||
{
|
||||
text: 'Getting started',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Overview', link: '/getting-started/overview' },
|
||||
{ text: 'Installation', link: '/getting-started/installation' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Core',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Client', link: '/core/client' },
|
||||
{ text: 'Keys', link: '/core/keys' },
|
||||
{ text: 'User', link: '/core/user' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Relays',
|
||||
items: [
|
||||
{ text: 'Connecting to a relay', link: '/relays/connecting-to-a-relay' },
|
||||
{ text: 'Publishing events', link: '/relays/publishing-events' },
|
||||
{ text: 'Receiving events', link: '/relays/receiving-events' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Subscriptions',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Creating a subscription', link: '/subscriptions/creating-a-subscription' },
|
||||
{ text: 'Filtering subscription events', link: '/subscriptions/filtering-subscription-events' },
|
||||
{ text: 'Updating a subscription', link: '/subscriptions/updating-a-subscription' },
|
||||
{ text: 'Deleting a subscription', link: '/subscriptions/deleting-a-subscription' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Events',
|
||||
link: '/events',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Set Metadata', link: '/events/set-metadata' },
|
||||
{ text: 'Text Note', link: '/events/text-note' },
|
||||
{ text: 'Recommend Server', link: '/events/recommend-server' },
|
||||
{ text: 'Contact List', link: '/events/contact-list' },
|
||||
{ text: 'Encrypted Direct Message', link: '/events/encrypted-direct-message' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Common use cases',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Logging and debugging', link: '/common-use-cases/logging-and-debugging' },
|
||||
{ text: 'Bech32 enc/decoding (NIP-19)', link: '/common-use-cases/bech32-encoding-and-decoding-(NIP-19)' },
|
||||
{ text: 'Signing/verifying messages', link: '/common-use-cases/signing-and-verifying-messages' },
|
||||
{ text: 'Signing/verifying events', link: '/common-use-cases/signing-and-verifying-events' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Implemented NIPs',
|
||||
link: '/implemented-nips',
|
||||
},
|
||||
],
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-config#sociallinks
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/wilsonsilva/nostr' }
|
||||
],
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-edit-link
|
||||
editLink: {
|
||||
pattern: 'https://github.com/wilsonsilva/nostr/edit/main/docs/:path',
|
||||
text: 'Edit this page on GitHub'
|
||||
},
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-footer
|
||||
footer: {
|
||||
message: 'Released under the <a href="https://github.com/wilsonsilva/nostr/blob/main/LICENSE.txt">MIT License</a>.',
|
||||
copyright: 'Copyright © 2023-present <a href="https://github.com/wilsonsilva">Wilson Silva</a>'
|
||||
}
|
||||
},
|
||||
|
||||
// https://vitepress.dev/reference/site-config#ignoredeadlinks
|
||||
ignoreDeadLinks: [
|
||||
/^https?:\/\/localhost/
|
||||
],
|
||||
}))
|
|
@ -0,0 +1,44 @@
|
|||
# Nostr Docs
|
||||
|
||||
VitePress-powered documentation for the Nostr Ruby gem.
|
||||
|
||||
## Live Demo
|
||||
|
||||
https://nostr-ruby.com/
|
||||
|
||||
## Development
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Bun](https://bun.sh/)
|
||||
|
||||
### Installation
|
||||
|
||||
```shell
|
||||
bun install
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
The `docs:dev` script will start a local dev server with instant hot updates. Run it with the following command:
|
||||
|
||||
```shell
|
||||
bun run docs:dev
|
||||
```
|
||||
|
||||
Run this command to build the docs:
|
||||
|
||||
```shell
|
||||
bun run docs:build
|
||||
```
|
||||
|
||||
Once built, preview it locally by running:
|
||||
|
||||
```shell
|
||||
bun run docs:preview
|
||||
```
|
||||
|
||||
The preview command will boot up a local static web server that will serve the output directory .`vitepress/dist` at
|
||||
http://localhost:4173. You can use this to make sure everything looks good before pushing to production.
|
||||
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Runtime API Examples
|
||||
|
||||
This page demonstrates usage of some of the runtime APIs provided by VitePress.
|
||||
|
||||
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
|
||||
|
||||
```md
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
```
|
||||
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { site, theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
|
Binary file not shown.
|
@ -0,0 +1,190 @@
|
|||
# Encoding/decoding bech-32 strings (NIP-19)
|
||||
|
||||
[NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) standardizes bech32-formatted strings that can be
|
||||
used to display keys, ids and other information in clients. These formats are not meant to be used anywhere in the core
|
||||
protocol, they are only meant for displaying to users, copy-pasting, sharing, rendering QR codes and inputting data.
|
||||
|
||||
|
||||
In order to guarantee the deterministic nature of the documentation, the examples below assume that there is a `keypair`
|
||||
variable with the following values:
|
||||
|
||||
```ruby
|
||||
keypair = Nostr::KeyPair.new(
|
||||
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
|
||||
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
|
||||
)
|
||||
|
||||
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
||||
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
```
|
||||
|
||||
## Public key (npub)
|
||||
|
||||
### Encoding
|
||||
|
||||
```ruby
|
||||
npub = Nostr::Bech32.npub_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
||||
npub # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
||||
```
|
||||
|
||||
### Decoding
|
||||
|
||||
```ruby
|
||||
type, public_key = Nostr::Bech32.decode('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg')
|
||||
type # => 'npub'
|
||||
public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
```
|
||||
|
||||
## Private key (nsec)
|
||||
|
||||
### Encoding
|
||||
|
||||
```ruby
|
||||
nsec = Nostr::Bech32.nsec_encode('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
|
||||
nsec # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
|
||||
```
|
||||
|
||||
### Decoding
|
||||
|
||||
```ruby
|
||||
type, private_key = Nostr::Bech32.decode('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5')
|
||||
type # => 'npub'
|
||||
private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
||||
```
|
||||
|
||||
## Relay (nrelay)
|
||||
|
||||
### Encoding
|
||||
|
||||
```ruby
|
||||
nrelay = Nostr::Bech32.nrelay_encode('wss://relay.damus.io')
|
||||
nrelay # => 'nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x'
|
||||
```
|
||||
|
||||
### Decoding
|
||||
|
||||
```ruby
|
||||
type, data = Nostr::Bech32.decode('nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x')
|
||||
|
||||
type # => 'nrelay'
|
||||
data.entries.first.label # => 'relay'
|
||||
data.entries.first.value # => 'wss://relay.damus.io'
|
||||
```
|
||||
|
||||
## Event (nevent)
|
||||
|
||||
### Encoding
|
||||
|
||||
```ruby{8-12}
|
||||
user = Nostr::User.new(keypair: keypair)
|
||||
text_note_event = user.create_event(
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
created_at: 1700467997,
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
|
||||
nevent = Nostr::Bech32.nevent_encode(
|
||||
id: text_note_event.id,
|
||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
)
|
||||
|
||||
nevent # => 'nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0praxwkjagcpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqs03k8v3'
|
||||
```
|
||||
|
||||
### Decoding
|
||||
|
||||
```ruby
|
||||
type, event = Nostr::Bech32.decode('nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0praxwkjagcpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqs03k8v3')
|
||||
|
||||
type # => 'nevent'
|
||||
event.entries[0].label # => 'author'
|
||||
event.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
event.entries[1].relay # => 'relay'
|
||||
event.entries[1].value # => 'wss://relay.damus.io'
|
||||
event.entries[2].label # => 'relay'
|
||||
event.entries[2].value # => 'wss://nos.lol'
|
||||
event.entries[3].label # => 'kind'
|
||||
event.entries[3].value # => 1
|
||||
```
|
||||
|
||||
## Address (naddr)
|
||||
|
||||
### Encoding
|
||||
|
||||
```ruby
|
||||
naddr = Nostr::Bech32.naddr_encode(
|
||||
pubkey: keypair.public_key,
|
||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
identifier: 'damus',
|
||||
)
|
||||
|
||||
naddr # => 'naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqsqptyv9kh2uc3qfs2p'
|
||||
```
|
||||
|
||||
### Decoding
|
||||
|
||||
```ruby
|
||||
type, addr = Nostr::Bech32.decode('naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqsqptyv9kh2uc3qfs2p')
|
||||
|
||||
type # => 'naddr'
|
||||
addr.entries[0].label # => 'author'
|
||||
addr.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
addr.entries[1].label # => 'relay'
|
||||
addr.entries[1].value # => 'wss://relay.damus.io'
|
||||
addr.entries[2].label # => 'relay'
|
||||
addr.entries[2].value # => 'wss://nos.lol'
|
||||
addr.entries[3].label # => 'kind'
|
||||
addr.entries[3].value # => 1
|
||||
addr.entries[4].label # => 'identifier'
|
||||
addr.entries[4].value # => 'damus'
|
||||
```
|
||||
|
||||
## Profile (nprofile)
|
||||
|
||||
### Encoding
|
||||
```ruby
|
||||
relay_urls = %w[wss://relay.damus.io wss://nos.lol]
|
||||
nprofile = Nostr::Bech32.nprofile_encode(pubkey: keypair.public_key, relays: relay_urls)
|
||||
|
||||
nprofile # => nprofile1qqs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dsxe58m5
|
||||
```
|
||||
|
||||
### Decoding
|
||||
|
||||
```ruby
|
||||
type, profile = Nostr::Bech32.decode('nprofile1qqs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dsxe58m5')
|
||||
|
||||
type # => 'nprofile'
|
||||
profile.entries[0].label # => 'pubkey'
|
||||
profile.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
profile.entries[1].label # => 'relay'
|
||||
profile.entries[1].value # => 'wss://relay.damus.io'
|
||||
profile.entries[2].label # => 'relay'
|
||||
profile.entries[2].value # => 'wss://nos.lol'
|
||||
```
|
||||
|
||||
## Other simple types (note)
|
||||
|
||||
### Encoding
|
||||
|
||||
```ruby{8-9}
|
||||
user = Nostr::User.new(keypair: keypair)
|
||||
text_note_event = user.create_event(
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
created_at: 1700467997,
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
|
||||
note = Nostr::Bech32.encode(hrp: 'note', data: text_note_event.id)
|
||||
note # => 'note10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qnx3ujq'
|
||||
```
|
||||
|
||||
### Decoding
|
||||
|
||||
```ruby
|
||||
type, note = Nostr::Bech32.decode('note1pldep78zxnf5qrk6lhfx6jflzthup47793he7g0ej7z86vad963s42v0rr')
|
||||
type # => 'note'
|
||||
note # => '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3'
|
||||
```
|
|
@ -0,0 +1,60 @@
|
|||
# Logging and debugging
|
||||
|
||||
The `Nostr::Client` class provides built-in logging functionality to help you debug and monitor client interactions with
|
||||
relays. By default, the client uses the `ColorLogger`, which logs events in color. However, you can customize the
|
||||
logging behavior or disable it entirely.
|
||||
|
||||
## Disabling logging
|
||||
|
||||
To instantiate a client without any logging, simply pass `logger: nil` when creating the client instance:
|
||||
|
||||
```ruby
|
||||
client = Nostr::Client.new(logger: nil)
|
||||
```
|
||||
|
||||
This will disable all logging for the client.
|
||||
|
||||
## Formatting the logging
|
||||
|
||||
The `Nostr::Client::Logger` class is the base class for logging functionality. It defines the following methods for
|
||||
logging different events:
|
||||
|
||||
- `on_connect(relay)`: Logs when the client connects to a relay.
|
||||
- `on_message(message)`: Logs a message received from the relay.
|
||||
- `on_send(message)`: Logs a message sent to the relay.
|
||||
- `on_error(message)`: Logs an error message.
|
||||
- `on_close(code, reason)`: Logs when the connection with a relay is closed.
|
||||
|
||||
You can create your own logger by subclassing `Nostr::Client::Logger` and overriding these methods to customize the
|
||||
logging format.
|
||||
|
||||
The `Nostr::Client::ColorLogger` is a built-in logger that logs events in color. It uses ANSI escape codes to add color
|
||||
to the log output. Here's an example of how the ColorLogger formats the log messages:
|
||||
|
||||
- Connection: `"\u001b[32m\u001b[1mConnected to the relay\u001b[22m #{relay.name} (#{relay.url})\u001b[0m"`
|
||||
- Message received: `"\u001b[32m\u001b[1m◄-\u001b[0m #{message}"`
|
||||
- Message sent: `"\u001b[32m\u001b[1m-►\u001b[0m #{message}"`
|
||||
- Error: `"\u001b[31m\u001b[1mError: \u001b[22m#{message}\u001b[0m"`
|
||||
- Connection closed: `"\u001b[31m\u001b[1mConnection closed: \u001b[22m#{reason} (##{code})\u001b[0m"`
|
||||
|
||||
## Plain text logging
|
||||
|
||||
If you prefer plain text logging without colors, you can use the `Nostr::Client::PlainLogger`. This logger formats the
|
||||
log messages in a simple, readable format without any ANSI escape codes.
|
||||
|
||||
To use the `PlainLogger`, pass it as the `logger` option when creating the client instance:
|
||||
|
||||
```ruby
|
||||
client = Nostr::Client.new(logger: Nostr::Client::PlainLogger.new)
|
||||
```
|
||||
|
||||
The `PlainLogger` formats the log messages as follows:
|
||||
|
||||
- Connection: `"Connected to the relay #{relay.name} (#{relay.url})"`
|
||||
- Message received: `"◄- #{message}"`
|
||||
- Message sent: `"-► #{message}"`
|
||||
- Error: `"Error: #{message}"`
|
||||
- Connection closed: `"Connection closed: #{reason} (##{code})"`
|
||||
|
||||
By using the appropriate logger or creating your own custom logger, you can effectively debug and monitor your Nostr
|
||||
client's interactions with relays.
|
|
@ -0,0 +1,50 @@
|
|||
# Signing and verifying events
|
||||
|
||||
Signing an event in Nostr proves it was sent by the owner of a specific private key.
|
||||
|
||||
## Signing an event
|
||||
|
||||
To sign an event, use the private key associated with the event's creator. Here's how to sign a message using a
|
||||
predefined keypair:
|
||||
|
||||
```ruby{14}
|
||||
require 'nostr'
|
||||
|
||||
private_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
|
||||
public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
|
||||
|
||||
event = Nostr::Event.new(
|
||||
pubkey: public_key.to_s,
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
content: 'We did it with security, now we’re going to do it with the economy.',
|
||||
created_at: Time.now.to_i,
|
||||
)
|
||||
|
||||
# Sign the event with the private key
|
||||
event.sign(private_key)
|
||||
|
||||
puts "Event ID: #{event.id}"
|
||||
puts "Event Signature: #{event.sig}"
|
||||
```
|
||||
|
||||
## Verifying an event's signature
|
||||
|
||||
To verify an event, you must ensure the event's signature is valid. This indicates the event was created by the owner
|
||||
of the corresponding public key.
|
||||
|
||||
When the event was signed with the private key corresponding to the public key, the `verify_signature` method will
|
||||
return `true`.
|
||||
|
||||
```ruby
|
||||
event.verify_signature # => true
|
||||
```
|
||||
|
||||
And when the event was not signed with the private key corresponding to the public key, the `verify_signature` method
|
||||
will return `false`.
|
||||
|
||||
An event without an `id`, `pubkey`, `sig` is considered invalid and will return `false` when calling `verify_signature`.
|
||||
|
||||
```ruby
|
||||
other_public_key = Nostr::PublicKey.new('10be96d345ed58d923a734560680f1adfd2b1006c28ac93b8e1b032a9a32c6e9')
|
||||
event.verify_signature # => false
|
||||
```
|
|
@ -0,0 +1,43 @@
|
|||
# Signing and verifying messages
|
||||
|
||||
Signing a message in Nostr proves it was sent by the owner of a specific private key.
|
||||
|
||||
## Signing a message
|
||||
|
||||
To sign a message, you'll need a private key. Here's how to sign a message using a predefined keypair:
|
||||
|
||||
```ruby{9}
|
||||
require 'nostr'
|
||||
|
||||
private_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
|
||||
public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
|
||||
|
||||
message = 'We did it with security, now we’re going to do it with the economy.' # The message you want to sign
|
||||
|
||||
crypto = Nostr::Crypto.new
|
||||
signature = crypto.sign_message(message, private_key)
|
||||
signature # => "d7a0aac1fadcddf1aa2949bedfcdf25ce0c1604e648e55d31431fdacbff8e8256f7c2166d98292f80bc5f79105a0b6e8a89236a47d97cf5d0e7cc1ebf34dea5c"
|
||||
```
|
||||
|
||||
## Verifying a signature
|
||||
|
||||
To verify a signature, you need the original message, the public key of the signer, and the signature.
|
||||
|
||||
```ruby
|
||||
crypto.valid_sig?(message, public_key, signature) # => true
|
||||
crypto.check_sig!(message, public_key, signature) # => true
|
||||
```
|
||||
|
||||
When the message was not signed with the private key corresponding to the public key, the `valid_sig?` method will return `false`.
|
||||
|
||||
```ruby
|
||||
other_public_key = Nostr::PublicKey.new('10be96d345ed58d923a734560680f1adfd2b1006c28ac93b8e1b032a9a32c6e9')
|
||||
crypto.valid_sig?(message, public_key, signature) # => false
|
||||
```
|
||||
|
||||
And when the message was not signed with the private key corresponding to the public key, the `check_sig!` method will raise an error.
|
||||
|
||||
```ruby
|
||||
other_public_key = Nostr::PublicKey.new('10be96d345ed58d923a734560680f1adfd2b1006c28ac93b8e1b032a9a32c6e9')
|
||||
crypto.check_sig!(message, other_public_key, signature) # => Schnorr::InvalidSignatureError: signature verification failed
|
||||
```
|
|
@ -0,0 +1,108 @@
|
|||
# Client
|
||||
|
||||
Clients establish a WebSocket connection to [relays](../relays/connecting-to-a-relay). Through this connection, clients
|
||||
communicate and subscribe to a range of [Nostr events](../events) based on specified subscription filters. These filters
|
||||
define the Nostr events a client wishes to receive updates about.
|
||||
|
||||
::: info
|
||||
Clients do not need to sign up or create an account to use Nostr. Upon connecting to a relay, a client provides
|
||||
its subscription filters. The relay then streams events that match these filters to the client for the duration of the
|
||||
connection.
|
||||
:::
|
||||
|
||||
## WebSocket events
|
||||
|
||||
Communication between clients and relays happen via WebSockets. The client will emit WebSocket events when the
|
||||
connection is __opened__, __closed__, when a __message__ is received or when there's an __error__.
|
||||
|
||||
::: info
|
||||
WebSocket events are not [Nostr events](https://nostr.com/the-protocol/events). They are events emitted by the
|
||||
WebSocket connection. The WebSocket `:message` event, however, contains a Nostr event in its payload.
|
||||
:::
|
||||
|
||||
### connect
|
||||
|
||||
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
|
||||
|
||||
```ruby
|
||||
client = Nostr::Client.new
|
||||
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
|
||||
|
||||
client.on :connect do |relay|
|
||||
# When this block executes, you're connected to the relay
|
||||
end
|
||||
|
||||
# Connect to a relay asynchronously
|
||||
client.connect(relay)
|
||||
```
|
||||
|
||||
Once the connection is open, you can send events to the relay, manage subscriptions, etc.
|
||||
|
||||
::: tip
|
||||
Define the connection event handler before calling
|
||||
[`Nostr::Client#connect`](https://www.rubydoc.info/gems/nostr/Nostr/Client#connect-instance_method). Otherwise,
|
||||
you may miss the event.
|
||||
:::
|
||||
|
||||
### error
|
||||
|
||||
The `:error` event is fired when a connection with a WebSocket has been closed because of an error.
|
||||
|
||||
```ruby
|
||||
client.on :error do |error_message|
|
||||
puts error_message
|
||||
end
|
||||
|
||||
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the
|
||||
# server certificate for 'rsslay.fiatjaf.com'
|
||||
```
|
||||
|
||||
### message
|
||||
|
||||
The `:message` event is fired when data is received through a WebSocket.
|
||||
|
||||
```ruby
|
||||
client.on :message do |message|
|
||||
puts message
|
||||
end
|
||||
```
|
||||
|
||||
The message will be one of these 4 types, which must also be JSON arrays, according to the following patterns:
|
||||
- `["EVENT", <subscription_id>, <event JSON>]`
|
||||
- `["OK", <event_id>, <true|false>, <message>]`
|
||||
- `["EOSE", <subscription_id>]`
|
||||
- `["NOTICE", <message>]`
|
||||
|
||||
::: details Click me to see how a WebSocket message looks like
|
||||
```ruby
|
||||
[
|
||||
"EVENT",
|
||||
"d34107357089bfc9882146d3bfab0386",
|
||||
{
|
||||
"content": "",
|
||||
"created_at": 1676456512,
|
||||
"id": "18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
|
||||
"kind": 3,
|
||||
"pubkey": "117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
|
||||
"sig": "d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
|
||||
"tags":[
|
||||
["p", "1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
:::
|
||||
|
||||
### close
|
||||
|
||||
The `:close` event is fired when a connection with a WebSocket is closed.
|
||||
|
||||
```ruby
|
||||
client.on :close do |code, reason|
|
||||
puts "Error: #{code} - #{reason}"
|
||||
end
|
||||
```
|
||||
|
||||
::: tip
|
||||
This handler is useful to attempt to reconnect to the relay.
|
||||
:::
|
|
@ -0,0 +1,136 @@
|
|||
# Keys
|
||||
|
||||
To [sign events](#signing-an-event), you need a **private key**. To verify signatures, you need a **public key**. The combination of a
|
||||
private and a public key is called a **keypair**.
|
||||
|
||||
Both public and private keys are 64-character hexadecimal strings. They can be represented in bech32 format,
|
||||
which is a human-readable format that starts with `nsec` for private keys and `npub` for public keys.
|
||||
|
||||
There are a few ways to generate a keypair.
|
||||
|
||||
## a) Generating a keypair
|
||||
|
||||
If you don't have any keys, you can generate a keypair using the
|
||||
[`Nostr::Keygen`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen) class:
|
||||
|
||||
```ruby
|
||||
keygen = Nostr::Keygen.new
|
||||
keypair = keygen.generate_key_pair
|
||||
|
||||
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
||||
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
```
|
||||
|
||||
## b) Generating a private key and a public key
|
||||
|
||||
Alternatively, if you have already generated a private key, you can extract the corresponding public key by calling
|
||||
`Keygen#extract_public_key`:
|
||||
|
||||
```ruby
|
||||
keygen = Nostr::Keygen.new
|
||||
|
||||
private_key = keygen.generate_private_key
|
||||
public_key = keygen.extract_public_key(private_key)
|
||||
|
||||
private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
||||
public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
```
|
||||
|
||||
## c) Using existing hexadecimal keys
|
||||
|
||||
If you already have a private key and a public key in hexadecimal format, you can create a keypair using the
|
||||
`Nostr::KeyPair` class:
|
||||
|
||||
```ruby
|
||||
keypair = Nostr::KeyPair.new(
|
||||
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
|
||||
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
|
||||
)
|
||||
|
||||
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
||||
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
```
|
||||
|
||||
### d) Use existing bech32 keys
|
||||
|
||||
If you already have a private key and a public key in bech32 format, you can create a keypair using the
|
||||
`Nostr::KeyPair` class:
|
||||
|
||||
```ruby
|
||||
keypair = Nostr::KeyPair.new(
|
||||
private_key: Nostr::PrivateKey.from_bech32('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'),
|
||||
public_key: Nostr::PublicKey.from_bech32('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'),
|
||||
)
|
||||
|
||||
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
||||
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
```
|
||||
|
||||
## e) Using an existing hexadecimal private key
|
||||
|
||||
If you already have a private key in hexadecimal format, you can create a keypair using the method
|
||||
[`Nostr::Keygen#get_key_pair_from_private_key`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen#get_key_pair_from_private_key-instance_method):
|
||||
|
||||
```ruby
|
||||
private_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
|
||||
|
||||
keygen= Nostr::Keygen.new
|
||||
keypair = keygen.get_key_pair_from_private_key(private_key)
|
||||
|
||||
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
||||
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
```
|
||||
|
||||
## f) Using an existing bech32 private key
|
||||
|
||||
If you already have a private key in bech32 format, you can create a keypair using the methods
|
||||
[`Nostr::PrivateKey.from_bech32`](https://www.rubydoc.info/gems/nostr/Nostr/PrivateKey.from_bech32-class_method) and
|
||||
[`Nostr::Keygen#get_key_pair_from_private_key`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen#get_key_pair_from_private_key-instance_method):
|
||||
|
||||
```ruby
|
||||
private_key = Nostr::PrivateKey.from_bech32('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5')
|
||||
|
||||
keygen= Nostr::Keygen.new
|
||||
keypair = keygen.get_key_pair_from_private_key(private_key)
|
||||
|
||||
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
||||
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
||||
```
|
||||
|
||||
## Signing an event
|
||||
|
||||
KeyPairs are used to sign [events](../events). To create a signed event, you need to instantiate a
|
||||
[`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) with a keypair:
|
||||
|
||||
```ruby{8,11-14}
|
||||
# Use an existing keypair
|
||||
keypair = Nostr::KeyPair.new(
|
||||
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
|
||||
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
|
||||
)
|
||||
|
||||
# Add the keypair to a user
|
||||
user = Nostr::User.new(keypair: keypair)
|
||||
|
||||
# Create signed events
|
||||
text_note = user.create_event(
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
```
|
||||
|
||||
::: details Click me to view the text_note
|
||||
|
||||
```ruby
|
||||
# text_note.to_h
|
||||
{
|
||||
id: '030fbc71151379e5b58e7428ed6e7f2884e5dfc9087fd64d1dc4cc677f5097c8',
|
||||
pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e', # from the keypair
|
||||
created_at: 1700119819,
|
||||
kind: 1, # Nostr::EventKind::TEXT_NOTE,
|
||||
tags: [],
|
||||
content: 'Your feedback is appreciated, now pay $8',
|
||||
sig: '586877896ef6f7d54fa4dd2ade04e3fdc4dfcd6166dd0df696b3c3c768868c0b690338f5baed6ab4fc717785333cb487363384de9fb0f740ac4775522cb4acb3' # signed with the private key from the keypair
|
||||
}
|
||||
```
|
||||
:::
|
|
@ -0,0 +1,43 @@
|
|||
# User
|
||||
|
||||
The class [`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) is an abstraction to facilitate the creation
|
||||
of signed events. It is not required to use it to create events, but it is recommended.
|
||||
|
||||
Here's an example of how to create a signed event without the class `Nostr::User`:
|
||||
|
||||
```ruby
|
||||
event = Nostr::Event.new(
|
||||
pubkey: keypair.public_key,
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
tags: [],
|
||||
content: 'Your feedback is appreciated, now pay $8',
|
||||
)
|
||||
event.sign(keypair.private_key)
|
||||
```
|
||||
|
||||
::: details Click me to view the event
|
||||
|
||||
```ruby
|
||||
# event.to_h
|
||||
{
|
||||
id: '5feb10973dbcf5f210cfc1f0aa338fee62bed6a29696a67957713599b9baf0eb',
|
||||
pubkey: 'b9b9821074d1b60b8fb4a3983632af3ef9669f55b20d515bf982cda5c439ad61',
|
||||
created_at: 1699847447,
|
||||
kind: 1, # Nostr::EventKind::TEXT_NOTE,
|
||||
tags: [],
|
||||
content: 'Your feedback is appreciated, now pay $8',
|
||||
sig: 'e30f2f08331f224e41a4099d16aefc780bf9f2d1191b71777e1e1789e6b51fdf7bb956f25d4ea9a152d1c66717a9d68c081ce6c89c298c3c5e794914013381ab'
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
And here's how to create it with the class `Nostr::User`:
|
||||
|
||||
```ruby
|
||||
user = Nostr::User.new(keypair: keypair)
|
||||
|
||||
event = user.create_event(
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
# Events
|
||||
|
||||
## Event Kinds
|
||||
|
||||
| kind | description | NIP |
|
||||
| ------------- |----------------------------------------------------------------| -------------------------------------------------------------- |
|
||||
| `0` | [Metadata](./events/set-metadata) | [1](https://github.com/nostr-protocol/nips/blob/master/01.md) |
|
||||
| `1` | [Short Text Note](./events/text-note) | [1](https://github.com/nostr-protocol/nips/blob/master/01.md) |
|
||||
| `2` | [Recommend Relay](./events/recommend-server) | |
|
||||
| `3` | [Contacts](./events/contact-list) | [2](https://github.com/nostr-protocol/nips/blob/master/02.md) |
|
||||
| `4` | [Encrypted Direct Messages](./events/encrypted-direct-message) | [4](https://github.com/nostr-protocol/nips/blob/master/04.md) |
|
|
@ -0,0 +1,29 @@
|
|||
# Contact List
|
||||
|
||||
## Creating/updating your contact list
|
||||
|
||||
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
|
||||
|
||||
```ruby
|
||||
# Creating a contact list event with 2 contacts
|
||||
update_contacts_event = user.create_event(
|
||||
kind: Nostr::EventKind::CONTACT_LIST,
|
||||
tags: [
|
||||
[
|
||||
"p", # mandatory
|
||||
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
|
||||
"wss://alicerelay.com/", # can be an empty string or can be omitted
|
||||
"alice" # can be an empty string or can be omitted
|
||||
],
|
||||
[
|
||||
"p", # mandatory
|
||||
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
|
||||
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
|
||||
"bob" # can be an empty string or can be omitted
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
# Send it to the Relay
|
||||
client.publish(update_contacts_event)
|
||||
```
|
|
@ -0,0 +1,28 @@
|
|||
# Encrypted Direct Message
|
||||
|
||||
## Sending an encrypted direct message
|
||||
|
||||
```ruby
|
||||
sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
|
||||
|
||||
encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
|
||||
sender_private_key: sender_private_key,
|
||||
recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
|
||||
plain_text: 'Your feedback is appreciated, now pay $8',
|
||||
previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
|
||||
)
|
||||
|
||||
encrypted_direct_message.sign(sender_private_key)
|
||||
|
||||
# #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
|
||||
# @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
|
||||
# @created_at=1676456512,
|
||||
# @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
|
||||
# @kind=4,
|
||||
# @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
|
||||
# @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
|
||||
# @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
|
||||
|
||||
# Send it to the Relay
|
||||
client.publish(encrypted_direct_message)
|
||||
```
|
|
@ -0,0 +1,32 @@
|
|||
# Recommend Server
|
||||
|
||||
The `Recommend Server` event, has a set of tags with the following structure `['e', <event-id>, <relay-url>, <marker>]`
|
||||
|
||||
Where:
|
||||
|
||||
- `<event-id>` is the id of the event being referenced.
|
||||
- `<relay-url>` is the URL of a recommended relay associated with the reference. Clients SHOULD add a valid `<relay-URL>`
|
||||
field, but may instead leave it as `''`.
|
||||
- `<marker>` is optional and if present is one of `'reply'`, `'root'`, or `'mention'`.
|
||||
Those marked with `'reply'` denote the id of the reply event being responded to. Those marked with `'root'` denote the
|
||||
root id of the reply thread being responded to. For top level replies (those replying directly to the root event),
|
||||
only the `'root'` marker should be used. Those marked with `'mention'` denote a quoted or reposted event id.
|
||||
|
||||
A direct reply to the root of a thread should have a single marked `'e'` tag of type `'root'`.
|
||||
|
||||
## Recommending a server
|
||||
|
||||
```ruby
|
||||
recommend_server_event = user.create_event(
|
||||
kind: Nostr::EventKind::RECOMMEND_SERVER,
|
||||
tags: [
|
||||
[
|
||||
'e',
|
||||
'461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
|
||||
'wss://relay.damus.io'
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
client.publish(recommend_server_event)
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
# Set Metadata
|
||||
|
||||
In the `Metadata` event, the `content` is set to a stringified JSON object
|
||||
`{name: <username>, about: <string>, picture: <url, string>}` describing the [user](../core/user) who created the event. A relay may
|
||||
delete older events once it gets a new one for the same pubkey.
|
||||
|
||||
## Setting the user's metadata
|
||||
|
||||
```ruby
|
||||
metadata_event = user.create_event(
|
||||
kind: Nostr::EventKind::SET_METADATA,
|
||||
content: {
|
||||
name: 'Wilson Silva',
|
||||
about: 'Used to make hydrochloric acid bombs in high school.',
|
||||
picture: 'https://thispersondoesnotexist.com/'
|
||||
}
|
||||
)
|
||||
|
||||
client.publish(metadata_event)
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
# Text Note
|
||||
|
||||
In the `Text Note` event, the `content` is set to the plaintext content of a note (anything the user wants to say).
|
||||
Content that must be parsed, such as Markdown and HTML, should not be used.
|
||||
|
||||
## Sending a text note event
|
||||
|
||||
```ruby
|
||||
text_note_event = user.create_event(
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
|
||||
client.publish(text_note_event)
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
# Installation
|
||||
|
||||
Install the gem and add to the application's Gemfile by executing:
|
||||
|
||||
```shell
|
||||
bundle add nostr
|
||||
```
|
||||
|
||||
If bundler is not being used to manage dependencies, install the gem by executing:
|
||||
|
||||
```shell
|
||||
gem install nostr
|
||||
```
|
||||
|
||||
## Requiring the gem
|
||||
|
||||
All examples in this guide assume that the gem has been required:
|
||||
|
||||
```ruby
|
||||
require 'nostr'
|
||||
```
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
editLink: true
|
||||
---
|
||||
|
||||
# Getting started
|
||||
|
||||
This gem abstracts the complexity that you would face when trying to connect to relays web sockets, send and receive
|
||||
events, handle events callbacks and much more.
|
||||
|
||||
## Visual overview
|
||||
|
||||
Begin your journey with an overview of the essential functions. A visual representation below maps out the key
|
||||
components we'll delve into in this section.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Client {
|
||||
connect(relay)
|
||||
publish(event)
|
||||
subscribe(subscription_id, filter)
|
||||
unsubscribe(subscription_id)
|
||||
}
|
||||
class Relay {
|
||||
url
|
||||
name
|
||||
}
|
||||
class Event {
|
||||
pubkey
|
||||
created_at
|
||||
kind
|
||||
tags
|
||||
content
|
||||
add_event_reference(event_id)
|
||||
add_pubkey_reference(pubkey)
|
||||
serialize()
|
||||
to_h()
|
||||
sign(private_key)
|
||||
verify_signature()
|
||||
}
|
||||
class Subscription {
|
||||
id
|
||||
filter
|
||||
}
|
||||
class Filter {
|
||||
ids
|
||||
authors
|
||||
kinds
|
||||
since
|
||||
until
|
||||
limit
|
||||
to_h()
|
||||
}
|
||||
class EventKind {
|
||||
<<Enumeration>>
|
||||
SET_METADATA
|
||||
TEXT_NOTE
|
||||
RECOMMEND_SERVER
|
||||
CONTACT_LIST
|
||||
ENCRYPTED_DIRECT_MESSAGE
|
||||
}
|
||||
class KeyPair {
|
||||
private_key
|
||||
public_key
|
||||
}
|
||||
class Keygen {
|
||||
generate_key_pair()
|
||||
generate_private_key()
|
||||
extract_public_key(private_key)
|
||||
}
|
||||
class User {
|
||||
keypair
|
||||
create_event(event_attributes)
|
||||
}
|
||||
|
||||
Client --> Relay : connects via <br> WebSockets to
|
||||
Client --> Event : uses WebSockets to <br> publish and receive
|
||||
Client --> Subscription : receives Events via
|
||||
Subscription --> Filter : uses
|
||||
Event --> EventKind : is of kind
|
||||
User --> KeyPair : has
|
||||
User --> Event : creates and signs
|
||||
User --> Keygen : uses to generate keys
|
||||
Keygen --> KeyPair : generates
|
||||
```
|
||||
|
||||
## Code overview
|
||||
|
||||
Explore the provided code snippet to learn about initializing the Nostr [client](../core/client.md), generating
|
||||
a [keypair](../core/keys), [publishing](../relays/publishing-events) an event, and
|
||||
efficiently [managing event subscriptions](../subscriptions/creating-a-subscription) (including event reception,
|
||||
filtering, and WebSocket event handling).
|
||||
|
||||
```ruby
|
||||
# Require the gem
|
||||
require 'nostr'
|
||||
|
||||
# Instantiate a client
|
||||
client = Nostr::Client.new
|
||||
|
||||
# a) Use an existing keypair
|
||||
keypair = Nostr::KeyPair.new(
|
||||
private_key: Nostr::PrivateKey.new('your-hex-private-key'),
|
||||
public_key: Nostr::PublicKey.new('your-hex-public-key'),
|
||||
)
|
||||
|
||||
# b) Or build a keypair from a private key
|
||||
keygen = Nostr::Keygen.new
|
||||
keypair = keygen.get_key_pair_from_private_key(
|
||||
Nostr::PrivateKey.new('your-hex-private-key')
|
||||
)
|
||||
|
||||
# c) Or create a new keypair
|
||||
keygen = Nostr::Keygen.new
|
||||
keypair = keygen.generate_key_pair
|
||||
|
||||
# Create a user with the keypair
|
||||
user = Nostr::User.new(keypair: keypair)
|
||||
|
||||
# Create a signed event
|
||||
text_note_event = user.create_event(
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
|
||||
# Connect asynchronously to a relay
|
||||
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
|
||||
client.connect(relay)
|
||||
|
||||
# Listen asynchronously for the connect event
|
||||
client.on :connect do
|
||||
# Send the event to the Relay
|
||||
client.publish(text_note_event)
|
||||
|
||||
# Create a filter to receive the first 20 text notes
|
||||
# and encrypted direct messages from the relay that
|
||||
# were created in the previous hour
|
||||
filter = Nostr::Filter.new(
|
||||
kinds: [
|
||||
Nostr::EventKind::TEXT_NOTE,
|
||||
Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE
|
||||
],
|
||||
since: Time.now.to_i - 3600, # 1 hour ago
|
||||
until: Time.now.to_i,
|
||||
limit: 20,
|
||||
)
|
||||
|
||||
# Subscribe to events matching conditions of a filter
|
||||
subscription = client.subscribe(filter: filter)
|
||||
|
||||
# Unsubscribe from events matching the filter above
|
||||
client.unsubscribe(subscription.id)
|
||||
end
|
||||
|
||||
# Listen for incoming messages and print them
|
||||
client.on :message do |message|
|
||||
puts message
|
||||
end
|
||||
|
||||
# Listen for error messages
|
||||
client.on :error do |error_message|
|
||||
# Handle the error
|
||||
end
|
||||
|
||||
# Listen for the close event
|
||||
client.on :close do |code, reason|
|
||||
# You may attempt to reconnect to the relay here
|
||||
end
|
||||
|
||||
# This line keeps the background client from exiting immediately.
|
||||
gets
|
||||
```
|
||||
|
||||
Beyond what's covered here, the Nostr protocol and this gem boast a wealth of additional functionalities. For an
|
||||
in-depth exploration of these capabilities, proceed to the next page.
|
|
@ -0,0 +1,9 @@
|
|||
# Implemented NIPs
|
||||
|
||||
NIPs stand for Nostr Implementation Possibilities. They exist to document what may be implemented by Nostr-compatible
|
||||
relay and client software.
|
||||
|
||||
- [NIP-01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
||||
- [NIP-02: Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md)
|
||||
- [NIP-04: Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
|
||||
- [NIP-19: Bech32-encoded entities](https://github.com/nostr-protocol/nips/blob/master/19.md)
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "Nostr"
|
||||
text: "Ruby"
|
||||
tagline: "The Nostr protocol implemented in a Ruby gem."
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Getting Started
|
||||
link: /getting-started/overview
|
||||
- theme: alt
|
||||
text: View on Github
|
||||
link: https://github.com/wilsonsilva/nostr
|
||||
- theme: alt
|
||||
text: View on RubyDoc
|
||||
link: https://www.rubydoc.info/gems/nostr
|
||||
- theme: alt
|
||||
text: View on RubyGems
|
||||
link: https://rubygems.org/gems/nostr
|
||||
|
||||
features:
|
||||
- title: Asynchronous
|
||||
details: Non-blocking I/O for maximum performance.
|
||||
icon: ⚡
|
||||
- title: Lightweight
|
||||
details: Minimal runtime dependencies.
|
||||
icon: 🪶
|
||||
- title: Intuitive
|
||||
details: The API mirrors the Nostr protocol specification domain language.
|
||||
icon: 💡
|
||||
- title: Fully documented
|
||||
details: All code is documented from both the maintainer's as well as the consumer's perspective.
|
||||
icon: 📚
|
||||
- title: Fully tested
|
||||
details: All code is tested with 100% coverage.
|
||||
icon: 🧪
|
||||
- title: Fully typed
|
||||
details: All code is typed with <a href="https://rubygems.org/gems/rbs" target="_blank">RBS</a> with the help of <a href="https://rubygems.org/gems/typeprof" target="_blank">TypeProf</a>. Type correctness is enforced by <a href="https://rubygems.org/gems/steep" target="_blank">Steep</a>.
|
||||
icon: ✅
|
||||
---
|
|
@ -0,0 +1,85 @@
|
|||
# Markdown Extension Examples
|
||||
|
||||
This page demonstrates some of the built-in markdown extensions provided by VitePress.
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
|
||||
|
||||
**Input**
|
||||
|
||||
````
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Output**
|
||||
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Containers
|
||||
|
||||
**Input**
|
||||
|
||||
```md
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
"docs:build": "vitepress build",
|
||||
"docs:preview": "vitepress preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "^10.9.0",
|
||||
"vitepress": "^1.1.0",
|
||||
"vitepress-plugin-mermaid": "^2.0.16"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Connecting to a Relay
|
||||
|
||||
You must connect your nostr [Client](../core/client) to a relay in order to send and receive [Events](../events).
|
||||
Instantiate a [`Nostr::Client`](https://www.rubydoc.info/gems/nostr/Nostr/Client) and a
|
||||
[`Nostr::Relay`](https://www.rubydoc.info/gems/nostr/Nostr/Relay) giving it the `url` of your relay. The `name`
|
||||
attribute is just descriptive.
|
||||
Calling [`Client#connect`](https://www.rubydoc.info/gems/nostr/Nostr/Client#connect-instance_method) attempts to
|
||||
establish a WebSocket connection between the Client and the Relay.
|
||||
|
||||
```ruby
|
||||
client = Nostr::Client.new
|
||||
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
|
||||
|
||||
# Listen for the connect event
|
||||
client.on :connect do
|
||||
# When this block executes, you're connected to the relay
|
||||
end
|
||||
|
||||
# Connect to a relay asynchronously
|
||||
client.connect(relay)
|
||||
```
|
|
@ -0,0 +1,29 @@
|
|||
# Publishing events
|
||||
|
||||
Create a [signed event](../core/keys) and call the method
|
||||
[`Nostr::Client#publish`](https://www.rubydoc.info/gems/nostr/Nostr/Client#publish-instance_method) to send the
|
||||
event to the relay.
|
||||
|
||||
```ruby{4-8,17}
|
||||
# Create a user with the keypair
|
||||
user = Nostr::User.new(keypair: keypair)
|
||||
|
||||
# Create a signed event
|
||||
text_note_event = user.create_event(
|
||||
kind: Nostr::EventKind::TEXT_NOTE,
|
||||
content: 'Your feedback is appreciated, now pay $8'
|
||||
)
|
||||
|
||||
# Connect asynchronously to a relay
|
||||
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
|
||||
client.connect(relay)
|
||||
|
||||
# Listen asynchronously for the connect event
|
||||
client.on :connect do
|
||||
# Send the event to the relay
|
||||
client.publish(text_note_event)
|
||||
end
|
||||
```
|
||||
|
||||
The relay will verify the signature of the event with the public key. If the signature is valid, the relay should
|
||||
broadcast the event to all subscribers.
|
|
@ -0,0 +1,6 @@
|
|||
# Receiving events
|
||||
|
||||
To receive events from Relays, you must create a subscription on the relay. A subscription is a filter that defines the
|
||||
events you want to receive.
|
||||
|
||||
For more information, read the [Subscription](../subscriptions/creating-a-subscription.md) section.
|
|
@ -0,0 +1,49 @@
|
|||
# Creating a subscription
|
||||
|
||||
A client can request events and subscribe to new updates __after__ it has established a connection with the Relay.
|
||||
|
||||
You may use a [`Nostr::Filter`](https://www.rubydoc.info/gems/nostr/Nostr/Filter) instance with as many attributes as
|
||||
you wish:
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
filter = Nostr::Filter.new(
|
||||
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
|
||||
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
|
||||
kinds: [Nostr::EventKind::TEXT_NOTE],
|
||||
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
|
||||
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
|
||||
since: 1230981305,
|
||||
until: 1292190341,
|
||||
limit: 420,
|
||||
)
|
||||
|
||||
subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
|
||||
end
|
||||
```
|
||||
|
||||
With just a few:
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
|
||||
subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
|
||||
end
|
||||
```
|
||||
|
||||
Or omit the filter:
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
subscription = client.subscribe(subscription_id: 'an-id')
|
||||
end
|
||||
```
|
||||
|
||||
Or even omit the subscription id:
|
||||
|
||||
```ruby
|
||||
client.on :connect do
|
||||
subscription = client.subscribe(filter: filter)
|
||||
subscription.id # => "13736f08dee8d7b697222ba605c6fab2" (randomly generated)
|
||||
end
|
||||
```
|
|
@ -0,0 +1,10 @@
|
|||
# Stop previous subscriptions
|
||||
|
||||
You can stop receiving messages from a subscription by calling
|
||||
[`Nostr::Client#unsubscribe`](https://www.rubydoc.info/gems/nostr/Nostr/Client#unsubscribe-instance_method) with the
|
||||
ID of the subscription you want to stop receiving messages from:
|
||||
|
||||
```ruby
|
||||
client.unsubscribe('your-subscription-id')
|
||||
client.unsubscribe(subscription.id)
|
||||
```
|
|
@ -0,0 +1,115 @@
|
|||
# Filtering events
|
||||
|
||||
## Filtering by id
|
||||
|
||||
You can filter events by their ids:
|
||||
|
||||
```ruby
|
||||
filter = Nostr::Filter.new(
|
||||
ids: [
|
||||
# matches events with these exact IDs
|
||||
'8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8',
|
||||
'461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
|
||||
]
|
||||
)
|
||||
subscription = client.subscribe(filter: filter)
|
||||
```
|
||||
|
||||
## Filtering by author
|
||||
|
||||
You can filter events by their author's pubkey:
|
||||
|
||||
```ruby
|
||||
filter = Nostr::Filter.new(
|
||||
authors: [
|
||||
# matches events whose (authors) pubkey match these exact IDs
|
||||
'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
|
||||
'51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
|
||||
]
|
||||
)
|
||||
subscription = client.subscribe(filter: filter)
|
||||
```
|
||||
|
||||
## Filtering by kind
|
||||
|
||||
You can filter events by their kind:
|
||||
|
||||
```ruby
|
||||
filter = Nostr::Filter.new(
|
||||
kinds: [
|
||||
# matches events whose kind is TEXT_NOTE
|
||||
Nostr::EventKind::TEXT_NOTE,
|
||||
# and matches events whose kind is CONTACT_LIST
|
||||
Nostr::EventKind::CONTACT_LIST,
|
||||
]
|
||||
)
|
||||
subscription = client.subscribe(filter: filter)
|
||||
```
|
||||
|
||||
## Filtering by referenced event
|
||||
|
||||
You can filter events by the events they reference (in their `e` tag):
|
||||
|
||||
```ruby
|
||||
filter = Nostr::Filter.new(
|
||||
e: [
|
||||
# matches events that reference other events whose ids match these exact IDs
|
||||
'f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52',
|
||||
'f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8',
|
||||
]
|
||||
)
|
||||
subscription = client.subscribe(filter: filter)
|
||||
```
|
||||
|
||||
## Filtering by referenced pubkey
|
||||
|
||||
You can filter events by the pubkeys they reference (in their `p` tag):
|
||||
|
||||
```ruby
|
||||
filter = Nostr::Filter.new(
|
||||
p: [
|
||||
# matches events that reference other pubkeys that match these exact IDs
|
||||
'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
|
||||
'51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
|
||||
]
|
||||
)
|
||||
subscription = client.subscribe(filter: filter)
|
||||
```
|
||||
|
||||
## Filtering by timestamp
|
||||
|
||||
You can filter events by their timestamp:
|
||||
|
||||
```ruby
|
||||
filter = Nostr::Filter.new(
|
||||
since: 1230981305, # matches events that are newer than this timestamp
|
||||
until: 1292190341, # matches events that are older than this timestamp
|
||||
)
|
||||
subscription = client.subscribe(filter: filter)
|
||||
```
|
||||
|
||||
## Limiting the number of events
|
||||
|
||||
You can limit the number of events received:
|
||||
|
||||
```ruby
|
||||
filter = Nostr::Filter.new(
|
||||
limit: 420, # matches at most 420 events
|
||||
)
|
||||
subscription = client.subscribe(filter: filter)
|
||||
```
|
||||
|
||||
## Combining filters
|
||||
|
||||
You can combine filters. For example, to match `5` text note events that are newer than `1230981305` from the author
|
||||
`ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577`:
|
||||
|
||||
```ruby
|
||||
filter = Nostr::Filter.new(
|
||||
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
|
||||
kinds: [Nostr::EventKind::TEXT_NOTE],
|
||||
since: 1230981305,
|
||||
limit: 5,
|
||||
)
|
||||
subscription = client.subscribe(filter: filter)
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
# Updating a subscription
|
||||
|
||||
Updating a subscription is done by creating a new subscription with the same id as the previous one. See
|
||||
[creating a subscription](./creating-a-subscription.md) for more information.
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'nostr/errors'
|
||||
require_relative 'nostr/bech32'
|
||||
require_relative 'nostr/crypto'
|
||||
require_relative 'nostr/version'
|
||||
require_relative 'nostr/keygen'
|
||||
|
@ -7,12 +9,19 @@ require_relative 'nostr/client_message_type'
|
|||
require_relative 'nostr/filter'
|
||||
require_relative 'nostr/subscription'
|
||||
require_relative 'nostr/relay'
|
||||
require_relative 'nostr/relay_message_type'
|
||||
require_relative 'nostr/key_pair'
|
||||
require_relative 'nostr/event_kind'
|
||||
require_relative 'nostr/signature'
|
||||
require_relative 'nostr/event'
|
||||
require_relative 'nostr/events/encrypted_direct_message'
|
||||
require_relative 'nostr/client'
|
||||
require_relative 'nostr/client/logger'
|
||||
require_relative 'nostr/client/color_logger'
|
||||
require_relative 'nostr/user'
|
||||
require_relative 'nostr/key'
|
||||
require_relative 'nostr/private_key'
|
||||
require_relative 'nostr/public_key'
|
||||
|
||||
# Encapsulates all the gem's logic
|
||||
module Nostr
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'bech32'
|
||||
require 'bech32/nostr'
|
||||
require 'bech32/nostr/entity'
|
||||
|
||||
module Nostr
|
||||
# Bech32 encoding and decoding
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
module Bech32
|
||||
# Decodes a bech32-encoded string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# bech32_value = 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
||||
# Nostr::Bech32.decode(bech32_value) # => ['npub', '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d8...']
|
||||
#
|
||||
# @param [String] bech32_value The bech32-encoded string to decode
|
||||
#
|
||||
# @return [Array<String, String>] The human readable part and the data
|
||||
#
|
||||
def self.decode(bech32_value)
|
||||
entity = ::Bech32::Nostr::NIP19.decode(bech32_value)
|
||||
|
||||
case entity
|
||||
in ::Bech32::Nostr::BareEntity
|
||||
[entity.hrp, entity.data]
|
||||
in ::Bech32::Nostr::TLVEntity
|
||||
[entity.hrp, entity.entries]
|
||||
end
|
||||
end
|
||||
|
||||
# Encodes data into a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# Nostr::Bech32.encode(hrp: 'npub', data: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
||||
# # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
||||
#
|
||||
# @param [String] hrp The human readable part (npub, nsec, nprofile, nrelay, nevent, naddr, etc)
|
||||
# @param [String] data The data to encode
|
||||
#
|
||||
# @return [String] The bech32-encoded string
|
||||
#
|
||||
def self.encode(hrp:, data:)
|
||||
::Bech32::Nostr::BareEntity.new(hrp, data).encode
|
||||
end
|
||||
|
||||
# Encodes a hex-encoded public key into a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# Nostr::Bech32.npub_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
||||
# # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
||||
#
|
||||
# @param [String] npub The public key to encode
|
||||
#
|
||||
# @see Nostr::Bech32#encode
|
||||
# @see Nostr::PublicKey#to_bech32
|
||||
# @see Nostr::PrivateKey#to_bech32
|
||||
#
|
||||
# @return [String] The bech32-encoded string
|
||||
#
|
||||
def self.npub_encode(npub)
|
||||
encode(hrp: 'npub', data: npub)
|
||||
end
|
||||
|
||||
# Encodes a hex-encoded private key into a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# Nostr::Bech32.nsec_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
||||
# # => 'nsec10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
||||
#
|
||||
# @param [String] nsec The private key to encode
|
||||
#
|
||||
# @see Nostr::Bech32#encode
|
||||
# @see Nostr::PrivateKey#to_bech32
|
||||
# @see Nostr::PublicKey#to_bech32
|
||||
#
|
||||
# @return [String] The bech32-encoded string
|
||||
#
|
||||
def self.nsec_encode(nsec)
|
||||
encode(hrp: 'nsec', data: nsec)
|
||||
end
|
||||
|
||||
# Encodes an address into a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# naddr = Nostr::Bech32.naddr_encode(
|
||||
# pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
|
||||
# relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
# kind: Nostr::EventKind::TEXT_NOTE,
|
||||
# identifier: 'damus'
|
||||
# )
|
||||
# naddr # => 'naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7ns...'
|
||||
#
|
||||
# @param [PublicKey] pubkey The public key to encode
|
||||
# @param [Array<String>] relays The relays to encode
|
||||
# @param [String] kind The kind of address to encode
|
||||
# @param [String] identifier The identifier of the address to encode
|
||||
#
|
||||
# @return [String] The bech32-encoded string
|
||||
#
|
||||
def self.naddr_encode(pubkey:, relays: [], kind: nil, identifier: nil)
|
||||
entry_relays = relays.map do |relay_url|
|
||||
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
||||
end
|
||||
|
||||
pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, pubkey)
|
||||
kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
|
||||
identifier_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, identifier)
|
||||
|
||||
entries = [pubkey_entry, *entry_relays, kind_entry, identifier_entry].compact
|
||||
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT_COORDINATE, entries)
|
||||
entity.encode
|
||||
end
|
||||
|
||||
# Encodes an event into a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# nevent = Nostr::Bech32.nevent_encode(
|
||||
# id: '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3',
|
||||
# relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
# kind: Nostr::EventKind::TEXT_NOTE,
|
||||
# )
|
||||
# nevent # => 'nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0pra...'
|
||||
#
|
||||
# @param [PublicKey] id The id the event to encode
|
||||
# @param [Array<String>] relays The relays to encode
|
||||
# @param [String] kind The kind of event to encode
|
||||
#
|
||||
# @return [String] The bech32-encoded string
|
||||
#
|
||||
def self.nevent_encode(id:, relays: [], kind: nil)
|
||||
entry_relays = relays.map do |relay_url|
|
||||
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
||||
end
|
||||
|
||||
id_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, id)
|
||||
kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
|
||||
|
||||
entries = [id_entry, *entry_relays, kind_entry].compact
|
||||
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT, entries)
|
||||
entity.encode
|
||||
end
|
||||
|
||||
# Encodes a profile into a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# nprofile = Nostr::Bech32.nprofile_encode(
|
||||
# pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
|
||||
# relays: ['wss://relay.damus.io', 'wss://nos.lol']
|
||||
# )
|
||||
#
|
||||
# @param [PublicKey] pubkey The public key to encode
|
||||
# @param [Array<String>] relays The relays to encode
|
||||
#
|
||||
# @return [String] The bech32-encoded string
|
||||
#
|
||||
def self.nprofile_encode(pubkey:, relays: [])
|
||||
entry_relays = relays.map do |relay_url|
|
||||
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
||||
end
|
||||
|
||||
pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, pubkey)
|
||||
entries = [pubkey_entry, *entry_relays].compact
|
||||
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_PROFILE, entries)
|
||||
entity.encode
|
||||
end
|
||||
|
||||
# Encodes a relay URL into a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# nrelay = Nostr::Bech32.nrelay_encode('wss://relay.damus.io')
|
||||
# nrelay # => 'nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x'
|
||||
#
|
||||
# @param [String] relay_url The relay url to encode
|
||||
#
|
||||
# @return [String] The bech32-encoded string
|
||||
#
|
||||
def self.nrelay_encode(relay_url)
|
||||
relay_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, relay_url)
|
||||
|
||||
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_RELAY, [relay_entry])
|
||||
entity.encode
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,11 +18,19 @@ module Nostr
|
|||
# @api public
|
||||
#
|
||||
# @example Instantiating a client that logs all the events it sends and receives
|
||||
# client = Nostr::Client.new(debug: true)
|
||||
# client = Nostr::Client.new
|
||||
#
|
||||
def initialize
|
||||
# @example Instantiating a client with no logging
|
||||
# client = Nostr::Client.new(logger: nil)
|
||||
#
|
||||
# @example Instantiating a client with your own logger
|
||||
# client = Nostr::Client.new(logger: YourLogger.new)
|
||||
#
|
||||
def initialize(logger: ColorLogger.new)
|
||||
@subscriptions = {}
|
||||
@logger = logger
|
||||
|
||||
logger&.attach_to(self)
|
||||
initialize_channels
|
||||
end
|
||||
|
||||
|
@ -40,11 +48,11 @@ module Nostr
|
|||
#
|
||||
def connect(relay)
|
||||
execute_within_an_em_thread do
|
||||
client = Faye::WebSocket::Client.new(relay.url, [], { tls: { verify_peer: false } })
|
||||
parent_to_child_channel.subscribe { |msg| client.send(msg) }
|
||||
client = build_websocket_client(relay.url)
|
||||
parent_to_child_channel.subscribe { |msg| client.send(msg) && emit(:send, msg) }
|
||||
|
||||
client.on :open do
|
||||
child_to_parent_channel.push(type: :open)
|
||||
child_to_parent_channel.push(type: :open, relay:)
|
||||
end
|
||||
|
||||
client.on :message do |event|
|
||||
|
@ -74,7 +82,8 @@ module Nostr
|
|||
# @example Subscribing to all events created after a certain time
|
||||
# subscription = client.subscribe(filter: Nostr::Filter.new(since: 1230981305))
|
||||
#
|
||||
# @param subscription_id [String] The subscription id. A random string.
|
||||
# @param subscription_id [String] The subscription id. An arbitrary, non-empty string of max length 64
|
||||
# chars used to represent a subscription.
|
||||
# @param filter [Filter] A set of attributes that represent the events that the client is interested in.
|
||||
#
|
||||
# @return [Subscription] The subscription object
|
||||
|
@ -121,6 +130,14 @@ module Nostr
|
|||
|
||||
private
|
||||
|
||||
# The logger that prints all the events that the client sends and receives
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [ClientLogger]
|
||||
#
|
||||
attr_reader :logger
|
||||
|
||||
# The subscriptions that the client has created
|
||||
#
|
||||
# @api private
|
||||
|
@ -166,11 +183,21 @@ module Nostr
|
|||
@child_to_parent_channel = EventMachine::Channel.new
|
||||
|
||||
child_to_parent_channel.subscribe do |msg|
|
||||
emit :connect if msg[:type] == :open
|
||||
emit :connect, msg[:relay] if msg[:type] == :open
|
||||
emit :message, msg[:data] if msg[:type] == :message
|
||||
emit :error, msg[:message] if msg[:type] == :error
|
||||
emit :close, msg[:code], msg[:reason] if msg[:type] == :close
|
||||
end
|
||||
end
|
||||
|
||||
# Builds a websocket client
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [Faye::WebSocket::Client]
|
||||
#
|
||||
def build_websocket_client(relay_url)
|
||||
Faye::WebSocket::Client.new(relay_url, [], { tls: { verify_peer: false } })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
class Client
|
||||
# Logs connection events, messages sent and received, errors, and connection closures in color.
|
||||
class ColorLogger < Logger
|
||||
# Logs connection to a relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Nostr::Relay] relay The relay the client connected to.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_connect(relay)
|
||||
puts "\u001b[32m\u001b[1mConnected to the relay\u001b[22m #{relay.name} (#{relay.url})\u001b[0m"
|
||||
end
|
||||
|
||||
# Logs a message received from the relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The message received.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_message(message)
|
||||
puts "\u001b[32m\u001b[1m◄-\u001b[0m #{message}"
|
||||
end
|
||||
|
||||
# Logs a message sent to the relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The message sent.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_send(message)
|
||||
puts "\u001b[32m\u001b[1m-►\u001b[0m #{message}"
|
||||
end
|
||||
|
||||
# Logs an error message
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The error message.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_error(message)
|
||||
puts "\u001b[31m\u001b[1mError: \u001b[22m#{message}\u001b[0m"
|
||||
end
|
||||
|
||||
# Logs a closure of connection with a relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] code The closure code.
|
||||
# @param [String] reason The reason for the closure.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_close(code, reason)
|
||||
puts "\u001b[31m\u001b[1mConnection closed: \u001b[22m#{reason} (##{code})\u001b[0m"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,85 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
class Client
|
||||
# Logs connection events, messages sent and received, errors, and connection closures.
|
||||
class Logger
|
||||
# Attaches event handlers to the specified Nostr client for logging purposes
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Attaching the logger to a client
|
||||
# client = Nostr::Client.new
|
||||
# logger = Nostr::Client::Logger.new
|
||||
# logger.attach_to(client)
|
||||
#
|
||||
# # Now, actions like connecting, sending messages, receiving messages,
|
||||
# # errors, and closing the connection will be logged to the console.
|
||||
#
|
||||
# @param [Nostr::Client] client The client to attach logging functionality to.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def attach_to(client)
|
||||
logger_instance = self
|
||||
|
||||
client.on(:connect) { |relay| logger_instance.on_connect(relay) }
|
||||
client.on(:message) { |message| logger_instance.on_message(message) }
|
||||
client.on(:send) { |message| logger_instance.on_send(message) }
|
||||
client.on(:error) { |message| logger_instance.on_error(message) }
|
||||
client.on(:close) { |code, reason| logger_instance.on_close(code, reason) }
|
||||
end
|
||||
|
||||
# Logs connection to a relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Nostr::Relay] relay The relay the client connected to.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_connect(relay); end
|
||||
|
||||
# Logs a message received from the relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The message received.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_message(message); end
|
||||
|
||||
# Logs a message sent to the relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The message sent.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_send(message); end
|
||||
|
||||
# Logs an error message
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The error message.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_error(message); end
|
||||
|
||||
# Logs a closure of connection with a relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] code The closure code.
|
||||
# @param [String] reason The reason for the closure.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_close(code, reason); end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
class Client
|
||||
# Logs connection events, messages sent and received, errors, and connection closures.
|
||||
class PlainLogger < Logger
|
||||
# Logs connection to a relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Nostr::Relay] relay The relay the client connected to.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_connect(relay)
|
||||
puts "Connected to the relay #{relay.name} (#{relay.url})"
|
||||
end
|
||||
|
||||
# Logs a message received from the relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The message received.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_message(message)
|
||||
puts "◄- #{message}"
|
||||
end
|
||||
|
||||
# Logs a message sent to the relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The message sent.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_send(message)
|
||||
puts "-► #{message}"
|
||||
end
|
||||
|
||||
# Logs an error message
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] message The error message.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_error(message)
|
||||
puts "Error: #{message}"
|
||||
end
|
||||
|
||||
# Logs a closure of connection with a relay
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] code The closure code.
|
||||
# @param [String] reason The reason for the closure.
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def on_close(code, reason)
|
||||
puts "Connection closed: #{reason} (##{code})"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Performs cryptographic operations on a +Nostr::Event+.
|
||||
# Performs cryptographic operations.
|
||||
class Crypto
|
||||
# Numeric base of the OpenSSL big number used in an event content's encryption.
|
||||
#
|
||||
|
@ -30,8 +30,8 @@ module Nostr
|
|||
# encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8')
|
||||
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
||||
#
|
||||
# @param sender_private_key [String] 32-bytes hex-encoded private key of the creator.
|
||||
# @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient.
|
||||
# @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the creator.
|
||||
# @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient.
|
||||
# @param plain_text [String] The text to be encrypted
|
||||
#
|
||||
# @return [String] Encrypted text.
|
||||
|
@ -54,14 +54,18 @@ module Nostr
|
|||
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
||||
# decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted)
|
||||
#
|
||||
# @param sender_public_key [String] 32-bytes hex-encoded public key of the message creator.
|
||||
# @param recipient_private_key [String] 32-bytes hex-encoded public key of the recipient.
|
||||
# @param sender_public_key [PublicKey] 32-bytes hex-encoded public key of the message creator.
|
||||
# @param recipient_private_key [PrivateKey] 32-bytes hex-encoded public key of the recipient.
|
||||
# @param encrypted_text [String] The text to be decrypted
|
||||
#
|
||||
# @return [String] Decrypted text.
|
||||
#
|
||||
def decrypt_text(recipient_private_key, sender_public_key, encrypted_text)
|
||||
base64_encoded_text, iv = encrypted_text.split('?iv=')
|
||||
|
||||
# Ensure iv and base64_encoded_text are not nil
|
||||
return '' unless iv && base64_encoded_text
|
||||
|
||||
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
|
||||
cipher.iv = Base64.decode64(iv)
|
||||
cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
|
||||
|
@ -80,31 +84,107 @@ module Nostr
|
|||
# event.sig # => a signature
|
||||
#
|
||||
# @param event [Event] The event to be signed
|
||||
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
#
|
||||
# @return [Event] An unsigned event.
|
||||
#
|
||||
def sign_event(event, private_key)
|
||||
event_digest = hash_event(event)
|
||||
|
||||
hex_private_key = Array(private_key).pack('H*')
|
||||
hex_message = Array(event_digest).pack('H*')
|
||||
event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
||||
signature = sign_message(event_digest, private_key)
|
||||
|
||||
event.id = event_digest
|
||||
event.sig = event_signature
|
||||
event.sig = signature
|
||||
|
||||
event
|
||||
end
|
||||
|
||||
# Signs a message using the Schnorr signature algorithm
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Signing a message
|
||||
# crypto = Nostr::Crypto.new
|
||||
# message = 'Viva la libertad carajo'
|
||||
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
|
||||
# signature = crypto.sign_message(message, private_key)
|
||||
# signature # => 'b2115694a576f5bdcebf8c0951a3c7adcfbdb17b11cb9e6d6b7017691138bc6' \
|
||||
# '38fee642a7bd26f71b313a7057181294198900a9770d1435e43f182acf3d34c26'
|
||||
#
|
||||
# @param [String] message The message to be signed
|
||||
# @param [PrivateKey] private_key The private key used for signing
|
||||
#
|
||||
# @return [Signature] A signature object containing the signature as a 64-byte hexadecimal string.
|
||||
#
|
||||
def sign_message(message, private_key)
|
||||
hex_private_key = Array(private_key).pack('H*')
|
||||
hex_message = Array(message).pack('H*')
|
||||
hex_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
||||
|
||||
Signature.new(hex_signature.to_s)
|
||||
end
|
||||
|
||||
# Verifies the given {Signature} and returns true if it is valid
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Checking a signature
|
||||
# public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6')
|
||||
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
|
||||
# message = 'Viva la libertad carajo'
|
||||
# crypto = Nostr::Crypto.new
|
||||
# signature = crypto.sign_message(message, private_key)
|
||||
# valid = crypto.valid_sig?(message, public_key, signature)
|
||||
# valid # => true
|
||||
#
|
||||
# @see #check_sig!
|
||||
#
|
||||
# @param [String] message A message to be signed with binary format.
|
||||
# @param [PublicKey] public_key The public key with binary format.
|
||||
# @param [Signature] signature The signature with binary format.
|
||||
#
|
||||
# @return [Boolean] whether signature is valid.
|
||||
#
|
||||
def valid_sig?(message, public_key, signature)
|
||||
signature = Schnorr::Signature.decode([signature].pack('H*'))
|
||||
Schnorr.valid_sig?([message].pack('H*'), [public_key].pack('H*'), signature.encode)
|
||||
end
|
||||
|
||||
# Verifies the given {Signature} and raises an +Schnorr::InvalidSignatureError+ if it is invalid
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Checking a signature
|
||||
# public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6')
|
||||
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
|
||||
# message = 'Viva la libertad carajo'
|
||||
# crypto = Nostr::Crypto.new
|
||||
# signature = crypto.sign_message(message, private_key)
|
||||
# valid = crypto.valid_sig?(message, public_key, signature)
|
||||
# valid # => true
|
||||
#
|
||||
# @see #valid_sig?
|
||||
#
|
||||
# @param [String] message A message to be signed with binary format.
|
||||
# @param [PublicKey] public_key The public key with binary format.
|
||||
# @param [Signature] signature The signature with binary format.
|
||||
#
|
||||
# @raise [Schnorr::InvalidSignatureError] if the signature is invalid.
|
||||
#
|
||||
# @return [Boolean] whether signature is valid.
|
||||
#
|
||||
def check_sig!(message, public_key, signature)
|
||||
signature = Schnorr::Signature.decode([signature].pack('H*'))
|
||||
Schnorr.check_sig!([message].pack('H*'), [public_key].pack('H*'), signature.encode)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Finds a shared key between two keys
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||
# @param public_key [String] 32-bytes hex-encoded public key.
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
||||
#
|
||||
# @return [String] A shared key used in the event's content encryption and decryption.
|
||||
#
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'errors/error'
|
||||
require_relative 'errors/key_validation_error'
|
||||
require_relative 'errors/invalid_hrp_error'
|
||||
require_relative 'errors/invalid_key_type_error'
|
||||
require_relative 'errors/invalid_key_length_error'
|
||||
require_relative 'errors/invalid_key_format_error'
|
||||
require_relative 'errors/signature_validation_error'
|
||||
require_relative 'errors/invalid_signature_type_error'
|
||||
require_relative 'errors/invalid_signature_length_error'
|
||||
require_relative 'errors/invalid_signature_format_error'
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Base error class
|
||||
class Error < StandardError
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the human readable part of a Bech32 string is invalid
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidHRPError < KeyValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidHRPError.new('example wrong hrp', 'nsec')
|
||||
#
|
||||
# @param given_hrp [String] The given human readable part of the Bech32 string
|
||||
# @param allowed_hrp [String] The allowed human readable part of the Bech32 string
|
||||
#
|
||||
def initialize(given_hrp, allowed_hrp)
|
||||
super("Invalid hrp: #{given_hrp}. The allowed hrp value for this kind of entity is '#{allowed_hrp}'.")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the private key is in an invalid format
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidKeyFormatError < KeyValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidKeyFormatError.new('private')
|
||||
#
|
||||
# @param [String] key_kind The kind of key that is invalid (public or private)
|
||||
#
|
||||
def initialize(key_kind)
|
||||
super("Only lowercase hexadecimal characters are allowed in #{key_kind} keys.")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the private key's length is not 64 characters
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidKeyLengthError < KeyValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidKeyLengthError.new('private')
|
||||
#
|
||||
# @param [String] key_kind The kind of key that is invalid (public or private)
|
||||
#
|
||||
def initialize(key_kind)
|
||||
super("Invalid #{key_kind} key length. It should have 64 characters.")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the private key is not a string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidKeyTypeError < KeyValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidKeyTypeError.new('private')
|
||||
#
|
||||
# @param [String] key_kind The kind of key that is invalid (public or private)
|
||||
#
|
||||
def initialize(key_kind) = super("Invalid #{key_kind} key type")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the signature is in an invalid format
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidSignatureFormatError < SignatureValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidSignatureFormatError.new
|
||||
#
|
||||
def initialize
|
||||
super('Only lowercase hexadecimal characters are allowed in signatures.')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the signature's length is not 128 characters
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidSignatureLengthError < SignatureValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidSignatureLengthError.new
|
||||
#
|
||||
def initialize
|
||||
super('Invalid signature length. It should have 128 characters.')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Raised when the signature is not a string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
class InvalidSignatureTypeError < SignatureValidationError
|
||||
# Initializes the error
|
||||
#
|
||||
# @example
|
||||
# InvalidSignatureTypeError.new
|
||||
#
|
||||
def initialize = super('Invalid signature type. It must be a string with lowercase hexadecimal characters.')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Base class for all key validation errors
|
||||
class KeyValidationError < Error; end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Base class for all signature validation errors
|
||||
class SignatureValidationError < Error; end
|
||||
end
|
|
@ -100,15 +100,15 @@ module Nostr
|
|||
#
|
||||
# @example Instantiating a new event
|
||||
# Nostr::Event.new(
|
||||
# id: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
||||
# pubkey: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
|
||||
# created_at: 1230981305,
|
||||
# kind: 1,
|
||||
# tags: [],
|
||||
# content: 'Your feedback is appreciated, now pay $8',
|
||||
# sig: '123ac2923b792ce730b3da34f16155470ab13c8f97f9c53eaeb334f1fb3a5dc9a7f643
|
||||
# 937c6d6e9855477638f5655c5d89c9aa5501ea9b578a66aced4f1cd7b3'
|
||||
# )
|
||||
# id: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
||||
# pubkey: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
|
||||
# created_at: 1230981305,
|
||||
# kind: 1,
|
||||
# tags: [],
|
||||
# content: 'Your feedback is appreciated, now pay $8',
|
||||
# sig: '123ac2923b792ce730b3da34f16155470ab13c8f97f9c53eaeb334f1fb3a5dc9a7f643
|
||||
# 937c6d6e9855477638f5655c5d89c9aa5501ea9b578a66aced4f1cd7b3'
|
||||
# )
|
||||
#
|
||||
# @param id [String|nil] 32-bytes sha256 of the the serialized event data.
|
||||
# @param sig [String|nil] 64-bytes signature of the sha256 hash of the serialized event data, which is
|
||||
|
@ -128,7 +128,6 @@ module Nostr
|
|||
id: nil,
|
||||
sig: nil
|
||||
)
|
||||
|
||||
@id = id
|
||||
@sig = sig
|
||||
@pubkey = pubkey
|
||||
|
@ -160,11 +159,11 @@ module Nostr
|
|||
# pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
|
||||
# event.add_pubkey_reference(pubkey)
|
||||
#
|
||||
# @param pubkey [String] 32-bytes hex-encoded public key.
|
||||
# @param pubkey [PublicKey] 32-bytes hex-encoded public key.
|
||||
#
|
||||
# @return [Array<String>] The event's updated list of tags
|
||||
#
|
||||
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey])
|
||||
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey.to_s])
|
||||
|
||||
# Signs an event with the user's private key
|
||||
#
|
||||
|
@ -173,7 +172,7 @@ module Nostr
|
|||
# @example Signing an event
|
||||
# event.sign(private_key)
|
||||
#
|
||||
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
#
|
||||
# @return [Event] A signed event.
|
||||
#
|
||||
|
@ -182,6 +181,33 @@ module Nostr
|
|||
crypto.sign_event(self, private_key)
|
||||
end
|
||||
|
||||
# Verifies if the signature of the event is valid. A valid signature means that the event was signed by the owner
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Verifying the signature of an event
|
||||
# event = Nostr::Event.new(
|
||||
# id: '90b75b78daf883ae57fbcc414d43faa028560b3211ee58e4ea82bf395bb82042',
|
||||
# pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
|
||||
# created_at: 1667422587,
|
||||
# kind: Nostr::EventKind::TEXT_NOTE,
|
||||
# content: 'Your feedback is appreciated, now pay $8',
|
||||
# sig: '32f18adebe942e19b171c1c7d2fb27ce794dfea4155e289dca7952b43ed1ec39' \
|
||||
# '1d3dc198ba2761bc6d40c737a6eaf4edcc8963acabd3bfcebd04f16637025bdc'
|
||||
# )
|
||||
#
|
||||
# event.verify_signature # => true
|
||||
#
|
||||
# @return [Boolean] Whether the signature is valid or not.
|
||||
#
|
||||
def verify_signature
|
||||
crypto = Crypto.new
|
||||
|
||||
return false if id.nil? || pubkey.nil? || sig.nil?
|
||||
|
||||
crypto.valid_sig?(id, pubkey, sig)
|
||||
end
|
||||
|
||||
# Serializes the event, to obtain a SHA256 digest of it
|
||||
#
|
||||
# @api public
|
||||
|
|
|
@ -21,6 +21,7 @@ module Nostr
|
|||
# The content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to
|
||||
# recommend to its followers.
|
||||
#
|
||||
# @deprecated This event kind was removed in https://github.com/nostr-protocol/nips/pull/703/files#diff-39307f1617417657ee9874be314f13aabdc74401b124d0afe8217f2919c9c7d8L105
|
||||
# @return [Integer]
|
||||
#
|
||||
RECOMMEND_SERVER = 2
|
||||
|
|
|
@ -11,23 +11,24 @@ module Nostr
|
|||
# @api public
|
||||
#
|
||||
# @example Instantiating a new encrypted direct message
|
||||
# Nostr::Events::EncryptedDirectMessage.new(
|
||||
# Nostr::Events::EncryptedDirectMessage.new(
|
||||
# sender_private_key: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
||||
# recipient_public_key: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
|
||||
# plain_text: 'Your feedback is appreciated, now pay $8',
|
||||
# )
|
||||
# )
|
||||
#
|
||||
# @example Instantiating a new encrypted direct message that references a previous direct message
|
||||
# Nostr::Events::EncryptedDirectMessage.new(
|
||||
# Nostr::Events::EncryptedDirectMessage.new(
|
||||
# sender_private_key: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
|
||||
# recipient_public_key: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
|
||||
# plain_text: 'Your feedback is appreciated, now pay $8',
|
||||
# previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
||||
# )
|
||||
# )
|
||||
#
|
||||
# @param plain_text [String] The +content+ of the encrypted message.
|
||||
# @param sender_private_key [String] 32-bytes hex-encoded private key of the message's author.
|
||||
# @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient of the encrypted message.
|
||||
# @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the message's author.
|
||||
# @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient of the encrypted
|
||||
# message.
|
||||
# @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a
|
||||
# conversation or a message we are explicitly replying to (such that contextual, more organized conversations
|
||||
# may happen
|
||||
|
@ -43,7 +44,7 @@ module Nostr
|
|||
pubkey: sender_public_key,
|
||||
kind: Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE,
|
||||
content: encrypted_content,
|
||||
)
|
||||
)
|
||||
|
||||
add_pubkey_reference(recipient_public_key)
|
||||
add_event_reference(previous_direct_message) if previous_direct_message
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Nostr
|
||||
# A filter determines what events will be sent in a subscription.
|
||||
class Filter
|
||||
# A list of event ids or prefixes
|
||||
# A list of event ids
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
|
@ -14,7 +14,7 @@ module Nostr
|
|||
#
|
||||
attr_reader :ids
|
||||
|
||||
# A list of pubkeys or prefixes, the pubkey of an event must be one of these
|
||||
# A list of pubkeys, the pubkey of an event must be one of these
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
|
@ -107,8 +107,8 @@ module Nostr
|
|||
# )
|
||||
#
|
||||
# @param kwargs [Hash]
|
||||
# @option kwargs [Array<String>, nil] ids A list of event ids or prefixes
|
||||
# @option kwargs [Array<String>, nil] authors A list of pubkeys or prefixes, the pubkey of an event must be one
|
||||
# @option kwargs [Array<String>, nil] ids A list of event ids
|
||||
# @option kwargs [Array<String>, nil] authors A list of pubkeys, the pubkey of an event must be one
|
||||
# of these
|
||||
# @option kwargs [Array<Integer>, nil] kinds A list of a kind numbers
|
||||
# @option kwargs [Array<String>, nil] e A list of event ids that are referenced in an "e" tag
|
||||
|
@ -133,13 +133,16 @@ module Nostr
|
|||
# @api public
|
||||
#
|
||||
# @example
|
||||
# filter.to_h # => {:ids=>["c24881c305c5cfb7c1168be7e9b0e150"],
|
||||
# :authors=>["000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7"],
|
||||
# :kinds=>[0, 1, 2],
|
||||
# :"#e"=>["7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2"],
|
||||
# :"#p"=>["000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7"],
|
||||
# :since=>1230981305,
|
||||
# :until=>1292190341}
|
||||
# filter.to_h # =>
|
||||
# {
|
||||
# ids: ['c24881c305c5cfb7c1168be7e9b0e150'],
|
||||
# authors: ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7'],
|
||||
# kinds: [0, 1, 2],
|
||||
# '#e': ['7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2'],
|
||||
# '#p': ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7'],
|
||||
# since: 1230981305,
|
||||
# until: 1292190341
|
||||
# }
|
||||
#
|
||||
# @return [Hash] The filter as a hash.
|
||||
#
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Abstract class for all keys
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
class Key < String
|
||||
# The regular expression for hexadecimal lowercase characters
|
||||
#
|
||||
# @return [Regexp] The regular expression for hexadecimal lowercase characters
|
||||
#
|
||||
FORMAT = /^[a-f0-9]+$/
|
||||
|
||||
# The length of the key in hex format
|
||||
#
|
||||
# @return [Integer] The length of the key in hex format
|
||||
#
|
||||
LENGTH = 64
|
||||
|
||||
# Instantiates a new key. Can't be used directly because this is an abstract class. Raises a +KeyValidationError+
|
||||
#
|
||||
# @see Nostr::PrivateKey
|
||||
# @see Nostr::PublicKey
|
||||
#
|
||||
# @param [String] hex_value Hex-encoded value of the key
|
||||
#
|
||||
# @raise [KeyValidationError]
|
||||
#
|
||||
def initialize(hex_value)
|
||||
validate_hex_value(hex_value)
|
||||
|
||||
super(hex_value)
|
||||
end
|
||||
|
||||
# Instantiates a key from a bech32 string
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# bech32_key = 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
|
||||
# bech32_key.to_key # => #<Nostr::PublicKey:0x000000010601e3c8 @hex_value="...">
|
||||
#
|
||||
# @raise [Nostr::InvalidHRPError] if the bech32 string is invalid.
|
||||
#
|
||||
# @param [String] bech32_value The bech32 string representation of the key.
|
||||
#
|
||||
# @return [Key] the key.
|
||||
#
|
||||
def self.from_bech32(bech32_value)
|
||||
type, data = Bech32.decode(bech32_value)
|
||||
|
||||
raise InvalidHRPError.new(type, hrp) unless type == hrp
|
||||
|
||||
new(data)
|
||||
end
|
||||
|
||||
# Abstract method to be implemented by subclasses to provide the HRP (npub, nsec)
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String] The HRP
|
||||
#
|
||||
def self.hrp
|
||||
raise 'Subclasses must implement this method'
|
||||
end
|
||||
|
||||
# Converts the key to a bech32 string representation
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Converting a private key to a bech32 string
|
||||
# public_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
|
||||
# public_key.to_bech32 # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
|
||||
#
|
||||
# @example Converting a public key to a bech32 string
|
||||
# public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
||||
# public_key.to_bech32 # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
||||
#
|
||||
# @return [String] The bech32 string representation of the key
|
||||
#
|
||||
def to_bech32 = Bech32.encode(hrp: self.class.hrp, data: self)
|
||||
|
||||
protected
|
||||
|
||||
# Validates the hex value during initialization
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] _hex_value The hex value of the key
|
||||
#
|
||||
# @raise [KeyValidationError] When the hex value is invalid
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_hex_value(_hex_value)
|
||||
raise 'Subclasses must implement this method'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@ module Nostr
|
|||
# @example
|
||||
# keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
|
||||
#
|
||||
# @return [String]
|
||||
# @return [PrivateKey]
|
||||
#
|
||||
attr_reader :private_key
|
||||
|
||||
|
@ -21,7 +21,7 @@ module Nostr
|
|||
# @example
|
||||
# keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
|
||||
#
|
||||
# @return [String]
|
||||
# @return [PublicKey]
|
||||
#
|
||||
attr_reader :public_key
|
||||
|
||||
|
@ -31,16 +31,64 @@ module Nostr
|
|||
#
|
||||
# @example
|
||||
# keypair = Nostr::KeyPair.new(
|
||||
# private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
|
||||
# public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
|
||||
# private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
|
||||
# public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
|
||||
# )
|
||||
#
|
||||
# @param private_key [String] 32-bytes hex-encoded private key.
|
||||
# @param public_key [String] 32-bytes hex-encoded public key.
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
||||
#
|
||||
# @raise ArgumentError when the private key is not a {PrivateKey}
|
||||
# @raise ArgumentError when the public key is not a {PublicKey}
|
||||
#
|
||||
def initialize(private_key:, public_key:)
|
||||
validate_keys(private_key, public_key)
|
||||
|
||||
@private_key = private_key
|
||||
@public_key = public_key
|
||||
end
|
||||
|
||||
# Allows array destructuring of the KeyPair, enabling the extraction of +PrivateKey+ and +PublicKey+ separately
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Implicit usage of `to_ary` for destructuring
|
||||
# keypair = Nostr::KeyPair.new(
|
||||
# private_key: Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d'),
|
||||
# public_key: Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6'),
|
||||
# )
|
||||
# # The `to_ary` method can be implicitly used for array destructuring:
|
||||
# private_key, public_key = keypair
|
||||
# # Now `private_key` and `public_key` hold the respective values.
|
||||
#
|
||||
# @example Explicit usage of `to_ary`
|
||||
# array_representation = keypair.to_ary
|
||||
# # array_representation is now an array: [PrivateKey, PublicKey]
|
||||
# # where PrivateKey and PublicKey are the respective objects.
|
||||
#
|
||||
# @return [Array<PrivateKey, PublicKey>] An array containing the {PrivateKey} and {PublicKey} in that order
|
||||
#
|
||||
def to_ary
|
||||
[private_key, public_key]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Validates the keys
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
||||
#
|
||||
# @raise ArgumentError when the private key is not a +PrivateKey+
|
||||
# @raise ArgumentError when the public key is not a +PublicKey+
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_keys(private_key, public_key)
|
||||
raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
|
||||
raise ArgumentError, 'public_key is not an instance of PublicKey' unless public_key.is_a?(Nostr::PublicKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ module Nostr
|
|||
# @api public
|
||||
#
|
||||
# @example
|
||||
# keypair = keygen.generate_keypair
|
||||
# keypair = keygen.generate_key_pair
|
||||
# keypair # #<Nostr::KeyPair:0x0000000107bd3550
|
||||
# @private_key="893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900",
|
||||
# @public_key="2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558">
|
||||
|
@ -44,10 +44,11 @@ module Nostr
|
|||
# private_key = keygen.generate_private_key
|
||||
# private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
|
||||
#
|
||||
# @return [String] A 32-bytes hex-encoded private key.
|
||||
# @return [PrivateKey] A 32-bytes hex-encoded private key.
|
||||
#
|
||||
def generate_private_key
|
||||
(SecureRandom.random_number(group.order - 1) + 1).to_s(16)
|
||||
hex_value = (SecureRandom.random_number(group.order - 1) + 1).to_s(16).rjust(64, '0')
|
||||
PrivateKey.new(hex_value)
|
||||
end
|
||||
|
||||
# Extracts a public key from a private key
|
||||
|
@ -59,10 +60,36 @@ module Nostr
|
|||
# public_key = keygen.extract_public_key(private_key)
|
||||
# public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
|
||||
#
|
||||
# @return [String] A 32-bytes hex-encoded public key.
|
||||
# @param [PrivateKey] private_key A 32-bytes hex-encoded private key.
|
||||
#
|
||||
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
|
||||
#
|
||||
# @return [PublicKey] A 32-bytes hex-encoded public key.
|
||||
#
|
||||
def extract_public_key(private_key)
|
||||
group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
|
||||
validate_private_key(private_key)
|
||||
hex_value = group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
|
||||
PublicKey.new(hex_value)
|
||||
end
|
||||
|
||||
# Builds a key pair from an existing private key
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example
|
||||
# private_key = Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900')
|
||||
# keygen.get_key_pair_from_private_key(private_key)
|
||||
#
|
||||
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
||||
#
|
||||
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
|
||||
#
|
||||
# @return [Nostr::KeyPair]
|
||||
#
|
||||
def get_key_pair_from_private_key(private_key)
|
||||
validate_private_key(private_key)
|
||||
public_key = extract_public_key(private_key)
|
||||
KeyPair.new(private_key:, public_key:)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -74,5 +101,17 @@ module Nostr
|
|||
# @return [ECDSA::Group]
|
||||
#
|
||||
attr_reader :group
|
||||
|
||||
# Validates that the private key is an instance of +PrivateKey+
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_private_key(private_key)
|
||||
raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# 32-bytes lowercase hex-encoded private key
|
||||
class PrivateKey < Key
|
||||
# Human-readable part of the Bech32 encoded address
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String] The human-readable part of the Bech32 encoded address
|
||||
#
|
||||
def self.hrp
|
||||
'nsec'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Validates the hex value of the private key
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] hex_value The private key in hex format
|
||||
#
|
||||
# @raise InvalidKeyTypeError when the private key is not a string
|
||||
# @raise InvalidKeyLengthError when the private key's length is not 64 characters
|
||||
# @raise InvalidKeyFormatError when the private key is in an invalid format
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_hex_value(hex_value)
|
||||
raise InvalidKeyTypeError, 'private' unless hex_value.is_a?(String)
|
||||
raise InvalidKeyLengthError, 'private' unless hex_value.size == Key::LENGTH
|
||||
raise InvalidKeyFormatError, 'private' unless hex_value.match(Key::FORMAT)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# 32-bytes lowercase hex-encoded public key
|
||||
class PublicKey < Key
|
||||
# Human-readable part of the Bech32 encoded address
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String] The human-readable part of the Bech32 encoded address
|
||||
#
|
||||
def self.hrp
|
||||
'npub'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Validates the hex value of the public key
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] hex_value The public key in hex format
|
||||
#
|
||||
# @raise InvalidKeyTypeError when the public key is not a string
|
||||
# @raise InvalidKeyLengthError when the public key's length is not 64 characters
|
||||
# @raise InvalidKeyFormatError when the public key is in an invalid format
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate_hex_value(hex_value)
|
||||
raise InvalidKeyTypeError, 'public' unless hex_value.is_a?(String)
|
||||
raise InvalidKeyLengthError, 'public' unless hex_value.size == Key::LENGTH
|
||||
raise InvalidKeyFormatError, 'public' unless hex_value.match(Key::FORMAT)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Clients can send 4 types of messages, which must be JSON arrays
|
||||
module RelayMessageType
|
||||
# @return [String] Used to notify clients all stored events have been sent
|
||||
EOSE = 'EOSE'
|
||||
|
||||
# @return [String] Used to send events requested to clients
|
||||
EVENT = 'EVENT'
|
||||
|
||||
# @return [String] Used to send human-readable messages to clients
|
||||
NOTICE = 'NOTICE'
|
||||
|
||||
# @return [String] Used to notify clients if an EVENT was successful
|
||||
OK = 'OK'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# 64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data,
|
||||
# which is the same as the "id" field
|
||||
class Signature < String
|
||||
# The regular expression for hexadecimal lowercase characters
|
||||
#
|
||||
# @return [Regexp] The regular expression for hexadecimal lowercase characters
|
||||
#
|
||||
FORMAT = /^[a-f0-9]+$/
|
||||
|
||||
# The length of the signature in hex format
|
||||
#
|
||||
# @return [Integer] The length of the signature in hex format
|
||||
#
|
||||
LENGTH = 128
|
||||
|
||||
# Instantiates a new Signature
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Instantiating a new signature
|
||||
# Nostr::Signature.new(
|
||||
# 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
|
||||
# '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
|
||||
# )
|
||||
#
|
||||
# @param [String] hex_value Hex-encoded value of the signature
|
||||
#
|
||||
# @raise [SignatureValidationError]
|
||||
#
|
||||
def initialize(hex_value)
|
||||
validate(hex_value)
|
||||
|
||||
super(hex_value)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Hex-encoded value of the signature
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String] hex_value Hex-encoded value of the signature
|
||||
#
|
||||
attr_reader :hex_value
|
||||
|
||||
# Validates the hex value of the signature
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] hex_value The signature in hex format
|
||||
#
|
||||
# @raise InvalidSignatureTypeError when the signature is not a string
|
||||
# @raise InvalidSignatureLengthError when the signature's length is not 128 characters
|
||||
# @raise InvalidSignatureFormatError when the signature is in an invalid format
|
||||
#
|
||||
# @return [void]
|
||||
#
|
||||
def validate(hex_value)
|
||||
raise InvalidSignatureTypeError unless hex_value.is_a?(String)
|
||||
raise InvalidSignatureLengthError unless hex_value.size == LENGTH
|
||||
raise InvalidSignatureFormatError unless hex_value.match(FORMAT)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@ require 'securerandom'
|
|||
module Nostr
|
||||
# A subscription the result of a request to receive events from a relay
|
||||
class Subscription
|
||||
# A random string that should be used to represent a subscription
|
||||
# An arbitrary, non-empty string of max length 64 chars used to represent a subscription
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
|
@ -41,7 +41,7 @@ module Nostr
|
|||
# @example Subscribing to all events created after a certain time
|
||||
# subscription = Nostr::Subscription.new(filter: Nostr::Filter.new(since: 1230981305))
|
||||
#
|
||||
# @param id [String] A random string that should be used to represent a subscription
|
||||
# @param id [String] An arbitrary, non-empty string of max length 64 chars used to represent a subscription
|
||||
# @param filter [Filter] An object that determines what events will be sent in that subscription
|
||||
#
|
||||
def initialize(filter:, id: SecureRandom.hex)
|
||||
|
|
|
@ -47,17 +47,26 @@ module Nostr
|
|||
# content: 'Your feedback is appreciated, now pay $8'
|
||||
# )
|
||||
#
|
||||
# @param event_attributes [Hash]
|
||||
# @option event_attributes [String] :pubkey 32-bytes hex-encoded public key of the event creator.
|
||||
# @option event_attributes [Integer] :created_at Date of the creation of the vent. A UNIX timestamp, in seconds.
|
||||
# @option event_attributes [Integer] :kind The kind of the event. An integer from 0 to 3.
|
||||
# @option event_attributes [Array<Array>] :tags An array of tags. Each tag is an array of strings.
|
||||
# @option event_attributes [String] :content Arbitrary string.
|
||||
# @param created_at [Integer] Date of the creation of the vent. A UNIX timestamp, in seconds.
|
||||
# @param kind [Integer] The kind of the event. An integer from 0 to 3.
|
||||
# @param tags [Array<Array>] An array of tags. Each tag is an array of strings.
|
||||
# @param content [String] Arbitrary string.
|
||||
#
|
||||
# @return [Event]
|
||||
#
|
||||
def create_event(event_attributes)
|
||||
event = Event.new(**event_attributes.merge(pubkey: keypair.public_key))
|
||||
def create_event(
|
||||
kind:,
|
||||
content:,
|
||||
created_at: Time.now.to_i,
|
||||
tags: []
|
||||
)
|
||||
event = Event.new(
|
||||
pubkey: keypair.public_key,
|
||||
kind:,
|
||||
content:,
|
||||
created_at:,
|
||||
tags:
|
||||
)
|
||||
event.sign(keypair.private_key)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
module Nostr
|
||||
# The version of the gem
|
||||
VERSION = '0.4.0'
|
||||
VERSION = '0.7.0'
|
||||
end
|
||||
|
|
|
@ -10,9 +10,9 @@ Gem::Specification.new do |spec|
|
|||
|
||||
spec.summary = 'Client and relay implementation of the Nostr protocol.'
|
||||
spec.description = 'Client and relay implementation of the Nostr protocol.'
|
||||
spec.homepage = 'https://github.com/wilsonsilva/nostr'
|
||||
spec.homepage = 'https://nostr-ruby.com/'
|
||||
spec.license = 'MIT'
|
||||
spec.required_ruby_version = '>= 3.2.0'
|
||||
spec.required_ruby_version = '>= 3.3.0'
|
||||
spec.metadata['rubygems_mfa_required'] = 'true'
|
||||
|
||||
spec.metadata['homepage_uri'] = spec.homepage
|
||||
|
@ -31,33 +31,33 @@ Gem::Specification.new do |spec|
|
|||
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
||||
spec.require_paths = ['lib']
|
||||
|
||||
spec.add_dependency 'bech32', '~> 1.3'
|
||||
spec.add_dependency 'bip-schnorr', '~> 0.4'
|
||||
spec.add_dependency 'bech32', '~> 1.4'
|
||||
spec.add_dependency 'bip-schnorr', '~> 0.7'
|
||||
spec.add_dependency 'ecdsa', '~> 1.2'
|
||||
spec.add_dependency 'event_emitter', '~> 0.2'
|
||||
spec.add_dependency 'faye-websocket', '~> 0.11'
|
||||
spec.add_dependency 'json', '~> 2.6'
|
||||
spec.add_dependency 'json', '~> 2.7'
|
||||
|
||||
spec.add_development_dependency 'bundler-audit', '~> 0.9'
|
||||
spec.add_development_dependency 'dotenv', '~> 2.8'
|
||||
spec.add_development_dependency 'dotenv', '~> 3.1'
|
||||
spec.add_development_dependency 'guard', '~> 2.18'
|
||||
spec.add_development_dependency 'guard-bundler', '~> 3.0'
|
||||
spec.add_development_dependency 'guard-bundler-audit', '~> 0.1'
|
||||
spec.add_development_dependency 'guard-rspec', '~> 4.7'
|
||||
spec.add_development_dependency 'guard-rubocop', '~> 1.5'
|
||||
spec.add_development_dependency 'overcommit', '~> 0.59'
|
||||
spec.add_development_dependency 'overcommit', '~> 0.63'
|
||||
spec.add_development_dependency 'pry', '~> 0.14'
|
||||
spec.add_development_dependency 'puma', '~> 5.6'
|
||||
spec.add_development_dependency 'puma', '~> 6.4'
|
||||
spec.add_development_dependency 'rack', '~> 3.0'
|
||||
spec.add_development_dependency 'rake', '~> 13.0'
|
||||
spec.add_development_dependency 'rbs', '~> 2.8'
|
||||
spec.add_development_dependency 'rspec', '~> 3.12'
|
||||
spec.add_development_dependency 'rubocop', '~> 1.42'
|
||||
spec.add_development_dependency 'rake', '~> 13.1'
|
||||
spec.add_development_dependency 'rbs', '~> 3.4'
|
||||
spec.add_development_dependency 'rspec', '~> 3.13'
|
||||
spec.add_development_dependency 'rubocop', '~> 1.62'
|
||||
spec.add_development_dependency 'rubocop-rake', '~> 0.6'
|
||||
spec.add_development_dependency 'rubocop-rspec', '2.16'
|
||||
spec.add_development_dependency 'rubocop-rspec', '2.29'
|
||||
spec.add_development_dependency 'simplecov', '= 0.17'
|
||||
spec.add_development_dependency 'simplecov-console', '~> 0.9'
|
||||
spec.add_development_dependency 'steep', '~> 1.3'
|
||||
spec.add_development_dependency 'steep', '~> 1.7.dev3'
|
||||
spec.add_development_dependency 'typeprof', '~> 0.21'
|
||||
spec.add_development_dependency 'yard', '~> 0.9'
|
||||
spec.add_development_dependency 'yard-junk', '~> 0.0.9'
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
module Nostr
|
||||
module Bech32
|
||||
# Perhaps a bug in RBS/Steep. +decode+ and +encode+ are not recognized as public class methods.
|
||||
def self?.decode: (String data) -> [String, String]
|
||||
def self?.encode: (hrp: String, data: String) -> String
|
||||
|
||||
def naddr_encode: (pubkey: PublicKey, ?relays: Array[String], ?kind: Integer, ?identifier: String) -> String
|
||||
def nevent_encode: (id: PublicKey, ?relays: Array[String], ?kind: Integer) -> String
|
||||
def nprofile_encode: (pubkey: PublicKey, ?relays: Array[String]) -> String
|
||||
def npub_encode: (String npub) -> String
|
||||
def nrelay_encode: (String nrelay) -> String
|
||||
def nsec_encode: (String nsec) -> String
|
||||
end
|
||||
end
|
|
@ -5,16 +5,18 @@ module Nostr
|
|||
def initialize: -> void
|
||||
def connect: (Relay relay) -> Thread
|
||||
def subscribe: (?subscription_id: String, ?filter: Filter) -> Subscription
|
||||
def unsubscribe: (String subscription_id) -> untyped
|
||||
def unsubscribe: (String subscription_id) -> void
|
||||
def publish: (Event event) -> untyped
|
||||
|
||||
private
|
||||
|
||||
attr_reader logger: Logger
|
||||
attr_reader subscriptions: Hash[String, Subscription]
|
||||
attr_reader parent_to_child_channel: untyped
|
||||
attr_reader child_to_parent_channel: untyped
|
||||
attr_reader parent_to_child_channel: EventMachine::Channel
|
||||
attr_reader child_to_parent_channel: EventMachine::Channel
|
||||
|
||||
def execute_within_an_em_thread: { -> untyped } -> Thread
|
||||
def initialize_channels: -> untyped
|
||||
def execute_within_an_em_thread: { -> void } -> Thread
|
||||
def initialize_channels: -> void
|
||||
def build_websocket_client: (String relay_name) -> Faye::WebSocket::Client
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
module Nostr
|
||||
class Client
|
||||
class ColorLogger < Logger
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
module Nostr
|
||||
class Client
|
||||
class Logger
|
||||
def attach_to: (Client client) -> void
|
||||
def on_connect: (Relay relay) -> void
|
||||
def on_message: (String message) -> void
|
||||
def on_send: (String message) -> void
|
||||
def on_error: (String message) -> void
|
||||
def on_close: (String code, String reason) -> void
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
module Nostr
|
||||
class Client
|
||||
class PlainLogger < Logger
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,16 +1,19 @@
|
|||
module Nostr
|
||||
class Crypto
|
||||
BN_BASE: Integer
|
||||
BN_BASE: 0 | 2 | 10 | 16
|
||||
CIPHER_CURVE: String
|
||||
CIPHER_ALGORITHM: String
|
||||
|
||||
def encrypt_text: (String, String, String) -> String
|
||||
def decrypt_text: (String, String, String) -> String
|
||||
def sign_event: (Event, String) -> Event
|
||||
def encrypt_text: (PrivateKey, PublicKey, String) -> String
|
||||
def decrypt_text: (PrivateKey, PublicKey, String) -> String
|
||||
def sign_event: (Event, PrivateKey) -> Event
|
||||
def sign_message: (String, PrivateKey) -> Signature
|
||||
def valid_sig?: (String, PublicKey, Signature) -> bool
|
||||
def check_sig!: (String, PublicKey, Signature) -> bool
|
||||
|
||||
private
|
||||
|
||||
def compute_shared_key: (String, String) -> String
|
||||
def compute_shared_key: (PrivateKey, PublicKey) -> String
|
||||
def hash_event:(Event) -> String
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
module Nostr
|
||||
class Error < StandardError
|
||||
end
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
module Nostr
|
||||
class InvalidHRPError < KeyValidationError
|
||||
def initialize: (String, String) -> void
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module Nostr
|
||||
class InvalidKeyFormatError < KeyValidationError
|
||||
def initialize: (String) -> void
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module Nostr
|
||||
class InvalidKeyLengthError < KeyValidationError
|
||||
def initialize: (String) -> void
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module Nostr
|
||||
class InvalidKeyTypeError < KeyValidationError
|
||||
def initialize: (String) -> void
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module Nostr
|
||||
class InvalidSignatureFormatError < SignatureValidationError
|
||||
def initialize: -> void
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module Nostr
|
||||
class InvalidSignatureLengthError < SignatureValidationError
|
||||
def initialize: -> void
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module Nostr
|
||||
class InvalidSignatureTypeError < SignatureValidationError
|
||||
def initialize: -> void
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
module Nostr
|
||||
class KeyValidationError < Error
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
module Nostr
|
||||
class SignatureValidationError < Error
|
||||
end
|
||||
end
|
|
@ -1,39 +1,40 @@
|
|||
module Nostr
|
||||
class Event
|
||||
attr_reader pubkey: String
|
||||
attr_reader pubkey: PublicKey
|
||||
attr_reader created_at: Integer
|
||||
attr_reader kind: Integer
|
||||
attr_reader tags: Array[Array[String]]
|
||||
attr_reader content: String
|
||||
attr_accessor id: String?|nil
|
||||
attr_accessor sig: String?|nil
|
||||
attr_accessor id: String?
|
||||
attr_accessor sig: Signature?
|
||||
|
||||
def initialize: (
|
||||
pubkey: String,
|
||||
pubkey: PublicKey,
|
||||
kind: Integer,
|
||||
content: String,
|
||||
?created_at: Integer,
|
||||
?tags: Array[Array[String]],
|
||||
?id: String|nil,
|
||||
?sig: String|nil
|
||||
?id: String?,
|
||||
?sig: Signature?
|
||||
) -> void
|
||||
|
||||
def serialize: -> [Integer, String, Integer, Integer, Array[Array[String]], String]
|
||||
|
||||
def to_h: -> {
|
||||
id: String?|nil,
|
||||
id: String?,
|
||||
pubkey: String,
|
||||
created_at: Integer,
|
||||
kind: Integer,
|
||||
tags: Array[Array[String]],
|
||||
content: String,
|
||||
sig: String?|nil
|
||||
sig: String?
|
||||
}
|
||||
def ==: (Event other) -> bool
|
||||
|
||||
def sign:(String) -> Event
|
||||
def sign:(PrivateKey) -> Event
|
||||
def verify_signature: -> bool
|
||||
|
||||
def add_event_reference: (String) -> Array[Array[String]]
|
||||
def add_pubkey_reference: (String) -> Array[Array[String]]
|
||||
def add_pubkey_reference: (PublicKey) -> Array[Array[String]]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,8 @@ module Nostr
|
|||
class EncryptedDirectMessage < Event
|
||||
def initialize: (
|
||||
plain_text: String,
|
||||
sender_private_key: String,
|
||||
recipient_public_key: String,
|
||||
sender_private_key: PrivateKey,
|
||||
recipient_public_key: PublicKey,
|
||||
?previous_direct_message: String|nil
|
||||
) -> void
|
||||
end
|
||||
|
|
|
@ -3,23 +3,14 @@ module Nostr
|
|||
attr_reader ids: Array[String]
|
||||
attr_reader authors: Array[String]
|
||||
attr_reader kinds: Array[Integer]
|
||||
attr_reader e: String
|
||||
attr_reader p: String
|
||||
attr_reader e: Array[String]
|
||||
attr_reader p: Array[String]
|
||||
attr_reader since: Integer
|
||||
attr_reader until: Integer
|
||||
attr_reader limit: Integer
|
||||
|
||||
def initialize: (**untyped) -> void
|
||||
def to_h: -> {
|
||||
ids: Array[String],
|
||||
authors: Array[String],
|
||||
kinds: Array[Integer],
|
||||
e: String,
|
||||
p: String,
|
||||
since: Integer,
|
||||
until: Integer,
|
||||
limit: Integer
|
||||
}
|
||||
def to_h: -> Hash[::Symbol, (::Array[::String] | ::Array[::Integer] | ::Integer)]
|
||||
def ==: (Filter other) -> bool
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
module Nostr
|
||||
class Key < String
|
||||
FORMAT: Regexp
|
||||
LENGTH: int
|
||||
|
||||
def self.from_bech32: (String) -> Key
|
||||
def self.hrp: -> String
|
||||
|
||||
def initialize: (String) -> void
|
||||
def to_bech32: -> String
|
||||
|
||||
private
|
||||
|
||||
def validate_hex_value: (String) -> nil
|
||||
end
|
||||
end
|
|
@ -1,9 +1,14 @@
|
|||
# Classes
|
||||
module Nostr
|
||||
class KeyPair
|
||||
attr_reader private_key: String
|
||||
attr_reader public_key: String
|
||||
attr_reader private_key: PrivateKey
|
||||
attr_reader public_key: PublicKey
|
||||
|
||||
def initialize: (private_key: String, public_key: String) -> void
|
||||
def initialize: (private_key: PrivateKey, public_key: PublicKey) -> void
|
||||
def to_ary: -> [PrivateKey, PublicKey]
|
||||
|
||||
private
|
||||
|
||||
def validate_keys: (PrivateKey, PublicKey) -> void
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,11 +3,14 @@ module Nostr
|
|||
class Keygen
|
||||
def initialize: -> void
|
||||
def generate_key_pair: -> KeyPair
|
||||
def generate_private_key: -> String
|
||||
def extract_public_key: (String private_key) -> String
|
||||
def generate_private_key: -> PrivateKey
|
||||
def extract_public_key: (PrivateKey private_key) -> PublicKey
|
||||
def get_key_pair_from_private_key: (PrivateKey private_key) -> KeyPair
|
||||
|
||||
private
|
||||
|
||||
attr_reader group: untyped
|
||||
|
||||
def validate_private_key: (PrivateKey private_key) -> void
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue