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