peatio-bitgo-jruby 2.5.1

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
+
2
+ - name: Ethereum Deposit Wallet
3
+ blockchain_key: eth-rinkeby
4
+ currency_id: eth
5
+ # Address where deposits will be collected to.
6
+ address: '0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C' # IMPORTANT: Always wrap this value in quotes!
7
+ kind: deposit # Wallet kind (deposit, hot, warm, cold or fee).
8
+ max_balance: 0.0
9
+ status: active
10
+ gateway: geth # Gateway client name.
11
+ settings:
12
+ #
13
+ # Geth gateway client settings.
14
+ uri: http://127.0.0.1:8545
15
+ secret: 'changeme'
16
+ testnet: true
17
+ access_token: 'v2x659261647b540ee3acda5c50ae4e878we323474eea5cbff9b9615139629'
18
+ wallet_id: '5e5388ad80334347ceb3540c741d'
19
+
20
+ - name: Ethereum Hot Wallet
21
+ blockchain_key: eth-rinkeby
22
+ currency_id: eth
23
+ # Address where deposits will be collected to.
24
+ address: '0x270704935783087a01c7a28d8f2d8f01670c8050' # IMPORTANT: Always wrap this value in quotes!
25
+ kind: hot # Wallet kind (deposit, hot, warm, cold or fee).
26
+ max_balance: 100.0
27
+ status: active
28
+ gateway: geth # Gateway client name.
29
+ settings:
30
+ #
31
+ # Geth gateway client settings.
32
+ uri: http://127.0.0.1:8545
33
+ secret: 'test'
34
+ testnet: true
35
+ access_token: 'v2x659261647b540ee3acda5c50ae4e878we323474eea5cbff9b9615139629'
36
+ wallet_id: '5e5388ad80334347ceb3540c741d'
37
+
38
+ - name: Ethereum Warm Wallet
39
+ blockchain_key: eth-rinkeby
40
+ currency_id: eth
41
+ # Address where deposits will be collected to.
42
+ address: '0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C' # IMPORTANT: Always wrap this value in quotes!
43
+ kind: warm # Wallet kind (deposit, hot, warm, cold or fee).
44
+ max_balance: 1000.0
45
+ status: active
46
+ gateway: geth # Gateway client name.
47
+ settings:
48
+ #
49
+ # Geth gateway client settings.
50
+ uri: http://127.0.0.1:8545
51
+ secret: 'test'
52
+ testnet: true
53
+ access_token: 'v2x659261647b540ee3acda5c50ae4e878we323474eea5cbff9b9615139629'
54
+ wallet_id: '5e5388ad80334347ceb3540c741d'
@@ -0,0 +1,36 @@
1
+ ## Bitgo wallet configuration
2
+
3
+ 1. Login to your bitgo account
4
+ 2. Click in create wallet button
5
+ ![scheme](images/create_wallet.png)
6
+ 3. Choose wallet for appropriate currency
7
+ ![scheme](images/choose_wallet.png)
8
+ 4. Setup your wallet
9
+ ![scheme](images/setup_wallet.png)
10
+ 5. Put name of your wallet
11
+ ![scheme](images/wallet_name.png)
12
+ 6. Put password of your wallet
13
+ ![scheme](images/wallet_secret.png)
14
+ P.S. You should save this password for future wallet configuration
15
+
16
+ ## Peatio BITGO wallet configuration
17
+
18
+ 1. Go to tower admin panel Settings -> Wallets -> Add wallet
19
+ * Uri == Bitgo service URI
20
+ * Secret == Wallet password
21
+ * Bitgo Wallet Id
22
+ ![scheme](images/wallet_id.png)
23
+ * Bitgo Access Token
24
+ ![scheme](images/wallet_access_token.png)
25
+ ![scheme](images/create_wallet_access_token.png)
26
+ ![scheme](images/access_token.png)
27
+
28
+ ## Webhook configuration
29
+
30
+ ![scheme](images/webhook.png)
31
+ ![scheme](images/webhook_creating.png)
32
+
33
+ Where url should be "https://{host_url}/api/v2/peatio/public/webhooks/{event}"
34
+
35
+ * For deposit wallets event should be 'deposit'
36
+ * For hot wallets event should be 'withdraw'
Binary file
Binary file
Binary file
@@ -0,0 +1,29 @@
1
+ # Integration.
2
+
3
+ For Peatio bitgo plugin integration you need to do the following steps:
4
+
5
+ ## Image Build.
6
+
7
+ 1. Add peatio-bitgo gem into your Gemfile.plugin
8
+ ```ruby
9
+ gem 'peatio-bitgo', '~> 0.1.0'
10
+ ```
11
+
12
+ 2. Run `bundle install` for updating Gemfile.lock
13
+
14
+ 3. Build custom Peatio [docker image with bitgo plugin](https://github.com/rubykube/peatio/blob/master/docs/plugins.md#build)
15
+
16
+ 4. Push your image using `docker push`
17
+
18
+ 5. Update your deployment to use image with peatio-bitgo gem
19
+
20
+ ## Peatio Configuration.
21
+
22
+ 1. Create bitgo Blockchain [config example](../config/blockchains.yml).
23
+ * No additional steps are needed
24
+
25
+ 2. Create bitgo Currency [config example](../config/currencies.yml).
26
+ * No additional steps are needed
27
+
28
+ 3. Create bitgo Wallets [config example](../config/wallets.yml)(deposit and hot wallets are required).
29
+ * No additional steps are needed
@@ -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,21 @@
1
+
2
+ module Peatio
3
+ module Bitgo
4
+ # TODO: Processing of unconfirmed transactions from mempool isn't supported now.
5
+ class Blockchain < Peatio::Blockchain::Abstract
6
+
7
+ DEFAULT_FEATURES = {case_sensitive: true, cash_addr_format: 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
+ @settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,56 @@
1
+ require 'faraday'
2
+ require 'better-faraday'
3
+
4
+ module Peatio
5
+ module Bitgo
6
+ class Client
7
+ Error = Class.new(StandardError)
8
+ class ConnectionError < Error; end
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
+ rescue StandardError => e
52
+ raise Error, e
53
+ end
54
+ end
55
+ end
56
+ 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 requiremnt was not suttisfied 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 requiremnt was not suttisfied 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.5.1"
4
+ end
5
+ end
@@ -0,0 +1,300 @@
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
+ def initialize(settings = {})
8
+ @settings = settings
9
+ end
10
+
11
+ def configure(settings = {})
12
+ # Clean client state during configure.
13
+ @client = nil
14
+
15
+ @settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
16
+
17
+ @wallet = @settings.fetch(:wallet) do
18
+ raise Peatio::Wallet::MissingSettingError, :wallet
19
+ end.slice(:uri, :address, :secret, :access_token, :wallet_id, :testnet)
20
+
21
+ @currency = @settings.fetch(:currency) do
22
+ raise Peatio::Wallet::MissingSettingError, :currency
23
+ end.slice(:id, :base_factor, :code, :options)
24
+ end
25
+
26
+ def create_address!(options = {})
27
+ currency = erc20_currency_id
28
+ options.deep_symbolize_keys!
29
+
30
+ if options.dig(:pa_details, :address_id).present? &&
31
+ options.dig(:pa_details, :updated_at).present? &&
32
+ time_difference_in_minutes(options.dig(:pa_details, :updated_at)) >= TIME_DIFFERENCE_IN_MINUTES
33
+
34
+ response = client.rest_api(:get, "#{currency}/wallet/#{wallet_id}/address/#{options.dig(:pa_details, :address_id)}")
35
+ { address: response['address'], secret: bitgo_wallet_passphrase }
36
+ elsif options.dig(:pa_details, :address_id).blank?
37
+ response = client.rest_api(:post, "#{currency}/wallet/#{wallet_id}/address")
38
+ { address: response['address'], secret: bitgo_wallet_passphrase, details: { address_id: response['id'] }}
39
+ end
40
+ rescue Bitgo::Client::Error => e
41
+ raise Peatio::Wallet::ClientError, e
42
+ end
43
+
44
+ def create_transaction!(transaction, options = {})
45
+ currency_options = @currency.fetch(:options).slice(:gas_limit, :gas_price, :erc20_contract_address)
46
+
47
+ if currency_options[:gas_limit].present? && currency_options[:gas_price].present?
48
+ options.merge!(currency_options)
49
+ create_eth_transaction(transaction, options)
50
+ else
51
+ amount = convert_to_base_unit(transaction.amount)
52
+
53
+ if options[:subtract_fee].to_s == 'true'
54
+ fee = build_raw_transaction(transaction)
55
+ baseFeeInfo = fee.dig('feeInfo','fee')
56
+ fee = baseFeeInfo.present? ? baseFeeInfo : fee.dig('txInfo','Fee')
57
+ amount -= fee.to_i
58
+ end
59
+
60
+ txid = client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/sendcoins", {
61
+ address: normalize_address(transaction.to_address.to_s),
62
+ amount: amount.to_s,
63
+ walletPassphrase: bitgo_wallet_passphrase,
64
+ memo: xlm_memo(transaction.to_address.to_s)
65
+ }.compact).fetch('txid')
66
+
67
+ transaction.hash = normalize_txid(txid)
68
+ transaction
69
+ end
70
+ rescue Bitgo::Client::Error => e
71
+ raise Peatio::Wallet::ClientError, e
72
+ end
73
+
74
+
75
+ def build_raw_transaction(transaction)
76
+ client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/tx/build", {
77
+ recipients: [{
78
+ address: transaction.to_address,
79
+ amount: convert_to_base_unit(transaction.amount).to_s
80
+ }]
81
+ }.compact)
82
+ end
83
+
84
+ def create_eth_transaction(transaction, options = {})
85
+ amount = convert_to_base_unit(transaction.amount)
86
+ hop = true unless options.slice(:erc20_contract_address).present?
87
+
88
+ txid = client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/sendcoins", {
89
+ address: transaction.to_address.to_s,
90
+ amount: amount.to_s,
91
+ walletPassphrase: bitgo_wallet_passphrase,
92
+ gas: options.fetch(:gas_limit).to_i,
93
+ gasPrice: options.fetch(:gas_price).to_i,
94
+ hop: hop
95
+ }.compact).fetch('txid')
96
+
97
+ transaction.hash = normalize_txid(txid)
98
+ transaction
99
+ end
100
+
101
+ def load_balance!
102
+ if @currency.fetch(:options).slice(:erc20_contract_address).present?
103
+ load_erc20_balance!
104
+ else
105
+ response = client.rest_api(:get, "#{currency_id}/wallet/#{wallet_id}")
106
+ convert_from_base_unit(response.fetch('balanceString'))
107
+ end
108
+ rescue Bitgo::Client::Error => e
109
+ raise Peatio::Wallet::ClientError, e
110
+ end
111
+
112
+ def load_erc20_balance!
113
+ response = client.rest_api(:get, "#{erc20_currency_id}/wallet/#{wallet_id}?allTokens=true")
114
+ convert_from_base_unit(response.dig('tokens', currency_id, 'balanceString'))
115
+ rescue Bitgo::Client::Error => e
116
+ raise Peatio::Wallet::ClientError, e
117
+ end
118
+
119
+ def trigger_webhook_event(event)
120
+ currency = @wallet.fetch(:testnet).present? ? 't' + @currency.fetch(:id) : @currency.fetch(:id)
121
+ return unless currency == event['coin'] && @wallet.fetch(:wallet_id) == event['wallet']
122
+
123
+ if event['type'] == 'transfer'
124
+ transactions = fetch_transfer!(event['transfer'])
125
+ return { transfers: transactions }
126
+ elsif event['type'] == 'address_confirmation'
127
+ address_id = fetch_address_id(event['address'])
128
+ return { address_id: address_id, currency_id: currency_id }
129
+ end
130
+ end
131
+
132
+ def register_webhooks!(url)
133
+ transfer_webhook(url)
134
+ address_confirmation_webhook(url)
135
+ end
136
+
137
+ def fetch_address_id(address)
138
+ currency = erc20_currency_id
139
+ client.rest_api(:get, "#{currency}/wallet/#{wallet_id}/address/#{address}")
140
+ .fetch('id')
141
+ rescue Bitgo::Client::Error => e
142
+ raise Peatio::Wallet::ClientError, e
143
+ end
144
+
145
+ def fetch_transfer!(id)
146
+ # TODO: Add Rspecs for this one
147
+ response = client.rest_api(:get, "#{currency_id}/wallet/#{wallet_id}/transfer/#{id}")
148
+ parse_entries(response['entries']).map do |entry|
149
+ to_address = if response.dig('coinSpecific', 'memo').present?
150
+ memo = response.dig('coinSpecific', 'memo')
151
+ memo_type = memo.kind_of?(Array) ? memo.first : memo
152
+ build_address(entry['address'], memo_type)
153
+ else
154
+ entry['address']
155
+ end
156
+ state = define_transaction_state(response['state'])
157
+
158
+ transaction = Peatio::Transaction.new(
159
+ currency_id: @currency.fetch(:id),
160
+ amount: convert_from_base_unit(entry['valueString']),
161
+ hash: normalize_txid(response['txid']),
162
+ to_address: to_address,
163
+ block_number: response['height'],
164
+ # TODO: Add sendmany support
165
+ txout: 0,
166
+ status: state
167
+ )
168
+
169
+ transaction if transaction.valid?
170
+ end.compact
171
+ rescue Bitgo::Client::Error => e
172
+ raise Peatio::Wallet::ClientError, e
173
+ end
174
+
175
+ def transfer_webhook(url)
176
+ client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/webhooks", {
177
+ type: 'transfer',
178
+ allToken: true,
179
+ url: url,
180
+ label: "webhook for #{url}",
181
+ listenToFailureStates: false
182
+ })
183
+ end
184
+
185
+ def address_confirmation_webhook(url)
186
+ client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/webhooks", {
187
+ type: 'address_confirmation',
188
+ allToken: true,
189
+ url: url,
190
+ label: "webhook for #{url}",
191
+ listenToFailureStates: false
192
+ })
193
+ end
194
+
195
+ def parse_entries(entries)
196
+ entries.map do |e|
197
+ e if e["valueString"].to_i.positive?
198
+ end.compact
199
+ end
200
+
201
+ private
202
+
203
+ def client
204
+ uri = @wallet.fetch(:uri) { raise Peatio::Wallet::MissingSettingError, :uri }
205
+ access_token = @wallet.fetch(:access_token) { raise Peatio::Wallet::MissingSettingError, :access_token }
206
+
207
+ currency_code_prefix = @wallet.fetch(:testnet) ? 't' : ''
208
+ uri = uri.gsub(/\/+\z/, '') + '/' + currency_code_prefix
209
+ @client ||= Client.new(uri, access_token)
210
+ end
211
+
212
+ def build_address(address, memo)
213
+ "#{address}?memoId=#{memo['value']}"
214
+ end
215
+
216
+ # All these functions will have to be done with the coin set to eth or teth
217
+ # since that is the actual coin type being used.
218
+ def erc20_currency_id
219
+ return 'eth' if @currency.fetch(:options).slice(:erc20_contract_address).present?
220
+
221
+ currency_id
222
+ end
223
+
224
+ def xlm_memo(address)
225
+ build_xlm_memo(address) if @currency.fetch(:id) == 'xlm'
226
+ end
227
+
228
+ def build_xlm_memo(address)
229
+ case address.split('?').last.split('=').first
230
+ when 'memoId'
231
+ memo_value_from(address, 'memoId')
232
+ when 'memoText'
233
+ memo_value_from(address, 'memoText')
234
+ when 'memoHash'
235
+ memo_value_from(address, 'memoHash')
236
+ when 'memoReturn'
237
+ memo_value_from(address, 'memoReturn')
238
+ end
239
+ end
240
+
241
+ def memo_value_from(address, type)
242
+ memo_value = address.partition(type + '=').last
243
+ return { type: XLM_MEMO_TYPES[type.to_sym], value: memo_value } if memo_value.present?
244
+ end
245
+
246
+ def currency_id
247
+ @currency.fetch(:id) { raise Peatio::Wallet::MissingSettingError, :id }
248
+ end
249
+
250
+ def bitgo_wallet_passphrase
251
+ @wallet.fetch(:secret)
252
+ end
253
+
254
+ def wallet_id
255
+ @wallet.fetch(:wallet_id)
256
+ end
257
+
258
+ def normalize_address(address)
259
+ if @currency.fetch(:id) == 'xlm'
260
+ address.split('?').first
261
+ else
262
+ address
263
+ end
264
+ end
265
+
266
+ def normalize_txid(txid)
267
+ txid.downcase
268
+ end
269
+
270
+ def convert_from_base_unit(value)
271
+ value.to_d / @currency.fetch(:base_factor)
272
+ end
273
+
274
+ def convert_to_base_unit(value)
275
+ x = value.to_d * @currency.fetch(:base_factor)
276
+ unless (x % 1).zero?
277
+ raise Peatio::WalletClient::Error,
278
+ "Failed to convert value to base (smallest) unit because it exceeds the maximum precision: " \
279
+ "#{value.to_d} - #{x.to_d} must be equal to zero."
280
+ end
281
+ x.to_i
282
+ end
283
+
284
+ def time_difference_in_minutes(updated_at)
285
+ (Time.now - updated_at)/60
286
+ end
287
+
288
+ def define_transaction_state(state)
289
+ case state
290
+ when 'unconfirmed'
291
+ 'pending'
292
+ when 'confirmed'
293
+ 'success'
294
+ when 'failed','rejected'
295
+ 'failed'
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end