nostr 0.4.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|