ouroboros-peatio-bitgo 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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_2xx!
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 = "3.1.0".freeze
4
+ end
5
+ end
@@ -0,0 +1,345 @@
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
+ # Retrieve testnet status
149
+ testnet_status = @wallet.fetch(:testnet)
150
+ # Determine if the 't' prefix should be applied
151
+ currency = (testnet_status == false || testnet_status.nil? || (testnet_status.is_a?(String) && testnet_status.downcase == "false")) ? @currency.fetch(:id) : 't' + @currency.fetch(:id)
152
+ if request.params['type'] == 'transfer'
153
+ return unless currency == request.params['coin'] &&
154
+ @wallet.fetch(:wallet_id) == request.params['wallet']
155
+ else
156
+ return unless @wallet.fetch(:wallet_id) == request.params['walletId']
157
+ end
158
+
159
+ if request.params['type'] == 'transfer'
160
+ transactions = fetch_transfer!(request.params['transfer'])
161
+ return transactions
162
+ elsif request.params['type'] == 'address_confirmation'
163
+ address_id = fetch_address_id(request.params['address'])
164
+ return { address_id: address_id, address: request.params['address'], currency_id: currency_id }
165
+ end
166
+ end
167
+
168
+ def register_webhooks!(url)
169
+ transfer_webhook(url)
170
+ address_confirmation_webhook(url)
171
+ end
172
+
173
+ def fetch_address_id(address)
174
+ currency = erc20_currency_id
175
+ client.rest_api(:get, "#{currency}/wallet/#{wallet_id}/address/#{address}")
176
+ .fetch('id')
177
+ rescue Bitgo::Client::Error => e
178
+ raise Peatio::Wallet::ClientError, e
179
+ end
180
+
181
+ def fetch_transfer!(id)
182
+ response = client.rest_api(:get, "#{currency_id}/wallet/#{wallet_id}/transfer/#{id}")
183
+ parse_entries(response['entries']).map do |entry|
184
+ to_address = if response.dig('coinSpecific', 'memo').present?
185
+ memo = response.dig('coinSpecific', 'memo')
186
+ memo_type = memo.kind_of?(Array) ? memo.first : memo
187
+ build_address(entry['address'], memo_type)
188
+ else
189
+ entry['address']
190
+ end
191
+ state = define_transaction_state(response['state'])
192
+
193
+ if response['outputs'].present?
194
+ output = response['outputs'].find { |out| out['address'] == to_address }
195
+ txout = output['index'] if output.present?
196
+ end
197
+
198
+ if response['feeString'].present?
199
+ fee = convert_from_base_unit(response['feeString']) / response['entries'].count
200
+ end
201
+
202
+ transaction = Peatio::Transaction.new(
203
+ currency_id: @currency.fetch(:id),
204
+ amount: convert_from_base_unit(entry['valueString']),
205
+ fee: fee,
206
+ fee_currency_id: erc20_currency_id,
207
+ hash: normalize_txid(response['txid']),
208
+ to_address: to_address,
209
+ block_number: response['height'],
210
+ txout: txout.to_i,
211
+ status: state
212
+ )
213
+
214
+ transaction if transaction.valid?
215
+ end.compact
216
+ rescue Bitgo::Client::Error => e
217
+ raise Peatio::Wallet::ClientError, e
218
+ end
219
+
220
+ def transfer_webhook(url)
221
+ client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/webhooks", {
222
+ type: 'transfer',
223
+ allToken: true,
224
+ url: url,
225
+ label: "webhook for #{url}",
226
+ listenToFailureStates: false
227
+ })
228
+ end
229
+
230
+ def address_confirmation_webhook(url)
231
+ client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/webhooks", {
232
+ type: 'address_confirmation',
233
+ allToken: true,
234
+ url: url,
235
+ label: "webhook for #{url}",
236
+ listenToFailureStates: false
237
+ })
238
+ end
239
+
240
+ def parse_entries(entries)
241
+ entries.map do |e|
242
+ e if e["valueString"].to_i.positive?
243
+ end.compact
244
+ end
245
+
246
+ private
247
+
248
+ def client
249
+ uri = @wallet.fetch(:uri) { raise Peatio::Wallet::MissingSettingError, :uri }
250
+ access_token = @wallet.fetch(:access_token) { raise Peatio::Wallet::MissingSettingError, :access_token }
251
+
252
+ currency_code_prefix = @wallet.fetch(:testnet) && @wallet.fetch(:testnet).to_s.downcase != "false" ? 't' : ''
253
+ uri = uri.gsub(/\/+\z/, '') + '/' + currency_code_prefix
254
+ @client ||= Client.new(uri, access_token)
255
+ end
256
+
257
+ def build_address(address, memo)
258
+ "#{address}?memoId=#{memo['value']}"
259
+ end
260
+
261
+ # All these functions will have to be done with the coin set to eth or teth
262
+ # since that is the actual coin type being used.
263
+ def erc20_currency_id
264
+ return 'eth' if @currency.fetch(:options).slice(:erc20_contract_address).present?
265
+
266
+ currency_id
267
+ end
268
+
269
+ def xlm_memo(address)
270
+ build_xlm_memo(address) if @currency.fetch(:id) == 'xlm'
271
+ end
272
+
273
+ def build_xlm_memo(address)
274
+ case address.split('?').last.split('=').first
275
+ when 'memoId'
276
+ memo_value_from(address, 'memoId')
277
+ when 'memoText'
278
+ memo_value_from(address, 'memoText')
279
+ when 'memoHash'
280
+ memo_value_from(address, 'memoHash')
281
+ when 'memoReturn'
282
+ memo_value_from(address, 'memoReturn')
283
+ end
284
+ end
285
+
286
+ def memo_value_from(address, type)
287
+ memo_value = address.partition(type + '=').last
288
+ return { type: XLM_MEMO_TYPES[type.to_sym], value: memo_value } if memo_value.present?
289
+ end
290
+
291
+ def currency_id
292
+ @currency.fetch(:id) { raise Peatio::Wallet::MissingSettingError, :id }
293
+ end
294
+
295
+ def bitgo_wallet_passphrase
296
+ @wallet.fetch(:secret)
297
+ end
298
+
299
+ def wallet_id
300
+ @wallet.fetch(:wallet_id)
301
+ end
302
+
303
+ def normalize_address(address)
304
+ if @currency.fetch(:id) == 'xlm'
305
+ address.split('?').first
306
+ else
307
+ address
308
+ end
309
+ end
310
+
311
+ def normalize_txid(txid)
312
+ txid.downcase
313
+ end
314
+
315
+ def convert_from_base_unit(value)
316
+ value.to_d / @currency.fetch(:base_factor)
317
+ end
318
+
319
+ def convert_to_base_unit(value)
320
+ x = value.to_d * @currency.fetch(:base_factor)
321
+ unless (x % 1).zero?
322
+ raise Peatio::WalletClient::Error,
323
+ "Failed to convert value to base (smallest) unit because it exceeds the maximum precision: " \
324
+ "#{value.to_d} - #{x.to_d} must be equal to zero."
325
+ end
326
+ x.to_i
327
+ end
328
+
329
+ def time_difference_in_minutes(updated_at)
330
+ (Time.now - updated_at)/60
331
+ end
332
+
333
+ def define_transaction_state(state)
334
+ case state
335
+ when 'unconfirmed'
336
+ 'pending'
337
+ when 'confirmed'
338
+ 'success'
339
+ when 'failed','rejected'
340
+ 'failed'
341
+ end
342
+ end
343
+ end
344
+ end
345
+ 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 = ["ISPOS"]
10
+ spec.email = ["contacto@ispos.mx"]
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://ispos.mx/"
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", "~> 6.1.0"
27
+ spec.add_dependency "better-faraday", "~> 2.0.1"
28
+ spec.add_dependency "faraday", "~> 1.10"
29
+ spec.add_dependency "memoist", "~> 0.16.0"
30
+ spec.add_dependency "peatio", ">= 3.1.1"
31
+ spec.add_dependency 'net-http-persistent', '~> 4.0.1'
32
+
33
+ spec.add_development_dependency "bundler", "~> 2.4.7"
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