peatio-jruby 2.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.drone.yml +29 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +148 -0
  6. data/.simplecov +17 -0
  7. data/.tool-versions +1 -0
  8. data/.travis.yml +18 -0
  9. data/Gemfile +8 -0
  10. data/Gemfile.lock +198 -0
  11. data/README.md +47 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/peatio +12 -0
  15. data/bin/setup +8 -0
  16. data/lib/peatio.rb +52 -0
  17. data/lib/peatio/adapter_registry.rb +25 -0
  18. data/lib/peatio/auth/error.rb +18 -0
  19. data/lib/peatio/auth/jwt_authenticator.rb +127 -0
  20. data/lib/peatio/block.rb +29 -0
  21. data/lib/peatio/blockchain/abstract.rb +161 -0
  22. data/lib/peatio/blockchain/error.rb +37 -0
  23. data/lib/peatio/blockchain/registry.rb +16 -0
  24. data/lib/peatio/command/base.rb +11 -0
  25. data/lib/peatio/command/db.rb +20 -0
  26. data/lib/peatio/command/inject.rb +13 -0
  27. data/lib/peatio/command/root.rb +14 -0
  28. data/lib/peatio/command/security.rb +29 -0
  29. data/lib/peatio/command/service.rb +40 -0
  30. data/lib/peatio/error.rb +18 -0
  31. data/lib/peatio/executor.rb +64 -0
  32. data/lib/peatio/injectors/peatio_events.rb +240 -0
  33. data/lib/peatio/logger.rb +39 -0
  34. data/lib/peatio/metrics/server.rb +15 -0
  35. data/lib/peatio/mq/client.rb +51 -0
  36. data/lib/peatio/ranger/connection.rb +117 -0
  37. data/lib/peatio/ranger/events.rb +11 -0
  38. data/lib/peatio/ranger/router.rb +234 -0
  39. data/lib/peatio/ranger/web_socket.rb +68 -0
  40. data/lib/peatio/security/key_generator.rb +26 -0
  41. data/lib/peatio/sql/client.rb +19 -0
  42. data/lib/peatio/sql/schema.rb +72 -0
  43. data/lib/peatio/transaction.rb +122 -0
  44. data/lib/peatio/upstream/base.rb +116 -0
  45. data/lib/peatio/upstream/registry.rb +14 -0
  46. data/lib/peatio/version.rb +3 -0
  47. data/lib/peatio/wallet/abstract.rb +189 -0
  48. data/lib/peatio/wallet/error.rb +37 -0
  49. data/lib/peatio/wallet/registry.rb +16 -0
  50. data/peatio.gemspec +59 -0
  51. metadata +480 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio
4
+ class Logger
5
+ class << self
6
+ def logger
7
+ @logger ||= ::Logger.new(STDERR, level: level)
8
+ end
9
+
10
+ def level
11
+ (ENV["LOG_LEVEL"] || "info").downcase.to_sym
12
+ end
13
+
14
+ def debug(progname=nil, &block)
15
+ logger.debug(progname, &block)
16
+ end
17
+
18
+ def info(progname=nil, &block)
19
+ logger.info(progname, &block)
20
+ end
21
+
22
+ def warn(progname=nil, &block)
23
+ logger.warn(progname, &block)
24
+ end
25
+
26
+ def error(progname=nil, &block)
27
+ logger.error(progname, &block)
28
+ end
29
+
30
+ def fatal(progname=nil, &block)
31
+ logger.fatal(progname, &block)
32
+ end
33
+
34
+ def unknown(progname=nil, &block)
35
+ logger.unknown(progname, &block)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::Metrics
4
+ class Server
5
+ def self.app(registry)
6
+ Rack::Builder.new do |builder|
7
+ builder.use Rack::CommonLogger
8
+ builder.use Rack::ShowExceptions
9
+ builder.use Rack::Deflater
10
+ builder.use Prometheus::Middleware::Exporter, registry: registry
11
+ builder.run ->(_) { [404, {"Content-Type" => "text/html"}, ["Not found\n"]] }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::MQ
4
+ class Client
5
+ class << self
6
+ attr_accessor :connection
7
+
8
+ def connect!
9
+ options = {
10
+ host: ENV["RABBITMQ_HOST"] || "0.0.0.0",
11
+ port: ENV["RABBITMQ_PORT"] || "5672",
12
+ username: ENV["RABBITMQ_USER"],
13
+ password: ENV["RABBITMQ_PASSWORD"],
14
+ }
15
+ @connection = Bunny.new(options)
16
+ @connection.start
17
+ end
18
+
19
+ def disconnect
20
+ @connection.close
21
+ end
22
+ end
23
+
24
+ def initialize
25
+ Client.connect! unless Peatio::MQ::Client.connection
26
+ @channel = Client.connection.create_channel
27
+ @exchanges = {}
28
+ end
29
+
30
+ def exchange(name, type="topic")
31
+ @exchanges[name] ||= @channel.exchange(name, type: type)
32
+ end
33
+
34
+ def publish(ex_name, type, id, event, payload)
35
+ routing_key = [type, id, event].join(".")
36
+ serialized_data = JSON.dump(payload)
37
+ exchange(ex_name).publish(serialized_data, routing_key: routing_key)
38
+ Peatio::Logger.debug { "published event to #{routing_key} " }
39
+ end
40
+
41
+ def subscribe(ex_name, &callback)
42
+ suffix = "#{Socket.gethostname.split(/-/).last}#{Random.rand(10_000)}"
43
+ queue_name = "ranger.#{suffix}"
44
+
45
+ @channel
46
+ .queue(queue_name, durable: false, auto_delete: true)
47
+ .bind(exchange(ex_name), routing_key: "#").subscribe(&callback)
48
+ Peatio::Logger.info "Subscribed to exchange #{ex_name}"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,117 @@
1
+ module Peatio::Ranger
2
+ class Connection
3
+ attr_reader :socket, :user, :authorized, :streams, :logger, :id
4
+
5
+ def initialize(router, socket, logger)
6
+ @id = SecureRandom.hex(10)
7
+ @router = router
8
+ @socket = socket
9
+ @logger = logger
10
+ @user = nil
11
+ @authorized = false
12
+ @streams = {}
13
+ end
14
+
15
+ def inspect
16
+ if authorized
17
+ "<Connection id=#{id} user=#{user}>"
18
+ else
19
+ "<Connection id=#{id}>"
20
+ end
21
+ end
22
+
23
+ def send_raw(payload)
24
+ if user
25
+ logger.debug { "sending to user #{user.inspect} payload: #{payload}" }
26
+ else
27
+ logger.debug { "sending to anonymous payload: #{payload}" }
28
+ end
29
+ @socket.send(payload)
30
+ end
31
+
32
+ def send(method, data)
33
+ payload = JSON.dump(method => data)
34
+ send_raw(payload)
35
+ end
36
+
37
+ def authenticate(authenticator, jwt)
38
+ payload = {}
39
+ authorized = false
40
+ begin
41
+ payload = authenticator.authenticate!(jwt)
42
+ authorized = true
43
+ rescue Peatio::Auth::Error => e
44
+ logger.warn e.message
45
+ end
46
+ [authorized, payload]
47
+ end
48
+
49
+ def subscribe(subscribed_streams)
50
+ raise "Streams must be an array of strings" unless subscribed_streams.is_a?(Array)
51
+
52
+ subscribed_streams.each do |stream|
53
+ stream = stream.to_s
54
+ next if stream.empty?
55
+
56
+ unless @streams[stream]
57
+ @streams[stream] = true
58
+ @router.on_subscribe(self, stream)
59
+ end
60
+ end
61
+ send(:success, message: "subscribed", streams: streams.keys)
62
+ end
63
+
64
+ def unsubscribe(unsubscribed_streams)
65
+ raise "Streams must be an array of strings" unless unsubscribed_streams.is_a?(Array)
66
+
67
+ unsubscribed_streams.each do |stream|
68
+ stream = stream.to_s
69
+ next if stream.empty?
70
+
71
+ if @streams[stream]
72
+ @streams.delete(stream)
73
+ @router.on_unsubscribe(self, stream)
74
+ end
75
+ end
76
+ send(:success, message: "unsubscribed", streams: streams.keys)
77
+ end
78
+
79
+ def handle(msg)
80
+ return if msg.to_s.empty?
81
+
82
+ if msg =~ /^ping/
83
+ send_raw("pong")
84
+ return
85
+ end
86
+
87
+ data = JSON.parse(msg)
88
+ case data["event"]
89
+ when "subscribe"
90
+ subscribe(data["streams"])
91
+ when "unsubscribe"
92
+ unsubscribe(data["streams"])
93
+ end
94
+ rescue JSON::ParserError => e
95
+ logger.debug { "#{e}, msg: `#{msg}`" }
96
+ end
97
+
98
+ def handshake(authenticator, hs)
99
+ query = URI.decode_www_form(hs.query_string)
100
+ subscribe(query.map {|item| item.last if item.first == "stream" })
101
+ logger.debug "WebSocket connection openned"
102
+ headers = hs.headers_downcased
103
+ return unless headers.key?("authorization")
104
+
105
+ authorized, payload = authenticate(authenticator, headers["authorization"])
106
+
107
+ if !authorized
108
+ logger.debug "Authentication failed for UID:#{payload[:uid]}"
109
+ raise EM::WebSocket::HandshakeError, "Authorization failed"
110
+ else
111
+ @user = payload[:uid]
112
+ @authorized = true
113
+ logger.debug "User #{@user} authenticated #{@streams}"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::Ranger
4
+ class Events
5
+ def self.publish(type, id, event, payload, opts={})
6
+ ex_name = opts[:ex_name] || "peatio.events.ranger"
7
+ @client ||= Peatio::MQ::Client.new
8
+ @client.publish(ex_name, type, id, event, payload)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peatio::Ranger
4
+ class Router
5
+ attr_reader :connections
6
+ attr_reader :connections_by_userid
7
+ attr_reader :streams_sockets
8
+ attr_reader :logger
9
+
10
+ class ConnectionArray < Array
11
+ def delete(connection)
12
+ delete_if do |c|
13
+ c.id == connection.id
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(prometheus=nil)
19
+ @connections = {}
20
+ @connections_by_userid = {}
21
+ @streams_sockets = {}
22
+ @logger = Peatio::Logger.logger
23
+ @stores = {}
24
+ init_metrics(prometheus)
25
+ end
26
+
27
+ def init_metrics(prometheus)
28
+ return unless prometheus
29
+
30
+ @prometheus = prometheus
31
+ @metric_connections_total = @prometheus.counter(
32
+ :ranger_connections_total,
33
+ docstring: "Total number of connections to ranger from the start",
34
+ labels: [:auth]
35
+ )
36
+ @metric_connections_current = @prometheus.gauge(
37
+ :ranger_connections_current,
38
+ docstring: "Current number of connections to ranger",
39
+ labels: [:auth]
40
+ )
41
+ @metric_subscriptions_current = @prometheus.gauge(
42
+ :ranger_subscriptions_current,
43
+ docstring: "Current number of streams subscriptions to ranger",
44
+ labels: [:stream]
45
+ )
46
+ end
47
+
48
+ def snapshot?(stream)
49
+ stream.end_with?("-snap")
50
+ end
51
+
52
+ def increment?(stream)
53
+ stream.end_with?("-inc")
54
+ end
55
+
56
+ def storekey(stream)
57
+ stream.gsub(/-(snap|inc)$/, "")
58
+ end
59
+
60
+ def stats
61
+ [
62
+ "==== Metrics ====",
63
+ "ranger_connections_total{auth=\"public\"}: %d" % [@metric_connections_total.get(labels: {auth: "public"})],
64
+ "ranger_connections_total{auth=\"private\"}: %d" % [@metric_connections_total.get(labels: {auth: "private"})],
65
+ "ranger_connections_current{auth=\"public\"}: %d" % [@metric_connections_current.get(labels: {auth: "public"})],
66
+ "ranger_connections_current{auth=\"private\"}: %d" % [@metric_connections_current.get(labels: {auth: "private"})],
67
+ "ranger_subscriptions_current: %d" % [compute_streams_subscriptions()],
68
+ "ranger_streams_kinds: %d" % [compute_streams_kinds()],
69
+ ].join("\n")
70
+ end
71
+
72
+ def debug
73
+ [
74
+ "==== Debug ====",
75
+ "connections: %s" % [@connections.inspect],
76
+ "connections_by_userid: %s" % [@connections_by_userid],
77
+ "streams_sockets: %s" % [@streams_sockets],
78
+ ].join("\n")
79
+ end
80
+
81
+ def compute_connections_all
82
+ @connections.size
83
+ end
84
+
85
+ def compute_connections_private
86
+ @connections_by_userid.each_value.map(&:size).reduce(0, :+)
87
+ end
88
+
89
+ def compute_stream_subscriptions(stream)
90
+ @streams_sockets[stream]&.size || 0
91
+ end
92
+
93
+ def compute_streams_subscriptions
94
+ @streams_sockets.each_value.map(&:size).reduce(0, :+)
95
+ end
96
+
97
+ def compute_streams_kinds
98
+ @streams_sockets.size
99
+ end
100
+
101
+ def sanity_check_metrics_connections
102
+ return unless @metric_connections_current
103
+
104
+ connections_current_all = @metric_connections_current.values.values.reduce(0, :+)
105
+ return if connections_current_all == compute_connections_all()
106
+
107
+ logger.warn "slip detected in metric_connections_current, recalculating"
108
+ connections_current_private = compute_connections_private()
109
+ @metric_connections_current.set(connections_current_private, labels: {auth: "private"})
110
+ @metric_connections_current.set(compute_connections_all() - connections_current_private, labels: {auth: "public"})
111
+ end
112
+
113
+ def on_connection_open(connection)
114
+ @connections[connection.id] = connection
115
+ unless connection.authorized
116
+ @metric_connections_current&.increment(labels: {auth: "public"})
117
+ @metric_connections_total&.increment(labels: {auth: "public"})
118
+ return
119
+ end
120
+ @metric_connections_current&.increment(labels: {auth: "private"})
121
+ @metric_connections_total&.increment(labels: {auth: "private"})
122
+
123
+ @connections_by_userid[connection.user] ||= ConnectionArray.new
124
+ @connections_by_userid[connection.user] << connection
125
+ end
126
+
127
+ def on_connection_close(connection)
128
+ @connections.delete(connection.id)
129
+ connection.streams.keys.each do |stream|
130
+ on_unsubscribe(connection, stream)
131
+ end
132
+
133
+ unless connection.authorized
134
+ @metric_connections_current&.decrement(labels: {auth: "public"})
135
+ sanity_check_metrics_connections
136
+ return
137
+ end
138
+ @metric_connections_current&.decrement(labels: {auth: "private"})
139
+
140
+ @connections_by_userid[connection.user].delete(connection)
141
+ @connections_by_userid.delete(connection.user) \
142
+ if @connections_by_userid[connection.user].empty?
143
+ sanity_check_metrics_connections
144
+ end
145
+
146
+ def on_subscribe(connection, stream)
147
+ @streams_sockets[stream] ||= ConnectionArray.new
148
+ @streams_sockets[stream] << connection
149
+ send_snapshot_and_increments(connection, storekey(stream)) if increment?(stream)
150
+ @metric_subscriptions_current&.set(compute_stream_subscriptions(stream), labels: {stream: stream})
151
+ end
152
+
153
+ def send_snapshot_and_increments(connection, key)
154
+ return unless @stores[key]
155
+ return unless @stores[key][:snapshot]
156
+
157
+ connection.send_raw(@stores[key][:snapshot])
158
+ @stores[key][:increments]&.each {|inc| connection.send_raw(inc) }
159
+ end
160
+
161
+ def on_unsubscribe(connection, stream)
162
+ return unless @streams_sockets[stream]
163
+
164
+ @streams_sockets[stream].delete(connection)
165
+ @streams_sockets.delete(stream) if @streams_sockets[stream].empty?
166
+ @metric_subscriptions_current&.set(compute_stream_subscriptions(stream), labels: {stream: stream})
167
+ end
168
+
169
+ def send_private_message(user_id, event, payload_decoded)
170
+ Array(@connections_by_userid[user_id]).each do |connection|
171
+ connection.send(event, payload_decoded) if connection.streams.include?(event)
172
+ end
173
+ end
174
+
175
+ def send_public_message(stream, raw_message)
176
+ Array(@streams_sockets[stream]).each do |connection|
177
+ connection.send_raw(raw_message)
178
+ end
179
+ end
180
+
181
+ #
182
+ # routing key format: type.id.event
183
+ # * `type` can be *public* or *private*
184
+ # * `id` can be user id or market id
185
+ # * `event` is the event identifier, ex: order_completed, trade, ...
186
+ #
187
+ def on_message(delivery_info, _metadata, payload)
188
+ routing_key = delivery_info.routing_key
189
+ if routing_key.count(".") != 2
190
+ logger.error { "invalid routing key from amqp: #{routing_key}" }
191
+ return
192
+ end
193
+
194
+ type, id, event = routing_key.split(".")
195
+ payload_decoded = JSON.parse(payload)
196
+
197
+ if type == "private"
198
+ send_private_message(id, event, payload_decoded)
199
+ return
200
+ end
201
+
202
+ stream = [id, event].join(".")
203
+ message = JSON.dump(stream => payload_decoded)
204
+
205
+ if snapshot?(event)
206
+ key = storekey(stream)
207
+
208
+ unless @stores[key]
209
+ # Send the snapshot to subscribers of -inc stream if there were no snapshot before
210
+ send_public_message("#{key}-inc", message)
211
+ end
212
+
213
+ @stores[key] = {
214
+ snapshot: message,
215
+ increments: [],
216
+ }
217
+ return
218
+ end
219
+
220
+ if increment?(event)
221
+ key = storekey(stream)
222
+
223
+ unless @stores[key]
224
+ logger.warn { "Discard increment received before snapshot for store:#{key}" }
225
+ return
226
+ end
227
+
228
+ @stores[key][:increments] << message
229
+ end
230
+
231
+ send_public_message(stream, message)
232
+ end
233
+ end
234
+ end