├── .gitignore ├── .rspec ├── CHANGELOG ├── COPYING ├── Gemfile ├── README.rdoc ├── Rakefile ├── bin └── bitcoin_wallet ├── bitcoin-ruby-wallet.gemspec ├── lib └── bitcoin │ ├── wallet.rb │ └── wallet │ ├── coinselector.rb │ ├── keygenerator.rb │ ├── keystore.rb │ ├── version.rb │ └── wallet.rb └── spec ├── spec_helper.rb └── wallet ├── coinselector_spec.rb ├── keygenerator_spec.rb ├── keystore_spec.rb └── wallet_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock 2 | /.yardoc 3 | *.gem -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation --color -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.0.1 move code from bitcoin-ruby to separate repo and start recording changes here -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Marius Hanne 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'ffi' 4 | 5 | gem 'bitcoin-ruby', require: 'bitcoin', 6 | git: "git://github.com/mhanne/bitcoin-ruby", branch: "next" 7 | # path: '../bitcoin-ruby' 8 | 9 | gem 'bitcoin-ruby-blockchain', 10 | git: "git://github.com/mhanne/bitcoin-ruby-blockchain" 11 | # path: '../bitcoin-ruby-blockchain' 12 | 13 | gem 'bitcoin-ruby-node', 14 | git: "git://github.com/mhanne/bitcoin-ruby-node" 15 | # path: '../bitcoin-ruby-node' 16 | 17 | gem 'namecoin-ruby', 18 | git: "git://github.com/mhanne/namecoin-ruby", branch: "resolver" 19 | # path: '../namecoin-ruby' 20 | 21 | 22 | gem 'eventmachine' 23 | 24 | gem 'sequel' 25 | gem 'sqlite3' 26 | gem 'pg' 27 | # gem 'mysql' 28 | 29 | gem 'log4r' 30 | 31 | group :development do 32 | gem 'rake' 33 | gem 'rspec' 34 | gem 'pry' 35 | gem 'minitest' 36 | end 37 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Bitcoin-ruby-wallet 2 | 3 | This is a simple wallet based on bitcoin-ruby[http://github.com/lian/bitcoin-ruby], bitcoin-ruby-blockchain[http://github.com/mhanne/bitcoin-ruby-blockchain], and bitcoin-ruby-node[http://github.com/mhanne/bitcoin-ruby-node]. 4 | 5 | It is in very early development and serves mostly as a proof-of-concept prototype. 6 | 7 | Features include 8 | 9 | * support for multiple addresses (no BIP32 yet) 10 | * import/export keys from/to base58 format 11 | * display balance and transaction history 12 | * create transactions with multiple outputs of all bitcoin script types 13 | * register and manage namecoin name records 14 | 15 | == Installation 16 | 17 | We assume you already have a ruby 1.9 or 2.0 compatible interpreter and rubygems environment. 18 | 19 | git clone https://github.com/mhanne/bitcoin-ruby-wallet.git; cd bitcoin-ruby-wallet 20 | ruby bin/bitcoin_wallet 21 | 22 | TODO: gem install 23 | 24 | == Usage 25 | 26 | If you are running the node with default options, the wallet should just work: 27 | 28 | ruby bin/bitcoin_wallet 29 | 30 | You can specify the network to run on (must match the running node and blockchain), 31 | the keystore to use, and how to connect to the node and its database. 32 | See +--help+ for details. 33 | 34 | TODO: explain keystores 35 | 36 | == CLI 37 | 38 | === balance 39 | 40 | Display balance for given addr or whole wallet 41 | 42 | balance [] 43 | 44 | Example (balance for whole wallet) 45 | 46 | > balance 47 | Total balance: 1.00000000 48 | 49 | Example (balance for given address) 50 | 51 | > balance mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER 52 | mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER balance: 1.00000000 53 | 54 | === list 55 | 56 | List addresses or transaction history for given address 57 | 58 | list [] 59 | 60 | Example (listing all addresses) 61 | 62 | > list 63 | Wallet addresses: 64 | P (mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER) - 0.48700000 65 | P (mg4z8NFcDHdSzWhuJReH64SyQCNPhwswQa) - 0.50000000 66 | Total balance: 0.98700000 67 | 68 | Example (listing transactions for given address) 69 | 70 | > list mg4z8NFcDHdSzWhuJReH64SyQCNPhwswQa 71 | INFO storage: opened archive store postgres:/namecoin_regtest 72 | 84b4be9b28140658e7a767046572047b08469bf0cc9f13ab889273fbe9680ad9 | + 0.50000000 | 0.50000000 | 56 73 | <- mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER 74 | 75 | Total balance: 0.50000000 76 | 77 | === send 78 | 79 | Create a transaction and send it to the bitcoin network 80 | 81 | send [:]:[,[:]:...] [] 82 | 83 | Example sending 0.5 coins to an address with 0.01 fee 84 | 85 | > send mg4z8NFcDHdSzWhuJReH64SyQC:0.5 0.01 86 | Hash: 84b4be9b28140658e7a767046572047b08469bf0cc9f13ab889273fbe9680ad9 87 | inputs: 88 | mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER - 1.00000000 89 | outputs: 90 | 0.50000000 mg4z8NFcDHdSzWhuJReH64SyQCNPhwswQa (address) 91 | 0.49000000 mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER (address) 92 | Fee: 0.01000000 93 | Really send transaction? (y/N) y 94 | Transaction 84b4be9b28140658e7a767046572047b08469bf0cc9f13ab889273fbe9680ad9 relayed to approx. 100.00% of the network. 95 | 96 | Example creating an OP_RETURN output 97 | 98 | > send op_return:deadbeef:0 99 | inputs: 100 | mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER - 1.00000000 101 | outputs: 102 | 0.00000000 deadbeef (op_return) 103 | 1.00000000 mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER (address) 104 | Fee: 0.00000000 105 | 106 | === new 107 | 108 | Generate a new key and add it to the keystore 109 | 110 | new 111 | 112 | Example 113 | 114 | > new 115 | Generated new key with address: mg4z8NFcDHdSzWhuJReH64SyQCNPhwswQa 116 | 117 | === import 118 | 119 | Import a private key in base58 (WIF) format 120 | 121 | import 122 | 123 | Example 124 | 125 | > import cPWtu2h81G4ai635woPWghhr619EQVpgKimELRstdbzpJ31Y8NXW 126 | Key for mg4z8NFcDHdSzWhuJReH64SyQCNPhwswQa imported. 127 | 128 | === export 129 | 130 | Export private key for given to base58 (WIF) format 131 | 132 | export 133 | 134 | Example 135 | 136 | > export mg4z8NFcDHdSzWhuJReH64SyQCNPhwswQa 137 | Base58 encoded private key for mg4z8NFcDHdSzWhuJReH64SyQCNPhwswQa: 138 | cPWtu2h81G4ai635woPWghhr619EQVpgKimELRstdbzpJ31Y8NXW 139 | 140 | === name_list 141 | 142 | List names in the wallet 143 | 144 | name_list 145 | 146 | Example 147 | 148 | > name_list 149 | [ 150 | { 151 | "name": "d/foo", 152 | "value": "{\"foo\":\"baz\"}", 153 | "address": "mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER", 154 | "expires_in": 35985 155 | } 156 | ] 157 | 158 | === name_show 159 | 160 | Display current state of given 161 | 162 | name_show 163 | 164 | Example 165 | 166 | > name_show d/foo 167 | { 168 | "name": "d/foo", 169 | "value": "{\"foo\":\"baz\"}", 170 | "txid": "66c3d247489a7d926e77f04a2cf469988e1f991ff463b2895c85544ad87d42bc", 171 | "address": "mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER", 172 | "expires_in": 35985 173 | } 174 | 175 | === name_history 176 | 177 | Display history for given 178 | 179 | name_history - display name history 180 | 181 | Example 182 | 183 | > name_history d/foo 184 | [ 185 | { 186 | "name": "d/foo", 187 | "value": "{\"foo\":\"bar\"}", 188 | "txid": "a5718f2f075bc040fbb30a4d0145d8ada15286a6c72866d4619b7402faf61a39", 189 | "address": "mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER", 190 | "expires_in": 35981 191 | }, 192 | { 193 | "name": "d/foo", 194 | "value": "{\"foo\":\"baz\"}", 195 | "txid": "66c3d247489a7d926e77f04a2cf469988e1f991ff463b2895c85544ad87d42bc", 196 | "address": "mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER", 197 | "expires_in": 35985 198 | } 199 | ] 200 | 201 | === name_new 202 | 203 | Reserve the given . This will output a secret random value used to "lock" the name. 204 | You MUST remember this value, since you need it for the name_firstupdate. 205 | 206 | name_new 207 | 208 | Example 209 | 210 | > name_new d/foo 211 | 212 | Hash: 9b01846b55e06af65d5b66b09999b08199bd4467262242f1c7dd7905f4f3a42b 213 | inputs: 214 | mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER - 0.49000000 215 | outputs: 216 | 0.01000000 mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER (name_new) 217 | Name Hash: d1c8352c69755680571d3c921944c1ee0a7d37fe 218 | 0.47900000 mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER (address) 219 | Fee: 0.00100000 220 | Really send transaction? (y/N) y 221 | Transaction 9b01846b55e06af65d5b66b09999b08199bd4467262242f1c7dd7905f4f3a42b relayed to approx. 100.00% of the network. 222 | [ 223 | "9b01846b55e06af65d5b66b09999b08199bd4467262242f1c7dd7905f4f3a42b", 224 | "2c40c546f77e9ef6" 225 | ] 226 | 227 | The very last number is our secret random value. 228 | Write it down and remember it until your +name_firstupdate+ succeeds. 229 | 230 | === name_firstupdate 231 | 232 | Register the given with secret and set it to . 233 | 234 | name_firstupdate 235 | 236 | Example (Note that we are now passing in the random value from +name_new+) 237 | 238 | > name_firstupdate d/foo 2c40c546f77e9ef6 '{"foo":"bar"}' 239 | Hash: a5718f2f075bc040fbb30a4d0145d8ada15286a6c72866d4619b7402faf61a39 240 | inputs: 241 | mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER - 0.47900000 242 | mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER - 0.01000000 243 | outputs: 244 | 0.01000000 mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER (name_firstupdate) 245 | d/foo: {"foo":"bar"} 246 | 0.47800000 mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER (address) 247 | Fee: 0.00100000 248 | Really send transaction? (y/N) y 249 | Transaction a5718f2f075bc040fbb30a4d0145d8ada15286a6c72866d4619b7402faf61a39 relayed to approx. 100.00% of the network. 250 | a5718f2f075bc040fbb30a4d0145d8ada15286a6c72866d4619b7402faf61a39 251 | 252 | === name_update 253 | 254 | Update the given with given . Transfer ownership to if specified. 255 | 256 | name_update [] 257 | 258 | Example 259 | 260 | > name_update d/foo '{"foo":"baz"}' 261 | Hash: 66c3d247489a7d926e77f04a2cf469988e1f991ff463b2895c85544ad87d42bc 262 | inputs: 263 | mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER - 0.47800000 264 | mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER - 0.01000000 265 | outputs: 266 | 0.01000000 mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER (name_update) 267 | d/foo: {"foo":"baz"} 268 | 0.47700000 mvUrN88gJKNWH4Qbetozu17by8oSEvC9ER (address) 269 | Fee: 0.00100000 270 | Really send transaction? (y/N) y 271 | Transaction 66c3d247489a7d926e77f04a2cf469988e1f991ff463b2895c85544ad87d42bc relayed to approx. 100.00% of the network. 272 | 66c3d247489a7d926e77f04a2cf469988e1f991ff463b2895c85544ad87d42bc 273 | 274 | == Documentation 275 | 276 | Always trying to improve, any help appreciated! If anything is unclear to you, let us know! 277 | 278 | Documentation is generated using yardoc: 279 | 280 | rake doc 281 | 282 | The specs are also a good place to see how something works. 283 | 284 | 285 | == Specs 286 | 287 | The specs can be run with 288 | 289 | rake 290 | 291 | or, if you want to run a single spec 292 | 293 | rspec spec/wallet/wallet_spec.rb 294 | 295 | 296 | == License 297 | 298 | This software is licensed under the terms of the MIT license. See {file:COPYING} for details. 299 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new do |t| 5 | t.rspec_opts = ["-f documentation", "--color"] 6 | end 7 | 8 | task :default => [:spec] 9 | -------------------------------------------------------------------------------- /bin/bitcoin_wallet: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift( File.expand_path("../../lib", __FILE__) ) 3 | 4 | require 'bundler' 5 | Bundler.setup 6 | require 'bitcoin' 7 | require 'bitcoin/namecoin' 8 | require 'bitcoin/blockchain' 9 | require 'bitcoin/node' 10 | require 'eventmachine' 11 | require 'optparse' 12 | require 'yaml' 13 | 14 | require 'bitcoin/wallet.rb' 15 | 16 | defaults = { 17 | network: "testnet", 18 | storage: nil, 19 | keystore: nil, 20 | command: ["127.0.0.1", 9999], 21 | } 22 | options = Bitcoin::Config.load(defaults, :wallet) 23 | 24 | optparse = OptionParser.new do |opts| 25 | opts.banner = 26 | "Usage: bitcoin_wallet [options] []\n" 27 | 28 | opts.separator("\nAvailable options:\n") 29 | 30 | opts.on("-c", "--config FILE", 31 | "Config file (default: #{Bitcoin::Config::CONFIG_PATHS})") do |file| 32 | options = Bitcoin::Config.load_file(options, file, :wallet) 33 | end 34 | 35 | opts.on("-n", "--network NETWORK", 36 | "User Network (default: #{options[:network]})") do |network| 37 | options[:network] = network 38 | end 39 | 40 | opts.on("-s", "--storage BACKEND::CONFIG", 41 | "Use storage backend (default: #{options[:storage]})") do |storage| 42 | options[:storage] = storage 43 | end 44 | 45 | opts.on("--command [HOST:PORT]", 46 | "Node command socket (default: #{options[:command]})") do |command| 47 | options[:command] = command 48 | end 49 | 50 | opts.on("-k", "--keystore [backend::]", 51 | "Key store (default: #{options[:store]})") do |store| 52 | options[:keystore] = store.gsub("~", ENV['HOME']) 53 | end 54 | 55 | opts.on("-h", "--help", "Display this help") do 56 | puts opts; exit 57 | end 58 | 59 | opts.separator "\nAvailable commands:\n" + 60 | " balance [] - display balance for given addr or whole wallet\n" + 61 | " list - list transaction history for address\n" + 62 | " send :[,:...] [] - send transaction\n" + 63 | " new - generate new key and add to keystore\n" + 64 | " import - import key in base58 format\n" + 65 | " export - export key to base58 format\n" + 66 | " name_list - list names in the wallet\n" + 67 | " name_show - display name information\n" + 68 | " name_history - display name history\n" + 69 | " name_new - reserve a name\n" + 70 | " name_firstupdate - register a name\n" + 71 | " name_update [] - update/transfer a name\n" 72 | 73 | end 74 | 75 | optparse.parse! 76 | 77 | cmd = ARGV.shift; cmdopts = ARGV 78 | unless cmd 79 | puts optparse; exit 80 | end 81 | 82 | Bitcoin.network = options[:network] 83 | 84 | options[:keystore] ||= "simple::file=~/.bitcoin-ruby/#{Bitcoin.network_name}/keys.json" 85 | backend, config = options[:keystore].split("::") 86 | config = Hash[config.split(",").map{|c| c.split("=")}] 87 | keystore = Bitcoin::Wallet.const_get("#{backend.capitalize}KeyStore").new(config) 88 | if backend == "deterministic" && !config["nonce"] 89 | puts "nonce: #{keystore.generator.nonce}" 90 | end 91 | 92 | options[:storage] ||= "sequel::sqlite://~/.bitcoin-ruby/#{Bitcoin.network_name}/blocks.db" 93 | backend, config = options[:storage].split("::") 94 | storage = Bitcoin::Blockchain.create_store(backend, :db => config) 95 | 96 | wallet = Bitcoin::Wallet::Wallet.new(storage, keystore, Bitcoin::Wallet::SimpleCoinSelector) 97 | 98 | def str_val(val, pre='') 99 | ("#{pre}%.8f" % (val / 1e8)).rjust(15) 100 | end 101 | 102 | def val_str(str) 103 | (str.to_f * 1e8).to_i 104 | end 105 | 106 | def send_transaction(storage, options, tx, ask = true) 107 | # puts tx.to_json 108 | if ask 109 | total = 0 110 | puts "Hash: #{tx.hash}" 111 | puts "inputs:" 112 | tx.in.each do |txin| 113 | prev_out = storage.get_txout_for_txin(txin) 114 | total += prev_out.value 115 | puts " #{prev_out.get_address} - #{str_val prev_out.value}" 116 | end 117 | 118 | puts "outputs:" 119 | tx.out.each do |txout| 120 | total -= txout.value 121 | script = Bitcoin::Script.new(txout.pk_script) 122 | print "#{str_val txout.value} " 123 | if script.is_pubkey? 124 | puts "#{script.get_pubkey} (pubkey)" 125 | elsif script.is_hash160? 126 | puts "#{script.get_address} (address)" 127 | elsif script.is_multisig? 128 | puts "#{script.get_addresses.join(' ')} (multisig)" 129 | elsif script.is_op_return? 130 | puts "#{script.get_op_return_data} (op_return)" 131 | elsif script.is_namecoin? 132 | puts "#{script.get_address} (#{script.type})" 133 | print " " * 16 134 | if script.is_name_new? 135 | puts "Name Hash: #{script.get_namecoin_hash}" 136 | else 137 | puts "#{script.get_namecoin_name}: #{script.get_namecoin_value}" 138 | end 139 | else 140 | puts "#{str_val txout.value} (unknown type)" 141 | end 142 | end 143 | puts "Fee: #{str_val total}" 144 | 145 | $stdout.sync = true 146 | print "Really send transaction? (y/N) " and $stdout.flush 147 | unless $stdin.gets.chomp.downcase == 'y' 148 | puts "Aborted."; exit 149 | end 150 | end 151 | EM.run do 152 | Bitcoin::Node::CommandClient.connect(*options[:command]) do 153 | on_connected do 154 | request(:relay_tx, hex: tx.to_payload.hth) 155 | end 156 | on_relay_tx do |res| 157 | if res["success"] 158 | puts "Transaction #{tx.hash} relayed to approx. #{"%.2f" % res['propagation']['percent']}% of the network." 159 | else 160 | puts "Error relaying tx: #{res['error']}" 161 | end 162 | EM.stop 163 | end 164 | end 165 | end 166 | end 167 | 168 | case cmd 169 | when "balance" 170 | if cmdopts && cmdopts.size == 1 171 | addr = cmdopts[0] 172 | balance = storage.get_balance(Bitcoin.hash160_from_address(addr)) 173 | puts "#{addr} balance: #{str_val balance}" 174 | else 175 | puts "Total balance: #{str_val wallet.get_balance}" 176 | end 177 | 178 | when "new" 179 | puts "Generated new key with address: #{wallet.get_new_addr}" 180 | 181 | when "add" 182 | key = {:label => ARGV[2]} 183 | case ARGV[0] 184 | when "pub" 185 | k = Bitcoin::Key.new(nil, ARGV[1]) 186 | key[:key] = k 187 | key[:addr] = k.addr 188 | when "priv" 189 | k = Bitcoin::Key.new(ARGV[1], nil) 190 | k.regenerate_pubkey 191 | key[:key] = k 192 | key[:addr] = k.addr 193 | when "addr" 194 | key[:addr] = ARGV[1] 195 | else 196 | raise "unknown type #{ARGV[0]}" 197 | end 198 | wallet.add_key key 199 | 200 | when "label" 201 | wallet.label(ARGV[0], ARGV[1]) 202 | 203 | when "flag" 204 | wallet.flag(ARGV[0], *ARGV[1].split("=")) 205 | 206 | when "key" 207 | key = wallet.keystore.key(ARGV[0]) 208 | puts "Label: #{key[:label]}" 209 | puts "Address: #{key[:addr]}" 210 | puts "Pubkey: #{key[:key].pub}" 211 | puts "Privkey: #{key[:key].priv}" if ARGV[1] == '-p' 212 | puts "Mine: #{key[:mine]}" 213 | 214 | when "import" 215 | if wallet.keystore.respond_to?(:import) 216 | addr = wallet.import_key(cmdopts[0]) 217 | puts "Key for #{addr} imported." 218 | else 219 | puts "Keystore doesn't support importing." 220 | end 221 | 222 | when "rescan" 223 | wallet.rescan 224 | 225 | when "export" 226 | base58 = wallet.keystore.export(cmdopts[0]) 227 | puts "Base58 encoded private key for #{cmdopts[0]}:" 228 | puts base58 229 | 230 | when "list" 231 | if cmdopts && cmdopts.size == 1 232 | depth = storage.get_depth 233 | total = 0 234 | key = wallet.keystore.key(cmdopts[0]) 235 | storage.get_txouts_for_address(key[:addr]).each do |txout| 236 | total += txout.value 237 | tx = txout.get_tx 238 | blocks = depth - tx.get_block.depth rescue 0 239 | puts "#{tx.hash} | #{str_val txout.value, '+ '} | " + 240 | "#{str_val total} | #{blocks}" 241 | tx.in.map(&:get_prev_out).each do |prev_out| 242 | if prev_out 243 | puts " <- #{prev_out.get_address}" 244 | else 245 | puts " <- generation" 246 | end 247 | end 248 | puts 249 | 250 | if txin = txout.get_next_in 251 | tx = txin.get_tx 252 | total -= txout.value 253 | blocks = depth - tx.get_block.depth rescue 0 254 | puts "#{tx.hash} | #{str_val txout.value, '- '} | " + 255 | "#{str_val total} | #{blocks}" 256 | txin.get_tx.out.each do |out| 257 | if Bitcoin.namecoin? && out.type.to_s =~ /^name_/ 258 | script = out.parsed_script 259 | puts " -> #{script.get_namecoin_name || script.get_namecoin_hash} (#{out.type})" 260 | else 261 | puts " -> #{out.get_addresses.join(', ') rescue 'unknown'}" 262 | end 263 | end 264 | puts 265 | end 266 | end 267 | puts "Total balance: #{str_val total}" 268 | else 269 | puts "Wallet addresses:" 270 | total = 0 271 | wallet.list.each do |key, balance| 272 | total += balance 273 | icon = key[:key] && key[:key].priv ? "P" : (key[:mine] ? "M" : " ") 274 | puts " #{icon} #{key[:label].to_s.ljust(10)} (#{key[:addr].to_s.ljust(34)}) - #{("%.8f" % (balance / 1e8)).rjust(15)}" 275 | end 276 | puts "Total balance: #{str_val wallet.get_balance}" 277 | end 278 | 279 | when "name_list" 280 | names = wallet.get_txouts.select {|o| [:name_firstupdate, :name_update].include?(o.type)} 281 | .map(&:get_namecoin_name).group_by(&:name).map {|n, l| l.sort_by(&:expires_in).last }.map {|name| 282 | { name: name.name, value: name.value, address: name.get_address, expires_in: name.expires_in } } 283 | puts JSON.pretty_generate(names) 284 | 285 | when "name_show" 286 | name = storage.name_show(cmdopts[0]) 287 | puts name.to_json 288 | 289 | when "name_history" 290 | names = storage.name_history(cmdopts[0]) 291 | puts JSON.pretty_generate(names) 292 | 293 | when "name_new" 294 | name = cmdopts[0] 295 | address = wallet.keystore.keys.sample[:key].addr 296 | @rand = nil 297 | def self.set_rand rand 298 | @rand = rand 299 | end 300 | tx = wallet.new_tx([[:name_new, self, name, address, 1000000]], 100000, :back) 301 | (puts "Error creating tx."; exit) unless tx 302 | send_transaction(storage, options, tx, true) 303 | puts JSON.pretty_generate([tx.hash, @rand]) 304 | 305 | when "name_firstupdate" 306 | name, rand, value = *cmdopts 307 | address = wallet.keystore.keys.sample[:key].addr 308 | tx = wallet.new_tx([[:name_firstupdate, name, rand, value, address, 1000000]], 100000, :back) 309 | (puts "Error creating tx."; exit) unless tx 310 | send_transaction(storage, options, tx, true) 311 | puts tx.hash 312 | 313 | when "name_update" 314 | name, value, address = *cmdopts 315 | address ||= wallet.keystore.keys.sample[:key].addr 316 | tx = wallet.new_tx([[:name_update, name, value, address, 1000000]], 100000, :back) 317 | (puts "Error creating tx."; exit) unless tx 318 | send_transaction(storage, options, tx, true) 319 | puts tx.hash 320 | 321 | when "send" 322 | to = cmdopts[0].split(',').map do |opts| 323 | o = opts.split(":") 324 | type, *addrs, value = *(o.size == 2 ? [:address, *o] : o) 325 | value = val_str(value) 326 | [type.to_sym, *addrs, value] 327 | end 328 | fee = val_str(cmdopts[1]) || 0 329 | value = val_str value 330 | 331 | unless wallet.get_balance >= (to.map{|t|t[-1]}.inject{|a,b|a+=b;a} + fee) 332 | puts "Insufficient funds."; exit 333 | end 334 | 335 | tx = wallet.new_tx(to, fee) 336 | (puts "Error creating tx."; exit) unless tx 337 | 338 | send_transaction(storage, options, tx, true) 339 | 340 | when "sign" 341 | txt = File.read(ARGV[0]) 342 | txdp = Bitcoin::Wallet::TxDP.parse(txt) 343 | puts txdp.tx[0].to_json 344 | 345 | print "Really sign transaction? (y/N) " and $stdout.flush 346 | unless $stdin.gets.chomp.downcase == 'y' 347 | puts "Aborted."; exit 348 | end 349 | 350 | txdp.sign_inputs do |tx, prev_tx, i, addr| 351 | key = keystore.key(addr)[:key] rescue nil 352 | next nil unless key && !key.priv.nil? 353 | sig_hash = tx.signature_hash_for_input(i, prev_tx) 354 | sig = key.sign(sig_hash) 355 | script_sig = Bitcoin::Script.to_pubkey_script_sig(sig, [key.pub].pack("H*")) 356 | script_sig.unpack("H*")[0] 357 | end 358 | File.open(ARGV[0], "w") {|f| f.write txdp.serialize } 359 | 360 | when "relay" 361 | txt = File.read(ARGV[0]) 362 | txdp = Bitcoin::Wallet::TxDP.parse(txt) 363 | tx = txdp.tx[0] 364 | puts tx.to_json 365 | txdp.inputs.each_with_index do |s, i| 366 | value, sigs = *s 367 | tx.in[i].script_sig = [sigs[0][1]].pack("H*") 368 | end 369 | tx.in.each_with_index do |txin, i| 370 | p txdp.tx.map(&:hash) 371 | prev_tx = storage.get_tx(txin.prev_out.reverse_hth) 372 | raise "prev tx #{txin.prev_out.reverse_hth} not found" unless prev_tx 373 | raise "signature error" unless tx.verify_input_signature(i, prev_tx) 374 | end 375 | 376 | $stdout.sync = true 377 | print "Really send transaction? (y/N) " and $stdout.flush 378 | unless $stdin.gets.chomp.downcase == 'y' 379 | puts "Aborted."; exit 380 | end 381 | 382 | EM.run do 383 | EM.connect(*options[:command]) do |conn| 384 | conn.send_data(["relay_tx", tx.to_payload.unpack("H*")[0]].to_json) 385 | def conn.receive_data(data) 386 | (@buf ||= BufferedTokenizer.new("\x00")).extract(data).each do |packet| 387 | res = JSON.load(packet) 388 | puts "Transaction relayed: #{res[1]["hash"]}" 389 | EM.stop 390 | end 391 | end 392 | end 393 | end 394 | 395 | else 396 | puts "Unknown command. See --help for available commands." 397 | end 398 | -------------------------------------------------------------------------------- /bitcoin-ruby-wallet.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "bitcoin/wallet/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "bitcoin-ruby-wallet" 6 | s.version = Bitcoin::Wallet::VERSION 7 | s.authors = ["Marius Hanne"] 8 | s.email = ["marius.hanne@sourceagency.org"] 9 | s.summary = %q{simple wallet based on bitcoin-ruby, bitcoin-ruby-blockchain, and bitcoin-ruby-node} 10 | s.description = %q{very early development and serves mostly as a proof-of-concept prototype} 11 | s.homepage = "https://github.com/mhanne/bitcoin-ruby-wallet" 12 | s.license = "MIT" 13 | 14 | # s.rubyforge_project = "bitcoin-ruby-wallet" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.required_rubygems_version = ">= 1.3.6" 22 | # s.add_dependency "bitcoin-ruby" 23 | end 24 | -------------------------------------------------------------------------------- /lib/bitcoin/wallet.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | 3 | Bundler.setup 4 | require 'bitcoin' 5 | 6 | # The wallet implementation consists of several concepts: 7 | # Wallet:: the high-level API used to manage a wallet 8 | # SimpleKeyStore:: key store to manage keys/addresses/labels 9 | # SimpleCoinSelector:: coin selector to find unspent outputs to use when creating tx 10 | module Bitcoin::Wallet 11 | 12 | end 13 | 14 | 15 | require_relative "wallet/keystore" 16 | require_relative "wallet/keygenerator" 17 | require_relative "wallet/coinselector" 18 | require_relative "wallet/wallet" 19 | -------------------------------------------------------------------------------- /lib/bitcoin/wallet/coinselector.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | 3 | module Bitcoin::Wallet 4 | 5 | # select unspent txouts to be used by the Wallet when creating a new transaction 6 | class SimpleCoinSelector 7 | 8 | # create coinselector with given +txouts+ 9 | def initialize txouts 10 | @txouts = txouts 11 | end 12 | 13 | # select txouts needed to spend +value+ btc (base units) 14 | def select(value) 15 | txouts = [] 16 | @txouts.each do |txout| 17 | begin 18 | next if txout.next_in 19 | next if Bitcoin.namecoin? && txout.parsed_script.is_namecoin? 20 | next unless txout.address 21 | next unless txout.tx.block 22 | txouts << txout 23 | return txouts if txouts.map(&:value).inject(:+) >= value 24 | rescue 25 | p $! 26 | end 27 | end 28 | txouts 29 | end 30 | 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/bitcoin/wallet/keygenerator.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | 3 | module Bitcoin::Wallet 4 | 5 | # Deterministic key generator as described in 6 | # https://bitcointalk.org/index.php?topic=11665.0. 7 | # 8 | # Takes a seed and generates an arbitrary amount of keys. 9 | # Protects against brute-force attacks by requiring the 10 | # key hash to fit a difficulty target, much like the block chain. 11 | class KeyGenerator 12 | 13 | # difficulty target (0x0000FFFF00000000000000000000000000000000000000000000000000000000) 14 | DEFAULT_TARGET = 0x0000FFFF00000000000000000000000000000000000000000000000000000000 15 | 16 | attr_accessor :seed, :nonce, :target 17 | 18 | # Initialize key generator with optional +seed+ and +nonce+ and +target+. 19 | # [seed] the seed data for the keygenerator (default: random) 20 | # [nonce] the nonce required to satisfy the target (default: computed) 21 | # [target] custom difficulty target (default: DEFAULT_TARGET) 22 | # 23 | # Example: 24 | # g = KeyGenerator.new # random seed, computed nonce, default target 25 | # KeyGenerator.new(g.seed) 26 | # KeyGenerator.new(g.seed, g.nonce) 27 | # g.get_key(0) #=> 28 | # 29 | # Note: When initializing without seed, you should obviously save the 30 | # seed once it is generated. Saving the nonce is optional; it only saves time. 31 | def initialize seed = nil, nonce = nil, target = nil 32 | @seed = seed || OpenSSL::Random.random_bytes(64) 33 | @target = target || DEFAULT_TARGET 34 | @nonce = check_nonce(nonce) 35 | end 36 | 37 | # get key number +n+ from chain 38 | def get_key(n = 0) 39 | key = get_hash(@seed, @nonce) 40 | (n + 1).times { key = sha256(key) } 41 | key 42 | Bitcoin::Key.new(key.unpack("H*")[0]) 43 | end 44 | 45 | # find a nonce that leads to the privkey satisfying the target 46 | def find_nonce 47 | n = 0 48 | n += 1 while !check_target(get_hash(@seed, n)) 49 | n 50 | end 51 | 52 | protected 53 | 54 | # check the nonce; compute if missing, raise if invalid. 55 | def check_nonce(nonce) 56 | return find_nonce unless nonce 57 | # check_target(get_hash(@seed, nonce)) ? nonce : find_nonce 58 | raise ArgumentError, "Nonce invalid." unless check_target(get_hash(@seed, nonce)) 59 | nonce 60 | end 61 | 62 | # check if given +hash+ satisfies the difficulty target 63 | def check_target(hash) 64 | hash.unpack("H*")[0].to_i(16) < @target 65 | end 66 | 67 | # compute a single SHA256 hash for +d+. 68 | def sha256(d); Digest::SHA256.digest(d); end 69 | 70 | # get the hash corresponding to +seed+ and +n+. 71 | def get_hash(seed, n) 72 | sha256( sha256(seed) + sha256(n.to_s) ) 73 | end 74 | 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /lib/bitcoin/wallet/keystore.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | 3 | require 'json' 4 | require 'stringio' 5 | 6 | module Bitcoin::Wallet 7 | 8 | # JSON-file-based keystore used by the Wallet. 9 | class SimpleKeyStore 10 | 11 | attr_reader :config 12 | 13 | # Initialize keystore. 14 | # [config] Hash of settings ({:file => "/foo/bar.json"}) 15 | def initialize config 16 | @config = Hash[config.map{|k,v|[k.to_sym,v]}] 17 | @config[:file].sub!("~", ENV["HOME"]) if @config[:file].is_a?(String) 18 | @keys = [] 19 | load_keys 20 | end 21 | 22 | # List all stored keys. 23 | def keys(need = nil) 24 | @keys.select do |key| 25 | next !(key[:hidden] && key[:hidden] == "true") unless need 26 | case need 27 | when :label 28 | !!key[:label] 29 | when :pub 30 | !!key[:key].pub 31 | when :priv 32 | !!key[:key].priv 33 | when :hidden 34 | !!key[:hidden] 35 | when :mine 36 | !!key[:mine] 37 | end 38 | end 39 | end 40 | 41 | # Get key for given +label+, +addr+ or +pubkey+. 42 | def key(name) 43 | find_key(name) 44 | end 45 | 46 | # Generate and store a new key. 47 | def new_key(label = nil) 48 | raise ArgumentError, "Label #{label} already in use" if label && find_key(label) 49 | key = Bitcoin::Key.generate 50 | @keys << {:label => label, :addr => key.addr, :key => key} 51 | save_keys 52 | key 53 | end 54 | 55 | # Add a key which can consist only of +addr+ and +label+. 56 | def add_key key 57 | label = key[:label] 58 | raise ArgumentError, "Label #{label} already in use" if label && find_key(label) 59 | addr = key[:addr] 60 | raise ArgumentError, "Address #{addr} is invalid" if addr && !Bitcoin.valid_address?(addr) 61 | @keys << key 62 | save_keys 63 | key 64 | end 65 | 66 | def label_key(name, label) 67 | find_key(name) do |key| 68 | key[:label] = label 69 | end 70 | save_keys 71 | end 72 | 73 | def flag_key(name, flag, value) 74 | find_key(name, true) do |key| 75 | key[flag.to_sym] = value 76 | end 77 | save_keys 78 | end 79 | 80 | # Delete key for given +label+, +addr+ or +pubkey+. 81 | def delete(name) 82 | key = find_key(name) 83 | @keys.delete(key) 84 | save_keys 85 | end 86 | 87 | # Export key for given +name+ to base58 format. 88 | # (See Bitcoin::Key#to_base58) 89 | def export(name) 90 | find_key(name)[:key].to_base58 rescue nil 91 | end 92 | 93 | # Import key from given +base58+ string. 94 | # (See Bitcoin::Key.from_base58) 95 | def import(base58, label = nil) 96 | raise ArgumentError, "Label #{label} already in use" if label && find_key(label) 97 | key = Bitcoin::Key.from_base58(base58) 98 | raise ArgumentError, "Address #{key.addr} already in use" if label && find_key(key.addr) 99 | @keys << {:label => label, :addr => key.addr, :key => key} 100 | save_keys 101 | key 102 | end 103 | 104 | # Load keys from file. 105 | # If file is empty this will generate a new key 106 | # and store it, creating the file. 107 | def load_keys 108 | loader = proc{|keys| 109 | keys.map!{|k| Hash[k.map{|k,v| [k.to_sym, v] }]} 110 | keys.map do |key| 111 | key[:key] = Bitcoin::Key.new(key[:priv], key[:pub]) 112 | key[:priv], key[:pub] = nil 113 | @keys << key 114 | end 115 | } 116 | if @config[:file].is_a?(StringIO) 117 | json = JSON.load(@config[:file].read) 118 | loader.call(json) 119 | elsif File.exist?(@config[:file]) 120 | json = JSON.load(File.read(@config[:file])) 121 | loader.call(json) 122 | else 123 | new_key; save_keys 124 | end 125 | end 126 | 127 | # Save keys to file. 128 | def save_keys 129 | dumper = proc{|file| 130 | keys = @keys.map do |key| 131 | key = key.dup 132 | if key[:key] 133 | key[:priv] = key[:key].priv 134 | key[:pub] = key[:key].pub 135 | key.delete(:key) 136 | end 137 | key 138 | end 139 | file.write(JSON.pretty_generate(keys)) 140 | } 141 | 142 | if @config[:file].is_a?(StringIO) 143 | @config[:file].reopen 144 | dumper.call(@config[:file]) 145 | @config[:file].rewind 146 | else 147 | File.open(@config[:file], 'w'){|file| dumper.call(file) } 148 | end 149 | end 150 | 151 | private 152 | 153 | def find_key(name, hidden = false) 154 | key = if Bitcoin.valid_address?(name) 155 | @keys.find{|k| k[:addr] == name } 156 | elsif name.size == 130 157 | @keys.find{|k| k[:key].pub == name } 158 | else 159 | @keys.find{|k| k[:label] == name } 160 | end 161 | return nil if !key || (!hidden && key[:hidden] == "true") 162 | block_given? ? yield(key) : key 163 | end 164 | 165 | end 166 | 167 | # Deterministic keystore. 168 | class DeterministicKeyStore 169 | 170 | attr_reader :generator 171 | 172 | # Initialize keystore. 173 | # [config] Hash of settings ({:keys => 1, :seed => ..., :nonce => ...}) 174 | def initialize config 175 | @config = Hash[config.map{|k,v|[k.to_sym,v]}] 176 | @config[:keys] = (@config[:keys] || 1).to_i 177 | @generator = Bitcoin::Wallet::KeyGenerator.new(@config[:seed], @config[:nonce]) 178 | end 179 | 180 | # List all keys upto configured limit. 181 | def keys 182 | 1.upto(@config[:keys].to_i).map {|i| @generator.get_key(i) } 183 | end 184 | 185 | # Get key for given +addr+. 186 | def key(addr) 187 | 1.upto(@config[:keys].to_i).map do |i| 188 | key = @generator.get_key(i) 189 | return key if key.addr == addr 190 | end 191 | end 192 | 193 | # Get new key (actually just increase the key limit). 194 | def new_key 195 | @config[:keys] += 1 196 | @generator.get_key(@config[:keys]) 197 | end 198 | 199 | # Export key for given +addr+ to base58. 200 | # (See Bitcoin::Key.to_base58) 201 | def export(addr) 202 | key(addr).to_base58 rescue nil 203 | end 204 | 205 | end 206 | 207 | end 208 | -------------------------------------------------------------------------------- /lib/bitcoin/wallet/version.rb: -------------------------------------------------------------------------------- 1 | module Bitcoin 2 | module Wallet 3 | VERSION = "0.0.1" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/bitcoin/wallet/wallet.rb: -------------------------------------------------------------------------------- 1 | module Bitcoin::Wallet 2 | 3 | # A wallet manages a set of keys (through a +keystore+), can 4 | # list transactions/balances for those keys (using a Storage backend for 5 | # blockchain data). 6 | # It can also create transactions with various kinds of outputs and 7 | # connect with a CommandClient to relay those transactions through a node. 8 | # 9 | # TODO: new tx notification, keygenerators, keystore cleanup 10 | class Wallet 11 | 12 | include Bitcoin 13 | include Builder 14 | 15 | # the keystore (SimpleKeyStore) managing keys/addresses/labels 16 | attr_reader :keystore 17 | 18 | # the Storage which holds the blockchain 19 | attr_reader :storage 20 | 21 | # open wallet with given +storage+ Storage backend, +keystore+ SimpleKeyStore 22 | # and +selector+ SimpleCoinSelector 23 | def initialize storage, keystore, selector = SimpleCoinSelector 24 | @storage = storage 25 | @keystore = keystore 26 | @selector = selector 27 | @callbacks = {} 28 | 29 | @keystore.keys.each {|key| @storage.add_watched_address(key[:addr]) } 30 | # connect_node if defined?(EM) 31 | 32 | end 33 | 34 | def connect_node 35 | return unless EM.reactor_running? 36 | host, port = "127.0.0.1", 9999 37 | @node = Network::CommandClient.connect(host, port, self, @storage) do 38 | on_connected { request :monitor, "block", "tx" } 39 | on_block do |block, height| 40 | EM.defer do 41 | block['tx'].each do |tx| 42 | relevant, tx = @args[0].check_tx(tx['hash']) 43 | @args[0].callback(:tx, :confirmed, tx) if relevant 44 | end 45 | end 46 | end 47 | 48 | on_tx do |response| 49 | EM.defer do 50 | relevant, tx = @args[0].check_tx(response['hash']) 51 | @args[0].callback(:tx, relevant, tx) if relevant 52 | end 53 | end 54 | end 55 | end 56 | 57 | def check_tx tx_hash 58 | relevant = false 59 | addrs = addrs 60 | tx = @storage.tx(tx_hash) 61 | unless tx 62 | log.warn { "Received tx #{response['hash']} but not found in storage" } 63 | binding.pry 64 | return false 65 | end 66 | addrs = @keystore.keys.map {|k| k[:addr] } 67 | tx.out.each do |txout| 68 | return :incoming, tx if (txout.addresses & addrs).any? 69 | end 70 | tx.in.each do |txin| 71 | next unless prev_out = txin.prev_out 72 | return :outgoing, tx if (prev_out.addresses & addrs).any? 73 | end 74 | return false 75 | end 76 | 77 | def log 78 | return @log if @log 79 | @log = Logger.create("wallet") 80 | @log.level = :debug 81 | @log 82 | end 83 | 84 | # call the callback specified by +name+ passing in +args+ 85 | def callback name, *args 86 | cb = @callbacks[name.to_sym] 87 | return unless cb 88 | log.debug { "callback: #{name}" } 89 | cb.call(*args) 90 | end 91 | 92 | # register callback methods 93 | def method_missing(name, *args, &block) 94 | if name =~ /^on_/ 95 | @callbacks[name.to_s.split("on_")[1].to_sym] = block 96 | log.debug { "callback #{name} registered" } 97 | else 98 | super(name, *args) 99 | end 100 | end 101 | 102 | # get all Storage::Models::TxOut concerning any address from this wallet 103 | def get_txouts(unconfirmed = false) 104 | txouts = @keystore.keys.map {|k| 105 | @storage.txouts_for_address(k[:addr])}.flatten.uniq 106 | (unconfirmed || @storage.class.name =~ /Utxo/) ? txouts : txouts.select {|o| !!o.tx.block} 107 | end 108 | 109 | # get total balance for all addresses in this wallet 110 | def get_balance(unconfirmed = false) 111 | values = get_txouts(unconfirmed).select{|o| !o.next_in}.map(&:value) 112 | ([0] + values).inject(:+) 113 | end 114 | 115 | # list all addresses in this wallet 116 | def addrs 117 | @keystore.keys.map{|k| k[:addr]} 118 | end 119 | 120 | # add +key+ to wallet 121 | def add_key key 122 | @keystore.add_key(key) 123 | @storage.add_watched_address(key[:addr]) 124 | end 125 | 126 | # set label for key +old+ to +new+ 127 | def label old, new 128 | @keystore.label_key(old, new) 129 | end 130 | 131 | # set +flag+ for key +name+ to +value+ 132 | def flag name, flag, value 133 | @keystore.flag_key(name, flag, value) 134 | end 135 | 136 | # list all keys along with their balances 137 | def list 138 | @keystore.keys.map do |key| 139 | [key, @storage.balance(Bitcoin.hash160_from_address(key[:addr]))] 140 | end 141 | end 142 | 143 | # create new key and return its address 144 | def get_new_addr 145 | key = @keystore.new_key 146 | @storage.add_watched_address(key.addr) 147 | key.addr 148 | end 149 | 150 | def import_key base58, label = nil 151 | key = @keystore.import(base58, label) 152 | @storage.add_watched_address(key.addr) 153 | key.addr 154 | end 155 | 156 | def rescan 157 | @storage.rescan 158 | end 159 | 160 | # get SimpleCoinSelector with txouts for this wallet 161 | def get_selector 162 | @selector.new(get_txouts) 163 | end 164 | 165 | # create a transaction with given +outputs+, +fee+ and +change_policy+. 166 | # 167 | # outputs are of the form 168 | # [, , ] 169 | # examples: 170 | # [:address, , ] 171 | # [:multisig, 2, 3, , , , ] 172 | # 173 | # inputs are selected automatically by the SimpleCoinSelector. 174 | # 175 | # change_policy controls where the change_output is spent to. 176 | # see #get_change_addr 177 | def new_tx outputs, fee = 0, change_policy = :back 178 | output_value = outputs.map{|o| o[-1] }.inject(:+) 179 | 180 | prev_outs = get_selector.select(output_value) || [] 181 | if Bitcoin.namecoin? 182 | prev_out = nil 183 | outputs.each do |out| 184 | if out[0] == :name_firstupdate 185 | name_hash = Bitcoin.hash160(out[2] + out[1].hth) 186 | break if prev_out = get_txouts.find {|o| 187 | o.type == :name_new && o.parsed_script.get_namecoin_hash == name_hash } 188 | elsif out[0] == :name_update 189 | break if prev_out = storage.name_show(out[1]).txout rescue nil 190 | end 191 | end 192 | if outputs.find{|o| [:name_firstupdate, :name_update].include?(o[0]) } 193 | raise "previous name tx not found in wallet." unless prev_out 194 | prev_outs += [prev_out] 195 | end 196 | end 197 | 198 | input_value = prev_outs.map(&:value).inject(:+) || 0 199 | raise "Insufficient funds." unless input_value >= (output_value + fee) 200 | 201 | tx = build_tx do |t| 202 | t.version 0x7100 if Bitcoin.namecoin? && outputs.find {|o| o[0].to_s =~ /^name_/ } 203 | outputs.each do |type, *addrs, value| 204 | t.output do |o| 205 | o.value value 206 | o.script do |s| 207 | s.type type 208 | s.recipient *addrs 209 | end 210 | end 211 | end 212 | 213 | change_value = input_value - output_value - fee 214 | if change_value > 0 215 | change_addr = get_change_addr(change_policy, prev_outs.sample.get_address) 216 | t.output do |o| 217 | o.value change_value 218 | o.script do |s| 219 | s.type :address 220 | s.recipient change_addr 221 | end 222 | end 223 | end 224 | 225 | prev_outs.each_with_index do |prev_out, idx| 226 | t.input do |i| 227 | prev_tx = prev_out.tx 228 | i.prev_out prev_tx 229 | i.prev_out_index prev_tx.out.index(prev_out) 230 | pk_script = Script.new(prev_out.pk_script) 231 | if pk_script.is_pubkey? || pk_script.is_hash160? || pk_script.is_namecoin? 232 | i.signature_key @keystore.key(prev_out.address)[:key] 233 | elsif pk_script.is_multisig? 234 | raise "multisig not implemented" 235 | end 236 | end 237 | end 238 | end 239 | 240 | # TODO: spend multisig outputs again 241 | # TODO: verify signatures 242 | raise "Payload Error" unless P::Tx.new(tx.to_payload).to_payload == tx.to_payload 243 | 244 | tx 245 | end 246 | 247 | protected 248 | 249 | # get address to send change output to. 250 | # +policy+ controls which address is chosen: 251 | # first:: send to the first key in the wallets keystore 252 | # random:: send to a random key from the wallets keystore 253 | # new:: send to a new key generated in the wallets keystore 254 | # back:: send to the address given as +in_addr+ 255 | def get_change_addr(policy, in_addr) 256 | case policy 257 | when :first 258 | @keystore.keys[0].addr 259 | when :random 260 | @keystore.keys.sample.addr 261 | when :new 262 | @keystore.new_key.addr 263 | when :back 264 | in_addr 265 | else 266 | policy 267 | end 268 | end 269 | 270 | end 271 | 272 | end 273 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | $: << File.expand_path(File.join(File.dirname(__FILE__), '/../lib')) 3 | 4 | begin 5 | require 'simplecov' 6 | SimpleCov.start 7 | rescue LoadError 8 | end 9 | 10 | RSpec.configure do |config| 11 | config.fail_fast = true 12 | config.expect_with(:rspec) {|c| c.syntax = [:should, :expect] } 13 | end 14 | 15 | require 'bundler' 16 | Bundler.setup 17 | require 'bitcoin' 18 | require 'bitcoin/blockchain' 19 | require 'bitcoin/wallet' 20 | 21 | begin 22 | require 'minitest' 23 | rescue LoadError 24 | end 25 | require 'minitest/mock' 26 | include MiniTest 27 | 28 | -------------------------------------------------------------------------------- /spec/wallet/coinselector_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | 3 | require_relative '../spec_helper' 4 | 5 | include Bitcoin::Wallet 6 | 7 | describe Bitcoin::Wallet::SimpleCoinSelector do 8 | 9 | def txout_mock(value, next_in = true, in_block = true) 10 | tx, txout = Mock.new, Mock.new 11 | 2.times { tx.expect(:block, in_block) } 12 | 5.times { txout.expect(:value, value) } 13 | 2.times do 14 | txout.expect(:next_in, next_in) 15 | txout.expect(:address, "addr") 16 | txout.expect(:tx, tx) 17 | end 18 | txout 19 | end 20 | 21 | it "should select only txouts which have not been spent" do 22 | txouts = [txout_mock(1000, nil), txout_mock(2000, nil), 23 | txout_mock(1000), txout_mock(3000, nil)] 24 | cs = SimpleCoinSelector.new(txouts) 25 | cs.select(2000).should == txouts[0..1] 26 | cs.select(4000).should == [txouts[0], txouts[1], txouts[3]] 27 | end 28 | 29 | it "should select only txouts which are in a block" do 30 | txouts = [txout_mock(1000, nil, false), txout_mock(2000, nil), 31 | txout_mock(1000), txout_mock(3000, nil)] 32 | cs = SimpleCoinSelector.new(txouts) 33 | cs.select(2000).should == txouts[1..1] 34 | cs.select(4000).should == [txouts[1], txouts[3]] 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /spec/wallet/keygenerator_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | 3 | require_relative '../spec_helper' 4 | 5 | 6 | include Bitcoin::Wallet 7 | describe "Bitcoin::KeyGenerator" do 8 | 9 | let(:target) { ("\x00\xff" + "\x00"*30).unpack("H*")[0].to_i(16) } 10 | 11 | before do 12 | Bitcoin.network = :bitcoin 13 | end 14 | 15 | it "should use random data if no seed given" do 16 | g = KeyGenerator.new(nil, nil, target) 17 | g.seed.size.should == 64 18 | end 19 | 20 | it "should find the nonce if not given" do 21 | KeyGenerator.new("etd").nonce.should == 622 22 | KeyGenerator.new("foo").nonce.should == 2116 23 | KeyGenerator.new("bar").nonce.should == 72353 24 | KeyGenerator.new("baz").nonce.should == 385471 25 | KeyGenerator.new("qux").nonce.should == 29559 26 | end 27 | 28 | it "should use given nonce" do 29 | g = KeyGenerator.new("foo", 2116) 30 | g.nonce.should == 2116 31 | key = g.get_key(0) 32 | key.addr.should == '1JvRdnShvscPtoP44VxPk5VaFBAo7ozRPb' 33 | key.instance_eval { @pubkey_compressed = false } 34 | key.addr.should == '1GjyUrY3XcR4BvfgL8HqoAJbNDEgxSJdm1' 35 | end 36 | 37 | it "should check nonce if given" do 38 | expect { KeyGenerator.new("foo", 42) }.to raise_error(ArgumentError, "Nonce invalid.") 39 | end 40 | 41 | it "should use different target if given" do 42 | g = KeyGenerator.new("foo", nil, target) 43 | g.nonce.should == 127 44 | g.get_key(0).addr.should == "1KLBACvBnz9BTdBnuJmNuQpKQrsi55sstj" 45 | g = KeyGenerator.new("bar", nil, target) 46 | g.nonce.should == 40 47 | g.get_key(0).addr.should == "14T4deW5BGVA7wXpR3eoU9U8xprUJepxcy" 48 | end 49 | 50 | it "should find keys" do 51 | g = KeyGenerator.new("foo") 52 | [ 53 | "05221211a9c3edb9bdf0c120770dc58d2359098c6f16f6e269f722f7dda27cc9", 54 | "7f27bb0ca02e558c4b4b4e267417437adac01403e0d0bb9b07797d1dbb1adfd1", 55 | "da53dec9916406bb9a412bfdc81a3892bbcb1560ab394cb9b9fc3ee2a41101ff", 56 | "7d63c88d0ab023de3441ff268548dc5f59623efe38fdf481bdebc8bb5047c2f2", 57 | "f582838dcba2a1739307448405905028e330e2c9de2a8ec24eed1648b8bddaa4", 58 | "f438a3ff8ea0ee4422f83a456fa6cadf853381c09a4734ae5fbbae616c535a91", 59 | "3a7442aa54f66ae1c8a0d352346587492269b7c800a0319c9789a8164054c59e", 60 | "523d76467f9c091b0c7240dcc509797c8900d4303b720c6afdc4f218b43a1329", 61 | "a11bfa40a0e920bf449ef0ec1d170513c7c82daafd8c4ae3c0e321ddf5fa5cce", 62 | "86a60cbbad2aadfba910f63dc558dd87777561297810674bec020f0f9f86f630", 63 | "cd1fca7ec2bddddc57fa696aefa1391bf5eeea332b79a1f29cfccfccf082a474", 64 | ].map{|h| [h].pack("H*")}.each_with_index do |key, i| 65 | g.get_key(i).priv.should == key.unpack("H*")[0] 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/wallet/keystore_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | 3 | require_relative '../spec_helper' 4 | require 'json' 5 | require 'fileutils' 6 | include Bitcoin 7 | include Bitcoin::Wallet 8 | 9 | describe "Bitcoin::Wallet::SimpleKeyStore" do 10 | 11 | before(:all) do 12 | @test1 = [ 13 | { label: "test1", addr: "174xCfTggAovtDezgswTgfUeCp1hWJ1i7F", pub: "040795786162a1a2fb5bb82310fc1b0da3ced5ed8fc3495bbf848b0156eca465688b0cf08d5389c026556213b7e5ccf471d259575e1756e3352ded2a3eec6a59c8", priv: "c04ea613926036d2782d43eca89512724c9f33f3e8484adb8b952a3837564bcb", mine: true, hidden: false }, 14 | # 5J4iJt8Co9uzmAK7SnLLkvP6dY9s6882kiF4ZCJCNBpZf8QHjVf 15 | { addr: "135o74rH4r7vxEuDdozehLeTuzBG7ABdCA", pub: "04608f68aafef3f216dcb0851bbda7834097a43a0b25794611ebea1177d60c52b25d944ee8c1974f4c9de3d2069cb7ebff803b75487f1f725a6c36a68c2a5ec4ad", priv: "20c1bb60d9242db6240ae125baa0c2eea838e1e33085ff23e36b7dc4e76bb869", mine: true, hidden: false }, 16 | # 5JPbwuNwBWDAsKHzSCUWjvZUkwMFooSXEZrDmnQo5wpEGXcfjJY 17 | { label: "test3", addr: "1Esx52p3MXsjkWWvUM8Pwm2NP14Rj5GkDF", 18 | mine: false, hidden: false }, 19 | # 5Hz9HLAm4t8Mgh8i5mGQm7dgqb2R4V88yVUX6RUf2o77uZus7NP 20 | { label: "test4", addr: "1F17yu83Rhtg78f8ZoEseXo6aprC1D9fwi", 21 | mine: false, hidden: true }, 22 | # 5JQnYo4DNdUKKwMiMwQQxova9NExAgPjZybipjx73RxzTTwARch 23 | { label: "test5", addr: "17NgKZgaDphrfvdBxmX1EssLX7Jyq4ZA22", 24 | mine: true, hidden: false }, 25 | # 5KD3KboVn9a31FWZKZ7NxbbvWcbc5f32D3MkU8kfBzFkw7abRZL 26 | { addr: "1MnSMHjyVSEJE8eC4GUHtuDbvzHbnDBGP7", pub: "04d4aa8b12642e533a8c3c63a8d99d03b77e642b23134cc4dde11065845a24bca86dd3fa2d4d8801bbc2c032597f9f780e72940a90081be743c0051f9cd286b935", mine: false, hidden: false }, 27 | ] 28 | end 29 | 30 | before do 31 | Bitcoin.network = :bitcoin 32 | file_stub = StringIO.new 33 | file_stub.write(@test1.to_json); file_stub.rewind 34 | @ks = SimpleKeyStore.new(file: file_stub) 35 | @key = Bitcoin::Key.generate 36 | end 37 | 38 | it "should create new store" do 39 | file_stub = StringIO.new 40 | file_stub.write(@test1.to_json); file_stub.rewind 41 | ks = SimpleKeyStore.new(file: file_stub) 42 | ks.keys.size.should == 6 43 | end 44 | 45 | it "should load store" do 46 | @ks.keys.size.should == 6 47 | end 48 | 49 | it "should save store" do 50 | file_stub = StringIO.new 51 | file_stub.write(@test1.to_json); file_stub.rewind 52 | ks = SimpleKeyStore.new(file: file_stub) 53 | ks.save_keys 54 | ks2 = SimpleKeyStore.new(file: file_stub) 55 | ks2.keys.should == ks.keys 56 | end 57 | 58 | it "should create new key" do 59 | key = @ks.new_key 60 | @ks.keys.last.should == {label: nil, addr: key.addr, key: key} 61 | end 62 | 63 | it "should delete key" do 64 | @ks.delete(@ks.keys.last[:addr]) 65 | @ks.keys.size.should == 5 66 | end 67 | 68 | it "should get key" do 69 | k1 = @ks.key('174xCfTggAovtDezgswTgfUeCp1hWJ1i7F')[:key] 70 | k2 = @ks.key('test1')[:key] 71 | k3 = @ks.key(k2.pub)[:key] 72 | [k1,k2,k3].each{|k| k.priv.should == 73 | 'c04ea613926036d2782d43eca89512724c9f33f3e8484adb8b952a3837564bcb'} 74 | end 75 | 76 | it "should get keys" do 77 | @ks.key('test1')[:key].priv.should == 78 | 'c04ea613926036d2782d43eca89512724c9f33f3e8484adb8b952a3837564bcb' 79 | end 80 | 81 | it "should export key" do 82 | k1 = @ks.export('174xCfTggAovtDezgswTgfUeCp1hWJ1i7F') 83 | k2 = @ks.export('test1') 84 | k3 = @ks.export(@ks.key('test1')[:key].pub) 85 | [k1,k2,k3].uniq.should == ['5KGyp1k36dqprA9zBuzEJzf327vw4bTkJARcW13zAKBhAfVmeT3'] 86 | end 87 | 88 | it "should import key" do 89 | @ks.import('5JUw75N58166KuA4Pb9s2iJARfu6MC7VaQtFZn523VMuXVYUVSm') 90 | @ks.key('1JovdwZKSby5q3kHLMCX3cCais5YBKVA9x')[:key].priv.should == 91 | '57c0aea88323c96a75e461499571482ee90d98670a023213f8000047dfa3755c' 92 | @ks.delete('1JovdwZKSby5q3kHLMCX3cCais5YBKVA9x') 93 | @ks.import('5JUw75N58166KuA4Pb9s2iJARfu6MC7VaQtFZn523VMuXVYUVSm', "test2") 94 | @ks.key('test2')[:key].priv.should == 95 | '57c0aea88323c96a75e461499571482ee90d98670a023213f8000047dfa3755c' 96 | end 97 | 98 | it "should not allow the same label twice" do 99 | expect { @ks.new_key("test1") }.to raise_error(ArgumentError) 100 | expect { @ks.add_key({label: "test1", addr: "12345"}) }.to raise_error(ArgumentError) 101 | expect { @ks.import("foobar", "test1") }.to raise_error(ArgumentError) 102 | end 103 | 104 | it "should not allow invalid addrs" do 105 | expect { @ks.add_key(addr: "foobar") }.to raise_error(ArgumentError) 106 | end 107 | 108 | it "should store only address" do 109 | k = { label: 'test6', addr: @key.addr } 110 | @ks.add_key(k) 111 | @ks.keys.size.should == 7 112 | @ks.key('test6').should == k 113 | @ks.key(@key.addr).should == k 114 | end 115 | 116 | it "should store only pubkey and addr" do 117 | k = { label: 'test6', addr: @key.addr, pub: @key.pub } 118 | @ks.add_key(k) 119 | @ks.keys.size.should == 7 120 | @ks.key('test6').should == k 121 | @ks.key(@key.addr).should == k 122 | end 123 | 124 | it "should store flags" do 125 | @ks.key('test1')[:mine].should == true 126 | @ks.key('test1')[:hidden].should == false 127 | @ks.flag_key 'test1', :hidden, true 128 | @ks.key('test1')[:hidden].should == true 129 | end 130 | 131 | it "should list only keys which have a label" do 132 | @ks.keys(:label).size.should == 4 133 | end 134 | 135 | it "should list only keys which have a pubkey" do 136 | @ks.keys(:pub).size.should == 3 137 | end 138 | 139 | it "should list only keys which have a privkey" do 140 | @ks.keys(:priv).size.should == 2 141 | end 142 | 143 | it "should list only hidden keys" do 144 | @ks.keys(:hidden).size.should == 1 145 | end 146 | 147 | it "should list only keys which are 'mine'" do 148 | @ks.keys(:mine).size.should == 3 149 | end 150 | 151 | end 152 | 153 | 154 | describe "Bitcoin::Wallet::DeterministicKeyStore" do 155 | 156 | before do 157 | @ks = DeterministicKeyStore.new(seed: "foo", keys: 1, nonce: 2116) 158 | end 159 | 160 | it "should create new store" do 161 | ks = DeterministicKeyStore.new(seed: "etd", keys: 3) 162 | ks.keys.size.should == 3 163 | ks.generator.nonce.should == 622 164 | end 165 | 166 | it "should load store" do 167 | @ks.keys.map(&:priv).should == 168 | ['7f27bb0ca02e558c4b4b4e267417437adac01403e0d0bb9b07797d1dbb1adfd1'] 169 | end 170 | 171 | it "should create new key" do 172 | key = @ks.new_key 173 | key.priv.should == 'da53dec9916406bb9a412bfdc81a3892bbcb1560ab394cb9b9fc3ee2a41101ff' 174 | @ks.keys.last.should == key 175 | end 176 | 177 | it "should get key" do 178 | @ks.key('1GKjKQemNRhxL1ChTRFJNLZCXeCDxut2d7').priv.should == 179 | '7f27bb0ca02e558c4b4b4e267417437adac01403e0d0bb9b07797d1dbb1adfd1' 180 | end 181 | 182 | it "should get keys" do 183 | @ks.keys.map(&:priv).should == 184 | ['7f27bb0ca02e558c4b4b4e267417437adac01403e0d0bb9b07797d1dbb1adfd1'] 185 | end 186 | 187 | it "should export key" do 188 | @ks.export('1GKjKQemNRhxL1ChTRFJNLZCXeCDxut2d7').should == 189 | 'L1UtDvpnffnVg1szqSmQAgFexzvcysZrs3jwLH1FT4uREpZqcXaR' 190 | end 191 | 192 | end 193 | -------------------------------------------------------------------------------- /spec/wallet/wallet_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: ascii-8bit 2 | 3 | require_relative '../spec_helper' 4 | require 'json' 5 | require 'fileutils' 6 | include Bitcoin 7 | include Bitcoin::Wallet 8 | 9 | def txout_mock(value, next_in = true, in_block = true) 10 | tx, txout = Mock.new, Mock.new 11 | tx.expect(:block, in_block) 12 | 4.times { txout.expect(:value, value) } 13 | 2.times { txout.expect(:next_in, next_in) } 14 | 6.times { txout.expect(:hash, [value, next_in].hash) } 15 | txout.expect(:eql?, false, [1]) 16 | txout.expect(:==, false, [1]) 17 | txout.expect(:tx, tx) 18 | end 19 | 20 | describe Bitcoin::Wallet::Wallet do 21 | 22 | class DummyKeyStore 23 | 24 | def initialize keys 25 | @keys = keys.map{|k| { key: k, addr: k.addr } } 26 | end 27 | 28 | def key(addr) 29 | @keys.select{|k| k[:key].addr == addr }.first 30 | end 31 | 32 | def keys 33 | @keys 34 | end 35 | 36 | def new_key 37 | k=Bitcoin::Key.generate 38 | @keys << { key: k, addr: k.addr} 39 | @keys[-1] 40 | end 41 | end 42 | 43 | before do 44 | Bitcoin.network = :bitcoin 45 | @storage = Mock.new 46 | @key = Key.from_base58('5J2hn1E8KEXmQn5gqykzKotyCcHbKrVbe8fjegsfVXRdv6wwon8') 47 | @addr = '1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo' 48 | #@key2 = Key.from_base58('5KK9Lw8gtNd4kcaXQJmkwcmNy8Y5rLGm49RqhcYAb7qRhWxaWMJ') 49 | #@addr2 = '134A4Bi8jN5V2KjkwmXUHjokDqdyqZ778J' 50 | #@key3 = Key.from_base58('5JFcJByQvwYnWjQ2RHTTu6LLGiBj9oPQYsHqKWuKLDVAvv4cQ7E') 51 | #@addr3 = '1EnrPVaRiRgrs1D7pujYZNN1N6iD9unZV6' 52 | 53 | @storage.expect(:add_watched_address, [], [@addr]) 54 | 55 | keystore_data = [{:addr => @key.addr, :priv => @key.priv, :pub => @key.pub}] 56 | file_stub = StringIO.new 57 | file_stub.write(keystore_data.to_json); file_stub.rewind 58 | @keystore = SimpleKeyStore.new(file: file_stub) 59 | @selector = Mock.new 60 | @wallet = Bitcoin::Wallet::Wallet.new(@storage, @keystore, @selector) 61 | end 62 | 63 | it "should get total balance" do 64 | @storage.expect(:class, Bitcoin::Blockchain::Backends::Archive, []) 65 | @storage.expect(:txouts_for_address, [], [@addr]) 66 | 2.times { @storage.expect(:class, Bitcoin::Blockchain::Backends::Archive, []) } 67 | @wallet.get_balance.should == 0 68 | 69 | @storage.expect(:txouts_for_address, [txout_mock(5000, nil)], [@addr]) 70 | @wallet.get_balance.should == 5000 71 | 72 | @storage.expect(:txouts_for_address, [txout_mock(5000, true), txout_mock(1000, nil)], 73 | [@addr]) 74 | @wallet.get_balance.should == 1000 75 | end 76 | 77 | it "should get all addrs" do 78 | @wallet.addrs.should == [@addr] 79 | @wallet.addrs.size.should == 1 80 | end 81 | 82 | it "should list all addrs with balances" do 83 | @storage.expect(:balance, 0, ['dcbc93494b38ae96b14b1cc080d2acb514b7e955']) 84 | list = @wallet.list 85 | list.size.should == 1 86 | list = list[0] 87 | list.size.should == 2 88 | list[0][:addr].should == "1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo" 89 | list[1].should == 0 90 | 91 | @storage.expect(:balance, 5000, ['dcbc93494b38ae96b14b1cc080d2acb514b7e955']) 92 | list = @wallet.list 93 | list.size.should == 1 94 | list = list[0] 95 | list.size.should == 2 96 | list[0][:addr].should == @addr 97 | list[1].should == 5000 98 | end 99 | 100 | it "should create new addr" do 101 | @wallet.addrs.size.should == 1 102 | 103 | @storage.expect(:add_watched_address, [], [String]) 104 | a = @wallet.get_new_addr 105 | @wallet.addrs.size.should == 2 106 | @wallet.addrs[1].should == a 107 | end 108 | 109 | # describe "Bitcoin::Wallet::Wallet#tx" do 110 | 111 | # before do 112 | # txout = txout_mock(5000, nil) 113 | # tx = Mock.new 114 | # 2.times { tx.expect(:binary_hash, "foo") } 115 | # 8.times { tx.expect(:out, [txout]) } 116 | # 3.times { tx.expect(:block, true) } 117 | # 5.times { txout.expect(:tx, tx) } 118 | # 6.times { txout.expect(:address, @addr) } 119 | # 8.times { txout.expect(:pk_script, Script.to_address_script(@addr)) } 120 | # 2.times { @storage.expect(:txouts_for_address, [txout], [@addr]) } 121 | # 2.times { @storage.expect(:class, Bitcoin::Storage::Backends::SequelStore, []) } 122 | # selector = Bitcoin::Wallet::SimpleCoinSelector.new([txout]) 123 | # 2.times { @selector.expect(:new, selector, [[txout]]) } 124 | # @tx = @wallet.new_tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]]) 125 | # end 126 | 127 | 128 | # it "should have hash" do 129 | # @tx.hash.size.should == 64 130 | # end 131 | 132 | # it "should have correct inputs" do 133 | # @tx.in.size.should == 1 134 | # @tx.in.first.prev_out.should == ("foo" + "\x00"*29) 135 | # @tx.in.first.prev_out_index.should == 0 136 | # end 137 | 138 | # it "should have correct outputs" do 139 | # @tx.out.size.should == 2 140 | # @tx.out.first.value.should == 1000 141 | # s = Script.new(@tx.out.first.pk_script) 142 | # s.get_address.should == '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7' 143 | # end 144 | 145 | # it "should have change output" do 146 | # @tx.out.last.value.should == 4000 147 | # s = Script.new(@tx.out.last.pk_script) 148 | # s.get_address.should == @addr 149 | # end 150 | 151 | # it "should leave tx fee" do 152 | # @tx = @wallet.new_tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50) 153 | # @tx.out.last.value.should == 3950 154 | # end 155 | 156 | # it "should send change to specified address" do 157 | # @tx = @wallet.new_tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50, 158 | # '1EAntvSjkNeaJJTBQeQcN1ieU2mYf4wU9p') 159 | # Script.new(@tx.out.last.pk_script).get_address.should == 160 | # '1EAntvSjkNeaJJTBQeQcN1ieU2mYf4wU9p' 161 | # end 162 | 163 | # it "should send change to new address" do 164 | # @tx = @wallet.new_tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50, :new) 165 | # @wallet.addrs.size.should == 2 166 | # Script.new(@tx.out.last.pk_script).get_address.should == @wallet.addrs.last 167 | # end 168 | 169 | # it "should raise exception if insufficient balance" do 170 | # -> {@tx = @wallet.new_tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 7000]])} 171 | # .should.raise(RuntimeError).message.should == "Insufficient funds." 172 | # end 173 | 174 | 175 | # it "should create unsigned tx" do 176 | # Bitcoin.network = :spec 177 | # @key = Bitcoin::Key.generate 178 | # @key2 = Bitcoin::Key.generate 179 | # @store = Storage.sequel(db: "sqlite:/") 180 | # @store.log.level = :debug 181 | 182 | # @keystore = SimpleKeyStore.new(file: StringIO.new("[]")) 183 | # @wallet = Wallet.new(@store, @keystore, SimpleCoinSelector) 184 | 185 | # @wallet.keystore.add_key(addr: @key.addr) 186 | 187 | # @genesis = Bitcoin::P::Block.new("0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000".htb) 188 | 189 | # @store.new_block @genesis 190 | # create_block(@genesis.hash, true, [], @key, 50e8) 191 | 192 | # list = @wallet.list 193 | # list.size.should == 1 194 | # list[0][0].should == {addr: @key.addr} 195 | # list[0][1].should == 50e8 196 | 197 | # tx = @wallet.new_tx([[:address, @key2.addr, 10e8]]) 198 | # tx.in[0].sig_hash.should != nil 199 | # end 200 | 201 | # end 202 | 203 | # # TODO 204 | # describe "Bitcoin::Wallet::Wallet#tx (multisig)" do 205 | 206 | 207 | # before do 208 | # txout = txout_mock(5000, nil) 209 | # tx = Mock.new 210 | # tx.expect(:binary_hash, "foo") 211 | # 4.times { tx.expect(:out, [txout]) } 212 | # tx.expect(:block, true) 213 | # txout.expect(:tx, tx) 214 | # 2.times { txout.expect(:address, @addr) } 215 | # 4.times { txout.expect(:pk_script, Script.to_address_script(@addr)) } 216 | # @storage.expect(:txouts_for_address, [txout], [@key.addr]) 217 | # @storage.expect(:txouts_for_address, [txout], [@key2.addr]) 218 | # @storage.expect(:txouts_for_address, [txout], [@key3.addr]) 219 | # @storage.expect(:class, Bitcoin::Storage::Backends::SequelStore, []) 220 | # @keystore = DummyKeyStore.new([@key, @key2, @key3]) 221 | # selector = Mock.new 222 | # selector.expect(:select, [txout], [1000]) 223 | # @selector.expect(:new, selector, [[txout]]) 224 | # @wallet = Wallet.new(@storage, @keystore, @selector) 225 | # @tx = @wallet.new_tx([[:multisig, 1, @key2.pub, @key3.pub, 1000]]) 226 | # end 227 | 228 | # it "should have correct outputs" do 229 | # @tx.out.size.should == 2 230 | # @tx.out.first.value.should == 1000 231 | # s = Script.new(@tx.out.first.pk_script) 232 | # s.get_addresses.should == [@addr2, @addr3] 233 | # end 234 | 235 | # end 236 | 237 | end 238 | --------------------------------------------------------------------------------