Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add deposit and withdraw endpoints and fix nonce issues #20

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 55 additions & 10 deletions lib/kraken_ruby/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -110,6 +115,19 @@ def trade_volume(asset_pairs)
post_private 'TradeVolume', opts
end

def deposit_methods(asset, opts={})
opts['asset'] = asset
post_private 'DepositMethods', opts
end

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={})
Expand All @@ -133,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)

Expand All @@ -143,18 +161,45 @@ 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

["UnreliableKrakenAPIError: Tried 5 times without success. Request method: #{method} opts: #{opts} Last reponse: #{response.inspect}"]
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)
Expand Down
156 changes: 106 additions & 50 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,122 @@

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 3 # to prevent Kraken's "EGeneral:Temporary lockout" error
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
expect(nonce.to_i.size).to eq(8)
end
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'
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

it "gets withdraw status" do
results = kraken.withdraw_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