Initial commit

This commit is contained in:
Yaser
2026-08-13 19:50:53 +03:30
commit 38084458fe
879 changed files with 95198 additions and 0 deletions

12
app/workers/amqp/base.rb Normal file
View File

@@ -0,0 +1,12 @@
# encoding: UTF-8
# frozen_string_literal: true
module Workers
module AMQP
class Base
def is_db_connection_error?(exception)
exception.is_a?(Mysql2::Error::ConnectionError) || exception.cause.is_a?(Mysql2::Error)
end
end
end
end

View File

@@ -0,0 +1,58 @@
# encoding: UTF-8
# frozen_string_literal: true
module Workers
module AMQP
class DepositCoinAddress < Base
def process(payload)
payload.symbolize_keys!
member = Member.find_by_id(payload[:member_id])
unless member
Rails.logger.warn do
'Unable to generate deposit address.'\
"Member with id: #{payload[:member_id]} doesn't exist"
end
return
end
wallet = Wallet.find_by_id(payload[:wallet_id])
unless wallet
Rails.logger.warn do
'Unable to generate deposit address.'\
"Deposit Wallet with id: #{payload[:wallet_id]} doesn't exist"
end
return
end
wallet_service = WalletService.new(wallet)
member.payment_address(wallet.id).tap do |pa|
pa.with_lock do
next if pa.address.present?
# Supply address ID in case of BitGo address generation if it exists.
result = wallet_service.create_address!(member.uid, pa.details.merge(updated_at: pa.updated_at))
if result.present?
pa.update!(address: result[:address],
secret: result[:secret],
details: result.fetch(:details, {}).merge(pa.details))
end
end
pa.trigger_address_event unless pa.address.blank?
end
# Don't re-enqueue this job in case of error.
# The system is designed in such way that when user will
# request list of accounts system will ask to generate address again (if it is not generated of course).
rescue StandardError => e
raise e if is_db_connection_error?(e)
report_exception(e)
end
end
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
module Workers
module AMQP
class InfluxWriter < Base
def process(payload, metadata, _delivery_info)
case metadata[:headers]['type']
when 'local'
trade = Trade.new payload
trade.write_to_influx
when 'upstream'
trade = Trade.new payload.merge(total: payload['price'].to_d * payload['amount'].to_d)
trade.write_to_influx
end
end
end
end
end

View File

@@ -0,0 +1,160 @@
# encoding: UTF-8
# frozen_string_literal: true
module Workers
module AMQP
class Matching < Base
class DryrunError < StandardError
attr :engine
def initialize(engine)
@engine = engine
end
end
def initialize(options={})
@options = options
reload 'all'
end
def process(payload, metadata, delivery_info)
payload.symbolize_keys!
case payload[:action]
when 'submit'
submit build_order(payload[:order])
when 'cancel'
cancel build_order(payload[:order])
when 'reload'
reload payload[:market]
when 'new'
initialize_engine Market.find(payload[:market])
else
Rails.logger.fatal { "Unknown action: #{payload[:action]}" }
end
end
def submit(order)
engines[order.market].submit(order)
end
def cancel(order)
engines[order.market].cancel(order)
end
def reload(market)
if market == 'all'
# NOTE: Run matching engine for disabled markets.
Market.find_each(&method(:initialize_engine))
Rails.logger.info { "All engines reloaded." }
else
initialize_engine Market.find(market)
Rails.logger.info { "#{market} engine reloaded." }
end
rescue DryrunError => e
# stop started engines
engines.each {|id, engine| engine.shift_gears(:dryrun) unless engine == e.engine }
Rails.logger.fatal { "#{market} engine failed to start. Matched during dryrun:" }
e.engine.queue.each do |trade|
Rails.logger.info { trade[1].inspect }
end
end
def build_order(attrs)
::Matching::OrderBookManager.build_order attrs
end
def initialize_engine(market)
engine = create_engine(market)
load_orders(market)
engine.initializing = false
engine.publish_snapshot
start_engine(market)
end
def create_engine(market)
engines[market.id] = ::Matching::Engine.new(market, @options)
end
def load_orders(market)
::Order.active.with_market(market.id).order('id asc').each do |order|
submit build_order(order.to_matching_attributes)
end
end
def start_engine(market)
engine = engines[market.id]
if engine.mode == :dryrun
if engine.queue.empty?
engine.shift_gears :run
else
accept = ENV['ACCEPT_MINUTES'] ? ENV['ACCEPT_MINUTES'].to_i : 30
order_ids = engine.queue
.map {|args| [args[1][:trade][:maker_order_id], args[1][:trade][:taker_order_id]] }
.flatten.uniq
orders = Order.where('created_at < ?', accept.minutes.ago).where(id: order_ids)
if orders.exists?
# there're very old orders matched, need human intervention
raise DryrunError, engine
else
# only buffered orders matched, just publish trades and continue
engine.queue.each {|args| ::AMQP::Queue.enqueue(*args) }
engine.shift_gears :run
end
end
else
Rails.logger.info { "#{market.id} engine already started. mode=#{engine.mode}" }
end
end
def engines
@engines ||= {}
end
# dump limit orderbook
def on_usr1
engines.each do |id, eng|
dump_file = File.join('/', 'tmp', "limit_orderbook_#{id}")
limit_orders = eng.limit_orders
File.open(dump_file, 'w') do |f|
f.puts "ASK"
limit_orders[:ask].keys.reverse.each do |k|
f.puts k.to_s('F')
limit_orders[:ask][k].each {|o| f.puts "\t#{o.label}" }
end
f.puts "-"*40
limit_orders[:bid].keys.reverse.each do |k|
f.puts k.to_s('F')
limit_orders[:bid][k].each {|o| f.puts "\t#{o.label}" }
end
f.puts "BID"
end
puts "#{id} limit orderbook dumped to #{dump_file}."
end
end
# dump market orderbook
def on_usr2
engines.each do |id, eng|
dump_file = File.join('/', 'tmp', "market_orderbook_#{id}")
market_orders = eng.market_orders
File.open(dump_file, 'w') do |f|
f.puts "ASK"
market_orders[:ask].each {|o| f.puts "\t#{o.label}" }
f.puts "-"*40
market_orders[:bid].each {|o| f.puts "\t#{o.label}" }
f.puts "BID"
end
puts "#{id} market orderbook dumped to #{dump_file}."
end
end
end
end
end

View File

@@ -0,0 +1,33 @@
# encoding: UTF-8
# frozen_string_literal: true
module Workers
module AMQP
class OrderProcessor < Base
def initialize
Order.where(state: ::Order::PENDING).find_each do |order|
Order.submit(order.id)
rescue StandardError => e
::AMQP::Queue.enqueue(:trade_error, e.message)
report_exception_to_screen(e)
raise e if is_db_connection_error?(e)
end
end
def process(payload)
case payload['action']
when 'submit'
Order.submit(payload.dig('order', 'id'))
when 'cancel'
Order.cancel(payload.dig('order', 'id'))
end
rescue StandardError => e
::AMQP::Queue.enqueue(:trade_error, e.message)
report_exception_to_screen(e)
raise e if is_db_connection_error?(e)
end
end
end
end

View File

@@ -0,0 +1,12 @@
# encoding: UTF-8
# frozen_string_literal: true
module Workers
module AMQP
class TradeExecutor < Base
def process(payload)
::Matching::Executor.new(payload.symbolize_keys).process
end
end
end
end

View File

@@ -0,0 +1,95 @@
# encoding: UTF-8
# frozen_string_literal: true
module Workers
module AMQP
class WithdrawCoin < Base
def initialize
@logger = TaggedLogger.new(Rails.logger, worker: __FILE__)
end
def process(payload)
payload.symbolize_keys!
@logger.warn id: payload[:id], message: 'Received request for processing withdraw.'
withdraw = Withdraw.find_by_id(payload[:id])
if withdraw.blank?
@logger.warn id: payload[:id], message: 'The withdraw with such ID doesn\'t exist in database.'
return
end
withdraw.with_lock do
unless withdraw.processing?
@logger.warn id: withdraw.id,
message: 'The withdraw is being processed by another worker or has already been processed.'
return
end
if withdraw.rid.blank?
@logger.warn id: withdraw.id,
message: 'The destination address doesn\'t exist.'
withdraw.fail!
return
end
@logger.warn id: withdraw.id,
amount: withdraw.amount.to_s('F'),
fee: withdraw.fee.to_s('F'),
currency: withdraw.currency.code.upcase,
rid: withdraw.rid,
message: 'Sending witdraw.'
wallet = Wallet.active.joins(:currencies)
.find_by(currencies: { id: withdraw.currency_id }, kind: :hot)
unless wallet
@logger.warn id: withdraw.id,
currency: withdraw.currency.code.upcase,
message: 'Can\'t find active hot wallet for currency.'
withdraw.skip!
return
end
balance = wallet.current_balance(withdraw.currency)
if balance == 'N/A' || balance < withdraw.amount
@logger.warn id: withdraw.id,
balance: balance.to_s,
amount: withdraw.amount.to_s,
message: 'The withdraw skipped because wallet balance is not sufficient or amount greater than wallet max_balance.'
return withdraw.skip!
end
@logger.warn id: withdraw.id,
message: 'Sending request to Wallet Service.'
wallet_service = WalletService.new(wallet)
transaction = wallet_service.build_withdrawal!(withdraw)
@logger.warn id: withdraw.id,
tid: transaction.hash,
message: 'The currency API accepted withdraw and assigned transaction ID.'
@logger.warn id: withdraw.id,
message: 'Updating withdraw state in database.'
withdraw.txid = transaction.hash
withdraw.dispatch
withdraw.save!
@logger.warn id: withdraw.id, message: 'Withdrawal has processed'
rescue StandardError => e
@logger.warn id: withdraw.id, message: 'Failed to process withdrawal. See exception details below.'
report_exception(e)
withdraw.err! e
raise e if is_db_connection_error?(e)
@logger.warn id: withdraw.id,
message: 'Setting withdrawal state to errored.'
end
end
end
end
end