lib/sibit.rb in sibit-0.14.2 vs lib/sibit.rb in sibit-0.14.3

- old
+ new

@@ -103,13 +103,65 @@ # +sources+: the hashmap of bitcoin addresses where the coins are now, with # their addresses as keys and private keys as values # +target+: the target address to send to # +change+: the address where the change has to be sent to def pay(amount, fee, sources, target, change) + p = price('USD') + satoshi = satoshi(amount) + builder = Bitcoin::Builder::TxBuilder.new + unspent = 0 + size = 100 + utxos = first_one { |api| api.utxos(sources) } + @log.info("#{utxos.count} UTXOs found, these will be used \ +(value/confirmations at tx_hash):") + utxos.each do |utxo| + unspent += utxo[:value] + builder.input do |i| + i.prev_out(utxo[:hash]) + i.prev_out_index(utxo[:index]) + i.prev_out_script = utxo[:script] + address = Bitcoin::Script.new(utxo[:script]).get_address + i.signature_key(key(sources[address])) + end + size += 180 + @log.info( + " #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}" + ) + break if unspent > satoshi + end + if unspent < satoshi + raise Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left" + end + builder.output(satoshi, target) + f = mfee(fee, size) + satoshi += f if f.negative? + raise Error, "The fee #{f.abs} covers the entire amount" if satoshi.zero? + raise Error, "The fee #{f.abs} is bigger than the amount #{satoshi}" if satoshi.negative? + tx = builder.tx( + input_value: unspent, + leave_fee: true, + extra_fee: [f, Bitcoin.network[:min_tx_fee]].max, + change_address: change + ) + left = unspent - tx.outputs.map(&:value).inject(&:+) + @log.info("A new Bitcoin transaction #{tx.hash} prepared: + #{tx.in.count} input#{tx.in.count > 1 ? 's' : ''}: + #{tx.inputs.map { |i| " in: #{i.prev_out.bth}:#{i.prev_out_index}" }.join("\n ")} + #{tx.out.count} output#{tx.out.count > 1 ? 's' : ''}: + #{tx.outputs.map { |o| "out: #{o.script.bth} / #{num(o.value, p)}" }.join("\n ")} + Min tx fee: #{num(Bitcoin.network[:min_tx_fee], p)} + Fee requested: #{num(f, p)} as \"#{fee}\" + Fee actually paid: #{num(left, p)} + Tx size: #{size} bytes + Unspent: #{num(unspent, p)} + Amount: #{num(satoshi, p)} + Target address: #{target} + Change address is #{change}") first_one do |api| - api.pay(amount, fee, sources, target, change) + api.push(tx.to_payload.bth) end + tx.hash end # Gets the hash of the latest block. def latest first_one(&:latest) @@ -184,9 +236,53 @@ break rescue Sibit::Error => e @log.info("The API #{api.class.name} failed: #{e.message}") end end - raise Sibit::Error, 'No APIs managed to succeed' unless done + unless done + raise Sibit::Error, "No APIs out of #{@api.length} managed to succeed: \ +#{@api.map { |a| a.class.name }.join(', ')}" + end result + end + + def num(satoshi, usd) + format( + '%<satoshi>ss/$%<dollars>0.2f', + satoshi: satoshi.to_s.gsub(/\d(?=(...)+$)/, '\0,'), + dollars: satoshi * usd / 100_000_000 + ) + end + + # Convert text to amount. + def satoshi(amount) + return amount if amount.is_a?(Integer) + raise Error, 'Amount should either be a String or Integer' unless amount.is_a?(String) + return (amount.gsub(/BTC$/, '').to_f * 100_000_000).to_i if amount.end_with?('BTC') + raise Error, "Can't understand the amount #{amount.inspect}" + end + + # Calculates a fee in satoshi for the transaction of the specified size. + # The +fee+ argument could be a number in satoshi, in which case it will + # be returned as is, or a string like "XL" or "S", in which case the + # fee will be calculated using the +size+ argument (which is the size + # of the transaction in bytes). + def mfee(fee, size) + return fee.to_i if fee.is_a?(Integer) + raise Error, 'Fee should either be a String or Integer' unless fee.is_a?(String) + mul = 1 + if fee.start_with?('+', '-') + mul = -1 if fee.start_with?('-') + fee = fee[1..-1] + end + sat = fees[fee.to_sym] + raise Error, "Can't understand the fee: #{fee.inspect}" if sat.nil? + mul * sat * size + end + + # Make key from private key string in Hash160. + def key(hash160) + key = Bitcoin::Key.new + key.priv = hash160 + key end end