From 58607c08d6f2e0f6ca673dd840eba651730743f3 Mon Sep 17 00:00:00 2001 From: Barnabas Debreczeni Date: Mon, 18 Jul 2016 18:53:01 +0200 Subject: [PATCH 01/10] add DepositMethods API call --- lib/kraken_ruby/client.rb | 5 +++++ spec/client_spec.rb | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/lib/kraken_ruby/client.rb b/lib/kraken_ruby/client.rb index 4f2f37c..acb3fed 100644 --- a/lib/kraken_ruby/client.rb +++ b/lib/kraken_ruby/client.rb @@ -110,6 +110,11 @@ def trade_volume(asset_pairs) post_private 'TradeVolume', opts end + def deposit_methods(asset, opts={}) + opts['asset'] = asset + post_private 'DepositMethods', opts + end + #### Private User Trading #### def add_order(opts={}) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index a189beb..87ce192 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -62,6 +62,13 @@ nonce = kraken.send :nonce expect(nonce.to_i.size).to eq(8) end + + it "gets deposit methods" do + result = kraken.deposit_methods("XXBT").first + expect(result).to have_key 'method' + expect(result).to have_key 'limit' + expect(result).to have_key 'fee' + end end end From 335b27418105cb97f68252de77e2fba5f330cc50 Mon Sep 17 00:00:00 2001 From: Barnabas Debreczeni Date: Mon, 18 Jul 2016 19:34:25 +0200 Subject: [PATCH 02/10] add DepositStatus API call --- lib/kraken_ruby/client.rb | 4 ++++ spec/client_spec.rb | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/kraken_ruby/client.rb b/lib/kraken_ruby/client.rb index acb3fed..4814904 100644 --- a/lib/kraken_ruby/client.rb +++ b/lib/kraken_ruby/client.rb @@ -115,6 +115,10 @@ def deposit_methods(asset, opts={}) post_private 'DepositMethods', opts end + def deposit_status(opts={}) + post_private 'DepositStatus', opts + end + #### Private User Trading #### def add_order(opts={}) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 87ce192..64ba63d 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -69,6 +69,22 @@ expect(result).to have_key 'limit' expect(result).to have_key 'fee' end + + it "gets deposit status" do + results = kraken.deposit_status(asset: "XXBT") + expect(results).to be_instance_of(Array) + if result = results.first + expect(result).to have_key 'method' + expect(result).to have_key 'aclass' + expect(result).to have_key 'refid' + expect(result).to have_key 'txid' + expect(result).to have_key 'info' + expect(result).to have_key 'amount' + expect(result).to have_key 'fee' + expect(result).to have_key 'status' + expect(result).to have_key 'time' + end + end end end From 8269aae5b5467260f4022c8629a811a00996bc16 Mon Sep 17 00:00:00 2001 From: Barnabas Debreczeni Date: Mon, 18 Jul 2016 20:00:36 +0200 Subject: [PATCH 03/10] add WithdrawStatus API call --- lib/kraken_ruby/client.rb | 4 ++++ spec/client_spec.rb | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/kraken_ruby/client.rb b/lib/kraken_ruby/client.rb index 4814904..02b7c71 100644 --- a/lib/kraken_ruby/client.rb +++ b/lib/kraken_ruby/client.rb @@ -119,6 +119,10 @@ def deposit_status(opts={}) post_private 'DepositStatus', opts end + def withdraw_status(opts={}) + post_private 'WithdrawStatus', opts + end + #### Private User Trading #### def add_order(opts={}) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 64ba63d..f21586c 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -85,6 +85,22 @@ expect(result).to have_key 'time' end end - end + it "gets withdraw status" do + results = kraken.withdraw_status(asset: "XXBT") + pp results + expect(results).to be_instance_of(Array) + if result = results.first + expect(result).to have_key 'method' + expect(result).to have_key 'aclass' + expect(result).to have_key 'refid' + expect(result).to have_key 'txid' + expect(result).to have_key 'info' + expect(result).to have_key 'amount' + expect(result).to have_key 'fee' + expect(result).to have_key 'status' + expect(result).to have_key 'time' + end + end + end end From 2333cacd04c7369eef2f8079202e30d92f01415e Mon Sep 17 00:00:00 2001 From: David Debreczeni Date: Fri, 29 Jul 2016 19:09:18 +0200 Subject: [PATCH 04/10] fix EAPI:Invalid nonce by using the most fine-grained time unit available from Time.now instead of using a random number --- lib/kraken_ruby/client.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/kraken_ruby/client.rb b/lib/kraken_ruby/client.rb index 02b7c71..73e9ad9 100644 --- a/lib/kraken_ruby/client.rb +++ b/lib/kraken_ruby/client.rb @@ -160,14 +160,11 @@ def post_private(method, opts={}) r['error'].empty? ? r['result'] : r['error'] end - # Generate a 64-bit nonce where the 48 high bits come directly from the current - # timestamp and the low 16 bits are pseudorandom. We can't use a pure [P]RNG here - # because the Kraken API requires every request within a given session to use a - # monotonically increasing nonce value. This approach splits the difference. + # Generate a 61-bit nonce + # 51 bits would be enough but we padded with 10 bits + # so existing users won't have to create a new API key after an update def nonce - high_bits = (Time.now.to_f * 10000).to_i << 16 - low_bits = SecureRandom.random_number(2 ** 16) & 0xffff - (high_bits | low_bits).to_s + ((Time.now.to_f * 1000000).to_i << 10).to_s end def encode_options(opts) From 653fe92dcca95861bb7033440597f7890030993b Mon Sep 17 00:00:00 2001 From: David Debreczeni Date: Fri, 29 Jul 2016 19:23:23 +0200 Subject: [PATCH 05/10] whtspc commit, use spaces instead of tabs --- spec/client_spec.rb | 98 ++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index f21586c..45b4665 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -2,61 +2,61 @@ describe Kraken::Client do - # YOU MUST SET ENVIRONMENT VARIABLES KRAKEN_API_KEY AND + # YOU MUST SET ENVIRONMENT VARIABLES KRAKEN_API_KEY AND # KRAKEN_API_SECRET TO TEST PRIVATE DATA QUERIES. PRIVATE # TESTS WILL FAIL OTHERWISE. API_KEY = ENV['KRAKEN_API_KEY'] API_SECRET = ENV['KRAKEN_API_SECRET'] - before :each do - sleep 0.3 # to prevent rapidly pinging the Kraken server - end - - let(:kraken){Kraken::Client.new(API_KEY, API_SECRET)} - - context "public data" do - it "gets the proper server time" do - kraken_time = DateTime.parse(kraken.server_time.rfc1123) - utc_time = Time.now.getutc - expect(kraken_time.day).to eq utc_time.day - expect(kraken_time.hour).to eq utc_time.hour - end - - it "gets list of tradeable assets" do - expect(kraken.assets).to respond_to :XXBT - end - - it "gets list of asset pairs" do - expect(kraken.asset_pairs).to respond_to :XXBTZEUR - end - - it "gets public ticker data for given asset pairs" do - result = kraken.ticker('XXBTZEUR, XXBTZGBP') - expect(result).to respond_to :XXBTZEUR - expect(result).to respond_to :XXBTZGBP - end - - it "gets order book data for a given asset pair" do - order_book = kraken.order_book('XXBTZEUR') - expect(order_book.XXBTZEUR).to respond_to :asks - end - - it "gets an array of trades data for a given asset pair" do - trades = kraken.trades('XXBTZEUR') - expect(trades.XXBTZEUR).to be_instance_of(Array) - end - - it "gets an array of spread data for a given asset pair" do - spread = kraken.spread('XXBTZEUR') - expect(spread.XXBTZEUR).to be_instance_of(Array) - end - end - - context "private data" do # More tests to come - it "gets the user's balance" do - expect(kraken.balance).to be_instance_of(Hash) - end + before :each do + sleep 0.3 # to prevent rapidly pinging the Kraken server + end + + let(:kraken){Kraken::Client.new(API_KEY, API_SECRET)} + + context "public data" do + it "gets the proper server time" do + kraken_time = DateTime.parse(kraken.server_time.rfc1123) + utc_time = Time.now.getutc + expect(kraken_time.day).to eq utc_time.day + expect(kraken_time.hour).to eq utc_time.hour + end + + it "gets list of tradeable assets" do + expect(kraken.assets).to respond_to :XXBT + end + + it "gets list of asset pairs" do + expect(kraken.asset_pairs).to respond_to :XXBTZEUR + end + + it "gets public ticker data for given asset pairs" do + result = kraken.ticker('XXBTZEUR, XXBTZGBP') + expect(result).to respond_to :XXBTZEUR + expect(result).to respond_to :XXBTZGBP + end + + it "gets order book data for a given asset pair" do + order_book = kraken.order_book('XXBTZEUR') + expect(order_book.XXBTZEUR).to respond_to :asks + end + + it "gets an array of trades data for a given asset pair" do + trades = kraken.trades('XXBTZEUR') + expect(trades.XXBTZEUR).to be_instance_of(Array) + end + + it "gets an array of spread data for a given asset pair" do + spread = kraken.spread('XXBTZEUR') + expect(spread.XXBTZEUR).to be_instance_of(Array) + end + end + + context "private data" do # More tests to come + it "gets the user's balance" do + expect(kraken.balance).to be_instance_of(Hash) + end it "uses a 64 bit nonce" do nonce = kraken.send :nonce From b26a065829db69609fc0846da417442aaa6f69b3 Mon Sep 17 00:00:00 2001 From: David Debreczeni Date: Fri, 29 Jul 2016 19:46:58 +0200 Subject: [PATCH 06/10] add readme for newly added deposits and withdrawal endpoint --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 304761e..8c03735 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,41 @@ asset_pairs = 'XLTCXXDG, ZEURXXDG' volume = kraken.query_ledgers(asset_pairs) ``` +#### Get deposit methods + +**Input:** asset being deposited + +```ruby +asset = 'XXBT' +deposit_methods = kraken.deposit_methods(asset) +``` + +#### Get status of recent deposits + +**Input:** options hash: asset: asset being deposited, method: name of the deposit method (optional) + +```ruby +opts = { + asset: 'XXBT', + method: 'Bitcoin' # Optional +} +deposit_status = kraken.deposit_status(opts) +``` + +#### Get status of recent withdrawals + +**Important**: due to a bug in Kraken's API, this endpoint requires an API token that has "Withdraw funds" Key Permission instead of "Query funds", otherwise an "EGeneral:Permission denied" Error will be raised. + +**Input:** options hash: asset: asset being withdrawn, method: withdrawal method name (optional) + +```ruby +opts = { + asset: 'XXBT', + method: 'Bitcoin' # Optional +} +withdraw_status = kraken.withdraw_status(opts) +``` + ### Adding and Cancelling Orders #### Add Order From 622806f5fc11ed5aed34e172affc2ecb8443a0dd Mon Sep 17 00:00:00 2001 From: David Debreczeni Date: Fri, 29 Jul 2016 19:59:48 +0200 Subject: [PATCH 07/10] remove pp from specs --- spec/client_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 45b4665..85290df 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -88,7 +88,6 @@ it "gets withdraw status" do results = kraken.withdraw_status(asset: "XXBT") - pp results expect(results).to be_instance_of(Array) if result = results.first expect(result).to have_key 'method' From 3e7b5b2bf39bb14cd7c3c3fb748542110196373c Mon Sep 17 00:00:00 2001 From: David Debreczeni Date: Fri, 5 Aug 2016 17:14:59 +0200 Subject: [PATCH 08/10] Retry on 'EAPI:Invalid nonce' error up to ENV['KRAKEN_API_RETRIES'] or 5 times --- lib/kraken_ruby/client.rb | 39 ++++++++++++++++++++++++++++++++++++--- spec/client_spec.rb | 20 +++++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/lib/kraken_ruby/client.rb b/lib/kraken_ruby/client.rb index 73e9ad9..94663b9 100644 --- a/lib/kraken_ruby/client.rb +++ b/lib/kraken_ruby/client.rb @@ -8,6 +8,11 @@ module Kraken class Client include HTTParty + RETRIES = (ENV['KRAKEN_API_RETRIES'] || 5).to_i + ERRORS = { + invalid_nonce: 'EAPI:Invalid nonce' + } + def initialize(api_key=nil, api_secret=nil, options={}) @api_key = api_key @api_secret = api_secret @@ -146,7 +151,7 @@ def cancel_order(txid) private - def post_private(method, opts={}) + def post_private_request(method, opts) opts['nonce'] = nonce post_data = encode_options(opts) @@ -156,8 +161,36 @@ def post_private(method, opts={}) } url = @base_uri + url_path(method) - r = self.class.post(url, { headers: headers, body: post_data }).parsed_response - r['error'].empty? ? r['result'] : r['error'] + self.class.post(url, { headers: headers, body: post_data }).parsed_response + end + + def post_private(method, opts={}) + tries = 1 + + while tries <= RETRIES + + response = post_private_request(method, opts) + + if response['error'].nil? || response['error'].empty? + return response['result'] + else + error = response['error'] + + # Contrary to their documentation, Kraken sometimes sends the error message as a String instead of an Array + # We keep using an array for compatibility + error = [error] if error.is_a?(String) + + case error.first + when ERRORS[:invalid_nonce] + return error if tries >= RETRIES + + tries += 1 + sleep tries # Prevent Kraken's "EGeneral:Temporary lockout" error message + else + return error + end + end + end end # Generate a 61-bit nonce diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 85290df..1779b50 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -10,7 +10,7 @@ API_SECRET = ENV['KRAKEN_API_SECRET'] before :each do - sleep 0.3 # to prevent rapidly pinging the Kraken server + sleep 3 # to prevent Kraken's "EGeneral:Temporary lockout" error end let(:kraken){Kraken::Client.new(API_KEY, API_SECRET)} @@ -63,6 +63,24 @@ expect(nonce.to_i.size).to eq(8) end + it "retries up to 5 times if nonce is invalid" do + expect(kraken).to receive(:post_private_request) + .exactly(Kraken::Client::RETRIES).times + .and_return('error' => [Kraken::Client::ERRORS[:invalid_nonce]]) + + expect(kraken.balance).to eq([Kraken::Client::ERRORS[:invalid_nonce]]) + + # when it works for the 5th time + + error = {'error' => [Kraken::Client::ERRORS[:invalid_nonce]]} + success = {'result' => 'success'} + + expect(kraken).to receive(:post_private_request) + .and_return(error, error, error, error, success) + + expect(kraken.balance).to eq('success') + end + it "gets deposit methods" do result = kraken.deposit_methods("XXBT").first expect(result).to have_key 'method' From d0c341bf87a3c73d1d02f46a4958f365e12b2b6d Mon Sep 17 00:00:00 2001 From: David Debreczeni Date: Mon, 5 Sep 2016 13:55:59 +0200 Subject: [PATCH 09/10] handle and retry when Kraken is partly down and responds with nil or the string "error" instead of a hash --- lib/kraken_ruby/client.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/kraken_ruby/client.rb b/lib/kraken_ruby/client.rb index 94663b9..f20ecd0 100644 --- a/lib/kraken_ruby/client.rb +++ b/lib/kraken_ruby/client.rb @@ -171,6 +171,12 @@ def post_private(method, opts={}) response = post_private_request(method, opts) + if response.nil? or response.is_a?(String) + tries += 1 + sleep tries + next + end + if response['error'].nil? || response['error'].empty? return response['result'] else @@ -191,6 +197,8 @@ def post_private(method, opts={}) end end end + + ["UnreliableKrakenAPIError: Tried 5 times without success. Request method: #{method} opts: #{opts} Last reponse: #{response.inspect}"] end # Generate a 61-bit nonce From a92bd288e9cfd967fe47845eff59ae7305d4bed1 Mon Sep 17 00:00:00 2001 From: David Debreczeni Date: Wed, 15 Nov 2017 17:00:59 +0100 Subject: [PATCH 10/10] Kraken errors can not be trusted, we must not retry on general errors! An error can mean a successful request in Kraken land! We don't want to retry general errors without checking first if they ended up being carried through on their servers. Only retry a specific nonce error to (which again must a kraken bug, but kraken is just being kraken!) --- lib/kraken_ruby/client.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/kraken_ruby/client.rb b/lib/kraken_ruby/client.rb index f20ecd0..efae6e1 100644 --- a/lib/kraken_ruby/client.rb +++ b/lib/kraken_ruby/client.rb @@ -171,12 +171,6 @@ def post_private(method, opts={}) response = post_private_request(method, opts) - if response.nil? or response.is_a?(String) - tries += 1 - sleep tries - next - end - if response['error'].nil? || response['error'].empty? return response['result'] else