ouroboros-peatio-bitgo 2.6.10

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.
@@ -0,0 +1,54 @@
1
+ require 'faraday'
2
+ require 'better-faraday'
3
+
4
+ module Peatio
5
+ module Bitgo
6
+ class Client
7
+ Error = Class.new(StandardError)
8
+ ConnectionError = Class.new(Error)
9
+
10
+ class ResponseError < Error
11
+ def initialize(msg)
12
+ super "#{msg}"
13
+ end
14
+ end
15
+
16
+ def initialize(endpoint, access_token)
17
+ @endpoint = URI.parse(endpoint)
18
+ @access_token = access_token
19
+ end
20
+
21
+ def rest_api(verb, path, data = nil)
22
+ args = [@endpoint.to_s + path]
23
+
24
+ if data
25
+ if %i[post put patch].include?(verb)
26
+ args << data.compact.to_json
27
+ args << { 'Content-Type' => 'application/json' }
28
+ else
29
+ args << data.compact
30
+ args << {}
31
+ end
32
+ else
33
+ args << nil
34
+ args << {}
35
+ end
36
+
37
+ args.last['Accept'] = 'application/json'
38
+ args.last['Authorization'] = 'Bearer ' + @access_token
39
+
40
+ response = Faraday.send(verb, *args)
41
+ response.assert_success!
42
+ response = JSON.parse(response.body)
43
+ response['error'].tap { |error| raise ResponseError.new(error) if error }
44
+ response
45
+ rescue Faraday::Error => e
46
+ if e.is_a?(Faraday::ConnectionFailed) || e.is_a?(Faraday::TimeoutError)
47
+ raise ConnectionError, e
48
+ else
49
+ raise ConnectionError, JSON.parse(e.response.body)['message']
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,42 @@
1
+ module Peatio
2
+ module Bitgo
3
+ module Hooks
4
+ BLOCKCHAIN_VERSION_REQUIREMENT = "~> 1.0.0"
5
+ WALLET_VERSION_REQUIREMENT = "~> 1.0.0"
6
+
7
+ class << self
8
+ def check_compatibility
9
+ unless Gem::Requirement.new(BLOCKCHAIN_VERSION_REQUIREMENT)
10
+ .satisfied_by?(Gem::Version.new(Peatio::Blockchain::VERSION))
11
+ [
12
+ "Bitgo blockchain version requirement was not satisfied by Peatio::Blockchain.",
13
+ "Bitgo blockchain requires #{BLOCKCHAIN_VERSION_REQUIREMENT}.",
14
+ "Peatio::Blockchain version is #{Peatio::Blockchain::VERSION}"
15
+ ].join('\n').tap { |s| Kernel.abort s }
16
+ end
17
+
18
+ unless Gem::Requirement.new(WALLET_VERSION_REQUIREMENT)
19
+ .satisfied_by?(Gem::Version.new(Peatio::Wallet::VERSION))
20
+ [
21
+ "Bitgo wallet version requirement was not satisfied by Peatio::Wallet.",
22
+ "Bitgo wallet requires #{WALLET_VERSION_REQUIREMENT}.",
23
+ "Peatio::Wallet version is #{Peatio::Wallet::VERSION}"
24
+ ].join('\n').tap { |s| Kernel.abort s }
25
+ end
26
+ end
27
+
28
+ def register
29
+ Peatio::Blockchain.registry[:bitgo] = Bitgo::Blockchain
30
+ Peatio::Wallet.registry[:bitgo] = Bitgo::Wallet
31
+ end
32
+ end
33
+
34
+ if defined?(Rails::Railtie)
35
+ require "peatio/bitgo/railtie"
36
+ else
37
+ check_compatibility
38
+ register
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ module Peatio
2
+ module Bitgo
3
+ class Railtie < Rails::Railtie
4
+ config.before_initialize do
5
+ Hooks.check_compatibility
6
+ end
7
+
8
+ config.after_initialize do
9
+ Hooks.register
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module Peatio
2
+ module Bitgo
3
+ VERSION = "2.6.10".freeze
4
+ end
5
+ end
@@ -0,0 +1,342 @@
1
+ module Peatio
2
+ module Bitgo
3
+ class Wallet < Peatio::Wallet::Abstract
4
+ TIME_DIFFERENCE_IN_MINUTES = 10
5
+ XLM_MEMO_TYPES = { 'memoId': 'id', 'memoText': 'text', 'memoHash': 'hash', 'memoReturn': 'return' }
6
+
7
+ DEFAULT_FEATURES = { skip_deposit_collection: false }.freeze
8
+
9
+ def initialize(custom_features = {})
10
+ @features = DEFAULT_FEATURES.merge(custom_features).slice(*SUPPORTED_FEATURES)
11
+ @settings = {}
12
+ end
13
+
14
+ def configure(settings = {})
15
+ # Clean client state during configure.
16
+ @client = nil
17
+
18
+ @settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
19
+
20
+ @wallet = @settings.fetch(:wallet) do
21
+ raise Peatio::Wallet::MissingSettingError, :wallet
22
+ end.slice(:uri, :address, :secret, :access_token, :wallet_id, :testnet)
23
+
24
+ @currency = @settings.fetch(:currency) do
25
+ raise Peatio::Wallet::MissingSettingError, :currency
26
+ end.slice(:id, :base_factor, :code, :options)
27
+ end
28
+
29
+ def create_address!(options = {})
30
+ currency = erc20_currency_id
31
+ options.deep_symbolize_keys!
32
+
33
+ if options.dig(:pa_details, :address_id).present? &&
34
+ options.dig(:pa_details, :updated_at).present? &&
35
+ time_difference_in_minutes(options.dig(:pa_details, :updated_at)) >= TIME_DIFFERENCE_IN_MINUTES
36
+
37
+ response = client.rest_api(:get, "#{currency}/wallet/#{wallet_id}/address/#{options.dig(:pa_details, :address_id)}")
38
+ { address: response['address'], secret: bitgo_wallet_passphrase }
39
+ elsif options.dig(:pa_details, :address_id).blank?
40
+ response = client.rest_api(:post, "#{currency}/wallet/#{wallet_id}/address")
41
+ { address: response['address'], secret: bitgo_wallet_passphrase, details: { address_id: response['id'] }}
42
+ end
43
+ rescue Bitgo::Client::Error => e
44
+ raise Peatio::Wallet::ClientError, e
45
+ end
46
+
47
+ def create_transaction!(transaction, options = {})
48
+ currency_options = @currency.fetch(:options).slice(:gas_limit, :gas_price, :erc20_contract_address)
49
+
50
+ if currency_options[:gas_limit].present? && currency_options[:gas_price].present?
51
+ options.merge!(currency_options)
52
+ create_eth_transaction(transaction, options)
53
+ else
54
+ amount = convert_to_base_unit(transaction.amount)
55
+
56
+ if options[:subtract_fee].to_s == 'true'
57
+ fee = build_raw_transaction(transaction)
58
+ baseFeeInfo = fee.dig('feeInfo','fee')
59
+ fee = baseFeeInfo.present? ? baseFeeInfo : fee.dig('txInfo','Fee')
60
+ amount -= fee.to_i
61
+ end
62
+
63
+ response = client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/sendcoins", {
64
+ address: normalize_address(transaction.to_address.to_s),
65
+ amount: amount.to_s,
66
+ walletPassphrase: bitgo_wallet_passphrase,
67
+ memo: xlm_memo(transaction.to_address.to_s)
68
+ }.compact)
69
+
70
+ if response['feeString'].present?
71
+ fee = convert_from_base_unit(response['feeString'])
72
+ transaction.fee = fee
73
+ end
74
+
75
+ transaction.hash = normalize_txid(response['txid'])
76
+ transaction.fee_currency_id = erc20_currency_id
77
+ transaction
78
+ end
79
+ rescue Bitgo::Client::Error => e
80
+ raise Peatio::Wallet::ClientError, e
81
+ end
82
+
83
+
84
+ def build_raw_transaction(transaction)
85
+ client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/tx/build", {
86
+ recipients: [{
87
+ address: transaction.to_address,
88
+ amount: convert_to_base_unit(transaction.amount).to_s
89
+ }]
90
+ }.compact)
91
+ end
92
+
93
+ def create_eth_transaction(transaction, options = {})
94
+ amount = convert_to_base_unit(transaction.amount)
95
+ hop = true unless options.slice(:gas_price).present?
96
+
97
+ fee_estimate = fee_estimate(amount.to_s, hop)
98
+
99
+ if transaction.options.present? && transaction.options[:gas_price].present?
100
+ options[:gas_price] = transaction.options[:gas_price]
101
+ else
102
+ options[:gas_price] = fee_estimate['minGasPrice'].to_i
103
+ end
104
+
105
+ response = client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/sendcoins", {
106
+ address: transaction.to_address.to_s,
107
+ amount: amount.to_s,
108
+ walletPassphrase: bitgo_wallet_passphrase,
109
+ gas: options.fetch(:gas_limit).to_i,
110
+ gasPrice: options.fetch(:gas_price).to_i,
111
+ hop: hop
112
+ }.compact)
113
+
114
+ if response['feeString'].present?
115
+ fee = convert_from_base_unit(response['feeString'])
116
+ transaction.fee = fee
117
+ end
118
+
119
+ transaction.hash = normalize_txid(response['txid'])
120
+ transaction.fee_currency_id = erc20_currency_id
121
+ transaction.options = options
122
+ transaction
123
+ end
124
+
125
+ def fee_estimate(amount, hop)
126
+ client.rest_api(:get, "#{erc20_currency_id}/tx/fee", { amount: amount, hop: hop }.compact)
127
+ end
128
+
129
+ def load_balance!
130
+ if @currency.fetch(:options).slice(:erc20_contract_address).present?
131
+ load_erc20_balance!
132
+ else
133
+ response = client.rest_api(:get, "#{currency_id}/wallet/#{wallet_id}")
134
+ convert_from_base_unit(response.fetch('balanceString'))
135
+ end
136
+ rescue Bitgo::Client::Error => e
137
+ raise Peatio::Wallet::ClientError, e
138
+ end
139
+
140
+ def load_erc20_balance!
141
+ response = client.rest_api(:get, "#{erc20_currency_id}/wallet/#{wallet_id}?allTokens=true")
142
+ convert_from_base_unit(response.dig('tokens', currency_id, 'balanceString'))
143
+ rescue Bitgo::Client::Error => e
144
+ raise Peatio::Wallet::ClientError, e
145
+ end
146
+
147
+ def trigger_webhook_event(request)
148
+ currency = @wallet.fetch(:testnet).present? ? 't' + @currency.fetch(:id) : @currency.fetch(:id)
149
+ if request.params['type'] == 'transfer'
150
+ return unless currency == request.params['coin'] &&
151
+ @wallet.fetch(:wallet_id) == request.params['wallet']
152
+ else
153
+ return unless @wallet.fetch(:wallet_id) == request.params['walletId']
154
+ end
155
+
156
+ if request.params['type'] == 'transfer'
157
+ transactions = fetch_transfer!(request.params['transfer'])
158
+ return transactions
159
+ elsif request.params['type'] == 'address_confirmation'
160
+ address_id = fetch_address_id(request.params['address'])
161
+ return { address_id: address_id, address: request.params['address'], currency_id: currency_id }
162
+ end
163
+ end
164
+
165
+ def register_webhooks!(url)
166
+ transfer_webhook(url)
167
+ address_confirmation_webhook(url)
168
+ end
169
+
170
+ def fetch_address_id(address)
171
+ currency = erc20_currency_id
172
+ client.rest_api(:get, "#{currency}/wallet/#{wallet_id}/address/#{address}")
173
+ .fetch('id')
174
+ rescue Bitgo::Client::Error => e
175
+ raise Peatio::Wallet::ClientError, e
176
+ end
177
+
178
+ def fetch_transfer!(id)
179
+ response = client.rest_api(:get, "#{currency_id}/wallet/#{wallet_id}/transfer/#{id}")
180
+ parse_entries(response['entries']).map do |entry|
181
+ to_address = if response.dig('coinSpecific', 'memo').present?
182
+ memo = response.dig('coinSpecific', 'memo')
183
+ memo_type = memo.kind_of?(Array) ? memo.first : memo
184
+ build_address(entry['address'], memo_type)
185
+ else
186
+ entry['address']
187
+ end
188
+ state = define_transaction_state(response['state'])
189
+
190
+ if response['outputs'].present?
191
+ output = response['outputs'].find { |out| out['address'] == to_address }
192
+ txout = output['index'] if output.present?
193
+ end
194
+
195
+ if response['feeString'].present?
196
+ fee = convert_from_base_unit(response['feeString']) / response['entries'].count
197
+ end
198
+
199
+ transaction = Peatio::Transaction.new(
200
+ currency_id: @currency.fetch(:id),
201
+ amount: convert_from_base_unit(entry['valueString']),
202
+ fee: fee,
203
+ fee_currency_id: erc20_currency_id,
204
+ hash: normalize_txid(response['txid']),
205
+ to_address: to_address,
206
+ block_number: response['height'],
207
+ txout: txout.to_i,
208
+ status: state
209
+ )
210
+
211
+ transaction if transaction.valid?
212
+ end.compact
213
+ rescue Bitgo::Client::Error => e
214
+ raise Peatio::Wallet::ClientError, e
215
+ end
216
+
217
+ def transfer_webhook(url)
218
+ client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/webhooks", {
219
+ type: 'transfer',
220
+ allToken: true,
221
+ url: url,
222
+ label: "webhook for #{url}",
223
+ listenToFailureStates: false
224
+ })
225
+ end
226
+
227
+ def address_confirmation_webhook(url)
228
+ client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/webhooks", {
229
+ type: 'address_confirmation',
230
+ allToken: true,
231
+ url: url,
232
+ label: "webhook for #{url}",
233
+ listenToFailureStates: false
234
+ })
235
+ end
236
+
237
+ def parse_entries(entries)
238
+ entries.map do |e|
239
+ e if e["valueString"].to_i.positive?
240
+ end.compact
241
+ end
242
+
243
+ private
244
+
245
+ def client
246
+ uri = @wallet.fetch(:uri) { raise Peatio::Wallet::MissingSettingError, :uri }
247
+ access_token = @wallet.fetch(:access_token) { raise Peatio::Wallet::MissingSettingError, :access_token }
248
+
249
+ currency_code_prefix = @wallet.fetch(:testnet) ? 't' : ''
250
+ uri = uri.gsub(/\/+\z/, '') + '/' + currency_code_prefix
251
+ @client ||= Client.new(uri, access_token)
252
+ end
253
+
254
+ def build_address(address, memo)
255
+ "#{address}?memoId=#{memo['value']}"
256
+ end
257
+
258
+ # All these functions will have to be done with the coin set to eth or teth
259
+ # since that is the actual coin type being used.
260
+ def erc20_currency_id
261
+ return 'eth' if @currency.fetch(:options).slice(:erc20_contract_address).present?
262
+
263
+ currency_id
264
+ end
265
+
266
+ def xlm_memo(address)
267
+ build_xlm_memo(address) if @currency.fetch(:id) == 'xlm'
268
+ end
269
+
270
+ def build_xlm_memo(address)
271
+ case address.split('?').last.split('=').first
272
+ when 'memoId'
273
+ memo_value_from(address, 'memoId')
274
+ when 'memoText'
275
+ memo_value_from(address, 'memoText')
276
+ when 'memoHash'
277
+ memo_value_from(address, 'memoHash')
278
+ when 'memoReturn'
279
+ memo_value_from(address, 'memoReturn')
280
+ end
281
+ end
282
+
283
+ def memo_value_from(address, type)
284
+ memo_value = address.partition(type + '=').last
285
+ return { type: XLM_MEMO_TYPES[type.to_sym], value: memo_value } if memo_value.present?
286
+ end
287
+
288
+ def currency_id
289
+ @currency.fetch(:id) { raise Peatio::Wallet::MissingSettingError, :id }
290
+ end
291
+
292
+ def bitgo_wallet_passphrase
293
+ @wallet.fetch(:secret)
294
+ end
295
+
296
+ def wallet_id
297
+ @wallet.fetch(:wallet_id)
298
+ end
299
+
300
+ def normalize_address(address)
301
+ if @currency.fetch(:id) == 'xlm'
302
+ address.split('?').first
303
+ else
304
+ address
305
+ end
306
+ end
307
+
308
+ def normalize_txid(txid)
309
+ txid.downcase
310
+ end
311
+
312
+ def convert_from_base_unit(value)
313
+ value.to_d / @currency.fetch(:base_factor)
314
+ end
315
+
316
+ def convert_to_base_unit(value)
317
+ x = value.to_d * @currency.fetch(:base_factor)
318
+ unless (x % 1).zero?
319
+ raise Peatio::WalletClient::Error,
320
+ "Failed to convert value to base (smallest) unit because it exceeds the maximum precision: " \
321
+ "#{value.to_d} - #{x.to_d} must be equal to zero."
322
+ end
323
+ x.to_i
324
+ end
325
+
326
+ def time_difference_in_minutes(updated_at)
327
+ (Time.now - updated_at)/60
328
+ end
329
+
330
+ def define_transaction_state(state)
331
+ case state
332
+ when 'unconfirmed'
333
+ 'pending'
334
+ when 'confirmed'
335
+ 'success'
336
+ when 'failed','rejected'
337
+ 'failed'
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,16 @@
1
+ require "peatio"
2
+
3
+ module Peatio
4
+ module Bitgo
5
+ require "bigdecimal"
6
+ require "bigdecimal/util"
7
+
8
+ require "peatio/bitgo/blockchain"
9
+ require "peatio/bitgo/client"
10
+ require "peatio/bitgo/wallet"
11
+
12
+ require "peatio/bitgo/hooks"
13
+
14
+ require "peatio/bitgo/version"
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "peatio/bitgo/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ouroboros-peatio-bitgo"
8
+ spec.version = Peatio::Bitgo::VERSION
9
+ spec.authors = ["Nadia Ch., Maksym N."]
10
+ spec.email = ["nchumak@heliostech.fr"]
11
+
12
+ spec.summary = %q{Gem for extending Peatio plugable system with Bitgo implementation.}
13
+ spec.description = %q{Bitgo Peatio gem which implements Peatio::Blockchain::Abstract & Peatio::Wallet::Abstract.}
14
+ spec.homepage = "https://openware.com/"
15
+ spec.license = "MIT"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "activesupport", "~> 5.2.3"
27
+ spec.add_dependency "better-faraday", "~> 1.0.5"
28
+ spec.add_dependency "faraday", "~> 0.17"
29
+ spec.add_dependency "memoist", "~> 0.16.0"
30
+ spec.add_dependency "peatio", ">= 0.6.3"
31
+ spec.add_dependency 'net-http-persistent', '~> 3.0.1'
32
+
33
+ spec.add_development_dependency "bundler", "~> 1.16"
34
+ spec.add_development_dependency "mocha", "~> 1.8"
35
+ spec.add_development_dependency "pry-byebug"
36
+ spec.add_development_dependency "rake", "~> 13.0"
37
+ spec.add_development_dependency "rspec", "~> 3.0"
38
+ spec.add_development_dependency "webmock", "~> 3.5"
39
+ end