taunchpad 3.1.0

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.
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
+ #