# encoding: UTF-8 # frozen_string_literal: true class Currency < ApplicationRecord # == Constants ============================================================ OPTIONS_ATTRIBUTES = %i[erc20_contract_address gas_limit gas_price].freeze TOP_POSITION = 1 # == Attributes =========================================================== attr_readonly :id, :type, :base_factor # Code is aliased to id because it's more user-friendly primary key. # It's preferred to use code where this attributes are equal. alias_attribute :code, :id # == Extensions =========================================================== serialize :options, JSON unless Rails.configuration.database_support_json include Helpers::ReorderPosition OPTIONS_ATTRIBUTES.each do |attribute| define_method attribute do self.options[attribute.to_s] end define_method "#{attribute}=".to_sym do |value| self.options = options.merge(attribute.to_s => value) end end # == Relationships ======================================================== belongs_to :blockchain, foreign_key: :blockchain_key, primary_key: :key has_and_belongs_to_many :wallets has_one :parent, class_name: 'Currency', foreign_key: :id, primary_key: :parent_id # == Validations ========================================================== validate on: :create do if ENV['MAX_CURRENCIES'].present? && Currency.count >= ENV['MAX_CURRENCIES'].to_i errors.add(:max, 'Currency limit has been reached') end end validates :code, presence: true, uniqueness: { case_sensitive: false } validates :position, presence: true, numericality: { greater_than_or_equal_to: TOP_POSITION, only_integer: true } validates :parent_id, allow_blank: true, inclusion: { in: ->(_) { Currency.coins_without_tokens.pluck(:id).map(&:to_s) } }, if: :coin? validates :blockchain_key, inclusion: { in: ->(_) { Blockchain.pluck(:key).map(&:to_s) } }, if: :coin? validates :type, inclusion: { in: ->(_) { Currency.types.map(&:to_s) } } validates :options, length: { maximum: 1000 } validates :base_factor, numericality: { greater_than_or_equal_to: 1, only_integer: true } validates :deposit_fee, :min_deposit_amount, :min_collection_amount, :withdraw_fee, :min_withdraw_amount, :withdraw_limit_24h, :withdraw_limit_72h, :precision, numericality: { greater_than_or_equal_to: 0 } # == Scopes =============================================================== scope :visible, -> { where(visible: true) } scope :irt, -> { find('irt')} scope :deposit_enabled, -> { where(deposit_enabled: true) } scope :withdrawal_enabled, -> { where(withdrawal_enabled: true) } scope :ordered, -> { order(position: :asc) } scope :coins, -> { where(type: :coin) } scope :fiats, -> { where(type: :fiat) } # This scope select all coins without parent_id, which means that they are not tokens scope :coins_without_tokens, -> { coins.where(parent_id: nil) } # == Callbacks ============================================================ after_initialize :initialize_defaults after_create do link_wallets insert_position(self) end before_validation { self.code = code.downcase } before_validation { self.deposit_fee = 0 unless fiat? } before_validation { self.blockchain_key = parent.blockchain_key if token? && blockchain_key.blank? } before_validation(on: :create) { self.position = Currency.count + 1 unless position.present? } before_validation do self.erc20_contract_address = erc20_contract_address.try(:downcase) if erc20_contract_address.present? end before_update { update_position(self) if position_changed? } after_commit :wipe_cache # == Class Methods ======================================================== # NOTE: type column reserved for STI self.inheritance_column = nil class << self def codes(options = {}) pluck(:id).yield_self do |downcase_codes| case when options.fetch(:bothcase, false) downcase_codes + downcase_codes.map(&:upcase) when options.fetch(:upcase, false) downcase_codes.map(&:upcase) else downcase_codes end end end def types %i[fiat coin].freeze end end # == Instance Methods ===================================================== delegate :explorer_transaction, :blockchain_api, :explorer_address, to: :blockchain types.each { |t| define_method("#{t}?") { type == t.to_s } } def blockchain Rails.cache.fetch("#{code}_blockchain", expires_in: 60) { Blockchain.find_by(key: blockchain_key) } end def wipe_cache Rails.cache.delete_matched("currencies*") end def initialize_defaults self.options = {} if options.blank? end def link_wallets if parent_id.present? # Iterate through active deposit/withdraw wallets Wallet.active.where.not(kind: :fee).with_currency(parent_id).each do |wallet| # Link parent currency with wallet CurrencyWallet.create(currency_id: id, wallet_id: wallet.id) end end end # Allows to dynamically check value of id/code: # # id.btc? # true if code equals to "btc". # code.eth? # true if code equals to "eth". def id super&.inquiry end # subunit (or fractional monetary unit) - a monetary unit # that is valued at a fraction (usually one hundredth) # of the basic monetary unit def subunits=(n) self.base_factor = 10 ** n end # This method defines that token currency need to have parent_id and coin type # We use parent_id for token type to inherit some useful info such as blockchain_key from parent currency # For coin currency enough to have only coin type def token? parent_id.present? && coin? end def get_price if price.blank? || price.zero? raise "Price for currency #{id} is unknown" else price end end def to_blockchain_api_settings # We pass options are available as top-level hash keys and via options for # compatibility with Wallet#to_wallet_api_settings. opt = options.compact.deep_symbolize_keys opt.deep_symbolize_keys.merge(id: id, base_factor: base_factor, options: opt) end def dependent_markets Market.where('base_unit = ? OR quote_unit = ?', id, id) end def subunits Math.log(base_factor, 10).round end end # == Schema Information # Schema version: 20201207134745 # # Table name: currencies # # id :string(10) not null, primary key # name :string(255) # description :text(65535) # homepage :string(255) # blockchain_key :string(32) # parent_id :string(255) # type :string(30) default("coin"), not null # deposit_fee :decimal(32, 16) default(0.0), not null # min_deposit_amount :decimal(32, 16) default(0.0), not null # min_collection_amount :decimal(32, 16) default(0.0), not null # withdraw_fee :decimal(32, 16) default(0.0), not null # min_withdraw_amount :decimal(32, 16) default(0.0), not null # withdraw_limit_24h :decimal(32, 16) default(0.0), not null # withdraw_limit_72h :decimal(32, 16) default(0.0), not null # position :integer not null # options :json # visible :boolean default(TRUE), not null # deposit_enabled :boolean default(TRUE), not null # withdrawal_enabled :boolean default(TRUE), not null # base_factor :bigint default(1), not null # precision :integer default(8), not null # icon_url :string(255) # price :decimal(32, 16) default(1.0), not null # created_at :datetime not null # updated_at :datetime not null # # Indexes # # index_currencies_on_parent_id (parent_id) # index_currencies_on_position (position) # index_currencies_on_visible (visible) #