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.
- checksums.yaml +7 -0
- data/lib/coinapult.rb +388 -0
- 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: []
|