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,371 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "launchpad/precision_validator"
4
+ require "peatio"
5
+ module Launchpad
6
+ module IEO
7
+ # TODO: We need to validate that currency exists on peatio side on sale creation step.
8
+ class Sale < ApplicationRecord
9
+ # == Constants ============================================================
10
+
11
+ # include Launchpad::PrecisionValidator
12
+ include AASM
13
+
14
+ RESULTS = %w[nothing listing].freeze
15
+ PUBLIC_STATES = %w[preparing cancelled ongoing refunding distributing finished].freeze
16
+ EDITABLE_STATES = {draft: %w[name description owner_uid currency_id supply low_goal commission min_amount
17
+ max_amount min_unit result starts_at finishes_at],
18
+ preparing: %w[name description starts_at finishes_at],
19
+ ongoing: %w[finishes_at]}.freeze
20
+ # fcfs - First Come, First Served algorithm.
21
+ TYPES = %w[fcfs proportional].freeze
22
+
23
+ # subtract - contribution * fees
24
+ # add - contribution + contribution * fees
25
+ FEES_POLICY = %w[subtract add].freeze
26
+
27
+ # == Attributes ===========================================================
28
+
29
+ # == Extensions ===========================================================
30
+
31
+ aasm column: :state do
32
+ state :draft, initial: true
33
+ state :deleted
34
+ state :preparing
35
+ state :cancelled
36
+ state :ongoing
37
+ state :refunding
38
+ state :distributing
39
+ state :finished
40
+ state :released
41
+
42
+ after_all_events do
43
+ notify_ranger if state.in?(PUBLIC_STATES)
44
+ end
45
+
46
+ # We don't check if starts_at is in the future since it will get
47
+ # executed immediately if starts_at was in the past.
48
+ # TODO: This does not handle case when starts_at changes to less that current value.
49
+ event :approve, after_commit: :enqueue_start_job do
50
+ transitions from: :draft, to: :preparing
51
+ end
52
+
53
+ event :start do
54
+ transitions from: :preparing, to: :ongoing do
55
+ guard do
56
+ starts_at.past?
57
+ end
58
+ after do
59
+ enqueue_distribute_job if finishes_at.present?
60
+ end
61
+ end
62
+
63
+ transitions from: :preparing, to: :preparing do
64
+ after do
65
+ enqueue_start_job
66
+ end
67
+ end
68
+ end
69
+
70
+ event :distribute do
71
+ transitions from: :ongoing, to: :distributing do
72
+ guard do
73
+ finishes_at.try(:past?) && tokens_ordered >= low_goal
74
+ end
75
+ end
76
+
77
+ transitions from: :ongoing, to: :refunding do
78
+ guard do
79
+ !type.fcfs?
80
+ end
81
+ guard do
82
+ finishes_at.try(:past?) && tokens_ordered < low_goal
83
+ end
84
+ end
85
+
86
+ transitions from: :ongoing, to: :ongoing
87
+
88
+ # We want to make sure that depending actions happens only after the transaction is committed
89
+ after_commit do
90
+ if ongoing?
91
+ enqueue_distribute_job if finishes_at.present?
92
+ elsif distributing?
93
+ orders.active.each do |order|
94
+ order.execute!
95
+ sleep 0.9
96
+ end unless type.fcfs?
97
+ enqueue_finish_job
98
+ elsif refunding?
99
+ orders.active.each(&:refund!) unless type.fcfs?
100
+ enqueue_cancel_job
101
+ end
102
+ end
103
+ end
104
+
105
+ event :cancel do
106
+ transitions from: :refunding, to: :cancelled
107
+ end
108
+
109
+ event :finish do
110
+ transitions from: :ongoing, to: :finished do
111
+ guard do
112
+ type.fcfs? && orders.active.blank?
113
+ end
114
+ end
115
+
116
+ transitions from: :distributing, to: :finished do
117
+ guard do
118
+ orders.active.blank?
119
+ end
120
+ end
121
+
122
+ transitions from: :distributing, to: :distributing
123
+
124
+ after_commit do
125
+ if finished? && result.listing?
126
+ enqueue_list_job
127
+ pairs.each(&:enqueue_list_job)
128
+ elsif distributing?
129
+ enqueue_finish_job
130
+ end
131
+ end
132
+ end
133
+
134
+ event :release do
135
+ transitions from: :finished, to: :released
136
+
137
+ transitions from: :released, to: :released do
138
+ guard do
139
+ orders.purchased.present?
140
+ end
141
+ end
142
+
143
+ after_commit do
144
+ orders.purchased.each(&:release!)
145
+ end
146
+ end
147
+
148
+ event :stop do
149
+ transitions from: :draft, to: :deleted
150
+ end
151
+ end
152
+
153
+ # == Relationships ========================================================
154
+
155
+ has_many :pairs, class_name: "Launchpad::IEO::SalePair"
156
+
157
+ has_many :orders, class_name: "Launchpad::IEO::Order", through: :pairs
158
+
159
+ # == Validations ==========================================================
160
+
161
+ validates :name, :owner_uid, :currency_id, :supply, :low_goal, :commission, :min_amount,
162
+ :max_amount, :min_unit, :type, :starts_at, :state, :result, :lockup_percentage,
163
+ :fees_policy,
164
+ presence: {
165
+ message: ->(_, _) { "missing_" }
166
+ }
167
+
168
+ validates :finishes_at, presence: {message: "missing_"}, if: ->(sale) { sale.type&.proportional? }
169
+
170
+ validates :supply,
171
+ numericality: {
172
+ greater_than: 0,
173
+ message: ->(_, _data) { "non_positive_" }
174
+ }
175
+
176
+ validates :low_goal, :commission, :min_amount, :min_unit, :lockup_percentage,
177
+ numericality: {
178
+ greater_than_or_equal_to: 0,
179
+ message: ->(_, _data) { "negative_" }
180
+ }
181
+
182
+ validates :low_goal,
183
+ numericality: {
184
+ equal_to: 0,
185
+ message: ->(_, _data) { "for_fcfs_sale_non_zero_" }
186
+ }, if: ->(sale) { sale.type&.fcfs? }
187
+
188
+ validates :max_amount,
189
+ numericality: {
190
+ greater_than: ->(sale) { sale.min_amount },
191
+ message: ->(_, _data) { "_less_than_min_amount" }
192
+ }
193
+
194
+ validates :result,
195
+ inclusion: {
196
+ in: RESULTS,
197
+ message: ->(_, _data) { "invalid_" }
198
+ }
199
+
200
+ validates :type,
201
+ inclusion: {
202
+ in: TYPES,
203
+ message: ->(_, _data) { "invalid_" }
204
+ }
205
+
206
+ validates :commission,
207
+ numericality: {
208
+ greater_than_or_equal_to: 0, less_than: 1,
209
+ message: ->(_, _data) { "invalid_" }
210
+ }
211
+
212
+ validates :commission, 'launchpad/precision': {less_than_or_eq_to: Launchpad::IEO::COMMISSION_PRECISION}
213
+
214
+ validate :finish_date_after_start_date
215
+
216
+ validates :fees_policy,
217
+ inclusion: {
218
+ in: FEES_POLICY,
219
+ message: -> (_, _data) { "invalid_" }
220
+ }
221
+
222
+ # == Scopes ===============================================================
223
+
224
+ scope :public_states, -> { where(state: PUBLIC_STATES) }
225
+
226
+ # == Callbacks ============================================================
227
+
228
+ before_validation do
229
+ self.type = type.try(:downcase)
230
+ self.currency_id = currency_id.try(:downcase)
231
+ self.fees_policy ||= ENV['FEES_POLICY']
232
+ end
233
+
234
+ # == Class Methods ========================================================
235
+
236
+ self.inheritance_column = nil
237
+
238
+ # == Instance Methods =====================================================
239
+
240
+ def tokens_ordered(ndigits: Launchpad::IEO::TOKENS_AMOUNT_PRECISION)
241
+ if type.proportional?
242
+ orders.executable.tokens_ordered(ndigits: ndigits)
243
+ elsif type.fcfs?
244
+ orders.active_and_prepared_and_completed.tokens_ordered(ndigits: ndigits)
245
+ end
246
+ end
247
+
248
+ # we keep to this method as on previous version for FE convenience
249
+ alias collected_amount tokens_ordered
250
+
251
+ def ratio
252
+ (collected_amount / supply).round(Launchpad::IEO::RATIO_PRECISION, BigDecimal::ROUND_DOWN)
253
+ end
254
+
255
+ # Finish fcfs sale when all tokens + min possible amount will be greater than total sale supply\
256
+ # since it means that user would not be able to buy anymore.
257
+ def full?
258
+ tokens_ordered + min_amount >= supply
259
+ end
260
+
261
+ def type
262
+ super&.inquiry
263
+ end
264
+
265
+ def result
266
+ super&.inquiry
267
+ end
268
+
269
+ def fees_policy
270
+ super&.inquiry
271
+ end
272
+
273
+ def as_json(_options={})
274
+ super(include: :pairs, methods: %i[collected_amount tokens_ordered ratio])
275
+ end
276
+
277
+ def notify_ranger
278
+ Peatio::MQ::Events.publish("public", "ieo", "tickers", sale: as_json)
279
+ end
280
+
281
+ def list_currency
282
+ Peatio::ManagementAPIV2::Client.new.update_currency(
283
+ id: currency_id,
284
+ visible: true
285
+ )
286
+ end
287
+
288
+ def sales_to_json
289
+ attributes.merge!(
290
+ collected_amount: collected_amount,
291
+ tokens_ordered: tokens_ordered,
292
+ ratio: ratio,
293
+ pairs: pairs.as_json
294
+ )
295
+ end
296
+
297
+ private
298
+
299
+ def finish_date_after_start_date
300
+ return if finishes_at.blank? || starts_at.blank?
301
+
302
+ errors.add(:finishes_at, "finishes_at_less_than_starts_at") if finishes_at <= starts_at
303
+ end
304
+
305
+ def commission_precision
306
+ unless commission&.round(Launchpad::IEO::FEE_PRECISION) == commission
307
+ errors.add(commission, "is too precise (max fractional part size is #{::IEO::FEE_PRECISION})")
308
+ end
309
+ end
310
+
311
+ def enqueue_start_job
312
+ Launchpad::IEO::SaleStartWorker.perform_at(starts_at, to_sgid(for: "sale_start"))
313
+ end
314
+
315
+ def enqueue_distribute_job
316
+ Launchpad::IEO::SaleDistributeWorker.perform_at(finishes_at, to_sgid(for: "sale_distribute"))
317
+ end
318
+
319
+ def enqueue_cancel_job
320
+ finish_delay = 5.minutes + orders.active.count
321
+ Launchpad::IEO::SaleCancelWorker.perform_at(finish_delay.since, to_sgid(for: "sale_cancel"))
322
+ end
323
+
324
+ def enqueue_list_job
325
+ Launchpad::IEO::SaleCurrencyListWorker.perform_async(to_sgid(for: "sale_currency_list"))
326
+ end
327
+
328
+ def enqueue_finish_job
329
+ # Delay sale finish job for (5 minutes + active order number seconds)
330
+ # since orders execution start.
331
+ # Publish without delay for fcfs sale.
332
+ finish_delay = type.fcfs? ? 0.seconds : 5.minutes + orders.active.count
333
+ Launchpad::IEO::SaleFinishWorker.perform_at(finish_delay.since, to_sgid(for: "sale_finish"))
334
+ end
335
+
336
+ def enqueue_release_job
337
+ Launchpad::IEO::SaleReleaseWorker.perform_async(to_sgid(for: "sale_release"))
338
+ end
339
+
340
+ def enqueue_release_funds_job(funds_percent = 0)
341
+ Launchpad::IEO::SaleReleaseFundsWorker.perform_async(to_sgid(for: "sale_release_funds"), funds_percent)
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ # == Schema Information
348
+ # Schema version: 20191014123503
349
+ #
350
+ # Table name: ieo_sales
351
+ #
352
+ # id :bigint not null, primary key
353
+ # name :string(32) not null
354
+ # introduction_url :string(2048)
355
+ # owner_uid :string(255) not null
356
+ # currency_id :string(10) not null
357
+ # supply :decimal(32, 16) not null
358
+ # low_goal :decimal(32, 16) default(0.0), not null
359
+ # commission :decimal(16, 16) default(0.0), not null
360
+ # min_amount :decimal(32, 16) default(0.0), not null
361
+ # max_amount :decimal(32, 16) not null
362
+ # min_unit :decimal(32, 16) default(0.0), not null
363
+ # state :string(32) not null
364
+ # type :string(32) not null
365
+ # result :string(32) not null
366
+ # lockup_percentage :decimal(17, 16)
367
+ # starts_at :datetime not null
368
+ # finishes_at :datetime not null
369
+ # created_at :datetime not null
370
+ # updated_at :datetime not null
371
+ #
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Launchpad
4
+ module IEO
5
+ # TODO: We need to validate that currency exists on peatio side on sale creation step.
6
+ class SalePair < ApplicationRecord
7
+ # == Constants ============================================================
8
+
9
+ # == Attributes ===========================================================
10
+
11
+ # == Extensions ===========================================================
12
+
13
+ # == Relationships ========================================================
14
+
15
+ belongs_to :sale, class_name: "Launchpad::IEO::Sale", required: true
16
+
17
+ has_many :orders, class_name: "Launchpad::IEO::Order"
18
+
19
+ # == Validations ==========================================================
20
+
21
+ validates :quote_currency_id, :price,
22
+ presence: {
23
+ message: ->(_, _data) { "missing_" }
24
+ }
25
+
26
+ validates :quote_currency_id,
27
+ uniqueness: {
28
+ scope: :sale_id,
29
+ case_sensitive: false,
30
+ message: ->(_, _data) { "not_unique_" }
31
+ }
32
+
33
+ validates :price,
34
+ numericality: {
35
+ greater_than: 0,
36
+ message: ->(_, _data) { "non_positive_" }
37
+ }
38
+
39
+ # == Scopes ===============================================================
40
+
41
+ # == Callbacks ============================================================
42
+
43
+ before_validation { self.quote_currency_id = quote_currency_id.try(:downcase) }
44
+ before_validation do
45
+ self.listed = false if listed.nil? && sale&.result&.listing?
46
+ end
47
+
48
+ # == Class Methods ========================================================
49
+
50
+ # == Instance Methods =====================================================
51
+
52
+ def list_market
53
+ Peatio::ManagementAPIV2::Client.new.update_market(
54
+ id: sale.currency_id + quote_currency_id,
55
+ state: "enabled"
56
+ )
57
+ end
58
+
59
+ def enqueue_list_job
60
+ Launchpad::IEO::SalePairListWorker.perform_async(to_sgid(for: "sale_pair_list"))
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # == Schema Information
67
+ # Schema version: 20191004105729
68
+ #
69
+ # Table name: ieo_sale_pairs
70
+ #
71
+ # id :bigint not null, primary key
72
+ # sale_id :bigint not null
73
+ # quote_currency_id :string(10) not null
74
+ # price :decimal(32, 16) not null
75
+ # listed :boolean
76
+ # created_at :datetime not null
77
+ # updated_at :datetime not null
78
+ #
79
+ # Indexes
80
+ #
81
+ # index_ieo_sale_pairs_on_sale_id (sale_id)
82
+ # index_ieo_sale_pairs_on_sale_id_and_quote_currency_id (sale_id,quote_currency_id) UNIQUE
83
+ #