nostr 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.adr-dir +1 -0
- data/.editorconfig +1 -1
- data/.rubocop.yml +24 -1
- data/.tool-versions +2 -1
- data/CHANGELOG.md +70 -1
- data/README.md +93 -228
- data/adr/0001-record-architecture-decisions.md +19 -0
- data/adr/0002-introduction-of-signature-class.md +27 -0
- data/docs/.gitignore +4 -0
- data/docs/.vitepress/config.mjs +114 -0
- data/docs/README.md +44 -0
- data/docs/api-examples.md +49 -0
- data/docs/bun.lockb +0 -0
- data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
- data/docs/common-use-cases/signing-and-verifying-events.md +50 -0
- data/docs/common-use-cases/signing-and-verifying-messages.md +43 -0
- data/docs/core/client.md +108 -0
- data/docs/core/keys.md +136 -0
- data/docs/core/user.md +43 -0
- data/docs/events/contact-list.md +29 -0
- data/docs/events/encrypted-direct-message.md +28 -0
- data/docs/events/recommend-server.md +32 -0
- data/docs/events/set-metadata.md +20 -0
- data/docs/events/text-note.md +15 -0
- data/docs/events.md +11 -0
- data/docs/getting-started/installation.md +21 -0
- data/docs/getting-started/overview.md +171 -0
- data/docs/implemented-nips.md +9 -0
- data/docs/index.md +42 -0
- data/docs/markdown-examples.md +85 -0
- data/docs/package.json +12 -0
- data/docs/relays/connecting-to-a-relay.md +21 -0
- data/docs/relays/publishing-events.md +29 -0
- data/docs/relays/receiving-events.md +6 -0
- data/docs/subscriptions/creating-a-subscription.md +49 -0
- data/docs/subscriptions/deleting-a-subscription.md +10 -0
- data/docs/subscriptions/filtering-subscription-events.md +115 -0
- data/docs/subscriptions/updating-a-subscription.md +4 -0
- data/lib/nostr/bech32.rb +203 -0
- data/lib/nostr/client.rb +2 -1
- data/lib/nostr/crypto.rb +93 -13
- data/lib/nostr/errors/error.rb +7 -0
- data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
- data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
- data/lib/nostr/errors/invalid_signature_format_error.rb +18 -0
- data/lib/nostr/errors/invalid_signature_length_error.rb +18 -0
- data/lib/nostr/errors/invalid_signature_type_error.rb +16 -0
- data/lib/nostr/errors/key_validation_error.rb +6 -0
- data/lib/nostr/errors/signature_validation_error.rb +6 -0
- data/lib/nostr/errors.rb +12 -0
- data/lib/nostr/event.rb +40 -13
- data/lib/nostr/event_kind.rb +1 -0
- data/lib/nostr/events/encrypted_direct_message.rb +8 -7
- data/lib/nostr/filter.rb +14 -11
- data/lib/nostr/key.rb +100 -0
- data/lib/nostr/key_pair.rb +54 -6
- data/lib/nostr/keygen.rb +44 -5
- data/lib/nostr/private_key.rb +36 -0
- data/lib/nostr/public_key.rb +36 -0
- data/lib/nostr/relay_message_type.rb +18 -0
- data/lib/nostr/signature.rb +67 -0
- data/lib/nostr/subscription.rb +2 -2
- data/lib/nostr/user.rb +17 -8
- data/lib/nostr/version.rb +1 -1
- data/lib/nostr.rb +7 -0
- data/nostr.gemspec +13 -13
- data/sig/nostr/bech32.rbs +14 -0
- data/sig/nostr/client.rbs +5 -5
- data/sig/nostr/crypto.rbs +8 -5
- data/sig/nostr/errors/error.rbs +4 -0
- data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
- data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
- data/sig/nostr/errors/invalid_signature_format_error.rbs +5 -0
- data/sig/nostr/errors/invalid_signature_length_error.rbs +5 -0
- data/sig/nostr/errors/invalid_signature_type_error.rbs +5 -0
- data/sig/nostr/errors/key_validation_error.rbs +4 -0
- data/sig/nostr/errors/signature_validation_error.rbs +4 -0
- data/sig/nostr/event.rbs +11 -10
- data/sig/nostr/events/encrypted_direct_message.rbs +2 -2
- data/sig/nostr/filter.rbs +3 -12
- data/sig/nostr/key.rbs +16 -0
- data/sig/nostr/key_pair.rbs +8 -3
- data/sig/nostr/keygen.rbs +5 -2
- data/sig/nostr/private_key.rbs +4 -0
- data/sig/nostr/public_key.rbs +4 -0
- data/sig/nostr/relay_message_type.rbs +8 -0
- data/sig/nostr/signature.rbs +14 -0
- data/sig/nostr/user.rbs +4 -8
- data/sig/vendor/bech32/nostr/entity.rbs +41 -0
- data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
- data/sig/vendor/bech32/segwit_addr.rbs +21 -0
- data/sig/vendor/bech32.rbs +25 -0
- data/sig/vendor/event_emitter.rbs +10 -3
- data/sig/vendor/event_machine/channel.rbs +1 -1
- data/sig/vendor/faye/websocket/api.rbs +45 -0
- data/sig/vendor/faye/websocket/client.rbs +43 -0
- data/sig/vendor/faye/websocket.rbs +30 -0
- data/sig/vendor/schnorr/signature.rbs +16 -0
- data/sig/vendor/schnorr.rbs +3 -1
- metadata +102 -28
@@ -0,0 +1,29 @@
|
|
1
|
+
# Publishing events
|
2
|
+
|
3
|
+
Create a [signed event](../core/keys) and call the method
|
4
|
+
[`Nostr::Client#publish`](https://www.rubydoc.info/gems/nostr/Nostr/Client#publish-instance_method) to send the
|
5
|
+
event to the relay.
|
6
|
+
|
7
|
+
```ruby{4-8,17}
|
8
|
+
# Create a user with the keypair
|
9
|
+
user = Nostr::User.new(keypair: keypair)
|
10
|
+
|
11
|
+
# Create a signed event
|
12
|
+
text_note_event = user.create_event(
|
13
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
14
|
+
content: 'Your feedback is appreciated, now pay $8'
|
15
|
+
)
|
16
|
+
|
17
|
+
# Connect asynchronously to a relay
|
18
|
+
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
|
19
|
+
client.connect(relay)
|
20
|
+
|
21
|
+
# Listen asynchronously for the connect event
|
22
|
+
client.on :connect do
|
23
|
+
# Send the event to the relay
|
24
|
+
client.publish(text_note_event)
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
The relay will verify the signature of the event with the public key. If the signature is valid, the relay should
|
29
|
+
broadcast the event to all subscribers.
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# Receiving events
|
2
|
+
|
3
|
+
To receive events from Relays, you must create a subscription on the relay. A subscription is a filter that defines the
|
4
|
+
events you want to receive.
|
5
|
+
|
6
|
+
For more information, read the [Subscription](../subscriptions/creating-a-subscription.md) section.
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Creating a subscription
|
2
|
+
|
3
|
+
A client can request events and subscribe to new updates __after__ it has established a connection with the Relay.
|
4
|
+
|
5
|
+
You may use a [`Nostr::Filter`](https://www.rubydoc.info/gems/nostr/Nostr/Filter) instance with as many attributes as
|
6
|
+
you wish:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
client.on :connect do
|
10
|
+
filter = Nostr::Filter.new(
|
11
|
+
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
|
12
|
+
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
|
13
|
+
kinds: [Nostr::EventKind::TEXT_NOTE],
|
14
|
+
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
|
15
|
+
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
|
16
|
+
since: 1230981305,
|
17
|
+
until: 1292190341,
|
18
|
+
limit: 420,
|
19
|
+
)
|
20
|
+
|
21
|
+
subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
With just a few:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
client.on :connect do
|
29
|
+
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
|
30
|
+
subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
Or omit the filter:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
client.on :connect do
|
38
|
+
subscription = client.subscribe(subscription_id: 'an-id')
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Or even omit the subscription id:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
client.on :connect do
|
46
|
+
subscription = client.subscribe(filter: filter)
|
47
|
+
subscription.id # => "13736f08dee8d7b697222ba605c6fab2" (randomly generated)
|
48
|
+
end
|
49
|
+
```
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Stop previous subscriptions
|
2
|
+
|
3
|
+
You can stop receiving messages from a subscription by calling
|
4
|
+
[`Nostr::Client#unsubscribe`](https://www.rubydoc.info/gems/nostr/Nostr/Client#unsubscribe-instance_method) with the
|
5
|
+
ID of the subscription you want to stop receiving messages from:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
client.unsubscribe('your-subscription-id')
|
9
|
+
client.unsubscribe(subscription.id)
|
10
|
+
```
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# Filtering events
|
2
|
+
|
3
|
+
## Filtering by id
|
4
|
+
|
5
|
+
You can filter events by their ids:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
filter = Nostr::Filter.new(
|
9
|
+
ids: [
|
10
|
+
# matches events with these exact IDs
|
11
|
+
'8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8',
|
12
|
+
'461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
|
13
|
+
]
|
14
|
+
)
|
15
|
+
subscription = client.subscribe(filter: filter)
|
16
|
+
```
|
17
|
+
|
18
|
+
## Filtering by author
|
19
|
+
|
20
|
+
You can filter events by their author's pubkey:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
filter = Nostr::Filter.new(
|
24
|
+
authors: [
|
25
|
+
# matches events whose (authors) pubkey match these exact IDs
|
26
|
+
'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
|
27
|
+
'51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
|
28
|
+
]
|
29
|
+
)
|
30
|
+
subscription = client.subscribe(filter: filter)
|
31
|
+
```
|
32
|
+
|
33
|
+
## Filtering by kind
|
34
|
+
|
35
|
+
You can filter events by their kind:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
filter = Nostr::Filter.new(
|
39
|
+
kinds: [
|
40
|
+
# matches events whose kind is TEXT_NOTE
|
41
|
+
Nostr::EventKind::TEXT_NOTE,
|
42
|
+
# and matches events whose kind is CONTACT_LIST
|
43
|
+
Nostr::EventKind::CONTACT_LIST,
|
44
|
+
]
|
45
|
+
)
|
46
|
+
subscription = client.subscribe(filter: filter)
|
47
|
+
```
|
48
|
+
|
49
|
+
## Filtering by referenced event
|
50
|
+
|
51
|
+
You can filter events by the events they reference (in their `e` tag):
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
filter = Nostr::Filter.new(
|
55
|
+
e: [
|
56
|
+
# matches events that reference other events whose ids match these exact IDs
|
57
|
+
'f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52',
|
58
|
+
'f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8',
|
59
|
+
]
|
60
|
+
)
|
61
|
+
subscription = client.subscribe(filter: filter)
|
62
|
+
```
|
63
|
+
|
64
|
+
## Filtering by referenced pubkey
|
65
|
+
|
66
|
+
You can filter events by the pubkeys they reference (in their `p` tag):
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
filter = Nostr::Filter.new(
|
70
|
+
p: [
|
71
|
+
# matches events that reference other pubkeys that match these exact IDs
|
72
|
+
'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
|
73
|
+
'51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
|
74
|
+
]
|
75
|
+
)
|
76
|
+
subscription = client.subscribe(filter: filter)
|
77
|
+
```
|
78
|
+
|
79
|
+
## Filtering by timestamp
|
80
|
+
|
81
|
+
You can filter events by their timestamp:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
filter = Nostr::Filter.new(
|
85
|
+
since: 1230981305, # matches events that are newer than this timestamp
|
86
|
+
until: 1292190341, # matches events that are older than this timestamp
|
87
|
+
)
|
88
|
+
subscription = client.subscribe(filter: filter)
|
89
|
+
```
|
90
|
+
|
91
|
+
## Limiting the number of events
|
92
|
+
|
93
|
+
You can limit the number of events received:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
filter = Nostr::Filter.new(
|
97
|
+
limit: 420, # matches at most 420 events
|
98
|
+
)
|
99
|
+
subscription = client.subscribe(filter: filter)
|
100
|
+
```
|
101
|
+
|
102
|
+
## Combining filters
|
103
|
+
|
104
|
+
You can combine filters. For example, to match `5` text note events that are newer than `1230981305` from the author
|
105
|
+
`ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577`:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
filter = Nostr::Filter.new(
|
109
|
+
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
|
110
|
+
kinds: [Nostr::EventKind::TEXT_NOTE],
|
111
|
+
since: 1230981305,
|
112
|
+
limit: 5,
|
113
|
+
)
|
114
|
+
subscription = client.subscribe(filter: filter)
|
115
|
+
```
|
data/lib/nostr/bech32.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bech32'
|
4
|
+
require 'bech32/nostr'
|
5
|
+
require 'bech32/nostr/entity'
|
6
|
+
|
7
|
+
module Nostr
|
8
|
+
# Bech32 encoding and decoding
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
#
|
12
|
+
module Bech32
|
13
|
+
# Decodes a bech32-encoded string
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# bech32_value = 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
19
|
+
# Nostr::Bech32.decode(bech32_value) # => ['npub', '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d8...']
|
20
|
+
#
|
21
|
+
# @param [String] bech32_value The bech32-encoded string to decode
|
22
|
+
#
|
23
|
+
# @return [Array<String, String>] The human readable part and the data
|
24
|
+
#
|
25
|
+
def self.decode(bech32_value)
|
26
|
+
entity = ::Bech32::Nostr::NIP19.decode(bech32_value)
|
27
|
+
|
28
|
+
case entity
|
29
|
+
in ::Bech32::Nostr::BareEntity
|
30
|
+
[entity.hrp, entity.data]
|
31
|
+
in ::Bech32::Nostr::TLVEntity
|
32
|
+
[entity.hrp, entity.entries]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Encodes data into a bech32 string
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# Nostr::Bech32.encode(hrp: 'npub', data: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
42
|
+
# # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
43
|
+
#
|
44
|
+
# @param [String] hrp The human readable part (npub, nsec, nprofile, nrelay, nevent, naddr, etc)
|
45
|
+
# @param [String] data The data to encode
|
46
|
+
#
|
47
|
+
# @return [String] The bech32-encoded string
|
48
|
+
#
|
49
|
+
def self.encode(hrp:, data:)
|
50
|
+
::Bech32::Nostr::BareEntity.new(hrp, data).encode
|
51
|
+
end
|
52
|
+
|
53
|
+
# Encodes a hex-encoded public key into a bech32 string
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# Nostr::Bech32.npub_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
59
|
+
# # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
60
|
+
#
|
61
|
+
# @param [String] npub The public key to encode
|
62
|
+
#
|
63
|
+
# @see Nostr::Bech32#encode
|
64
|
+
# @see Nostr::PublicKey#to_bech32
|
65
|
+
# @see Nostr::PrivateKey#to_bech32
|
66
|
+
#
|
67
|
+
# @return [String] The bech32-encoded string
|
68
|
+
#
|
69
|
+
def self.npub_encode(npub)
|
70
|
+
encode(hrp: 'npub', data: npub)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Encodes a hex-encoded private key into a bech32 string
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# Nostr::Bech32.nsec_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
79
|
+
# # => 'nsec10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
80
|
+
#
|
81
|
+
# @param [String] nsec The private key to encode
|
82
|
+
#
|
83
|
+
# @see Nostr::Bech32#encode
|
84
|
+
# @see Nostr::PrivateKey#to_bech32
|
85
|
+
# @see Nostr::PublicKey#to_bech32
|
86
|
+
#
|
87
|
+
# @return [String] The bech32-encoded string
|
88
|
+
#
|
89
|
+
def self.nsec_encode(nsec)
|
90
|
+
encode(hrp: 'nsec', data: nsec)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Encodes an address into a bech32 string
|
94
|
+
#
|
95
|
+
# @api public
|
96
|
+
#
|
97
|
+
# @example
|
98
|
+
# naddr = Nostr::Bech32.naddr_encode(
|
99
|
+
# pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
|
100
|
+
# relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
101
|
+
# kind: Nostr::EventKind::TEXT_NOTE,
|
102
|
+
# identifier: 'damus'
|
103
|
+
# )
|
104
|
+
# naddr # => 'naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7ns...'
|
105
|
+
#
|
106
|
+
# @param [PublicKey] pubkey The public key to encode
|
107
|
+
# @param [Array<String>] relays The relays to encode
|
108
|
+
# @param [String] kind The kind of address to encode
|
109
|
+
# @param [String] identifier The identifier of the address to encode
|
110
|
+
#
|
111
|
+
# @return [String] The bech32-encoded string
|
112
|
+
#
|
113
|
+
def self.naddr_encode(pubkey:, relays: [], kind: nil, identifier: nil)
|
114
|
+
entry_relays = relays.map do |relay_url|
|
115
|
+
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
116
|
+
end
|
117
|
+
|
118
|
+
pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, pubkey)
|
119
|
+
kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
|
120
|
+
identifier_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, identifier)
|
121
|
+
|
122
|
+
entries = [pubkey_entry, *entry_relays, kind_entry, identifier_entry].compact
|
123
|
+
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT_COORDINATE, entries)
|
124
|
+
entity.encode
|
125
|
+
end
|
126
|
+
|
127
|
+
# Encodes an event into a bech32 string
|
128
|
+
#
|
129
|
+
# @api public
|
130
|
+
#
|
131
|
+
# @example
|
132
|
+
# nevent = Nostr::Bech32.nevent_encode(
|
133
|
+
# id: '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3',
|
134
|
+
# relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
135
|
+
# kind: Nostr::EventKind::TEXT_NOTE,
|
136
|
+
# )
|
137
|
+
# nevent # => 'nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0pra...'
|
138
|
+
#
|
139
|
+
# @param [PublicKey] id The id the event to encode
|
140
|
+
# @param [Array<String>] relays The relays to encode
|
141
|
+
# @param [String] kind The kind of event to encode
|
142
|
+
#
|
143
|
+
# @return [String] The bech32-encoded string
|
144
|
+
#
|
145
|
+
def self.nevent_encode(id:, relays: [], kind: nil)
|
146
|
+
entry_relays = relays.map do |relay_url|
|
147
|
+
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
148
|
+
end
|
149
|
+
|
150
|
+
id_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, id)
|
151
|
+
kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
|
152
|
+
|
153
|
+
entries = [id_entry, *entry_relays, kind_entry].compact
|
154
|
+
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT, entries)
|
155
|
+
entity.encode
|
156
|
+
end
|
157
|
+
|
158
|
+
# Encodes a profile into a bech32 string
|
159
|
+
#
|
160
|
+
# @api public
|
161
|
+
#
|
162
|
+
# @example
|
163
|
+
# nprofile = Nostr::Bech32.nprofile_encode(
|
164
|
+
# pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
|
165
|
+
# relays: ['wss://relay.damus.io', 'wss://nos.lol']
|
166
|
+
# )
|
167
|
+
#
|
168
|
+
# @param [PublicKey] pubkey The public key to encode
|
169
|
+
# @param [Array<String>] relays The relays to encode
|
170
|
+
#
|
171
|
+
# @return [String] The bech32-encoded string
|
172
|
+
#
|
173
|
+
def self.nprofile_encode(pubkey:, relays: [])
|
174
|
+
entry_relays = relays.map do |relay_url|
|
175
|
+
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
176
|
+
end
|
177
|
+
|
178
|
+
pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, pubkey)
|
179
|
+
entries = [pubkey_entry, *entry_relays].compact
|
180
|
+
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_PROFILE, entries)
|
181
|
+
entity.encode
|
182
|
+
end
|
183
|
+
|
184
|
+
# Encodes a relay URL into a bech32 string
|
185
|
+
#
|
186
|
+
# @api public
|
187
|
+
#
|
188
|
+
# @example
|
189
|
+
# nrelay = Nostr::Bech32.nrelay_encode('wss://relay.damus.io')
|
190
|
+
# nrelay # => 'nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x'
|
191
|
+
#
|
192
|
+
# @param [String] relay_url The relay url to encode
|
193
|
+
#
|
194
|
+
# @return [String] The bech32-encoded string
|
195
|
+
#
|
196
|
+
def self.nrelay_encode(relay_url)
|
197
|
+
relay_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, relay_url)
|
198
|
+
|
199
|
+
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_RELAY, [relay_entry])
|
200
|
+
entity.encode
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
data/lib/nostr/client.rb
CHANGED
@@ -74,7 +74,8 @@ module Nostr
|
|
74
74
|
# @example Subscribing to all events created after a certain time
|
75
75
|
# subscription = client.subscribe(filter: Nostr::Filter.new(since: 1230981305))
|
76
76
|
#
|
77
|
-
# @param subscription_id [String] The subscription id.
|
77
|
+
# @param subscription_id [String] The subscription id. An arbitrary, non-empty string of max length 64
|
78
|
+
# chars used to represent a subscription.
|
78
79
|
# @param filter [Filter] A set of attributes that represent the events that the client is interested in.
|
79
80
|
#
|
80
81
|
# @return [Subscription] The subscription object
|
data/lib/nostr/crypto.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Nostr
|
4
|
-
# Performs cryptographic operations
|
4
|
+
# Performs cryptographic operations.
|
5
5
|
class Crypto
|
6
6
|
# Numeric base of the OpenSSL big number used in an event content's encryption.
|
7
7
|
#
|
@@ -30,8 +30,8 @@ module Nostr
|
|
30
30
|
# encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8')
|
31
31
|
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
32
32
|
#
|
33
|
-
# @param sender_private_key [
|
34
|
-
# @param recipient_public_key [
|
33
|
+
# @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the creator.
|
34
|
+
# @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient.
|
35
35
|
# @param plain_text [String] The text to be encrypted
|
36
36
|
#
|
37
37
|
# @return [String] Encrypted text.
|
@@ -54,14 +54,18 @@ module Nostr
|
|
54
54
|
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
|
55
55
|
# decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted)
|
56
56
|
#
|
57
|
-
# @param sender_public_key [
|
58
|
-
# @param recipient_private_key [
|
57
|
+
# @param sender_public_key [PublicKey] 32-bytes hex-encoded public key of the message creator.
|
58
|
+
# @param recipient_private_key [PrivateKey] 32-bytes hex-encoded public key of the recipient.
|
59
59
|
# @param encrypted_text [String] The text to be decrypted
|
60
60
|
#
|
61
61
|
# @return [String] Decrypted text.
|
62
62
|
#
|
63
63
|
def decrypt_text(recipient_private_key, sender_public_key, encrypted_text)
|
64
64
|
base64_encoded_text, iv = encrypted_text.split('?iv=')
|
65
|
+
|
66
|
+
# Ensure iv and base64_encoded_text are not nil
|
67
|
+
return '' unless iv && base64_encoded_text
|
68
|
+
|
65
69
|
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
|
66
70
|
cipher.iv = Base64.decode64(iv)
|
67
71
|
cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
|
@@ -80,31 +84,107 @@ module Nostr
|
|
80
84
|
# event.sig # => a signature
|
81
85
|
#
|
82
86
|
# @param event [Event] The event to be signed
|
83
|
-
# @param private_key [
|
87
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
84
88
|
#
|
85
89
|
# @return [Event] An unsigned event.
|
86
90
|
#
|
87
91
|
def sign_event(event, private_key)
|
88
92
|
event_digest = hash_event(event)
|
89
|
-
|
90
|
-
hex_private_key = Array(private_key).pack('H*')
|
91
|
-
hex_message = Array(event_digest).pack('H*')
|
92
|
-
event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
93
|
+
signature = sign_message(event_digest, private_key)
|
93
94
|
|
94
95
|
event.id = event_digest
|
95
|
-
event.sig =
|
96
|
+
event.sig = signature
|
96
97
|
|
97
98
|
event
|
98
99
|
end
|
99
100
|
|
101
|
+
# Signs a message using the Schnorr signature algorithm
|
102
|
+
#
|
103
|
+
# @api public
|
104
|
+
#
|
105
|
+
# @example Signing a message
|
106
|
+
# crypto = Nostr::Crypto.new
|
107
|
+
# message = 'Viva la libertad carajo'
|
108
|
+
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
|
109
|
+
# signature = crypto.sign_message(message, private_key)
|
110
|
+
# signature # => 'b2115694a576f5bdcebf8c0951a3c7adcfbdb17b11cb9e6d6b7017691138bc6' \
|
111
|
+
# '38fee642a7bd26f71b313a7057181294198900a9770d1435e43f182acf3d34c26'
|
112
|
+
#
|
113
|
+
# @param [String] message The message to be signed
|
114
|
+
# @param [PrivateKey] private_key The private key used for signing
|
115
|
+
#
|
116
|
+
# @return [Signature] A signature object containing the signature as a 64-byte hexadecimal string.
|
117
|
+
#
|
118
|
+
def sign_message(message, private_key)
|
119
|
+
hex_private_key = Array(private_key).pack('H*')
|
120
|
+
hex_message = Array(message).pack('H*')
|
121
|
+
hex_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
122
|
+
|
123
|
+
Signature.new(hex_signature.to_s)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Verifies the given {Signature} and returns true if it is valid
|
127
|
+
#
|
128
|
+
# @api public
|
129
|
+
#
|
130
|
+
# @example Checking a signature
|
131
|
+
# public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6')
|
132
|
+
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
|
133
|
+
# message = 'Viva la libertad carajo'
|
134
|
+
# crypto = Nostr::Crypto.new
|
135
|
+
# signature = crypto.sign_message(message, private_key)
|
136
|
+
# valid = crypto.valid_sig?(message, public_key, signature)
|
137
|
+
# valid # => true
|
138
|
+
#
|
139
|
+
# @see #check_sig!
|
140
|
+
#
|
141
|
+
# @param [String] message A message to be signed with binary format.
|
142
|
+
# @param [PublicKey] public_key The public key with binary format.
|
143
|
+
# @param [Signature] signature The signature with binary format.
|
144
|
+
#
|
145
|
+
# @return [Boolean] whether signature is valid.
|
146
|
+
#
|
147
|
+
def valid_sig?(message, public_key, signature)
|
148
|
+
signature = Schnorr::Signature.decode([signature].pack('H*'))
|
149
|
+
Schnorr.valid_sig?([message].pack('H*'), [public_key].pack('H*'), signature.encode)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Verifies the given {Signature} and raises an +Schnorr::InvalidSignatureError+ if it is invalid
|
153
|
+
#
|
154
|
+
# @api public
|
155
|
+
#
|
156
|
+
# @example Checking a signature
|
157
|
+
# public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6')
|
158
|
+
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
|
159
|
+
# message = 'Viva la libertad carajo'
|
160
|
+
# crypto = Nostr::Crypto.new
|
161
|
+
# signature = crypto.sign_message(message, private_key)
|
162
|
+
# valid = crypto.valid_sig?(message, public_key, signature)
|
163
|
+
# valid # => true
|
164
|
+
#
|
165
|
+
# @see #valid_sig?
|
166
|
+
#
|
167
|
+
# @param [String] message A message to be signed with binary format.
|
168
|
+
# @param [PublicKey] public_key The public key with binary format.
|
169
|
+
# @param [Signature] signature The signature with binary format.
|
170
|
+
#
|
171
|
+
# @raise [Schnorr::InvalidSignatureError] if the signature is invalid.
|
172
|
+
#
|
173
|
+
# @return [Boolean] whether signature is valid.
|
174
|
+
#
|
175
|
+
def check_sig!(message, public_key, signature)
|
176
|
+
signature = Schnorr::Signature.decode([signature].pack('H*'))
|
177
|
+
Schnorr.check_sig!([message].pack('H*'), [public_key].pack('H*'), signature.encode)
|
178
|
+
end
|
179
|
+
|
100
180
|
private
|
101
181
|
|
102
182
|
# Finds a shared key between two keys
|
103
183
|
#
|
104
184
|
# @api private
|
105
185
|
#
|
106
|
-
# @param private_key [
|
107
|
-
# @param public_key [
|
186
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
187
|
+
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
108
188
|
#
|
109
189
|
# @return [String] A shared key used in the event's content encryption and decryption.
|
110
190
|
#
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Raised when the human readable part of a Bech32 string is invalid
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
class InvalidHRPError < KeyValidationError
|
9
|
+
# Initializes the error
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# InvalidHRPError.new('example wrong hrp', 'nsec')
|
13
|
+
#
|
14
|
+
# @param given_hrp [String] The given human readable part of the Bech32 string
|
15
|
+
# @param allowed_hrp [String] The allowed human readable part of the Bech32 string
|
16
|
+
#
|
17
|
+
def initialize(given_hrp, allowed_hrp)
|
18
|
+
super("Invalid hrp: #{given_hrp}. The allowed hrp value for this kind of entity is '#{allowed_hrp}'.")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Raised when the private key is in an invalid format
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
class InvalidKeyFormatError < KeyValidationError
|
9
|
+
# Initializes the error
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# InvalidKeyFormatError.new('private')
|
13
|
+
#
|
14
|
+
# @param [String] key_kind The kind of key that is invalid (public or private)
|
15
|
+
#
|
16
|
+
def initialize(key_kind)
|
17
|
+
super("Only lowercase hexadecimal characters are allowed in #{key_kind} keys.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Raised when the private key's length is not 64 characters
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
class InvalidKeyLengthError < KeyValidationError
|
9
|
+
# Initializes the error
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# InvalidKeyLengthError.new('private')
|
13
|
+
#
|
14
|
+
# @param [String] key_kind The kind of key that is invalid (public or private)
|
15
|
+
#
|
16
|
+
def initialize(key_kind)
|
17
|
+
super("Invalid #{key_kind} key length. It should have 64 characters.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Raised when the private key is not a string
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
class InvalidKeyTypeError < KeyValidationError
|
9
|
+
# Initializes the error
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# InvalidKeyTypeError.new('private')
|
13
|
+
#
|
14
|
+
# @param [String] key_kind The kind of key that is invalid (public or private)
|
15
|
+
#
|
16
|
+
def initialize(key_kind) = super("Invalid #{key_kind} key type")
|
17
|
+
end
|
18
|
+
end
|