Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,15 @@ EM.run do
end
```

**User Data:**
**User Data (Spot WebSocket API):**

Spot listenKey REST endpoints (`POST/PUT/DELETE /api/v3/userDataStream`) were removed by Binance on 2026-02-20. Subscribe via the WebSocket API instead:

```ruby
EM.run do
websocket = Binance::WebSocket.new
websocket = Binance::WebSocketApi.new

listen_key = Binance::Api::UserDataStream.start!
websocket.user_data_stream!(listen_key) do |listen_key, data|
websocket.user_data_stream! do |subscription_id, data|
case data[:e].to_sym
when :outboundAccountPosition
when :balanceUpdate
Expand Down Expand Up @@ -158,11 +159,10 @@ You can find more info on all `kline_candlestick` attributes & available interva
- [`deposit_history`](https://binance-docs.github.io/apidocs/spot/en/#fiat-deposit-history-user_data): Get fiat deposit history.
- [`withdraw_history`](https://binance-docs.github.io/apidocs/spot/en/#fiat-withdraw-history-user_data): Get fiat withdrawal history.

### Binance::Api::DataStream class methods
### Binance::Api::UserDataStream class methods

Spot listenKey methods (`start!`, `keepalive!`, `stop!`) were removed by Binance and now raise. Use `Binance::WebSocketApi#user_data_stream!` for Spot user data.

- [`start!`](https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#start-user-data-stream-user_stream): Start a new user data stream.
- [`keepalive!`](https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#keepalive-user-data-stream-user_stream): Keepalive a user data stream.
- [`stop!`](https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#close-user-data-stream-user_stream): Close out a user data stream.
- [`margin_start!`](https://binance-docs.github.io/apidocs/spot/en/#start-user-data-stream-user_stream): Start a new margin user data stream.
- [`margin_keepalive!`](https://binance-docs.github.io/apidocs/spot/en/#keepalive-user-data-stream-user_stream): Keepalive a margin user data stream.

Expand All @@ -182,10 +182,14 @@ You can find more info on all `kline_candlestick` attributes & available interva

- [`candlesticks!`](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md#klinecandlestick-streams): Kline/candlestick bars for a symbol.
- [`trades!`](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md#trade-streams): The Trade Streams push raw trade information.
- [`user_data_stream!`](https://github.com/binance/binance-spot-api-docs/blob/master/user-data-stream.md#web-socket-payloads): Account updates, balances changes, and order updates.
- [`partial_book_depth!`](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md#partial-book-depth-streams): Top levels bids and asks, pushed every second.
- [`book_depth!`](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md#partial-book-depth-streams): Order book price and quantity depth updates used to locally manage an order book.

### Binance::WebSocketApi instance methods

- [`user_data_stream!`](https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/user-data-stream-requests): Account updates, balance changes, and order updates via `userDataStream.subscribe.signature`.
- `user_data_unsubscribe!`: Stop a User Data Stream subscription.

See the [rubydoc](http://www.rubydoc.info/gems/binance-ruby/0.1.2/Binance) for information about parameters for each method listed above.

For more information, please refer to the [official Rest API documentation](https://github.com/binance-exchange/binance-official-api-docs) written by the Binance team.
Expand Down
1 change: 1 addition & 0 deletions lib/binance-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
require "binance/api/user_data_stream"
require "binance/api/version"
require "binance/websocket"
require "binance/websocket_api"
8 changes: 8 additions & 0 deletions lib/binance/api/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ def signed_request_signature(payload:, api_secret_key: nil)
OpenSSL::HMAC.hexdigest(digest, api_secret_key || secret_key, payload)
end

# WebSocket API SIGNED params: sort keys alphabetically, then HMAC-SHA256.
# See https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/request-security
def signed_ws_api_params(params:, api_secret_key: nil)
unsigned = params.reject { |key, _| key.to_s == "signature" }
payload = unsigned.sort_by { |key, _| key.to_s }.map { |key, value| "#{key}=#{value}" }.join("&")
unsigned.merge(signature: signed_request_signature(payload: payload, api_secret_key: api_secret_key))
end

def timestamp
Time.now.utc.strftime("%s%3N")
end
Expand Down
27 changes: 13 additions & 14 deletions lib/binance/api/user_data_stream.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
module Binance
module Api
class UserDataStream
SPOT_LISTEN_KEY_REMOVED = <<~MSG.freeze
Spot listenKey User Data Stream endpoints were removed by Binance on 2026-02-20.
Use Binance::WebSocketApi#user_data_stream! instead
(userDataStream.subscribe.signature on the WebSocket API).
See https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/user-data-stream-requests
MSG

class << self
# It's recommended to send a ping about every 30 minutes.
def keepalive!(listen_key: nil, api_key: nil, api_secret_key: nil)
raise Error.new(message: "listen_key is required") if listen_key.nil?
Request.send!(api_key_type: :none, method: :put, path: "/api/v1/userDataStream",
params: { listenKey: listen_key }, security_type: :user_stream,
api_key: api_key, api_secret_key: api_secret_key)
raise Error.new(message: SPOT_LISTEN_KEY_REMOVED)
end

def start!(api_key: nil, api_secret_key: nil)
Request.send!(api_key_type: :none, method: :post, path: "/api/v1/userDataStream",
security_type: :user_stream, api_key: api_key, api_secret_key: api_secret_key)[:listenKey]
raise Error.new(message: SPOT_LISTEN_KEY_REMOVED)
end

def stop!(listen_key: nil, api_key: nil, api_secret_key: nil)
raise Error.new(message: SPOT_LISTEN_KEY_REMOVED)
end

def margin_start!(api_key: nil, api_secret_key: nil)
Expand All @@ -26,13 +32,6 @@ def margin_keepalive!(listen_key: nil, api_key: nil, api_secret_key: nil)
params: { listenKey: listen_key }, security_type: :user_stream,
api_key: api_key, api_secret_key: api_secret_key)
end

def stop!(listen_key: nil, api_key: nil, api_secret_key: nil)
raise Error.new(message: "listen_key is required") if listen_key.nil?
Request.send!(api_key_type: :none, method: :delete, path: "/api/v1/userDataStream",
params: { listenKey: listen_key }, security_type: :user_stream,
api_key: api_key, api_secret_key: api_secret_key)
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/binance/api/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Binance
module Api
VERSION = "1.5.1"
VERSION = "1.6.0"
end
end
17 changes: 8 additions & 9 deletions lib/binance/websocket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ def initialize(on_open: nil, on_close: nil)
super wss_uri, nil, ping: 180

@request_id_inc = 0
@user_stream_handlers = {}

on :open do |event|
on_open&.call(event)
Expand Down Expand Up @@ -55,9 +54,14 @@ def candlesticks!(symbols, interval, &on_receive)
subscribe(symbols_fmt.map { |s| "#{s.downcase}@kline_#{interval}" })
end

def user_data_stream!(listen_key, &on_receive)
@user_stream_handlers[listen_key] = on_receive
subscribe([listen_key])
# Spot listenKey streams were removed by Binance on 2026-02-20.
# Use Binance::WebSocketApi#user_data_stream! instead.
def user_data_stream!(*)
raise Error.new(
"Spot listenKey user data streams are no longer available. " \
"Use Binance::WebSocketApi#user_data_stream! " \
"(userDataStream.subscribe.signature on the WebSocket API)."
)
end

# stream name: <symbol>@trade
Expand Down Expand Up @@ -189,11 +193,6 @@ def process_data(data)
@candlesticks_handler&.call(json[:stream], json[:data])
when :depthUpdate
@book_depth_handler&.call(json[:stream], json[:data])
when :outboundAccountPosition
when :balanceUpdate
when :executionReport # order update
listen_key = json[:stream]
@user_stream_handlers[listen_key]&.call(listen_key, json[:data])
when :trade
@trades_handler&.call(json[:stream], json[:data])
end
Expand Down
134 changes: 134 additions & 0 deletions lib/binance/websocket_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
require "securerandom"

module Binance
# Spot WebSocket API client (wss://ws-api.binance.com).
# Use this for User Data Stream subscriptions after the listenKey REST endpoints were removed.
class WebSocketApi < Faye::WebSocket::Client
class Error < StandardError; end

USER_DATA_EVENTS = %i[
outboundAccountPosition
balanceUpdate
executionReport
listStatus
listenKeyExpired
eventStreamTerminated
externalLockUpdate
].freeze

def initialize(on_open: nil, on_close: nil, api_key: nil, api_secret_key: nil)
wss_uri = if ENV["BINANCE_TEST_NET_ENABLE"]
"wss://ws-api.testnet.binance.vision/ws-api/v3"
else
"wss://ws-api.binance.com:443/ws-api/v3"
end

super wss_uri, nil, ping: 180

@api_key = api_key
@api_secret_key = api_secret_key
@user_data_handler = nil
@subscribe_on_open = false
@subscribe_options = {}
@subscription_id = nil

on :open do |event|
send_user_data_subscribe! if @subscribe_on_open
on_open&.call(event)
end

on :message do |event|
process_data(event.data)
end

on :close do |event|
on_close&.call(event)
end
end

# Subscribe to Spot user data events via userDataStream.subscribe.signature.
# Works with HMAC, RSA, and Ed25519 API keys (HMAC signing used by this gem).
#
# Yields |subscription_id, event_data| for each user data event.
def user_data_stream!(api_key: nil, api_secret_key: nil, recv_window: nil, &on_receive)
raise ArgumentError, "block required" unless on_receive

@user_data_handler = on_receive
@subscribe_options = {
api_key: api_key,
api_secret_key: api_secret_key,
recv_window: recv_window,
}

if ready_state == Faye::WebSocket::API::OPEN
send_user_data_subscribe!
else
@subscribe_on_open = true
end
end

def user_data_unsubscribe!(subscription_id: nil)
params = {}
params[:subscriptionId] = subscription_id unless subscription_id.nil?
request("userDataStream.unsubscribe", params)
end

private

def send_user_data_subscribe!
@subscribe_on_open = false
options = @subscribe_options || {}
api_key = options[:api_key] || @api_key || Api::Configuration.api_key
api_secret_key = options[:api_secret_key] || @api_secret_key

raise Error.new("API key is required for user data stream") if api_key.nil? || api_key.empty?

params = {
apiKey: api_key,
timestamp: Api::Configuration.timestamp.to_i,
}
params[:recvWindow] = options[:recv_window] unless options[:recv_window].nil?

signed = Api::Configuration.signed_ws_api_params(params: params, api_secret_key: api_secret_key)
request("userDataStream.subscribe.signature", signed)
end

def request(method, params = {})
payload = {
id: SecureRandom.uuid,
method: method,
}
payload[:params] = params unless params.nil? || params.empty?
send(payload.to_json)
end

def process_data(data)
json = JSON.parse(data, symbolize_names: true)

if json.key?(:event)
dispatch_user_event(json[:subscriptionId], json[:event])
elsif json.key?(:status)
process_response(json)
end
end

def process_response(json)
if json[:status] != 200
error = json[:error] || {}
raise Error.new("(#{error[:code]}) #{error[:msg]}")
end

subscription_id = json.dig(:result, :subscriptionId)
@subscription_id = subscription_id unless subscription_id.nil?
end

def dispatch_user_event(subscription_id, event)
return unless event.is_a?(Hash)

event_type = event[:e]&.to_sym
return unless event_type.nil? || USER_DATA_EVENTS.include?(event_type)

@user_data_handler&.call(subscription_id, event)
end
end
end
17 changes: 17 additions & 0 deletions spec/binance/api/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,21 @@
subject { Binance::Api::Configuration.secret_key }
it { is_expected.to eq('456') }
end

describe '.signed_ws_api_params' do
before { Binance::Api::Configuration.secret_key = 'NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j' }

it 'returns params with an HMAC signature over alphabetically sorted keys' do
params = {
timestamp: 1645423376532,
apiKey: 'vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A',
}

signed = Binance::Api::Configuration.signed_ws_api_params(params: params)
payload = "apiKey=#{params[:apiKey]}&timestamp=#{params[:timestamp]}"
expect(signed[:signature]).to eq(
Binance::Api::Configuration.signed_request_signature(payload: payload)
)
end
end
end
Loading