bin/sibit
#!/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