deribit 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,206 @@
1
+ module Deribit
2
+ class API
3
+ attr_reader :request
4
+
5
+ def initialize(key, secret, test_server: nil)
6
+ @request = Request.new(key, secret, test_server: test_server)
7
+ end
8
+
9
+ def orderbook(instrument)
10
+ request.send(path: "/api/v1/public/getorderbook", params: {instrument: instrument})
11
+ end
12
+
13
+ def instruments(expired: false, only_active: true)
14
+ response = request.send(path: '/api/v1/public/getinstruments', params: {expired: expired})
15
+ if response.is_a?(Array) and only_active
16
+ response = response.select {|i| i[:isActive] == true}
17
+ end
18
+
19
+ response
20
+ end
21
+
22
+ def index
23
+ request.send(path: '/api/v1/public/index', params: {})
24
+ end
25
+
26
+ def test
27
+ request.send(path: '/api/v1/public/test', params: {})
28
+ end
29
+
30
+ def currencies
31
+ request.send(path: '/api/v1/public/getcurrencies', params: {})
32
+ end
33
+
34
+ def last_trades(instrument, count: nil, since: nil)
35
+ params = {instrument: instrument}
36
+ params[:count] = count if count
37
+ params[:since] = since if since
38
+
39
+ request.send(path: '/api/v1/public/getlasttrades', params: params)
40
+ end
41
+
42
+ def summary(instrument = 'all')
43
+ params = {}
44
+ params[:instrument] = instrument if instrument
45
+
46
+ request.send(path: '/api/v1/public/getsummary', params: params)
47
+ end
48
+
49
+ def margins(instrument, quantity: 1, price: 0.01)
50
+ params = {
51
+ instrument: instrument,
52
+ quantity: quantity,
53
+ price: price
54
+ }
55
+
56
+ request.send(path: '/api/v1/private/getmargins', params: params)
57
+ end
58
+
59
+ def account(full: false)
60
+ params = {
61
+ ext: full
62
+ }
63
+
64
+ request.send(path: '/api/v1/private/account', params: params)
65
+ end
66
+
67
+
68
+ # | Name | Type | Decription |
69
+ # |--------------|------------|-----------------------------------------------------------------------------------|
70
+ # | `instrument` | `string` | Required, instrument name |
71
+ # | `quantity` | `integer` | Required, quantity, in contracts ($10 per contract for futures, ฿1 — for options) |
72
+ # | `price` | `float` | Required, USD for futures, BTC for options |
73
+ # | `type` | `string` | Required, "limit", "market" or for futures only: "stop_limit" |
74
+ # | `stopPx` | `string` | Required, needed for stop_limit order, defines stop price |
75
+ # | `post_only` | `boolean` | Optional, if true then the order will be POST ONLY |
76
+ # | `label` | `string` | Optional, user defined maximum 32-char label for the order |
77
+ # | `max_show` | `string` | Optional, optional parameter, if "0" then the order will be hidden |
78
+ # | `adv` | `string` | Optional, can be "implv", "usd", or absent (advanced order type) |
79
+
80
+ def buy(instrument, quantity, price, type: "limit", stopPx: nil, execInst: "index_price", post_only: nil, reduce_only: nil, label: nil, max_show: nil, adv: nil)
81
+ params = {
82
+ instrument: instrument,
83
+ quantity: quantity,
84
+ price: price
85
+ }
86
+
87
+ %i(type stopPx post_only reduce_only label max_show adv execInst).each do |var|
88
+ variable = eval(var.to_s)
89
+ params[var] = variable if variable
90
+ end
91
+
92
+ request.send(path: '/api/v1/private/buy', params: params)
93
+ end
94
+
95
+
96
+ # | Name | Type | Decription |
97
+ # |--------------|------------|-----------------------------------------------------------------------------------|
98
+ # | `instrument` | `string` | Required, instrument name |
99
+ # | `quantity` | `integer` | Required, quantity, in contracts ($10 per contract for futures, ฿1 — for options) |
100
+ # | `price` | `float` | Required, USD for futures, BTC for options |
101
+ # | `post_only` | `boolean` | Optional, if true then the order will be POST ONLY |
102
+ # | `label` | `string` | Optional, user defined maximum 32-char label for the order |
103
+ # | `max_show` | `string` | Optional, optional parameter, if "0" then the order will be hidden |
104
+ # | `adv` | `string` | Optional, can be "implv", "usd", or absent (advanced order type) |
105
+ #
106
+
107
+ def sell(instrument, quantity, price, type: "limit", stopPx: nil, execInst: "index_price", post_only: nil, reduce_only: nil, label: nil, max_show: nil, adv: nil)
108
+ params = {
109
+ instrument: instrument,
110
+ quantity: quantity,
111
+ price: price
112
+ }
113
+
114
+ %i(type stopPx post_only reduce_only label max_show adv execInst).each do |var|
115
+ variable = eval(var.to_s)
116
+ params[var] = variable if variable
117
+ end
118
+
119
+ request.send(path: '/api/v1/private/sell', params: params)
120
+ end
121
+
122
+ #
123
+ # | Name | Type | Decription |
124
+ # |--------------|------------|-----------------------------------------------------------------------------------|
125
+ # | `order_id` | `integer` | Required, ID of the order returned by "sell" or "buy" request |
126
+ # | `quantity` | `integer` | Required, quantity, in contracts ($10 per contract for futures, ฿1 — for options) |
127
+ # | `price` | `float` | Required, USD for futures, BTC for options |
128
+ # | `post_only` | `boolean` | Optional, if true then the order will be POST ONLY |
129
+ # | `adv` | `string` | Optional, can be "implv", "usd", or absent (advanced order type) |
130
+
131
+ def edit(order_id, quantity, price, post_only: nil, adv: nil, stopPx: nil)
132
+ params = {
133
+ orderId: order_id,
134
+ quantity: quantity,
135
+ price: price
136
+ }
137
+
138
+ %i(post_only adv stopPx).each do |var|
139
+ variable = eval(var.to_s)
140
+ params[var] = variable if variable
141
+ end
142
+
143
+ request.send(path: '/api/v1/private/edit', params: params)
144
+ end
145
+
146
+ def cancel(order_id)
147
+ params = {
148
+ "orderId": order_id
149
+ }
150
+
151
+ request.send(path: '/api/v1/private/cancel', params: params)
152
+ end
153
+
154
+ def cancel_all(type = "all")
155
+ params = {
156
+ "type": type
157
+ }
158
+
159
+ request.send(path: '/api/v1/private/cancelall', params: params)
160
+ end
161
+
162
+ def open_orders(instrument: nil, order_id: nil, type: nil)
163
+ params = {}
164
+ params[:instrument] = instrument if instrument
165
+ params[:orderId] = order_id if order_id
166
+ params[:type] = type if type
167
+
168
+ request.send(path: '/api/v1/private/getopenorders', params: params)
169
+ end
170
+
171
+ def positions
172
+ request.send(path: '/api/v1/private/positions', params: {})
173
+ end
174
+
175
+ def order_state(order_id)
176
+ params = {
177
+ "orderId": order_id
178
+ }
179
+
180
+ request.send(path: '/api/v1/private/orderstate', params: params)
181
+ end
182
+
183
+ def order_history(instrument: nil, count: nil, offset: nil)
184
+ params = {}
185
+ params[:instrument] = instrument if instrument
186
+ params[:count] = count if count
187
+ params[:offset] = offset if offset
188
+
189
+ request.send(path: '/api/v1/private/orderhistory', params: params)
190
+ end
191
+
192
+ def trade_history(instrument: nil, count: nil, start_trade_id: nil)
193
+ params = {}
194
+
195
+ %i(count instrument).each do |var|
196
+ variable = eval(var.to_s)
197
+ params[var] = variable if variable
198
+ end
199
+
200
+ params[:startTradeId] = start_trade_id if start_trade_id
201
+
202
+ request.send(path: '/api/v1/private/tradehistory', params: params)
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,24 @@
1
+ module Deribit
2
+ class Error < StandardError
3
+ attr_reader :code, :msg
4
+
5
+ def initialize(code: nil, json: {}, message: nil)
6
+ @code = code || json[:code]
7
+ @msg = message || json[:msg]
8
+ end
9
+
10
+ def inspect
11
+ message = ""
12
+ message += "(#{code}) " unless code.nil?
13
+ message += "#{msg}" unless msg.nil?
14
+ end
15
+
16
+ def message
17
+ inspect
18
+ end
19
+
20
+ def to_s
21
+ inspect
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ class Deribit::ImmutableHeaderKey
2
+ attr_reader :key
3
+
4
+ def initialize(key)
5
+ @key = key
6
+ end
7
+
8
+ def downcase
9
+ self
10
+ end
11
+
12
+ def capitalize
13
+ self
14
+ end
15
+
16
+ def split(*)
17
+ [self]
18
+ end
19
+
20
+ def hash
21
+ key.hash
22
+ end
23
+
24
+ def eql?(other)
25
+ key.eql? other.key.eql?
26
+ end
27
+
28
+ def to_s
29
+ def self.to_s
30
+ key
31
+ end
32
+ self
33
+ end
34
+ end
@@ -0,0 +1,91 @@
1
+ require 'base64'
2
+ require 'net/http'
3
+
4
+ module Deribit
5
+ class Request
6
+ attr_accessor :key, :secret, :base_uri
7
+
8
+ def initialize(key, secret, test_server: nil)
9
+ test_server = ENV['DERIBIT_TEST_SERVER'] if test_server == nil
10
+ @key = key
11
+ @secret = secret
12
+ @base_uri = test_server ? URI(TEST_URL) : URI(SERVER_URL)
13
+ end
14
+
15
+ def send(path: DEFAULT_REQUEST_PATH, params: {})
16
+ uri = base_uri + path
17
+
18
+ if path.start_with?(PRIVATE_PATH)
19
+ request = Net::HTTP::Post.new(uri.path)
20
+ request.body = URI.encode_www_form(params)
21
+ request.add_field 'x-deribit-sig', generate_signature(path, params)
22
+
23
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
24
+ http.request(request)
25
+ end
26
+ else
27
+ uri.query = URI.encode_www_form(params)
28
+ response = Net::HTTP.get_response(uri)
29
+ end
30
+
31
+ if is_error_response?(response)
32
+ raise Error.new(code: response.code)
33
+ else
34
+ process(response)
35
+ end
36
+ end
37
+
38
+ def process(response)
39
+ json = JSON.parse(response.body, symbolize_names: true)
40
+
41
+ raise Error.new(message: "Failed for #{key}: " + json[:message]) unless json[:success]
42
+
43
+ if json.include?(:result)
44
+ json[:result]
45
+ elsif json.include?(:message)
46
+ json[:message]
47
+ else
48
+ "ok"
49
+ end
50
+ end
51
+
52
+ def generate_signature(path, params = {})
53
+ timestamp = Time.now.utc.to_i + 1000
54
+
55
+ signature_data = {
56
+ _: timestamp,
57
+ _ackey: key,
58
+ _acsec: secret,
59
+ _action: path
60
+ }
61
+
62
+ signature_data.update(params)
63
+ sorted_signature_data = signature_data.sort
64
+
65
+ converter = ->(data){
66
+ key = data[0]
67
+ value = data[1]
68
+ if value.is_a?(Array)
69
+ [key.to_s, value.join].join('=')
70
+ else
71
+ [key.to_s, value.to_s].join('=')
72
+ end
73
+ }
74
+
75
+ items = sorted_signature_data.map(&converter)
76
+ signature_string = items.join('&')
77
+
78
+ sha256 = OpenSSL::Digest::SHA256.new
79
+ sha256_signature = sha256.digest(signature_string.encode('utf-8'))
80
+
81
+ base64_signature = Base64.encode64(sha256_signature).encode('utf-8')
82
+
83
+ [key, timestamp, base64_signature].join('.').strip
84
+ end
85
+
86
+ def is_error_response?(response)
87
+ code = response.code.to_i
88
+ code == 0 || code >= 400
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module Deribit
2
+ VERSION = "1.3.2"
3
+ end
@@ -0,0 +1,56 @@
1
+ module Deribit
2
+ class WS
3
+
4
+ class Handler
5
+ attr_accessor :timestamp
6
+
7
+ AVAILABLE_METHODS = [
8
+ :account,
9
+ :getcurrencies,
10
+ :subscribe,
11
+ :subscribed,
12
+ :unsubscribe,
13
+ :buy,
14
+ :sell,
15
+ :trade,
16
+ :trade_event,
17
+ :order_book_event,
18
+ :user_order_event,
19
+ :user_orders_event,
20
+ :announcements,
21
+ :index,
22
+ :heartbeat,
23
+ :order,
24
+ :pong
25
+ ]
26
+ SILENT = [:setheartbeat, :subscribed, :heartbeat, :"public API test"]
27
+
28
+ def initialize
29
+ @timestamp = Time.now.to_i
30
+ end
31
+
32
+ def method_missing(m, *json, &block)
33
+ return false if SILENT.include?(m.to_sym)
34
+
35
+ puts "Delegating #{m}"
36
+ if AVAILABLE_METHODS.include?(m.to_sym)
37
+ notice(json)
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ def notice(json)
44
+ return json.each { |e| notice(e) } if json.is_a?(Array)
45
+
46
+ msg = json.is_a?(String) ? json : json[:message]
47
+ puts "Notice: #{msg}" if msg && !SILENT.include?(msg.to_sym)
48
+ end
49
+
50
+ def handle_error(json, error)
51
+ puts "Alert! #{error.class} on message: '#{json.try(:fetch, :message)}', #{json.inspect}. Message: #{error.full_message}"
52
+ end
53
+ end
54
+
55
+ end
56
+ end
data/lib/deribit/ws.rb ADDED
@@ -0,0 +1,285 @@
1
+ require 'websocket-client-simple'
2
+
3
+ module Deribit
4
+ class WS
5
+ AVAILABLE_EVENTS = [:order_book, :trade, :my_trade, :user_order, :index, :portfolio, :announcement]
6
+
7
+ attr_reader :socket, :response, :ids_stack, :handler, :subscribed_instruments
8
+
9
+ def initialize(key, secret, handler: Handler, test_server: nil)
10
+ test_server = ENV["DERIBIT_TEST_SERVER"] if test_server == nil
11
+ @request = Request.new(key, secret, test_server: test_server)
12
+ @socket = connect(test_server ? WS_TEST_URL : WS_SERVER_URL)
13
+ @handler = handler.instance_of?(Class) ? handler.new : handler
14
+ @ids_stack = []
15
+
16
+ # the structure of subscribed_instruments: {'event_name' => ['instrument1', 'instrument2']]}
17
+ @subscribed_instruments = {}
18
+
19
+ start_handle
20
+ end
21
+
22
+ def add_subscribed_instruments(instruments: , events: )
23
+ instruments = [instruments] unless instruments.is_a?(Array)
24
+
25
+ events.each do |event|
26
+ _event = event.to_sym
27
+ @subscribed_instruments[_event] = if sub_instr = @subscribed_instruments[_event]
28
+ (sub_instr + instruments).uniq
29
+ else
30
+ instruments.uniq
31
+ end
32
+ end
33
+ end
34
+
35
+ def connect(url)
36
+ puts "Connecting to #{url}"
37
+ WebSocket::Client::Simple.connect(url)
38
+ end
39
+
40
+ def reconnect!
41
+ @socket = connect
42
+ start_handle
43
+ sleep 3
44
+ resubscribe!
45
+ end
46
+
47
+ def resubscribe!
48
+ if subscribed_instruments.any?
49
+ subscribed_instruments.each do |event, instruments|
50
+ p "Reconnecting to event: #{event} at instrument: #{instruments}"
51
+ subscribe(instruments, events: event.to_s)
52
+ end
53
+ end
54
+ end
55
+
56
+ def close
57
+ @socket.close
58
+ end
59
+
60
+ def ping
61
+ message = {action: "/api/v1/public/ping"}
62
+ @socket.send(message.to_json)
63
+ end
64
+
65
+ def test
66
+ message = {action: "/api/v1/public/test"}
67
+ @socket.send(message.to_json)
68
+ end
69
+
70
+ # events to be reported, possible events:
71
+ # "order_book" -- order book change
72
+ # "trade" -- trade notification
73
+ # "announcements" -- announcements (list of new announcements titles is send)
74
+ # "user_order" -- change of user orders (openning, cancelling, filling)
75
+ # "my_trade" -- filtered trade notification, only trades of the
76
+ # subscribed user are reported with trade direction "buy"/"sell" from the
77
+ # subscribed user point of view ("I sell ...", "I buy ..."), see below.
78
+ # Note, for "index" - events are ignored and can be []
79
+ def subscribe(instruments = ['BTC-PERPETUAL'] , events: ["user_order"], arguments: {})
80
+ instruments = [instruments] unless instruments.is_a?(Array)
81
+ events = [events] unless events.is_a?(Array)
82
+
83
+ raise "Events must include only #{AVAILABLE_EVENTS.join(", ")} actions" if events.map{|e| AVAILABLE_EVENTS.include?(e.to_sym)}.index(false) or events.empty?
84
+ raise "instruments are required" if instruments.empty?
85
+
86
+ arguments = arguments.merge(instrument: instruments, event: events)
87
+ send(path: '/api/v1/private/subscribe', arguments: arguments)
88
+ end
89
+
90
+ #unsubscribe for all notifications if instruments is empty
91
+ def unsubscribe(instruments=[])
92
+ instruments = [instruments] unless instruments.is_a?(Array)
93
+ send(path: '/api/v1/private/unsubscribe')
94
+ sleep(0.2)
95
+ if instruments.any?
96
+ @subscribed_instruments.each do |event, _instruments|
97
+ @subscribed_instruments[event] = _instruments - instruments
98
+ subscribe(@subscribed_instruments[event], events: [event])
99
+ sleep(0.2)
100
+ end
101
+ end
102
+ end
103
+
104
+ # | Name | Type | Decription |
105
+ # |--------------|------------|-----------------------------------------------------------------------------------|
106
+ # | `instrument` | `string` | Required, instrument name |
107
+ # | `quantity` | `integer` | Required, quantity, in contracts ($10 per contract for futures, ฿1 — for options) |
108
+ # | `price` | `float` | Required, USD for futures, BTC for options |
109
+ # | `type` | `string` | Required, "limit", "market" or for futures only: "stop_limit" |
110
+ # | `stopPx` | `string` | Required, needed for stop_limit order, defines stop price |
111
+ # | `post_only` | `boolean` | Optional, if true then the order will be POST ONLY |
112
+ # | `label` | `string` | Optional, user defined maximum 32-char label for the order |
113
+ # | `max_show` | `string` | Optional, optional parameter, if "0" then the order will be hidden |
114
+ # | `adv` | `string` | Optional, can be "implv", "usd", or absent (advanced order type) |
115
+
116
+ def buy(instrument, quantity, price, type: "limit", stopPx: nil, execInst: "mark_price", post_only: nil, label: nil, max_show: nil, adv: nil)
117
+ params = {
118
+ instrument: instrument,
119
+ quantity: quantity
120
+ }
121
+ params[:price] = price if price
122
+
123
+ %i(type stopPx post_only label max_show adv execInst).each do |var|
124
+ variable = eval(var.to_s)
125
+ params[var] = variable if variable
126
+ end
127
+
128
+ send(path: '/api/v1/private/buy', arguments: params)
129
+ end
130
+
131
+ # | Name | Type | Decription |
132
+ # |--------------|------------|-----------------------------------------------------------------------------------|
133
+ # | `instrument` | `string` | Required, instrument name |
134
+ # | `quantity` | `integer` | Required, quantity, in contracts ($10 per contract for futures, ฿1 — for options) |
135
+ # | `price` | `float` | Required, USD for futures, BTC for options |
136
+ # | `post_only` | `boolean` | Optional, if true then the order will be POST ONLY |
137
+ # | `label` | `string` | Optional, user defined maximum 32-char label for the order |
138
+ # | `max_show` | `string` | Optional, optional parameter, if "0" then the order will be hidden |
139
+ # | `adv` | `string` | Optional, can be "implv", "usd", or absent (advanced order type) |
140
+ #
141
+
142
+ def sell(instrument, quantity, price, type: "limit", stopPx: nil, execInst: "mark_price", post_only: nil, label: nil, max_show: nil, adv: nil)
143
+ params = {
144
+ instrument: instrument,
145
+ quantity: quantity
146
+ }
147
+ params[:price] = price if price
148
+
149
+ %i(type stopPx post_only label max_show adv execInst).each do |var|
150
+ variable = eval(var.to_s)
151
+ params[var] = variable if variable
152
+ end
153
+
154
+ send(path: '/api/v1/private/sell', arguments: params)
155
+ end
156
+
157
+ def account
158
+ send(path: '/api/v1/private/account')
159
+ end
160
+
161
+ def instruments(expired: false)
162
+ send(path: '/api/v1/public/getinstruments', arguments: {expired: expired})
163
+ end
164
+
165
+ def currencies
166
+ send(path: '/api/v1/public/getcurrencies')
167
+ end
168
+
169
+ def summary(instrument = 'BTC-PERPETUAL')
170
+ send(path: '/api/v1/public/getsummary', arguments: { instrument: instrument })
171
+ end
172
+
173
+ def openorders(instrument: "BTC-PERPETUAL", order_id: nil, type: nil)
174
+ params = {}
175
+ params[:instrument] = instrument if instrument
176
+ params[:orderId] = order_id if order_id
177
+ params[:type] = type if type
178
+
179
+ send(path: '/api/v1/private/getopenorders', arguments: params)
180
+ end
181
+
182
+ def cancel(order_id)
183
+ params = {
184
+ "orderId": order_id
185
+ }
186
+
187
+ send(path: '/api/v1/private/cancel', arguments: params)
188
+ end
189
+
190
+ def cancel_all(type = "all")
191
+ params = {
192
+ "type": type
193
+ }
194
+
195
+ send(path: '/api/v1/private/cancelall', arguments: params)
196
+ end
197
+
198
+ def set_heartbeat(interval = "60")
199
+ params = {
200
+ "interval": interval
201
+ }
202
+
203
+ send(path: '/api/v1/public/setheartbeat', arguments: params)
204
+ end
205
+
206
+ def handle_notifications(notifications)
207
+ return if notifications.empty?
208
+ notification, *tail = notifications
209
+ handler.send(notification[:message], notification[:result])
210
+
211
+ handle_notifications(tail)
212
+ end
213
+
214
+ private
215
+
216
+ def start_handle
217
+ instance = self
218
+ @socket.on :message do |msg|
219
+ # puts "msg = #{msg.inspect}"
220
+ begin
221
+ if msg.type == :text
222
+ json = JSON.parse(msg.data, symbolize_names: true)
223
+ puts "Subscribed!" if json[:message] == "subscribed"
224
+
225
+ if json[:message] == "test_request"
226
+ # puts "Got test request: #{json.inspect}" # DEBUG
227
+ instance.test
228
+ elsif json[:id] and stack_id = instance.ids_stack.find{|i| i[json[:id]]}
229
+ method = stack_id[json[:id]][0]
230
+ #pass the method to handler
231
+ params = instance.ids_stack.delete(stack_id)
232
+
233
+ #save subscribed_instruments for resubscribe in unsubscribe action
234
+ if method == 'subscribe'
235
+ params = params[json[:id]][1][:arguments]
236
+ instance.add_subscribed_instruments(instruments: params[:instrument], events: params[:event])
237
+ end
238
+
239
+ instance.handler.send(method, json)
240
+ elsif json[:notifications]
241
+ instance.handle_notifications(json[:notifications])
242
+ else
243
+ instance.handler.send(:notice, json)
244
+ end
245
+
246
+ instance.handler.timestamp = Time.now.to_i
247
+ elsif msg.type == :close
248
+ puts "trying to reconnect = got close event, msg: #{msg.inspect}"
249
+ instance.reconnect!
250
+ end
251
+ rescue StandardError => e
252
+ instance.handler.handle_error(json, e)
253
+ end
254
+ end
255
+
256
+ @socket.on :error do |e|
257
+ puts e
258
+ end
259
+ end
260
+
261
+ def send(path: , arguments: {})
262
+ return unless path
263
+ params = {action: path, arguments: arguments}
264
+ sig = @request.generate_signature(path, arguments)
265
+ params[:sig] = sig
266
+ params[:id] = Time.now.to_i
267
+
268
+ action = path[/\/api.*\/([^\/]+)$/, 1]
269
+ put_id(params[:id], [action, params])
270
+
271
+ p params
272
+ @socket.send(params.to_json)
273
+ end
274
+
275
+
276
+ def put_id(id, action)
277
+ @ids_stack << {id => action}
278
+ end
279
+
280
+ def pop_id(id)
281
+ @ids_stack.delete(id)
282
+ end
283
+ end
284
+
285
+ end