nostr 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # A filter determines what events will be sent in a subscription.
5
+ class Filter
6
+ # A list of event ids or prefixes
7
+ #
8
+ # @api public
9
+ #
10
+ # @example
11
+ # filter.ids # => ['c24881c305c5cfb7c1168be7e9b0e150', '35deb2612efdb9e13e8b0ca4fc162341']
12
+ #
13
+ # @return [Array<String>, nil]
14
+ #
15
+ attr_reader :ids
16
+
17
+ # A list of pubkeys or prefixes, the pubkey of an event must be one of these
18
+ #
19
+ # @api public
20
+ #
21
+ # @example
22
+ # filter.authors # => ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7']
23
+ #
24
+ # @return [Array<String>, nil]
25
+ #
26
+ attr_reader :authors
27
+
28
+ # A list of a kind numbers
29
+ #
30
+ # @api public
31
+ #
32
+ # @example
33
+ # filter.kinds # => [0, 1, 2]
34
+ #
35
+ # @return [Array<Integer>, nil]
36
+ #
37
+ attr_reader :kinds
38
+
39
+ # A list of event ids that are referenced in an "e" tag
40
+ #
41
+ # @api public
42
+ #
43
+ # @example
44
+ # filter.e # => ['7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2']
45
+ #
46
+ # @return [Array<String>, nil]
47
+ #
48
+ attr_reader :e
49
+
50
+ # A list of pubkeys that are referenced in a "p" tag
51
+ #
52
+ # @api public
53
+ #
54
+ # @example
55
+ # filter.p # => ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7']
56
+ #
57
+ # @return [Array<String>, nil]
58
+ #
59
+ attr_reader :p
60
+
61
+ # A timestamp, events must be newer than this to pass
62
+ #
63
+ # @api public
64
+ #
65
+ # @example
66
+ # filter.since # => 1230981305
67
+ #
68
+ # @return [Integer, nil]
69
+ #
70
+ attr_reader :since
71
+
72
+ # A timestamp, events must be older than this to pass
73
+ #
74
+ # @api public
75
+ #
76
+ # @example
77
+ # filter.until # => 1292190341
78
+ #
79
+ # @return [Integer, nil]
80
+ #
81
+ attr_reader :until
82
+
83
+ # Maximum number of events to be returned in the initial query
84
+ #
85
+ # @api public
86
+ #
87
+ # @example
88
+ # filter.limit # => 420
89
+ #
90
+ # @return [Integer, nil]
91
+ #
92
+ attr_reader :limit
93
+
94
+ # Instantiates a new Filter
95
+ #
96
+ # @api public
97
+ #
98
+ # @example
99
+ # Nostr::Filter.new(
100
+ # ids: ['c24881c305c5cfb7c1168be7e9b0e150'],
101
+ # authors: ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7'],
102
+ # kinds: [0, 1, 2],
103
+ # since: 1230981305,
104
+ # until: 1292190341,
105
+ # e: ['7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2'],
106
+ # p: ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7']
107
+ # )
108
+ #
109
+ # @param kwargs [Hash]
110
+ # @option kwargs [Array<String>, nil] ids A list of event ids or prefixes
111
+ # @option kwargs [Array<String>, nil] authors A list of pubkeys or prefixes, the pubkey of an event must be one
112
+ # of these
113
+ # @option kwargs [Array<Integer>, nil] kinds A list of a kind numbers
114
+ # @option kwargs [Array<String>, nil] e A list of event ids that are referenced in an "e" tag
115
+ # @option kwargs [Array<String, nil>] p A list of pubkeys that are referenced in a "p" tag
116
+ # @option kwargs [Integer, nil] since A timestamp, events must be newer than this to pass
117
+ # @option kwargs [Integer, nil] until A timestamp, events must be older than this to pass
118
+ # @option kwargs [Integer, nil] limit Maximum number of events to be returned in the initial query
119
+ #
120
+ def initialize(**kwargs)
121
+ @ids = kwargs[:ids]
122
+ @authors = kwargs[:authors]
123
+ @kinds = kwargs[:kinds]
124
+ @e = kwargs[:e]
125
+ @p = kwargs[:p]
126
+ @since = kwargs[:since]
127
+ @until = kwargs[:until]
128
+ @limit = kwargs[:limit]
129
+ end
130
+
131
+ # Converts the filter to a hash, removing all empty attributes
132
+ #
133
+ # @api public
134
+ #
135
+ # @example
136
+ # filter.to_h # => {:ids=>["c24881c305c5cfb7c1168be7e9b0e150"],
137
+ # :authors=>["000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7"],
138
+ # :kinds=>[0, 1, 2],
139
+ # :"#e"=>["7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2"],
140
+ # :"#p"=>["000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7"],
141
+ # :since=>1230981305,
142
+ # :until=>1292190341}
143
+ #
144
+ # @return [Hash] The filter as a hash.
145
+ #
146
+ def to_h
147
+ {
148
+ ids:,
149
+ authors:,
150
+ kinds:,
151
+ '#e': e,
152
+ '#p': p,
153
+ since:,
154
+ until: self.until,
155
+ limit:
156
+ }.compact
157
+ end
158
+
159
+ # Compares two filters. Returns true if all attributes are equal and false otherwise
160
+ #
161
+ # @api public
162
+ #
163
+ # @example
164
+ # filter == filter # => true
165
+ #
166
+ # @return [Boolean] True if all attributes are equal and false otherwise
167
+ #
168
+ def ==(other)
169
+ to_h == other.to_h
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # A pair of public and private keys
5
+ class KeyPair
6
+ # 32-bytes hex-encoded private key
7
+ #
8
+ # @api public
9
+ #
10
+ # @example
11
+ # keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
12
+ #
13
+ # @return [String]
14
+ #
15
+ attr_reader :private_key
16
+
17
+ # 32-bytes hex-encoded public key
18
+ #
19
+ # @api public
20
+ #
21
+ # @example
22
+ # keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
23
+ #
24
+ # @return [String]
25
+ #
26
+ attr_reader :public_key
27
+
28
+ # Instantiates a key pair
29
+ #
30
+ # @api public
31
+ #
32
+ # @example
33
+ # keypair = Nostr::KeyPair.new(
34
+ # private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
35
+ # public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
36
+ # )
37
+ #
38
+ # @param private_key [String] 32-bytes hex-encoded private key.
39
+ # @param public_key [String] 32-bytes hex-encoded public key.
40
+ #
41
+ def initialize(private_key:, public_key:)
42
+ @private_key = private_key
43
+ @public_key = public_key
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ecdsa'
4
+ require 'securerandom'
5
+
6
+ module Nostr
7
+ # Generates private keys, public keys and key pairs.
8
+ class Keygen
9
+ # Instantiates a new keygen
10
+ #
11
+ # @api public
12
+ #
13
+ # @example
14
+ # keygen = Nostr::Keygen.new
15
+ #
16
+ def initialize
17
+ @group = ECDSA::Group::Secp256k1
18
+ end
19
+
20
+ # Generates a pair of private and public keys
21
+ #
22
+ # @api public
23
+ #
24
+ # @example
25
+ # keypair = keygen.generate_keypair
26
+ # keypair # #<Nostr::KeyPair:0x0000000107bd3550
27
+ # @private_key="893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900",
28
+ # @public_key="2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558">
29
+ #
30
+ # @return [KeyPair] An object containing a private key and a public key.
31
+ #
32
+ def generate_key_pair
33
+ private_key = generate_private_key
34
+ public_key = extract_public_key(private_key)
35
+
36
+ KeyPair.new(private_key:, public_key:)
37
+ end
38
+
39
+ # Generates a private key
40
+ #
41
+ # @api public
42
+ #
43
+ # @example
44
+ # private_key = keygen.generate_private_key
45
+ # private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
46
+ #
47
+ # @return [String] A 32-bytes hex-encoded private key.
48
+ #
49
+ def generate_private_key
50
+ (SecureRandom.random_number(group.order - 1) + 1).to_s(16)
51
+ end
52
+
53
+ # Extracts a public key from a private key
54
+ #
55
+ # @api public
56
+ #
57
+ # @example
58
+ # private_key = keygen.generate_private_key
59
+ # public_key = keygen.extract_public_key(private_key)
60
+ # public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
61
+ #
62
+ # @return [String] A 32-bytes hex-encoded public key.
63
+ #
64
+ def extract_public_key(private_key)
65
+ group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16)
66
+ end
67
+
68
+ private
69
+
70
+ # The elliptic curve group. Used to generate public and private keys
71
+ #
72
+ # @api private
73
+ #
74
+ # @return [ECDSA::Group]
75
+ #
76
+ attr_reader :group
77
+ end
78
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Relays expose a websocket endpoint to which clients can connect.
5
+ class Relay
6
+ # The websocket URL of the relay
7
+ #
8
+ # @api public
9
+ #
10
+ # @example
11
+ # relay.url # => 'wss://relay.damus.io'
12
+ #
13
+ # @return [String]
14
+ #
15
+ attr_reader :url
16
+
17
+ # The name of the relay
18
+ #
19
+ # @api public
20
+ #
21
+ # @example
22
+ # relay.name # => 'Damus'
23
+ #
24
+ # @return [String]
25
+ #
26
+ attr_reader :name
27
+
28
+ # Instantiates a new Relay
29
+ #
30
+ # @api public
31
+ #
32
+ # @example
33
+ # relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
34
+ #
35
+ # @return [String] The websocket URL of the relay
36
+ # @return [String] The name of the relay
37
+ #
38
+ def initialize(url:, name:)
39
+ @url = url
40
+ @name = name
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Nostr
6
+ # A subscription the result of a request to receive events from a relay
7
+ class Subscription
8
+ # A random string that should be used to represent a subscription
9
+ #
10
+ # @api public
11
+ #
12
+ # @example
13
+ # subscription.id # => 'c24881c305c5cfb7c1168be7e9b0e150'
14
+ #
15
+ # @return [String]
16
+ #
17
+ attr_reader :id
18
+
19
+ # An object that determines what events will be sent in the subscription
20
+ #
21
+ # @api public
22
+ #
23
+ # @example
24
+ # subscription.filter # => #<Nostr::Subscription:0x0000000110eea460 @filter=#<Nostr::Filter:0x0000000110c24de8>,
25
+ # @id="0dd7f3fa06cd5f797438dd0b7477f3c7">
26
+ #
27
+ # @return [Filter]
28
+ #
29
+ attr_reader :filter
30
+
31
+ # Initializes a subscription
32
+ #
33
+ # @api public
34
+ #
35
+ # @example Creating a subscription with no id and no filters
36
+ # subscription = Nostr::Subscription.new
37
+ #
38
+ # @example Creating a subscription with an ID
39
+ # subscription = Nostr::Subscription.new(id: 'c24881c305c5cfb7c1168be7e9b0e150')
40
+ #
41
+ # @example Subscribing to all events created after a certain time
42
+ # subscription = Nostr::Subscription.new(filter: Nostr::Filter.new(since: 1230981305))
43
+ #
44
+ # @param id [String] A random string that should be used to represent a subscription
45
+ # @param filter [Filter] An object that determines what events will be sent in that subscription
46
+ #
47
+ def initialize(filter:, id: SecureRandom.hex)
48
+ @id = id
49
+ @filter = filter
50
+ end
51
+
52
+ # Compares two subscriptions. Returns true if all attributes are equal and false otherwise
53
+ #
54
+ # @api public
55
+ #
56
+ # @example
57
+ # subscription1 == subscription1 # => true
58
+ #
59
+ # @return [Boolean] True if all attributes are equal and false otherwise
60
+ #
61
+ def ==(other)
62
+ id == other.id && filter == other.filter
63
+ end
64
+ end
65
+ end
data/lib/nostr/user.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schnorr'
4
+ require 'json'
5
+
6
+ module Nostr
7
+ # Each user has a keypair. Signatures, public key, and encodings are done according to the
8
+ # Schnorr signatures standard for the curve secp256k1.
9
+ class User
10
+ # A pair of private and public keys
11
+ #
12
+ # @api public
13
+ #
14
+ # @example
15
+ # user.keypair # #<Nostr::KeyPair:0x0000000107bd3550
16
+ # @private_key="893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900",
17
+ # @public_key="2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558">
18
+ #
19
+ # @return [KeyPair]
20
+ #
21
+ attr_reader :keypair
22
+
23
+ # Instantiates a user
24
+ #
25
+ # @api public
26
+ #
27
+ # @example Creating a user with no keypair
28
+ # user = Nostr::User.new
29
+ #
30
+ # @example Creating a user with a keypair
31
+ # user = Nostr::User.new(keypair: keypair)
32
+ #
33
+ # @param keypair [Keypair] A pair of private and public keys
34
+ # @param keygen [Keygen] A private key and public key generator
35
+ #
36
+ def initialize(keypair: nil, keygen: Keygen.new)
37
+ @keypair = keypair || keygen.generate_key_pair
38
+ end
39
+
40
+ # Builds an Event
41
+ #
42
+ # @api public
43
+ #
44
+ # @example Creating a note event
45
+ # event = user.create_event(
46
+ # kind: Nostr::EventKind::TEXT_NOTE,
47
+ # content: 'Your feedback is appreciated, now pay $8'
48
+ # )
49
+ #
50
+ # @param event_attributes [Hash]
51
+ # @option event_attributes [String] :pubkey 32-bytes hex-encoded public key of the event creator.
52
+ # @option event_attributes [Integer] :created_at Date of the creation of the vent. A UNIX timestamp, in seconds.
53
+ # @option event_attributes [Integer] :kind The kind of the event. An integer from 0 to 2.
54
+ # @option event_attributes [Array<Array>] :tags An array of tags. Each tag is an array of strings.
55
+ # @option event_attributes [String] :content Arbitrary string.
56
+ #
57
+ # @return [Event]
58
+ #
59
+ def create_event(event_attributes)
60
+ event_fragment = EventFragment.new(**event_attributes.merge(pubkey: keypair.public_key))
61
+ event_sha256 = Digest::SHA256.hexdigest(JSON.dump(event_fragment.serialize))
62
+
63
+ signature = sign(event_sha256)
64
+
65
+ Event.new(
66
+ id: event_sha256,
67
+ pubkey: event_fragment.pubkey,
68
+ created_at: event_fragment.created_at,
69
+ kind: event_fragment.kind,
70
+ tags: event_fragment.tags,
71
+ content: event_fragment.content,
72
+ sig: signature
73
+ )
74
+ end
75
+
76
+ private
77
+
78
+ # Signs an event with the user's private key
79
+ #
80
+ # @api private
81
+ #
82
+ # @param event_sha256 [String] The SHA256 hash of the event.
83
+ #
84
+ # @return [String] The signature of the event.
85
+ #
86
+ def sign(event_sha256)
87
+ hex_private_key = Array(keypair.private_key).pack('H*')
88
+ hex_message = Array(event_sha256).pack('H*')
89
+ Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
90
+ end
91
+ end
92
+ end
data/lib/nostr/version.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nostr
4
- VERSION = '0.1.0'
4
+ # The version of the gem
5
+ VERSION = '0.2.0'
5
6
  end
data/lib/nostr.rb CHANGED
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'nostr/version'
4
+ require_relative 'nostr/keygen'
5
+ require_relative 'nostr/client_message_type'
6
+ require_relative 'nostr/filter'
7
+ require_relative 'nostr/subscription'
8
+ require_relative 'nostr/relay'
9
+ require_relative 'nostr/key_pair'
10
+ require_relative 'nostr/event_kind'
11
+ require_relative 'nostr/event_fragment'
12
+ require_relative 'nostr/event'
13
+ require_relative 'nostr/client'
14
+ require_relative 'nostr/user'
4
15
 
5
16
  # Encapsulates all the gem's logic
6
17
  module Nostr
7
- class Error < StandardError; end
8
- # Your code goes here...
9
18
  end
data/nostr.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
 
18
18
  spec.metadata['homepage_uri'] = spec.homepage
19
19
  spec.metadata['source_code_uri'] = 'https://github.com/wilsonsilva/nostr'
20
- spec.metadata['changelog_uri'] = 'https://github.com/wilsonsilva/nostr/blob/master/CHANGELOG.md'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/wilsonsilva/nostr/blob/main/CHANGELOG.md'
21
21
 
22
22
  # Specify which files should be added to the gem when it is released.
23
23
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -31,7 +31,15 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ['lib']
33
33
 
34
+ spec.add_dependency 'bech32', '~> 1.3'
35
+ spec.add_dependency 'bip-schnorr', '~> 0.4'
36
+ spec.add_dependency 'ecdsa', '~> 1.2'
37
+ spec.add_dependency 'event_emitter', '~> 0.2'
38
+ spec.add_dependency 'faye-websocket', '~> 0.11'
39
+ spec.add_dependency 'json', '~> 2.6'
40
+
34
41
  spec.add_development_dependency 'bundler-audit', '~> 0.9'
42
+ spec.add_development_dependency 'dotenv', '~> 2.8'
35
43
  spec.add_development_dependency 'guard', '~> 2.18'
36
44
  spec.add_development_dependency 'guard-bundler', '~> 3.0'
37
45
  spec.add_development_dependency 'guard-bundler-audit', '~> 0.1'
@@ -39,12 +47,14 @@ Gem::Specification.new do |spec|
39
47
  spec.add_development_dependency 'guard-rubocop', '~> 1.5'
40
48
  spec.add_development_dependency 'overcommit', '~> 0.59'
41
49
  spec.add_development_dependency 'pry', '~> 0.14'
50
+ spec.add_development_dependency 'puma', '~> 5.6'
51
+ spec.add_development_dependency 'rack', '~> 3.0'
42
52
  spec.add_development_dependency 'rake', '~> 13.0'
43
53
  spec.add_development_dependency 'rspec', '~> 3.12'
44
54
  spec.add_development_dependency 'rubocop', '~> 1.42'
45
55
  spec.add_development_dependency 'rubocop-rake', '~> 0.6'
46
56
  spec.add_development_dependency 'rubocop-rspec', '2.16'
47
- spec.add_development_dependency 'simplecov', '~> 0.22'
57
+ spec.add_development_dependency 'simplecov', '= 0.17'
48
58
  spec.add_development_dependency 'simplecov-console', '~> 0.9'
49
59
  spec.add_development_dependency 'yard', '~> 0.9'
50
60
  spec.add_development_dependency 'yard-junk', '~> 0.0.9'