Initial commit
This commit is contained in:
10
lib/api_bench.rb
Normal file
10
lib/api_bench.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class ApiBench
|
||||
def create_order(base_url, market, side, volume, token, ord_type, price)
|
||||
url = "#{base_url}/api/v2/market/orders?market=#{market}&side=#{side}&volume=#{volume}&ord_type=#{ord_type}&price=#{price}"
|
||||
connection = Faraday.new(url)
|
||||
_response = connection.post do |request|
|
||||
request.headers["Authorization"] = token
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
96
lib/daemons/amqp_daemon.rb
Normal file
96
lib/daemons/amqp_daemon.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
ROOT = File.expand_path('../../..', __FILE__)
|
||||
|
||||
require File.join(ROOT, 'config', 'environment')
|
||||
|
||||
raise "bindings must be provided." if ARGV.size == 0
|
||||
|
||||
logger = Rails.logger
|
||||
|
||||
conn = Bunny.new AMQP::Config.connect
|
||||
conn.start
|
||||
|
||||
ch = conn.create_channel
|
||||
id = $0.split(':')[2]
|
||||
prefetch = AMQP::Config.channel(id)[:prefetch] || 0
|
||||
ch.prefetch(prefetch) if prefetch > 0
|
||||
logger.info { "Connected to AMQP broker (prefetch: #{prefetch > 0 ? prefetch : 'default'})" }
|
||||
|
||||
terminate = proc do
|
||||
# logger is forbidden in signal handling, just use puts here
|
||||
puts "Terminating threads .."
|
||||
ch.work_pool.kill
|
||||
puts "Stopped."
|
||||
end
|
||||
|
||||
at_exit { conn.close }
|
||||
|
||||
Signal.trap("INT", &terminate)
|
||||
Signal.trap("TERM", &terminate)
|
||||
|
||||
workers = []
|
||||
ARGV.each do |id|
|
||||
worker = AMQP::Config.binding_worker(id)
|
||||
queue = ch.queue *AMQP::Config.binding_queue(id)
|
||||
|
||||
if args = AMQP::Config.binding_exchange(id)
|
||||
x = ch.send *args
|
||||
|
||||
case args.first
|
||||
when 'direct'
|
||||
queue.bind x, routing_key: AMQP::Config.routing_key(id)
|
||||
when 'topic'
|
||||
AMQP::Config.topics(id).each do |topic|
|
||||
queue.bind x, routing_key: topic
|
||||
end
|
||||
else
|
||||
queue.bind x
|
||||
end
|
||||
end
|
||||
|
||||
clean_start = AMQP::Config.data[:binding][id][:clean_start]
|
||||
queue.purge if clean_start
|
||||
|
||||
# Enable manual acknowledge mode by setting manual_ack: true.
|
||||
queue.subscribe manual_ack: true do |delivery_info, metadata, payload|
|
||||
logger.info { "Received: #{payload}" }
|
||||
begin
|
||||
|
||||
# Invoke Worker#process with floating number of arguments.
|
||||
args = [JSON.parse(payload), metadata, delivery_info]
|
||||
arity = worker.method(:process).arity
|
||||
resized_args = arity < 0 ? args : args[0...arity]
|
||||
worker.process(*resized_args)
|
||||
|
||||
# Send confirmation to RabbitMQ that message has been successfully processed.
|
||||
# See http://rubybunny.info/articles/queues.html
|
||||
ch.ack(delivery_info.delivery_tag)
|
||||
|
||||
rescue StandardError => e
|
||||
|
||||
# Ask RabbitMQ to deliver message once again later.
|
||||
# See http://rubybunny.info/articles/queues.html
|
||||
ch.nack(delivery_info.delivery_tag, false, true)
|
||||
|
||||
if worker.is_db_connection_error?(e)
|
||||
logger.error(db: :unhealthy, message: e.message)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
report_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
workers << worker
|
||||
end
|
||||
|
||||
%w(USR1 USR2).each do |signal|
|
||||
Signal.trap(signal) do
|
||||
puts "#{signal} received."
|
||||
handler = "on_#{signal.downcase}"
|
||||
workers.each {|w| w.send handler if w.respond_to?(handler) }
|
||||
end
|
||||
end
|
||||
|
||||
ch.work_pool.join
|
||||
113
lib/daemons/daemons.god
Normal file
113
lib/daemons/daemons.god
Normal file
@@ -0,0 +1,113 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
RAILS_ENV = ENV.fetch('RAILS_ENV', 'development')
|
||||
RAILS_ROOT = File.expand_path('../../..', __FILE__)
|
||||
|
||||
require 'shellwords'
|
||||
|
||||
# Create non-default log/daemons directory.
|
||||
require 'fileutils'
|
||||
FileUtils.mkdir_p "#{RAILS_ROOT}/log/daemons"
|
||||
|
||||
def daemon(name, options = {})
|
||||
God.watch do |w|
|
||||
command = "bundle exec ruby lib/daemons/#{options.fetch(:script)}"
|
||||
command += ' ' + options[:arguments].join(' ') if options.key?(:arguments)
|
||||
filesafe_name = name.gsub(/\W/, '_')
|
||||
|
||||
w.name = name
|
||||
w.start = command
|
||||
w.dir = RAILS_ROOT
|
||||
w.env = { 'RAILS_ENV' => RAILS_ENV, 'RAILS_ROOT' => RAILS_ROOT }
|
||||
|
||||
# Peatio has lot of dependencies which take some time to load ever on fast disks.
|
||||
# God, by default, doesn't wait before resuming normal monitoring operations.
|
||||
# So we need to adjust this variable so God will wait 10 seconds on start/restart operations.
|
||||
w.grace = 10.seconds
|
||||
|
||||
# God will send SIGTERM to the process and wait 10 seconds.
|
||||
# If process has still not exited it will be killed by sending SIGKILL.
|
||||
w.stop_signal = 'TERM'
|
||||
w.stop_timeout = 10.seconds
|
||||
|
||||
# God will always keep process running unless it was manually terminated.
|
||||
w.keepalive
|
||||
|
||||
# In production Docker environment logs go to /dev/stdout.
|
||||
if RAILS_ENV == 'production'
|
||||
w.log_cmd = "#{RAILS_ROOT}/bin/logger #{name.shellescape}"
|
||||
#
|
||||
# In non-production environment logs go to files.
|
||||
else
|
||||
w.log = "#{RAILS_ROOT}/log/daemons/#{filesafe_name}.log"
|
||||
end
|
||||
|
||||
# Allow customizations.
|
||||
yield(w) if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
daemon 'amqp:deposit_collection',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ deposit_collection ]
|
||||
|
||||
daemon 'amqp:deposit_collection_fees',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ deposit_collection_fees ]
|
||||
|
||||
daemon 'amqp:deposit_coin_address',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ deposit_coin_address ]
|
||||
|
||||
daemon 'amqp:influx_writer',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ influx_writer ]
|
||||
|
||||
daemon 'amqp:market_ticker',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ market_ticker ]
|
||||
|
||||
daemon 'amqp:matching',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ matching ]
|
||||
|
||||
daemon 'amqp:order_processor',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ order_processor ]
|
||||
|
||||
daemon 'amqp:pusher_market',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ pusher_market ]
|
||||
|
||||
daemon 'amqp:pusher_member',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ pusher_member ]
|
||||
|
||||
daemon 'amqp:trade_executor',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ trade_executor ]
|
||||
|
||||
daemon 'amqp:withdraw_coin',
|
||||
script: 'amqp_daemon.rb',
|
||||
arguments: %w[ withdraw_coin ]
|
||||
|
||||
daemon 'daemon:blockchain',
|
||||
script: 'daemons.rb',
|
||||
arguments: %w[ blockchain ]
|
||||
|
||||
daemon 'daemon:k',
|
||||
script: 'daemons.rb',
|
||||
arguments: %w[ k ]
|
||||
|
||||
daemon 'daemon:global_state',
|
||||
script: 'daemons.rb',
|
||||
arguments: %w[ global_state ]
|
||||
|
||||
daemon 'daemon:withdraw_audit',
|
||||
script: 'daemons.rb',
|
||||
arguments: %w[ withdraw_audit ]
|
||||
|
||||
daemon 'daemon:slave_book',
|
||||
script: 'daemons.rb',
|
||||
arguments: %w[ slave_book ]
|
||||
31
lib/daemons/daemons.rb
Normal file
31
lib/daemons/daemons.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
ROOT = File.expand_path('../../..', __FILE__)
|
||||
require File.join(ROOT, 'config', 'environment')
|
||||
# default in peatio image
|
||||
# require File.join(ENV.fetch('RAILS_ROOT'), 'config', 'environment')
|
||||
|
||||
raise "Worker name must be provided." if ARGV.size == 0
|
||||
|
||||
name = ARGV[0]
|
||||
worker = "Workers::Daemons::#{name.camelize}".constantize.new
|
||||
|
||||
terminate = proc do
|
||||
puts "Terminating worker .."
|
||||
worker.stop
|
||||
puts "Stopped."
|
||||
end
|
||||
|
||||
Signal.trap("INT", &terminate)
|
||||
Signal.trap("TERM", &terminate)
|
||||
|
||||
begin
|
||||
worker.run
|
||||
rescue StandardError => e
|
||||
if worker.is_db_connection_error?(e)
|
||||
logger.error(db: :unhealthy, message: e.message)
|
||||
raise e
|
||||
end
|
||||
|
||||
report_exception(e)
|
||||
end
|
||||
9
lib/peatio/aasm/locking.rb
Normal file
9
lib/peatio/aasm/locking.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AASM::Locking
|
||||
def aasm_write_state(*)
|
||||
lock!
|
||||
super
|
||||
end
|
||||
end
|
||||
42
lib/peatio/airdrop.rb
Normal file
42
lib/peatio/airdrop.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Peatio
|
||||
class Airdrop
|
||||
def process(src_user, params)
|
||||
Transfer.transaction do
|
||||
CSV.parse(params[:file][:tempfile], headers: true, quote_empty: false).each do |row|
|
||||
row = row.to_h.compact.symbolize_keys!
|
||||
currency = Currency.find(row[:currency_id])
|
||||
amount = row[:amount]
|
||||
credited_user = Member.find_by_uid(row[:uid])
|
||||
next if credited_user.blank?
|
||||
|
||||
code = currency.coin? ? 202 : 201
|
||||
liabilities = [
|
||||
Operations::Liability.new(
|
||||
code: code,
|
||||
currency: currency,
|
||||
debit: amount,
|
||||
member_id: src_user.id
|
||||
),
|
||||
Operations::Liability.new(
|
||||
code: code,
|
||||
currency: currency,
|
||||
credit: amount,
|
||||
member_id: credited_user.id
|
||||
)
|
||||
]
|
||||
|
||||
Transfer.create!(
|
||||
key: "#{credited_user.uid}_#{currency.id}_#{Time.now.to_i}",
|
||||
category: 'airdrop',
|
||||
description: "Transfer from #{src_user.uid} to #{credited_user.uid} currency_id: #{currency.id}, amount: #{amount}",
|
||||
liabilities: liabilities
|
||||
)
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error { e.message }
|
||||
end
|
||||
end
|
||||
end
|
||||
19
lib/peatio/aml.rb
Normal file
19
lib/peatio/aml.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Peatio
|
||||
module AML
|
||||
class << self
|
||||
attr_accessor :adapter
|
||||
|
||||
def check!(address, currency_id, uid)
|
||||
adapter.check!(address, currency_id, uid)
|
||||
end
|
||||
end
|
||||
|
||||
class Abstract
|
||||
def check!(_address, _currency_id, _uid)
|
||||
method_not_implemented
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
62
lib/peatio/amqp/config.rb
Normal file
62
lib/peatio/amqp/config.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AMQP
|
||||
class Config
|
||||
class <<self
|
||||
def data
|
||||
@data ||= Hashie::Mash.new(
|
||||
YAML.safe_load(
|
||||
ERB.new(File.read(Rails.root.join('config', 'amqp.yml'))).result
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def connect
|
||||
data[:connect]
|
||||
end
|
||||
|
||||
def binding_exchange_id(id)
|
||||
data[:binding][id][:exchange]
|
||||
end
|
||||
|
||||
def binding_exchange(id)
|
||||
eid = binding_exchange_id(id)
|
||||
eid && exchange(eid)
|
||||
end
|
||||
|
||||
def binding_queue(id)
|
||||
queue data[:binding][id][:queue]
|
||||
end
|
||||
|
||||
def binding_worker(id)
|
||||
::Workers::AMQP.const_get(id.to_s.camelize).new
|
||||
end
|
||||
|
||||
def routing_key(id)
|
||||
binding_queue(id).first
|
||||
end
|
||||
|
||||
def topics(id)
|
||||
data[:binding][id][:topics].split(',')
|
||||
end
|
||||
|
||||
def channel(id)
|
||||
(data[:channel] && data[:channel][id]) || {}
|
||||
end
|
||||
|
||||
def queue(id)
|
||||
name = data[:queue][id][:name]
|
||||
settings = { durable: data[:queue][id][:durable] }
|
||||
[name, settings]
|
||||
end
|
||||
|
||||
def exchange(id)
|
||||
type = data[:exchange][id][:type]
|
||||
name = data[:exchange][id][:name]
|
||||
[type, name]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
219
lib/peatio/amqp/event_api.rb
Normal file
219
lib/peatio/amqp/event_api.rb
Normal file
@@ -0,0 +1,219 @@
|
||||
module EventAPI
|
||||
|
||||
MAPPING = {
|
||||
'withdraw' => 'system.user.withdraw.confirm.code',
|
||||
'withdraw-coin' => 'system.user.withdraw.confirm.code',
|
||||
'withdraw-fiat' => 'system.user.withdraw.confirm.code'
|
||||
}.freeze
|
||||
|
||||
class << self
|
||||
def notify(event_name, event_payload)
|
||||
event_name = MAPPING.dig(event_name).present? ? MAPPING.dig(event_name) : event_name
|
||||
raise('Dena mailer event name is unknown') unless event_name.present?
|
||||
|
||||
arguments = [event_name, event_payload]
|
||||
middlewares.each do |middleware|
|
||||
returned_value = middleware.call(*arguments)
|
||||
case returned_value
|
||||
when Array then arguments = returned_value
|
||||
else return returned_value
|
||||
end
|
||||
rescue StandardError => e
|
||||
report_exception(e)
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def middlewares=(list)
|
||||
@middlewares = list
|
||||
end
|
||||
|
||||
def middlewares
|
||||
@middlewares ||= []
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveRecord
|
||||
class Mediator
|
||||
attr_reader :record
|
||||
|
||||
def initialize(record)
|
||||
@record = record
|
||||
end
|
||||
|
||||
def notify(partial_event_name, event_payload)
|
||||
tokens = ['model']
|
||||
tokens << record.class.event_api_settings.fetch(:prefix) { record.class.name.underscore.gsub(/\//, '_') }
|
||||
tokens << partial_event_name.to_s
|
||||
full_event_name = tokens.join('.')
|
||||
EventAPI.notify(full_event_name, event_payload)
|
||||
end
|
||||
|
||||
def notify_record_created
|
||||
notify(:created, record: record.as_json_for_event_api.compact)
|
||||
end
|
||||
|
||||
def notify_record_updated
|
||||
return if record.previous_changes.blank?
|
||||
|
||||
current_record = record
|
||||
previous_record = record.dup
|
||||
record.previous_changes.each { |attribute, values| previous_record.send("#{attribute}=", values.first) }
|
||||
|
||||
# Guarantee timestamps.
|
||||
previous_record.created_at ||= current_record.created_at
|
||||
previous_record.updated_at ||= current_record.created_at
|
||||
|
||||
before = previous_record.as_json_for_event_api.compact
|
||||
after = current_record.as_json_for_event_api.compact
|
||||
|
||||
notify :updated, \
|
||||
record: after,
|
||||
changes: before.delete_if { |attribute, value| after[attribute] == value }
|
||||
end
|
||||
end
|
||||
|
||||
module Extension
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# We add «after_commit» callbacks immediately after inclusion.
|
||||
%i[create update].each do |event|
|
||||
after_commit on: event, prepend: true do
|
||||
if self.class.event_api_settings[:on]&.include?(event)
|
||||
event_api.public_send("notify_record_#{event}d")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def acts_as_eventable(settings = {})
|
||||
settings[:on] = %i[create update] unless settings.key?(:on)
|
||||
@event_api_settings = event_api_settings.merge(settings)
|
||||
end
|
||||
|
||||
def event_api_settings
|
||||
@event_api_settings || superclass.instance_variable_get(:@event_api_settings) || {}
|
||||
end
|
||||
end
|
||||
|
||||
def event_api
|
||||
@event_api ||= Mediator.new(self)
|
||||
end
|
||||
|
||||
def as_json_for_event_api
|
||||
as_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# To continue processing by further middlewares return array with event name and payload.
|
||||
# To stop processing event return any value which isn't an array.
|
||||
module Middlewares
|
||||
|
||||
class << self
|
||||
def application_name
|
||||
Rails.application.class.name.split('::').first.underscore
|
||||
end
|
||||
|
||||
def application_version
|
||||
"#{application_name.camelize}::VERSION".constantize
|
||||
end
|
||||
end
|
||||
|
||||
class IncludeEventMetadata
|
||||
def call(event_name, event_payload)
|
||||
event_payload[:name] = event_name
|
||||
[event_name, event_payload]
|
||||
end
|
||||
end
|
||||
|
||||
class GenerateJWT
|
||||
def call(event_name, event_payload)
|
||||
jwt_payload = {
|
||||
iss: Middlewares.application_name,
|
||||
jti: SecureRandom.uuid,
|
||||
iat: Time.now.to_i,
|
||||
exp: Time.now.to_i + 60,
|
||||
event: event_payload
|
||||
}
|
||||
private_key = OpenSSL::PKey.read(Base64.urlsafe_decode64(ENV.fetch('EVENT_API_JWT_PRIVATE_KEY')))
|
||||
algorithm = 'RS256'
|
||||
jwt = JWT::Multisig.generate_jwt jwt_payload, \
|
||||
{ Middlewares.application_name.to_sym => private_key },
|
||||
{ Middlewares.application_name.to_sym => algorithm }
|
||||
|
||||
[event_name, jwt]
|
||||
end
|
||||
end
|
||||
|
||||
class PrintToScreen
|
||||
def call(event_name, event_payload)
|
||||
Rails.logger.debug do
|
||||
['',
|
||||
'Produced new event at ' + Time.current.to_s + ': ',
|
||||
'name = ' + event_name,
|
||||
'payload = ' + event_payload.to_json,
|
||||
''].join("\n")
|
||||
end
|
||||
[event_name, event_payload]
|
||||
end
|
||||
end
|
||||
|
||||
class PublishToRabbitMQ
|
||||
extend Memoist
|
||||
|
||||
def call(event_name, event_payload)
|
||||
Rails.logger.debug do
|
||||
"\nPublishing #{routing_key(event_name)} (routing key) to #{exchange_name(event_name)} (exchange name).\n"
|
||||
end
|
||||
exchange = bunny_exchange(exchange_name(event_name))
|
||||
exchange.publish(event_payload.to_json, routing_key: routing_key(event_name))
|
||||
[event_name, event_payload]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def bunny_session
|
||||
Bunny::Session.new(rabbitmq_credentials).tap do |session|
|
||||
session.start
|
||||
Kernel.at_exit { session.stop }
|
||||
end
|
||||
end
|
||||
memoize :bunny_session
|
||||
|
||||
def bunny_channel
|
||||
bunny_session.channel
|
||||
end
|
||||
memoize :bunny_channel
|
||||
|
||||
def bunny_exchange(name)
|
||||
bunny_channel.direct(name)
|
||||
end
|
||||
memoize :bunny_exchange
|
||||
|
||||
def rabbitmq_credentials
|
||||
return ENV['EVENT_API_RABBITMQ_URL'] if ENV['EVENT_API_RABBITMQ_URL'].present?
|
||||
|
||||
{ host: ENV.fetch('EVENT_API_RABBITMQ_HOST'),
|
||||
port: ENV.fetch('EVENT_API_RABBITMQ_PORT'),
|
||||
username: ENV.fetch('EVENT_API_RABBITMQ_USERNAME'),
|
||||
password: ENV.fetch('EVENT_API_RABBITMQ_PASSWORD') }
|
||||
end
|
||||
|
||||
def exchange_name(event_name)
|
||||
"#{Middlewares.application_name}.events.#{event_name.split('.').first}"
|
||||
end
|
||||
|
||||
def routing_key(event_name)
|
||||
event_name.split('.').drop(1).join('.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
middlewares << Middlewares::IncludeEventMetadata.new
|
||||
middlewares << Middlewares::GenerateJWT.new
|
||||
middlewares << Middlewares::PrintToScreen.new
|
||||
middlewares << Middlewares::PublishToRabbitMQ.new
|
||||
end
|
||||
46
lib/peatio/amqp/queue.rb
Normal file
46
lib/peatio/amqp/queue.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AMQP
|
||||
class Queue
|
||||
|
||||
class <<self
|
||||
def connection
|
||||
@connection ||= ::Bunny.new(AMQP::Config.connect).tap do |conn|
|
||||
conn.start
|
||||
end
|
||||
end
|
||||
|
||||
def channel
|
||||
@channel ||= connection.create_channel
|
||||
end
|
||||
|
||||
def exchanges
|
||||
@exchanges ||= { default: channel.default_exchange }
|
||||
end
|
||||
|
||||
def exchange(id)
|
||||
exchanges[id] ||= channel.send *AMQP::Config.exchange(id)
|
||||
end
|
||||
|
||||
def publish(eid, payload, attrs={})
|
||||
payload = JSON.dump payload
|
||||
exchange(eid).publish(payload, attrs)
|
||||
end
|
||||
|
||||
# enqueue = publish to direct exchange
|
||||
def enqueue(id, payload, attrs={})
|
||||
eid = ::AMQP::Config.binding_exchange_id(id) || :default
|
||||
attrs.merge!({ routing_key: AMQP::Config.routing_key(id) })
|
||||
publish(eid, payload, attrs)
|
||||
end
|
||||
|
||||
def enqueue_event(type, id, event, payload, opts={})
|
||||
#::AMQP::Queue.enqueue_event("private", maker.uid, "trade", for_notify(maker))
|
||||
routing_key = [type, id, event].join('.')
|
||||
serialized_data = JSON.dump(payload)
|
||||
channel.exchange('peatio.events.ranger', type: 'topic').publish(serialized_data, routing_key: routing_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
91
lib/peatio/app.rb
Normal file
91
lib/peatio/app.rb
Normal file
@@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Peatio
|
||||
class App
|
||||
include ActiveSupport::Configurable
|
||||
|
||||
class Error < ::StandardError; end
|
||||
|
||||
class << self
|
||||
def define
|
||||
yield self
|
||||
end
|
||||
|
||||
def set(key, default = nil, options = {})
|
||||
value = fetch!(key, default)
|
||||
|
||||
validate!(key, value, options)
|
||||
value = type!(key, value, options)
|
||||
|
||||
config[key] = value
|
||||
end
|
||||
|
||||
def write(key, value)
|
||||
config[key] = value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch!(key, default)
|
||||
if env(key)
|
||||
return env(key)
|
||||
|
||||
elsif Rails.application.credentials[key]
|
||||
return Rails.application.credentials[key]
|
||||
|
||||
elsif !default.nil?
|
||||
return default
|
||||
|
||||
else
|
||||
raise Error, "Config #{key} missing" if default.nil?
|
||||
end
|
||||
end
|
||||
|
||||
def env(key)
|
||||
ENV['PEATIO_' + key.to_s.upcase]
|
||||
end
|
||||
|
||||
def validate!(key, value, options)
|
||||
regex!(key, value, options[:regex]) if options[:regex]
|
||||
values!(key, value, options[:values]) if options[:values]
|
||||
end
|
||||
|
||||
def type!(key, value, options)
|
||||
return value unless options[:type]
|
||||
|
||||
case options[:type]
|
||||
when :array
|
||||
return value.split(',').map { |v| v.squish }
|
||||
when :bool
|
||||
values!(key, value, %w(true false))
|
||||
return value == 'true'
|
||||
when :integer
|
||||
regex!(key, value, /^\d+$/)
|
||||
return value.to_i
|
||||
when :path
|
||||
return Rails.root.join(value).tap { |p| path!(key, p) }
|
||||
when :regexp
|
||||
return Regexp.new value
|
||||
end
|
||||
end
|
||||
|
||||
def path!(key, path)
|
||||
unless File.exists?(path)
|
||||
raise Error.new("#{key.to_s.upcase} path is invalid #{path.to_s}")
|
||||
end
|
||||
end
|
||||
|
||||
def regex!(key, value, regex)
|
||||
unless regex =~ value
|
||||
raise Error.new("#{key.to_s.upcase} does not match regex #{regex.inspect}")
|
||||
end
|
||||
end
|
||||
|
||||
def values!(key, value, values)
|
||||
unless values.include?(value)
|
||||
raise Error.new("#{key.to_s.upcase} invalid, enabled values: #{values.to_s}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
lib/peatio/bench/barong_session.rb
Normal file
28
lib/peatio/bench/barong_session.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module Bench
|
||||
module Barong_session
|
||||
class Sessions
|
||||
def initialize(trader:)
|
||||
@trader = trader
|
||||
end
|
||||
|
||||
def run!
|
||||
config = YAML.load_file("#{Rails.root}/config/application.yml")
|
||||
file = "#{Rails.root}/public/user_data.csv"
|
||||
|
||||
Kernel.puts "Creating members ..."
|
||||
@members = Factories.create_list(:member, @trader)
|
||||
Kernel.puts "sign up URL #{config["development"]["SIGN_UP_URL"]}"
|
||||
|
||||
@members.each do |member|
|
||||
puts %x{http POST #{config["development"]["SIGN_UP_URL"]} email=#{member.email} password=#{config["development"]["SIGN_UP_PASSWORD"]}}
|
||||
Kernel.puts "#{member.email} was requested for sign up with password=#{config["development"]["SIGN_UP_PASSWORD"]}"
|
||||
end
|
||||
CSV.open(file, 'w+', write_headers: true) do |csv|
|
||||
@members.each do |member|
|
||||
csv << %w{email}.map { |attr| member.send(attr) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
85
lib/peatio/bench/factories.rb
Normal file
85
lib/peatio/bench/factories.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bench
|
||||
module Factories
|
||||
class << self
|
||||
def create(model, options = {})
|
||||
"#{self.name}/#{model}"
|
||||
.camelize
|
||||
.constantize
|
||||
.new(options)
|
||||
.create
|
||||
end
|
||||
|
||||
def create_list(model, number, options = {level: 2})
|
||||
"#{self.name}/#{model}"
|
||||
.camelize
|
||||
.constantize
|
||||
.new(options)
|
||||
.create_list(number)
|
||||
end
|
||||
end
|
||||
|
||||
class Member
|
||||
def initialize(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def create
|
||||
::Member.create!(construct_member)
|
||||
end
|
||||
|
||||
def create_list(number)
|
||||
Array.new(number) { create }
|
||||
end
|
||||
|
||||
def construct_member
|
||||
{ email: unique_email,
|
||||
uid: "U#{Faker::Number.number(9)}",
|
||||
level: 3,
|
||||
role: 'member',
|
||||
state: 'active' }.merge(@options)
|
||||
end
|
||||
|
||||
def unique_email
|
||||
@used_emails ||= ::Member.pluck(:email)
|
||||
loop do
|
||||
email = Faker::Internet.unique.email
|
||||
unless @used_emails.include?(email)
|
||||
@used_emails << email
|
||||
return email
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Deposit
|
||||
DEFAULT_DEPOSIT_AMOUNT = 1_000_000_000_000_000
|
||||
def initialize(options)
|
||||
@options = options
|
||||
@currency = Currency.find(options[:currency_id])
|
||||
end
|
||||
|
||||
def create
|
||||
if @currency.fiat?
|
||||
::Deposit.create!(construct_fiat_deposit).tap(&:charge!)
|
||||
else
|
||||
::Deposit.create!(construct_coin_deposit).tap { |d| d.with_lock { d.accept! } }
|
||||
end
|
||||
end
|
||||
|
||||
def construct_fiat_deposit
|
||||
{ amount: DEFAULT_DEPOSIT_AMOUNT,
|
||||
type: 'Deposits::Fiat' }.merge(@options)
|
||||
end
|
||||
|
||||
def construct_coin_deposit
|
||||
{ amount: DEFAULT_DEPOSIT_AMOUNT,
|
||||
address: Faker::Blockchain::Bitcoin.address,
|
||||
txid: Faker::Lorem.characters(64),
|
||||
txout: 0,
|
||||
type: 'Deposits::Coin' }.merge(@options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
lib/peatio/bench/helpers.rb
Normal file
29
lib/peatio/bench/helpers.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bench
|
||||
module Helpers
|
||||
def become_billionaire(member)
|
||||
@currencies.each do |c|
|
||||
Factories.create(:deposit, member_id: member.id, currency_id: c.id)
|
||||
end
|
||||
|
||||
remove_locked
|
||||
end
|
||||
|
||||
### for remove locked create in billioner method
|
||||
def remove_locked
|
||||
::Account.where.not(locked: 0).each do |re|
|
||||
locked = re.locked
|
||||
re.update(balance: locked, locked: 0)
|
||||
end
|
||||
end
|
||||
|
||||
def my_create_order(options)
|
||||
Order.new(options)
|
||||
.tap(&:round_amount_and_price)
|
||||
.tap { |o| o.locked = o.origin_locked = o.compute_locked }
|
||||
.tap { |o| o.hold_account!.lock_funds(o.locked) }
|
||||
.tap(&:save)
|
||||
end
|
||||
end
|
||||
end
|
||||
60
lib/peatio/bench/injectors.rb
Normal file
60
lib/peatio/bench/injectors.rb
Normal file
@@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bench
|
||||
module Injectors
|
||||
|
||||
class << self
|
||||
def initialize_injector(config)
|
||||
"#{self.name}/#{config[:injector]}"
|
||||
.camelize
|
||||
.constantize
|
||||
.new(config)
|
||||
end
|
||||
end
|
||||
|
||||
class Base
|
||||
attr_reader :config
|
||||
|
||||
def initialize(config)
|
||||
@config = config
|
||||
@number = config[:number].to_i
|
||||
@step = config.fetch(:step, 1000).to_i
|
||||
@markets = ::Market.where(id: config[:markets].split(',').map(&:squish).reject(&:blank?))
|
||||
end
|
||||
|
||||
def generate!(members = nil)
|
||||
@members = members || Member.all
|
||||
@queue = Queue.new
|
||||
Array.new(@number / @step) do
|
||||
::Rails.logger.info { "Created orders: #{@queue.size}" }
|
||||
ActiveRecord::Base.transaction do
|
||||
Array.new(@step) do
|
||||
create_order.tap { |o| @queue << o }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def pop
|
||||
# Use non_blocking pop.
|
||||
@queue.pop(true)
|
||||
rescue ThreadError
|
||||
# Return nil in case of empty queue.
|
||||
nil
|
||||
end
|
||||
|
||||
def size
|
||||
@queue.size
|
||||
end
|
||||
|
||||
def create_order
|
||||
Order.new(construct_order)
|
||||
.tap(&:round_amount_and_price)
|
||||
.tap { |o| o.locked = o.origin_locked = o.compute_locked }
|
||||
.tap { |o| o.hold_account!.lock_funds(o.locked) }
|
||||
.tap(&:save)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
37
lib/peatio/bench/injectors/bitfinex.rb
Normal file
37
lib/peatio/bench/injectors/bitfinex.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bench
|
||||
module Injectors
|
||||
class Bitfinex < Base
|
||||
|
||||
def initialize(config)
|
||||
super
|
||||
if config[:data_load_path].present?
|
||||
@data = YAML.load_file(Rails.root.join(config[:data_load_path]))
|
||||
@index = 0
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def construct_order
|
||||
@index = 0 if @data[@index].blank?
|
||||
order_data = @data[@index]
|
||||
price = order_data[1]
|
||||
amount = order_data[2]
|
||||
market = @markets.sample
|
||||
type = amount > 0 ? 'OrderBid' : 'OrderAsk'
|
||||
@index += 1
|
||||
{ type: type,
|
||||
state: Order::WAIT,
|
||||
member: @members.sample,
|
||||
market: market,
|
||||
ask: market.base_unit,
|
||||
bid: market.quote_unit,
|
||||
ord_type: :limit,
|
||||
price: price,
|
||||
volume: amount.abs }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
42
lib/peatio/bench/injectors/dummy.rb
Normal file
42
lib/peatio/bench/injectors/dummy.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bench
|
||||
module Injectors
|
||||
class Dummy < Base
|
||||
extend Memoist
|
||||
|
||||
def initialize(config)
|
||||
super
|
||||
config.reverse_merge!(default_config)
|
||||
%i[min_volume max_volume min_price max_price].each do |var|
|
||||
instance_variable_set(:"@#{var}", config[var])
|
||||
end
|
||||
end
|
||||
|
||||
def construct_order(memberes = nil, order_type = nil)
|
||||
market = @markets.sample
|
||||
@members = memberes if memberes.present?
|
||||
type = order_type || config.fetch(:side) { %w[OrderBid OrderAsk].sample }
|
||||
{ type: type,
|
||||
state: Order::PENDING,
|
||||
member: @members.sample,
|
||||
market: market,
|
||||
ask: market.base_unit,
|
||||
bid: market.quote_unit,
|
||||
ord_type: :limit,
|
||||
price: config.fetch(:price) { rand(@min_price..@max_price) },
|
||||
volume: rand(@min_volume..@max_volume) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_config
|
||||
{ min_volume: 0.1,
|
||||
max_volume: 1,
|
||||
min_price: 0.5,
|
||||
max_price: 2 }
|
||||
end
|
||||
memoize :default_config
|
||||
end
|
||||
end
|
||||
end
|
||||
181
lib/peatio/bench/matching/amqp.rb
Normal file
181
lib/peatio/bench/matching/amqp.rb
Normal file
@@ -0,0 +1,181 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# TODO: Add Bench::Error and better errors processing.
|
||||
# TODO: Add Bench::Report and extract all metrics to it.
|
||||
module Bench
|
||||
module Matching
|
||||
class AMQP
|
||||
# include Helpers
|
||||
include ::Bench::Helpers
|
||||
def initialize(config)
|
||||
@config = config
|
||||
|
||||
@rmq_http_client = RabbitMQHTTP.default_client
|
||||
|
||||
@injector = Injectors.initialize_injector(@config[:orders])
|
||||
@currencies = Currency.where(id: @config[:currencies].split(',').map(&:squish).reject(&:blank?))
|
||||
# TODO: Print errors in the end of benchmark and include them into report.
|
||||
@errors = []
|
||||
end
|
||||
|
||||
def run_fee!
|
||||
# TODO: Check if Matching daemon is running before start (use queue_info[:consumers]).
|
||||
Kernel.puts "Creating members ..."
|
||||
if Member.count == 0
|
||||
@members = Factories.create_list(:member, @config[:traders])
|
||||
else
|
||||
@members = Member.all
|
||||
end
|
||||
|
||||
Kernel.puts "Depositing funds ..."
|
||||
@members.map(&method(:become_billionaire))
|
||||
|
||||
Kernel.puts "Generating orders by injector and saving them in db..."
|
||||
# TODO: Add orders generation progress bar.
|
||||
@config[:round].times do
|
||||
@injector.generate!(@members)
|
||||
|
||||
@orders_number = @injector.size
|
||||
|
||||
Kernel.puts "Publishing messages to RabbitMQ..."
|
||||
@matching_started_at = @publish_started_at = Time.now
|
||||
# TODO: Add orders publishing progress bar.
|
||||
publish_messages
|
||||
end
|
||||
|
||||
|
||||
@publish_finished_at = Time.now
|
||||
Kernel.puts "Messages are published to RabbitMQ."
|
||||
|
||||
Kernel.puts "Waiting for order processing by matching daemon..."
|
||||
wait_for_matching
|
||||
@matching_finished_at = Time.now
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
def run!
|
||||
# TODO: Check if Matching daemon is running before start (use queue_info[:consumers]).
|
||||
Kernel.puts "Creating members ..."
|
||||
if Member.count == 0
|
||||
@members = Factories.create_list(:member, @config[:traders])
|
||||
else
|
||||
@members = Member.all
|
||||
end
|
||||
|
||||
Kernel.puts "Depositing funds ..."
|
||||
@members.map(&method(:become_billionaire))
|
||||
|
||||
Kernel.puts "Generating orders by injector and saving them in db..."
|
||||
# TODO: Add orders generation progress bar.
|
||||
@injector.generate!(@members)
|
||||
|
||||
@orders_number = @injector.size
|
||||
|
||||
Kernel.puts "Publishing messages to RabbitMQ..."
|
||||
@matching_started_at = @publish_started_at = Time.now
|
||||
# TODO: Add orders publishing progress bar.
|
||||
publish_messages
|
||||
|
||||
@publish_finished_at = Time.now
|
||||
Kernel.puts "Messages are published to RabbitMQ."
|
||||
|
||||
Kernel.puts "Waiting for order processing by matching daemon..."
|
||||
wait_for_matching
|
||||
@matching_finished_at = Time.now
|
||||
end
|
||||
|
||||
def publish_messages
|
||||
Array.new(@config[:threads]) do
|
||||
Thread.new do
|
||||
loop do
|
||||
order = @injector.pop
|
||||
break unless order
|
||||
|
||||
p Kernel.puts "user number #{order.member_id} and order number is #{order.id} maker fee = #{order.maker_fee} submiited order level is #{order.member.group} and trades = #{order.member.trades.last_month.inject(0){ |sum, x| sum + x.rls }}"
|
||||
|
||||
# ::AMQP::Queue.enqueue(:matching, action: 'submit', order: order.to_matching_attributes)
|
||||
::AMQP::Queue.enqueue(:order_processor,
|
||||
{ action: 'submit', order: order.to_matching_attributes },
|
||||
{ persistent: false })
|
||||
rescue StandardError => e
|
||||
Kernel.puts e
|
||||
@errors << e
|
||||
end
|
||||
end
|
||||
end.map(&:join)
|
||||
end
|
||||
|
||||
# TODO: Find better solution for getting message number in queue.
|
||||
# E.g there is rabbitmqctl list_queues.
|
||||
def wait_for_matching
|
||||
last_log_time = Time.at(0)
|
||||
queue_status_file = File.open(queue_status_file_path('matching'), 'a')
|
||||
|
||||
loop do
|
||||
queue_status = matching_queue_status
|
||||
break if queue_status[:messages].zero? &&
|
||||
queue_status[:idle_since].present? &&
|
||||
Time.parse("#{queue_status[:idle_since]} UTC") >= @publish_started_at
|
||||
|
||||
if last_log_time + 5 < Time.now
|
||||
queue_status_file.puts(YAML.dump([queue_status.merge(timestamp: Time.now.iso8601).deep_stringify_keys]))
|
||||
last_log_time = Time.now
|
||||
end
|
||||
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Add more useful metrics to result.
|
||||
def result
|
||||
@result ||=
|
||||
begin
|
||||
publish_ops = @orders_number / (@publish_finished_at - @publish_started_at)
|
||||
matching_ops = @orders_number / (@matching_finished_at - @publish_started_at)
|
||||
|
||||
# TODO: Deal with calling iso8601(6) everywhere.
|
||||
{ config: @config,
|
||||
submit_publish: {
|
||||
started_at: @publish_started_at.iso8601(6),
|
||||
finished_at: @publish_started_at.iso8601(6),
|
||||
operations: @orders_number,
|
||||
ops: publish_ops
|
||||
},
|
||||
matching: {
|
||||
finished_at: @matching_finished_at.iso8601(6),
|
||||
operations: @orders_number,
|
||||
ops: matching_ops,
|
||||
started_at: @matching_started_at.iso8601(6)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def save_report
|
||||
report_path = Rails.root.join(@config[:report_path])
|
||||
FileUtils.mkpath(report_path)
|
||||
report_name = "#{self.class.parent.name.demodulize.downcase}-"\
|
||||
"#{self.class.name.humanize.demodulize}-#{@config[:orders][:injector]}-"\
|
||||
"#{@config[:orders][:number]}-#{@publish_started_at.iso8601}.yml"
|
||||
File.open(report_path.join(report_name), 'w') do |f|
|
||||
f.puts YAML.dump(result.deep_stringify_keys)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# TODO: Use get queue by name.
|
||||
# TODO: Use Faraday instead of RabbitMQ::HTTP::Client.
|
||||
def matching_queue_status
|
||||
@rmq_http_client.list_queues.find { |q| q[:name] == ::AMQP::Config.binding_queue(:matching).first }
|
||||
end
|
||||
|
||||
def queue_status_file_path(name)
|
||||
log_path = Rails.root.join(@config[:log_path])
|
||||
FileUtils.mkpath(log_path)
|
||||
log_path.join("#{name}-#{@publish_started_at.iso8601}.yml")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
81
lib/peatio/bench/matching/direct.rb
Normal file
81
lib/peatio/bench/matching/direct.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# TODO: Add Bench::Error and better errors processing.
|
||||
# TODO: Add Bench::Report and extract all metrics to it.
|
||||
module Bench
|
||||
module Matching
|
||||
class Direct
|
||||
include Helpers
|
||||
|
||||
def initialize(config)
|
||||
@config = config
|
||||
|
||||
@injector = Injectors.initialize_injector(@config[:orders])
|
||||
@currencies = Currency.where(id: @config[:currencies].split(',').map(&:squish).reject(&:blank?))
|
||||
@matching = Workers::AMQP::Matching.new
|
||||
# TODO: Print errors in the end of benchmark and include them into report.
|
||||
@errors = []
|
||||
end
|
||||
|
||||
def run!
|
||||
Kernel.puts "Creating members ..."
|
||||
@members = Factories.create_list(:member, @config[:traders])
|
||||
|
||||
Kernel.puts "Depositing funds ..."
|
||||
@members.map(&method(:become_billionaire))
|
||||
|
||||
Kernel.puts "Generating orders by injector and saving them in db..."
|
||||
# TODO: Add orders generation progress bar.
|
||||
@injector.generate!(@members)
|
||||
|
||||
@orders_number = @injector.size
|
||||
|
||||
@matching_started_at = Time.now
|
||||
|
||||
process_messages
|
||||
|
||||
@matching_finished_at = Time.now
|
||||
end
|
||||
|
||||
def process_messages
|
||||
loop do
|
||||
order = @injector.pop
|
||||
break unless order
|
||||
@matching.process({action: 'submit', order: order.to_matching_attributes}, 'metadata', 'delivery_info')
|
||||
rescue StandardError => e
|
||||
Kernel.puts e
|
||||
@errors << e
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Add more useful metrics to result.
|
||||
def result
|
||||
@result ||=
|
||||
begin
|
||||
matching_ops = @orders_number / (@matching_finished_at - @matching_started_at)
|
||||
|
||||
# TODO: Deal with calling iso8601(6) everywhere.
|
||||
{ config: @config,
|
||||
matching: {
|
||||
started_at: @matching_started_at.iso8601(6),
|
||||
finished_at: @matching_finished_at.iso8601(6),
|
||||
operations: @orders_number,
|
||||
ops: matching_ops
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def save_report
|
||||
report_path = Rails.root.join(@config[:report_path])
|
||||
FileUtils.mkpath(report_path)
|
||||
report_name = "#{self.class.parent.name.demodulize.downcase}-"\
|
||||
"#{self.class.name.humanize.demodulize}-#{@config[:orders][:injector]}-"\
|
||||
"#{@config[:orders][:number]}-#{@matching_started_at.iso8601}.yml"
|
||||
File.open(report_path.join(report_name), 'w') do |f|
|
||||
f.puts YAML.dump(result.deep_stringify_keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
95
lib/peatio/bench/order_processing/amqp.rb
Normal file
95
lib/peatio/bench/order_processing/amqp.rb
Normal file
@@ -0,0 +1,95 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bench
|
||||
module OrderProcessing
|
||||
class AMQP < TradeExecution::AMQP
|
||||
def run!
|
||||
# TODO: Check if OrderProcessing daemon is running before start (use queue_info[:consumers]).
|
||||
super
|
||||
Kernel.puts "Init wait orders queue..."
|
||||
@orders_for_cancel_number = init_wait_orders_queue!.size # TODO: If zero? raise Error.
|
||||
|
||||
Kernel.puts "Start wait orders publish..."
|
||||
@cancel_publish_started_at = @order_processing_started_at = Time.now
|
||||
publish_cancel_messages
|
||||
@cancel_publish_finished_at = Time.now
|
||||
|
||||
Kernel.puts "Messages are published to RabbitMQ."
|
||||
Kernel.puts "Waiting for order processing by order processor..."
|
||||
wait_for_order_processing
|
||||
@order_processing_finished_at = Time.now
|
||||
end
|
||||
|
||||
def publish_cancel_messages
|
||||
Array.new(@config[:threads]) do
|
||||
Thread.new do
|
||||
loop do
|
||||
break if @wait_orders_queue.blank?
|
||||
order = @wait_orders_queue.pop
|
||||
AMQP::Queue.enqueue(:matching, action: 'cancel', order: order.to_matching_attributes)
|
||||
rescue StandardError => e
|
||||
Kernel.puts e
|
||||
@errors << e
|
||||
end
|
||||
end
|
||||
end.map(&:join)
|
||||
end
|
||||
|
||||
def wait_for_order_processing
|
||||
last_log_time = Time.at(0)
|
||||
queue_status_file = File.open(queue_status_file_path('order-processing'), 'a')
|
||||
|
||||
loop do
|
||||
queue_status = order_processing_queue_status
|
||||
# NOTE: If no orders where cancelled idle_since would not change.
|
||||
break if queue_status[:messages].zero? &&
|
||||
queue_status[:idle_since].present? &&
|
||||
Time.parse("#{queue_status[:idle_since]} UTC") >= @order_processing_started_at
|
||||
|
||||
if last_log_time + 5 < Time.now
|
||||
queue_status_file.puts(YAML.dump([queue_status.merge(timestamp: Time.now.iso8601).deep_stringify_keys]))
|
||||
last_log_time = Time.now
|
||||
end
|
||||
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
def result
|
||||
@result ||=
|
||||
begin
|
||||
cancel_publish_ops = @orders_for_cancel_number / (@cancel_publish_finished_at - @cancel_publish_started_at)
|
||||
order_processing_ops = @orders_for_cancel_number / (@order_processing_finished_at - @order_processing_started_at)
|
||||
|
||||
super.merge(
|
||||
cancel_publish: {
|
||||
started_at: @cancel_publish_started_at.iso8601(6),
|
||||
finished_at: @cancel_publish_finished_at.iso8601(6),
|
||||
operations: @orders_for_cancel_number,
|
||||
ops: cancel_publish_ops
|
||||
},
|
||||
order_processing: {
|
||||
started_at: @order_processing_started_at.iso8601(6),
|
||||
finished_at: @order_processing_finished_at.iso8601(6),
|
||||
operations: @orders_for_cancel_number,
|
||||
ops: order_processing_ops
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def init_wait_orders_queue!
|
||||
orders = Order.where(state: Order::WAIT).shuffle
|
||||
@wait_orders_queue =
|
||||
orders.each_with_object(Queue.new) do |o, queue|
|
||||
queue << o
|
||||
end
|
||||
end
|
||||
|
||||
def order_processing_queue_status
|
||||
@rmq_http_client.list_queues.find { |q| q[:name] == AMQP::Config.binding_queue(:order_processor).first }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
82
lib/peatio/bench/order_processing/direct.rb
Normal file
82
lib/peatio/bench/order_processing/direct.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# TODO: Add Bench::Error and better errors processing.
|
||||
# TODO: Add Bench::Report and extract all metrics to it.
|
||||
module Bench
|
||||
module OrderProcessing
|
||||
class Direct
|
||||
include Helpers
|
||||
|
||||
def initialize(config)
|
||||
@config = config
|
||||
|
||||
@injector = Injectors.initialize_injector(@config[:orders])
|
||||
@currencies = Currency.where(id: @config[:currencies].split(',').map(&:squish).reject(&:blank?))
|
||||
@order_processor = Workers::AMQP::OrderProcessor.new
|
||||
# TODO: Print errors in the end of benchmark and include them into report.
|
||||
@errors = []
|
||||
end
|
||||
|
||||
def run!
|
||||
Kernel.puts "Creating members ..."
|
||||
@members = Factories.create_list(:member, @config[:traders])
|
||||
|
||||
Kernel.puts "Depositing funds ..."
|
||||
@members.map(&method(:become_billionaire))
|
||||
|
||||
Kernel.puts "Generating orders by injector and saving them in db..."
|
||||
# TODO: Add orders generation progress bar.
|
||||
@injector.generate!(@members)
|
||||
|
||||
@orders_number = @injector.size
|
||||
|
||||
@processing_started_at = Time.now
|
||||
|
||||
process_orders
|
||||
|
||||
@processing_finished_at = Time.now
|
||||
end
|
||||
|
||||
def process_orders
|
||||
loop do
|
||||
order = @injector.pop
|
||||
break unless order
|
||||
|
||||
@order_processor.process({action: 'cancel', order: order.to_matching_attributes}.deep_stringify_keys!)
|
||||
rescue StandardError => e
|
||||
Kernel.puts e
|
||||
@errors << e
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Add more useful metrics to result.
|
||||
def result
|
||||
@result ||=
|
||||
begin
|
||||
processing_ops = @orders_number / (@processing_finished_at - @processing_started_at)
|
||||
|
||||
# TODO: Deal with calling iso8601(6) everywhere.
|
||||
{ config: @config,
|
||||
order_processing: {
|
||||
started_at: @processing_started_at.iso8601(6),
|
||||
finished_at: @processing_finished_at.iso8601(6),
|
||||
operations: @orders_number,
|
||||
ops: processing_ops
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def save_report
|
||||
report_path = Rails.root.join(@config[:report_path])
|
||||
FileUtils.mkpath(report_path)
|
||||
report_name = "#{self.class.parent.name.demodulize.downcase}-"\
|
||||
"#{self.class.name.humanize.demodulize}-#{@config[:orders][:injector]}-"\
|
||||
"#{@config[:orders][:number]}-#{@processing_started_at.iso8601}.yml"
|
||||
File.open(report_path.join(report_name), 'w') do |f|
|
||||
f.puts YAML.dump(result.deep_stringify_keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
67
lib/peatio/bench/trade_execution/amqp.rb
Normal file
67
lib/peatio/bench/trade_execution/amqp.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bench
|
||||
module TradeExecution
|
||||
class AMQP < Matching::AMQP
|
||||
def run!
|
||||
# TODO: Check if TradeExecutor daemon is running before start (use queue_info[:consumers]).
|
||||
super
|
||||
Kernel.puts 'Waiting for trades processing by trade execution daemon...'
|
||||
@execution_started_at = @publish_started_at
|
||||
wait_for_execution
|
||||
@execution_finished_at = Time.now
|
||||
end
|
||||
|
||||
def run_fee!
|
||||
# TODO: Check if TradeExecutor daemon is running before start (use queue_info[:consumers]).
|
||||
super
|
||||
Kernel.puts 'Waiting for trades processing by trade execution daemon...'
|
||||
@execution_started_at = @publish_started_at
|
||||
wait_for_execution
|
||||
@execution_finished_at = Time.now
|
||||
end
|
||||
def wait_for_execution
|
||||
last_log_time = Time.at(0)
|
||||
queue_status_file = File.open(queue_status_file_path('trade-execution'), 'a')
|
||||
|
||||
loop do
|
||||
queue_status = trade_execution_queue_status
|
||||
# NOTE: If no orders where matched idle_since would not change.
|
||||
break if queue_status[:messages].zero? &&
|
||||
queue_status[:idle_since].present? &&
|
||||
Time.parse("#{queue_status[:idle_since]} UTC") >= @execution_started_at
|
||||
|
||||
if last_log_time + 5 < Time.now
|
||||
queue_status_file.puts(YAML.dump([queue_status.merge(timestamp: Time.now.iso8601).deep_stringify_keys]))
|
||||
last_log_time = Time.now
|
||||
end
|
||||
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
def result
|
||||
@result ||=
|
||||
begin
|
||||
trades_number = Trade.where('created_at >= ?', @publish_started_at).length
|
||||
trades_ops = trades_number / (@execution_finished_at - @execution_started_at)
|
||||
|
||||
super.merge(
|
||||
trade_execution: {
|
||||
started_at: @execution_started_at.iso8601(6),
|
||||
finished_at: @execution_finished_at.iso8601(6),
|
||||
operations: trades_number,
|
||||
ops: trades_ops
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trade_execution_queue_status
|
||||
@rmq_http_client.list_queues.find { |q| q[:name] == ::AMQP::Config.binding_queue(:trade_executor).first }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
85
lib/peatio/bench/trade_execution/direct.rb
Normal file
85
lib/peatio/bench/trade_execution/direct.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bench
|
||||
module TradeExecution
|
||||
class Direct
|
||||
include Helpers
|
||||
|
||||
def initialize(config)
|
||||
@config = config
|
||||
raise "This benchmark doesn't support Bitfinex injector" if config[:orders][:injector] == 'bitfinex'
|
||||
|
||||
@bid_injector = Injectors.initialize_injector(@config[:orders].merge(price: 1, side: 'OrderBid'))
|
||||
@ask_injector = Injectors.initialize_injector(@config[:orders].merge(price: 0.9, side: 'OrderAsk'))
|
||||
@currencies = Currency.where(id: @config[:currencies].split(',').map(&:squish).reject(&:blank?))
|
||||
@executor = Workers::AMQP::TradeExecutor.new
|
||||
# TODO: Print errors in the end of benchmark and include them into report.
|
||||
@errors = []
|
||||
end
|
||||
|
||||
def run!
|
||||
Kernel.puts "Creating members ..."
|
||||
@members = Factories.create_list(:member, @config[:traders])
|
||||
|
||||
Kernel.puts "Depositing funds ..."
|
||||
@members.map(&method(:become_billionaire))
|
||||
|
||||
Kernel.puts "Generating orders by injector and saving them in db..."
|
||||
|
||||
Kernel.puts 'Waiting for trades processing by trade execution daemon...'
|
||||
@bid_injector.generate!(@members)
|
||||
@ask_injector.generate!(@members)
|
||||
|
||||
@execution_started_at = Time.now
|
||||
process_messages
|
||||
@execution_finished_at = Time.now
|
||||
end
|
||||
|
||||
def process_messages
|
||||
loop do
|
||||
ask = @ask_injector.pop
|
||||
bid = @bid_injector.pop
|
||||
break unless ask && bid
|
||||
volume = ask.volume > bid.volume ? bid.volume : ask.volume
|
||||
@executor.process({ market_id: ask.market_id,
|
||||
ask_id: ask.id,
|
||||
bid_id: bid.id,
|
||||
strike_price: ask.price,
|
||||
volume: volume,
|
||||
funds: volume * ask.price })
|
||||
rescue StandardError => e
|
||||
Kernel.puts e
|
||||
@errors << e
|
||||
end
|
||||
end
|
||||
|
||||
def result
|
||||
@result ||=
|
||||
begin
|
||||
trades_number = Trade.where('created_at >= ?', @execution_started_at).length
|
||||
trades_ops = trades_number / (@execution_finished_at - @execution_started_at)
|
||||
|
||||
{ config: @config,
|
||||
trade_execution: {
|
||||
started_at: @execution_started_at.iso8601(6),
|
||||
finished_at: @execution_finished_at.iso8601(6),
|
||||
operations: trades_number,
|
||||
ops: trades_ops
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def save_report
|
||||
report_path = Rails.root.join(@config[:report_path])
|
||||
FileUtils.mkpath(report_path)
|
||||
report_name = "#{self.class.parent.name.demodulize.downcase}-"\
|
||||
"#{self.class.name.humanize.demodulize}-#{@config[:orders][:injector]}-"\
|
||||
"#{@config[:orders][:number]}-#{@execution_started_at.iso8601}.yml"
|
||||
File.open(report_path.join(report_name), 'w') do |f|
|
||||
f.puts YAML.dump(result.deep_stringify_keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
117
lib/peatio/bitcoin/blockchain.rb
Normal file
117
lib/peatio/bitcoin/blockchain.rb
Normal file
@@ -0,0 +1,117 @@
|
||||
module Bitcoin
|
||||
# TODO: Processing of unconfirmed transactions from mempool isn't supported now.
|
||||
class Blockchain < Peatio::Blockchain::Abstract
|
||||
|
||||
DEFAULT_FEATURES = {case_sensitive: true, cash_addr_format: false}.freeze
|
||||
|
||||
def initialize(custom_features = {})
|
||||
@features = DEFAULT_FEATURES.merge(custom_features).slice(*SUPPORTED_FEATURES)
|
||||
@settings = {}
|
||||
end
|
||||
|
||||
def configure(settings = {})
|
||||
# Clean client state during configure.
|
||||
@client = nil
|
||||
@settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
|
||||
end
|
||||
|
||||
def fetch_block!(block_number)
|
||||
block_hash = client.json_rpc(:getblockhash, [block_number])
|
||||
|
||||
client.json_rpc(:getblock, [block_hash, 2])
|
||||
.fetch('tx').each_with_object([]) do |tx, txs_array|
|
||||
txs = build_transaction(tx).map do |ntx|
|
||||
Peatio::Transaction.new(ntx.merge(block_number: block_number))
|
||||
end
|
||||
txs_array.append(*txs)
|
||||
end.yield_self { |txs_array| Peatio::Block.new(block_number, txs_array) }
|
||||
rescue Bitcoin::Client::Error => e
|
||||
raise Peatio::Blockchain::ClientError, e
|
||||
end
|
||||
|
||||
def latest_block_number
|
||||
client.json_rpc(:getblockcount)
|
||||
rescue Bitcoin::Client::Error => e
|
||||
raise Peatio::Blockchain::ClientError, e
|
||||
end
|
||||
|
||||
def fetch_transaction(transaction)
|
||||
transaction_hash = client.json_rpc(:getrawtransaction, [transaction.hash, 1])
|
||||
tx = nil
|
||||
transaction_hash.fetch('vout').select do |entry|
|
||||
entry.fetch('value').to_d > 0 &&
|
||||
entry['scriptPubKey'].has_key?('addresses')
|
||||
end.each do |entry|
|
||||
if transaction.to_address == entry['scriptPubKey']['addresses'][0] && entry.fetch('value').to_d == transaction.amount
|
||||
tx = { hash: transaction_hash['txid'], txout: entry['n'],
|
||||
to_address: entry['scriptPubKey']['addresses'][0],
|
||||
amount: entry.fetch('value').to_d,
|
||||
status: 'success' }
|
||||
break
|
||||
else
|
||||
next
|
||||
end
|
||||
end
|
||||
if tx.present?
|
||||
Peatio::Transaction.new(tx)
|
||||
else
|
||||
Peatio::Transaction.new
|
||||
end
|
||||
end
|
||||
|
||||
def transaction_sources(transaction)
|
||||
transaction_hash = client.json_rpc(:getrawtransaction, [transaction.hash, 1])
|
||||
transaction_hash['vin'].each_with_object([]) do |vin, source_addresses|
|
||||
next if vin['txid'].blank?
|
||||
|
||||
vin_transaction = client.json_rpc(:getrawtransaction, [vin['txid'], 1])
|
||||
source = vin_transaction['vout'].find { |hash| hash['n'] == vin['vout'] }
|
||||
source_addresses << source['scriptPubKey']['addresses'][0]
|
||||
end.compact.uniq
|
||||
end
|
||||
|
||||
def load_balance_of_address!(address, _currency_id)
|
||||
address_with_balance = client.json_rpc(:listaddressgroupings)
|
||||
.flatten(1)
|
||||
.find { |addr| addr[0] == address }
|
||||
|
||||
if address_with_balance.blank?
|
||||
raise Peatio::Blockchain::UnavailableAddressBalanceError, address
|
||||
end
|
||||
|
||||
address_with_balance[1].to_d
|
||||
rescue Bitcoin::Client::Error => e
|
||||
raise Peatio::Blockchain::ClientError, e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_transaction(tx_hash)
|
||||
tx_hash.fetch('vout')
|
||||
.select do |entry|
|
||||
entry.fetch('value').to_d > 0 &&
|
||||
entry['scriptPubKey'].has_key?('addresses')
|
||||
end
|
||||
.each_with_object([]) do |entry, formatted_txs|
|
||||
no_currency_tx =
|
||||
{ hash: tx_hash['txid'], txout: entry['n'],
|
||||
to_address: entry['scriptPubKey']['addresses'][0],
|
||||
amount: entry.fetch('value').to_d,
|
||||
status: 'success' }
|
||||
|
||||
# Build transaction for each currency belonging to blockchain.
|
||||
settings_fetch(:currencies).pluck(:id).each do |currency_id|
|
||||
formatted_txs << no_currency_tx.merge(currency_id: currency_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Bitcoin::Client.new(settings_fetch(:server))
|
||||
end
|
||||
|
||||
def settings_fetch(key)
|
||||
@settings.fetch(key) { raise Peatio::Blockchain::MissingSettingError, key.to_s }
|
||||
end
|
||||
end
|
||||
end
|
||||
49
lib/peatio/bitcoin/client.rb
Normal file
49
lib/peatio/bitcoin/client.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module Bitcoin
|
||||
class Client
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class ConnectionError < Error; end
|
||||
|
||||
class ResponseError < Error
|
||||
def initialize(code, msg)
|
||||
super "#{msg} (#{code})"
|
||||
end
|
||||
end
|
||||
|
||||
extend Memoist
|
||||
|
||||
def initialize(endpoint, idle_timeout: 5)
|
||||
@json_rpc_endpoint = URI.parse(endpoint)
|
||||
@path = @json_rpc_endpoint.path.empty? ? "/" : @json_rpc_endpoint.path
|
||||
@idle_timeout = idle_timeout
|
||||
end
|
||||
|
||||
def json_rpc(method, params = [])
|
||||
response = connection.post \
|
||||
@path,
|
||||
{ jsonrpc: '1.0', method: method, params: params }.to_json,
|
||||
{ 'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json' }
|
||||
response.assert_success!
|
||||
response = JSON.parse(response.body)
|
||||
response['error'].tap { |error| raise ResponseError.new(error['code'], error['message']) if error }
|
||||
response.fetch('result')
|
||||
rescue Faraday::Error => e
|
||||
raise ConnectionError, e
|
||||
rescue StandardError => e
|
||||
raise Error, e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def connection
|
||||
@connection ||= Faraday.new(@json_rpc_endpoint) do |f|
|
||||
f.adapter :net_http_persistent, pool_size: 5, idle_timeout: @idle_timeout
|
||||
end.tap do |connection|
|
||||
unless @json_rpc_endpoint.user.blank?
|
||||
connection.basic_auth(@json_rpc_endpoint.user, @json_rpc_endpoint.password)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
61
lib/peatio/bitcoin/wallet.rb
Normal file
61
lib/peatio/bitcoin/wallet.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
module Bitcoin
|
||||
class Wallet < Peatio::Wallet::Abstract
|
||||
|
||||
DEFAULT_FEATURES = { skip_deposit_collection: false }.freeze
|
||||
|
||||
def initialize(custom_features = {})
|
||||
@features = DEFAULT_FEATURES.merge(custom_features).slice(*SUPPORTED_FEATURES)
|
||||
@settings = {}
|
||||
end
|
||||
|
||||
def configure(settings = {})
|
||||
# Clean client state during configure.
|
||||
@client = nil
|
||||
|
||||
@settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
|
||||
|
||||
@wallet = @settings.fetch(:wallet) do
|
||||
raise Peatio::Wallet::MissingSettingError, :wallet
|
||||
end.slice(:uri, :address)
|
||||
|
||||
@currency = @settings.fetch(:currency) do
|
||||
raise Peatio::Wallet::MissingSettingError, :currency
|
||||
end.slice(:id, :base_factor, :options)
|
||||
end
|
||||
|
||||
def create_address!(_options = {})
|
||||
{ address: client.json_rpc(:getnewaddress) }
|
||||
rescue Bitcoin::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def create_transaction!(transaction, options = {})
|
||||
txid = client.json_rpc(:sendtoaddress,
|
||||
[
|
||||
transaction.to_address,
|
||||
transaction.amount,
|
||||
'',
|
||||
'',
|
||||
options[:subtract_fee].to_s == 'true' # subtract fee from transaction amount.
|
||||
])
|
||||
transaction.hash = txid
|
||||
transaction
|
||||
rescue Bitcoin::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def load_balance!
|
||||
client.json_rpc(:getbalance).to_d
|
||||
|
||||
rescue Bitcoin::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def client
|
||||
uri = @wallet.fetch(:uri) { raise Peatio::Wallet::MissingSettingError, :uri }
|
||||
@client ||= Client.new(uri, idle_timeout: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
43
lib/peatio/coin_market_cap.rb
Normal file
43
lib/peatio/coin_market_cap.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CoinMarketCap
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class << self
|
||||
def default_client
|
||||
new(default_options)
|
||||
end
|
||||
|
||||
def default_options
|
||||
{
|
||||
host: ENV.fetch('CMC_HOST','pro-api.coinmarketcap.com'),
|
||||
path: '/v1/cryptocurrency/map',
|
||||
query: {
|
||||
CMC_PRO_API_KEY: 'UNIFIED-CRYPTOASSET-INDEX',
|
||||
listing_status: 'active'
|
||||
}.to_query
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(options)
|
||||
url = ::URI::HTTPS.build(options.slice(:host, :path, :query))
|
||||
|
||||
@connection = Faraday.new(url) do |conn|
|
||||
conn.response :raise_error
|
||||
conn.adapter Faraday.default_adapter
|
||||
end
|
||||
end
|
||||
|
||||
def get(params={})
|
||||
response = @connection.get do |req|
|
||||
req.params = @connection.params.merge(params.as_json)
|
||||
end
|
||||
|
||||
JSON(response.body).deep_symbolize_keys.yield_self do |body|
|
||||
# Error code will be equal to zero if there is no error
|
||||
raise Error, body[:status][:error_message] if body[:status][:error_code].nonzero? || body[:data].blank?
|
||||
body[:data]
|
||||
end
|
||||
end
|
||||
end
|
||||
31
lib/peatio/cors/validations.rb
Normal file
31
lib/peatio/cors/validations.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides CORS variables validation.
|
||||
module CORS::Validations
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class << self
|
||||
def validate_origins(origins)
|
||||
if origins.include?('*') || origins.blank?
|
||||
Rails.logger.info{ "WARNING: API_CORS_ORIGIN is set to '*'" }
|
||||
return '*'
|
||||
end
|
||||
origins.split(',').each_with_object([]) do |origin, domains|
|
||||
if origin.match?(/https?:\/\/([a-zA-Z0-9]+)(\.[a-zA-Z0-9]+)*(:^[0-9]*$+)?/)
|
||||
domains << origin
|
||||
else
|
||||
raise CORS::Validations::Error, "Set right origin domain name instead of #{origin}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_max_age(max_age)
|
||||
if max_age.present? && max_age.match?(/^[0-9]*$/)
|
||||
max_age
|
||||
else
|
||||
Rails.logger.info{ 'WARNING: Incorect or missing API_CORS_MAX_AGE value. Using default value: 3600' }
|
||||
'3600'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
5
lib/peatio/csv_formatter.rb
Normal file
5
lib/peatio/csv_formatter.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class CSVFormatter
|
||||
def self.call(object, env)
|
||||
object.to_csv
|
||||
end
|
||||
end
|
||||
237
lib/peatio/ethereum/blockchain.rb
Normal file
237
lib/peatio/ethereum/blockchain.rb
Normal file
@@ -0,0 +1,237 @@
|
||||
module Ethereum
|
||||
class Blockchain < Peatio::Blockchain::Abstract
|
||||
|
||||
UndefinedCurrencyError = Class.new(StandardError)
|
||||
|
||||
TOKEN_EVENT_IDENTIFIER = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
|
||||
SUCCESS = '0x1'
|
||||
FAILED = '0x0'
|
||||
|
||||
DEFAULT_FEATURES = { case_sensitive: false, cash_addr_format: false }.freeze
|
||||
|
||||
def initialize(custom_features = {})
|
||||
@features = DEFAULT_FEATURES.merge(custom_features).slice(*SUPPORTED_FEATURES)
|
||||
@settings = {}
|
||||
end
|
||||
|
||||
def configure(settings = {})
|
||||
# Clean client state during configure.
|
||||
@client = nil
|
||||
@erc20 = []; @eth = []
|
||||
@whitelisted_addresses = if settings[:whitelisted_addresses].present?
|
||||
settings[:whitelisted_addresses].pluck(:address).to_set
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
@settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
|
||||
@settings[:currencies]&.each do |c|
|
||||
if c.dig(:options, :erc20_contract_address).present?
|
||||
@erc20 << c
|
||||
else
|
||||
@eth << c
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_block!(block_number)
|
||||
block_json = client.json_rpc(:eth_getBlockByNumber, ["0x#{block_number.to_s(16)}", true])
|
||||
|
||||
if block_json.blank? || block_json['transactions'].blank?
|
||||
return Peatio::Block.new(block_number, [])
|
||||
end
|
||||
block_json.fetch('transactions').each_with_object([]) do |tx, block_arr|
|
||||
if tx.fetch('input').hex <= 0
|
||||
next if invalid_eth_transaction?(tx)
|
||||
else
|
||||
next if @erc20.find do |c|
|
||||
# Check `to` and `input` options to find erc-20 smart contract contract
|
||||
c.dig(:options, :erc20_contract_address) == normalize_address(tx.fetch('to')) ||
|
||||
c.dig(:options, :erc20_contract_address) == '0x' + tx.fetch('input')[34...74].to_s ||
|
||||
# Check if `to` in whitelisted smart contracts
|
||||
@whitelisted_addresses.include?(tx.fetch('to'))
|
||||
end.blank?
|
||||
|
||||
tx = client.json_rpc(:eth_getTransactionReceipt, [normalize_txid(tx.fetch('hash'))])
|
||||
next if tx.nil? || tx.fetch('to').blank?
|
||||
end
|
||||
|
||||
txs = build_transactions(tx).map do |ntx|
|
||||
Peatio::Transaction.new(ntx)
|
||||
end
|
||||
|
||||
block_arr.append(*txs)
|
||||
end.yield_self { |block_arr| Peatio::Block.new(block_number, block_arr) }
|
||||
rescue Ethereum::Client::Error => e
|
||||
raise Peatio::Blockchain::ClientError, e
|
||||
end
|
||||
|
||||
def latest_block_number
|
||||
client.json_rpc(:eth_blockNumber).to_i(16)
|
||||
rescue Ethereum::Client::Error => e
|
||||
raise Peatio::Blockchain::ClientError, e
|
||||
end
|
||||
|
||||
def load_balance_of_address!(address, currency_id)
|
||||
currency = settings[:currencies].find { |c| c[:id] == currency_id.to_s }
|
||||
raise UndefinedCurrencyError unless currency
|
||||
|
||||
if currency.dig(:options, :erc20_contract_address).present?
|
||||
load_erc20_balance(address, currency)
|
||||
else
|
||||
client.json_rpc(:eth_getBalance, [normalize_address(address), 'latest'])
|
||||
.hex
|
||||
.to_d
|
||||
.yield_self { |amount| convert_from_base_unit(amount, currency) }
|
||||
end
|
||||
rescue Ethereum::Client::Error => e
|
||||
raise Peatio::Blockchain::ClientError, e
|
||||
end
|
||||
|
||||
def fetch_transaction(transaction)
|
||||
currency = settings[:currencies].find { |c| c.fetch(:id) == transaction.currency_id }
|
||||
return if currency.blank?
|
||||
txn_receipt = client.json_rpc(:eth_getTransactionReceipt, [transaction.hash])
|
||||
if currency.in?(@eth)
|
||||
txn_json = client.json_rpc(:eth_getTransactionByHash, [transaction.hash])
|
||||
attributes = {
|
||||
amount: convert_from_base_unit(txn_json.fetch('value').hex, currency),
|
||||
to_address: normalize_address(txn_json['to']),
|
||||
txout: txn_json.fetch('transactionIndex').to_i(16),
|
||||
status: transaction_status(txn_receipt)
|
||||
}
|
||||
else
|
||||
if transaction.txout.present?
|
||||
txn_json = txn_receipt.fetch('logs').find { |log| log['logIndex'].to_i(16) == transaction.txout }
|
||||
else
|
||||
txn_json = txn_receipt.fetch('logs').first
|
||||
end
|
||||
attributes = {
|
||||
amount: convert_from_base_unit(txn_json.fetch('data').hex, currency),
|
||||
to_address: normalize_address('0x' + txn_json.fetch('topics').last[-40..-1]),
|
||||
status: transaction_status(txn_receipt)
|
||||
}
|
||||
end
|
||||
transaction.assign_attributes(attributes)
|
||||
transaction
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_erc20_balance(address, currency)
|
||||
data = abi_encode('balanceOf(address)', normalize_address(address))
|
||||
client.json_rpc(:eth_call, [{ to: contract_address(currency), data: data }, 'latest'])
|
||||
.hex
|
||||
.to_d
|
||||
.yield_self { |amount| convert_from_base_unit(amount, currency) }
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Ethereum::Client.new(settings_fetch(:server))
|
||||
end
|
||||
|
||||
def settings_fetch(key)
|
||||
@settings.fetch(key) { raise Peatio::Blockchain::MissingSettingError, key.to_s }
|
||||
end
|
||||
|
||||
def normalize_txid(txid)
|
||||
txid.try(:downcase)
|
||||
end
|
||||
|
||||
def normalize_address(address)
|
||||
address.try(:downcase)
|
||||
end
|
||||
|
||||
def build_transactions(tx_hash)
|
||||
if tx_hash.has_key?('logs')
|
||||
build_erc20_transactions(tx_hash)
|
||||
else
|
||||
build_eth_transactions(tx_hash)
|
||||
end
|
||||
end
|
||||
|
||||
def build_eth_transactions(block_txn)
|
||||
@eth.map do |currency|
|
||||
{ hash: normalize_txid(block_txn.fetch('hash')),
|
||||
amount: convert_from_base_unit(block_txn.fetch('value').hex, currency),
|
||||
from_addresses: [normalize_address(block_txn['from'])],
|
||||
to_address: normalize_address(block_txn['to']),
|
||||
txout: block_txn.fetch('transactionIndex').to_i(16),
|
||||
block_number: block_txn.fetch('blockNumber').to_i(16),
|
||||
currency_id: currency.fetch(:id),
|
||||
status: transaction_status(block_txn) }
|
||||
end
|
||||
end
|
||||
|
||||
def build_erc20_transactions(txn_receipt)
|
||||
# Build invalid transaction for failed withdrawals
|
||||
if transaction_status(txn_receipt) == 'fail' && txn_receipt.fetch('logs').blank?
|
||||
return build_invalid_erc20_transaction(txn_receipt)
|
||||
end
|
||||
|
||||
txn_receipt.fetch('logs').each_with_object([]) do |log, formatted_txs|
|
||||
|
||||
next if log['blockHash'].blank? && log['blockNumber'].blank?
|
||||
next if log.fetch('topics').blank? || log.fetch('topics')[0] != TOKEN_EVENT_IDENTIFIER
|
||||
|
||||
# Skip if ERC20 contract address doesn't match.
|
||||
currencies = @erc20.select { |c| c.dig(:options, :erc20_contract_address) == log.fetch('address') }
|
||||
next if currencies.blank?
|
||||
|
||||
destination_address = normalize_address('0x' + log.fetch('topics').last[-40..-1])
|
||||
|
||||
currencies.each do |currency|
|
||||
formatted_txs << { hash: normalize_txid(txn_receipt.fetch('transactionHash')),
|
||||
amount: convert_from_base_unit(log.fetch('data').hex, currency),
|
||||
from_addresses: [normalize_address(txn_receipt['from'])],
|
||||
to_address: destination_address,
|
||||
txout: log['logIndex'].to_i(16),
|
||||
block_number: txn_receipt.fetch('blockNumber').to_i(16),
|
||||
currency_id: currency.fetch(:id),
|
||||
status: transaction_status(txn_receipt) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_invalid_erc20_transaction(txn_receipt)
|
||||
currencies = @erc20.select { |c| c.dig(:options, :erc20_contract_address) == txn_receipt.fetch('to') }
|
||||
return if currencies.blank?
|
||||
|
||||
currencies.each_with_object([]) do |currency, invalid_txs|
|
||||
invalid_txs << { hash: normalize_txid(txn_receipt.fetch('transactionHash')),
|
||||
block_number: txn_receipt.fetch('blockNumber').to_i(16),
|
||||
currency_id: currency.fetch(:id),
|
||||
status: transaction_status(txn_receipt) }
|
||||
end
|
||||
end
|
||||
|
||||
def transaction_status(block_txn)
|
||||
if block_txn.dig('status') == SUCCESS
|
||||
'success'
|
||||
elsif block_txn.dig('status') == FAILED
|
||||
'failed'
|
||||
else
|
||||
'pending'
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_eth_transaction?(block_txn)
|
||||
block_txn.fetch('to').blank? \
|
||||
|| block_txn.fetch('value').hex.to_d <= 0 && block_txn.fetch('input').hex <= 0
|
||||
end
|
||||
|
||||
def contract_address(currency)
|
||||
normalize_address(currency.dig(:options, :erc20_contract_address))
|
||||
end
|
||||
|
||||
def abi_encode(method, *args)
|
||||
'0x' + args.each_with_object(Digest::SHA3.hexdigest(method, 256)[0...8]) do |arg, data|
|
||||
data.concat(arg.gsub(/\A0x/, '').rjust(64, '0'))
|
||||
end
|
||||
end
|
||||
|
||||
def convert_from_base_unit(value, currency)
|
||||
value.to_d / currency.fetch(:base_factor).to_d
|
||||
end
|
||||
end
|
||||
end
|
||||
54
lib/peatio/ethereum/client.rb
Normal file
54
lib/peatio/ethereum/client.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
module Ethereum
|
||||
class Client
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class ConnectionError < Error; end
|
||||
|
||||
class ResponseError < Error
|
||||
def initialize(code, msg)
|
||||
super "#{msg} (#{code})"
|
||||
end
|
||||
end
|
||||
|
||||
extend Memoist
|
||||
|
||||
def initialize(endpoint, idle_timeout: 5)
|
||||
@json_rpc_endpoint = URI.parse(endpoint)
|
||||
@json_rpc_call_id = 0
|
||||
@path = @json_rpc_endpoint.path.empty? ? "/" : @json_rpc_endpoint.path
|
||||
@idle_timeout = idle_timeout
|
||||
end
|
||||
|
||||
def json_rpc(method, params = [])
|
||||
response = connection.post \
|
||||
@path,
|
||||
{jsonrpc: '2.0', id: rpc_call_id, method: method, params: params}.to_json,
|
||||
{'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json'}
|
||||
response.assert_success!
|
||||
response = JSON.parse(response.body)
|
||||
response['error'].tap { |error| raise ResponseError.new(error['code'], error['message']) if error }
|
||||
response.fetch('result')
|
||||
rescue Faraday::Error => e
|
||||
raise ConnectionError, e
|
||||
rescue StandardError => e
|
||||
raise Error, e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rpc_call_id
|
||||
@json_rpc_call_id += 1
|
||||
end
|
||||
|
||||
def connection
|
||||
@connection ||= Faraday.new(@json_rpc_endpoint) do |f|
|
||||
f.adapter :net_http_persistent, pool_size: 5, idle_timeout: @idle_timeout
|
||||
end.tap do |connection|
|
||||
unless @json_rpc_endpoint.user.blank?
|
||||
connection.basic_auth(@json_rpc_endpoint.user, @json_rpc_endpoint.password)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
219
lib/peatio/ethereum/wallet.rb
Normal file
219
lib/peatio/ethereum/wallet.rb
Normal file
@@ -0,0 +1,219 @@
|
||||
module Ethereum
|
||||
class Wallet < Peatio::Wallet::Abstract
|
||||
|
||||
DEFAULT_ETH_FEE = { gas_limit: 21_000, gas_price: :standard }.freeze
|
||||
|
||||
DEFAULT_ERC20_FEE = { gas_limit: 90_000, gas_price: :standard }.freeze
|
||||
|
||||
DEFAULT_FEATURES = { skip_deposit_collection: false }.freeze
|
||||
|
||||
GAS_PRICE_THRESHOLDS = { standard: 1, safelow: 0.9, fast: 1.1 }.freeze
|
||||
|
||||
def initialize(custom_features = {})
|
||||
@features = DEFAULT_FEATURES.merge(custom_features).slice(*SUPPORTED_FEATURES)
|
||||
@settings = {}
|
||||
end
|
||||
|
||||
def configure(settings = {})
|
||||
# Clean client state during configure.
|
||||
@client = nil
|
||||
|
||||
@settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
|
||||
|
||||
@wallet = @settings.fetch(:wallet) do
|
||||
raise Peatio::Wallet::MissingSettingError, :wallet
|
||||
end.slice(:uri, :address, :secret)
|
||||
|
||||
@currency = @settings.fetch(:currency) do
|
||||
raise Peatio::Wallet::MissingSettingError, :currency
|
||||
end.slice(:id, :base_factor, :options)
|
||||
end
|
||||
|
||||
def create_address!(options = {})
|
||||
secret = options.fetch(:secret) { PasswordGenerator.generate(64) }
|
||||
secret.yield_self do |password|
|
||||
{ address: normalize_address(client.json_rpc(:personal_newAccount, [password])),
|
||||
secret: password }
|
||||
end
|
||||
rescue Ethereum::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def create_transaction!(transaction, options = {})
|
||||
if @currency.dig(:options, :erc20_contract_address).present?
|
||||
create_erc20_transaction!(transaction)
|
||||
else
|
||||
create_eth_transaction!(transaction, options)
|
||||
end
|
||||
rescue Ethereum::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def prepare_deposit_collection!(transaction, deposit_spread, deposit_currency)
|
||||
# Don't prepare for deposit_collection in case of eth deposit.
|
||||
return [] if deposit_currency.dig(:options, :erc20_contract_address).blank?
|
||||
return [] if deposit_spread.blank?
|
||||
|
||||
options = DEFAULT_ERC20_FEE.merge(deposit_currency.fetch(:options).slice(:gas_limit, :gas_price))
|
||||
|
||||
# options[:gas_price] = calculate_gas_price(options)
|
||||
options[:gas_price] = transaction.currency.optioins['gas_price']
|
||||
|
||||
# We collect fees depending on the number of spread deposit size
|
||||
# Example: if deposit spreads on three wallets need to collect eth fee for 3 transactions
|
||||
fees = convert_from_base_unit(options.fetch(:gas_limit).to_i * options.fetch(:gas_price).to_i)
|
||||
transaction.amount = fees * deposit_spread.size
|
||||
transaction.options = options
|
||||
|
||||
[create_eth_transaction!(transaction)]
|
||||
rescue Ethereum::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def load_balance!
|
||||
if @currency.dig(:options, :erc20_contract_address).present?
|
||||
load_erc20_balance(@wallet.fetch(:address))
|
||||
else
|
||||
client.json_rpc(:eth_getBalance, [normalize_address(@wallet.fetch(:address)), 'latest'])
|
||||
.hex
|
||||
.to_d
|
||||
.yield_self { |amount| convert_from_base_unit(amount) }
|
||||
end
|
||||
rescue Ethereum::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_erc20_balance(address)
|
||||
data = abi_encode('balanceOf(address)', normalize_address(address))
|
||||
client.json_rpc(:eth_call, [{ to: contract_address, data: data }, 'latest'])
|
||||
.hex
|
||||
.to_d
|
||||
.yield_self { |amount| convert_from_base_unit(amount) }
|
||||
end
|
||||
|
||||
def create_eth_transaction!(transaction, options = {})
|
||||
currency_options = @currency.fetch(:options).slice(:gas_limit, :gas_price)
|
||||
options.merge!(DEFAULT_ETH_FEE, currency_options)
|
||||
|
||||
amount = convert_to_base_unit(transaction.amount)
|
||||
#TODO we should make a logic for fee calculation
|
||||
|
||||
# if transaction.options.present?
|
||||
# options[:gas_price] = transaction.options[:gas_price]
|
||||
# else
|
||||
# options[:gas_price] = calculate_gas_price(options)
|
||||
# end
|
||||
options[:gas_price] = currency_options[:gas_price]
|
||||
|
||||
# Subtract fees from initial deposit amount in case of deposit collection
|
||||
amount -= options.fetch(:gas_limit).to_i * options.fetch(:gas_price).to_i if options.dig(:subtract_fee)
|
||||
|
||||
txid = client.json_rpc(:personal_sendTransaction,
|
||||
[{
|
||||
from: normalize_address(@wallet.fetch(:address)),
|
||||
to: normalize_address(transaction.to_address),
|
||||
value: '0x' + amount.to_s(16),
|
||||
gas: '0x' + options.fetch(:gas_limit).to_i.to_s(16),
|
||||
gasPrice: '0x' + options.fetch(:gas_price).to_i.to_s(16)
|
||||
}.compact, @wallet.fetch(:secret)])
|
||||
|
||||
unless valid_txid?(normalize_txid(txid))
|
||||
raise Ethereum::Client::Error, \
|
||||
"Withdrawal from #{@wallet.fetch(:address)} to #{transaction.to_address} failed."
|
||||
end
|
||||
# Make sure that we return currency_id
|
||||
transaction.currency_id = 'eth' if transaction.currency_id.blank?
|
||||
transaction.amount = convert_from_base_unit(amount)
|
||||
transaction.hash = normalize_txid(txid)
|
||||
transaction.options = options
|
||||
transaction
|
||||
end
|
||||
|
||||
def create_erc20_transaction!(transaction, options = {})
|
||||
currency_options = @currency.fetch(:options).slice(:gas_limit, :gas_price, :erc20_contract_address)
|
||||
options.merge!(DEFAULT_ERC20_FEE, currency_options)
|
||||
|
||||
amount = convert_to_base_unit(transaction.amount)
|
||||
data = abi_encode('transfer(address,uint256)',
|
||||
normalize_address(transaction.to_address),
|
||||
'0x' + amount.to_s(16))
|
||||
#TODO we should make a logic for fee calculation
|
||||
|
||||
# if transaction.options.present?
|
||||
# options[:gas_price] = transaction.options[:gas_price]
|
||||
# else
|
||||
# options[:gas_price] = calculate_gas_price(options)
|
||||
# end
|
||||
options[:gas_price] = currency_options[:gas_price]
|
||||
|
||||
txid = client.json_rpc(:personal_sendTransaction,
|
||||
[{
|
||||
from: normalize_address(@wallet.fetch(:address)),
|
||||
to: options.fetch(:erc20_contract_address),
|
||||
data: data,
|
||||
gas: '0x' + options.fetch(:gas_limit).to_i.to_s(16),
|
||||
gasPrice: '0x' + options.fetch(:gas_price).to_i.to_s(16)
|
||||
}.compact, @wallet.fetch(:secret)])
|
||||
|
||||
unless valid_txid?(normalize_txid(txid))
|
||||
raise Ethereum::Client::Error, \
|
||||
"Withdrawal from #{@wallet.fetch(:address)} to #{transaction.to_address} failed."
|
||||
end
|
||||
transaction.hash = normalize_txid(txid)
|
||||
transaction.options = options
|
||||
transaction
|
||||
end
|
||||
|
||||
def normalize_address(address)
|
||||
address.downcase
|
||||
end
|
||||
|
||||
def normalize_txid(txid)
|
||||
txid.downcase
|
||||
end
|
||||
|
||||
def contract_address
|
||||
normalize_address(@currency.dig(:options, :erc20_contract_address))
|
||||
end
|
||||
|
||||
def valid_txid?(txid)
|
||||
txid.to_s.match?(/\A0x[A-F0-9]{64}\z/i)
|
||||
end
|
||||
|
||||
def abi_encode(method, *args)
|
||||
'0x' + args.each_with_object(Digest::SHA3.hexdigest(method, 256)[0...8]) do |arg, data|
|
||||
data.concat(arg.gsub(/\A0x/, '').rjust(64, '0'))
|
||||
end
|
||||
end
|
||||
|
||||
def convert_from_base_unit(value)
|
||||
value.to_d / @currency.fetch(:base_factor)
|
||||
end
|
||||
|
||||
def convert_to_base_unit(value)
|
||||
x = value.to_d * @currency.fetch(:base_factor)
|
||||
unless (x % 1).zero?
|
||||
raise Peatio::Wallet::ClientError,
|
||||
"Failed to convert value to base (smallest) unit because it exceeds the maximum precision: " \
|
||||
"#{value.to_d} - #{x.to_d} must be equal to zero."
|
||||
end
|
||||
x.to_i
|
||||
end
|
||||
|
||||
def calculate_gas_price(options = { gas_price: :standard })
|
||||
# Get current gas price
|
||||
gas_price = client.json_rpc(:eth_gasPrice, [])
|
||||
Rails.logger.info { "Current gas price #{gas_price.to_i(16)}" }
|
||||
|
||||
# Apply thresholds depending on currency configs by default it will be standard
|
||||
(gas_price.to_i(16) * GAS_PRICE_THRESHOLDS.fetch(options[:gas_price].try(:to_sym), 1)).to_i
|
||||
end
|
||||
|
||||
def client
|
||||
uri = @wallet.fetch(:uri) { raise Peatio::Wallet::MissingSettingError, :uri }
|
||||
@client ||= Client.new(uri, idle_timeout: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
47
lib/peatio/export.rb
Normal file
47
lib/peatio/export.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Peatio
|
||||
class Export
|
||||
def initialize; end
|
||||
|
||||
def export(model_name)
|
||||
model_name.constantize.all.map do |m|
|
||||
m.attributes.except('settings_encrypted', 'data_encrypted', 'created_at',
|
||||
'updated_at', 'key', 'secret')
|
||||
.merge('settings' => m.try(:settings),
|
||||
'data' => m.try(:data),
|
||||
'key' => m.try(:key),
|
||||
'secret' => m.try(:secret),
|
||||
'currency_ids' => m.try(:currency_ids))
|
||||
end.map { |r| r.transform_values! { |v| v.is_a?(BigDecimal) ? v.to_f : v } }.map(&:compact)
|
||||
end
|
||||
|
||||
def export_accounts
|
||||
export('Operations::Account').map { |a| a.except('id') }
|
||||
end
|
||||
|
||||
def export_blockchains
|
||||
export('Blockchain').map { |b| b.except('id', 'currency_ids') }
|
||||
end
|
||||
|
||||
def export_currencies
|
||||
export('Currency').map { |c| c['options'] = c['options'].to_h; c }
|
||||
end
|
||||
|
||||
def export_markets
|
||||
export('Market').map { |m| m['engine_name'] = Engine.find(m['engine_id']).name; m.except('engine_id') }
|
||||
end
|
||||
|
||||
def export_wallets
|
||||
export('Wallet').map { |w| w.except('id') }
|
||||
end
|
||||
|
||||
def export_trading_fees
|
||||
export('TradingFee').map { |t| t.except('id') }
|
||||
end
|
||||
|
||||
def export_engines
|
||||
export('Engine').map { |e| e.except('id') }
|
||||
end
|
||||
end
|
||||
end
|
||||
3
lib/peatio/gnosis/client.rb
Normal file
3
lib/peatio/gnosis/client.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
module Gnosis
|
||||
class Client < Ethereum::Client; end
|
||||
end
|
||||
15
lib/peatio/gnosis/wallet.rb
Normal file
15
lib/peatio/gnosis/wallet.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
module Gnosis
|
||||
class Wallet < Ethereum::Wallet
|
||||
def create_address!(_options = {})
|
||||
method_not_implemented
|
||||
end
|
||||
|
||||
def create_transaction!(_transaction, _options = {})
|
||||
method_not_implemented
|
||||
end
|
||||
|
||||
def prepare_deposit_collection!(_transaction, _deposit_spread, _deposit_currency)
|
||||
method_not_implemented
|
||||
end
|
||||
end
|
||||
end
|
||||
169
lib/peatio/import.rb
Normal file
169
lib/peatio/import.rb
Normal file
@@ -0,0 +1,169 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Peatio
|
||||
class Import
|
||||
attr_accessor :data
|
||||
|
||||
def initialize(data)
|
||||
@data = data
|
||||
end
|
||||
|
||||
def load_all
|
||||
load_accounts
|
||||
load_blockchains
|
||||
load_currencies
|
||||
load_wallets
|
||||
load_engines
|
||||
load_markets
|
||||
load_trading_fees
|
||||
load_whitelisted_smart_contracts
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
return unless @data.include? 'accounts'
|
||||
|
||||
Kernel.puts 'Importing accounts'
|
||||
::Operations::Account.transaction do
|
||||
@data['accounts'].each do |hash|
|
||||
next if ::Operations::Account.exists?(code: hash.fetch('code'))
|
||||
|
||||
::Operations::Account.create!(hash)
|
||||
Kernel.puts "Created #{hash.fetch('code')} account"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_blockchains
|
||||
return unless @data.include? 'blockchains'
|
||||
|
||||
Kernel.puts 'Importing blockchains'
|
||||
::Blockchain.transaction do
|
||||
@data['blockchains'].each do |hash|
|
||||
next if ::Blockchain.exists?(key: hash.fetch('key'))
|
||||
|
||||
::Blockchain.create!(hash)
|
||||
Kernel.puts "Created #{hash.fetch('key')} blockchain"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_currencies
|
||||
return unless @data.include? 'currencies'
|
||||
|
||||
Kernel.puts 'Importing currencies'
|
||||
::Currency.transaction do
|
||||
@data['currencies'].each do |hash|
|
||||
next if ::Currency.exists?(id: hash.fetch('id'))
|
||||
|
||||
::Currency.create!(hash)
|
||||
Kernel.puts "Created #{hash.fetch('id')} currency"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_wallets
|
||||
return unless @data.include? 'wallets'
|
||||
|
||||
Kernel.puts 'Importing wallets'
|
||||
::Wallet.transaction do
|
||||
@data['wallets'].each do |hash|
|
||||
next if ::Wallet.exists?(name: hash.fetch('name'))
|
||||
|
||||
if hash['currency_ids'].is_a?(String)
|
||||
hash['currency_ids'] = hash['currency_ids'].split(',')
|
||||
end
|
||||
|
||||
::Wallet.create!(hash)
|
||||
|
||||
Kernel.puts "Created #{hash.fetch('name')} wallet"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_whitelisted_smart_contracts
|
||||
return unless @data.include? 'whitelisted_smart_contracts'
|
||||
|
||||
Kernel.puts 'Importing whitelisted_smart_contracts'
|
||||
::Wallet.transaction do
|
||||
@data['whitelisted_smart_contracts'].each do |hash|
|
||||
next if ::WhitelistedSmartContract.exists?(address: hash.fetch('address'), blockchain_key: hash.fetch('blockchain_key'))
|
||||
|
||||
::WhitelistedSmartContract.create!(hash)
|
||||
|
||||
Kernel.puts "Created #{hash.fetch('address')}, #{hash.fetch('blockchain_key')} whitelisted_smart_contracts"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_trading_fees
|
||||
return unless @data.include? 'trading_fees'
|
||||
|
||||
Kernel.puts 'Importing trading_fees'
|
||||
::TradingFee.transaction do
|
||||
@data['trading_fees'].each do |hash|
|
||||
next if ::TradingFee.exists?(market_id: hash.fetch('market_id'), group: hash.fetch('group'))
|
||||
|
||||
::TradingFee.create!(hash)
|
||||
Kernel.puts "Created [#{hash.fetch('market_id')}, #{hash.fetch('group')}] trading fees"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_engines
|
||||
return unless @data.include? 'engines'
|
||||
|
||||
Kernel.puts 'Importing engines'
|
||||
::Engine.transaction do
|
||||
@data['engines'].each do |hash|
|
||||
next if ::Engine.exists?(name: hash.fetch('name'))
|
||||
|
||||
::Engine.create!(hash)
|
||||
Kernel.puts "Created #{hash.fetch('name')} engine"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_markets
|
||||
return unless @data.include? 'markets'
|
||||
|
||||
Kernel.puts 'Importing markets'
|
||||
::Market.transaction do
|
||||
@data['markets'].map(&:symbolize_keys).each do |hash|
|
||||
next if ::Market.exists?(id: hash.fetch(:id))
|
||||
|
||||
# For compatibility with old markets.yml
|
||||
# If state is not defined set it from enabled.
|
||||
enabled = hash.delete(:enabled)
|
||||
hash[:state] ||= enabled ? :enabled : :disabled
|
||||
|
||||
# For compatibility with old markets.yml we keep legacy-new keys mapping.
|
||||
# New key value has higher priority than legacy.
|
||||
legacy_keys_mappings = { ask_unit: :base_unit,
|
||||
bid_unit: :quote_unit,
|
||||
ask_precision: :amount_precision,
|
||||
bid_precision: :price_precision,
|
||||
min_ask_price: :min_price,
|
||||
max_bid_price: :max_price,
|
||||
min_ask_amount: :min_amount,
|
||||
min_bid_amount: :min_amount }
|
||||
|
||||
legacy_keys_mappings.each do |old_key, new_key|
|
||||
legacy_key_value = hash.delete(old_key)
|
||||
hash[new_key] ||= legacy_key_value
|
||||
end
|
||||
|
||||
# Select engine with provided name
|
||||
engine = ::Engine.find_by(name: hash[:engine_name])
|
||||
if engine.present?
|
||||
hash.delete :engine_name
|
||||
hash[:engine_id] = engine.id
|
||||
::Market.create!(hash)
|
||||
else
|
||||
Kernel.puts "Engine #{hash[:engine_name]} doesn't exist"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
lib/peatio/influxdb.rb
Normal file
31
lib/peatio/influxdb.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Peatio
|
||||
module InfluxDB
|
||||
class << self
|
||||
def client(opts={})
|
||||
# Map InfluxDB clients with received opts.
|
||||
clients[opts] ||= ::InfluxDB::Client.new(parse(config.merge(opts)))
|
||||
end
|
||||
|
||||
def config
|
||||
yaml = ::Pathname.new("config/influxdb.yml")
|
||||
return {} unless yaml.exist?
|
||||
|
||||
erb = ::ERB.new(yaml.read)
|
||||
::SafeYAML.load(erb.result)[ENV.fetch('RAILS_ENV', 'development')].symbolize_keys || {}
|
||||
end
|
||||
|
||||
def clients
|
||||
@clients ||= {}
|
||||
end
|
||||
|
||||
def parse(configs)
|
||||
hosts = configs[:host]
|
||||
configs[:host] = hosts[Zlib::crc32(configs[:keyshard].to_s) % hosts.count]
|
||||
configs
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
14
lib/peatio/json_log_formatter.rb
Normal file
14
lib/peatio/json_log_formatter.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class JSONLogFormatter < ::Logger::Formatter
|
||||
def call(severity, time, _progname, msg)
|
||||
begin
|
||||
obj = JSON.parse msg
|
||||
rescue StandardError
|
||||
obj = msg
|
||||
end
|
||||
if obj.is_a? Hash
|
||||
JSON.dump(obj.merge({ level: severity, time: time })) + "\n"
|
||||
else
|
||||
JSON.dump(level: severity, time: time, message: msg) + "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
21
lib/peatio/kline_db.rb
Normal file
21
lib/peatio/kline_db.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module KlineDB
|
||||
class << self
|
||||
|
||||
def redis
|
||||
@redis ||= Redis.new(
|
||||
url: ENV["REDIS_URL"],
|
||||
db: 1
|
||||
)
|
||||
end
|
||||
|
||||
def kline(market, period)
|
||||
key = "peatio:#{market}:k:#{period}"
|
||||
length = redis.llen(key)
|
||||
data = redis.lrange(key, length - 5000, -1).map{|str| JSON.parse(str)}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
55
lib/peatio/opendax_cloud/client.rb
Normal file
55
lib/peatio/opendax_cloud/client.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
module OpendaxCloud
|
||||
class Client
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class ConnectionError < Error; end
|
||||
class MissingEnvError < Error; end
|
||||
|
||||
def initialize(endpoint, idle_timeout: 5)
|
||||
@platform_id = ENV.fetch('PLATFORM_ID') do
|
||||
raise MissingEnvError, :platform_id
|
||||
end
|
||||
|
||||
@endpoint = URI.parse(endpoint)
|
||||
@private_key = OpenSSL::PKey.read(Base64.urlsafe_decode64(ENV.fetch('PEATIO_JWT_PRIVATE_KEY')))
|
||||
@path = @endpoint.path.empty? ? "/" : @endpoint.path
|
||||
@idle_timeout = idle_timeout
|
||||
end
|
||||
|
||||
def rest_api(verb, path, data = nil)
|
||||
args = [@endpoint.to_s + path]
|
||||
jwt = JWT.encode(data, @private_key, 'RS256')
|
||||
|
||||
headers = { 'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' + jwt,
|
||||
'Accept' => 'application/json',
|
||||
'PlatformID' => @platform_id }
|
||||
|
||||
if data.present?
|
||||
if %i[post put patch].include?(verb)
|
||||
args << data.compact.to_json << headers
|
||||
else
|
||||
args << data.compact << headers
|
||||
end
|
||||
else
|
||||
args << data << headers
|
||||
end
|
||||
|
||||
response = connection.send(verb, *args)
|
||||
response.assert_success!
|
||||
JSON.parse(response.body)
|
||||
rescue Faraday::Error => e
|
||||
raise ConnectionError, e
|
||||
rescue StandardError => e
|
||||
raise Error, e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def connection
|
||||
@connection ||= Faraday.new(@endpoint) do |f|
|
||||
f.adapter :net_http_persistent, pool_size: 5, idle_timeout: @idle_timeout
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
67
lib/peatio/opendax_cloud/wallet.rb
Normal file
67
lib/peatio/opendax_cloud/wallet.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
module OpendaxCloud
|
||||
class Wallet < Peatio::Wallet::Abstract
|
||||
Error = Class.new(StandardError)
|
||||
DEFAULT_FEATURES = { skip_deposit_collection: true }.freeze
|
||||
|
||||
def initialize(custom_features = {})
|
||||
@features = DEFAULT_FEATURES.merge(custom_features).slice(*SUPPORTED_FEATURES)
|
||||
@settings = {}
|
||||
end
|
||||
|
||||
def configure(settings = {})
|
||||
# Clean client state during configure.
|
||||
@client = nil
|
||||
|
||||
@settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
|
||||
|
||||
@wallet = @settings.fetch(:wallet) do
|
||||
raise Peatio::Wallet::MissingSettingError, :wallet
|
||||
end.slice(:uri, :address)
|
||||
|
||||
@currency = @settings.fetch(:currency) do
|
||||
raise Peatio::Wallet::MissingSettingError, :currency
|
||||
end.slice(:id, :base_factor, :options)
|
||||
end
|
||||
|
||||
def create_address!(_options = {})
|
||||
response = client.rest_api(:post, '/address/new', {
|
||||
currency_id: currency_id
|
||||
})
|
||||
|
||||
{ address: response['address'], details: response.except('address') }
|
||||
rescue OpendaxCloud::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def create_transaction!(transaction)
|
||||
response = client.rest_api(:post, '/tx/send', {
|
||||
currency_id: currency_id,
|
||||
to: transaction.to_address,
|
||||
amount: transaction.amount,
|
||||
options: transaction.options
|
||||
})
|
||||
transaction.options = response['options']
|
||||
transaction
|
||||
rescue OpendaxCloud::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def load_balance!
|
||||
response = client.rest_api(:post, '/address/balance', {
|
||||
currency_id: currency_id
|
||||
}.compact).fetch('balance')
|
||||
|
||||
response.to_d
|
||||
rescue OpendaxCloud::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def currency_id
|
||||
@currency.fetch(:id)
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Client.new(@wallet.fetch(:uri), idle_timeout: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
58
lib/peatio/owhdwallet/client.rb
Normal file
58
lib/peatio/owhdwallet/client.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
module OWHDWallet
|
||||
class Client
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class ConnectionError < Error; end
|
||||
|
||||
def initialize(endpoint, idle_timeout: 5)
|
||||
@endpoint = URI.parse(endpoint)
|
||||
@private_key = OpenSSL::PKey.read(Base64.urlsafe_decode64(ENV.fetch('PEATIO_JWT_PRIVATE_KEY')))
|
||||
@path = @endpoint.path.empty? ? "/" : @endpoint.path
|
||||
@idle_timeout = idle_timeout
|
||||
end
|
||||
|
||||
def rest_api(verb, path, data = nil)
|
||||
args = [@endpoint.to_s + path]
|
||||
jwt = JWT.encode({}, @private_key, 'RS256')
|
||||
|
||||
if data
|
||||
if %i[post put patch].include?(verb)
|
||||
args << data.compact.to_json
|
||||
args << { 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' + jwt }
|
||||
else
|
||||
args << data.compact
|
||||
args << {}
|
||||
end
|
||||
else
|
||||
args << nil
|
||||
args << {}
|
||||
end
|
||||
|
||||
args.last['Accept'] = 'application/json'
|
||||
|
||||
response = connection.send(verb, *args)
|
||||
response.assert_success!
|
||||
response = JSON.parse(response.body)
|
||||
rescue Faraday::Error => e
|
||||
raise ConnectionError, e
|
||||
rescue StandardError => e
|
||||
raise Error, e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def connection
|
||||
ca_file_path = ENV.fetch('HDWALLET_SSL_CERT_PATH', '')
|
||||
ssl = if ca_file_path.present?
|
||||
{ ca_file: ca_file_path }
|
||||
else
|
||||
Rails.logger.warn { "Peer verification turned off. SSL connection { verify: false }" }
|
||||
{ verify: false }
|
||||
end
|
||||
|
||||
@connection ||= Faraday.new(@endpoint, { ssl: ssl }) do |f|
|
||||
f.adapter :net_http_persistent, pool_size: 5, idle_timeout: @idle_timeout
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
185
lib/peatio/owhdwallet/wallet.rb
Normal file
185
lib/peatio/owhdwallet/wallet.rb
Normal file
@@ -0,0 +1,185 @@
|
||||
module OWHDWallet
|
||||
class Wallet < Peatio::Wallet::Abstract
|
||||
DEFAULT_FEATURES = { skip_deposit_collection: false }.freeze
|
||||
DEFAULT_ERC20_FEE = { eth_gas_limit: 21_000, erc20_gas_limit: 90_000, gas_price: :standard }.freeze
|
||||
GAS_PRICE_THRESHOLDS = %w[standard safelow fast].freeze
|
||||
|
||||
def initialize(custom_features = {})
|
||||
@features = DEFAULT_FEATURES.merge(custom_features).slice(*SUPPORTED_FEATURES)
|
||||
@settings = {}
|
||||
end
|
||||
|
||||
def configure(settings = {})
|
||||
# Clean client state during configure.
|
||||
@client = nil
|
||||
|
||||
@settings.merge!(settings.slice(*SUPPORTED_SETTINGS))
|
||||
|
||||
@wallet = @settings.fetch(:wallet) do
|
||||
raise Peatio::Wallet::MissingSettingError, :wallet
|
||||
end.slice(:uri, :gateway_url, :address, :secret, :wallet_index)
|
||||
|
||||
@currency = @settings.fetch(:currency) do
|
||||
raise Peatio::Wallet::MissingSettingError, :currency
|
||||
end.slice(:id, :base_factor, :options)
|
||||
end
|
||||
|
||||
def create_address!(options = {})
|
||||
# TODO: To define coin type for btc-testnet, btc-mainnet
|
||||
response = client.rest_api(:post, '/wallet/new', {
|
||||
coin_type: coin_type
|
||||
})
|
||||
|
||||
{ address: response['address'], secret: response['passphrase'], details: response.except('address', 'passphrase') }
|
||||
rescue OWHDWallet::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def create_transaction!(transaction, options = {})
|
||||
eth_params = coin_type == 'eth' ? eth_transaction(transaction, options) : {}
|
||||
|
||||
amount = convert_to_base_unit(transaction.amount)
|
||||
response = client.rest_api(:post, '/tx/send', {
|
||||
coin_type: coin_type,
|
||||
to: transaction.to_address,
|
||||
amount: amount.to_i,
|
||||
gateway_url: wallet_gateway_url,
|
||||
wallet_index: wallet_index,
|
||||
passphrase: wallet_secret
|
||||
}.merge(eth_params))
|
||||
|
||||
transaction.hash = response['tx']
|
||||
transaction.options = response['options'] if response['options'].present?
|
||||
transaction
|
||||
rescue OWHDWallet::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
# Only ERC-20 transaction
|
||||
def prepare_deposit_collection!(transaction, deposit_spread, deposit_currency)
|
||||
# Don't prepare for deposit_collection in case of eth deposit.
|
||||
return [] if deposit_currency.dig(:options, :erc20_contract_address).blank?
|
||||
return [] if deposit_spread.blank?
|
||||
|
||||
options = DEFAULT_ERC20_FEE.merge(deposit_currency.fetch(:options).slice(:gas_limit, :gas_price))
|
||||
gas_speed = options[:gas_price].in?(GAS_PRICE_THRESHOLDS) ? options[:gas_price] : 'standard'
|
||||
gas_limit = options[:gas_limit].present? ? options[:gas_limit].to_i : options[:erc20_gas_limit]
|
||||
|
||||
response = client.rest_api(:post, '/tx/before_collect', {
|
||||
coin_type: coin_type,
|
||||
gas_limit: gas_limit,
|
||||
gas_speed: gas_speed,
|
||||
spread_size: deposit_spread.size,
|
||||
to: transaction.to_address,
|
||||
gateway_url: wallet_gateway_url,
|
||||
wallet_index: wallet_index,
|
||||
passphrase: wallet_secret
|
||||
})
|
||||
|
||||
transaction.currency_id = 'eth' if transaction.currency_id.blank?
|
||||
transaction.hash = response['tx']
|
||||
transaction.options = {}
|
||||
transaction.options[:gas_limit] = gas_limit
|
||||
transaction.options[:gas_price] = response['gas_price']
|
||||
[transaction]
|
||||
rescue OWHDWallet::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
def load_balance!
|
||||
response = client.rest_api(:post, '/wallet/balance', {
|
||||
coin_type: coin_type,
|
||||
gateway_url: wallet_gateway_url,
|
||||
address: wallet_address,
|
||||
contract_address: erc20_contract_address
|
||||
}.compact).fetch('balance')
|
||||
|
||||
if coin_type == 'eth'
|
||||
response = response.yield_self { |amount| convert_from_base_unit(amount) }
|
||||
end
|
||||
|
||||
response.to_d
|
||||
rescue OWHDWallet::Client::Error => e
|
||||
raise Peatio::Wallet::ClientError, e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def eth_transaction(transaction, options)
|
||||
currency_options = @currency.fetch(:options).slice(:gas_limit, :gas_price)
|
||||
options.merge!(currency_options, DEFAULT_ERC20_FEE)
|
||||
|
||||
if transaction.options.present?
|
||||
gas_price = transaction.options[:gas_price]
|
||||
else
|
||||
gas_speed = options[:gas_price].in?(GAS_PRICE_THRESHOLDS) ? options[:gas_price] : 'standard'
|
||||
end
|
||||
|
||||
params = {
|
||||
gas_price: gas_price,
|
||||
gas_speed: gas_speed,
|
||||
subtract_fee: options.dig(:subtract_fee).present?
|
||||
}.compact!
|
||||
|
||||
if erc20_contract_address.present?
|
||||
gas_limit = options[:gas_limit].present? ? options[:gas_limit].to_i : options[:erc20_gas_limit]
|
||||
|
||||
params.merge!(contract_address: erc20_contract_address, gas_limit: gas_limit)
|
||||
else
|
||||
params[:gas_limit] = options[:gas_limit].present? ? options[:gas_limit].to_i : options[:eth_gas_limit]
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def coin_type
|
||||
if erc20_contract_address.present?
|
||||
'eth'
|
||||
else
|
||||
currency_id
|
||||
end
|
||||
end
|
||||
|
||||
def convert_to_base_unit(value)
|
||||
x = value.to_d * @currency.fetch(:base_factor)
|
||||
unless (x % 1).zero?
|
||||
raise Peatio::Wallet::ClientError,
|
||||
"Failed to convert value to base (smallest) unit because it exceeds the maximum precision: " \
|
||||
"#{value.to_d} - #{x.to_d} must be equal to zero."
|
||||
end
|
||||
x.to_i
|
||||
end
|
||||
|
||||
def convert_from_base_unit(value)
|
||||
value.to_d / @currency.fetch(:base_factor)
|
||||
end
|
||||
|
||||
def wallet_secret
|
||||
@wallet.fetch(:secret)
|
||||
end
|
||||
|
||||
def wallet_index
|
||||
@wallet.fetch(:wallet_index)
|
||||
end
|
||||
|
||||
def wallet_gateway_url
|
||||
@wallet.fetch(:gateway_url)
|
||||
end
|
||||
|
||||
def wallet_address
|
||||
@wallet.fetch(:address)
|
||||
end
|
||||
|
||||
def currency_id
|
||||
@currency.fetch(:id)
|
||||
end
|
||||
|
||||
def erc20_contract_address
|
||||
@currency.dig(:options, :erc20_contract_address)
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Client.new(@wallet.fetch(:uri), idle_timeout: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
21
lib/peatio/password_generator.rb
Normal file
21
lib/peatio/password_generator.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
#= PasswordGenerator
|
||||
#
|
||||
#== Usage
|
||||
#
|
||||
# >> PasswordGenerator.generate
|
||||
# => "2pN@cxj+zs!SVogtPZ&u"
|
||||
#
|
||||
# >> PasswordGenerator.generate(40)
|
||||
# => "B5xPy2unMKjRchfS($7v)q4N%oF*lGz@+OJ6LbVD"
|
||||
module PasswordGenerator
|
||||
CHARS = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a + %w{! @ # $ % & / ( ) + ? *}
|
||||
|
||||
class << self
|
||||
def generate(length = 20)
|
||||
CHARS.sort_by { rand }.join[0...length]
|
||||
end
|
||||
end
|
||||
end
|
||||
30
lib/peatio/rabbit_mq_http.rb
Normal file
30
lib/peatio/rabbit_mq_http.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
class RabbitMQHTTP
|
||||
class << self
|
||||
def default_client
|
||||
new(default_options)
|
||||
end
|
||||
|
||||
def default_options
|
||||
{ scheme: :http,
|
||||
host: ENV.fetch('RABBITMQ_HOST', 'localhost'),
|
||||
port: 15672,
|
||||
path: '/api',
|
||||
user: ENV.fetch('RABBITMQ_USER', 'guest'),
|
||||
password: ENV.fetch('RABBITMQ_PASSWORD', 'guest') }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(options)
|
||||
url = ::URI::HTTP.build(options.slice(:scheme, :host, :port, :path))
|
||||
|
||||
@connection = Faraday.new(url) do |conn|
|
||||
conn.basic_auth options.fetch(:user), options.fetch(:password)
|
||||
conn.adapter Faraday.default_adapter
|
||||
end
|
||||
end
|
||||
|
||||
def list_queues
|
||||
response = @connection.get('queues')
|
||||
JSON.parse(response.body).map(&:symbolize_keys)
|
||||
end
|
||||
end
|
||||
33
lib/peatio/tagged_logger.rb
Normal file
33
lib/peatio/tagged_logger.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# Examples:
|
||||
# logger = TaggedLogger(Rails.logger, app: 'peatio')
|
||||
# logger.info 'order processed'
|
||||
# # I, [2019-07-04T18:56:02.977542 #7987] INFO -- : {:app=>"peatio", :message=>"order processed"}
|
||||
|
||||
# with json format
|
||||
# logger = TaggedLogger.new(Rails.logger, app: 'peatio')
|
||||
# logger_extended = TaggedLogger.new(logger, version: '2.2', branch: 'master')
|
||||
#
|
||||
# logger.info 'order processed'
|
||||
# # {"app":"peatio","message":"order processed","level":"INFO","time":"2019-07-04 18:59:01"}
|
||||
#
|
||||
# logger_extended.info 'order processed'
|
||||
# # {"app":"peatio","version":"2.2","branch":"master","message":"order processed","level":"INFO","time":"2019-07-04 18:59:09"}
|
||||
|
||||
class TaggedLogger
|
||||
def initialize(logger, tags)
|
||||
@logger = logger.dup
|
||||
@tags = tags
|
||||
end
|
||||
|
||||
[:fatal, :error, :warn, :info, :debug].each do |log_method|
|
||||
define_method(log_method) do |msg|
|
||||
if msg.is_a? Hash
|
||||
msg = @tags.merge msg
|
||||
else
|
||||
msg = @tags.merge(message: msg)
|
||||
end
|
||||
|
||||
@logger.method(log_method).call(msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
42
lib/peatio/upstream/opendax.rb
Normal file
42
lib/peatio/upstream/opendax.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Peatio
|
||||
module Upstream
|
||||
class Opendax < Peatio::Upstream::Base
|
||||
def initialize(config)
|
||||
super
|
||||
@connection = Faraday.new(url: "#{config['rest']}") do |builder|
|
||||
builder.response :json
|
||||
builder.response :logger if config["debug"]
|
||||
builder.adapter(@adapter)
|
||||
builder.ssl[:verify] = config["verify_ssl"] unless config["verify_ssl"].nil?
|
||||
end
|
||||
@rest = "#{config['rest']}"
|
||||
@ws_url = "#{config['websocket']}/public"
|
||||
end
|
||||
|
||||
def ws_read_public_message(msg)
|
||||
if msg.keys.first.split('.').second == 'trades'
|
||||
detect_trade(msg)
|
||||
end
|
||||
end
|
||||
|
||||
def detect_trade(msg)
|
||||
msg.values.first['trades'].each do |trade|
|
||||
notify_public_trade(trade)
|
||||
end
|
||||
end
|
||||
|
||||
def subscribe_trades(market, ws)
|
||||
sub = {
|
||||
event: 'subscribe',
|
||||
streams: ["#{market}.trades"]
|
||||
}
|
||||
Rails.logger.info 'Open event' + sub.to_s
|
||||
EM.next_tick do
|
||||
ws.send(JSON.generate(sub))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
lib/peatio/uuid.rb
Normal file
31
lib/peatio/uuid.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
class UUID
|
||||
class << self
|
||||
def generate
|
||||
SecureRandom.uuid
|
||||
end
|
||||
|
||||
def validate(uuid)
|
||||
uuid.match?(/\A[\da-f]{32}\z/i) || uuid.match?(/\A(urn:uuid:)?[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}\z/i)
|
||||
end
|
||||
end
|
||||
|
||||
class Type < ActiveRecord::Type::Value
|
||||
def deserialize(value)
|
||||
return if value.nil?
|
||||
|
||||
value.unpack('H*').first.tap do |str|
|
||||
[20, 16, 12, 8].each { |pos| str.insert(pos, '-') }
|
||||
end
|
||||
end
|
||||
|
||||
def serialize(value)
|
||||
[ value.delete('-') ].pack('H*')
|
||||
end
|
||||
|
||||
def quoted_id(value)
|
||||
return if value.nil?
|
||||
|
||||
"x'#{value.unpack('H*').first}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
69
lib/peatio/vault/totp.rb
Normal file
69
lib/peatio/vault/totp.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'vault'
|
||||
|
||||
module Vault
|
||||
# Vault::TOTP helper
|
||||
module TOTP
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class << self
|
||||
|
||||
def server_available?
|
||||
read_data('sys/health').present?
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
def validate?(uid, code)
|
||||
write_data(totp_code_key(uid), code: code).data[:valid]
|
||||
end
|
||||
|
||||
def with_human_error
|
||||
raise ArgumentError, 'Block is required' unless block_given?
|
||||
yield
|
||||
rescue Vault::VaultError => e
|
||||
::Rails.logger.error { e }
|
||||
if e.message.include?('connection refused')
|
||||
raise Error, '2FA server is under maintenance'
|
||||
end
|
||||
|
||||
if e.message.include?('code already used')
|
||||
raise Error, 'This code was already used. Wait until the next time period'
|
||||
end
|
||||
|
||||
raise e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def totp_key(uid)
|
||||
"totp/keys/#{Vault.application}_#{uid}"
|
||||
end
|
||||
|
||||
def totp_code_key(uid)
|
||||
"totp/code/#{Vault.application}_#{uid}"
|
||||
end
|
||||
|
||||
def read_data(key)
|
||||
with_human_error do
|
||||
vault.read(key)
|
||||
end
|
||||
end
|
||||
|
||||
def read_code(uid)
|
||||
read_data(totp_code_key(uid)).data[:code]
|
||||
end
|
||||
|
||||
def write_data(key, params)
|
||||
with_human_error do
|
||||
vault.write(key, params)
|
||||
end
|
||||
end
|
||||
|
||||
def vault
|
||||
Vault.logical
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
108
lib/peatio/vault/totp_action.rb
Normal file
108
lib/peatio/vault/totp_action.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
require 'vault'
|
||||
|
||||
module Vault
|
||||
class TOTPAction
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
ISSUER_NAME = 'Dena'
|
||||
attr_reader :action
|
||||
|
||||
def initialize(action)
|
||||
raise 'Please determine action for totp code' if action.blank?
|
||||
|
||||
@action = action
|
||||
end
|
||||
|
||||
def server_available?
|
||||
read_data('sys/health').present?
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
def otp_secret(otp)
|
||||
CGI.parse(URI.parse(otp.data[:url]).query)['secret'][0]
|
||||
end
|
||||
|
||||
def safe_create(uid, email)
|
||||
return if exist?(uid)
|
||||
|
||||
create(uid, email)
|
||||
end
|
||||
|
||||
def create(uid, email, period: '360')
|
||||
write_data(totp_key(uid),
|
||||
generate: true,
|
||||
issuer: ::Peatio::App.config.app_name,
|
||||
period: period, account_name: email, qr_size: 300)
|
||||
end
|
||||
|
||||
def exist?(uid)
|
||||
read_data(totp_key(uid)).present?
|
||||
end
|
||||
|
||||
def validate?(uid, code)
|
||||
return false unless exist?(uid)
|
||||
|
||||
result = write_data(totp_code_key(uid), code: code)
|
||||
return false unless result
|
||||
|
||||
result.data[:valid]
|
||||
end
|
||||
|
||||
def safe_validate?(uid, code)
|
||||
return false unless exist?(uid)
|
||||
|
||||
code == read_code(uid)
|
||||
end
|
||||
|
||||
|
||||
def delete(uid)
|
||||
delete_data(totp_key(uid))
|
||||
end
|
||||
|
||||
def with_human_error
|
||||
raise ArgumentError, 'Block is required' unless block_given?
|
||||
|
||||
yield
|
||||
rescue Vault::VaultError => e
|
||||
::Rails.logger.error { e }
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def totp_key(uid)
|
||||
"totp/keys/#{Vault.application}_#{@action}_#{uid}"
|
||||
end
|
||||
|
||||
def totp_code_key(uid)
|
||||
"totp/code/#{Vault.application}_#{@action}_#{uid}"
|
||||
end
|
||||
|
||||
def read_data(key)
|
||||
with_human_error do
|
||||
vault.read(key)
|
||||
end
|
||||
end
|
||||
|
||||
def read_code(uid)
|
||||
read_data(totp_code_key(uid)).data[:code]
|
||||
end
|
||||
|
||||
def write_data(key, params)
|
||||
with_human_error { vault.write(key, params) }
|
||||
end
|
||||
|
||||
def delete_data(key)
|
||||
with_human_error do
|
||||
vault.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
def vault
|
||||
Vault.logical
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
9
lib/tasks/accounts.rake
Normal file
9
lib/tasks/accounts.rake
Normal file
@@ -0,0 +1,9 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :accounts do
|
||||
desc 'Create missing accounts for existing members.'
|
||||
task touch: :environment do
|
||||
Member.find_each(&:touch_accounts)
|
||||
end
|
||||
end
|
||||
67
lib/tasks/api_becnhmark.rake
Normal file
67
lib/tasks/api_becnhmark.rake
Normal file
@@ -0,0 +1,67 @@
|
||||
require 'faraday'
|
||||
require 'thread/pool'
|
||||
require 'progress_bar'
|
||||
require "#{Rails.root}/lib/api_bench.rb"
|
||||
|
||||
yml_data = YAML.load_file("#{Rails.root}/config/bench/api_bench.yml")
|
||||
namespace :api_benchmark do
|
||||
desc 'create random request for order creation and do trading'
|
||||
task :random_fire do
|
||||
bar = ProgressBar.new(yml_data.dig('order_count'), :percentage, :bar, :elapsed, :eta, :rate)
|
||||
|
||||
pool = Thread.pool(5)
|
||||
creator = ApiBench.new
|
||||
start_time = Time.now
|
||||
yml_data.dig('order_count').times do |_i|
|
||||
pool.process do
|
||||
price = rand(1000..1500)
|
||||
side = price.odd? ? 'sell' : 'buy'
|
||||
creator.create_order(yml_data.dig('base_url'),
|
||||
'btcusd',
|
||||
side,
|
||||
rand.round(2),
|
||||
yml_data.dig('server_token'),
|
||||
'limit',
|
||||
price)
|
||||
bar.increment!
|
||||
end
|
||||
end
|
||||
pool.shutdown
|
||||
puts 'elapsed_time:'
|
||||
puts Time.now - start_time
|
||||
end
|
||||
|
||||
|
||||
desc 'create one big sell request for support a lot of small buy order for trading'
|
||||
task :direct_fire do
|
||||
bar = ProgressBar.new(yml_data.dig('order_count'), :percentage, :bar, :elapsed, :eta, :rate)
|
||||
|
||||
pool = Thread.pool(5)
|
||||
creator = ApiBench.new
|
||||
creator.create_order(yml_data.dig('base_url'),
|
||||
'btcusd',
|
||||
'sell',
|
||||
yml_data.dig('order_count'),
|
||||
yml_data.dig('server_token'),
|
||||
'limit',
|
||||
1000)
|
||||
# sleep(1)
|
||||
start_time = Time.now
|
||||
yml_data.dig('order_count').times do |_i|
|
||||
pool.process do
|
||||
price = rand(1001..1500)
|
||||
creator.create_order(yml_data.dig('base_url'),
|
||||
'btcusd',
|
||||
'buy',
|
||||
1,
|
||||
yml_data.dig('server_token'),
|
||||
'limit',
|
||||
price)
|
||||
bar.increment!
|
||||
end
|
||||
end
|
||||
pool.shutdown
|
||||
puts 'elapsed_time:'
|
||||
puts Time.now - start_time
|
||||
end
|
||||
end
|
||||
54
lib/tasks/auto_annotate_models.rake
Normal file
54
lib/tasks/auto_annotate_models.rake
Normal file
@@ -0,0 +1,54 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
# NOTE: only doing this in development as some production environments (Heroku)
|
||||
# NOTE: are sensitive to local FS writes, and besides -- it's just not proper
|
||||
# NOTE: to have a dev-mode tool do its thing in production.
|
||||
if Rails.env.development? && defined?(Annotate)
|
||||
task :set_annotation_options do
|
||||
# You can override any of these by setting an environment variable of the
|
||||
# same name.
|
||||
Annotate.set_defaults \
|
||||
'routes' => 'false',
|
||||
'position_in_routes' => 'before',
|
||||
'position_in_class' => 'after',
|
||||
'position_in_test' => 'before',
|
||||
'position_in_fixture' => 'before',
|
||||
'position_in_factory' => 'before',
|
||||
'position_in_serializer' => 'before',
|
||||
'show_foreign_keys' => 'true',
|
||||
'show_complete_foreign_keys' => 'true',
|
||||
'show_indexes' => 'true',
|
||||
'simple_indexes' => 'false',
|
||||
'model_dir' => 'app/models',
|
||||
'root_dir' => '',
|
||||
'include_version' => 'true',
|
||||
'require' => '',
|
||||
'exclude_tests' => 'true',
|
||||
'exclude_fixtures' => 'true',
|
||||
'exclude_factories' => 'true',
|
||||
'exclude_serializers' => 'true',
|
||||
'exclude_scaffolds' => 'true',
|
||||
'exclude_controllers' => 'true',
|
||||
'exclude_helpers' => 'true',
|
||||
'exclude_sti_subclasses' => 'false',
|
||||
'ignore_model_sub_dir' => 'false',
|
||||
'ignore_columns' => nil,
|
||||
'ignore_routes' => nil,
|
||||
'ignore_unknown_models' => 'false',
|
||||
'hide_limit_column_types' => '',
|
||||
'hide_default_column_types' => '',
|
||||
'skip_on_db_migrate' => 'false',
|
||||
'format_bare' => 'true',
|
||||
'format_rdoc' => 'false',
|
||||
'format_markdown' => 'false',
|
||||
'sort' => 'false',
|
||||
'force' => 'false',
|
||||
'trace' => 'false',
|
||||
'wrapper_open' => nil,
|
||||
'wrapper_close' => nil,
|
||||
'with_comment' => true
|
||||
end
|
||||
|
||||
Annotate.load_tasks
|
||||
end
|
||||
32
lib/tasks/barong.rake
Normal file
32
lib/tasks/barong.rake
Normal file
@@ -0,0 +1,32 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :barong do
|
||||
desc 'Refresh access level for Barong members.'
|
||||
task levels: :environment do
|
||||
url = "https://#{ENV.fetch('BARONG_DOMAIN')}/api/account"
|
||||
t = Authentication.arel_table
|
||||
Authentication
|
||||
.where(provider: :barong)
|
||||
.where(t[:token].is_not_blank)
|
||||
.where(t[:member_id].is_not_blank)
|
||||
.includes(:member)
|
||||
.order(updated_at: :asc)
|
||||
.limit(1000)
|
||||
.each do |auth|
|
||||
next if auth.token.blank? || auth.member.blank?
|
||||
|
||||
profile = JSON.parse(Faraday.get(url, nil, 'Authorization' => "Bearer #{auth.token}").assert_success!.body)
|
||||
current_level = auth.member.level
|
||||
new_level = profile.fetch('level')
|
||||
|
||||
unless current_level == new_level
|
||||
auth.member.update!(level: new_level)
|
||||
auth.touch
|
||||
Rails.logger.info { "#{auth.member.email}: #{current_level} >> #{new_level}." }
|
||||
end
|
||||
rescue => e
|
||||
report_exception(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
140
lib/tasks/bench.rake
Normal file
140
lib/tasks/bench.rake
Normal file
@@ -0,0 +1,140 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# TODO: Add descriptions.
|
||||
# TODO: Don't sleep in case of last bench.
|
||||
# TODO: Remove legacy benchmarks.
|
||||
namespace :bench do
|
||||
|
||||
namespace :matching do
|
||||
|
||||
desc 'Matching with amqp messages'
|
||||
task :amqp, [:config_load_path] => [:environment] do |_t, args|
|
||||
args.with_defaults(:config_load_path => 'config/bench/matching.yml')
|
||||
|
||||
benches =
|
||||
YAML.load_file(Rails.root.join(args[:config_load_path]))
|
||||
.map(&:deep_symbolize_keys)
|
||||
.each_with_object([]) do |config, memo|
|
||||
Kernel.pp config
|
||||
|
||||
matching = Bench::Matching::AMQP.new(config)
|
||||
matching.run!
|
||||
memo << matching
|
||||
matching.save_report
|
||||
Kernel.puts "Sleep before next bench"
|
||||
sleep 5
|
||||
end
|
||||
|
||||
benches.each {|b| Kernel.pp b.result}
|
||||
end
|
||||
|
||||
desc 'Matching without amqp messages'
|
||||
task :direct, [:config_load_path] => [:environment] do |_t, args|
|
||||
args.with_defaults(:config_load_path => 'config/bench/matching.yml')
|
||||
|
||||
benches =
|
||||
YAML.load_file(Rails.root.join(args[:config_load_path]))
|
||||
.map(&:deep_symbolize_keys)
|
||||
.each_with_object([]) do |config, memo|
|
||||
Kernel.pp config
|
||||
|
||||
matching = Bench::Matching::Direct.new(config)
|
||||
matching.run!
|
||||
memo << matching
|
||||
matching.save_report
|
||||
Kernel.puts "Sleep before next bench"
|
||||
sleep 5
|
||||
end
|
||||
|
||||
benches.each {|b| Kernel.pp b.result}
|
||||
end
|
||||
end
|
||||
|
||||
namespace :trade_execution do
|
||||
|
||||
desc 'Trade Execution with amqp messages'
|
||||
task :amqp, [:config_load_path] => [:environment] do |_t, args|
|
||||
args.with_defaults(:config_load_path => 'config/bench/trade_execution.yml')
|
||||
benches =
|
||||
YAML.load_file(Rails.root.join(args[:config_load_path]))
|
||||
.map(&:deep_symbolize_keys)
|
||||
.each_with_object([]) do |config, memo|
|
||||
Kernel.pp config
|
||||
trade_execution = ::Bench::TradeExecution::AMQP.new(config)
|
||||
trade_execution.run!
|
||||
memo << trade_execution
|
||||
trade_execution.save_report
|
||||
Kernel.puts "Sleep before next bench"
|
||||
sleep 5
|
||||
end
|
||||
|
||||
benches.each {|b| Kernel.pp b.result}
|
||||
end
|
||||
|
||||
desc 'Trade execution without amqp messages'
|
||||
task :direct, [:config_load_path] => [:environment] do |_t, args|
|
||||
args.with_defaults(:config_load_path => 'config/bench/trade_execution.yml')
|
||||
|
||||
benches =
|
||||
YAML.load_file(Rails.root.join(args[:config_load_path]))
|
||||
.map(&:deep_symbolize_keys)
|
||||
.each_with_object([]) do |config, memo|
|
||||
Kernel.pp config
|
||||
|
||||
trade_execution = ::Bench::TradeExecution::Direct.new(config)
|
||||
trade_execution.run!
|
||||
memo << trade_execution
|
||||
trade_execution.save_report
|
||||
Kernel.puts "Sleep before next bench"
|
||||
sleep 5
|
||||
end
|
||||
|
||||
benches.each {|b| Kernel.pp b.result}
|
||||
end
|
||||
end
|
||||
|
||||
namespace :order_processing do
|
||||
|
||||
desc 'Order Processing with amqp messages'
|
||||
task :amqp, [:config_load_path] => [:environment] do |_t, args|
|
||||
args.with_defaults(:config_load_path => 'config/bench/order_processing.yml')
|
||||
|
||||
benches =
|
||||
YAML.load_file(Rails.root.join(args[:config_load_path]))
|
||||
.map(&:deep_symbolize_keys)
|
||||
.each_with_object([]) do |config, memo|
|
||||
Kernel.pp config
|
||||
|
||||
order_processing = ::Bench::OrderProcessing::AMQP.new(config)
|
||||
order_processing.run!
|
||||
memo << order_processing
|
||||
order_processing.save_report
|
||||
Kernel.puts "Sleep before next bench"
|
||||
sleep 5
|
||||
end
|
||||
|
||||
benches.each {|b| Kernel.pp b.result}
|
||||
end
|
||||
|
||||
desc 'Order Processing without amqp messages'
|
||||
task :direct, [:config_load_path] => [:environment] do |_t, args|
|
||||
args.with_defaults(:config_load_path => 'config/bench/order_processing.yml')
|
||||
|
||||
benches =
|
||||
YAML.load_file(Rails.root.join(args[:config_load_path]))
|
||||
.map(&:deep_symbolize_keys)
|
||||
.each_with_object([]) do |config, memo|
|
||||
Kernel.pp config
|
||||
|
||||
order_processing = Bench::OrderProcessing::Direct.new(config)
|
||||
order_processing.run!
|
||||
memo << order_processing
|
||||
order_processing.save_report
|
||||
Kernel.puts "Sleep before next bench"
|
||||
sleep 5
|
||||
end
|
||||
|
||||
benches.each { |b| Kernel.pp b.result }
|
||||
end
|
||||
end
|
||||
end
|
||||
72
lib/tasks/benchmark.rake
Normal file
72
lib/tasks/benchmark.rake
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace :benchmark do
|
||||
|
||||
desc "In memory matching engine benchmark"
|
||||
task :matching => %w(environment) do
|
||||
max_round = round(2)
|
||||
puts "\n>> Setup environment (num=#{num} round=#{max_round})"
|
||||
Dir[Rails.root.join('tmp', 'matching_result_*')].each {|f| FileUtils.rm(f) }
|
||||
|
||||
Benchmark::Matching.new(label, num, max_round).run
|
||||
end
|
||||
|
||||
desc "Trade execution benchmark"
|
||||
task :execution => %w(environment) do
|
||||
max_round = round(2)
|
||||
puts "\n>> Setup environment (executor=#{executor} num=#{num} round=#{max_round})"
|
||||
Dir[Rails.root.join('tmp', 'matching_result_*')].each {|f| FileUtils.rm(f) }
|
||||
|
||||
Benchmark::Execution.new(label, num, max_round, executor).run
|
||||
end
|
||||
|
||||
desc "Run integration benchmark"
|
||||
task :integration => %w(environment) do
|
||||
puts "Integration Benchmark (num: #{num(400)})\n"
|
||||
|
||||
Benchmark::Integration.new(num).run
|
||||
end
|
||||
|
||||
desc "Profiling"
|
||||
task :profiling, [:type]=> %w(environment) do |task, args|
|
||||
|
||||
case args[:type]
|
||||
|
||||
when 'matching'
|
||||
puts "\n>> Setup environment (num=#{num} round=#{round})"
|
||||
Dir[Rails.root.join('tmp', 'profiling_matching_result_*')].each {|f| FileUtils.rm(f) }
|
||||
file_path = Rails.root.join('tmp', "profiling_matching_result_#{Time.now.to_i}")
|
||||
|
||||
File.open(file_path, 'w') { |file| file.puts "\n>> Setup environment (num=#{num} round=#{round})" }
|
||||
|
||||
Benchmark::Profiling.matching(label, num, round, file_path)
|
||||
|
||||
when 'execution'
|
||||
puts "\n>> Setup environment (executor=#{executor} num=#{num} round=#{round})"
|
||||
Dir[Rails.root.join('tmp', 'profiling_execution_result_*')].each {|f| FileUtils.rm(f) }
|
||||
|
||||
file_path = Rails.root.join('tmp', "profiling_execution_result_#{Time.now.to_i}")
|
||||
File.open(file_path, 'w') { |file| file.puts "\n>> Setup environment (executor=#{executor} num=#{num} round=#{round})" }
|
||||
|
||||
Benchmark::Profiling.execution(label, num, round, executor, file_path)
|
||||
|
||||
else
|
||||
puts "\n>> Wrong parameter!"
|
||||
end
|
||||
end
|
||||
|
||||
def num
|
||||
ENV['NUM'] ? ENV['NUM'].to_i : 100
|
||||
end
|
||||
|
||||
def round r = 1
|
||||
ENV['ROUND'] ? ENV['ROUND'].to_i : r
|
||||
end
|
||||
|
||||
def label
|
||||
ENV['LABEL'] || Time.now.to_i
|
||||
end
|
||||
|
||||
def executor
|
||||
ENV['EXECUTOR'] ? ENV['EXECUTOR'].to_i : 6
|
||||
end
|
||||
end
|
||||
|
||||
12
lib/tasks/billoner_members.rake
Normal file
12
lib/tasks/billoner_members.rake
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace :billionaire do
|
||||
desc 'make your memberes billionaires'
|
||||
task :by_email, %i[market members] => [:environment] do |_t, args|
|
||||
include ::Bench::Helpers
|
||||
memberes_array = args[:members].split(' ')
|
||||
memberes = ::Member.where(email: memberes_array)
|
||||
raise 'members if empty' if memberes.empty? || args[:members].blank?
|
||||
|
||||
@currencies = ::Currency.where(id: args[:market].split('-').map(&:squish).reject(&:blank?))
|
||||
memberes.map(&method(:become_billionaire))
|
||||
end
|
||||
end
|
||||
11
lib/tasks/bitgo_webhooks.rake
Normal file
11
lib/tasks/bitgo_webhooks.rake
Normal file
@@ -0,0 +1,11 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :bitgo do
|
||||
desc 'Add a webhook that will result in an HTTP callback at the specified URL from BitGo when events are triggered.'
|
||||
task :webhooks, [:url] => [:environment] do
|
||||
Wallet.deposit.active.where(gateway: :bitgo).each do |w|
|
||||
w.service.register_webhooks!(args[:url])
|
||||
end
|
||||
end
|
||||
end
|
||||
11
lib/tasks/clear.rake
Normal file
11
lib/tasks/clear.rake
Normal file
@@ -0,0 +1,11 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :clear do
|
||||
desc 'Clear database from accounting information.'
|
||||
task accounting: :environment do
|
||||
table_names = %w[accounts adjustments assets beneficiaries deposits expenses
|
||||
liabilities orders payment_addresses revenues trades transfers triggers withdraws]
|
||||
table_names.each { |name| ActiveRecord::Base.connection.execute("TRUNCATE TABLE #{name}") }
|
||||
end
|
||||
end
|
||||
42
lib/tasks/create_user_barong.rake
Normal file
42
lib/tasks/create_user_barong.rake
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace :bench do
|
||||
namespace :barong_session do
|
||||
desc 'create user and create barong session'
|
||||
|
||||
task :create => [:environment] do
|
||||
members = Bench::Barong_session::Sessions.new(trader: 10)
|
||||
members.run!
|
||||
end
|
||||
|
||||
task :session_create => [:environment] do
|
||||
config = YAML.load_file("#{Rails.root}/config/application.yml")
|
||||
session_file = "#{Rails.root}/public/sessions.csv"
|
||||
file = "#{Rails.root}/public/user_data.csv"
|
||||
result = []
|
||||
CSV.foreach(file) do |row|
|
||||
Kernel.pp row[0]
|
||||
url = "#{config["development"]["LOG_IN_URL"]}"
|
||||
url_balance = "#{config["development"]["BALANCE_URL"]}"
|
||||
resp = Faraday.post(url) do |req|
|
||||
req.params['email'] = row[0]
|
||||
req.params['password'] = "#{config["development"]["SIGN_UP_PASSWORD"]}"
|
||||
Kernel.puts "#{req.params["email"]} tried to log in"
|
||||
end
|
||||
balance = Faraday.get(url_balance) do |req|
|
||||
req.headers['Cookie'] = resp.headers["set-cookie"].split(';')[0]
|
||||
end
|
||||
Kernel.pp balance.body
|
||||
result << resp.headers["set-cookie"].split(';')[0]
|
||||
|
||||
end
|
||||
CSV.open(session_file, 'w+', write_headers: true) do |csv|
|
||||
result.each do |res|
|
||||
csv << [res]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
42
lib/tasks/distribution.rake
Normal file
42
lib/tasks/distribution.rake
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'csv'
|
||||
|
||||
namespace :distribution do
|
||||
# Detailed instruction https://github.com/rubykube/peatio/blob/master/docs/tasks/distribution.md
|
||||
# Required fields for distribution:
|
||||
# - uid
|
||||
# - currency_id
|
||||
# - amount
|
||||
#
|
||||
# Usage:
|
||||
# For distribution process: -> bundle exec rake distribution:process['file_name.csv']
|
||||
|
||||
desc 'Distribution process'
|
||||
task :process, [:config_load_path] => [:environment] do |_, args|
|
||||
csv_table = File.read(Rails.root.join(args[:config_load_path]))
|
||||
count = 0
|
||||
errors_count = 0
|
||||
CSV.parse(csv_table, headers: true, quote_empty: false).each do |row|
|
||||
row = row.to_h.compact.symbolize_keys!
|
||||
uid = row[:uid]
|
||||
currency_id = row[:currency_id]
|
||||
amount = row[:amount].to_d
|
||||
member = Member.find_by_uid!(uid)
|
||||
currency = Currency.find(currency_id)
|
||||
account = member.get_account(currency)
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
Operations::Asset.credit!(currency: currency, amount: amount, reference_type: 'Distribution')
|
||||
Operations::Liability.credit!(kind: :main, currency: currency, member_id: member.id, amount: amount, reference_type: 'Distribution')
|
||||
account.update!(balance: account.balance + amount)
|
||||
end
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, uid: row[:uid] }
|
||||
::Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
Kernel.puts "Distributions processed #{count}"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
end
|
||||
138
lib/tasks/export.rake
Normal file
138
lib/tasks/export.rake
Normal file
@@ -0,0 +1,138 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
require 'csv'
|
||||
require 'peatio/export'
|
||||
|
||||
namespace :export do
|
||||
desc 'Export all configs to yaml files.'
|
||||
task configs: :environment do
|
||||
Rake::Task['export:blockchains'].invoke
|
||||
Rake::Task['export:currencies'].invoke
|
||||
Rake::Task['export:markets'].invoke
|
||||
Rake::Task['export:wallets'].invoke
|
||||
Rake::Task['export:engines'].invoke
|
||||
Rake::Task['export:trading_fees'].invoke
|
||||
end
|
||||
|
||||
desc 'Export blockchains config to yaml file.'
|
||||
task :blockchains, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'config/seed/blockchains.yml')
|
||||
File.write(args.export_path, Peatio::Export.new.export_blockchains.to_yaml)
|
||||
end
|
||||
|
||||
desc 'Export currencies config to yaml file.'
|
||||
task :currencies, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'config/seed/currencies_backup.yml')
|
||||
File.write(args.export_path, Peatio::Export.new.export_currencies.to_yaml)
|
||||
end
|
||||
|
||||
desc 'Export markets config to yaml file.'
|
||||
task :markets, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'config/seed/markets_backup.yml')
|
||||
File.write(args.export_path, Peatio::Export.new.export_markets.to_yaml)
|
||||
end
|
||||
|
||||
desc 'Export wallets config to yaml file.'
|
||||
task :wallets, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'config/seed/wallets_backup.yml')
|
||||
File.write(args.export_path, Peatio::Export.new.export_wallets.to_yaml)
|
||||
end
|
||||
|
||||
desc 'Export trading fees config to yaml file.'
|
||||
task :trading_fees, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'config/seed/trading_fees_backup.yml')
|
||||
File.write(args.export_path, Peatio::Export.new.export_trading_fees.to_yaml)
|
||||
end
|
||||
|
||||
desc 'Export engines to yaml file.'
|
||||
task :engines, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'config/seed/engines_backup.yml')
|
||||
File.write(args.export_path, Peatio::Export.new.export_engines.to_yaml)
|
||||
end
|
||||
|
||||
desc 'Export all members to csv file.'
|
||||
task :users, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'exported_users.csv')
|
||||
count = 0
|
||||
errors_count = 0
|
||||
begin
|
||||
CSV.open(args.export_path, 'w') do |csv|
|
||||
csv << %w[uid email level role state]
|
||||
Member.find_each do |member|
|
||||
csv << [member.uid, member.email, member.level, member.role, member.state]
|
||||
count += 1
|
||||
end
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, email: member.email, uid: member.uid }
|
||||
::Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
end
|
||||
Kernel.puts "Exported #{count} members"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
|
||||
desc 'Export accounts to csv file.'
|
||||
task :accounts, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'exported_accounts.csv')
|
||||
count = 0
|
||||
errors_count = 0
|
||||
begin
|
||||
CSV.open(args.export_path, 'w') do |csv|
|
||||
csv << %w[uid currency_id main_balance locked_balance]
|
||||
Account.find_each do |account|
|
||||
if account.balance.positive? || account.locked.positive?
|
||||
csv << [account.member.uid, account.currency_id, account.balance, account.locked]
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, uid: account.member.uid, currency_id: account.currency_id }
|
||||
::Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
end
|
||||
Kernel.puts "Exported #{count} accounts"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
|
||||
desc 'Export addresses to csv file.'
|
||||
task :addresses, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'exported_addresses.csv')
|
||||
count = 0
|
||||
errors_count = 0
|
||||
begin
|
||||
CSV.open(args.export_path, 'w') do |csv|
|
||||
csv << %w[uid wallet_name address secret details]
|
||||
PaymentAddress.find_each do |address|
|
||||
wallet = Wallet.find(address.wallet_id)
|
||||
# We save wallet name instead of id because id can change after export/import migration
|
||||
csv << [address.member.uid, wallet.name, address.address, address.secret, address.details]
|
||||
count += 1
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, uid: address.member.uid, wallet_name: address.wallet.name }
|
||||
::Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
Kernel.puts "Exported #{count} addresses"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
|
||||
desc 'Export configs(blockchains, currencies, wallets, markets, engines) from the database'
|
||||
task :configs, [:export_path] => [:environment] do |_, args|
|
||||
args.with_defaults(export_path: 'export_configs.yaml')
|
||||
ex = Peatio::Export.new
|
||||
File.write(args.export_path, {
|
||||
'accounts' => ex.export_accounts,
|
||||
'blockchains' => ex.export_blockchains,
|
||||
'currencies' => ex.export_currencies,
|
||||
'markets' => ex.export_markets,
|
||||
'wallets' => ex.export_wallets,
|
||||
'trading_fees' => ex.export_trading_fees,
|
||||
'engines' => ex.export_engines
|
||||
}.to_yaml)
|
||||
end
|
||||
end
|
||||
37
lib/tasks/failures.rake
Normal file
37
lib/tasks/failures.rake
Normal file
@@ -0,0 +1,37 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :failures do
|
||||
desc 'Fetch Trade Execution Errors'
|
||||
task trade_errors: :environment do
|
||||
|
||||
conn = Bunny.new AMQP::Config.connect
|
||||
conn.start
|
||||
|
||||
ch = conn.create_channel
|
||||
|
||||
q = ch.queue("peatio.trades.errors")
|
||||
puts "****** Fetching Queue Messages ******"
|
||||
count = q.message_count
|
||||
puts "****** Total Messages in queue = #{count} ******"
|
||||
errors = []
|
||||
until count == 0
|
||||
_delivery_info, _metadata, payload = q.pop
|
||||
payload = JSON.parse(payload).symbolize_keys!
|
||||
err_obj = payload.slice(:code, :message)
|
||||
if err = errors.find{|k| k[:code] == err_obj.fetch(:code)}
|
||||
err[:count] += 1
|
||||
else
|
||||
errors << {code: err_obj.fetch(:code), count: 1 }
|
||||
end
|
||||
count -= 1
|
||||
end
|
||||
|
||||
errors.each do |error|
|
||||
puts "****** Error Code: #{error[:code]} Count = #{error[:count]} ******"
|
||||
end
|
||||
puts "****** purging queue *******"
|
||||
q.purge
|
||||
|
||||
end
|
||||
end
|
||||
26
lib/tasks/fetch.rake
Normal file
26
lib/tasks/fetch.rake
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :fetch do
|
||||
desc 'Fetch currency price'
|
||||
task :price, %i[quote external_currencies_service] => [:environment] do |_, args|
|
||||
# TODO
|
||||
# Add ability to take price from platform market
|
||||
Currency.find_in_batches(batch_size: 50) do |group|
|
||||
group.each do |record|
|
||||
url = args.external_currencies_service || ENV['EXTERNAL_CURRENCIES_SERVICE']
|
||||
raise 'There is no external currencies service configured' unless url.present?
|
||||
|
||||
# API call to external currencies service to get current currency price
|
||||
response = Faraday.get(url, { code: record.code, quote: args.quote })
|
||||
response_body = JSON.parse(response.body)
|
||||
|
||||
next unless response_body['current_price'].present?
|
||||
|
||||
::Rails.logger.info { "Updating currency #{record.code} with price #{response_body["current_price"]}" }
|
||||
record.update!(price: response_body['current_price'])
|
||||
rescue Faraday::Error, StandardError => e
|
||||
::Rails.logger.error e.inspect
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
196
lib/tasks/import.rake
Normal file
196
lib/tasks/import.rake
Normal file
@@ -0,0 +1,196 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'csv'
|
||||
require 'peatio/import'
|
||||
|
||||
namespace :import do
|
||||
# Detailed instruction https://github.com/rubykube/peatio/blob/master/docs/tasks/import.md
|
||||
# Required fields for import users:
|
||||
# - uid
|
||||
# - email
|
||||
#
|
||||
# Usage:
|
||||
# For import users: -> bundle exec rake import:users['file_name.csv']
|
||||
|
||||
desc 'Load members from csv file.'
|
||||
task :users, [:config_load_path] => [:environment] do |_, args|
|
||||
csv_table = File.read(Rails.root.join(args[:config_load_path]))
|
||||
count = 0
|
||||
errors_count = 0
|
||||
CSV.parse(csv_table, headers: true, quote_empty: false).each do |row|
|
||||
row = row.to_h.compact.symbolize_keys!
|
||||
defaults = { level: 0, role: 'member', state: 'active' }
|
||||
permitted_attr = %i[uid email level role state]
|
||||
Member.create!(row.slice(*permitted_attr).reverse_merge(defaults))
|
||||
count += 1
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, email: row[:email], uid: row[:uid] }
|
||||
Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
Kernel.puts "Created #{count} members"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
|
||||
# Required fields for import accounts balances:
|
||||
# - uid
|
||||
# - currency_id
|
||||
#
|
||||
# Make sure that you create required currency
|
||||
# Usage:
|
||||
# For import account balances: -> bundle exec rake import:accounts['file_name.csv']
|
||||
|
||||
desc 'Load accounts balances from csv file.'
|
||||
task :accounts, %i[config_load_path balance_check] => [:environment] do |_, args|
|
||||
args.with_defaults(:config_load_path => 'exported_accounts.csv', :balance_check => false)
|
||||
csv_table = File.read(Rails.root.join(args[:config_load_path]))
|
||||
count = 0
|
||||
errors_count = 0
|
||||
CSV.parse(csv_table, headers: true).each do |row|
|
||||
row = row.to_h.compact.symbolize_keys!
|
||||
uid = row[:uid]
|
||||
member = Member.find_by_uid!(uid)
|
||||
currency = Currency.find(row[:currency_id])
|
||||
account = Account.find_or_create_by!(member: member, currency: currency)
|
||||
main_balance = row[:main_balance].to_d
|
||||
locked_balance = row[:locked_balance].to_d
|
||||
next if args[:balance_check] == 'true' && main_balance <= 0 && locked_balance <= 0
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
Operations::Asset.credit!(currency: currency, amount: main_balance + locked_balance)
|
||||
Operations::Liability.credit!(kind: :main, currency: currency, member_id: member.id, amount: main_balance)
|
||||
Operations::Liability.credit!(kind: :locked, currency: currency, member_id: member.id, amount: locked_balance)
|
||||
account.update!(balance: main_balance, locked: locked_balance)
|
||||
count += 1
|
||||
end
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, uid: row[:uid] }
|
||||
Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
Kernel.puts "Accounts created #{count}"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
|
||||
desc 'Load addresses from csv file. Export file from Peatio version >= 2.6.0'
|
||||
task :addresses, [:config_load_path] => [:environment] do |_, args|
|
||||
args.with_defaults(:config_load_path => 'exported_addresses.csv')
|
||||
csv_table = File.read(Rails.root.join(args[:config_load_path]))
|
||||
count = 0
|
||||
errors_count = 0
|
||||
CSV.parse(csv_table, headers: true).each do |row|
|
||||
row = row.to_h.compact.symbolize_keys!
|
||||
uid = row[:uid]
|
||||
member = Member.find_by_uid!(uid)
|
||||
wallet = Wallet.find_by(name: row[:wallet_name])
|
||||
PaymentAddress.create(member_id: member.id, wallet_id: wallet.id, address: row[:address], secret: row[:secret], details: row[:details])
|
||||
count += 1
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, uid: row[:uid], currency_id: currency_id[:currency_id] }
|
||||
Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
Kernel.puts "Addresses created #{count}"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
|
||||
desc 'Load addresses from csv file. Export file from Peatio version < 2.6.0'
|
||||
task :addresses_legacy, [:config_load_path] => [:environment] do |_, args|
|
||||
args.with_defaults(:config_load_path => 'exported_addresses.csv')
|
||||
csv_table = File.read(Rails.root.join(args[:config_load_path]))
|
||||
count = 0
|
||||
errors_count = 0
|
||||
CSV.parse(csv_table, headers: true).each do |row|
|
||||
row = row.to_h.compact.symbolize_keys!
|
||||
uid = row[:uid]
|
||||
member = Member.find_by_uid!(uid)
|
||||
wallet = Wallet.deposit_wallet(row[:currency_id])
|
||||
PaymentAddress.create(member_id: member.id, wallet_id: wallet.id, address: row[:address], secret: row[:secret], details: row[:details])
|
||||
count += 1
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, uid: row[:uid], currency_id: currency_id[:currency_id] }
|
||||
Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
Kernel.puts "Addresses created #{count}"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
|
||||
desc 'Load whitelisted smart contracts from CSV'
|
||||
task :whitelisted_smart_contracts, [:config_load_path] => [:environment] do |_, args|
|
||||
args.with_defaults(:config_load_path => 'exported_whitelisted_smart_contracts.csv')
|
||||
csv_table = File.read(Rails.root.join(args[:config_load_path]))
|
||||
count = 0
|
||||
errors_count = 0
|
||||
CSV.parse(csv_table, headers: true, quote_empty: false).each do |row|
|
||||
row = row.to_h.compact.symbolize_keys!
|
||||
address = row[:address]
|
||||
blockchain_key = row[:blockchain_key]
|
||||
description = row[:description]
|
||||
next if address.blank? || blockchain_key.blank? || ::Blockchain.pluck(:key).exclude?(blockchain_key)
|
||||
|
||||
::WhitelistedSmartContract.create!(description: description, address: address,
|
||||
blockchain_key: blockchain_key, state: 'active')
|
||||
count += 1
|
||||
rescue StandardError => e
|
||||
message = { error: e.message, uid: row[:uid], currency_id: currency_id[:currency_id] }
|
||||
Rails.logger.error message
|
||||
errors_count += 1
|
||||
end
|
||||
Kernel.puts "whitelisted contracts created #{count}"
|
||||
Kernel.puts "Errored #{errors_count}"
|
||||
end
|
||||
|
||||
desc 'Import configs(accounts, blockchains, currencies, wallets, trading_fees, markets, engines, whitelisted_smart_contracts) to the database'
|
||||
task :configs, [:config_load_path] => :environment do |_, args|
|
||||
args.with_defaults(config_load_path: 'import_configs.yaml')
|
||||
|
||||
import_data = YAML.load_file(Rails.root.join(args[:config_load_path]))
|
||||
Peatio::Import.new(import_data).load_all
|
||||
end
|
||||
|
||||
desc 'Load local trades to the Influx. By default, it will load trades starting from the last id in Influx'
|
||||
task :trade_to_influx, [:full_load] => :environment do |_, args|
|
||||
args.with_defaults(full_load: 'false')
|
||||
if args.full_load == 'false'
|
||||
ids = []
|
||||
Peatio::InfluxDB.config[:host].each do |host|
|
||||
client = Peatio::InfluxDB.client(host: [host])
|
||||
client.query('SELECT id from trades ORDER BY desc limit 1') do |_name, _tags, points|
|
||||
ids << points.map(&:deep_symbolize_keys!).first[:id]
|
||||
end
|
||||
end
|
||||
last_id = ids.max
|
||||
Trade.where('id > ?', last_id.to_i).find_in_batches do |batch|
|
||||
process_trades_batch(batch)
|
||||
end
|
||||
elsif args.full_load == 'true'
|
||||
Trade.find_in_batches do |batch|
|
||||
process_trades_batch(batch)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_trades_batch(batch)
|
||||
batch.each_with_index do |trade, index|
|
||||
# We will convert created_at to ms and update it with index to make sure that we have unique
|
||||
# timestamps for each trade because influxdb use timestamp as unique identifier.
|
||||
influx_data = trade.influx_data.merge(timestamp: trade.created_at.to_i * 1000 + index)
|
||||
Peatio::InfluxDB.client(keyshard: trade.market_id).write_point('trades', influx_data, "ms")
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Build candles for all trades in influx'
|
||||
task influx_build_candles: :environment do
|
||||
prev_from = 'trades'
|
||||
Peatio::InfluxDB.config[:host].each do |host|
|
||||
client = Peatio::InfluxDB.client(host: [host])
|
||||
client.query('SELECT FIRST(price) AS open, max(price) AS high, min(price) AS low, last(price) AS close, sum(amount) AS volume INTO candles_1m FROM trades GROUP BY time(1m), market')
|
||||
prev_from = 'candles_1m'
|
||||
KLineService::HUMANIZED_POINT_PERIODS.except(1).each do |_, v|
|
||||
client.query("SELECT FIRST(open) as open, MAX(high) as high, MIN(low) as low, LAST(close) as close, SUM(volume) as volume INTO candles_#{v} FROM #{prev_from} GROUP BY time(#{v}), market")
|
||||
prev_from = "candles_#{v}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
61
lib/tasks/init_order.rake
Normal file
61
lib/tasks/init_order.rake
Normal file
@@ -0,0 +1,61 @@
|
||||
namespace :init_order do
|
||||
desc 'initalizing orders for testing'
|
||||
task :fire, %i[order_count member_count] => [:environment] do |_t, args|
|
||||
include ::Bench::Helpers
|
||||
raise 'please set config' if args[:order_count].blank? || args[:member_count].blank?
|
||||
|
||||
my_config = { injector: 'dummy', number: args[:order_count].to_i, step: 100, markets: 'btcusd',
|
||||
currencies: 'btc,usd', min_volume: 0.001, max_volume: 0.005 }
|
||||
|
||||
memberes = ::Bench::Factories.create_list(:member, args[:member_count].to_i)
|
||||
@currencies = ::Currency.where(id: my_config[:currencies].split(',').map(&:squish).reject(&:blank?))
|
||||
raise 'check currencies' unless @currencies.present?
|
||||
raise 'check members' unless memberes.present?
|
||||
|
||||
memberes.map(&method(:become_billionaire))
|
||||
|
||||
buy_config = my_config.merge({ min_price: 19_000, max_price: 20_000 })
|
||||
buy_injector = ::Bench::Injectors.initialize_injector(buy_config)
|
||||
(1..my_config[:number]).each do |_i|
|
||||
my_create_order(buy_injector.construct_order(memberes, 'OrderBid'))
|
||||
end
|
||||
|
||||
sell_config = my_config.merge({ min_price: 50_000, max_price: 51_000 })
|
||||
sell_injector = ::Bench::Injectors.initialize_injector(sell_config)
|
||||
(1..my_config[:number]).each do |_i|
|
||||
my_create_order(sell_injector.construct_order(memberes, 'OrderAsk'))
|
||||
end
|
||||
end
|
||||
|
||||
task :fire_token => [:environment] do
|
||||
raise 'secretFile is not existed' unless File.exist?("#{Rails.root}/config/secrets/rsa-key")
|
||||
|
||||
secret_file = File.read("#{Rails.root}/config/secrets/rsa-key")
|
||||
puts secret_file
|
||||
raise 'secretFile is not existed' unless File.exist?("#{Rails.root}/config/secrets/rsa-key.pub")
|
||||
|
||||
public_file = File.read("#{Rails.root}/config/secrets/rsa-key.pub")
|
||||
puts public_file
|
||||
|
||||
token_file = "#{Rails.root}/public/member_jwt_token.csv"
|
||||
JWT_ALGORITHM = 'RS256'
|
||||
ENCODED_PRIVATE_KEY = Base64.urlsafe_encode64(secret_file)
|
||||
ENCODED_PUBLIC_KEY = Base64.urlsafe_encode64(public_file)
|
||||
result = []
|
||||
Member.all.each do |member|
|
||||
payload = { uid: member.uid, email: member.email, level: member.level,
|
||||
role: member.role, state: "active", aud: "peatio", sub: "session",
|
||||
jti: SecureRandom.uuid }
|
||||
token = JWT.encode(payload, OpenSSL::PKey.read(Base64.urlsafe_decode64(ENCODED_PRIVATE_KEY)), JWT_ALGORITHM)
|
||||
puts [member.uid, member.email, token]
|
||||
result << [member.uid.to_s, member.email.to_s, token.to_s]
|
||||
end
|
||||
|
||||
CSV.open(token_file, 'w', write_headers: true) do |csv|
|
||||
result.each do |res|
|
||||
csv << res
|
||||
end
|
||||
puts 'FINISHED'
|
||||
end
|
||||
end
|
||||
end
|
||||
93
lib/tasks/job.rake
Normal file
93
lib/tasks/job.rake
Normal file
@@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
namespace :job do
|
||||
namespace :order do
|
||||
desc 'Close orders older than ORDER_MAX_AGE.'
|
||||
task close: :environment do
|
||||
Job.execute('close_orders') do
|
||||
order_max_age = ENV.fetch('ORDER_MAX_AGE', 2_419_200).to_i
|
||||
|
||||
# Cancel orders that older than max_order_age
|
||||
orders = Order.where('created_at < ? AND state = ?', Time.now - order_max_age, 100)
|
||||
orders.each do |o|
|
||||
Order.cancel(o.id)
|
||||
end
|
||||
|
||||
{ pointer: Time.now.to_s(:db), counter: orders.count }
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Archive and delete old cancelled orders without trades to the archive database.'
|
||||
task archive: :environment do
|
||||
Job.execute('archive_orders') do
|
||||
time = Time.now
|
||||
# default batch 1000
|
||||
count = Order.where(state: :cancel, trades_count: 0).where('updated_at < ?', time - 1.week).count
|
||||
Order.where(state: :cancel, trades_count: 0).where('updated_at < ?', time - 1.week).find_in_batches do |batch|
|
||||
ActiveRecord::Base.establish_connection(:archive_db)
|
||||
batch.each do |o|
|
||||
Order.new(o.attributes).save!(validate: false)
|
||||
end
|
||||
ActiveRecord::Base.establish_connection
|
||||
batch.each do |o|
|
||||
o.delete
|
||||
end
|
||||
end
|
||||
{ pointer: time, counter: count }
|
||||
end
|
||||
end
|
||||
|
||||
def order_insert
|
||||
'INSERT INTO orders (id, uuid, remote_id, bid, ask, market_id, price, ' \
|
||||
'volume, origin_volume, maker_fee, taker_fee, state, type, member_id, ord_type, ' \
|
||||
'locked, origin_locked, funds_received, trades_count, created_at, updated_at) VALUES ' \
|
||||
end
|
||||
|
||||
def order_values(order)
|
||||
order['remote_id'] = order['remote_id'].nil? ? 'NULL' : order['remote_id']
|
||||
order['uuid'] = UUID::Type.new.quoted_id(order['uuid'])
|
||||
"(#{order['id']}, #{order['uuid']}, #{order['remote_id']}, " \
|
||||
"'#{order['bid']}', '#{order['ask']}', '#{order['market_id']}', " \
|
||||
"#{order['price']}, #{order['volume']}, #{order['origin_volume']}, " \
|
||||
"#{order['maker_fee']}, #{order['taker_fee']}, #{order['state']}, " \
|
||||
"'#{order['type']}', #{order['member_id']}, '#{order['ord_type']}', " \
|
||||
"#{order['locked']}, #{order['origin_locked']}, #{order['funds_received']}, " \
|
||||
"#{order['trades_count']}, '#{order['created_at']}', '#{order['updated_at']}')"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :liabilities do
|
||||
desc 'Compact liabilities using Stored Procedure'
|
||||
task :compact_orders, %i[min_time max_time] => [:environment] do |_, args|
|
||||
Job.execute('compact_orders') do
|
||||
# Connection to the main database
|
||||
main_db = if Rails.configuration.database_adapter.downcase == 'PostgreSQL'.downcase
|
||||
ActiveRecord::Base.connection.raw_connection
|
||||
else
|
||||
Mysql2::Client.new(sql_config(ENV.fetch('RAILS_ENV', 'development')))
|
||||
end
|
||||
# Execute Stored Procedure for Liabilities compacting
|
||||
# Example:
|
||||
# Current date: "2020-07-30 16:39:15"
|
||||
# min_time: "2020-07-23 00:00:00"
|
||||
# max_time: "2020-07-24 00:00:00"
|
||||
# Compact liabilities beetwen: "2020-07-23 00:00:00" and "2020-07-24 00:00:00"
|
||||
args.with_defaults(min_time: (Time.now - 1.week).beginning_of_day.to_s(:db),
|
||||
max_time: (Time.now - 6.day).beginning_of_day.to_s(:db))
|
||||
result = if Rails.configuration.database_adapter.downcase == 'PostgreSQL'.downcase
|
||||
main_db.query("select * from compact_orders('#{args.min_time}'::date, '#{args.max_time}'::date);")
|
||||
else
|
||||
main_db.query("call compact_orders('#{args.min_time}', '#{args.max_time}');")
|
||||
end
|
||||
result.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def sql_config(namespace)
|
||||
yaml = ::Pathname.new('config/database.yml')
|
||||
return {} unless yaml.exist?
|
||||
|
||||
::SafeYAML.load(::ERB.new(yaml.read).result)[namespace]
|
||||
end
|
||||
end
|
||||
37
lib/tasks/release.rake
Normal file
37
lib/tasks/release.rake
Normal file
@@ -0,0 +1,37 @@
|
||||
require 'bump'
|
||||
|
||||
def bot_username
|
||||
ENV.fetch('BOT_USERNAME', 'kite-bot')
|
||||
end
|
||||
|
||||
def repository_slug
|
||||
ENV.fetch('REPOSITORY_SLUG', 'openware/peatio')
|
||||
end
|
||||
|
||||
namespace 'release' do
|
||||
|
||||
desc 'Bump the version of the application'
|
||||
task :travis do
|
||||
unless ENV['TRAVIS_BRANCH'] == 'master' || ENV['TRAVIS_BRANCH'].match?(/^[0-9]+-[0-9]+-stable$/)
|
||||
Kernel.abort 'Bumping version aborted: GitHub pull request detected.'
|
||||
end
|
||||
|
||||
if ENV['TRAVIS_PULL_REQUEST'] != 'false'
|
||||
Kernel.abort 'Bumping version aborted: GitHub pull request detected.'
|
||||
end
|
||||
|
||||
unless ENV['TRAVIS_TAG'].to_s.empty?
|
||||
Kernel.abort 'Bumping version aborted: the build has been triggered by Git tag.'
|
||||
end
|
||||
|
||||
sh %(git config --global user.name 'OpenWare')
|
||||
sh %(git config --global user.email 'support@openware.com')
|
||||
sh %(git remote add authenticated-origin https://#{bot_username}:#{ENV.fetch('GITHUB_API_KEY')}@github.com/#{repository_slug})
|
||||
next_version = Bump::Bump.next_version('patch')
|
||||
sh %(V='#{next_version}' bin/gendocs)
|
||||
sh %(git add -A)
|
||||
Bump::Bump.run('patch', commit_message: '[skip ci]', tag: false)
|
||||
sh %(git tag #{Bump::Bump.current})
|
||||
sh %(git push --tags authenticated-origin HEAD:#{ENV.fetch('TRAVIS_BRANCH')})
|
||||
end
|
||||
end
|
||||
17
lib/tasks/revert.rake
Normal file
17
lib/tasks/revert.rake
Normal file
@@ -0,0 +1,17 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Usage
|
||||
# for revert all trading activity for particular user:
|
||||
# $> bundle exec rake revert:trading_activity["admin@barong.io"]
|
||||
namespace :revert do
|
||||
desc 'Revert user trade activity.'
|
||||
task :trading_activity, [:member_email] => [:environment] do |_, args|
|
||||
|
||||
member = Member.find_by(email: args[:member_email])
|
||||
|
||||
# For each trade create revert Liabilities, Revenues and update User balances
|
||||
# TODO: Add ability to revert particular trades.
|
||||
member.revert_trading_activity!(member.trades.order(id: :desc))
|
||||
end
|
||||
end
|
||||
143
lib/tasks/seed.rake
Normal file
143
lib/tasks/seed.rake
Normal file
@@ -0,0 +1,143 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
require 'yaml'
|
||||
|
||||
namespace :seed do
|
||||
desc 'Adds missing accounts to database defined at config/seed/accounts.yml.'
|
||||
task accounts: :environment do
|
||||
Operations::Account.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/accounts.yml')).each do |hash|
|
||||
next if Operations::Account.exists?(code: hash.fetch('code'))
|
||||
Operations::Account.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Deprecate seed tasks in favour of import:configs
|
||||
desc 'Adds missing currencies to database defined at config/seed/currencies.yml.'
|
||||
task currencies: :environment do
|
||||
Currency.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/currencies.yml')).each do |hash|
|
||||
next if Currency.exists?(id: hash.fetch('id'))
|
||||
Currency.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Adds missing blockchains to database defined at config/seed/blockchains.yml.'
|
||||
task blockchains: :environment do
|
||||
Blockchain.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/blockchains.yml')).each do |hash|
|
||||
next if Blockchain.exists?(key: hash.fetch('key'))
|
||||
Blockchain.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Adds missing engines to database defined at config/seed/engines.yml.'
|
||||
task engines: :environment do
|
||||
Engine.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/engines.yml')).each do |hash|
|
||||
next if Engine.exists?(name: hash.fetch('name'))
|
||||
Engine.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Adds missing markets to database defined at config/seed/markets.yml.'
|
||||
task markets: :environment do
|
||||
Market.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/markets.yml'))
|
||||
.map(&:symbolize_keys)
|
||||
.each do |hash|
|
||||
next if Market.exists?(id: hash.fetch(:id))
|
||||
# For compatibility with old markets.yml
|
||||
# If state is not defined set it from enabled.
|
||||
enabled = hash.delete(:enabled)
|
||||
hash[:state] ||= enabled ? :enabled : :disabled
|
||||
|
||||
# For compatibility with old markets.yml we keep legacy-new keys mapping.
|
||||
# New key value has higher priority than legacy.
|
||||
legacy_keys_mappings = { ask_unit: :base_unit,
|
||||
bid_unit: :quote_unit,
|
||||
ask_precision: :amount_precision,
|
||||
bid_precision: :price_precision,
|
||||
min_ask_price: :min_price,
|
||||
max_bid_price: :max_price,
|
||||
min_ask_amount: :min_amount,
|
||||
min_bid_amount: :min_amount }
|
||||
|
||||
legacy_keys_mappings.each do |old_key, new_key|
|
||||
legacy_key_value = hash.delete(old_key)
|
||||
hash[new_key] ||= legacy_key_value
|
||||
end
|
||||
|
||||
# Select engine with provided name
|
||||
engine = Engine.find_by(name: hash[:engine_name])
|
||||
if engine.present?
|
||||
hash.delete :engine_name
|
||||
hash[:engine_id] = engine.id
|
||||
Market.create!(hash)
|
||||
else
|
||||
Rails.logger.error "Engine doesn't exist"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Adds missing wallets to database defined at config/seed/wallets.yml.'
|
||||
task wallets: :environment do
|
||||
Wallet.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/wallets.yml')).each do |hash|
|
||||
next if Wallet.exists?(name: hash.fetch('name'))
|
||||
if hash['currency_ids'].is_a?(String)
|
||||
hash['currency_ids'] = hash['currency_ids'].split(',')
|
||||
end
|
||||
Wallet.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Adds missing trading_fees to database defined at config/seed/trading_fees.yml.'
|
||||
task trading_fees: :environment do
|
||||
TradingFee.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/trading_fees.yml')).each do |hash|
|
||||
next if TradingFee.exists?(market_id: hash.fetch('market_id'), group: hash.fetch('group'))
|
||||
TradingFee.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Adds missing whitelisted_smart_contracts to database defined at config/seed/whitelisted_smart_contracts.yml.'
|
||||
task whitelisted_smart_contracts: :environment do
|
||||
WhitelistedSmartContract.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/whitelisted_smart_contracts.yml')).each do |hash|
|
||||
next if WhitelistedSmartContract.exists?(address: hash.fetch('address'), blockchain_key: hash.fetch('blockchain_key'))
|
||||
|
||||
WhitelistedSmartContract.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Adds missing member for test'
|
||||
task members: :environment do
|
||||
Member.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/members.yml')).each do |hash|
|
||||
next if Member.exists?(id: hash.fetch('id'))
|
||||
|
||||
Member.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Adds deposit limits'
|
||||
task deposit_limits: :environment do
|
||||
::DepositLimit.transaction do
|
||||
YAML.load_file(Rails.root.join('config/seed/deposit_limit.yml')).each do |hash|
|
||||
next if ::DepositLimit.exists?(id: hash.fetch('id'))
|
||||
|
||||
::DepositLimit.create!(hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
10
lib/tasks/trade_taker_type.rake
Normal file
10
lib/tasks/trade_taker_type.rake
Normal file
@@ -0,0 +1,10 @@
|
||||
desc 'Select and update taker_type base on taker_order'
|
||||
task assign_taker_type: :environment do
|
||||
Trade.where(taker_type: '').find_in_batches do |batch|
|
||||
ActiveRecord::Base.transaction do
|
||||
batch.each do |t|
|
||||
t.update_attribute(:taker_type, t.taker_order.side) if t.taker_order.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
20
lib/tasks/trading_fee_test.rake
Normal file
20
lib/tasks/trading_fee_test.rake
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace :trading_fee_test do
|
||||
|
||||
task :amqp, [:config_load_path] => [:environment] do |_t, args|
|
||||
args.with_defaults(:config_load_path => 'config/bench/trade_execution.yml')
|
||||
benches =
|
||||
YAML.load_file(Rails.root.join(args[:config_load_path]))
|
||||
.map(&:deep_symbolize_keys)
|
||||
.each_with_object([]) do |config, memo|
|
||||
Kernel.pp config
|
||||
trade_execution = ::Bench::TradeExecution::AMQP.new(config)
|
||||
trade_execution.run_fee!
|
||||
memo << trade_execution
|
||||
trade_execution.save_report
|
||||
Kernel.puts "Sleep before next bench"
|
||||
sleep 5
|
||||
end
|
||||
|
||||
benches.each {|b| Kernel.pp b.result}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user