mirror of
https://github.com/Blockstream/satellite-api.git
synced 2025-02-22 13:42:33 +01:00
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.
179 lines
5.7 KiB
Ruby
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
|