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,75 @@
1
+ require 'bigdecimal'
2
+ require 'csv'
3
+ require 'time'
4
+
5
+ require_relative 'base'
6
+ require_relative '../currencies'
7
+ require_relative '../transaction'
8
+
9
+ module CoinSync
10
+ module Importers
11
+ class BittrexCSV < Base
12
+ register_importer :bittrex_csv
13
+
14
+ class HistoryEntry
15
+ TIME_FORMAT = '%m/%d/%Y %H:%M:%S %p %z'
16
+
17
+ attr_accessor :uuid, :currency, :asset, :type, :quantity, :limit, :commission, :price,
18
+ :time_opened, :time_closed
19
+
20
+ def initialize(line)
21
+ @uuid = line[0]
22
+ @currency, @asset = line[1].split('-').map { |c| CryptoCurrency.new(c) }
23
+ @type = line[2]
24
+ @quantity = BigDecimal.new(line[3])
25
+ @limit = BigDecimal.new(line[4])
26
+ @commission = BigDecimal.new(line[5])
27
+ @price = BigDecimal.new(line[6])
28
+ @time_opened = Time.strptime(line[7] + ' +0000', TIME_FORMAT)
29
+ @time_closed = Time.strptime(line[8] + ' +0000', TIME_FORMAT)
30
+ end
31
+ end
32
+
33
+ def read_transaction_list(source)
34
+ contents = source.read.gsub("\u0000", '').gsub("\r", '')
35
+ entries = []
36
+ transactions = []
37
+
38
+ CSV.parse(contents, col_sep: ',') do |line|
39
+ next if line[0] == 'OrderUuid'
40
+
41
+ entries << HistoryEntry.new(line)
42
+ end
43
+
44
+ entries.sort_by! { |e| [e.time_closed, e.uuid] }
45
+
46
+ entries.each do |entry|
47
+ case entry.type
48
+ when 'LIMIT_BUY', 'MARKET_BUY' then
49
+ transactions << Transaction.new(
50
+ exchange: 'Bittrex',
51
+ time: entry.time_closed,
52
+ bought_amount: entry.quantity,
53
+ bought_currency: entry.asset,
54
+ sold_amount: entry.price + entry.commission,
55
+ sold_currency: entry.currency
56
+ )
57
+ when 'LIMIT_SELL', 'MARKET_SELL' then
58
+ transactions << Transaction.new(
59
+ exchange: 'Bittrex',
60
+ time: entry.time_closed,
61
+ bought_amount: entry.price - entry.commission, # TODO check this
62
+ bought_currency: entry.currency,
63
+ sold_amount: entry.quantity,
64
+ sold_currency: entry.asset
65
+ )
66
+ else
67
+ raise "Bittrex importer error: unexpected entry type '#{entry.type}'"
68
+ end
69
+ end
70
+
71
+ transactions
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,57 @@
1
+ require 'bigdecimal'
2
+ require 'csv'
3
+ require 'time'
4
+
5
+ require_relative 'base'
6
+ require_relative '../currencies'
7
+ require_relative '../transaction'
8
+
9
+ module CoinSync
10
+ module Importers
11
+ class Changelly < Base
12
+ register_importer :changelly
13
+
14
+ class HistoryEntry
15
+ attr_accessor :status, :date, :exchanged_currency, :exchanged_amount, :received_currency, :received_amount
16
+
17
+ def initialize(line)
18
+ @status = line[0]
19
+ @date = Time.parse(line[1] + ' +0000')
20
+
21
+ amount, name = line[2].gsub(',', '').split(/\s+/)
22
+ @exchanged_currency = CryptoCurrency.new(name)
23
+ @exchanged_amount = BigDecimal.new(amount)
24
+
25
+ amount, name = line[6].gsub(',', '').split(/\s+/)
26
+ @received_currency = CryptoCurrency.new(name)
27
+ @received_amount = BigDecimal.new(amount)
28
+ end
29
+ end
30
+
31
+ def read_transaction_list(source)
32
+ csv = CSV.new(source, col_sep: ',')
33
+
34
+ transactions = []
35
+
36
+ csv.each do |line|
37
+ next if line[0] == 'Status'
38
+
39
+ entry = HistoryEntry.new(line)
40
+
41
+ next if entry.status != 'finished'
42
+
43
+ transactions << Transaction.new(
44
+ exchange: 'Changelly',
45
+ time: entry.date,
46
+ bought_amount: entry.received_amount,
47
+ bought_currency: entry.received_currency,
48
+ sold_amount: entry.exchanged_amount,
49
+ sold_currency: entry.exchanged_currency
50
+ )
51
+ end
52
+
53
+ transactions.reverse
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,58 @@
1
+ require 'bigdecimal'
2
+ require 'csv'
3
+ require 'time'
4
+
5
+ require_relative 'base'
6
+ require_relative '../currencies'
7
+ require_relative '../transaction'
8
+
9
+ module CoinSync
10
+ module Importers
11
+ class Circle < Base
12
+ register_importer :circle
13
+
14
+ class HistoryEntry
15
+ attr_accessor :date, :id, :type, :from_account, :to_account, :from_amount, :from_currency,
16
+ :to_amount, :to_currency, :status
17
+
18
+ def initialize(line)
19
+ @date = Time.strptime(line[0], '%a %b %d %Y %H:%M:%S GMT+0000 (%Z)')
20
+ @id = line[1]
21
+ @type = line[2]
22
+ @from_account = line[3]
23
+ @to_account = line[4]
24
+ @from_amount = BigDecimal.new(line[5].gsub(/[^\d\.]+/, ''))
25
+ @from_currency = FiatCurrency.new(line[6])
26
+ @to_amount = BigDecimal.new(line[7].gsub(/[^\d\.]+/, ''))
27
+ @to_currency = CryptoCurrency.new(line[8])
28
+ @status = line[9]
29
+ end
30
+ end
31
+
32
+ def read_transaction_list(source)
33
+ csv = CSV.new(source, col_sep: ',')
34
+
35
+ transactions = []
36
+
37
+ csv.each do |line|
38
+ next if line[0] == 'Date'
39
+
40
+ entry = HistoryEntry.new(line)
41
+
42
+ next if entry.type != 'deposit'
43
+
44
+ transactions << Transaction.new(
45
+ exchange: 'Circle',
46
+ bought_currency: entry.to_currency,
47
+ sold_currency: entry.from_currency,
48
+ time: entry.date,
49
+ bought_amount: entry.to_amount,
50
+ sold_amount: entry.from_amount
51
+ )
52
+ end
53
+
54
+ transactions
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,90 @@
1
+ require 'bigdecimal'
2
+ require 'csv'
3
+ require 'time'
4
+
5
+ require_relative 'base'
6
+ require_relative '../currencies'
7
+ require_relative '../formatter'
8
+ require_relative '../transaction'
9
+
10
+ module CoinSync
11
+ module Importers
12
+ class Default < Base
13
+ register_importer :default
14
+
15
+ class HistoryEntry
16
+ attr_accessor :lp, :exchange, :type, :date, :amount, :asset, :total, :currency
17
+ end
18
+
19
+ def initialize(config, params = {})
20
+ super
21
+ @decimal_separator = config.custom_decimal_separator
22
+ @formatter = Formatter.new(config)
23
+ end
24
+
25
+ def read_transaction_list(source)
26
+ csv = CSV.new(source, col_sep: @config.column_separator)
27
+
28
+ transactions = []
29
+
30
+ csv.each do |line|
31
+ next if line.empty?
32
+ next if line.all? { |f| f.to_s.strip == '' }
33
+ next if line[0] == 'Lp'
34
+
35
+ entry = parse_line(line)
36
+
37
+ if entry.type.downcase == Transaction::TYPE_PURCHASE.to_s
38
+ transactions << Transaction.new(
39
+ exchange: entry.exchange,
40
+ bought_currency: entry.asset,
41
+ sold_currency: entry.currency,
42
+ time: entry.date,
43
+ bought_amount: entry.amount,
44
+ sold_amount: entry.total
45
+ )
46
+ elsif entry.type.downcase == Transaction::TYPE_SALE.to_s
47
+ transactions << Transaction.new(
48
+ exchange: entry.exchange,
49
+ bought_currency: entry.currency,
50
+ sold_currency: entry.asset,
51
+ time: entry.date,
52
+ bought_amount: entry.total,
53
+ sold_amount: entry.amount
54
+ )
55
+ else
56
+ raise "Default importer error: unexpected entry type '#{entry.type}'"
57
+ end
58
+ end
59
+
60
+ transactions
61
+ end
62
+
63
+ private
64
+
65
+ def parse_line(line)
66
+ entry = HistoryEntry.new
67
+
68
+ entry.lp = line[0].to_i
69
+ entry.exchange = line[1]
70
+ entry.type = line[2]
71
+ entry.date = Time.parse(line[3])
72
+ entry.amount = @formatter.parse_decimal(line[4])
73
+ entry.asset = CryptoCurrency.new(line[5])
74
+ entry.total = @formatter.parse_decimal(line[6])
75
+
76
+ entry.currency = if line[7].to_s.start_with?('$')
77
+ CryptoCurrency.new(line[7][1..-1])
78
+ else
79
+ FiatCurrency.new(line[7])
80
+ end
81
+
82
+ if entry.currency.code.nil? && entry.total > 0
83
+ raise "Default importer error: Currency must be specified if total value is non-zero"
84
+ end
85
+
86
+ entry
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,93 @@
1
+ require 'bigdecimal'
2
+ require 'csv'
3
+ require 'time'
4
+
5
+ require_relative 'base'
6
+ require_relative '../currencies'
7
+ require_relative '../transaction'
8
+
9
+ module CoinSync
10
+ module Importers
11
+ # Look up your transactions using DeltaBalances at https://deltabalances.github.io/history.html,
12
+ # specifying the time range you need, and then download the "Default" CSV in the top-right section
13
+
14
+ class EtherDelta < Base
15
+ register_importer :etherdelta
16
+
17
+ ETH = CryptoCurrency.new('ETH')
18
+
19
+ class HistoryEntry
20
+ attr_accessor :type, :trade, :token, :amount, :price, :total, :date, :fee, :fee_token
21
+
22
+ def initialize(line)
23
+ @type = line[0]
24
+
25
+ if !['Maker', 'Taker'].include?(@type)
26
+ raise "EtherDelta importer: incorrect csv format - unexpected '#{@type}' in the first column"
27
+ end
28
+
29
+ @trade = line[1]
30
+
31
+ if !['Buy', 'Sell'].include?(@trade)
32
+ raise "EtherDelta importer: incorrect csv format - unexpected '#{@trade}' in the second column"
33
+ end
34
+
35
+ @token = CryptoCurrency.new(line[2])
36
+
37
+ @amount = BigDecimal.new(line[3])
38
+ @price = BigDecimal.new(line[4])
39
+ @total = BigDecimal.new(line[5])
40
+
41
+ @date = Time.parse(line[6])
42
+
43
+ @fee = BigDecimal.new(line[11])
44
+ @fee_token = CryptoCurrency.new(line[12])
45
+ end
46
+ end
47
+
48
+ def read_transaction_list(source)
49
+ csv = CSV.new(source, col_sep: ',')
50
+
51
+ transactions = []
52
+
53
+ csv.each do |line|
54
+ next if line[0] == 'Type'
55
+
56
+ entry = HistoryEntry.new(line)
57
+
58
+ next if entry.amount.round(8) == 0 || entry.total.round(8) == 0
59
+
60
+ if entry.trade == 'Buy'
61
+ if entry.fee_token != ETH
62
+ raise "EtherDelta importer: Unexpected fee currency: #{entry.fee_token.code}"
63
+ end
64
+
65
+ transactions << Transaction.new(
66
+ exchange: 'EtherDelta',
67
+ time: entry.date,
68
+ bought_amount: entry.amount,
69
+ bought_currency: entry.token,
70
+ sold_amount: entry.total + entry.fee,
71
+ sold_currency: ETH
72
+ )
73
+ else
74
+ if entry.fee_token != entry.token
75
+ raise "EtherDelta importer: Unexpected fee currency: #{entry.fee_token.code}"
76
+ end
77
+
78
+ transactions << Transaction.new(
79
+ exchange: 'EtherDelta',
80
+ time: entry.date,
81
+ bought_amount: entry.total,
82
+ bought_currency: ETH,
83
+ sold_amount: entry.amount + entry.fee,
84
+ sold_currency: entry.token
85
+ )
86
+ end
87
+ end
88
+
89
+ transactions.sort_by(&:time)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,134 @@
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 'kraken_common'
10
+ require_relative '../balance'
11
+ require_relative '../currencies'
12
+ require_relative '../request'
13
+
14
+ module CoinSync
15
+ module Importers
16
+ class KrakenAPI < Base
17
+ register_importer :kraken_api
18
+
19
+ include Kraken::Common
20
+
21
+ BASE_URL = "https://api.kraken.com"
22
+ API_RENEWAL_INTERVAL = 3.0
23
+
24
+ def initialize(config, params = {})
25
+ super
26
+ @api_key = params['api_key']
27
+ @secret_api_key = params['private_key']
28
+ @decoded_secret = Base64.decode64(@secret_api_key) if @secret_api_key
29
+ end
30
+
31
+ def can_import?(type)
32
+ @api_key && @secret_api_key && [:balances, :transactions].include?(type)
33
+ end
34
+
35
+ def import_transactions(filename)
36
+ offset = 0
37
+ entries = []
38
+ slowdown = false
39
+
40
+ loop do
41
+ response = make_request('/0/private/Ledgers', ofs: offset)
42
+ print slowdown ? '-' : '.'
43
+ sleep(2 * API_RENEWAL_INTERVAL) if slowdown # rate limiting
44
+
45
+ case response
46
+ when Net::HTTPSuccess
47
+ json = JSON.parse(response.body)
48
+
49
+ if json['result'].nil? || json['error'].length > 0
50
+ if json['error'].first == 'EAPI:Rate limit exceeded'
51
+ slowdown = true
52
+ print '!'
53
+ sleep(4 * API_RENEWAL_INTERVAL)
54
+ next
55
+ else
56
+ raise "Kraken importer: Invalid response: #{response.body}"
57
+ end
58
+ end
59
+
60
+ data = json['result']
61
+ list = data && data['ledger']
62
+
63
+ if !list
64
+ raise "Kraken importer: No data returned: #{response.body}"
65
+ end
66
+
67
+ break if list.empty?
68
+
69
+ entries.concat(list.values)
70
+ offset += list.length
71
+ when Net::HTTPBadRequest
72
+ raise "Kraken importer: Bad request: #{response.body}"
73
+ else
74
+ raise "Kraken importer: Bad response: #{response.body}"
75
+ end
76
+ end
77
+
78
+ File.write(filename, JSON.pretty_generate(entries) + "\n")
79
+ end
80
+
81
+ def import_balances
82
+ response = make_request('/0/private/Balance')
83
+
84
+ case response
85
+ when Net::HTTPSuccess
86
+ json = JSON.parse(response.body)
87
+
88
+ if !json['error'].empty? || !json['result']
89
+ raise "Kraken importer: Invalid response: #{response.body}"
90
+ end
91
+
92
+ return json['result'].map { |k, v|
93
+ [Kraken::LedgerEntry.parse_currency(k), BigDecimal.new(v)]
94
+ }.select { |currency, amount|
95
+ amount > 0 && currency.crypto?
96
+ }.map { |currency, amount|
97
+ Balance.new(currency, available: amount)
98
+ }
99
+ when Net::HTTPBadRequest
100
+ raise "Kraken importer: Bad request: #{response.body}"
101
+ else
102
+ raise "Kraken importer: Bad response: #{response.body}"
103
+ end
104
+ end
105
+
106
+ def read_transaction_list(source)
107
+ json = JSON.parse(source.read)
108
+
109
+ build_transaction_list(json.map { |hash| Kraken::LedgerEntry.from_json(hash) })
110
+ end
111
+
112
+ private
113
+
114
+ def make_request(path, params = {})
115
+ (@api_key && @secret_api_key) or raise "Public and secret API keys must be provided"
116
+
117
+ nonce = (Time.now.to_f * 1000).to_i
118
+
119
+ url = URI(BASE_URL + path)
120
+ params['nonce'] = nonce
121
+
122
+ post_data = URI.encode_www_form(params)
123
+ string_to_hash = path + OpenSSL::Digest.new('sha256', nonce.to_s + post_data).digest
124
+ hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('sha512', @decoded_secret, string_to_hash))
125
+
126
+ Request.post(url) do |request|
127
+ request.body = post_data
128
+ request['API-Key'] = @api_key
129
+ request['API-Sign'] = hmac
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end