Initial commit

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

10
lib/api_bench.rb Normal file
View 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

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

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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,5 @@
class CSVFormatter
def self.call(object, env)
object.to_csv
end
end

View 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

View 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

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

View File

@@ -0,0 +1,3 @@
module Gnosis
class Client < Ethereum::Client; end
end

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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

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

View 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

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

View 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

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

View 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

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

View 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

View 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