taunchpad 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +28 -0
- data/Rakefile +32 -0
- data/app/controllers/concerns/exception_handlers.rb +19 -0
- data/app/controllers/concerns/jwt_payload.rb +19 -0
- data/app/controllers/concerns/response.rb +25 -0
- data/app/controllers/launchpad/api/v2/admin/base_controller.rb +22 -0
- data/app/controllers/launchpad/api/v2/admin/ieo/orders_controller.rb +29 -0
- data/app/controllers/launchpad/api/v2/admin/ieo/sales_controller.rb +112 -0
- data/app/controllers/launchpad/api/v2/private/base_controller.rb +14 -0
- data/app/controllers/launchpad/api/v2/private/ieo/orders_controller.rb +97 -0
- data/app/controllers/launchpad/api/v2/private/ieo/sales_controller.rb +22 -0
- data/app/controllers/launchpad/api/v2/public/base_controller.rb +13 -0
- data/app/controllers/launchpad/api/v2/public/ieo/sales_controller.rb +56 -0
- data/app/controllers/launchpad/application_controller.rb +10 -0
- data/app/helpers/launchpad/application_helper.rb +4 -0
- data/app/models/launchpad/application_record.rb +5 -0
- data/app/models/launchpad/ieo.rb +21 -0
- data/app/models/launchpad/ieo/order.rb +576 -0
- data/app/models/launchpad/ieo/sale.rb +371 -0
- data/app/models/launchpad/ieo/sale_pair.rb +83 -0
- data/app/services/barong/management_api_v2/client.rb +33 -0
- data/app/services/management_api_v2/client.rb +73 -0
- data/app/services/management_api_v2/exception.rb +25 -0
- data/app/services/peatio/management_api_v2/client.rb +49 -0
- data/app/workers/launchpad/ieo/order_execute_worker.rb +26 -0
- data/app/workers/launchpad/ieo/order_refund_worker.rb +19 -0
- data/app/workers/launchpad/ieo/order_release_worker.rb +22 -0
- data/app/workers/launchpad/ieo/sale_cancel_worker.rb +20 -0
- data/app/workers/launchpad/ieo/sale_currency_list_worker.rb +19 -0
- data/app/workers/launchpad/ieo/sale_distribute_worker.rb +20 -0
- data/app/workers/launchpad/ieo/sale_finish_worker.rb +21 -0
- data/app/workers/launchpad/ieo/sale_pair_list_worker.rb +21 -0
- data/app/workers/launchpad/ieo/sale_release_funds_worker.rb +23 -0
- data/app/workers/launchpad/ieo/sale_start_worker.rb +21 -0
- data/config/initializers/active_model.rb +13 -0
- data/config/initializers/api_pagination.rb +33 -0
- data/config/initializers/inflections.rb +19 -0
- data/config/routes.rb +35 -0
- data/db/migrate/20191120145404_create_launchpad_ieo.rb +52 -0
- data/db/migrate/20200814114105_add_fees_policy_in_sale.rb +5 -0
- data/lib/launchpad.rb +10 -0
- data/lib/launchpad/engine.rb +17 -0
- data/lib/launchpad/precision_validator.rb +25 -0
- data/lib/launchpad/version.rb +3 -0
- data/lib/tasks/launchpad_tasks.rake +4 -0
- 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
|
+
#
|