nostr 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.adr-dir +1 -0
  3. data/.editorconfig +1 -1
  4. data/.rubocop.yml +24 -1
  5. data/.tool-versions +2 -1
  6. data/CHANGELOG.md +70 -1
  7. data/README.md +93 -228
  8. data/adr/0001-record-architecture-decisions.md +19 -0
  9. data/adr/0002-introduction-of-signature-class.md +27 -0
  10. data/docs/.gitignore +4 -0
  11. data/docs/.vitepress/config.mjs +114 -0
  12. data/docs/README.md +44 -0
  13. data/docs/api-examples.md +49 -0
  14. data/docs/bun.lockb +0 -0
  15. data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
  16. data/docs/common-use-cases/signing-and-verifying-events.md +50 -0
  17. data/docs/common-use-cases/signing-and-verifying-messages.md +43 -0
  18. data/docs/core/client.md +108 -0
  19. data/docs/core/keys.md +136 -0
  20. data/docs/core/user.md +43 -0
  21. data/docs/events/contact-list.md +29 -0
  22. data/docs/events/encrypted-direct-message.md +28 -0
  23. data/docs/events/recommend-server.md +32 -0
  24. data/docs/events/set-metadata.md +20 -0
  25. data/docs/events/text-note.md +15 -0
  26. data/docs/events.md +11 -0
  27. data/docs/getting-started/installation.md +21 -0
  28. data/docs/getting-started/overview.md +171 -0
  29. data/docs/implemented-nips.md +9 -0
  30. data/docs/index.md +42 -0
  31. data/docs/markdown-examples.md +85 -0
  32. data/docs/package.json +12 -0
  33. data/docs/relays/connecting-to-a-relay.md +21 -0
  34. data/docs/relays/publishing-events.md +29 -0
  35. data/docs/relays/receiving-events.md +6 -0
  36. data/docs/subscriptions/creating-a-subscription.md +49 -0
  37. data/docs/subscriptions/deleting-a-subscription.md +10 -0
  38. data/docs/subscriptions/filtering-subscription-events.md +115 -0
  39. data/docs/subscriptions/updating-a-subscription.md +4 -0
  40. data/lib/nostr/bech32.rb +203 -0
  41. data/lib/nostr/client.rb +2 -1
  42. data/lib/nostr/crypto.rb +93 -13
  43. data/lib/nostr/errors/error.rb +7 -0
  44. data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
  45. data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
  46. data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
  47. data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
  48. data/lib/nostr/errors/invalid_signature_format_error.rb +18 -0
  49. data/lib/nostr/errors/invalid_signature_length_error.rb +18 -0
  50. data/lib/nostr/errors/invalid_signature_type_error.rb +16 -0
  51. data/lib/nostr/errors/key_validation_error.rb +6 -0
  52. data/lib/nostr/errors/signature_validation_error.rb +6 -0
  53. data/lib/nostr/errors.rb +12 -0
  54. data/lib/nostr/event.rb +40 -13
  55. data/lib/nostr/event_kind.rb +1 -0
  56. data/lib/nostr/events/encrypted_direct_message.rb +8 -7
  57. data/lib/nostr/filter.rb +14 -11
  58. data/lib/nostr/key.rb +100 -0
  59. data/lib/nostr/key_pair.rb +54 -6
  60. data/lib/nostr/keygen.rb +44 -5
  61. data/lib/nostr/private_key.rb +36 -0
  62. data/lib/nostr/public_key.rb +36 -0
  63. data/lib/nostr/relay_message_type.rb +18 -0
  64. data/lib/nostr/signature.rb +67 -0
  65. data/lib/nostr/subscription.rb +2 -2
  66. data/lib/nostr/user.rb +17 -8
  67. data/lib/nostr/version.rb +1 -1
  68. data/lib/nostr.rb +7 -0
  69. data/nostr.gemspec +13 -13
  70. data/sig/nostr/bech32.rbs +14 -0
  71. data/sig/nostr/client.rbs +5 -5
  72. data/sig/nostr/crypto.rbs +8 -5
  73. data/sig/nostr/errors/error.rbs +4 -0
  74. data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
  75. data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
  76. data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
  77. data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
  78. data/sig/nostr/errors/invalid_signature_format_error.rbs +5 -0
  79. data/sig/nostr/errors/invalid_signature_length_error.rbs +5 -0
  80. data/sig/nostr/errors/invalid_signature_type_error.rbs +5 -0
  81. data/sig/nostr/errors/key_validation_error.rbs +4 -0
  82. data/sig/nostr/errors/signature_validation_error.rbs +4 -0
  83. data/sig/nostr/event.rbs +11 -10
  84. data/sig/nostr/events/encrypted_direct_message.rbs +2 -2
  85. data/sig/nostr/filter.rbs +3 -12
  86. data/sig/nostr/key.rbs +16 -0
  87. data/sig/nostr/key_pair.rbs +8 -3
  88. data/sig/nostr/keygen.rbs +5 -2
  89. data/sig/nostr/private_key.rbs +4 -0
  90. data/sig/nostr/public_key.rbs +4 -0
  91. data/sig/nostr/relay_message_type.rbs +8 -0
  92. data/sig/nostr/signature.rbs +14 -0
  93. data/sig/nostr/user.rbs +4 -8
  94. data/sig/vendor/bech32/nostr/entity.rbs +41 -0
  95. data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
  96. data/sig/vendor/bech32/segwit_addr.rbs +21 -0
  97. data/sig/vendor/bech32.rbs +25 -0
  98. data/sig/vendor/event_emitter.rbs +10 -3
  99. data/sig/vendor/event_machine/channel.rbs +1 -1
  100. data/sig/vendor/faye/websocket/api.rbs +45 -0
  101. data/sig/vendor/faye/websocket/client.rbs +43 -0
  102. data/sig/vendor/faye/websocket.rbs +30 -0
  103. data/sig/vendor/schnorr/signature.rbs +16 -0
  104. data/sig/vendor/schnorr.rbs +3 -1
  105. 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
+ ```
@@ -0,0 +1,4 @@
1
+ # Updating a subscription
2
+
3
+ Updating a subscription is done by creating a new subscription with the same id as the previous one. See
4
+ [creating a subscription](./creating-a-subscription.md) for more information.
@@ -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. A random string.
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 on a +Nostr::Event+.
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 [String] 32-bytes hex-encoded private key of the creator.
34
- # @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient.
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 [String] 32-bytes hex-encoded public key of the message creator.
58
- # @param recipient_private_key [String] 32-bytes hex-encoded public key of the recipient.
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 [String] 32-bytes hex-encoded 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 = event_signature
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 [String] 32-bytes hex-encoded private key.
107
- # @param public_key [String] 32-bytes hex-encoded 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Base error class
5
+ class Error < StandardError
6
+ end
7
+ end
@@ -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