Initial commit
This commit is contained in:
159
app/services/blockchain_service.rb
Normal file
159
app/services/blockchain_service.rb
Normal file
@@ -0,0 +1,159 @@
|
||||
class BlockchainService
|
||||
Error = Class.new(StandardError)
|
||||
BalanceLoadError = Class.new(StandardError)
|
||||
|
||||
attr_reader :blockchain, :whitelisted_smart_contract, :currencies, :adapter
|
||||
|
||||
def initialize(blockchain)
|
||||
@blockchain = blockchain
|
||||
@currencies = blockchain.currencies.deposit_enabled
|
||||
@whitelisted_addresses = blockchain.whitelisted_smart_contracts.active
|
||||
@adapter = Peatio::Blockchain.registry[blockchain.client.to_sym].new
|
||||
@adapter.configure(server: @blockchain.server,
|
||||
currencies: @currencies.map(&:to_blockchain_api_settings),
|
||||
whitelisted_addresses: @whitelisted_addresses)
|
||||
end
|
||||
|
||||
def latest_block_number
|
||||
@latest_block_number ||= @adapter.latest_block_number
|
||||
end
|
||||
|
||||
def load_balance!(address, currency_id)
|
||||
@adapter.load_balance_of_address!(address, currency_id)
|
||||
rescue Peatio::Blockchain::Error => e
|
||||
report_exception(e)
|
||||
raise BalanceLoadError
|
||||
end
|
||||
|
||||
def case_sensitive?
|
||||
@adapter.features[:case_sensitive]
|
||||
end
|
||||
|
||||
def supports_cash_addr_format?
|
||||
@adapter.features[:cash_addr_format]
|
||||
end
|
||||
|
||||
def fetch_transaction(transaction)
|
||||
tx = Peatio::Transaction.new(currency_id: transaction.currency_id,
|
||||
hash: transaction.txid,
|
||||
to_address: transaction.rid,
|
||||
amount: transaction.amount)
|
||||
if @adapter.respond_to?(:fetch_transaction)
|
||||
@adapter.fetch_transaction(tx)
|
||||
else
|
||||
tx
|
||||
end
|
||||
end
|
||||
|
||||
def process_block(block_number)
|
||||
block = @adapter.fetch_block!(block_number)
|
||||
deposits = filter_deposits(block)
|
||||
withdrawals = filter_withdrawals(block)
|
||||
# TODO: Process Transactions with `pending` status
|
||||
|
||||
accepted_deposits = []
|
||||
ActiveRecord::Base.transaction do
|
||||
accepted_deposits = deposits.map(&method(:update_or_create_deposit)).compact
|
||||
withdrawals.each(&method(:update_withdrawal))
|
||||
end
|
||||
accepted_deposits.each(&:process!)
|
||||
block
|
||||
end
|
||||
|
||||
# Resets current cached state.
|
||||
def reset!
|
||||
@latest_block_number = nil
|
||||
end
|
||||
|
||||
def update_height(block_number)
|
||||
raise Error, "#{blockchain.name} height was reset." if blockchain.height != blockchain.reload.height
|
||||
|
||||
# NOTE: We use update_column to not change updated_at timestamp
|
||||
# because we use it for detecting blockchain configuration changes see Workers::Daemon::Blockchain#run.
|
||||
blockchain.update_column(:height, block_number) if latest_block_number - block_number >= blockchain.min_confirmations
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_deposits(block)
|
||||
addresses = PaymentAddress.where(wallet: Wallet.deposit.with_currency(@currencies.codes), address: block.transactions.map(&:to_address)).pluck(:address)
|
||||
block.select { |transaction| transaction.to_address.in?(addresses) }
|
||||
end
|
||||
|
||||
def filter_withdrawals(block)
|
||||
# TODO: Process addresses in batch in case of huge number of confirming withdrawals.
|
||||
withdraw_txids = Withdraws::Coin.confirming.where(currency: @currencies).pluck(:txid)
|
||||
block.select { |transaction| transaction.hash.in?(withdraw_txids) }
|
||||
end
|
||||
|
||||
def update_or_create_deposit(transaction)
|
||||
if transaction.amount < Currency.find(transaction.currency_id).min_deposit_amount
|
||||
# Currently we just skip tiny deposits.
|
||||
Rails.logger.info do
|
||||
"Skipped deposit with txid: #{transaction.hash} with amount: #{transaction.hash}"\
|
||||
" to #{transaction.to_address} in block number #{transaction.block_number}"
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
# Fetch transaction from a blockchain that has `pending` status.
|
||||
transaction = adapter.fetch_transaction(transaction) if @adapter.respond_to?(:fetch_transaction) && transaction.status.pending?
|
||||
return unless transaction.status.success?
|
||||
|
||||
address = PaymentAddress.find_by(wallet: Wallet.deposit_wallet(transaction.currency_id), address: transaction.to_address)
|
||||
return if address.blank?
|
||||
|
||||
# Skip deposit tx if there is tx for deposit collection process
|
||||
# TODO: select only pending transactions
|
||||
tx_collect = Transaction.where(txid: transaction.hash, reference_type: 'Deposit')
|
||||
return if tx_collect.present?
|
||||
|
||||
if transaction.from_addresses.blank? && adapter.respond_to?(:transaction_sources)
|
||||
transaction.from_addresses = adapter.transaction_sources(transaction)
|
||||
end
|
||||
|
||||
deposit =
|
||||
Deposits::Coin.find_or_create_by!(
|
||||
currency_id: transaction.currency_id,
|
||||
txid: transaction.hash,
|
||||
txout: transaction.txout
|
||||
) do |d|
|
||||
d.address = transaction.to_address
|
||||
d.amount = transaction.amount
|
||||
d.member = address.member
|
||||
d.from_addresses = transaction.from_addresses
|
||||
d.block_number = transaction.block_number
|
||||
end
|
||||
|
||||
deposit.update_column(:block_number, transaction.block_number) if deposit.block_number != transaction.block_number
|
||||
# Manually calculating deposit confirmations, because blockchain height is not updated yet.
|
||||
if latest_block_number - deposit.block_number >= @blockchain.min_confirmations && deposit.accept!
|
||||
deposit
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def update_withdrawal(transaction)
|
||||
withdrawal =
|
||||
Withdraws::Coin.confirming
|
||||
.find_by(currency_id: transaction.currency_id, txid: transaction.hash)
|
||||
|
||||
# Skip non-existing in database withdrawals.
|
||||
if withdrawal.blank?
|
||||
Rails.logger.info { "Skipped withdrawal: #{transaction.hash}." }
|
||||
return
|
||||
end
|
||||
|
||||
withdrawal.update_column(:block_number, transaction.block_number)
|
||||
|
||||
# Fetch transaction from a blockchain that has `pending` status.
|
||||
transaction = adapter.fetch_transaction(transaction) if @adapter.respond_to?(:fetch_transaction) && transaction.status.pending?
|
||||
# Manually calculating withdrawal confirmations, because blockchain height is not updated yet.
|
||||
if transaction.status.failed?
|
||||
withdrawal.fail!
|
||||
elsif transaction.status.success? && latest_block_number - withdrawal.block_number >= @blockchain.min_confirmations
|
||||
withdrawal.success!
|
||||
end
|
||||
end
|
||||
end
|
||||
80
app/services/k_line_service.rb
Normal file
80
app/services/k_line_service.rb
Normal file
@@ -0,0 +1,80 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'peatio/influxdb'
|
||||
class KLineService
|
||||
POINT_PERIOD_IN_SECONDS = 60
|
||||
|
||||
# Point period units are calculated in POINT_PERIOD_IN_SECONDS.
|
||||
# It means that period with value 5 is equal to 5 minutes (5 * POINT_PERIOD_IN_SECONDS = 300).
|
||||
AVAILABLE_POINT_PERIODS = [1, 5, 15, 30, 60, 120, 240, 360, 720, 1440, 4320, 10_080].freeze
|
||||
|
||||
AVAILABLE_POINT_LIMITS = (1..10_000).freeze
|
||||
|
||||
HUMANIZED_POINT_PERIODS = {
|
||||
1 => '1m', 5 => '5m', 15 => '15m', 30 => '30m', # minutes
|
||||
60 => '1h', 120 => '2h', 240 => '4h', 360 => '6h', 720 => '12h', # hours
|
||||
1440 => '1d', 4320 => '3d', # days
|
||||
10_080 => '1w' # weeks
|
||||
}.freeze
|
||||
|
||||
class << self
|
||||
def [](market, period)
|
||||
services[[market, period]] ||= new(market, period)
|
||||
end
|
||||
|
||||
def services
|
||||
@services ||= {}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
attr_accessor :market_id, :period
|
||||
|
||||
def initialize(marked_id, period)
|
||||
@market_id = marked_id
|
||||
@period = humanize_period(period)
|
||||
end
|
||||
|
||||
# OHCL - open, high, closing, and low prices.
|
||||
def get_ohlc(options = {})
|
||||
options = options.symbolize_keys.tap do |o|
|
||||
o.delete(:limit) if o[:time_from].present? && o[:time_to].present?
|
||||
end
|
||||
|
||||
time_from = options[:time_from]
|
||||
time_to = options[:time_to]
|
||||
offset = calculate_offset(options) if time_from.blank?
|
||||
|
||||
q = ["SELECT * FROM candles_#{@period} WHERE market='#{@market_id}'"]
|
||||
q << "AND time >= #{time_from.to_i * 1_000_000_000}" if time_from.present?
|
||||
q << "AND time <= #{time_to.to_i * 1_000_000_000}" if time_to.present?
|
||||
q << "ORDER BY #{options[:order_by]}" if options[:order_by]
|
||||
q << "LIMIT #{options[:limit]}" if options[:limit]
|
||||
q << "OFFSET #{offset}" if offset.present? && options[:offset]
|
||||
|
||||
Peatio::InfluxDB.client(keyshard: @market_id, epoch: 's').query(q.join(' ')) do |_name, _tags, points|
|
||||
return points.map do |point|
|
||||
[point['time'], point['open'], point['high'], point['low'], point['close'], point['volume']]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_offset(options)
|
||||
q = ["SELECT COUNT(high) FROM candles_#{@period} WHERE market='#{@market_id}'"]
|
||||
q << "AND time <= #{options[:time_to].to_i * 1_000_000_000}" if options[:time_to].present?
|
||||
Peatio::InfluxDB.client(keyshard: @market_id, epoch: 's').query(q.join(' ')) do |_, _, values|
|
||||
return options[:limit].to_i < values.first['count'] ? values.first['count'] - options[:limit] : 0
|
||||
end
|
||||
end
|
||||
|
||||
def event_name(period)
|
||||
"kline-#{humanize_period(period)}"
|
||||
end
|
||||
|
||||
def humanize_period(period)
|
||||
HUMANIZED_POINT_PERIODS.fetch(period) do
|
||||
raise StandardError, "Not available period #{period}"
|
||||
end
|
||||
end
|
||||
end
|
||||
44
app/services/services/health_checker.rb
Normal file
44
app/services/services/health_checker.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Services
|
||||
module HealthChecker
|
||||
LIVENESS_CHECKS = %i[check_db check_redis check_rabbitmq].freeze
|
||||
READINESS_CHECKS = %i[check_db].freeze
|
||||
|
||||
class << self
|
||||
def alive?
|
||||
check! LIVENESS_CHECKS
|
||||
rescue StandardError => e
|
||||
report_exception_to_screen(e)
|
||||
false
|
||||
end
|
||||
|
||||
def ready?
|
||||
check! READINESS_CHECKS
|
||||
rescue StandardError => e
|
||||
report_exception_to_screen(e)
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check!(checks)
|
||||
checks.all? { |m| send(m) }
|
||||
end
|
||||
|
||||
def check_db
|
||||
Market.count
|
||||
Market.connected?
|
||||
end
|
||||
|
||||
def check_redis
|
||||
Rails.cache.redis.ping == 'PONG'
|
||||
end
|
||||
|
||||
def check_rabbitmq
|
||||
Bunny.run(AMQP::Config.connect) { |c| c.connected? }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
62
app/services/tickers_service.rb
Normal file
62
app/services/tickers_service.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'peatio/influxdb'
|
||||
class TickersService
|
||||
ZERO = '0.0'.to_d
|
||||
|
||||
class << self
|
||||
def [](market)
|
||||
services[market] ||= new(market)
|
||||
end
|
||||
|
||||
def services
|
||||
@services ||= {}
|
||||
end
|
||||
end
|
||||
|
||||
attr_accessor :market_id
|
||||
|
||||
def initialize(market)
|
||||
if market.is_a? Market
|
||||
@market_id = market.id
|
||||
else
|
||||
@market_id = market.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def ticker
|
||||
ticker = Trade.market_ticker_from_influx(market_id)
|
||||
format(ticker)
|
||||
end
|
||||
|
||||
def default_ticker
|
||||
{ min: ZERO, max: ZERO, last: ZERO, first: ZERO, volume: ZERO, amount: ZERO, vwap: ZERO }
|
||||
end
|
||||
|
||||
def format(ticker)
|
||||
if ticker.blank?
|
||||
ticker = default_ticker
|
||||
last_trade = Trade.public_from_influx(market_id, 1).first
|
||||
ticker[:last] = last_trade[:price] if last_trade.present?
|
||||
end
|
||||
|
||||
{
|
||||
at: Time.now.to_i,
|
||||
avg_price: ticker[:vwap].to_d,
|
||||
high: ticker[:max].to_d,
|
||||
last: ticker[:last].to_d,
|
||||
low: ticker[:min].to_d,
|
||||
open: ticker[:first].to_d,
|
||||
price_change_percent: change_ratio(ticker[:first].to_d, ticker[:last].to_d),
|
||||
volume: ticker[:volume].to_d,
|
||||
amount: ticker[:amount].to_d
|
||||
}.transform_values(&:to_s)
|
||||
end
|
||||
|
||||
def change_ratio(open, last)
|
||||
percent = open.zero? ? 0 : (last - open) / open * 100
|
||||
|
||||
# Prepend sign. Show two digits after the decimal point. Append '%'.
|
||||
"#{'%+.2f' % percent}%"
|
||||
end
|
||||
end
|
||||
155
app/services/vandar_service.rb
Normal file
155
app/services/vandar_service.rb
Normal file
@@ -0,0 +1,155 @@
|
||||
# vandar is payment proxy
|
||||
module Vandar
|
||||
# data keys are:
|
||||
# api_key string required
|
||||
# amount Integer required
|
||||
# callback_url String required
|
||||
# mobile_number String optional
|
||||
# factorNumber String optional
|
||||
# description String optional
|
||||
# valid_card_number String optional
|
||||
module Deposit
|
||||
def generate_token(data)
|
||||
url = 'https://ipg.vandar.io/api/v3/send'
|
||||
# POST JSON content
|
||||
data.merge!(api_key: VandarService::API_KEY)
|
||||
parse Faraday.post(url, data.to_json, 'Content-Type' => 'application/json')
|
||||
end
|
||||
|
||||
# step two
|
||||
# handle by ui, open payment page by token
|
||||
# METHOD: get
|
||||
# URL: https://ipg.vandar.io/v3/{token}
|
||||
|
||||
# step three
|
||||
def transaction(token)
|
||||
url = 'https://vandar.io/api/ipg/2step/transaction'
|
||||
data = { api_key: VandarService::API_KEY, token: token }
|
||||
# POST JSON content
|
||||
parse Faraday.post(url, data.to_json, 'Content-Type' => 'application/json')
|
||||
end
|
||||
|
||||
# step four
|
||||
def verify(token)
|
||||
url = 'https://ipg.vandar.io/api/v3/verify'
|
||||
data = { api_key: VandarService::API_KEY, token: token }
|
||||
# POST JSON content
|
||||
parse Faraday.post(url, data.to_json, 'Content-Type' => 'application/json')
|
||||
end
|
||||
end
|
||||
|
||||
# vandar withdraw fiat
|
||||
module Withdraw
|
||||
def list_withdraw
|
||||
url = "https://api.vandar.io/v2.1/business/#{VandarService::BUSINESS_NAME}/settlement"
|
||||
headers = { 'Content-Type' => 'application/json', 'authorization' => "Bearer #{@login_token}" }
|
||||
Faraday.get(url, nil, headers).body
|
||||
end
|
||||
|
||||
def create_withdraw(data)
|
||||
# amount: تومان و بزرگتر یا مساوی 5000
|
||||
url = "https://api.vandar.io/v3/business/#{VandarService::BUSINESS_NAME}/settlement/store"
|
||||
headers = { 'Content-Type' => 'application/json', 'authorization' => "Bearer #{@login_token}" }
|
||||
|
||||
# POST JSON content
|
||||
data.merge!(track_id: ::SecureRandom.uuid)
|
||||
self.class.parse Faraday.post(url, data.to_json, headers)
|
||||
end
|
||||
|
||||
def info_withdraw(id)
|
||||
url = "https://api.vandar.io/v2.1/business/#{VandarService::BUSINESS_NAME}/settlement/#{id}"
|
||||
headers = { 'Content-Type' => 'application/json', 'authorization' => "Bearer #{@login_token}" }
|
||||
self.class.parse Faraday.get(url, nil, headers)
|
||||
end
|
||||
|
||||
def delete_withdraw
|
||||
url = "https://api.vandar.io/v2.1/business/#{VandarService::BUSINESS_NAME}/settlement/{transaction_id}"
|
||||
headers = { 'Content-Type' => 'application/json', 'authorization' => "Bearer #{@login_token}" }
|
||||
self.class.parse Faraday.delete(url, nil, headers)
|
||||
end
|
||||
end
|
||||
|
||||
# for login in Vandar Account
|
||||
module Login
|
||||
MOBILE = ENV.fetch('VANDAR_MOBILE') || raise('please set Vandar mobile')
|
||||
PASSWORD = ENV.fetch('VANDAR_PASSWORD') || raise('please set Vandar password')
|
||||
def login
|
||||
return @login_token if @login_token.present?
|
||||
|
||||
url = 'https://api.vandar.io/v3/login'
|
||||
# POST JSON content
|
||||
data = { mobile: MOBILE, password: PASSWORD }
|
||||
response = self.class.parse Faraday.post(url, data.to_json, 'Content-Type' => 'application/json')
|
||||
@login_token = response.dig('access_token') || raise('login in vandar accrued error')
|
||||
::Rails.cache.write('vandar_login_token', @login_token, expires_in: 345_600)
|
||||
@login_token
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# related to Vandar transactions
|
||||
module Transaction
|
||||
# Name Type Status
|
||||
# fromDate String optional
|
||||
# toDate String optional
|
||||
# statusKind String optional
|
||||
# status String optional
|
||||
# channel String optional
|
||||
# formId String optional
|
||||
# ref_id String optional
|
||||
# tracking_code String optional
|
||||
# per_page String optional
|
||||
# q String optional
|
||||
#
|
||||
#
|
||||
# status
|
||||
# 1: نشان دهنده تراکنش با وضعیت موفق
|
||||
# - 1: نشان دهنده تراکنش با وضعیت ناموفق
|
||||
# 3 : نشان دهنده تراکنش با وضعیت درحال انجام
|
||||
# 2 : نشان دهنده تسویه با وضعیت موفق
|
||||
# -2 : نشان دهنده تسویه با وضعیت درحال انجام
|
||||
# -3 : نشان دهنده تسویه با وضعیت ناموفق
|
||||
# -4 : نشان دهنده تسویه با وضعیت لغو شده
|
||||
# 5 : نشان دهنده تراکنش واریزی از نوع انتقال وجه داخلی
|
||||
# -5 : نشان دهنده تراکنش خروجی از نوع انتقال وجه داخلی
|
||||
# 6 : نشان دهنده تراکنش نوع حق اشتراک
|
||||
|
||||
def list_transactions(**filters)
|
||||
url = "https://api.vandar.io/v2/business/#{VandarService::BUSINESS_NAME}/transaction"
|
||||
headers = { 'Content-Type' => 'application/json', 'authorization' => "Bearer #{@login_token}" }
|
||||
response = Faraday.get(url, filters, headers)
|
||||
self.class.parse response
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class VandarService
|
||||
API_KEY = ENV.fetch('VANDAR_API_KEY') || raise('please set Vandar Api_Key')
|
||||
BUSINESS_NAME = ENV.fetch('VANDAR_BUSINESS_NAME') || 'developers'
|
||||
SUCCESS_WITHDRAW = [2].freeze
|
||||
UNSUCCESS_WITHDRAW = [-3, -4].freeze
|
||||
|
||||
attr_accessor :login_token
|
||||
|
||||
def initialize
|
||||
@login_token = ::Rails.cache.read('vandar_login_token') || login
|
||||
end
|
||||
|
||||
extend Vandar::Deposit
|
||||
include Vandar::Login
|
||||
include Vandar::Withdraw
|
||||
include Vandar::Transaction
|
||||
|
||||
class << self
|
||||
# # step one
|
||||
def parse(response)
|
||||
raise 'response is empty' unless response.present?
|
||||
raise "unexpected response status #{response.status}" unless response.status == 200
|
||||
|
||||
JSON.parse(response.body)
|
||||
end
|
||||
end
|
||||
end
|
||||
202
app/services/wallet_service.rb
Normal file
202
app/services/wallet_service.rb
Normal file
@@ -0,0 +1,202 @@
|
||||
class WalletService
|
||||
attr_reader :wallet, :adapter
|
||||
|
||||
def initialize(wallet)
|
||||
@wallet = wallet
|
||||
@adapter = Peatio::Wallet.registry[wallet.gateway.to_sym].new(wallet.settings.symbolize_keys)
|
||||
end
|
||||
|
||||
def create_address!(uid, pa_details)
|
||||
@adapter.configure(wallet: @wallet.to_wallet_api_settings,
|
||||
currency: @wallet.currencies.first.to_blockchain_api_settings)
|
||||
# uid is member uid
|
||||
@adapter.create_address!(uid: uid, pa_details: pa_details)
|
||||
end
|
||||
|
||||
def build_withdrawal!(withdrawal)
|
||||
@adapter.configure(wallet: @wallet.to_wallet_api_settings,
|
||||
currency: withdrawal.currency.to_blockchain_api_settings)
|
||||
transaction = Peatio::Transaction.new(to_address: withdrawal.rid,
|
||||
amount: withdrawal.amount,
|
||||
currency_id: withdrawal.currency_id,
|
||||
options: { tid: withdrawal.tid })
|
||||
transaction = @adapter.create_transaction!(transaction)
|
||||
save_transaction(transaction.as_json.merge(from_address: @wallet.address), withdrawal) if transaction.present?
|
||||
transaction
|
||||
end
|
||||
|
||||
def spread_deposit(deposit)
|
||||
@adapter.configure(wallet: @wallet.to_wallet_api_settings,
|
||||
currency: deposit.currency.to_blockchain_api_settings)
|
||||
|
||||
destination_wallets =
|
||||
Wallet.active.withdraw.ordered
|
||||
.joins(:currencies).where(currencies: { id: deposit.currency_id })
|
||||
.map do |w|
|
||||
# NOTE: Consider min_collection_amount is defined per wallet.
|
||||
# For now min_collection_amount is currency config.
|
||||
{ address: w.address,
|
||||
balance: w.current_balance(deposit.currency),
|
||||
# Wallet max_balance will be in the platform currency
|
||||
max_balance: (w.max_balance / deposit.currency.get_price.to_d).round(deposit.currency.precision, BigDecimal::ROUND_DOWN),
|
||||
min_collection_amount: deposit.currency.min_collection_amount,
|
||||
skip_deposit_collection: w.service.skip_deposit_collection? }
|
||||
end
|
||||
raise StandardError, "destination wallets don't exist" if destination_wallets.blank?
|
||||
|
||||
# Since last wallet is considered to be the most secure we need always
|
||||
# have it in spread even if we don't know the balance.
|
||||
# All money which doesn't fit to other wallets will be collected to cold.
|
||||
# That is why cold wallet balance is considered to be 0 because there is no
|
||||
destination_wallets.last[:balance] = 0
|
||||
|
||||
# Remove all wallets not available current balance
|
||||
# (except the last one see previous comment).
|
||||
destination_wallets.reject! { |dw| dw[:balance] == Wallet::NOT_AVAILABLE }
|
||||
|
||||
spread_between_wallets(deposit, destination_wallets)
|
||||
end
|
||||
|
||||
# TODO: We don't need deposit_spread anymore.
|
||||
def collect_deposit!(deposit, deposit_spread)
|
||||
@adapter.configure(wallet: @wallet.to_wallet_api_settings,
|
||||
currency: deposit.currency.to_blockchain_api_settings)
|
||||
pa = deposit.member.payment_address(@wallet.id)
|
||||
# NOTE: Deposit wallet configuration is tricky because wallet UIR
|
||||
# is saved on Wallet model but wallet address and secret
|
||||
# are saved in PaymentAddress.
|
||||
@adapter.configure(
|
||||
wallet: @wallet.to_wallet_api_settings
|
||||
.merge(pa.details.symbolize_keys)
|
||||
.merge(address: pa.address)
|
||||
.tap { |s| s.merge!(secret: pa.secret) if pa.secret.present? }
|
||||
.compact
|
||||
)
|
||||
|
||||
deposit_spread.map do |transaction|
|
||||
# In #spread_deposit valid transactions saved with pending state
|
||||
if transaction.status.pending?
|
||||
transaction = @adapter.create_transaction!(transaction, subtract_fee: true)
|
||||
save_transaction(transaction.as_json.merge(from_address: deposit.address), deposit) if transaction.present?
|
||||
end
|
||||
transaction
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: We don't need deposit_spread anymore.
|
||||
def deposit_collection_fees!(deposit, deposit_spread)
|
||||
@adapter.configure(wallet: @wallet.to_wallet_api_settings,
|
||||
currency: deposit.currency.to_blockchain_api_settings)
|
||||
deposit_transaction = Peatio::Transaction.new(hash: deposit.txid,
|
||||
txout: deposit.txout,
|
||||
to_address: deposit.address,
|
||||
block_number: deposit.block_number,
|
||||
amount: deposit.amount)
|
||||
|
||||
transactions = @adapter.prepare_deposit_collection!(deposit_transaction,
|
||||
# In #spread_deposit valid transactions saved with pending state
|
||||
deposit_spread.select { |t| t.status.pending? },
|
||||
deposit.currency.to_blockchain_api_settings)
|
||||
|
||||
if transactions.present?
|
||||
deposit.update(spread: deposit.spread.map { |s| s.merge(options: transactions.first.options) })
|
||||
transactions.each { |t| save_transaction(t.as_json.merge(from_address: @wallet.address), deposit) }
|
||||
end
|
||||
transactions
|
||||
end
|
||||
|
||||
def refund!(refund)
|
||||
refund_transaction = Peatio::Transaction.new(to_address: refund.address,
|
||||
amount: refund.deposit.amount)
|
||||
|
||||
@adapter.create_transaction!(refund_transaction, subtract_fee: true)
|
||||
end
|
||||
|
||||
def load_balance!(currency)
|
||||
@adapter.configure(wallet: @wallet.to_wallet_api_settings,
|
||||
currency: currency)
|
||||
@adapter.load_balance!
|
||||
rescue Peatio::Wallet::Error => e
|
||||
report_exception(e)
|
||||
BlockchainService.new(wallet.blockchain).load_balance!(@wallet.address, currency.id)
|
||||
end
|
||||
|
||||
def register_webhooks!(url)
|
||||
@adapter.register_webhooks!(url)
|
||||
end
|
||||
|
||||
def fetch_transfer!(id)
|
||||
@adapter.fetch_transfer!(id)
|
||||
end
|
||||
|
||||
def trigger_webhook_event(event)
|
||||
currency = Currency.find(event[:coin])
|
||||
@adapter.configure(wallet: @wallet.to_wallet_api_settings,
|
||||
currency: currency.to_blockchain_api_settings)
|
||||
@adapter.trigger_webhook_event(event)
|
||||
end
|
||||
|
||||
def skip_deposit_collection?
|
||||
@adapter.features[:skip_deposit_collection]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @return [Array<Peatio::Transaction>] result of spread in form of
|
||||
# transactions array with amount and to_address defined.
|
||||
def spread_between_wallets(deposit, destination_wallets)
|
||||
original_amount = deposit.amount
|
||||
return [] if original_amount < destination_wallets.pluck(:min_collection_amount).min
|
||||
|
||||
left_amount = original_amount
|
||||
|
||||
spread = destination_wallets.map do |dw|
|
||||
amount_for_wallet = [dw[:max_balance] - dw[:balance], left_amount].min
|
||||
|
||||
# If free amount in current wallet is too small,
|
||||
# we will not able to collect it.
|
||||
# Put 0 for this wallet.
|
||||
amount_for_wallet = 0 if amount_for_wallet < [dw[:min_collection_amount], 0].max
|
||||
|
||||
left_amount -= amount_for_wallet
|
||||
|
||||
# If amount left is too small we will not able to collect it.
|
||||
# So we collect everything to current wallet.
|
||||
if left_amount < dw[:min_collection_amount]
|
||||
amount_for_wallet += left_amount
|
||||
left_amount = 0
|
||||
end
|
||||
|
||||
transaction = Peatio::Transaction.new(to_address: dw[:address],
|
||||
amount: amount_for_wallet.to_d,
|
||||
currency_id: deposit.currency_id)
|
||||
|
||||
# Tx will not be collected to this destination wallet
|
||||
transaction.status = :skipped if dw[:skip_deposit_collection]
|
||||
transaction
|
||||
rescue => e
|
||||
# If have exception skip wallet.
|
||||
report_exception(e)
|
||||
end
|
||||
|
||||
if left_amount.positive?
|
||||
# If deposit doesn't fit to any wallet, collect it to the last one.
|
||||
# Since the last wallet is considered to be the most secure.
|
||||
spread.last.amount += left_amount
|
||||
left_amount = 0
|
||||
end
|
||||
|
||||
# Remove zero and skipped transactions from spread.
|
||||
spread.filter { |t| t.amount > 0 }.tap do |sp|
|
||||
unless sp.map(&:amount).sum == original_amount
|
||||
raise Error, "Deposit spread failed deposit.amount != collection_spread.values.sum"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Record blockchain transactions in DB
|
||||
def save_transaction(transaction, reference)
|
||||
transaction['txid'] = transaction.delete('hash')
|
||||
Transaction.create!(transaction.merge(reference: reference))
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user