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