Created
September 12, 2015 02:23
-
-
Save JosephKu/75190115bd7e393f3788 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'bitcoin' | |
require 'open-uri' | |
require 'JSON' | |
require 'digest/sha2' | |
require 'pry' | |
require 'bigdecimal' | |
SATOSHI_PER_BITCOIN = BigDecimal.new("100000000") | |
@sender = ARGV[0] | |
@secret_wif = ARGV[1] | |
@recipient = ARGV[2] | |
@amount = BigDecimal.new(ARGV[3]) | |
@transaction_fee = @amount >= BigDecimal.new("0.01") ? BigDecimal.new("0") : BigDecimal.new("0.0005") | |
puts "About to send #{ @amount.to_f } bitcoins from #{ @sender[0..5] }... to #{ @recipient[0..5] }... " + (@transaction_fee > 0 ? "plus a #{ @transaction_fee.to_f } transaction fee." : "") | |
puts "Fetching the current balance for #{@sender[1..5]} from blockchain.info..." | |
url = "https://blockchain.info/address/#{ @sender }?format=json" | |
res = JSON.parse(open(url).read) | |
@balance = BigDecimal.new(res["final_balance"]) / SATOSHI_PER_BITCOIN | |
puts "Current balance of sender: #{ @balance.to_f } BTC" | |
raise "Insuffient funds" if @balance < @amount + @transaction_fee | |
url = "https://blockchain.info/unspent?active=#{ @sender }&format=json" | |
res = JSON.parse(open(url).read) | |
@unspent_outputs = res["unspent_outputs"] | |
@inputs = [] | |
input_total = BigDecimal.new("0") | |
@unspent_outputs.each do |output| | |
@inputs << { | |
previousTx: [output["tx_hash"]].pack("H*").reverse.unpack("H*")[0], # Reverse | |
index: output["tx_output_n"], | |
scriptSig: nil # We'll sign it later | |
} | |
amount = BigDecimal.new(output["value"]) / SATOSHI_PER_BITCOIN | |
puts "Using #{amount.to_f} from output #{output["tx_output_n"]} of transaction #{output["tx_hash"][0..5]}..." | |
input_total += amount | |
break if input_total >= @amount + @transaction_fee | |
end | |
@change = input_total - @transaction_fee - @amount | |
puts "Spend #{@amount.to_f} and return #{ @change.to_f } as change." | |
raise "Unable to process inputs for transaction" if input_total < @amount + @transaction_fee || @change < 0 | |
sender_hex = Bitcoin.decode_base58(@sender) | |
recipient_hex = Bitcoin.decode_base58(@recipient) | |
@outputs = [ | |
{ | |
value: @amount, | |
scriptPubKey: "OP_DUP OP_HASH160 " + (recipient_hex[2..-9].size / 2).to_s(16) + " " + recipient_hex[2..-9] + " OP_EQUALVERIFY OP_CHECKSIG " | |
} | |
] | |
if @change > 0 | |
@outputs << { | |
value: @change, | |
scriptPubKey: "OP_DUP OP_HASH160 " + (sender_hex[2..-9].size / 2).to_s(16) + " " + sender_hex[2..-9] + " OP_EQUALVERIFY OP_CHECKSIG " | |
} | |
end | |
w2 = Bitcoin.decode_base58(@secret_wif) | |
w3 = w2[0..-9] | |
@secret = w3[2..-1] | |
@keypair = Bitcoin.open_key(@secret) | |
raise "Invalid keypair." unless @keypair.check_key | |
step_2 = (Digest::SHA2.new << [@keypair.public_key_hex].pack("H*")).to_s | |
step_3 = (Digest::RMD160.new << [step_2].pack("H*")).to_s | |
step_4 = "00" + step_3 | |
step_5 = (Digest::SHA2.new << [step_4].pack("H*")).to_s | |
step_6 = (Digest::SHA2.new << [step_5].pack("H*")).to_s | |
step_7 = step_7 = step_6[0..7] | |
step_8 = step_4 + step_7 | |
step_9 = Bitcoin.encode_base58(step_8) | |
raise "Public key does not match private key" if @sender != step_9 | |
puts "Signing the transaction..." | |
scriptSig = "OP_DUP OP_HASH160 " + (sender_hex[2..-9].size / 2).to_s(16) + " " + sender_hex[2..-9] + " OP_EQUALVERIFY OP_CHECKSIG " | |
@inputs.collect!{|input| | |
{ | |
previousTx: input[:previousTx], | |
index: input[:index], | |
scriptLength: sender_hex[2..-9].length / 2 + 5, | |
scriptSig: scriptSig, | |
sequence_no: "ffffffff" | |
} | |
} | |
@transaction = { | |
version: 2, | |
in_counter: @inputs.count, | |
inputs: @inputs, | |
out_counter: @outputs.count, | |
outputs: @outputs, | |
lock_time: 0, | |
hash_code_type: "01000000" | |
} | |
pp @transaction | |
def little_endian_hex_of_n_bytes(i, n) | |
i.to_s(16).rjust(n * 2,"0").scan(/(..)/).reverse.join() | |
end | |
def parse_script(script) | |
script.gsub("OP_DUP", "76").gsub("OP_HASH160", "a9").gsub("OP_EQUALVERIFY", "88").gsub("OP_CHECKSIG", "ac") | |
end | |
def serialize_transaction(transaction) | |
tx = "" | |
tx << little_endian_hex_of_n_bytes(transaction[:version],4) + "\n" | |
tx << little_endian_hex_of_n_bytes(transaction[:in_counter],1) + "\n" | |
transaction[:inputs].each do |input| | |
tx << little_endian_hex_of_n_bytes(input[:previousTx].hex, input[:previousTx].length / 2) + " " | |
tx << little_endian_hex_of_n_bytes(input[:index],4) + "\n" | |
tx << little_endian_hex_of_n_bytes(input[:scriptLength],1) + "\n" | |
tx << parse_script(input[:scriptSig]) + " " | |
tx << input[:sequence_no] + "\n" | |
end | |
tx << little_endian_hex_of_n_bytes(transaction[:out_counter],1) + "\n" | |
transaction[:outputs].each do |output| | |
tx << little_endian_hex_of_n_bytes((output[:value] * SATOSHI_PER_BITCOIN).to_i,8) + "\n" | |
unparsed_script = output[:scriptPubKey] | |
tx << little_endian_hex_of_n_bytes(parse_script(unparsed_script).gsub(" ", "").length / 2, 1) + "\n" | |
tx << parse_script(unparsed_script) + "\n" | |
end | |
tx << little_endian_hex_of_n_bytes(transaction[:lock_time],4) + "\n" | |
tx << transaction[:hash_code_type] | |
tx | |
end | |
@utx = serialize_transaction(@transaction) | |
@utx.gsub!("\n", "") | |
@utx.gsub!(" ", "") | |
sha_first = (Digest::SHA2.new << [@utx].pack("H*")).to_s | |
sha_second = (Digest::SHA2.new << [sha_first].pack("H*")).to_s | |
puts "\nHash that we're going to sign: #{sha_second}" | |
signature_binary = @keypair.dsa_sign_asn1([sha_second].pack("H*")) | |
signature = signature_binary.unpack("H*").first | |
hash_code_type = "01" | |
signature_plus_hash_code_type_length = little_endian_hex_of_n_bytes((signature + hash_code_type).length / 2, 1) | |
pub_key_length = little_endian_hex_of_n_bytes(@keypair.public_key_hex.length / 2, 1) | |
scriptSig = signature_plus_hash_code_type_length + " " + signature + " " + hash_code_type + " " + pub_key_length + " " + @keypair.public_key_hex | |
@transaction[:inputs].collect!{|input| | |
{ | |
previousTx: input[:previousTx], | |
index: input[:index], | |
scriptLength: scriptSig.gsub(" ","").length / 2, | |
scriptSig: scriptSig, | |
sequence_no: input[:sequence_no] | |
} | |
} | |
@transaction[:hash_code_type] = "" | |
@tx = serialize_transaction(@transaction) | |
@tx.gsub!("\n", "") | |
@tx.gsub!(" ", "") | |
puts "\nHex signed transaction: (#{ @tx.size / 2 } bytes)\n\n" | |
puts @tx |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment