#!/usr/bin/env ruby # This script is designed to be launched with cron every n minutes to knock on # txcatcher's API and check whether new transactions are being picked up by it. # # New transactions are being fetched from a third-party source (currently: blockcypher.com) # and config is read from the same config file as the one txcatcher uses. # # If an incosistency is found, reports are sent to email and to the logfile # placed in your config_dir/txcatcher_monitor_errors.log # Successful checks are logged into config_dir/txcatcher_monitor.log # # To run this script, specify config dir like this: # # txcatcher-monitor -c ~/.txcatcher/ # # or # # txcatcher-monitor --config-dir ~/.txcatcher/ # # When you generated an example config file with txcatcher on the first run, # you should look at "monitor" section for settings related to this script. # # Just in case, here they are: # # monitor: # txcatcher_url: "https://txcatcher-btc-mainnet.mydomain.com" # blockchain_source_url: "https://api.blockcypher.com/v1/btc/main/txs" # aws_ses: # region: 'us-west-2' # access_key: YOUR_AWS_KEY # secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY # alert_mail: # from: "alert@email.com" # to: # - "your@email.com" # subject: "TxCatcher Alert" # # However remember, you'll also need a 'logger' section in this file. require "rubygems" require "faraday" require "aws-sdk-ses" require "yaml" require 'time' require_relative "../lib/txcatcher/config" require_relative "../lib/txcatcher/logger" require_relative "../lib/txcatcher/initializer" def send_alert(subject, text) return unless TxCatcher::Config["monitor"]["alert_mail"] # Do not send emails more often than once in 1 hour email_time_file = TxCatcher::Config.config_dir + "/txcatcher_monitor_last_email_time.txt" t = Time.parse(File.read(email_time_file)) if File.exists?(email_time_file) return if t && t > Time.now - 3600 ses_config = TxCatcher::Config["monitor"]["aws_ses"] ses = Aws::SES::Client.new( region: ses_config["region"], access_key_id: ses_config["access_key"], secret_access_key: ses_config["secret_access_key"] ) resp = ses.send_email({ destination: { to_addresses: TxCatcher::Config["monitor"]["alert_mail"]["to"] }, source: TxCatcher::Config["monitor"]["alert_mail"]["from"], message: { body: { text: { charset: "utf-8", data: text }}, subject: { charset: "utf-8", data: subject } } }) File.open(email_time_file, 'w') { |f| f.write Time.now.to_s } end def latest_output_addr(i=0) txs = nil attempts = 0 until txs || attempts > 10 attempts += 1 begin response = Faraday.get do |req| req.url TxCatcher::Config["monitor"]["blockchain_source_url"] req.options.timeout = 5 req.options.open_timeout = 5 end rescue StandardError => e LOGGER.report(e, :error, data: { attempt: attempts }) if attempts >= 10 send_alert( "TxCatcher monitor error", "Tried fetching new txs from #{TxCatcher::Config["monitor"]["blockchain_source_url"]}, got the following error\n\n" + "#{e.to_s}\n\n#{e.backtrace.join("\n")}" ) return nil else sleep 15 end end txs = JSON.parse(response.body) if response end txs.each do |tx| if tx["outputs"] && tx["outputs"][i] && tx["outputs"][i]["addresses"] tx["outputs"][i]["addresses"].each do |addr| # Blockcypher returns old Litecoin P2SH addresses which start with 3, which # txcatcher doesn't support. Let's just ignore them. if addr[0] != "3" || !TxCatcher::Config["monitor"]["ignore_3_litecoin_addr"] return addr end end end end end def fetch_txcatcher_response(addr, sleep_time:) response = {} 3.times do sleep sleep_time # let's wait, perhaps txcatcher hasn't caught up response = fetch_txcatcher_for_addr(addr) break unless response.empty? end return response end def fetch_txcatcher_for_addr(addr) response = Faraday.get do |req| req.url TxCatcher::Config["monitor"]["txcatcher_url"] + "/addr/#{addr}/utxo" req.options.timeout = 15 req.options.open_timeout = 15 end JSON.parse(response.body) end LOGGER = TxCatcher::Logger.new(log_file: "txcatcher_monitor.log", error_file: "txcatcher_monitor_errors.log", error_log_delimiter: "") include TxCatcher::Initializer ConfigFile.set! read_config_file response = nil attempts = 0 addr = nil while attempts < 3 && (response.nil? || response.empty?) attempts += 1 begin # if address is the same as in the previous attempt, let's try a different address. addr_index = 0 begin a = latest_output_addr(addr_index) exit unless a # error already sent in latest_output_addr, so just exit. addr_index += 1 end until addr != a || addr_index > 5 addr = a url = TxCatcher::Config["monitor"]["txcatcher_url"] + "/addr/#{addr}/utxo" response = fetch_txcatcher_response(addr, sleep_time: TxCatcher::Config["monitor"]["retry_sleep_time"] ) if response.empty? LOGGER.report("Checked #{url}, got empty response, attempt #{attempts}", :error) if attempts >= 3 send_alert("TxCatcher response empty", "TxCatcher returned an empty {} for #{url}") end else LOGGER.report("Checked #{url}, got #{response.size} utxo for addr #{addr}, attempt #{attempts}", :info) end rescue StandardError => e LOGGER.report(e, :error, data: { attempt: attempts, url: url }) if attempts >= 3 send_alert( "TxCatcher monitor error", "While checking #{url} response, got the following error\n\n" + "#{e.to_s}\n\n#{e.backtrace.join("\n")}" ) else # Let's give some sufficient time before we retry. # # Greater chance to have new transactions on blockcypher and new addresses to check. # This error could simply be due to a long list of tx utxos that's being loaded # and connection timeout hitting before that. sleep TxCatcher::Config["monitor"]["retry_sleep_time"] end end # begin/rescue end #while # Only report if we actually fetched something. Otherwise another error was probably reported already.