coinapult 0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []