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,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
|