coinsync 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE.txt +21 -0
- data/README.md +255 -0
- data/bin/coinsync +78 -0
- data/doc/importers.md +201 -0
- data/lib/coinsync.rb +18 -0
- data/lib/coinsync/balance.rb +19 -0
- data/lib/coinsync/balance_task.rb +65 -0
- data/lib/coinsync/build_task.rb +43 -0
- data/lib/coinsync/builder.rb +32 -0
- data/lib/coinsync/config.rb +94 -0
- data/lib/coinsync/crypto_classifier.rb +27 -0
- data/lib/coinsync/currencies.rb +35 -0
- data/lib/coinsync/currency_converter.rb +65 -0
- data/lib/coinsync/currency_converters/all.rb +1 -0
- data/lib/coinsync/currency_converters/base.rb +46 -0
- data/lib/coinsync/currency_converters/cache.rb +34 -0
- data/lib/coinsync/currency_converters/fixer.rb +36 -0
- data/lib/coinsync/currency_converters/nbp.rb +38 -0
- data/lib/coinsync/formatter.rb +44 -0
- data/lib/coinsync/import_task.rb +37 -0
- data/lib/coinsync/importers/all.rb +1 -0
- data/lib/coinsync/importers/ark_voting.rb +121 -0
- data/lib/coinsync/importers/base.rb +35 -0
- data/lib/coinsync/importers/binance_api.rb +210 -0
- data/lib/coinsync/importers/bitbay20.rb +144 -0
- data/lib/coinsync/importers/bitbay_api.rb +224 -0
- data/lib/coinsync/importers/bitcurex.rb +71 -0
- data/lib/coinsync/importers/bittrex_api.rb +81 -0
- data/lib/coinsync/importers/bittrex_csv.rb +75 -0
- data/lib/coinsync/importers/changelly.rb +57 -0
- data/lib/coinsync/importers/circle.rb +58 -0
- data/lib/coinsync/importers/default.rb +90 -0
- data/lib/coinsync/importers/etherdelta.rb +93 -0
- data/lib/coinsync/importers/kraken_api.rb +134 -0
- data/lib/coinsync/importers/kraken_common.rb +137 -0
- data/lib/coinsync/importers/kraken_csv.rb +28 -0
- data/lib/coinsync/importers/kucoin_api.rb +172 -0
- data/lib/coinsync/importers/lisk_voting.rb +110 -0
- data/lib/coinsync/outputs/all.rb +1 -0
- data/lib/coinsync/outputs/base.rb +32 -0
- data/lib/coinsync/outputs/list.rb +123 -0
- data/lib/coinsync/outputs/raw.rb +45 -0
- data/lib/coinsync/outputs/summary.rb +48 -0
- data/lib/coinsync/request.rb +31 -0
- data/lib/coinsync/run_command_task.rb +20 -0
- data/lib/coinsync/source.rb +43 -0
- data/lib/coinsync/source_filter.rb +10 -0
- data/lib/coinsync/table_printer.rb +29 -0
- data/lib/coinsync/transaction.rb +125 -0
- data/lib/coinsync/version.rb +3 -0
- 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
|