taunchpad 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,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