From 61a88981e6bc257cc6909449d72758447c0f635b Mon Sep 17 00:00:00 2001 From: Wilson Silva Date: Mon, 20 Nov 2023 21:03:24 +0700 Subject: [PATCH] Add full NIP-19 compatibility note, nprofile, nevent, naddr, npub, nsec and nrelay --- CHANGELOG.md | 3 +- README.md | 1 + docs/.vitepress/config.mjs | 7 + .../bech32-encoding-and-decoding-(NIP-19).md | 190 ++++++++++++++++ docs/implemented-nips.md | 1 + lib/nostr.rb | 1 + lib/nostr/bech32.rb | 203 ++++++++++++++++++ lib/nostr/key.rb | 10 +- sig/nostr/bech32.rbs | 14 ++ spec/nostr/bech32_spec.rb | 139 ++++++++++++ 10 files changed, 561 insertions(+), 8 deletions(-) create mode 100644 docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md create mode 100644 lib/nostr/bech32.rb create mode 100644 sig/nostr/bech32.rbs create mode 100644 spec/nostr/bech32_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a810835..2ed1788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Added relay message type enums `Nostr::RelayMessageType` -- Initial compliance with [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) - bech32-formatted private -keys and public keys +- Compliance with [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) - bech32-formatted strings - `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 diff --git a/README.md b/README.md index f9ea29f..da0d276 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ I made a detailed documentation for this gem and it's usage. The code is also fu - [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) ## 🔨 Development diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 7497fac..628730d 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -74,6 +74,13 @@ export default defineConfig(withMermaid({ { text: 'Encrypted Direct Message', link: '/events/encrypted-direct-message' }, ] }, + { + text: 'Common use cases', + collapsed: false, + items: [ + { text: 'Bech32 enc/decoding (NIP-19)', link: '/common-use-cases/bech32-encoding-and-decoding-(NIP-19)' }, + ] + }, { text: 'Implemented NIPs', link: '/implemented-nips', diff --git a/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md b/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md new file mode 100644 index 0000000..258fa64 --- /dev/null +++ b/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md @@ -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' +``` diff --git a/docs/implemented-nips.md b/docs/implemented-nips.md index b586dee..daee950 100644 --- a/docs/implemented-nips.md +++ b/docs/implemented-nips.md @@ -6,3 +6,4 @@ 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) diff --git a/lib/nostr.rb b/lib/nostr.rb index 6dfe5ae..f89a7a7 100644 --- a/lib/nostr.rb +++ b/lib/nostr.rb @@ -1,6 +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' diff --git a/lib/nostr/bech32.rb b/lib/nostr/bech32.rb new file mode 100644 index 0000000..01b2eb9 --- /dev/null +++ b/lib/nostr/bech32.rb @@ -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] 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] 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] 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] 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 diff --git a/lib/nostr/key.rb b/lib/nostr/key.rb index dc875d0..ba0946b 100644 --- a/lib/nostr/key.rb +++ b/lib/nostr/key.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'bech32' - module Nostr # Abstract class for all keys # @@ -50,11 +48,11 @@ def initialize(hex_value) # @return [Key] the key. # def self.from_bech32(bech32_value) - entity = Bech32::Nostr::NIP19.decode(bech32_value) + type, data = Bech32.decode(bech32_value) - raise InvalidHRPError.new(entity.hrp, hrp) unless entity.hrp == hrp + raise InvalidHRPError.new(type, hrp) unless type == hrp - new(entity.data) + new(data) end # Abstract method to be implemented by subclasses to provide the HRP (npub, nsec) @@ -81,7 +79,7 @@ def self.hrp # # @return [String] The bech32 string representation of the key # - def to_bech32 = Bech32::Nostr::BareEntity.new(self.class.hrp, self).encode + def to_bech32 = Bech32.encode(hrp: self.class.hrp, data: self) protected diff --git a/sig/nostr/bech32.rbs b/sig/nostr/bech32.rbs new file mode 100644 index 0000000..6955777 --- /dev/null +++ b/sig/nostr/bech32.rbs @@ -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 diff --git a/spec/nostr/bech32_spec.rb b/spec/nostr/bech32_spec.rb new file mode 100644 index 0000000..4aa8133 --- /dev/null +++ b/spec/nostr/bech32_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::Bech32 do + let(:keypair) { Nostr::Keygen.new.generate_key_pair } + let(:private_key) { keypair.private_key } + let(:public_key) { keypair.public_key } + + describe '.encode' do + it 'encodes data into the bech32 format' do + npub = described_class.encode(hrp: 'npub', data: public_key) + expect(npub).to match(/npub1\w+/) + end + end + + describe '.decode' do + it 'decodes data from the bech32 format' do + npub = described_class.encode(hrp: 'npub', data: public_key) + type, decoded = described_class.decode(npub) + + aggregate_failures do + expect(type).to eq('npub') + expect(decoded).to eq(public_key) + end + end + end + + describe '.nsec_encode' do + it 'encodes and decodes hexadecimal private keys' do + nsec = described_class.nsec_encode(private_key) + type, data = described_class.decode(nsec) + + aggregate_failures do + expect(nsec).to match(/nsec1\w+/) + expect(type).to eq('nsec') + expect(data).to eq(private_key) + end + end + end + + describe '.npub_encode' do + it 'encodes and decodes hexadecimal public keys' do + npub = described_class.npub_encode(public_key) + type, data = described_class.decode(npub) + + aggregate_failures do + expect(npub).to match(/npub1\w+/) + expect(type).to eq('npub') + expect(data).to eq(public_key) + end + end + end + + describe '.nprofile_encode' do + it 'encodes and decodes nprofiles with relays' do + relay_urls = %w[wss://relay.damus.io wss://nos.lol] + nprofile = described_class.nprofile_encode(pubkey: public_key, relays: relay_urls) + type, profile = described_class.decode(nprofile) + + aggregate_failures do + expect(nprofile).to match(/nprofile1\w+/) + expect(type).to eq('nprofile') + expect(profile.entries[0].value).to eq(public_key) + expect(profile.entries[1].value).to eq(relay_urls[0]) + expect(profile.entries[2].value).to eq(relay_urls[1]) + end + end + + it 'encodes and decodes nprofiles without relays' do + nprofile = described_class.nprofile_encode(pubkey: public_key) + type, profile = described_class.decode(nprofile) + + aggregate_failures do + expect(nprofile).to match(/nprofile1\w+/) + expect(type).to eq('nprofile') + expect(profile.entries[0].value).to eq(public_key) + end + end + end + + describe '.naddr_encode' do + it 'encodes and decodes naddr' do + relay_urls = %w[wss://relay.damus.io wss://nos.lol] + naddr = described_class.naddr_encode( + pubkey: public_key, + relays: relay_urls, + kind: 1984, + identifier: 'damus' + ) + type, addr = described_class.decode(naddr) + + aggregate_failures do + expect(naddr).to match(/naddr1\w+/) + expect(type).to eq('naddr') + expect(addr.entries[0].value).to eq(public_key) + expect(addr.entries[1].value).to eq(relay_urls[0]) + expect(addr.entries[2].value).to eq(relay_urls[1]) + expect(addr.entries[3].value).to eq(1984) + expect(addr.entries[4].value).to eq('damus') + end + end + end + + describe '.nevent_encode' do + it 'encodes and decodes nevent' do + relay_urls = %w[wss://relay.damus.io wss://nos.lol] + nevent = described_class.nevent_encode( + id: '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3', + relays: relay_urls, + kind: Nostr::EventKind::TEXT_NOTE + ) + type, event = described_class.decode(nevent) + + aggregate_failures do + expect(nevent).to match(/nevent1\w+/) + expect(type).to eq('nevent') + expect(event.entries[0].value).to eq('0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3') + expect(event.entries[1].value).to eq(relay_urls[0]) + expect(event.entries[2].value).to eq(relay_urls[1]) + expect(event.entries[3].value).to eq(Nostr::EventKind::TEXT_NOTE) + end + end + end + + describe '.nrelay_encode' do + it 'encodes and decodes nrelay' do + relay_url = 'wss://relay.damus.io' + nrelay = described_class.nrelay_encode(relay_url) + type, data = described_class.decode(nrelay) + + aggregate_failures do + expect(nrelay).to match(/nrelay1\w+/) + expect(type).to eq('nrelay') + expect(data.entries[0].value).to eq(relay_url) + end + end + end +end