lib/sibit.rb in sibit-0.0.1 vs lib/sibit.rb in sibit-0.1.0

- old
+ new

@@ -23,40 +23,144 @@ require 'bitcoin' require 'typhoeus' require 'json' # Sibit main class. +# +# It works through the Blockchain API at the moment: +# https://www.blockchain.com/api/blockchain_api +# # Author:: Yegor Bugayenko (yegor256@gmail.com) # Copyright:: Copyright (c) 2019 Yegor Bugayenko # License:: MIT class Sibit - # Generate new Bitcon private key. + # Constructor. + def initialize(log: STDOUT) + @log = log + end + + # Generate new Bitcon private key and returns in Hash160 format. def generate - key = Bitcoin::Key.generate - key.priv + Bitcoin::Key.generate.priv end - # Create Bitcon address using the private key. + # Create Bitcon address using the private key in Hash160 format. def create(pvt) - key = Bitcoin::Key.new - key.priv = pvt - key.addr + key(pvt).addr end # Get the balance of the address, in satoshi. def balance(address) - request = Typhoeus::Request.new( - "https://blockchain.info/rawaddr/#{address}", - method: :get, - headers: {} + get_json("https://blockchain.info/rawaddr/#{address}")['final_balance'] + end + + # Send a payment and return the transaction hash. + # + # +pvt+: the private key as a Hash160 string + # +amount+: the amount either in satoshis or ending with 'BTC', like '0.7BTC' + # +fee+: the miners fee in satoshis (as integer) or S/M/X/XL as a string + # +sources+: the array of bitcoin addresses where the coins are now + # +target+: the target address to send to + # +change+: the address where the change has to be sent to + def pay(pvt, amount, fee, sources, target, change) + satoshi = satoshi(amount) + builder = Bitcoin::Builder::TxBuilder.new + unspent = 0 + size = 100 + utxos(sources).each do |utxo| + unspent += utxo['value'] + builder.input do |i| + i.prev_out(utxo['tx_hash_big_endian']) + i.prev_out_index(utxo['tx_output_n']) + i.prev_out_script = [utxo['script']].pack('H*') + i.signature_key(key(pvt)) + end + size += 180 + break if unspent > satoshi + end + raise "Not enough funds to send #{amount}, only #{unspent} left" if unspent < satoshi + builder.output(satoshi, target) + tx = builder.tx( + input_value: unspent, + leave_fee: mfee(fee, size), + change_address: change ) + post_tx(tx.to_payload.bth) + tx.hash + end + + private + + # Retrieve all unspent outputs of the given list of + # addresses. + def utxos(sources) + offset = 0 + txns = [] + loop do + uri = [ + 'https://blockchain.info/unspent?', + "active=#{sources.join('|')}", + offset.positive? ? "&offset=#{offset}" : '' + ].join + list = get_json(uri)['unspent_outputs'] + txns += list + break if list.empty? + offset += list.count + end + txns + end + + # Convert text to amount. + def satoshi(amount) + return (amount.gsub(/BTC$/, '').to_f * 100_000_000).to_i if amount.end_with?('BTC') + amount.to_i + end + + def mfee(fee, size) + return fee.to_i if fee.is_a?(Integer) || /^[0-9]+$/.match?(fee) + case fee + when 'S' + return 10 * size + when 'M' + return 50 * size + when 'L' + return 100 * size + when 'XL' + return 250 * size + else + raise "Can't understand the fee: #{fee.inspect}" + end + end + + # Make key from private key string in Hash160. + def key(hash160) + key = Bitcoin::Key.new + key.priv = hash160 + key + end + + def post_tx(body) + uri = 'https://blockchain.info/pushtx' + request = Typhoeus::Request.new(uri, method: :post, body: { tx: body }) request.run response = request.response - json = JSON.parse(response.body) - json['final_balance'] + raise "Invalid response at #{uri}: #{response.code}" unless response.code == 200 + debug("POST #{uri}: #{response.code}") end - # Send a payment. - def pay(_pvt, _amount, _fee, _sources, _target) - raise 'Not implemented yet' + def get_json(uri) + request = Typhoeus::Request.new(uri, method: :get, headers: {}) + request.run + response = request.response + raise "Invalid response at #{uri}: #{response.code}" unless response.code == 200 + debug("GET #{uri}: #{response.code}") + JSON.parse(response.body) + end + + def debug(msg) + if @log.respond_to?(:debug) + @log.debug(msg) + elsif @log.respond_to?(:puts) + @log.puts(msg) + end end end