bitx 0.0.1 → 0.2.2

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
  SHA1:
3
- metadata.gz: 13e2a9513a9793a95cc11fd1825af4c4ba821bf9
4
- data.tar.gz: 7670ffa8ccc8785f061c4668a76705593eead385
3
+ metadata.gz: 8ac15c7b8200f1a0e442f57519cd1c4260f75ede
4
+ data.tar.gz: d10b5772fc1e4007d259dee8e90aa9deb6d23c50
5
5
  SHA512:
6
- metadata.gz: d999d9f6c64075e20a20844f6a68af0fa5f590fef835d69972696af90884e3cf7da1369e35fd6d1d5f690333b9ab40d2caf0fd47318d2d733605cd951e2ac461
7
- data.tar.gz: 21dc0f53de29a64685022567a7da2947f1d01b1d1244b0f8de6d408a6cd90b90fe3eefbc8d3436be9e6fbed56283dc9515baa7b92b7f5a1ff4de06cc346c92cf
6
+ metadata.gz: 0773b9dc8addb3b2e91cde75a5667570abdc323f7411e5f5fb719f4ecc23977cbc92413e3ef76129882f6a240e7826f4249a5c62dd8657a3b5a31d10efe7fc96
7
+ data.tar.gz: 8ddebbce2e27a8942e1dd9a45bd7ab26def4a9c40918d3b9b258c54fb2f772ebfeedcdcd8f786e1a05abd1d973779a4fdb1e240b026d84cbef4d62d71d414127
data/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  Ruby wrapper for the BitX API.
4
4
 
5
- Currently the public API (ticker, orderbook, trades) is wrapped.
6
- TODO: Add the private API.
5
+ ## Rate limits
6
+
7
+ If rate limits are exceeded BitX will return a 503 error. Make sure your code handles that appropriately.
7
8
 
8
9
  ## Installation
9
10
 
@@ -24,14 +25,75 @@ Or install it yourself as:
24
25
  ```
25
26
  require 'bitx'
26
27
 
28
+ ```
29
+ Public API usage
30
+
31
+ ```
32
+
27
33
  # Fetch the ticker
28
- BitX.new.ticker('XBTZAR')
34
+ BitX.ticker('XBTZAR')
29
35
 
30
36
  # Fetch the order book
31
- BitX.new.orderbook('XBTZAR')
37
+ BitX.orderbook('XBTZAR')
32
38
 
33
39
  # Fetch the latest trades
34
- BitX.new.trades('XBTZAR')
40
+ BitX.trades('XBTZAR')
41
+
42
+ ```
43
+
44
+ Private API usage
45
+
46
+ ```
47
+ # In a configure block somewhere in your app init:
48
+ BitX.configure do |config|
49
+ config.api_key_secret = 'yoursecretkeyfrombitx'
50
+ config.api_key_id = 'yourapiidfrombitx'
51
+ config.api_key_pin = 'yourapikeypinfrombitx'
52
+ end
53
+
54
+ # Your Bitcoin balance
55
+ BitX.balance_for 'XBT'
56
+
57
+ # Your Rand balance
58
+ BitX.balance_for 'ZAR'
59
+
60
+ # List your orders trading Bitcoin for Rand
61
+ BitX.list_orders 'XBTZAR'
62
+
63
+ # Place a new order
64
+ # BitX::ORDERTYPE_BID / BitX::ORDERTYPE_ASK
65
+ volume = '0.01'
66
+ price = '10000'
67
+ BitX.post_order(BitX::ORDERTYPE_BID, volume, price, 'XBTZAR')
68
+
69
+
70
+ #alternatively, if you need to change the api_key during the program you can pass options to the private methods specifying the :api_key_secret and :api_key_id
71
+ BitX.balance_for('XBT', {api_key_secret: 'yoursecretkeyfrombitx', api_key_id: 'yourapiidfrombitx'})
72
+
73
+
74
+
75
+
76
+ ```
77
+
78
+
79
+ Connection object
80
+
81
+ ```
82
+ # if you need to access the BitX api with different credentials in a concurrent system, you can use the BitX::Connection object
83
+
84
+ #if config changes:
85
+ bit_x = BitX::Connection.new() do |config|
86
+ config.api_key_secret = 'yoursecretkeyfrombitx'
87
+ config.api_key_id = 'yourapiidfrombitx'
88
+ config.api_key_pin = 'yourapikeypinfrombitx'
89
+ end
90
+ bit_x.balance
91
+
92
+ #if you want to use a mocked connection
93
+ bitx = BitX::Connection.new(stubbed_connection)
94
+ bitx.tickers
95
+
96
+
35
97
  ```
36
98
 
37
99
  ## Contributing
@@ -41,3 +103,28 @@ BitX.new.trades('XBTZAR')
41
103
  3. Commit your changes (`git commit -am 'Add some feature'`)
42
104
  4. Push to the branch (`git push origin my-new-feature`)
43
105
  5. Create new Pull Request
106
+
107
+
108
+ ## Changelog
109
+
110
+ ```
111
+ # 0.2.1 - force place order to specify price in integers
112
+ # 0.2.0 - adds a connection object to support concurrent systems where the connection or configuration objects may change
113
+ # 0.1.0 - adds a number of methods. introduces some breaking changes to existing methods.
114
+ add support for public method *tickers* to get all bitx tickers
115
+ add volume to *ticker* response to be the 24_hour_rolling_volume
116
+ add timestamp to *orderbook* response
117
+ deprecate *balance_for*
118
+ add *balance* to return a list of accounts with balances
119
+ add *new_receive_address*
120
+ add *received_by_address*
121
+ modify *funding_address*
122
+ deprecate *funding_address*
123
+ add *send* method
124
+ add *api_key_pin* config option
125
+ add *create_quote*
126
+ add *exercise_quote*
127
+ add *discard_quote*
128
+ add *view_quote*
129
+ add basic tests
130
+ ```
data/Rakefile CHANGED
@@ -1 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/*_test.rb']
7
+ t.verbose = true
8
+ end
@@ -2,14 +2,16 @@
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
+ require_relative 'lib/version'
6
+
5
7
  Gem::Specification.new do |spec|
6
8
  spec.name = "bitx"
7
- spec.version = '0.0.1'
8
- spec.authors = ["Timothy Stranex"]
9
- spec.email = ["timothy@switchless.com"]
9
+ spec.version = BitX::VERSION::STRING.dup
10
+ spec.authors = ["Timothy Stranex","Francois Paul"]
11
+ spec.email = ["timothy@bitx.co","franc@bitx.co"]
10
12
  spec.description = 'BitX API wrapper'
11
13
  spec.summary = 'Ruby wrapper for the BitX API'
12
- spec.homepage = "https://mybitx.com/api"
14
+ spec.homepage = "https://bitx.co/api"
13
15
  spec.license = "MIT"
14
16
 
15
17
  spec.files = `git ls-files`.split($/)
@@ -19,5 +21,6 @@ Gem::Specification.new do |spec|
19
21
 
20
22
  spec.add_development_dependency "bundler", "~> 1.3"
21
23
  spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "minitest"
22
25
  spec.add_runtime_dependency 'faraday'
23
26
  end
@@ -1,75 +1,63 @@
1
1
  require 'faraday'
2
2
  require 'json'
3
3
  require 'bigdecimal'
4
+ require_relative 'public_api'
5
+ require_relative 'private_api'
6
+ require_relative 'version'
4
7
 
5
- class BitX
6
-
7
- class Error < StandardError
8
+ module BitX
9
+ class Configuration
10
+ attr_accessor :api_key_id, :api_key_secret, :api_key_pin
8
11
  end
9
12
 
10
- def ticker(pair)
11
- t = get('/api/1/ticker', {pair: pair})
12
- {
13
- pair: pair,
14
- timestamp: Time.at(t["timestamp"].to_i/1000),
15
- ask: BigDecimal(t["ask"]),
16
- bid: BigDecimal(t["bid"]),
17
- last: BigDecimal(t["last_trade"]),
18
- }
13
+ def self.configuration
14
+ @configuration ||= Configuration.new
19
15
  end
20
16
 
21
- def orderbook(pair)
22
- t = get('/api/1/orderbook', {pair: pair})
17
+ def self.configure
18
+ yield(configuration) if block_given?
19
+ end
23
20
 
24
- bids = []
25
- t['bids'].each do |o|
26
- bids << {
27
- price: BigDecimal(o['price']),
28
- volume: BigDecimal(o['volume'])
29
- }
30
- end
21
+ class Error < StandardError
22
+ end
31
23
 
32
- asks = []
33
- t['asks'].each do |o|
34
- asks << {
35
- price: BigDecimal(o['price']),
36
- volume: BigDecimal(o['volume'])
37
- }
38
- end
24
+ extend PublicApi
25
+ extend PrivateApi
39
26
 
40
- return {bids: bids, asks: asks}
27
+ def self.set_conn(conn=nil)
28
+ @conn = conn || BitX.conn
41
29
  end
42
30
 
43
- def trades(pair)
44
- t = get('/api/1/trades', {pair: pair})
45
- trades = []
46
- t['trades'].each do |trade|
47
- trades << {
48
- timestamp: Time.at(trade['timestamp'].to_i/1000),
49
- price: BigDecimal(trade['price']),
50
- volume: BigDecimal(trade['volume'])
51
- }
52
- end
53
- trades
31
+ def self.conn
32
+ return @conn if @conn
33
+ @conn = Faraday.new(url: 'https://api.mybitx.com')
34
+ @conn.headers[:user_agent] = "bitx-ruby/#{BitX::VERSION::STRING}"
35
+ @conn
54
36
  end
55
37
 
56
- private
57
38
 
58
- def conn
59
- conn = Faraday.new(url: 'https://bitx.co.za')
60
- conn.headers[:user_agent] = "bitx-ruby/0.0.1"
61
- conn
62
- end
39
+ #connection object to be used in concurrent systems where connections and configurations might differ
40
+ class Connection
41
+ include PublicApi
42
+ include PrivateApi
43
+ attr_accessor :configuration, :conn
44
+
45
+ def initialize(connection=nil)
46
+ @conn = connection || BitX.conn
47
+ @configuration = BitX.configuration
48
+ yield(@configuration) if block_given?
49
+ end
63
50
 
64
- def get(url, params)
65
- r = conn.get(url, params)
66
- if r.status != 200
67
- raise BitX::Error.new("BitX error: #{r.status}")
51
+ def self.conn
52
+ @conn
68
53
  end
69
- t = JSON.parse r.body
70
- if t['error']
71
- raise BitX::Error.new('BitX error: ' + t['error'])
54
+
55
+ def self.get(url, params=nil)
56
+ get(url, params)
57
+ end
58
+
59
+ def self.configuration
60
+ @configuration
72
61
  end
73
- t
74
62
  end
75
63
  end
File without changes
@@ -0,0 +1,289 @@
1
+ module PrivateApi
2
+
3
+ class BitXError < StandardError
4
+ end
5
+
6
+ # <----- ORDERS .
7
+
8
+ #List Orders
9
+ def list_orders(pair, opt={})
10
+ opt.merge!({pair: pair})
11
+ path = '/api/1/listorders'
12
+ r = authed_request(path, {params: opt})
13
+ j = JSON.parse(r.body, {symbolize_names: true})
14
+
15
+ ol = []
16
+ if j[:orders]
17
+ j[:orders].each do |bo|
18
+ ol << format_order_hash(bo)
19
+ end
20
+ end
21
+ ol
22
+ end
23
+
24
+ #Post Order
25
+ #order_type 'BID'/'ASK'
26
+ def post_order(order_type, volume, price, pair, opt={})
27
+ params = {
28
+ pair: pair,
29
+ type: order_type,
30
+ volume: volume.to_d.round(6).to_f.to_s,
31
+ price: price.to_f.round.to_s
32
+ }
33
+ opt.merge!({params: params, method: :post})
34
+ path = '/api/1/postorder'
35
+ r = authed_request(path, opt)
36
+ j = JSON.parse(r.body, {symbolize_names: true})
37
+ raise BitXError.new("BitX postorder error: #{j['error']}") if j['error']
38
+ j
39
+ end
40
+
41
+ #Stop Order
42
+ def stop_order(order_id, opt={})
43
+ options = {params: opt.merge!({order_id: order_id}), method: :post}
44
+ path = '/api/1/stoporder'
45
+ r = authed_request(path, options)
46
+ JSON.parse(r.body, {symbolize_names: true})
47
+ end
48
+
49
+ #Get Order
50
+ def get_order(order_id, opt={})
51
+ path = "/api/1/orders/#{order_id}"
52
+ r = authed_request(path, {params: opt})
53
+ o = JSON.parse(r.body, {symbolize_names: true})
54
+
55
+ format_order_hash(o)
56
+ end
57
+
58
+ def format_order_hash(o)
59
+ {
60
+ completed: o[:state] == 'COMPLETE',
61
+ state: o[:state],
62
+ created_at: Time.at(o[:creation_timestamp].to_i/1000),
63
+ expires_at: Time.at(o[:expiration_timestamp].to_i/1000),
64
+ order_id: o[:order_id],
65
+ limit_price: BigDecimal(o[:limit_price]),
66
+ limit_volume: BigDecimal(o[:limit_volume]),
67
+ base: BigDecimal(o[:base]),
68
+ fee_base: BigDecimal(o[:fee_base]),
69
+ counter: BigDecimal(o[:counter]),
70
+ fee_counter: BigDecimal(o[:fee_counter]),
71
+ type: o[:type].to_sym
72
+ }
73
+ end
74
+
75
+ # ORDERS -----/>
76
+
77
+
78
+
79
+
80
+ # <----- BALANCE .
81
+
82
+ #Balance
83
+ def balance_for(asset='XBT', opt={})
84
+ puts "BitX.balance_for will be deprecated. please use BitX.balance to get a list of account balances"
85
+ opt.merge!({asset: asset})
86
+ path = '/api/1/balance'
87
+ r = authed_request(path, {params: opt})
88
+ j = JSON.parse(r.body)
89
+ raise BitXError.new("BitX balance error: #{j['error']}") if j['error']
90
+ balance = BigDecimal(j['balance'][0]['balance'])
91
+ reserved = BigDecimal(j['balance'][0]['reserved'])
92
+ {
93
+ asset: asset,
94
+ balance: balance,
95
+ reserved: reserved,
96
+ available: balance - reserved
97
+ }
98
+ end
99
+
100
+ def balance(opt={})
101
+ path = '/api/1/balance'
102
+ r = authed_request(path, opt)
103
+ j = JSON.parse(r.body)
104
+
105
+ balances = []
106
+ if j['balance']
107
+ j['balance'].each do |b|
108
+ balance = BigDecimal(b['balance'])
109
+ reserved = BigDecimal(b['reserved'])
110
+ balances << {
111
+ account_id: b['account_id'],
112
+ asset: b['asset'],
113
+ balance: balance,
114
+ reserved: reserved,
115
+ available: balance - reserved,
116
+ unconfirmed: BigDecimal(b['unconfirmed'])
117
+ }
118
+ end
119
+ end
120
+ balances
121
+ end
122
+
123
+ # BALANCE -----/>
124
+
125
+
126
+
127
+ # <----- receive addresses .
128
+ def new_receive_address(opt={})
129
+ receive_address_request(opt, :post)
130
+ end
131
+
132
+ # if you have multiple XBT accounts you can specify an `address` option with the funding address
133
+ def received_by_address(opt={})
134
+ receive_address_request(opt, :get)
135
+ end
136
+
137
+ def receive_address_request(opt, method)
138
+ opt.merge!({asset: 'XBT'})
139
+ path = '/api/1/funding_address'
140
+ r = authed_request(path, {params: opt, method: method})
141
+ JSON.parse(r.body, {symbolize_names: true})
142
+ end
143
+
144
+ #Bitcoin Funding Address
145
+ def funding_address(asset='XBT', opt={})
146
+ puts "BitX.funding_address will be deprecated. please use BitX.received_by_address with a specified :address option to get details for that address"
147
+ opt.merge!({asset: asset})
148
+ receive_address_request(opt, :get)
149
+ end
150
+ # receive addresses -----/>
151
+
152
+
153
+ # <----- withdrawal requests .
154
+ def withdrawals
155
+ path = '/api/1/withdrawals'
156
+ r = authed_request(path, {params: {}, method: :get})
157
+ j = JSON.parse(r.body, {symbolize_names: true})
158
+
159
+ return j[:withdrawals] if j[:withdrawals]
160
+ return j
161
+ end
162
+
163
+ def withdraw(type, amount)
164
+ # valid types
165
+ # ZAR_EFT,NAD_EFT,KES_MPESA,MYR_IBG,IDR_LLG
166
+
167
+ path = '/api/1/withdrawals'
168
+ r = authed_request(path, {params: {type: type, amount: amount}, method: :post})
169
+ JSON.parse(r.body, {symbolize_names: true})
170
+ end
171
+
172
+ def withdrawal(withdrawal_id)
173
+ path = "/api/1/withdrawals/#{withdrawal_id}"
174
+ r = authed_request(path, {params: {}, method: :get})
175
+ JSON.parse(r.body, {symbolize_names: true})
176
+ end
177
+
178
+ def cancel_withdrawal(withdrawal_id)
179
+ path = "/api/1/withdrawals/#{withdrawal_id}"
180
+ r = authed_request(path, {params: {}, method: :delete})
181
+ JSON.parse(r.body, {symbolize_names: true})
182
+ end
183
+ # withdrawal requests -----/>
184
+
185
+
186
+ # <------- send .
187
+
188
+ def send(amount, address, currency='XBT', description='', message='', pin=nil)
189
+ # valid types
190
+ # ZAR_EFT,NAD_EFT,KES_MPESA,MYR_IBG,IDR_LLG
191
+
192
+ pin ||= self.configuration.api_key_pin rescue nil
193
+
194
+ path = '/api/1/send'
195
+ r = authed_request(path, {
196
+ params: {
197
+ amount: amount.to_s,
198
+ address: address,
199
+ currency: currency,
200
+ description: description,
201
+ message: message,
202
+ pin: pin
203
+ },
204
+ method: :post
205
+ })
206
+ JSON.parse(r.body, {symbolize_names: true})
207
+ end
208
+
209
+ # send ------/>
210
+
211
+
212
+ # <------- quotes .
213
+
214
+ def create_quote(pair, base_amount, type)
215
+ #POST
216
+ path = '/api/1/quotes'
217
+ r = authed_request(path, {params: {type: type, pair: pair, base_amount: base_amount}, method: :post})
218
+ extract_quote_from_body(r.body)
219
+ end
220
+
221
+ def exercise_quote(quote_id, pin=nil)
222
+ #PUT
223
+ pin ||= self.configuration.api_key_pin rescue nil
224
+ path = "/api/1/quotes/#{quote_id}"
225
+ r = authed_request(path, {
226
+ params: {
227
+ pin: pin
228
+ },
229
+ method: :put})
230
+ extract_quote_from_body(r.body)
231
+ end
232
+
233
+ def discard_quote(quote_id)
234
+ #DELETE
235
+ path = "/api/1/quotes/#{quote_id}"
236
+ r = authed_request(path, {method: :delete})
237
+ extract_quote_from_body(r.body)
238
+ end
239
+
240
+ def view_quote(quote_id)
241
+ #GET
242
+ path = "/api/1/quotes/#{quote_id}"
243
+ r = authed_request(path, {method: :get})
244
+ extract_quote_from_body(r.body)
245
+ end
246
+
247
+ def extract_quote_from_body(body)
248
+ quote = JSON.parse(body, {symbolize_names: true})
249
+ return nil unless quote
250
+ quote[:created_at] = Time.at(quote[:created_at]/1000) if quote[:created_at]
251
+ quote[:expires_at] = Time.at(quote[:expires_at]/1000) if quote[:expires_at]
252
+ quote
253
+ end
254
+
255
+ # quotes ------/>
256
+
257
+
258
+
259
+ def api_auth(opt={})
260
+ api_key_id = opt[:api_key_id] || self.configuration.api_key_id
261
+ api_key_secret = opt[:api_key_secret] || self.configuration.api_key_secret
262
+ conn = self.conn
263
+ conn.basic_auth(api_key_id, api_key_secret)
264
+ conn
265
+ end
266
+
267
+ # opt can specify the method to be :post, :put, :delete
268
+ # opt can specify request params in :params
269
+ # opt could also include the api_key_id and api_key_secret
270
+ def authed_request(path, opt={})
271
+ method = opt[:method] || :get
272
+ conn = self.api_auth(opt)
273
+ r = case method
274
+ when :get
275
+ conn.get(path, opt[:params])
276
+ when :post
277
+ conn.post(path, opt[:params])
278
+ when :put
279
+ conn.put(path, opt[:params])
280
+ when :delete
281
+ conn.delete(path, opt[:params])
282
+ else
283
+ raise BitXError.new("Request must be :post or :get")
284
+ end
285
+ raise BitXError.new("BitX #{path} error: #{r.status}") unless r.status == 200
286
+ r
287
+ end
288
+
289
+ end