coinsync 0.1.0

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