taunchpad 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +28 -0
  3. data/Rakefile +32 -0
  4. data/app/controllers/concerns/exception_handlers.rb +19 -0
  5. data/app/controllers/concerns/jwt_payload.rb +19 -0
  6. data/app/controllers/concerns/response.rb +25 -0
  7. data/app/controllers/launchpad/api/v2/admin/base_controller.rb +22 -0
  8. data/app/controllers/launchpad/api/v2/admin/ieo/orders_controller.rb +29 -0
  9. data/app/controllers/launchpad/api/v2/admin/ieo/sales_controller.rb +112 -0
  10. data/app/controllers/launchpad/api/v2/private/base_controller.rb +14 -0
  11. data/app/controllers/launchpad/api/v2/private/ieo/orders_controller.rb +97 -0
  12. data/app/controllers/launchpad/api/v2/private/ieo/sales_controller.rb +22 -0
  13. data/app/controllers/launchpad/api/v2/public/base_controller.rb +13 -0
  14. data/app/controllers/launchpad/api/v2/public/ieo/sales_controller.rb +56 -0
  15. data/app/controllers/launchpad/application_controller.rb +10 -0
  16. data/app/helpers/launchpad/application_helper.rb +4 -0
  17. data/app/models/launchpad/application_record.rb +5 -0
  18. data/app/models/launchpad/ieo.rb +21 -0
  19. data/app/models/launchpad/ieo/order.rb +576 -0
  20. data/app/models/launchpad/ieo/sale.rb +371 -0
  21. data/app/models/launchpad/ieo/sale_pair.rb +83 -0
  22. data/app/services/barong/management_api_v2/client.rb +33 -0
  23. data/app/services/management_api_v2/client.rb +73 -0
  24. data/app/services/management_api_v2/exception.rb +25 -0
  25. data/app/services/peatio/management_api_v2/client.rb +49 -0
  26. data/app/workers/launchpad/ieo/order_execute_worker.rb +26 -0
  27. data/app/workers/launchpad/ieo/order_refund_worker.rb +19 -0
  28. data/app/workers/launchpad/ieo/order_release_worker.rb +22 -0
  29. data/app/workers/launchpad/ieo/sale_cancel_worker.rb +20 -0
  30. data/app/workers/launchpad/ieo/sale_currency_list_worker.rb +19 -0
  31. data/app/workers/launchpad/ieo/sale_distribute_worker.rb +20 -0
  32. data/app/workers/launchpad/ieo/sale_finish_worker.rb +21 -0
  33. data/app/workers/launchpad/ieo/sale_pair_list_worker.rb +21 -0
  34. data/app/workers/launchpad/ieo/sale_release_funds_worker.rb +23 -0
  35. data/app/workers/launchpad/ieo/sale_start_worker.rb +21 -0
  36. data/config/initializers/active_model.rb +13 -0
  37. data/config/initializers/api_pagination.rb +33 -0
  38. data/config/initializers/inflections.rb +19 -0
  39. data/config/routes.rb +35 -0
  40. data/db/migrate/20191120145404_create_launchpad_ieo.rb +52 -0
  41. data/db/migrate/20200814114105_add_fees_policy_in_sale.rb +5 -0
  42. data/lib/launchpad.rb +10 -0
  43. data/lib/launchpad/engine.rb +17 -0
  44. data/lib/launchpad/precision_validator.rb +25 -0
  45. data/lib/launchpad/version.rb +3 -0
  46. data/lib/tasks/launchpad_tasks.rake +4 -0
  47. metadata +229 -0
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Launchpad
5
+ module API
6
+ module V2
7
+ module Public
8
+ class BaseController < ApplicationController
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "launchpad/api/v2/public/base_controller"
4
+ module Launchpad
5
+ module API
6
+ module V2
7
+ module Public
8
+ module IEO
9
+ class SalesController < BaseController
10
+ def index
11
+ ransack_params = {
12
+ state_in: params[:state],
13
+ currency_id_eq: params[:currency_id],
14
+ name_cont_all: params[:name]
15
+ }
16
+
17
+ sales = Launchpad::IEO::Sale.public_states.order(created_at: (params[:ordering] || :asc))
18
+ .ransack(ransack_params)
19
+
20
+ json_response(paginate(sales.result), 200)
21
+ end
22
+
23
+ def show
24
+ launchpad = Launchpad::IEO::Sale.find(params[:id])
25
+ additional_params = {
26
+ generic_bids: launchpad.orders.where("state = ? OR state = ?", 'active', 'completed').last(50).as_json(uid: true),
27
+ }
28
+ json_response(launchpad.as_json.merge(additional_params), 200)
29
+ end
30
+
31
+ def search
32
+ ransack_params = {
33
+ state_in: params[:state],
34
+ currency_id_eq: params[:currency_id],
35
+ name_cont_all: params[:name]
36
+ }
37
+
38
+ sales = Launchpad::IEO::Sale.includes(:pairs)
39
+ .public_states.order(Arel.sql("CASE state
40
+ WHEN 'ongoing' THEN '1'
41
+ WHEN 'preparing' THEN '2'
42
+ WHEN 'distributing' THEN '3'
43
+ WHEN 'refunding' THEN '4'
44
+ WHEN 'finished' THEN '5'
45
+ WHEN 'cancelled' THEN '6'
46
+ END")).order(created_at: :desc)
47
+ .ransack(ransack_params)
48
+
49
+ json_response(paginate(sales.result).map(&:sales_to_json), 200)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Launchpad
4
+ class ApplicationController < ActionController::Base
5
+ skip_before_action :verify_authenticity_token
6
+ include Rails::Pagination
7
+ include Response
8
+ include ExceptionHandlers
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ module Launchpad
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module Launchpad
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ module Launchpad
2
+ module IEO
3
+ TOKENS_AMOUNT_PRECISION = 4
4
+ RATIO_PRECISION = 4
5
+ COMMISSION_PRECISION = 6
6
+
7
+ LIABILITY_CODES = {
8
+ fiat: { main: 201, locked: 211 },
9
+ coin: { main: 202, locked: 212 }
10
+ }
11
+
12
+ REVENUE_CODES = {
13
+ fiat: { main: 301 },
14
+ coin: { main: 302 }
15
+ }
16
+
17
+ def self.table_name_prefix
18
+ 'ieo_'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,576 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Launchpad
4
+ module IEO
5
+ # TODO: Add validation that orders are created only for ongoing sales.
6
+ # TODO: Add attr_readonly for all models.
7
+ class Order < ApplicationRecord
8
+ # == Constants ============================================================
9
+
10
+ include AASM
11
+
12
+ # == Attributes ===========================================================
13
+
14
+ # == Extensions ===========================================================
15
+
16
+ serialize :transfer_keys, Array
17
+
18
+ # NOTE: We use pessimistic locking for avoiding double transfer creation.
19
+ aasm requires_lock: true, column: :state do
20
+ state :preparing, initial: true
21
+ state :rejected
22
+ state :active
23
+ state :refunding
24
+ state :closed
25
+ state :cancelled
26
+ state :executing
27
+ state :completed
28
+ state :releasing
29
+ state :purchased
30
+
31
+ # Events docs:
32
+ # preparing -> active;
33
+ # preparing -> rejected;
34
+ # active -> executing;
35
+ # active -> refunfing;
36
+ # active -> cancelled;
37
+ # exucuting -> purchased;
38
+ # executing -> completed;
39
+ # executing -> refunding;
40
+ # executing -> cancelled;
41
+ # purchased -> releasing;
42
+ # releasing -> completed;
43
+ # refunding -> closed;
44
+
45
+ # dispatch! => preparing -> active;
46
+ #
47
+ # reject! => preparing -> rejected;
48
+ #
49
+ # execute! => active -> executing;
50
+ #
51
+ # - => executing -> purchased;
52
+ # /
53
+ # purchase! - - => executing -> completed;
54
+ # \
55
+ # - => executing -> refunding;
56
+ #
57
+ # refund! => active -> refunding;
58
+ #
59
+ # release! => purchased -> releasing;
60
+ #
61
+ # unlock! => releasing -> completed;
62
+ #
63
+ # close! => refunding -> closed;
64
+ #
65
+ # - => executing -> cancelled;
66
+ # /
67
+ # cancel! - |
68
+ # \
69
+ # - => active -> cancelled;
70
+ #
71
+
72
+ event :dispatch do
73
+ transitions from: :preparing, to: :active do
74
+ after do
75
+ perform_transfer(lock_transfer_params)
76
+ end
77
+ end
78
+ after_commit do
79
+ sale.notify_ranger
80
+ execute! if sale.type.fcfs?
81
+ end
82
+ end
83
+
84
+ event :reject do
85
+ transitions from: :preparing, to: :rejected
86
+ end
87
+
88
+ event :execute, after_commit: :enqueue_execute_job do
89
+ transitions from: :active, to: :executing
90
+ end
91
+
92
+ event :purchase do
93
+ transitions from: :executing, to: :purchased do
94
+ guard do
95
+ calculate_share
96
+ sale.lockup_percentage.positive?
97
+ end
98
+ after do
99
+ perform_transfer(execute_transfer_params)
100
+ end
101
+ end
102
+
103
+ transitions from: :executing, to: :completed do
104
+ guard do
105
+ calculate_share
106
+ sale.lockup_percentage.zero?
107
+ end
108
+ after do
109
+ perform_transfer(execute_transfer_params)
110
+ end
111
+ end
112
+
113
+ transitions from: :executing, to: :refunding
114
+
115
+ after_commit do
116
+ sale.finish! if sale.type.fcfs? && sale.full?
117
+ enqueue_refund_job if refunding?
118
+ end
119
+ end
120
+
121
+ event :refund do
122
+ transitions from: :active, to: :refunding
123
+
124
+ after_commit do
125
+ enqueue_refund_job
126
+ end
127
+ end
128
+
129
+ event :release do
130
+ transitions from: :purchased, to: :releasing
131
+
132
+ after_commit do
133
+ enqueue_released_job
134
+ end
135
+ end
136
+
137
+ event :unlock do
138
+ transitions from: :releasing, to: :completed do
139
+ after do
140
+ perform_transfer(release_transfer_params)
141
+ calculate_release
142
+ end
143
+ end
144
+ end
145
+
146
+ event :close do
147
+ transitions from: :refunding, to: :closed do
148
+ after do
149
+ calculate_refunded
150
+ perform_transfer(cancel_transfer_params)
151
+ end
152
+ end
153
+ end
154
+
155
+ event :cancel do
156
+ transitions from: :executing, to: :cancelled do
157
+ after do
158
+ calculate_refund
159
+ Rails.logger.info("Transition from executing to cancelled")
160
+ perform_transfer(cancel_transfer_params)
161
+ end
162
+ end
163
+
164
+ transitions from: :active, to: :cancelled do
165
+ after do
166
+ calculate_refund
167
+ Rails.logger.info("Transition from active to cancelled")
168
+ perform_transfer(cancel_transfer_params)
169
+ end
170
+ end
171
+
172
+ after_commit do
173
+ sale.notify_ranger
174
+ end
175
+ end
176
+ end
177
+
178
+ # == Relationships ========================================================
179
+
180
+ belongs_to :sale_pair, class_name: "Launchpad::IEO::SalePair", required: true
181
+
182
+ has_one :sale, class_name: "Launchpad::IEO::Sale", through: :sale_pair
183
+
184
+ # == Validations ==========================================================
185
+
186
+ validates :uid, :contribution, :executed, :refunded, :tokens_received,
187
+ :commission_rate, :commission_amount, :state, presence: {
188
+ message: ->(_, _data) { "missing_" }
189
+ }
190
+
191
+ validates :contribution,
192
+ numericality: {
193
+ greater_than: 0,
194
+ message: ->(_, _data) { "non_positive_" }
195
+ }
196
+
197
+ validates :executed, :refunded, :tokens_received,
198
+ :commission_rate, :commission_amount,
199
+ numericality: {
200
+ greater_than_or_equal_to: 0,
201
+ message: ->(_, _data) { "negative_" }
202
+ }
203
+
204
+ validates :tokens_ordered,
205
+ numericality: {
206
+ greater_than_or_equal_to: ->(order) { order.sale.min_amount },
207
+ message: ->(_, _data) { "_less_than_min_amount" }
208
+ }
209
+
210
+ validates :tokens_ordered,
211
+ numericality: {
212
+ less_than_or_equal_to: ->(order) { order.sale.max_amount - order.sale.orders.active_and_prepared.where(uid: order.uid).tokens_ordered },
213
+ message: ->(_, _data) { "_greater_than_max_amount" }
214
+ }, if: ->(order) { order.sale.type.proportional? },
215
+ on: :create
216
+
217
+ validates :tokens_ordered,
218
+ numericality: {
219
+ less_than_or_equal_to: ->(order) { order.sale.max_amount - order.sale.orders.active_and_prepared_and_completed.where(uid: order.uid).tokens_ordered },
220
+ message: ->(_, _data) { "_greater_than_max_amount" }
221
+ }, if: ->(order) { order.sale.type.fcfs? },
222
+ on: :create
223
+
224
+ validates :tokens_ordered,
225
+ numericality: {
226
+ less_than_or_equal_to: ->(order) { order.sale.supply - order.sale.orders.executable.tokens_ordered },
227
+ message: ->(_, _) { "_greater_than_sale_supply" }
228
+ }, if: ->(order) { order.sale.type.fcfs? },
229
+ on: :create
230
+
231
+ # == Scopes ===============================================================
232
+
233
+ scope :active_and_prepared, -> { where(state: %i[active preparing]) }
234
+
235
+ scope :active_and_prepared_and_completed, -> { where(state: %i[active preparing purchased completed]) }
236
+
237
+ # Scope represents all orders which may be executed.
238
+ # So this orders influence tokens_ordered and ratio.
239
+ scope :executable, -> { where(state: %i[preparing active executing purchased completed]) }
240
+
241
+ # == Callbacks ============================================================
242
+
243
+ before_validation(on: :create) { self.commission_rate = sale.commission }
244
+
245
+ # == Class Methods ========================================================
246
+
247
+ class << self
248
+ def tokens_ordered(ndigits: Launchpad::IEO::TOKENS_AMOUNT_PRECISION)
249
+ # Include SalePair for accessing price.
250
+ if first&.add?
251
+ all.includes(:sale_pair).sum("contribution / ieo_sale_pairs.price").round(ndigits).to_d
252
+ else
253
+ all.includes(:sale_pair).sum("(1 - commission_rate) * contribution / ieo_sale_pairs.price").round(ndigits).to_d
254
+ end
255
+ end
256
+
257
+ def release_fund(percent = 0)
258
+ return if percent.to_d == 0
259
+
260
+ all.each do |order|
261
+ next unless order.tokens_locked.positive?
262
+
263
+ funds = (order.tokens_locked + order.tokens_received) * percent.to_d
264
+ order.perform_transfer(order.release_funds_params(funds))
265
+ order.tokens_locked -= funds
266
+ order.tokens_received += funds
267
+ order.save!
268
+ end
269
+ end
270
+ end
271
+
272
+ # == Instance Methods =====================================================
273
+
274
+ delegate :price, to: :sale_pair
275
+
276
+ def as_json(_options={})
277
+ if _options[:uid]
278
+ super(except: [:transfer_keys, :uid],
279
+ methods: %i[tokens_ordered base_currency quote_currency ratio sale_name]).merge(sale_id: sale.id)
280
+ else
281
+ super(except: :transfer_keys,
282
+ methods: %i[tokens_ordered base_currency quote_currency ratio sale_name]).merge(sale_id: sale.id)
283
+ end
284
+ end
285
+
286
+ def tokens_ordered(ndigits: Launchpad::IEO::TOKENS_AMOUNT_PRECISION)
287
+ if add?
288
+ (contribution / price).round(ndigits)
289
+ else
290
+ ((1 - commission_rate) * contribution / price).round(ndigits)
291
+ end
292
+ end
293
+
294
+ def executed_without_commission
295
+ add? ? executed : executed - commission_amount
296
+ end
297
+
298
+ def sale_name
299
+ sale.name
300
+ end
301
+
302
+ def base_currency
303
+ sale.currency_id
304
+ end
305
+
306
+ def quote_currency
307
+ sale_pair.quote_currency_id
308
+ end
309
+
310
+ def ratio
311
+ sale_ratio = sale.ratio < 1 ? 1 : sale.ratio
312
+ (1 / sale_ratio).to_d.round(Launchpad::IEO::RATIO_PRECISION, BigDecimal::ROUND_DOWN)
313
+ end
314
+
315
+ def calculate_share
316
+ self.executed = contribution * ratio
317
+ self.refunded = contribution - executed
318
+
319
+ self.commission_amount = executed * commission_rate
320
+
321
+ tokens_ordered = (executed_without_commission / sale_pair.price).round(Launchpad::IEO::TOKENS_AMOUNT_PRECISION, BigDecimal::ROUND_DOWN)
322
+ self.tokens_locked = tokens_ordered * sale.lockup_percentage.to_d
323
+ self.tokens_received = tokens_ordered * (1 - sale.lockup_percentage.to_d)
324
+ end
325
+
326
+ def calculate_refund
327
+ # This logic may become complex later.
328
+ self.refunded = calculate_contribution
329
+ end
330
+
331
+ def calculate_refunded
332
+ self.refunded = contribution - executed
333
+ self.refunded = calculate_execute_refunded
334
+ end
335
+
336
+ def add?
337
+ sale.fees_policy.add?
338
+ end
339
+
340
+ def calculate_contribution
341
+ add? ? contribution + contribution * commission_rate : contribution
342
+ end
343
+
344
+ def calculate_release
345
+ self.tokens_received += tokens_locked
346
+ self.tokens_locked = 0
347
+ end
348
+
349
+ def lock_transfer_params
350
+ type = fetch_currency(sale_pair.quote_currency_id)
351
+ .fetch(:type)
352
+ .to_sym
353
+
354
+ operations =
355
+ [
356
+ {
357
+ currency: sale_pair.quote_currency_id,
358
+ amount: calculate_contribution,
359
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[type][:main], uid: uid},
360
+ account_dst: {code: Launchpad::IEO::LIABILITY_CODES[type][:locked], uid: uid}
361
+ }
362
+ ].delete_if { |op| op[:amount].zero? }
363
+
364
+ {
365
+ key: "IEO-lock-#{id}",
366
+ category: "purchases",
367
+ description: "Lock funds for IEO order #{id}",
368
+ operations: operations
369
+ }
370
+ end
371
+
372
+ def execute_transfer_params
373
+ base_currency_type = fetch_currency(sale.currency_id)
374
+ .fetch(:type)
375
+ .to_sym
376
+ quote_currency_type = fetch_currency(sale_pair.quote_currency_id)
377
+ .fetch(:type)
378
+ .to_sym
379
+
380
+ operations =
381
+ [
382
+ { # Debit seller main liabilities & credit user locked liabilities with base currency.
383
+ currency: sale.currency_id,
384
+ amount: tokens_locked,
385
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[base_currency_type][:main],
386
+ uid: sale.owner_uid},
387
+ account_dst: {code: Launchpad::IEO::LIABILITY_CODES[base_currency_type][:locked],
388
+ uid: uid}
389
+ },
390
+ { # Debit seller main liabilities & credit user main liabilities with base currency.
391
+ currency: sale.currency_id,
392
+ amount: tokens_received,
393
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[base_currency_type][:main],
394
+ uid: sale.owner_uid},
395
+ account_dst: {code: Launchpad::IEO::LIABILITY_CODES[base_currency_type][:main],
396
+ uid: uid}
397
+ },
398
+ { # Debit user locked liabilities & credit seller main liabilities with quote currency.
399
+ currency: sale_pair.quote_currency_id,
400
+ amount: executed_without_commission,
401
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[quote_currency_type][:locked],
402
+ uid: uid},
403
+ account_dst: {code: Launchpad::IEO::LIABILITY_CODES[quote_currency_type][:main],
404
+ uid: sale.owner_uid}
405
+ },
406
+ { # Debit user locked liabilities & credit user main liabilities with quote currency.
407
+ currency: sale_pair.quote_currency_id,
408
+ amount: calculate_execute_refunded,
409
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[quote_currency_type][:locked],
410
+ uid: uid},
411
+ account_dst: {code: Launchpad::IEO::LIABILITY_CODES[quote_currency_type][:main],
412
+ uid: uid}
413
+ },
414
+ { # Debit user locked liabilities & credit main revenues with quote currency.
415
+ currency: sale_pair.quote_currency_id,
416
+ amount: commission_amount,
417
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[quote_currency_type][:locked],
418
+ uid: uid},
419
+ account_dst: {code: Launchpad::IEO::REVENUE_CODES[quote_currency_type][:main]}
420
+ }
421
+ ].delete_if { |op| op[:amount].zero? }
422
+
423
+ {
424
+ key: "IEO-execute-#{id}",
425
+ category: :purchases,
426
+ description: "Execute IEO order #{id}",
427
+ operations: operations
428
+ }
429
+ end
430
+
431
+ def calculate_execute_refunded
432
+ add? ? refunded + refunded * commission_rate : refunded
433
+ end
434
+
435
+ def release_transfer_params
436
+ base_currency_type = fetch_currency(sale.currency_id)
437
+ .fetch(:type)
438
+ .to_sym
439
+
440
+ operations =
441
+ [
442
+ { # Debit user locked liabilities & credit user main liabilities with base currency.
443
+ currency: sale.currency_id,
444
+ amount: tokens_locked,
445
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[base_currency_type][:locked],
446
+ uid: uid},
447
+ account_dst: {code: Launchpad::IEO::LIABILITY_CODES[base_currency_type][:main],
448
+ uid: uid}
449
+ }
450
+ ].delete_if { |op| op[:amount].zero? }
451
+
452
+ {
453
+ key: "IEO-release-#{id}",
454
+ category: :purchases,
455
+ description: "Release IEO order #{id}",
456
+ operations: operations
457
+ }
458
+ end
459
+
460
+ def cancel_transfer_params
461
+ quote_currency_type = fetch_currency(sale_pair.quote_currency_id)
462
+ .fetch(:type)
463
+ .to_sym
464
+
465
+ operations =
466
+ [
467
+ { # Debit user locked liabilities & credit user main liabilities with quote currency.
468
+ currency: sale_pair.quote_currency_id,
469
+ amount: refunded,
470
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[quote_currency_type][:locked],
471
+ uid: uid},
472
+ account_dst: {code: Launchpad::IEO::LIABILITY_CODES[quote_currency_type][:main],
473
+ uid: uid}
474
+ }
475
+ ]
476
+ if add?
477
+ # Debit user locked liabilities & credit main revenues with quote currency.
478
+ operations << {
479
+ currency: sale_pair.quote_currency_id,
480
+ amount: commission_amount,
481
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[quote_currency_type][:locked],
482
+ uid: uid},
483
+ account_dst: {code: Launchpad::IEO::REVENUE_CODES[quote_currency_type][:main]}
484
+ }
485
+ end
486
+
487
+ operations.delete_if { |op| op[:amount].zero? }
488
+
489
+ {
490
+ key: "IEO-cancel-#{id}",
491
+ category: :purchases,
492
+ description: "Cancel IEO order #{id}",
493
+ operations: operations
494
+ }
495
+ end
496
+
497
+ def release_funds_params(funds)
498
+ base_currency_type = fetch_currency(sale.currency_id)
499
+ .fetch(:type)
500
+ .to_sym
501
+ operations =
502
+ [
503
+ { # Debit user locked liabilities & credit user main liabilities with base currency.
504
+ currency: sale.currency_id,
505
+ amount: funds,
506
+ account_src: {code: Launchpad::IEO::LIABILITY_CODES[base_currency_type][:locked],
507
+ uid: uid},
508
+ account_dst: {code: Launchpad::IEO::LIABILITY_CODES[base_currency_type][:main],
509
+ uid: uid}
510
+ }
511
+ ].delete_if { |op| op[:amount].zero? }
512
+
513
+ {
514
+ key: generate_key,
515
+ category: :purchases,
516
+ description: "Release IEO order locked funds #{id}",
517
+ operations: operations
518
+ }
519
+ end
520
+
521
+ def generate_key
522
+ "IEO-fund-release-#{id}-#{transfer_keys.grep(/IEO-fund-release-/).size}"
523
+ end
524
+
525
+ def perform_transfer(transfer_params)
526
+ Peatio::ManagementAPIV2::Client.new.create_transfer(transfer_params)
527
+ transfer_keys << transfer_params[:key]
528
+ end
529
+
530
+ def enqueue_execute_job
531
+ Launchpad::IEO::OrderExecuteWorker.perform_async(to_sgid(for: "order_execute"))
532
+ end
533
+
534
+ def enqueue_released_job
535
+ Launchpad::IEO::OrderReleaseWorker.perform_async(to_sgid(for: "order_release"))
536
+ end
537
+
538
+ def enqueue_refund_job
539
+ Launchpad::IEO::OrderRefundWorker.perform_async(to_sgid(for: "order_refund"))
540
+ end
541
+
542
+ private
543
+
544
+ # TODO: Move this method to helper or somewhere else.
545
+ def fetch_currency(code)
546
+ Rails.cache.fetch("currency_#{code}", expires_in: 1.day) do
547
+ Peatio::ManagementAPIV2::Client.new.currency(code: code)
548
+ end
549
+ end
550
+ end
551
+ end
552
+ end
553
+
554
+ # == Schema Information
555
+ # Schema version: 20191001145525
556
+ #
557
+ # Table name: ieo_orders
558
+ #
559
+ # id :bigint not null, primary key
560
+ # sale_pair_id :bigint not null
561
+ # uid :string(255) not null
562
+ # contribution :decimal(32, 16) not null
563
+ # executed :decimal(32, 16) default(0.0), not null
564
+ # refunded :decimal(32, 16) default(0.0), not null
565
+ # tokens_received :decimal(32, 16) default(0.0), not null
566
+ # commission_rate :decimal(16, 16) default(0.0), not null
567
+ # commission_amount :decimal(32, 16) default(0.0), not null
568
+ # transfer_keys :string(1000)
569
+ # state :string(32) not null
570
+ # created_at :datetime not null
571
+ # updated_at :datetime not null
572
+ #
573
+ # Indexes
574
+ #
575
+ # index_ieo_orders_on_sale_pair_id (sale_pair_id)
576
+ #