Files
Dena/app/services/wallet_service.rb
2026-08-13 19:50:53 +03:30

203 lines
8.3 KiB
Ruby

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