coinapult 0.1

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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/coinapult.rb +388 -0
  3. metadata +59 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 55eed310a4ba498354569d110479ee5b6ff122f2
4
+ data.tar.gz: 94cb2512c70f4739a988500e0901d99476d76d09
5
+ SHA512:
6
+ metadata.gz: f0e9d88318dfca61755474118526d57eca38913c83bb74a82bdfae3b0f4e2f077d64707f3f4abd767813790cc2c1dc84e555f4f8ab5253547f9ae92adc3e872a
7
+ data.tar.gz: cfb51620fe730cd95f3dad4a84e8ecc775ec99a973c813aba7c82505a183d48ba57a110682dbb71b98defbebe7e85cdfe380236e19624088648357ac87c0cb30
data/lib/coinapult.rb ADDED
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env ruby
2
+ require 'uri'
3
+ require 'json'
4
+ require 'base64'
5
+ require 'openssl'
6
+ require 'rest_client'
7
+ require 'securerandom'
8
+
9
+ SSL_VERSION = :TLSv1_2
10
+
11
+ ECC_CURVE = 'secp256k1'
12
+ ECC_COINAPULT_PUB = "
13
+ -----BEGIN PUBLIC KEY-----
14
+ MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEWp9wd4EuLhIZNaoUgZxQztSjrbqgTT0w
15
+ LBq8RwigNE6nOOXFEoGCjGfekugjrHWHUi8ms7bcfrowpaJKqMfZXg==
16
+ -----END PUBLIC KEY-----
17
+ "
18
+ ECC_COINAPULT_PUBKEY = OpenSSL::PKey.read(ECC_COINAPULT_PUB)
19
+
20
+ class CoinapultClient
21
+ def initialize(credentials: nil, baseURL: 'https://api.coinapult.com',
22
+ ecc: nil, authmethod: nil)
23
+ @key = ''
24
+ @secret = ''
25
+ @baseURL = baseURL
26
+ @authmethod = authmethod
27
+
28
+ if credentials
29
+ @key = credentials[:key]
30
+ @secret = credentials[:secret]
31
+ end
32
+ _setup_ECC_pair([ecc[:privkey], ecc[:pubkey]]) if ecc
33
+ end
34
+
35
+ def export_ECC
36
+ [@ecc[:privkey].to_pem,
37
+ @ecc[:pubkey].to_pem]
38
+ end
39
+
40
+ def _setup_ECC_pair(keypair)
41
+ if keypair.nil?
42
+ privkey = OpenSSL::PKey::EC.new(ECC_CURVE)
43
+ privkey.generate_key
44
+ pubkey = OpenSSL::PKey::EC.new(privkey.group)
45
+ pubkey.public_key = privkey.public_key
46
+ @ecc = { privkey: privkey, pubkey: pubkey }
47
+ else
48
+ privkey, pubkey = keypair
49
+ @ecc = {
50
+ privkey: OpenSSL::PKey.read(privkey),
51
+ pubkey: OpenSSL::PKey.read(pubkey)
52
+ }
53
+ end
54
+ @ecc_pub_pem = @ecc[:pubkey].to_pem.strip
55
+ @ecc_pub_hash = OpenSSL::Digest::SHA256.hexdigest(@ecc_pub_pem)
56
+ end
57
+
58
+ # Send a generic request to Coinapult.
59
+ def _send_request(url, values, sign: false, post: true)
60
+ headers = {}
61
+
62
+ if sign
63
+ values['timestamp'] = Time.now.to_i
64
+ values['nonce'] = SecureRandom.hex(10)
65
+ values['endpoint'] = url[4..-1]
66
+ headers['cpt-key'] = @key
67
+ signdata = Base64.urlsafe_encode64(JSON.generate(values))
68
+ headers['cpt-hmac'] = OpenSSL::HMAC.hexdigest('sha512', @secret, signdata)
69
+ data = { data: signdata }
70
+ else
71
+ data = values
72
+ end
73
+ if post
74
+ response = _send(:post, "#{@baseURL}#{url}", headers, payload: data)
75
+ else
76
+ url = "#{@baseURL}#{url}"
77
+ url += "?#{URI.encode_www_form(data)}" if data.length > 0
78
+ response = _send(:get, url, headers)
79
+ end
80
+ _format_response(response)
81
+ end
82
+
83
+ def _format_response(response)
84
+ resp = JSON.parse(response)
85
+ fail CoinapultError, resp['error'] if resp['error']
86
+ resp
87
+ end
88
+
89
+ def _send(method, url, headers, payload: nil)
90
+ RestClient::Request.execute(method: method, url: url,
91
+ headers: headers, payload: payload,
92
+ ssl_version: SSL_VERSION)
93
+ end
94
+
95
+ def _send_ECC(url, values, new_account: false, sign: true)
96
+ headers = {}
97
+
98
+ if !new_account
99
+ values['nonce'] = SecureRandom.hex(10)
100
+ values['endpoint'] = url[4..-1]
101
+ headers['cpt-ecc-pub'] = @ecc_pub_hash
102
+ else
103
+ headers['cpt-ecc-new'] = Base64.urlsafe_encode64(@ecc_pub_pem)
104
+ end
105
+ values['timestamp'] = Time.now.to_i
106
+
107
+ data = Base64.urlsafe_encode64(JSON.generate(values))
108
+ headers['cpt-ecc-sign'] = generate_ECC_sign(data, @ecc[:privkey])
109
+ response = _send(:post, "#{@baseURL}#{url}", headers,
110
+ payload: { data: data })
111
+ _format_response(response)
112
+ end
113
+
114
+ def _receive_ECC(resp)
115
+ if resp['sign'].nil? || resp['data'].nil?
116
+ fail CoinapultErrorECC, 'Invalid ECC message'
117
+ end
118
+ # Check signature.
119
+ unless verify_ECC_sign(resp['sign'], resp['data'], ECC_COINAPULT_PUBKEY)
120
+ fail CoinapultErrorECC, 'Invalid ECC signature'
121
+ end
122
+
123
+ JSON.parse(Base64.urlsafe_decode64(resp['data']))
124
+ end
125
+
126
+ def send_to_coinapult(endpoint, values, sign: false, **kwargs)
127
+ if sign && @authmethod == 'ecc'
128
+ method = self.method(:_send_ECC)
129
+ else
130
+ method = self.method(:_send_request)
131
+ end
132
+
133
+ method.call(endpoint, values, sign: sign, **kwargs)
134
+ end
135
+
136
+ def create_account(create_local_keys: true, change_authmethod: true,
137
+ **kwargs)
138
+ url = '/api/account/create'
139
+
140
+ _setup_ECC_pair(nil) if create_local_keys
141
+
142
+ pub_pem = @ecc_pub_pem
143
+ result = _receive_ECC(_send_ECC(url, kwargs, new_account: true))
144
+ unless result['success'].nil?
145
+ if result['success'] != OpenSSL::Digest::SHA256.hexdigest(pub_pem)
146
+ fail CoinapultErrorECC, 'Unexpected public key'
147
+ end
148
+ @authmethod = 'ecc' if change_authmethod
149
+ puts "Please read the terms of service in TERMS.txt before \
150
+ proceeding with the account creation. #{result['info']}"
151
+ end
152
+
153
+ result
154
+ end
155
+
156
+ def activate_account(agree, pubhash: nil)
157
+ url = '/api/account/activate'
158
+
159
+ pubhash = @ecc_pub_hash if pubhash.nil?
160
+ values = { agree: agree, hash: pubhash }
161
+ _receive_ECC(_send_ECC(url, values, new_account: true))
162
+ end
163
+
164
+ def receive(amount: 0, out_amount: 0, currency: 'BTC', out_currency: nil,
165
+ external_id: nil, callback: '')
166
+ url = '/api/t/receive/'
167
+
168
+ if amount > 0 && out_amount > 0
169
+ fail ArgumentError, 'specify either the input amount or the output amount'
170
+ end
171
+
172
+ values = {}
173
+ if amount > 0
174
+ values['amount'] = amount
175
+ elsif out_amount > 0
176
+ values['outAmount'] = out_amount
177
+ else
178
+ fail ArgumentError, 'no amount specified'
179
+ end
180
+
181
+ out_currency = currency if out_currency.nil?
182
+
183
+ values['currency'] = currency
184
+ values['outCurrency'] = out_currency
185
+ values['extOID'] = external_id unless external_id.nil?
186
+ values['callback'] = callback
187
+
188
+ send_to_coinapult(url, values, sign: true)
189
+ end
190
+
191
+ def send(amount, address, out_amount: 0, currency: 'BTC', typ: 'bitcoin',
192
+ callback: '')
193
+ url = '/api/t/send/'
194
+
195
+ if amount > 0 && out_amount > 0
196
+ fail ArgumentError, 'specify either the input amount or the output amount'
197
+ end
198
+
199
+ values = {}
200
+ if amount > 0
201
+ values['amount'] = amount
202
+ elsif out_amount > 0
203
+ values['outAmount'] = out_amount
204
+ else
205
+ fail ArgumentError, 'no amount specified'
206
+ end
207
+ values['currency'] = currency
208
+ values['address'] = address
209
+ values['type'] = typ
210
+ values['callback'] = callback
211
+
212
+ send_to_coinapult(url, values, sign: true)
213
+ end
214
+
215
+ def convert(amount, in_currency: 'USD', out_currency: 'BTC')
216
+ url = '/api/t/convert/'
217
+
218
+ if amount <= 0
219
+ fail ArgumentError, 'invalid amount'
220
+ elsif in_currency == out_currency
221
+ fail ArgumentError, 'cannot convert currency to itself'
222
+ end
223
+
224
+ values = {}
225
+ values['amount'] = amount
226
+ values['inCurrency'] = in_currency
227
+ values['outCurrency'] = out_currency
228
+
229
+ send_to_coinapult(url, values, sign: true)
230
+ end
231
+
232
+ def search(transaction_id: nil, typ: nil, currency: nil,
233
+ to: nil, fro: nil, external_id: nil, txhash: nil,
234
+ many: false, page: nil)
235
+ url = '/api/t/search/'
236
+
237
+ values = {}
238
+ values['transaction_id'] = transaction_id unless transaction_id.nil?
239
+ values['type'] = typ unless typ.nil?
240
+ values['currency'] = currency unless currency.nil
241
+ values['to'] = to unless to.nil?
242
+ values['from'] = fro unless fro.nil?
243
+ values['extOID'] = external_id unless external_id.nil?
244
+ values['txhash'] = txhash unless txhash.nil?
245
+ fail ArgumentError, 'no search parameters provided' if values.length == 0
246
+ values['many'] = '1' if many
247
+ values['page'] = page unless page.nil?
248
+
249
+ send_to_coinapult(url, values, sign: true)
250
+ end
251
+
252
+ def lock(amount, out_amount: 0, currency: 'USD', callback: nil)
253
+ url = '/api/t/lock/'
254
+
255
+ if amount > 0 && out_amount > 0
256
+ fail ArgumentError, 'specify either the input amount or the output amount'
257
+ end
258
+
259
+ values = {}
260
+ if amount > 0
261
+ values['amount'] = amount
262
+ elsif out_amount > 0
263
+ values['outAmount'] = out_amount
264
+ else
265
+ fail ArgumentError, 'no amount specified'
266
+ end
267
+ values['callback'] = callback if callback
268
+ values['currency'] = currency
269
+
270
+ send_to_coinapult(url, values, sign: true)
271
+ end
272
+
273
+ def unlock(amount, address, out_amount: 0, currency: 'USD', callback: nil)
274
+ url = '/api/t/unlock/'
275
+
276
+ if amount > 0 && out_amount > 0
277
+ fail ArgumentError, 'specify either the input amount or the output amount'
278
+ end
279
+
280
+ values = {}
281
+ if amount > 0
282
+ values['amount'] = amount
283
+ elsif out_amount > 0
284
+ values['outAmount'] = out_amount
285
+ else
286
+ fail ArgumentError, 'no amount specified'
287
+ end
288
+ values['callback'] = callback if callback
289
+ values['currency'] = currency
290
+ values['address'] = address
291
+
292
+ send_to_coinapult(url, values, sign: true)
293
+ end
294
+
295
+ def unlock_confirm(transaction_id)
296
+ url = '/api/t/unlock/confirm'
297
+
298
+ values = {}
299
+ values['transaction_id'] = transaction_id
300
+
301
+ send_to_coinapult(url, values, sign: true)
302
+ end
303
+
304
+ # Get the ticker
305
+ def ticker(begint: nil, endt: nil, market: nil, filter: nil)
306
+ data = {}
307
+ data['begin'] = begint unless begint.nil?
308
+ data['end'] = endt unless endt.nil?
309
+ data['market'] = market unless market.nil?
310
+ data['filter'] = filter unless filter.nil?
311
+
312
+ send_to_coinapult('/api/ticker', data, sign: false, post: false)
313
+ end
314
+
315
+ # Get a new bitcoin address
316
+ def new_bitcoin_address
317
+ send_to_coinapult('/api/getBitcoinAddress', {}, sign: true)
318
+ end
319
+
320
+ # Display basic account information
321
+ def account_info(balance_type: 'all', locks_as_BTC: false)
322
+ url = '/api/accountInfo'
323
+
324
+ values = {}
325
+ values['balanceType'] = balance_type
326
+ values['locksAsBTC'] = locks_as_BTC
327
+
328
+ send_to_coinapult(url, values, sign: true)
329
+ end
330
+
331
+ # Check if an address belongs to your account
332
+ def account_address(address)
333
+ url = '/api/accountInfo/address'
334
+
335
+ values = {}
336
+ values['address'] = address
337
+
338
+ send_to_coinapult(url, values, sign: true)
339
+ end
340
+
341
+ # Utility for authenticating callbacks.
342
+ def authenticate_callback(recv_key, recv_sign, recv_data)
343
+ if recv_key.nil?
344
+ # ECC
345
+ return verify_ECC_sign(recv_sign, recv_data, ECC_COINAPULT_PUBKEY)
346
+ end
347
+
348
+ # HMAC
349
+ if recv_key != @key
350
+ # Unexpected API key received.
351
+ return false
352
+ end
353
+ test_HMAC = OpenSSL::HMAC.hexdigest('sha512', @secret, recv_data)
354
+ if test_HMAC != recv_sign
355
+ # Signature does not match.
356
+ return false
357
+ end
358
+ true
359
+ end
360
+
361
+ end
362
+
363
+ class CoinapultError < StandardError; end
364
+
365
+ class CoinapultErrorECC < CoinapultError; end
366
+
367
+ def generate_ECC_sign(data, privkey)
368
+ curve = privkey.group.curve_name
369
+ if curve != ECC_CURVE
370
+ fail CoinapultErrorECC, "key on curve #{curve}, expected #{ECC_CURVE}"
371
+ end
372
+ hmsg = OpenSSL::Digest::SHA256.digest(data)
373
+ sign = OpenSSL::ASN1.decode(privkey.dsa_sign_asn1(hmsg))
374
+ # Encode the signature as the r and s values.
375
+ "#{sign.value[0].value.to_s(16)}#{sign.value[1].value.to_s(16)}"
376
+ end
377
+
378
+ def verify_ECC_sign(signstr, origdata, pubkey)
379
+ curve = pubkey.group.curve_name
380
+ if curve != ECC_CURVE
381
+ fail CoinapultErrorECC, "key on curve #{curve}, expected #{ECC_CURVE}"
382
+ end
383
+ r = OpenSSL::ASN1::Integer.new(signstr[0..63].to_i(16))
384
+ s = OpenSSL::ASN1::Integer.new(signstr[64..-1].to_i(16))
385
+ sign = OpenSSL::ASN1::Sequence.new([r, s])
386
+ pubkey.dsa_verify_asn1(OpenSSL::Digest::SHA256.digest(origdata),
387
+ sign.to_der)
388
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: coinapult
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Guilherme Polo
8
+ - Ira Miller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-10-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest_client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ description:
29
+ email: gp@coinapult.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/coinapult.rb
35
+ homepage: https://rubygems.org/gems/coinapult
36
+ licenses:
37
+ - Apache 2
38
+ metadata: {}
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 2.0.14
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Coinapult API client
59
+ test_files: []