module StraightServer::GatewayModule

This module contains common features of Gateway, later to be included in one of the classes below.

Constants

CALLBACK_URL_ATTEMPT_TIMEFRAME

Public Instance Methods

add_websocket_for_order(ws, order) click to toggle source
# File lib/straight-server/gateway.rb, line 177
def add_websocket_for_order(ws, order)
  raise WebsocketExists            unless websockets[order.id].nil?
  raise WebsocketForCompletedOrder unless order.status < 2
  StraightServer.logger.info "Opening ws connection for #{order.id}"
  ws.on(:close) do |event|
    websockets.delete(order.id)
    StraightServer.logger.info "Closing ws connection for #{order.id}"
  end
  websockets[order.id] = ws
  ws
end
create_order(attrs={}) click to toggle source

Creates a new order and saves into the DB. Checks if the MD5 hash is correct first.

# File lib/straight-server/gateway.rb, line 123
def create_order(attrs={})

  raise GatewayInactive unless self.active

  StraightServer.logger.info "Creating new order with attrs: #{attrs}"

  # If we decide to reuse the order, we simply need to supply the
  # keychain_id that was used in the order we're reusing.
  # The address will be generated correctly.
  if reused_order = find_reusable_order
    attrs[:keychain_id] = reused_order.keychain_id

  end
  
  attrs[:keychain_id] = nil if attrs[:keychain_id] == ''


  order = new_order(
    amount:           (attrs[:amount] && attrs[:amount].to_f),
    keychain_id:      attrs[:keychain_id] || get_next_last_keychain_id,
    currency:         attrs[:currency],
    btc_denomination: attrs[:btc_denomination]
  )

  order.id            = attrs[:id].to_i       if attrs[:id]
  order.data          = attrs[:data]          if attrs[:data]
  order.callback_data = attrs[:callback_data] if attrs[:callback_data]
  order.title         = attrs[:title]         if attrs[:title]
  order.callback_url  = attrs[:callback_url]  if attrs[:callback_url]
  order.gateway       = self
  order.test_mode     = test_mode
  order.description   = attrs[:description]
  order.reused        = reused_order.reused + 1 if reused_order
  order.save

  self.update_last_keychain_id(attrs[:keychain_id]) unless order.reused > 0
  self.save
  StraightServer.logger.info "Order #{order.id} created: #{order.to_h}"
  order
end
fetch_transactions_for(address) click to toggle source

END OF Initializers methods ##################################################

Calls superclass method
# File lib/straight-server/gateway.rb, line 114
def fetch_transactions_for(address)
  super
rescue Straight::Blockchain::Adapter::BitcoinAddressInvalid => e
  StraightServer.logger.warn "Address seems to be invalid, ignoring it. #{e.message}"
  return []
end
find_reusable_order() click to toggle source

If we have more than Config.reuse_address_orders_threshold i a row for this gateway, this method returns the one which keychain_id (and, consequently, address) is to be reused. It also checks (just in case) if any transactions has been made to the addres-to-be-reused, because even though the order itself might be expired, the address might have been used for something else.

If there were transactions to it, there's actually no need to reuse the address and we can safely return nil.

Also, see comments for find_expired_orders_row method.

# File lib/straight-server/gateway.rb, line 251
def find_reusable_order
  expired_orders = find_expired_orders_row
  if expired_orders.size >= Config.reuse_address_orders_threshold &&
  fetch_transactions_for(expired_orders.last.address).empty?
    return expired_orders.last
  end
  nil
end
get_next_last_keychain_id() click to toggle source
# File lib/straight-server/gateway.rb, line 164
def get_next_last_keychain_id
  self.test_mode ? self.test_last_keychain_id + 1 : self.last_keychain_id + 1
end
get_order_counter(counter_name) click to toggle source
# File lib/straight-server/gateway.rb, line 231
def get_order_counter(counter_name)
  raise OrderCountersDisabled unless StraightServer::Config.count_orders
  StraightServer.redis_connection.get("#{StraightServer::Config.redis[:prefix]}:gateway_#{id}:#{counter_name}_orders_counter").to_i || 0
end
increment_order_counter!(counter_name, by=1) click to toggle source
# File lib/straight-server/gateway.rb, line 236
def increment_order_counter!(counter_name, by=1)
  raise OrderCountersDisabled unless StraightServer::Config.count_orders
  StraightServer.redis_connection.incrby("#{StraightServer::Config.redis[:prefix]}:gateway_#{id}:#{counter_name}_orders_counter", by)
end
initialize_blockchain_adapters() click to toggle source
# File lib/straight-server/gateway.rb, line 72
def initialize_blockchain_adapters
  @blockchain_adapters = []
  StraightServer::Config.blockchain_adapters.each do |a|

    adapter = Straight::Blockchain.const_get("#{a}Adapter")
    next unless adapter
    begin
      main_url = StraightServer::Config.__send__("#{a.downcase}_url") rescue next
      test_url = StraightServer::Config.__send__("#{a.downcase}_test_url") rescue nil
      @blockchain_adapters << adapter.mainnet_adapter(main_url: main_url, test_url: test_url)
    rescue ArgumentError
      @blockchain_adapters << adapter.mainnet_adapter
    end
  end
  raise NoBlockchainAdapters if @blockchain_adapters.empty?
end
initialize_callbacks() click to toggle source
# File lib/straight-server/gateway.rb, line 89
def initialize_callbacks
  # When the status of an order changes, we send an http request to the callback_url
  # and also notify a websocket client (if present, of course).
  @order_callbacks = [
    lambda do |order|
      StraightServer::Thread.new do
        send_order_to_websocket_client order
      end
      StraightServer::Thread.new do
        send_callback_http_request order
      end
    end
  ]
end
initialize_exchange_rate_adapters() click to toggle source
Initializers methods ########################################################

We have separate methods, because with GatewayOnDB they are called from after_initialize but in GatewayOnConfig they are called from initialize intself. #########################################################################################

# File lib/straight-server/gateway.rb, line 59
def initialize_exchange_rate_adapters
  @exchange_rate_adapters ||= []
  if self.exchange_rate_adapter_names.kind_of?(Array) && self.exchange_rate_adapter_names
    self.exchange_rate_adapter_names.each do |adapter|
      begin
        @exchange_rate_adapters << Straight::ExchangeRate.const_get("#{adapter}Adapter").instance
      rescue NameError => e
        puts "WARNING: No exchange rate adapter with the name #{adapter} was found!"
      end
    end
  end
end
initialize_network() click to toggle source
# File lib/straight-server/gateway.rb, line 108
def initialize_network
  BTC::Network.default = test_mode ? BTC::Network.testnet : BTC::Network.mainnet
end
initialize_status_check_schedule() click to toggle source
# File lib/straight-server/gateway.rb, line 104
def initialize_status_check_schedule
  @status_check_schedule = Straight::GatewayModule::DEFAULT_STATUS_CHECK_SCHEDULE
end
order_counters(reload: false) click to toggle source
# File lib/straight-server/gateway.rb, line 218
def order_counters(reload: false)
  return @order_counters if @order_counters && !reload
  @order_counters = {
    new:         get_order_counter(:new),
    unconfirmed: get_order_counter(:unconfirmed),
    paid:        get_order_counter(:paid),
    underpaid:   get_order_counter(:underpaid),
    overpaid:    get_order_counter(:overpaid),
    expired:     get_order_counter(:expired),
    canceled:    get_order_counter(:canceled),
  }
end
order_status_changed(order) click to toggle source
Calls superclass method
# File lib/straight-server/gateway.rb, line 209
def order_status_changed(order)
  statuses = Order::STATUSES.invert
  if StraightServer::Config.count_orders
    increment_order_counter!(statuses[order.old_status], -1) if order.old_status
    increment_order_counter!(statuses[order.status])
  end
  super
end
send_order_to_websocket_client(order) click to toggle source
# File lib/straight-server/gateway.rb, line 194
def send_order_to_websocket_client(order)
  if ws = websockets[order.id]
    ws.send(order.to_json)
    ws.close
  end
end
sign_with_secret(content, level: 1) click to toggle source
# File lib/straight-server/gateway.rb, line 201
def sign_with_secret(content, level: 1)
  result = content.to_s
  level.times do
    result = OpenSSL::HMAC.digest('sha256', secret, result).unpack("H*").first
  end
  result
end
update_last_keychain_id(new_value=nil) click to toggle source

TODO: make it pretty

# File lib/straight-server/gateway.rb, line 169
def update_last_keychain_id(new_value=nil)
  if self.test_mode
    new_value ? self.test_last_keychain_id = new_value : self.test_last_keychain_id += 1
  else
    new_value ? self.last_keychain_id = new_value : self.last_keychain_id += 1
  end
end
websockets() click to toggle source
# File lib/straight-server/gateway.rb, line 189
def websockets
  raise NoWebsocketsForNewGateway unless self.id
  @@websockets[self.id]
end

Private Instance Methods

find_expired_orders_row() click to toggle source

Wallets that support BIP32 do a limited address lookup. If you have 20 empty addresses in a row (actually not 20, but Config.reuse_address_orders_threshold, 20 is the default value) it won't look past it and if an order is generated with the 21st address and Bitcoins are paid there, the wallet may not detect it. Thus we need to always check for the number of expired orders in a row and reuse an address.

This method takes care of the first part of that equation: finds the row of expired orders. It works like this:

  1. Finds 20 last orders

  2. Checks if they form a row of expired orders, that is if there is no non-expired non-new orders

in the array:

if YES (all orders in the row are indeed expired)
  a) Try the next 20 until we find that one non-expired, non-new order
  b) Put all orders in an array, then slice it so only the oldest 20 are there
  c) return 20 oldest expired orders

if NO (some orders are paid)
  Return the row of expired orders - which is not enough to trigger a reuse
  (the triger is in the #find_reusable_order method, which calls this one).
# File lib/straight-server/gateway.rb, line 320
def find_expired_orders_row

  orders = []
  row    = nil
  offset = 0

  while row.nil? || row.size > 0
    row = Order.where(gateway_id: self.id).order(Sequel.desc(:keychain_id), Sequel.desc(:reused)).limit(Config.reuse_address_orders_threshold).offset(offset).to_a

    row.reject! do |o|
      reject = false
      row.each do |o2|
        reject = true if o.keychain_id == o2.keychain_id && o.reused < o2.reused
      end
      reject
    end

    row.sort! { |o1, o2| o2.id <=> o1.id }

    row.each do |o|
      if o.status == Order::STATUSES[:expired]
        orders.unshift(o)
      elsif o.status == Order::STATUSES[:new]
        next
      else
        return orders[0...Config.reuse_address_orders_threshold]
      end
    end
    offset += Config.reuse_address_orders_threshold
  end

  orders

end
send_callback_http_request(order, delay: 5) click to toggle source

Tries to send a callback HTTP request to the resource specified in the callback_url. Use callback_url given to Order if it exist, otherwise default. If it fails for any reason, it keeps trying for an hour (3600 seconds) making 10 http requests, each delayed by twice the time the previous one was delayed. This method is supposed to be running in a separate thread.

# File lib/straight-server/gateway.rb, line 267
def send_callback_http_request(order, delay: 5)
  url = order.callback_url || self.callback_url
  return if url.to_s.empty?

  # Composing the request uri here
  callback_data = order.callback_data ? "&callback_data=#{CGI.escape(order.callback_data)}" : ''
  uri           = URI.parse("#{url}#{url.include?('?') ? '&' : '?'}#{order.to_http_params}#{callback_data}")

  StraightServer.logger.info "Attempting callback for order #{order.id}: #{uri.to_s}"

  begin
    request = Net::HTTP::Get.new(uri.request_uri)
    request.add_field 'X-Signature', SignatureValidator.signature(method: 'GET', request_uri: uri.request_uri, secret: secret, nonce: nil, body: nil)
    response = Net::HTTP.new(uri.host, uri.port).start do |http|
      http.request request
    end
    order.callback_response = { code: response.code, body: response.body }
    order.save
    raise CallbackUrlBadResponse unless response.code.to_i == 200
  rescue => ex
    if delay < CALLBACK_URL_ATTEMPT_TIMEFRAME
      sleep(delay)
      send_callback_http_request(order, delay: delay*2)
    else
      StraightServer.logger.warn "Callback request for order #{order.id} failed with #{ex.inspect}, see order's #callback_response field for details"
    end
  end

  StraightServer.logger.info "Callback request for order #{order.id} performed successfully"
end