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