blockstream-satellite-api/models/orders.rb
Igor Freire aabdc11e24 Fix criteria for transitioning into received state
Since there can be multiple Tx confirmations for the same region, we should not
check if the number of Tx confirmations matches with the number of
regions. Instead, we should check if all regions have at least one Tx
confirmation.
2020-06-15 14:34:29 -03:00

179 lines
5.7 KiB
Ruby

require 'aasm'
require 'redis'
require 'json'
require_relative '../constants'
require_relative './invoices'
require_relative './tx_confirmations'
require_relative './rx_confirmations'
require_relative '../helpers/digest_helpers'
class Order < ActiveRecord::Base
include AASM
PUBLIC_FIELDS = [:uuid, :unpaid_bid, :bid, :bid_per_byte, :message_size, :message_digest, :status, :created_at, :started_transmission_at, :ended_transmission_at, :tx_seq_num]
@@redis = Redis.new(url: REDIS_URI)
enum status: [:pending, :paid, :transmitting, :sent, :received, :cancelled, :expired]
before_validation :adjust_bids
validates :bid, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :unpaid_bid, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :message_size, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: MIN_MESSAGE_SIZE }
validates :message_digest, presence: true
validates :bid_per_byte, numericality: { greater_than_or_equal_to: 0 }
validates :status, presence: true
validates :uuid, presence: true
has_many :invoices, after_add: :adjust_bids_and_save, after_remove: :adjust_bids_and_save
has_many :tx_confirmations, after_add: :maybe_mark_as_received
has_many :tx_regions, through: :tx_confirmations, source: :region
has_many :rx_confirmations, after_add: :maybe_mark_as_received
has_many :rx_regions, through: :rx_confirmations, source: :region
aasm :column => :status, :enum => true, :whiny_transitions => false, :no_direct_assignment => true do
state :pending, initial: true
state :paid
state :expired
state :transmitting, before_enter: Proc.new { self.started_transmission_at = Time.now }
state :sent, before_enter: Proc.new { self.ended_transmission_at = Time.now }
state :received
state :cancelled, before_enter: Proc.new { self.cancelled_at = Time.now }
event :pay do
transitions :from => :pending, :to => :paid, :guard => :paid_enough?
transitions :from => :paid, :to => :paid
end
event :transmit, :before => :assign_tx_seq_num, :after => :notify_transmissions_channel do
transitions :from => :paid, :to => :transmitting
end
event :end_transmission, :after => :notify_transmissions_channel do
transitions :from => :transmitting, :to => :sent
end
event :receive, :after => :synthesize_presumed_rx_confirmations do
transitions :from => :sent, :to => :received
end
event :cancel, :after => :delete_message_file do
transitions :from => [:pending, :paid], :to => :cancelled
end
event :bump do
transitions :from => :pending, :to => :pending
transitions :from => :paid, :to => :paid
end
event :expire, :after => :delete_message_file do
transitions :from => :pending, :to => :expired
end
end
def adjust_bids_and_save(invoice)
self.adjust_bids
self.save
end
def adjust_bids
self.bid = paid_invoices_total
self.bid_per_byte = (self.bid.to_f / self.message_size_with_overhead).round(2)
self.unpaid_bid = unpaid_invoices_total
end
def maybe_mark_as_paid
self.pay! if self.paid_enough?
end
def maybe_mark_as_received(confirmation)
self.receive! if self.received_criteria_met?
end
def received_criteria_met?
self.tx_regions.exists?(Region::REGIONS[:north_america].id) &&
self.tx_regions.exists?(Region::REGIONS[:south_america].id) &&
self.tx_regions.exists?(Region::REGIONS[:africa].id) &&
self.tx_regions.exists?(Region::REGIONS[:europe].id) &&
self.tx_regions.exists?(Region::REGIONS[:asia_c].id) &&
self.tx_regions.exists?(Region::REGIONS[:asia_ku].id) &&
self.rx_regions.exists?(Region::REGIONS[:north_america].id) &&
self.rx_regions.exists?(Region::REGIONS[:south_america].id) &&
self.rx_regions.exists?(Region::REGIONS[:asia_c].id) &&
self.rx_regions.exists?(Region::REGIONS[:asia_ku].id)
end
def synthesize_presumed_rx_confirmations
[:africa, :europe].each do |r|
RxConfirmation.create(order: self, region: Region::REGIONS[r], presumed: true)
end
end
def paid_enough?
self.adjust_bids
self.bid_per_byte >= MIN_PER_BYTE_BID
end
def message_size_with_overhead
n_frags = (self.message_size.to_f / MAX_BLOCKSAT_PAYLOAD).ceil
overhead = n_frags * L2_OVERHEAD
self.message_size.to_f + overhead
end
def paid_invoices_total
self.invoices.where(status: :paid).pluck(:amount).reduce(:+) || 0
end
def unpaid_invoices_total
self.pending_invoices.pluck(:amount).reduce(:+) || 0
end
def pending_invoices
self.invoices.where(status: :pending)
end
def expire_if_pending_and_no_pending_invoices
self.expire! if self.pending? and self.pending_invoices.empty?
end
def notify_transmissions_channel
@@redis.publish 'transmissions', self.to_json(:only => Order::PUBLIC_FIELDS)
end
def message_path
File.join(MESSAGE_STORE_PATH, self.uuid)
end
# have all invoices been paid?
def invoices_all_paid?
self.invoices.pluck(:paid_at).map {|i| not i.nil?}.reduce(:&)
end
USER_AUTH_KEY = hash_hmac('sha256', 'user-token', CHARGE_API_TOKEN)
def user_auth_token
hash_hmac('sha256', USER_AUTH_KEY, self.uuid)
end
def as_sanitized_json
self.to_json(:only => Order::PUBLIC_FIELDS)
end
def delete_message_file
File.delete(self.message_path) if File.file?(self.message_path)
end
# NB: no mutex is needed around max_tx_seq_num because it is assumed that there is only one transmitter
def assign_tx_seq_num
self.update(tx_seq_num: Order.max_tx_seq_num + 1)
end
def Order.max_tx_seq_num
Order.maximum(:tx_seq_num) || 0
end
# TODO return queue length, top bid, time to front, and other stats.
def self.stats
end
end