coinsync 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE.txt +21 -0
  3. data/README.md +255 -0
  4. data/bin/coinsync +78 -0
  5. data/doc/importers.md +201 -0
  6. data/lib/coinsync.rb +18 -0
  7. data/lib/coinsync/balance.rb +19 -0
  8. data/lib/coinsync/balance_task.rb +65 -0
  9. data/lib/coinsync/build_task.rb +43 -0
  10. data/lib/coinsync/builder.rb +32 -0
  11. data/lib/coinsync/config.rb +94 -0
  12. data/lib/coinsync/crypto_classifier.rb +27 -0
  13. data/lib/coinsync/currencies.rb +35 -0
  14. data/lib/coinsync/currency_converter.rb +65 -0
  15. data/lib/coinsync/currency_converters/all.rb +1 -0
  16. data/lib/coinsync/currency_converters/base.rb +46 -0
  17. data/lib/coinsync/currency_converters/cache.rb +34 -0
  18. data/lib/coinsync/currency_converters/fixer.rb +36 -0
  19. data/lib/coinsync/currency_converters/nbp.rb +38 -0
  20. data/lib/coinsync/formatter.rb +44 -0
  21. data/lib/coinsync/import_task.rb +37 -0
  22. data/lib/coinsync/importers/all.rb +1 -0
  23. data/lib/coinsync/importers/ark_voting.rb +121 -0
  24. data/lib/coinsync/importers/base.rb +35 -0
  25. data/lib/coinsync/importers/binance_api.rb +210 -0
  26. data/lib/coinsync/importers/bitbay20.rb +144 -0
  27. data/lib/coinsync/importers/bitbay_api.rb +224 -0
  28. data/lib/coinsync/importers/bitcurex.rb +71 -0
  29. data/lib/coinsync/importers/bittrex_api.rb +81 -0
  30. data/lib/coinsync/importers/bittrex_csv.rb +75 -0
  31. data/lib/coinsync/importers/changelly.rb +57 -0
  32. data/lib/coinsync/importers/circle.rb +58 -0
  33. data/lib/coinsync/importers/default.rb +90 -0
  34. data/lib/coinsync/importers/etherdelta.rb +93 -0
  35. data/lib/coinsync/importers/kraken_api.rb +134 -0
  36. data/lib/coinsync/importers/kraken_common.rb +137 -0
  37. data/lib/coinsync/importers/kraken_csv.rb +28 -0
  38. data/lib/coinsync/importers/kucoin_api.rb +172 -0
  39. data/lib/coinsync/importers/lisk_voting.rb +110 -0
  40. data/lib/coinsync/outputs/all.rb +1 -0
  41. data/lib/coinsync/outputs/base.rb +32 -0
  42. data/lib/coinsync/outputs/list.rb +123 -0
  43. data/lib/coinsync/outputs/raw.rb +45 -0
  44. data/lib/coinsync/outputs/summary.rb +48 -0
  45. data/lib/coinsync/request.rb +31 -0
  46. data/lib/coinsync/run_command_task.rb +20 -0
  47. data/lib/coinsync/source.rb +43 -0
  48. data/lib/coinsync/source_filter.rb +10 -0
  49. data/lib/coinsync/table_printer.rb +29 -0
  50. data/lib/coinsync/transaction.rb +125 -0
  51. data/lib/coinsync/version.rb +3 -0
  52. metadata +95 -0
@@ -0,0 +1,137 @@
1
+ require 'bigdecimal'
2
+ require 'time'
3
+
4
+ require_relative '../currencies'
5
+ require_relative '../transaction'
6
+
7
+ module CoinSync
8
+ module Importers
9
+ module Kraken
10
+ class LedgerEntry
11
+ attr_accessor :txid, :refid, :time, :type, :aclass, :asset, :amount, :fee, :balance
12
+
13
+ def self.from_csv(line)
14
+ entry = self.new
15
+ entry.txid = line[0]
16
+ entry.refid = line[1]
17
+ entry.time = Time.parse(line[2] + " +0000")
18
+ entry.type = line[3]
19
+ entry.aclass = line[4]
20
+ entry.asset = parse_currency(line[5])
21
+ entry.amount = BigDecimal.new(line[6])
22
+ entry.fee = BigDecimal.new(line[7])
23
+ entry.balance = BigDecimal.new(line[8])
24
+ entry
25
+ end
26
+
27
+ def self.from_json(hash)
28
+ entry = self.new
29
+ entry.refid = hash['refid']
30
+ entry.time = Time.at(hash['time'])
31
+ entry.type = hash['type']
32
+ entry.aclass = hash['aclass']
33
+ entry.asset = parse_currency(hash['asset'])
34
+ entry.amount = BigDecimal.new(hash['amount'])
35
+ entry.fee = BigDecimal.new(hash['fee'])
36
+ entry.balance = BigDecimal.new(hash['balance'])
37
+ entry
38
+ end
39
+
40
+ def self.parse_currency(code)
41
+ case code
42
+ when 'BCH' then CryptoCurrency.new('BCH')
43
+ when 'DASH' then CryptoCurrency.new('DASH')
44
+ when 'EOS' then CryptoCurrency.new('EOS')
45
+ when 'GNO' then CryptoCurrency.new('GNO')
46
+ when 'KFEE' then CryptoCurrency.new('KFEE')
47
+ when 'USDT' then CryptoCurrency.new('USDT')
48
+
49
+ when 'XDAO' then CryptoCurrency.new('DAO')
50
+ when 'XETC' then CryptoCurrency.new('ETC')
51
+ when 'XETH' then CryptoCurrency.new('ETH')
52
+ when 'XICN' then CryptoCurrency.new('ICN')
53
+ when 'XLTC' then CryptoCurrency.new('LTC')
54
+ when 'XMLN' then CryptoCurrency.new('MLN')
55
+ when 'XNMC' then CryptoCurrency.new('NMC')
56
+ when 'XREP' then CryptoCurrency.new('REP')
57
+ when 'XXBT' then CryptoCurrency.new('BTC')
58
+ when 'XXDG' then CryptoCurrency.new('DOGE')
59
+ when 'XXLM' then CryptoCurrency.new('XLM')
60
+ when 'XXMR' then CryptoCurrency.new('XMR')
61
+ when 'XXRP' then CryptoCurrency.new('XRP')
62
+ when 'XXVN' then CryptoCurrency.new('VEN')
63
+ when 'XZEC' then CryptoCurrency.new('ZEC')
64
+
65
+ when 'ZCAD' then FiatCurrency.new('CAD')
66
+ when 'ZEUR' then FiatCurrency.new('EUR')
67
+ when 'ZGBP' then FiatCurrency.new('GBP')
68
+ when 'ZJPY' then FiatCurrency.new('JPY')
69
+ when 'ZKRW' then FiatCurrency.new('KRW')
70
+ when 'ZUSD' then FiatCurrency.new('USD')
71
+
72
+ else raise "Unknown currency: #{code}"
73
+ end
74
+ end
75
+
76
+ def crypto?
77
+ @asset.crypto?
78
+ end
79
+ end
80
+
81
+ module Common
82
+ def build_transaction_list(entries)
83
+ set = []
84
+ transactions = []
85
+
86
+ entries.each do |entry|
87
+ if entry.type == 'transfer'
88
+ transactions << Transaction.new(
89
+ exchange: 'Kraken',
90
+ time: entry.time,
91
+ bought_amount: entry.amount,
92
+ bought_currency: entry.asset,
93
+ sold_amount: BigDecimal(0),
94
+ sold_currency: FiatCurrency.new(nil)
95
+ )
96
+ next
97
+ end
98
+
99
+ next if entry.type != 'trade'
100
+
101
+ set << entry
102
+ next unless set.length == 2
103
+
104
+ if set[0].refid != set[1].refid
105
+ raise "Kraken importer error: Couldn't match a pair of ledger lines - ids don't match: #{set}"
106
+ end
107
+
108
+ if set.none? { |e| e.crypto? }
109
+ raise "Kraken importer error: Couldn't match a pair of ledger lines - " +
110
+ "no cryptocurrencies were exchanged: #{set}"
111
+ end
112
+
113
+ bought = set.detect { |e| e.amount > 0 }
114
+ sold = set.detect { |e| e.amount < 0 }
115
+
116
+ if bought.nil? || sold.nil?
117
+ raise "Kraken importer error: Couldn't match a pair of ledger lines - invalid transaction amounts: #{set}"
118
+ end
119
+
120
+ transactions << Transaction.new(
121
+ exchange: 'Kraken',
122
+ time: [bought.time, sold.time].max,
123
+ bought_amount: bought.amount - bought.fee,
124
+ bought_currency: bought.asset,
125
+ sold_amount: -(sold.amount - sold.fee),
126
+ sold_currency: sold.asset
127
+ )
128
+
129
+ set.clear
130
+ end
131
+
132
+ transactions
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,28 @@
1
+ require 'csv'
2
+
3
+ require_relative 'base'
4
+ require_relative 'kraken_common'
5
+
6
+ module CoinSync
7
+ module Importers
8
+ class KrakenCSV < Base
9
+ register_importer :kraken_csv
10
+
11
+ include Kraken::Common
12
+
13
+ def read_transaction_list(source)
14
+ csv = CSV.new(source, col_sep: ',')
15
+
16
+ entries = []
17
+
18
+ csv.each do |line|
19
+ next if line[0] == 'txid'
20
+
21
+ entries << Kraken::LedgerEntry.from_csv(line)
22
+ end
23
+
24
+ build_transaction_list(entries)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,172 @@
1
+ require 'base64'
2
+ require 'bigdecimal'
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'openssl'
6
+ require 'uri'
7
+
8
+ require_relative 'base'
9
+ require_relative '../balance'
10
+ require_relative '../currencies'
11
+ require_relative '../request'
12
+ require_relative '../transaction'
13
+
14
+ module CoinSync
15
+ module Importers
16
+ class KucoinAPI < Base
17
+ register_importer :kucoin_api
18
+
19
+ BASE_URL = "https://api.kucoin.com"
20
+
21
+ class HistoryEntry
22
+ attr_accessor :created_at, :amount, :direction, :coin_type, :coin_type_pair, :deal_value, :fee
23
+
24
+ def initialize(hash)
25
+ @created_at = Time.at(hash['createdAt'] / 1000)
26
+ @amount = BigDecimal.new(hash['amount'], 0)
27
+ @direction = hash['direction']
28
+ @coin_type = CryptoCurrency.new(hash['coinType'])
29
+ @coin_type_pair = CryptoCurrency.new(hash['coinTypePair'])
30
+ @deal_value = BigDecimal.new(hash['dealValue'], 0)
31
+ @fee = BigDecimal.new(hash['fee'], 0)
32
+ end
33
+ end
34
+
35
+ def initialize(config, params = {})
36
+ super
37
+ @api_key = params['api_key']
38
+ @api_secret = params['api_secret']
39
+ end
40
+
41
+ def can_import?(type)
42
+ @api_key && @api_secret && [:balances, :transactions].include?(type)
43
+ end
44
+
45
+ def import_transactions(filename)
46
+ response = make_request('/order/dealt', limit: 100) # TODO: what if there's more than 100?
47
+
48
+ case response
49
+ when Net::HTTPSuccess
50
+ json = JSON.parse(response.body)
51
+
52
+ if json['success'] != true || json['code'] != 'OK'
53
+ raise "Kucoin importer: Invalid response: #{response.body}"
54
+ end
55
+
56
+ data = json['data']
57
+ list = data && data['datas']
58
+
59
+ if !list
60
+ raise "Kucoin importer: No data returned: #{response.body}"
61
+ end
62
+
63
+ File.write(filename, JSON.pretty_generate(list) + "\n")
64
+ when Net::HTTPBadRequest
65
+ raise "Kucoin importer: Bad request: #{response}"
66
+ else
67
+ raise "Kucoin importer: Bad response: #{response}"
68
+ end
69
+ end
70
+
71
+ def import_balances
72
+ page = 1
73
+ full_list = []
74
+
75
+ loop do
76
+ response = make_request('/account/balances', limit: 20, page: page)
77
+
78
+ case response
79
+ when Net::HTTPSuccess
80
+ json = JSON.parse(response.body)
81
+
82
+ if json['success'] != true || json['code'] != 'OK'
83
+ raise "Kucoin importer: Invalid response: #{response.body}"
84
+ end
85
+
86
+ data = json['data']
87
+ list = data && data['datas']
88
+
89
+ if !list
90
+ raise "Kucoin importer: No data returned: #{response.body}"
91
+ end
92
+
93
+ full_list.concat(list)
94
+
95
+ page += 1
96
+ break if page > data['pageNos']
97
+ when Net::HTTPBadRequest
98
+ raise "Kucoin importer: Bad request: #{response}"
99
+ else
100
+ raise "Kucoin importer: Bad response: #{response}"
101
+ end
102
+ end
103
+
104
+ full_list.delete_if { |b| b['balance'] == 0.0 && b['freezeBalance'] == 0.0 }
105
+
106
+ full_list.map do |b|
107
+ Balance.new(
108
+ CryptoCurrency.new(b['coinType']),
109
+ available: BigDecimal.new(b['balanceStr']),
110
+ locked: BigDecimal.new(b['freezeBalanceStr'])
111
+ )
112
+ end
113
+ end
114
+
115
+ def read_transaction_list(source)
116
+ json = JSON.parse(source.read)
117
+ transactions = []
118
+
119
+ json.each do |hash|
120
+ entry = HistoryEntry.new(hash)
121
+
122
+ if entry.direction == 'BUY'
123
+ transactions << Transaction.new(
124
+ exchange: 'Kucoin',
125
+ time: entry.created_at,
126
+ bought_amount: entry.amount - entry.fee,
127
+ bought_currency: entry.coin_type,
128
+ sold_amount: entry.deal_value,
129
+ sold_currency: entry.coin_type_pair
130
+ )
131
+ else
132
+ # TODO sell
133
+ raise "Kucoin importer error: unexpected entry direction '#{entry.direction}'"
134
+ end
135
+ end
136
+
137
+ transactions.reverse
138
+ end
139
+
140
+ private
141
+
142
+ def make_request(path, params = {})
143
+ (@api_key && @api_secret) or raise "Public and secret API keys must be provided"
144
+
145
+ endpoint = '/v1' + path
146
+ nonce = (Time.now.to_f * 1000).to_i
147
+ url = URI(BASE_URL + endpoint)
148
+
149
+ url.query = build_query_string(params)
150
+
151
+ string_to_hash = Base64.strict_encode64("#{endpoint}/#{nonce}/#{url.query}")
152
+ hmac = OpenSSL::HMAC.hexdigest('sha256', @api_secret, string_to_hash)
153
+
154
+ Request.get(url) do |request|
155
+ request['KC-API-KEY'] = @api_key
156
+ request['KC-API-NONCE'] = nonce
157
+ request['KC-API-SIGNATURE'] = hmac
158
+ end
159
+ end
160
+
161
+ def build_query_string(params)
162
+ params.map { |k, v|
163
+ [k.to_s, v.to_s]
164
+ }.sort_by { |k, v|
165
+ [k[0] < 'a' ? 1 : 0, k]
166
+ }.map { |k, v|
167
+ "#{k}=#{v}"
168
+ }.join('&')
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,110 @@
1
+ require 'bigdecimal'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'time'
5
+ require 'uri'
6
+
7
+ require_relative 'base'
8
+ require_relative '../balance'
9
+ require_relative '../currencies'
10
+ require_relative '../request'
11
+ require_relative '../transaction'
12
+
13
+ module CoinSync
14
+ module Importers
15
+ class LiskVoting < Base
16
+ register_importer :lisk_voting
17
+
18
+ BASE_URL = "https://explorer.lisk.io/api"
19
+ EPOCH_TIME = Time.parse('2016-05-24 17:00 UTC')
20
+ LISK = CryptoCurrency.new('LSK')
21
+
22
+ class HistoryEntry
23
+ attr_accessor :timestamp, :amount
24
+
25
+ def initialize(hash)
26
+ @timestamp = EPOCH_TIME + hash['timestamp']
27
+ @amount = BigDecimal.new(hash['amount']) / 100_000_000
28
+ end
29
+ end
30
+
31
+ def initialize(config, params = {})
32
+ super
33
+ @address = params['address']
34
+ end
35
+
36
+ def can_import?(type)
37
+ @address && [:balances, :transactions].include?(type)
38
+ end
39
+
40
+ def import_transactions(filename)
41
+ response = make_request('/getTransactionsByAddress', address: @address, limit: 1000)
42
+
43
+ case response
44
+ when Net::HTTPSuccess
45
+ json = JSON.parse(response.body)
46
+
47
+ if json['success'] != true || !json['transactions']
48
+ raise "Lisk importer: Invalid response: #{response.body}"
49
+ end
50
+
51
+ rewards = json['transactions'].select { |tx| tx['senderDelegate'] }
52
+
53
+ File.write(filename, JSON.pretty_generate(rewards) + "\n")
54
+ when Net::HTTPBadRequest
55
+ raise "Lisk importer: Bad request: #{response}"
56
+ else
57
+ raise "Lisk importer: Bad response: #{response}"
58
+ end
59
+ end
60
+
61
+ def import_balances
62
+ response = make_request('/getAccount', address: @address)
63
+
64
+ case response
65
+ when Net::HTTPSuccess
66
+ json = JSON.parse(response.body)
67
+
68
+ if json['success'] != true || !json['balance']
69
+ raise "Lisk importer: Invalid response: #{response.body}"
70
+ end
71
+
72
+ [Balance.new(LISK, available: BigDecimal.new(json['balance']) / 100_000_000)]
73
+ when Net::HTTPBadRequest
74
+ raise "Lisk importer: Bad request: #{response}"
75
+ else
76
+ raise "Lisk importer: Bad response: #{response}"
77
+ end
78
+ end
79
+
80
+ def read_transaction_list(source)
81
+ json = JSON.parse(source.read)
82
+ transactions = []
83
+
84
+ json.each do |hash|
85
+ entry = HistoryEntry.new(hash)
86
+
87
+ transactions << Transaction.new(
88
+ exchange: 'Lisk voting',
89
+ time: entry.timestamp,
90
+ bought_amount: entry.amount,
91
+ bought_currency: LISK,
92
+ sold_amount: BigDecimal.new(0),
93
+ sold_currency: FiatCurrency.new(nil)
94
+ )
95
+ end
96
+
97
+ transactions.reverse
98
+ end
99
+
100
+ private
101
+
102
+ def make_request(path, params = {})
103
+ url = URI(BASE_URL + path)
104
+ url.query = URI.encode_www_form(params)
105
+
106
+ Request.get(url)
107
+ end
108
+ end
109
+ end
110
+ end