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