Skip to content

Instantly share code, notes, and snippets.

@JosephKu
Created September 12, 2015 02:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JosephKu/75190115bd7e393f3788 to your computer and use it in GitHub Desktop.
Save JosephKu/75190115bd7e393f3788 to your computer and use it in GitHub Desktop.
#!/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