bitx 0.0.1 → 0.2.2

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
  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