bitflyer 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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