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