Add full NIP-19 compatibility
note, nprofile, nevent, naddr, npub, nsec and nrelay
This commit is contained in:
parent
bba18d1bc0
commit
61a88981e6
@ -9,8 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added relay message type enums `Nostr::RelayMessageType`
|
- 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
|
- Compliance with [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) - bech32-formatted strings
|
||||||
keys and public keys
|
|
||||||
- `Nostr::PrivateKey` and `Nostr::PublicKey` to represent private and public keys, respectively
|
- `Nostr::PrivateKey` and `Nostr::PublicKey` to represent private and public keys, respectively
|
||||||
- Added a validation of private and public keys
|
- Added a validation of private and public keys
|
||||||
- Added an ability to convert keys to and from Bech32 format
|
- Added an ability to convert keys to and from Bech32 format
|
||||||
|
@ -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-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-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-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
|
## 🔨 Development
|
||||||
|
|
||||||
|
@ -74,6 +74,13 @@ export default defineConfig(withMermaid({
|
|||||||
{ text: 'Encrypted Direct Message', link: '/events/encrypted-direct-message' },
|
{ 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',
|
text: 'Implemented NIPs',
|
||||||
link: '/implemented-nips',
|
link: '/implemented-nips',
|
||||||
|
190
docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md
Normal file
190
docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md
Normal file
@ -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'
|
||||||
|
```
|
@ -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-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-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-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)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'nostr/errors'
|
require_relative 'nostr/errors'
|
||||||
|
require_relative 'nostr/bech32'
|
||||||
require_relative 'nostr/crypto'
|
require_relative 'nostr/crypto'
|
||||||
require_relative 'nostr/version'
|
require_relative 'nostr/version'
|
||||||
require_relative 'nostr/keygen'
|
require_relative 'nostr/keygen'
|
||||||
|
203
lib/nostr/bech32.rb
Normal file
203
lib/nostr/bech32.rb
Normal file
@ -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
|
@ -1,7 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'bech32'
|
|
||||||
|
|
||||||
module Nostr
|
module Nostr
|
||||||
# Abstract class for all keys
|
# Abstract class for all keys
|
||||||
#
|
#
|
||||||
@ -50,11 +48,11 @@ module Nostr
|
|||||||
# @return [Key] the key.
|
# @return [Key] the key.
|
||||||
#
|
#
|
||||||
def self.from_bech32(bech32_value)
|
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
|
end
|
||||||
|
|
||||||
# Abstract method to be implemented by subclasses to provide the HRP (npub, nsec)
|
# Abstract method to be implemented by subclasses to provide the HRP (npub, nsec)
|
||||||
@ -81,7 +79,7 @@ module Nostr
|
|||||||
#
|
#
|
||||||
# @return [String] The bech32 string representation of the key
|
# @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
|
protected
|
||||||
|
|
||||||
|
14
sig/nostr/bech32.rbs
Normal file
14
sig/nostr/bech32.rbs
Normal file
@ -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
|
139
spec/nostr/bech32_spec.rb
Normal file
139
spec/nostr/bech32_spec.rb
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user