bitflyer 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f49b4a666d8df93717c072384a5be911359f9374c323e06ed7ee50cc3780b22
4
- data.tar.gz: 719a1bc27381f6cf81a560050f306b7adc5946e36ddbd30da2f60686e34af652
3
+ metadata.gz: 0afb7c55d8bf1c0c68fdb043881e446ad9481b932a328c25cc68dadabbd935c4
4
+ data.tar.gz: 0d58c2aebb84b56f15bcc4bca7da2a2ace9f8c39ec6a6b87fa9460d03087d61f
5
5
  SHA512:
6
- metadata.gz: eb08f088aea6b006cef2f8b29c8d52c908653386909524cb3aed4057e1cba6f1a7992bc713b6726abb36c0f8a98d8581e13b712c6b665e69eda8565bb957bebc
7
- data.tar.gz: 63a9357c4f45bd9f77604e9393d32c852daf682c6da54c8552b1eea4c3be22ab89b7abe9392f784e3cf6099e07a77b286f7583620ed008b8e44d405996fd3925
6
+ metadata.gz: af27099352002ac7983bb1655c6f7d04e606fa06494d76e37943c1c1343c4d6fdf987d235d36a863e63365e667ff7a61a7564fa81800f8ef6f9798b362c6df76
7
+ data.tar.gz: 6e5f176afd23d3c438a1a5ec679a9e5dbd0b1a30784ce91968ce29f2914d7cd5dcd3eec1b5a96e260a203aef4375c4a90b31b2a77b2fb78c3e6fdfe28ec70a19
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- bitflyer (1.1.0)
4
+ bitflyer (1.2.0)
5
5
  faraday (>= 0.14, < 1.4)
6
6
  faraday_middleware (>= 0.12, < 1.1)
7
7
  websocket-client-simple (~> 0.3.0)
@@ -30,7 +30,7 @@ GEM
30
30
  method_source (~> 1.0)
31
31
  rainbow (3.0.0)
32
32
  rake (13.0.3)
33
- regexp_parser (2.0.3)
33
+ regexp_parser (2.1.1)
34
34
  rexml (3.2.4)
35
35
  rspec (3.10.0)
36
36
  rspec-core (~> 3.10.0)
@@ -45,7 +45,7 @@ GEM
45
45
  diff-lcs (>= 1.2.0, < 2.0)
46
46
  rspec-support (~> 3.10.0)
47
47
  rspec-support (3.10.2)
48
- rubocop (1.10.0)
48
+ rubocop (1.11.0)
49
49
  parallel (~> 1.10)
50
50
  parser (>= 3.0.0.0)
51
51
  rainbow (>= 2.2.2, < 4.0)
data/README.md CHANGED
@@ -37,20 +37,52 @@ p private_client.positions # will print your positions
37
37
 
38
38
  ### Realtime API
39
39
 
40
+ #### Public events
41
+
40
42
  Accessor format is like `{event_name}_{product_code}`.
41
43
  You can set lambda to get realtime events.
42
44
 
43
45
  `{event_name}` and `{product_code}` is defined at [client.rb](./lib/bitflyer/realtime/client.rb).
44
46
 
45
- #### Example
47
+ #### Private events
48
+
49
+ To subscribe to the private `child_order_events` and `parent_order_events`, pass your API key and secret when creating the `realtime_client`.
50
+
51
+ #### Connection status monitoring
46
52
 
53
+ The `ready` callback is called when the `realtime_client` is ready to receive events (after the socket connection is established, the optional authentication has succeeded, and the channels have been subscribed). If connection is lost, the `disconnected` callback is called, and reconnection is attempted automatically. When connection is restored, `ready` is called again.
54
+
55
+ #### Examples
56
+
57
+ For public events only:
47
58
  ```ruby
48
59
  client = Bitflyer.realtime_client
49
- client.ticker_btc_jpy = ->(json){ p json } # will print json object
60
+ client.ticker_btc_jpy = ->(json){ p json } # will print json object
61
+ client.executions_btc_jpy = ->(json){ p json }
62
+ # ...
63
+ ```
64
+
65
+ For both public and private events:
66
+ ```ruby
67
+ client = Bitflyer.realtime_client('YOUR_API_KEY', 'YOUR_API_SECRET')
68
+ # Private events:
69
+ client.child_order_events = ->(json){ p json }
70
+ client.parent_order_events = ->(json){ p json }
71
+ # Public events:
72
+ client.ticker_btc_jpy = ->(json){ p json }
50
73
  client.executions_btc_jpy = ->(json){ p json }
51
- # ...
74
+ # ...
75
+ ```
76
+
77
+ Connection monitoring:
78
+ ```ruby
79
+ client = Bitflyer.realtime_client
80
+ client.ready = -> { p "Client is ready to receive events" }
81
+ client.disconnected = ->(error) { p "Client got disconnected" }
52
82
  ```
53
83
 
84
+
85
+
54
86
  ## Contributing
55
87
 
56
88
  Bug reports and pull requests are welcome on GitHub at https://github.com/unhappychoice/bitflyer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -80,4 +112,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
80
112
 
81
113
  <!-- ALL-CONTRIBUTORS-LIST:END -->
82
114
 
83
- This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
115
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
data/lib/bitflyer.rb CHANGED
@@ -5,8 +5,8 @@ require 'bitflyer/http'
5
5
  require 'bitflyer/realtime'
6
6
 
7
7
  module Bitflyer
8
- def realtime_client
9
- Bitflyer::Realtime::Client.new
8
+ def realtime_client(key = nil, secret = nil)
9
+ Bitflyer::Realtime::Client.new(key, secret)
10
10
  end
11
11
 
12
12
  def http_public_client
@@ -4,13 +4,17 @@ require_relative './websocket'
4
4
 
5
5
  module Bitflyer
6
6
  module Realtime
7
- EVENT_NAMES = %w[lightning_board_snapshot lightning_board lightning_ticker lightning_executions].freeze
7
+ PUBLIC_EVENT_NAMES = %w[lightning_board_snapshot lightning_board lightning_ticker lightning_executions].freeze
8
8
  MARKETS = %w[BTC_JPY FX_BTC_JPY ETH_BTC BCH_BTC BTCJPY_MAT3M BTCJPY_MAT1WK BTCJPY_MAT2WK].freeze
9
- CHANNEL_NAMES = EVENT_NAMES.product(MARKETS).map { |e, m| "#{e}_#{m}" }
9
+ PUBLIC_CHANNEL_NAMES = PUBLIC_EVENT_NAMES.product(MARKETS).map { |e, m| "#{e}_#{m}" }.freeze
10
+ PRIVATE_CHANNEL_NAMES = %w[child_order_events parent_order_events].freeze
11
+ CHANNEL_NAMES = (PUBLIC_CHANNEL_NAMES + PRIVATE_CHANNEL_NAMES).freeze
10
12
 
11
13
  SOCKET_HOST = 'https://io.lightstream.bitflyer.com'
12
14
 
13
15
  class Client
16
+ extend Forwardable
17
+ def_delegators :@websocket_client, :ready=, :disconnected=
14
18
  attr_accessor :websocket_client, :ping_interval, :ping_timeout, :last_ping_at, :last_pong_at
15
19
 
16
20
  Realtime::CHANNEL_NAMES.each do |channel_name|
@@ -19,8 +23,8 @@ module Bitflyer
19
23
  end
20
24
  end
21
25
 
22
- def initialize
23
- @websocket_client = Bitflyer::Realtime::WebSocketClient.new(host: SOCKET_HOST)
26
+ def initialize(key = nil, secret = nil)
27
+ @websocket_client = Bitflyer::Realtime::WebSocketClient.new(host: SOCKET_HOST, key: key, secret: secret)
24
28
  end
25
29
  end
26
30
  end
@@ -2,54 +2,55 @@
2
2
 
3
3
  require 'websocket-client-simple'
4
4
  require 'json'
5
+ require 'openssl'
5
6
 
6
7
  module Bitflyer
7
8
  module Realtime
8
9
  class WebSocketClient
9
- attr_accessor :websocket_client, :channel_name, :channel_callbacks, :ping_interval, :ping_timeout,
10
- :last_ping_at, :last_pong_at, :error
10
+ attr_accessor :websocket_client, :channel_names, :channel_callbacks, :ping_interval, :ping_timeout,
11
+ :last_ping_at, :last_pong_at, :ready, :disconnected
11
12
 
12
- def initialize(host:, debug: false)
13
+ def initialize(host:, key:, secret:, debug: false)
13
14
  @host = host
15
+ @key = key
16
+ @secret = secret
14
17
  @debug = debug
15
- @error = nil
16
18
  @channel_names = []
17
19
  @channel_callbacks = {}
18
20
  connect
21
+ start_monitoring
19
22
  end
20
23
 
21
24
  def subscribe(channel_name:, &block)
22
25
  debug_log "Subscribe #{channel_name}"
23
26
  @channel_names = (@channel_names + [channel_name]).uniq
24
27
  @channel_callbacks[channel_name] = block
25
- websocket_client.send "42#{['subscribe', channel_name].to_json}"
28
+ @websocket_client.send "42#{['subscribe', channel_name].to_json}"
26
29
  end
27
30
 
28
31
  def connect
29
32
  @websocket_client = WebSocket::Client::Simple.connect "#{@host}/socket.io/?transport=websocket"
30
33
  this = self
34
+ @websocket_client.on(:message) { |payload| this.handle_message(payload: payload) }
35
+ @websocket_client.on(:error) { |error| this.handle_error(error: error) }
36
+ @websocket_client.on(:close) { |error| this.handle_close(error: error) }
37
+ rescue SocketError => e
38
+ puts e
39
+ puts e.backtrace.join("\n")
40
+ end
31
41
 
42
+ def start_monitoring
32
43
  Thread.new do
33
44
  loop do
34
45
  sleep 1
35
46
  if @websocket_client&.open?
36
47
  send_ping
37
48
  wait_pong
49
+ else
50
+ reconnect
38
51
  end
39
52
  end
40
53
  end
41
-
42
- Thread.new do
43
- loop do
44
- sleep 1
45
- next unless @error
46
-
47
- reconnect
48
- end
49
- end
50
-
51
- @websocket_client.on(:message) { |payload| this.handle_message(payload: payload) }
52
- @websocket_client.on(:error) { |error| this.handle_error(error: error) }
53
54
  end
54
55
 
55
56
  def send_ping
@@ -70,23 +71,19 @@ module Bitflyer
70
71
  end
71
72
 
72
73
  def reconnect
73
- return unless @error
74
+ return if @websocket_client&.open?
74
75
 
75
76
  debug_log 'Reconnecting...'
76
77
 
77
- @error = nil
78
78
  @websocket_client.close if @websocket_client.open?
79
79
  connect
80
- @channel_names.each do |channel_name|
81
- debug_log "42#{{ subscribe: channel_name }.to_json}"
82
- websocket_client.send "42#{['subscribe', channel_name].to_json}"
83
- end
84
80
  end
85
81
 
86
82
  def handle_error(error:)
87
83
  debug_log error
88
84
  return unless error.is_a? Errno::ECONNRESET
89
85
 
86
+ @disconnected&.call(error)
90
87
  reconnect
91
88
  end
92
89
 
@@ -101,20 +98,60 @@ module Bitflyer
101
98
  when 3 then receive_pong
102
99
  when 41 then disconnect
103
100
  when 42 then emit_message(json: body)
101
+ when 430 then authenticated(json: body)
104
102
  end
105
103
  rescue StandardError => e
106
104
  puts e
107
105
  puts e.backtrace.join("\n")
108
106
  end
109
107
 
108
+ def handle_close(error:)
109
+ debug_log error
110
+ @disconnected&.call(error)
111
+ end
112
+
110
113
  def setup_by_response(json:)
111
114
  body = JSON.parse json
112
115
  @ping_interval = body['pingInterval'].to_i || 25_000
113
116
  @ping_timeout = body['pingTimeout'].to_i || 60_000
114
117
  @last_ping_at = Time.now.to_i
115
118
  @last_pong_at = Time.now.to_i
116
- channel_callbacks.each do |channel_name, _|
117
- websocket_client.send "42#{['subscribe', channel_name].to_json}"
119
+ if @key && @secret
120
+ authenticate
121
+ else
122
+ subscribe_channels
123
+ @ready&.call
124
+ end
125
+ end
126
+
127
+ def authenticate
128
+ debug_log 'Authenticate'
129
+ timestamp = Time.now.to_i
130
+ nonce = Random.new.bytes(16).unpack('H*').first
131
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), @secret, timestamp.to_s + nonce)
132
+ auth_params = {
133
+ api_key: @key,
134
+ timestamp: timestamp,
135
+ nonce: nonce,
136
+ signature: signature
137
+ }
138
+ @websocket_client.send "420#{['auth', auth_params].to_json}"
139
+ end
140
+
141
+ def authenticated(json:)
142
+ if json == '[null]'
143
+ debug_log 'Authenticated'
144
+ subscribe_channels
145
+ @ready&.call
146
+ else
147
+ raise "Authentication failed: #{json}"
148
+ end
149
+ end
150
+
151
+ def subscribe_channels
152
+ @channel_callbacks.each do |channel_name, _|
153
+ debug_log "42#{{ subscribe: channel_name }.to_json}"
154
+ @websocket_client.send "42#{['subscribe', channel_name].to_json}"
118
155
  end
119
156
  end
120
157
 
@@ -125,7 +162,7 @@ module Bitflyer
125
162
 
126
163
  def disconnect
127
164
  debug_log 'Disconnecting from server...'
128
- @error = true
165
+ @websocket_client.close
129
166
  end
130
167
 
131
168
  def emit_message(json:)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitflyer
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitflyer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuji Ueki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-16 00:00:00.000000000 Z
11
+ date: 2021-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday