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,33 @@
|
|
1
|
+
module Barong
|
2
|
+
module ManagementAPIV2
|
3
|
+
class Client < ::ManagementAPIV2::Client
|
4
|
+
def initialize(*)
|
5
|
+
super ENV.fetch('BARONG_URL'), Rails.configuration.x.barong_management_api_v2_configuration
|
6
|
+
end
|
7
|
+
|
8
|
+
def otp_sign(request_params = {})
|
9
|
+
self.action = :otp_sign
|
10
|
+
params = request_params.slice(:user_uid, :otp_code, :jwt)
|
11
|
+
request(:post, 'otp/sign', params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_user_info(request_params={})
|
15
|
+
self.action = :read_users
|
16
|
+
params = request_params.slice(:uid, :extended, :jwt)
|
17
|
+
request(:post, "users/get", params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def update_label(request_params = {})
|
21
|
+
self.action = :write_labels
|
22
|
+
params = request_params.slice(:user_uid, :key, :value, :jwt, :replace)
|
23
|
+
request(:put, 'labels', params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_label(request_params = {})
|
27
|
+
self.action = :write_labels
|
28
|
+
params = request_params.slice(:user_uid, :key, :value, :jwt)
|
29
|
+
request(:post, 'labels', params)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module ManagementAPIV2
|
6
|
+
class Client
|
7
|
+
|
8
|
+
attr_reader :action
|
9
|
+
|
10
|
+
def initialize(root_url, security_configuration)
|
11
|
+
@root_api_url = root_url
|
12
|
+
@security_configuration = security_configuration
|
13
|
+
end
|
14
|
+
|
15
|
+
def request(request_method, request_path, request_parameters, options = {})
|
16
|
+
options = { jwt: false }.merge(options)
|
17
|
+
raise ArgumentError, "Request method is not supported: #{request_method.inspect}." unless request_method.in?(%i[post put])
|
18
|
+
|
19
|
+
request_parameters = generate_jwt(payload(request_parameters)) unless options[:jwt]
|
20
|
+
|
21
|
+
begin
|
22
|
+
http_client
|
23
|
+
.public_send(request_method, build_path(request_path), request_parameters)
|
24
|
+
.tap { |response| raise ManagementAPIV2::Exception.new(response) unless response.success? }
|
25
|
+
.body
|
26
|
+
.symbolize_keys
|
27
|
+
|
28
|
+
rescue Faraday::Error => e
|
29
|
+
raise ManagementAPIV2::Exception.new
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_path(path)
|
34
|
+
"api/v2/management/#{path}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def http_client
|
38
|
+
Faraday.new(url: @root_api_url) do |conn|
|
39
|
+
conn.request :json
|
40
|
+
conn.response :json
|
41
|
+
conn.adapter Faraday.default_adapter
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def keychain(field)
|
46
|
+
{}.tap do |h|
|
47
|
+
@security_configuration[:keychain].each do |id, key|
|
48
|
+
next unless action
|
49
|
+
next unless id.in?(action[:required_signatures])
|
50
|
+
h[id] = key[field]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def payload(data = {})
|
56
|
+
{
|
57
|
+
data: data,
|
58
|
+
iat: Time.now.to_i,
|
59
|
+
exp: Time.now.to_i + ENV.fetch('JWT_EXPIRE_DATE', 60).to_i,
|
60
|
+
jti: SecureRandom.hex(12),
|
61
|
+
iss: 'applogic'
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def generate_jwt(payload)
|
66
|
+
JWT::Multisig.generate_jwt(payload, keychain(:value), keychain(:algorithm))
|
67
|
+
end
|
68
|
+
|
69
|
+
def action=(value)
|
70
|
+
@action = @security_configuration[:actions].fetch(value)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ManagementAPIV2
|
4
|
+
class Exception < StandardError
|
5
|
+
attr_accessor :status
|
6
|
+
|
7
|
+
def initialize(response_or_ex="External services error")
|
8
|
+
@status = 503
|
9
|
+
if response_or_ex.respond_to?(:body)
|
10
|
+
@status = 422
|
11
|
+
body = response_or_ex.body || {}
|
12
|
+
|
13
|
+
if body.fetch("error", false)
|
14
|
+
super body.fetch("error")
|
15
|
+
elsif body.fetch("errors", false)
|
16
|
+
super Array(body.fetch("errors")).first
|
17
|
+
else
|
18
|
+
super response_or_ex.body
|
19
|
+
end
|
20
|
+
else
|
21
|
+
super response_or_ex
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Peatio
|
2
|
+
module ManagementAPIV2
|
3
|
+
class Client < ::ManagementAPIV2::Client
|
4
|
+
def initialize(*)
|
5
|
+
super ENV.fetch('PEATIO_URL'), Rails.configuration.x.peatio_management_api_v2_configuration
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_withdraw(request_params = {})
|
9
|
+
self.action = :write_withdraws
|
10
|
+
jwt = payload(request_params.slice(:uid, :tid, :beneficiary_id, :currency, :amount, :action, :note))
|
11
|
+
.yield_self { |payload| generate_jwt(payload) }
|
12
|
+
.yield_self do |jwt|
|
13
|
+
Barong::ManagementAPIV2::Client.new.otp_sign(request_params.merge(jwt: jwt, user_uid: request_params[:uid]))
|
14
|
+
end
|
15
|
+
request(:post, 'withdraws/new', jwt, jwt: true)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_transfer(request_params={})
|
19
|
+
self.action = :write_transfers
|
20
|
+
params = request_params.slice(:key, :category, :description, :operations)
|
21
|
+
request(:post, "transfers/new", params, {})
|
22
|
+
end
|
23
|
+
|
24
|
+
def balance(request_params={})
|
25
|
+
self.action = :read_accounts
|
26
|
+
params = request_params.slice(:uid, :currency)
|
27
|
+
request(:post, "/accounts/balance", params, {})
|
28
|
+
end
|
29
|
+
|
30
|
+
def currency(request_params={})
|
31
|
+
self.action = :read_currencies
|
32
|
+
params = request_params.slice(:code)
|
33
|
+
request(:post, "/currencies/#{params[:code]}", {})
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_market(request_params={})
|
37
|
+
self.action = :write_markets
|
38
|
+
params = request_params.slice(:id, :state)
|
39
|
+
request(:put, "/markets/update", params)
|
40
|
+
end
|
41
|
+
|
42
|
+
def update_currency(request_params={})
|
43
|
+
self.action = :write_currencies
|
44
|
+
params = request_params.slice(:id, :state)
|
45
|
+
request(:put, "/currencies/update", params)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
# TODO: Document code.
|
5
|
+
class OrderExecuteWorker
|
6
|
+
include Sidekiq::Worker
|
7
|
+
|
8
|
+
def perform(ogid)
|
9
|
+
order = GlobalID::Locator.locate_signed(ogid, for: 'order_execute')
|
10
|
+
|
11
|
+
Rails.logger.info { "Start #{order.id} order execution" }
|
12
|
+
begin
|
13
|
+
order.purchase!
|
14
|
+
rescue ManagementAPIV2::Exception => e
|
15
|
+
Rails.logger.error e.message
|
16
|
+
# TODO: Later this logic will be moved to separate worker.
|
17
|
+
# Which will create cancel transfer and cancel order if creation is
|
18
|
+
# successful and change order state to something like undefined.
|
19
|
+
order.restore_attributes
|
20
|
+
order.cancel!
|
21
|
+
return
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
class OrderRefundWorker
|
5
|
+
include Sidekiq::Worker
|
6
|
+
|
7
|
+
def perform(ogid)
|
8
|
+
order = GlobalID::Locator.locate_signed(ogid, for: 'order_refund')
|
9
|
+
|
10
|
+
order.close!
|
11
|
+
Rails.logger.info { "Refunding #{order.id} order" }
|
12
|
+
rescue AASM::InvalidTransition => e
|
13
|
+
Rails.logger.error order_id: order.id,
|
14
|
+
message: 'Failed to refund',
|
15
|
+
error: e.message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
# TODO: Document code.
|
5
|
+
class OrderReleaseWorker
|
6
|
+
include Sidekiq::Worker
|
7
|
+
|
8
|
+
def perform(ogid)
|
9
|
+
order = GlobalID::Locator.locate_signed(ogid, for: 'order_release')
|
10
|
+
|
11
|
+
Rails.logger.info { "Start #{order.id} order release" }
|
12
|
+
begin
|
13
|
+
order.unlock!
|
14
|
+
rescue ManagementAPIV2::Exception => e
|
15
|
+
Rails.logger.error order_id: order.id,
|
16
|
+
message: 'Failed to refund',
|
17
|
+
error: e.message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
class SaleCancelWorker
|
5
|
+
include Sidekiq::Worker
|
6
|
+
|
7
|
+
def perform(sgid)
|
8
|
+
sale = GlobalID::Locator.locate_signed(sgid, for: 'sale_cancel')
|
9
|
+
|
10
|
+
sale.cancel!
|
11
|
+
Rails.logger.info { "Start #{sale.id} sale cancelling" }
|
12
|
+
rescue AASM::InvalidTransition => e
|
13
|
+
# TODO: Improve logging using Tagged Logger.
|
14
|
+
Rails.logger.error sale_id: sale.id,
|
15
|
+
message: 'Failed to cancel',
|
16
|
+
error: e.message
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
class SaleCurrencyListWorker
|
5
|
+
include Sidekiq::Worker
|
6
|
+
|
7
|
+
def perform(sgid)
|
8
|
+
sale = GlobalID::Locator.locate_signed(sgid, for: "sale_currency_list")
|
9
|
+
sale.list_currency
|
10
|
+
|
11
|
+
Rails.logger.info { "Successfully listed #{sale.currency_id} for sale #{sale.id}" }
|
12
|
+
rescue ManagementAPIV2::Exception => e
|
13
|
+
Rails.logger.error sale_pair_id: sale.id,
|
14
|
+
message: "Failed to list",
|
15
|
+
error: e.message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
class SaleDistributeWorker
|
5
|
+
include Sidekiq::Worker
|
6
|
+
|
7
|
+
def perform(sgid)
|
8
|
+
sale = GlobalID::Locator.locate_signed(sgid, for: 'sale_distribute')
|
9
|
+
|
10
|
+
sale.distribute!
|
11
|
+
Rails.logger.info { "Start #{sale.id} sale distribution" }
|
12
|
+
rescue AASM::InvalidTransition => e
|
13
|
+
# TODO: Improve logging using Tagged Logger.
|
14
|
+
Rails.logger.error sale_id: sale.id,
|
15
|
+
message: 'Failed to distribute',
|
16
|
+
error: e.message
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
# TODO: Add logging to all workers.
|
5
|
+
class SaleFinishWorker
|
6
|
+
include Sidekiq::Worker
|
7
|
+
|
8
|
+
def perform(sgid)
|
9
|
+
sale = GlobalID::Locator.locate_signed(sgid, for: 'sale_finish')
|
10
|
+
|
11
|
+
sale.finish!
|
12
|
+
Rails.logger.info { "Finishing #{sale.id} sale" }
|
13
|
+
rescue AASM::InvalidTransition => e
|
14
|
+
# TODO: Improve logging using Tagged Logger.
|
15
|
+
Rails.logger.error sale_id: sale.id,
|
16
|
+
message: 'Failed to finish',
|
17
|
+
error: e.message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
class SalePairListWorker
|
5
|
+
include Sidekiq::Worker
|
6
|
+
|
7
|
+
def perform(sgid)
|
8
|
+
sale_pair = GlobalID::Locator.locate_signed(sgid, for: "sale_pair_list")
|
9
|
+
sale_pair.list_market
|
10
|
+
sale_pair.update!(listed: true)
|
11
|
+
|
12
|
+
Rails.logger.info { "Successfully listed #{sale_pair.id} sale pair" }
|
13
|
+
rescue ManagementAPIV2::Exception => e
|
14
|
+
sale_pair.update!(listed: false)
|
15
|
+
Rails.logger.error sale_pair_id: sale_pair.id,
|
16
|
+
message: "Failed to list",
|
17
|
+
error: e.message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
|
3
|
+
module Launchpad
|
4
|
+
module IEO
|
5
|
+
# TODO: Document code.
|
6
|
+
class SaleReleaseFundsWorker
|
7
|
+
include Sidekiq::Worker
|
8
|
+
|
9
|
+
def perform(sgid, funds_percent)
|
10
|
+
sale = GlobalID::Locator.locate_signed(sgid, for: 'sale_release_funds')
|
11
|
+
|
12
|
+
Rails.logger.info { "Start sale: #{sale.id} releasing funds" }
|
13
|
+
begin
|
14
|
+
sale.orders.release_fund(funds_percent)
|
15
|
+
rescue ManagementAPIV2::Exception => e
|
16
|
+
Rails.logger.error sale_id: sale.id,
|
17
|
+
message: 'Failed to release sale',
|
18
|
+
error: e.message
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Launchpad
|
3
|
+
module IEO
|
4
|
+
# TODO: Add logging to all workers.
|
5
|
+
class SaleStartWorker
|
6
|
+
include Sidekiq::Worker
|
7
|
+
|
8
|
+
def perform(sgid)
|
9
|
+
sale = GlobalID::Locator.locate_signed(sgid, for: 'sale_start')
|
10
|
+
|
11
|
+
sale.start!
|
12
|
+
Rails.logger.info { "Starting #{sale.id} sale" }
|
13
|
+
rescue AASM::InvalidTransition => e
|
14
|
+
# TODO: Improve logging using Tagged Logger.
|
15
|
+
Rails.logger.error sale_id: sale.id,
|
16
|
+
message: 'Failed to start',
|
17
|
+
error: e.message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Module.new do
|
4
|
+
def api_messages
|
5
|
+
map do |attr, err|
|
6
|
+
if err.start_with?("_")
|
7
|
+
[attr, err]
|
8
|
+
else
|
9
|
+
[err, attr]
|
10
|
+
end.join
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end.tap { |m| ActiveSupport.on_load(:active_record) { ActiveModel::Errors.include(m) } }
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "api-pagination"
|
4
|
+
require "pagy/countless"
|
5
|
+
require "pagy/extras/overflow"
|
6
|
+
|
7
|
+
ApiPagination.configure do |config|
|
8
|
+
# If you have more than one gem included, you can choose a paginator.
|
9
|
+
config.paginator = :pagy # or :will_ApiPagination.paginate
|
10
|
+
|
11
|
+
# By default, this is set to 'Total'
|
12
|
+
# config.total_header = 'X-Total'
|
13
|
+
|
14
|
+
# By default, this is set to 'Per-Page'
|
15
|
+
# config.per_page_header = 'X-Per-Page'
|
16
|
+
|
17
|
+
# Optional: set this to add a header with the current page number.
|
18
|
+
config.page_header = "Page"
|
19
|
+
|
20
|
+
# Optional: set this to add other response format. Useful with tools that define :jsonapi format
|
21
|
+
# config.response_formats = [:json, :xml, :jsonapi]
|
22
|
+
|
23
|
+
# Optional: what parameter should be used to set the page option
|
24
|
+
config.page_param = :page
|
25
|
+
|
26
|
+
# Optional: what parameter should be used to set the per page option
|
27
|
+
config.per_page_param = :limit
|
28
|
+
|
29
|
+
# Optional: Include the total and last_page link header
|
30
|
+
# By default, this is set to true
|
31
|
+
# Note: When using kaminari, this prevents the count call to the database
|
32
|
+
# config.include_total = false
|
33
|
+
end
|