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
# 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
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
END OF Initializers methods ##################################################
# 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
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
# 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
# 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
# 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
# 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
# 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
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
# File lib/straight-server/gateway.rb, line 108 def initialize_network BTC::Network.default = test_mode ? BTC::Network.testnet : BTC::Network.mainnet end
# File lib/straight-server/gateway.rb, line 104 def initialize_status_check_schedule @status_check_schedule = Straight::GatewayModule::DEFAULT_STATUS_CHECK_SCHEDULE end
# 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
# 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
# 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
# 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
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
# File lib/straight-server/gateway.rb, line 189 def websockets raise NoWebsocketsForNewGateway unless self.id @@websockets[self.id] end
Private Instance Methods
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:
-
Finds 20 last orders
-
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
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