yegor256/sibit

View on GitHub
bin/sibit

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby
# frozen_string_literal: true

# Copyright (c) 2019-2024 Yegor Bugayenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

$stdout.sync = true

# see https://stackoverflow.com/a/6048451/187141
require 'openssl'
OpenSSL::SSL::VERIFY_PEER ||= OpenSSL::SSL::VERIFY_NONE
puts OpenSSL::X509::DEFAULT_CERT_FILE

require 'slop'
require 'backtrace'
require 'retriable_proxy'
require_relative '../lib/sibit'
require_relative '../lib/sibit/version'
require_relative '../lib/sibit/blockchain'
require_relative '../lib/sibit/blockchair'
require_relative '../lib/sibit/btc'
require_relative '../lib/sibit/bitcoinchain'
require_relative '../lib/sibit/cex'
require_relative '../lib/sibit/earn'
require_relative '../lib/sibit/fake'
require_relative '../lib/sibit/firstof'

begin
  begin
    opts = Slop.parse(ARGV, strict: true, help: true) do |o|
      o.banner = "Usage (#{Sibit::VERSION}): sibit [options] command [args]
Commands are:
    price:    Get current price of BTC in USD
    fees:     Get currently recommended transaction fees
    latest:   Get hash of the latest block
    generate: Generate a new private key
    create:   Create a public Bitcoin address from the key
    balance:  Check the balance of the Bitcoin address
    pay:      Send a new Bitcoin transaction
Options are:"
      o.string '--proxy', 'HTTPS proxy for all requests, e.g. "localhost:3128"'
      o.integer(
        '--attempts',
        'How many times should we try before failing',
        default: 1
      )
      o.bool '--dry', 'Don\'t send a real payment, run in a read-only mode'
      o.bool '--help', 'Read this: https://github.com/yegor256/sibit' do
        puts o
        exit
      end
      o.bool '--verbose', 'Print all possible debug messages'
      o.array(
        '--api',
        'Ordered List of APIs to use, e.g. "earn,blockchain,btc,bitcoinchain"',
        default: %w[earn blockchain btc bitcoinchain blockchair cex]
      )
      o.array(
        '--skip-utxo',
        'List of UTXTO that must be skipped while paying',
        default: []
      )
    end
  rescue Slop::Error => e
    raise e.message
  end
  raise 'Try --help' if opts.arguments.empty?
  log = Sibit::Log.new(opts[:verbose] ? $stdout : nil)
  http = opts[:proxy] ? Sibit::HttpProxy.new(opts[:proxy]) : Sibit::Http.new
  apis = opts[:api].map(&:downcase).map do |a|
    api = nil
    case a
    when 'blockchain'
      api = Sibit::Blockchain.new(http: http, log: log, dry: opts[:dry])
    when 'btc'
      api = Sibit::Btc.new(http: http, log: log, dry: opts[:dry])
    when 'bitcoinchain'
      api = Sibit::Bitcoinchain.new(http: http, log: log, dry: opts[:dry])
    when 'blockchair'
      api = Sibit::Blockchair.new(http: http, log: log, dry: opts[:dry])
    when 'cex'
      api = Sibit::Cex.new(http: http, log: log, dry: opts[:dry])
    when 'fake'
      api = Sibit::Fake.new
    when 'earn'
      api = Sibit::Earn.new(http: http, log: log, dry: opts[:dry])
    else
      raise Sibit::Error, "Unknown API \"#{a}\""
    end
    api = RetriableProxy.for_object(api, on: Sibit::Error) if opts[:attempts] > 1
    api
  end
  sibit = Sibit.new(log: log, api: Sibit::FirstOf.new(apis, log: log, verbose: true))
  case opts.arguments[0]
  when 'price'
    puts sibit.price
  when 'fees'
    fees = sibit.fees
    text = %i[S M L XL].map do |m|
      sat = fees[m] * 250
      usd = sat * sibit.price / 100_000_000
      "#{m}: #{sat}sat / $#{format('%<usd>.02f', usd: usd)}"
    end.join("\n")
    puts text
  when 'latest'
    puts sibit.latest
  when 'generate'
    puts sibit.generate
  when 'create'
    pvt = opts.arguments[1]
    raise 'Private key argument is required' if pvt.nil?
    puts sibit.create(pvt)
  when 'balance'
    address = opts.arguments[1]
    raise 'Address argument is required' if address.nil?
    puts sibit.balance(address)
  when 'pay'
    amount = opts.arguments[1]
    raise 'Amount argument is required' if amount.nil?
    amount = amount.to_i if /^[0-9]+$/.match?(amount)
    fee = opts.arguments[2]
    raise 'Miners fee argument is required' if fee.nil?
    fee = fee.to_i if /^[0-9]+$/.match?(fee)
    sources = opts.arguments[3]
    raise 'Addresses argument is required' if sources.nil?
    target = opts.arguments[4]
    raise 'Target argument is required' if target.nil?
    change = opts.arguments[5]
    raise 'Change argument is required' if change.nil?
    puts sibit.pay(
      amount, fee,
      sources.split(',').map { |p| p.split(':') }.to_h,
      target, change,
      skip_utxo: opts['skip-utxo']
    )
  else
    raise "Command #{opts.arguments[0]} is not supported"
  end
rescue StandardError => e
  if opts[:verbose]
    puts Backtrace.new(e)
  else
    puts "ERROR: #{e.message}"
  end
  exit(255)
end