├── .gitignore ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bitcoind.gemspec ├── lib ├── bitcoind.rb └── bitcoind │ ├── account.rb │ ├── account_hash.rb │ ├── client.rb │ ├── transaction.rb │ └── version.rb └── test ├── account_hash_test.rb ├── account_test.rb ├── client_test.rb ├── helper.rb └── transaction_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in bitcoind.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'test' do 5 | watch(%r{lib/bitcoind/(.*)\.rb}) { |m| "test/#{m[1]}_test.rb" } 6 | watch(%r{test/.*_test\.rb}) 7 | watch('lib/bitcoind.rb') { 'test' } 8 | watch('test/test_helper.rb') { "test" } 9 | end 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Bryce Kerley 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoind 2 | 3 | Automate your [Bitcoin](http://bitcoin.org/) transactions with this Ruby interface to the `bitcoind` JSON-RPC API. 4 | 5 | ## Connecting 6 | 7 | Before connecting, you will need to configure a username and password for `bitcoind`, and start 8 | `bitcoind`. Once that's done: 9 | 10 | client = Bitcoind.new 'username', 'password' 11 | # => # 12 | 13 | ## Account Balances 14 | 15 | You can get the balance of all addresses controlled by the client: 16 | 17 | client.balance 18 | # => 12.34 19 | 20 | You can also get a hash of all accounts the client controls: 21 | 22 | client.accounts 23 | # => {"Your Address"=>#, "eve-online ransoms"=>#} 24 | 25 | And of course each account has its own balance too: 26 | 27 | ransom = client.accounts['eve-online ransoms'] 28 | # => # 29 | ransom.balance 30 | # => 2.19 31 | 32 | ## Transactions 33 | 34 | You can get all the transactions in an account: 35 | 36 | ransom.transactions 37 | # => [#] 38 | 39 | You can send money from an account too: 40 | 41 | ransom.send_to 'destinationaddress', 2 42 | # => # 43 | 44 | ## Making Accounts 45 | 46 | Creating an account with an associated address is done through the accounts interface: 47 | 48 | tiny_wings = client.accounts.new 'tiny wings ransoms' 49 | # => # 50 | tiny_wings.address 51 | # => "1KV5khnHbbHF2nNQkk7Pe5nPndEj43U27r" 52 | 53 | # Contributing 54 | 55 | * [Fork the project on GitHub](https://github.com/bkerley/bitcoind) 56 | * Write tests for your changes locally 57 | * Make the tests pass by implementing the changes 58 | * Push your changes to GitHub. 59 | * Send a pull request. 60 | 61 | # Copyright 62 | 63 | Copyright (c) 2011 Bryce Kerley. See LICENSE.txt for further details. 64 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rake/testtask' 5 | Rake::TestTask.new(:test) do |test| 6 | test.libs << 'lib' << 'test' 7 | test.pattern = 'test/*_test.rb' 8 | test.verbose = true 9 | end 10 | 11 | task :default => :test 12 | -------------------------------------------------------------------------------- /bitcoind.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "bitcoind/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "bitcoind" 7 | s.version = Bitcoind::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Bryce Kerley"] 10 | s.email = ["bkerley@brycekerley.net"] 11 | s.homepage = "" 12 | s.summary = %q{Control the bitcoin nework client over JSON-RPC.} 13 | s.description = %q{Automate your Bitcoin transactions with this Ruby interface to the bitcoind JSON-RPC API.} 14 | 15 | s.rubyforge_project = "bitcoind" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | s.add_dependency 'rest-client', ['~> 1.6.1'] 23 | s.add_dependency 'activesupport', ['~> 3.0.6'] 24 | 25 | s.add_development_dependency 'guard-test', ['~> 0.1.6'] 26 | s.add_development_dependency 'shoulda', ['~> 2.11.3'] 27 | s.add_development_dependency 'mocha', ['~> 0.9.12'] 28 | s.add_development_dependency 'bundler', ['~> 1.0.12'] 29 | end 30 | -------------------------------------------------------------------------------- /lib/bitcoind.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'rest_client' 3 | require 'active_support/memoizable' 4 | 5 | %w{ client account account_hash transaction }.each do |f| 6 | require File.join(File.dirname(__FILE__), 'bitcoind', f) 7 | end 8 | 9 | module Bitcoind 10 | def self.new(user, pass) 11 | return Client.new user, pass 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/bitcoind/account.rb: -------------------------------------------------------------------------------- 1 | module Bitcoind 2 | class Account 3 | extend ActiveSupport::Memoizable 4 | attr_accessor :name, :balance 5 | 6 | def initialize(client, name) 7 | @client = client 8 | self.name = name 9 | end 10 | 11 | def inspect 12 | "#" 13 | end 14 | 15 | def send_to(destination, amount) 16 | txn_id = @client.request 'sendfrom', self.name, destination, amount 17 | Transaction.new @clientm, self, txn_id 18 | end 19 | 20 | def balance 21 | @client.request 'getbalance', self.name 22 | end 23 | memoize :balance 24 | 25 | def address 26 | @client.request 'getaccountaddress', self.name 27 | end 28 | memoize :address 29 | 30 | def transactions 31 | txn_array = @client.request 'listtransactions', self.name 32 | 33 | txn_array.map do |h| 34 | Transaction.new @client, self, h['txid'] 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/bitcoind/account_hash.rb: -------------------------------------------------------------------------------- 1 | module Bitcoind 2 | class AccountHash < Hash 3 | def initialize(client, balance_hash) 4 | @client = client 5 | balance_hash.each do |name,b| 6 | self[name] = Account.new client, name 7 | end 8 | end 9 | 10 | def new(name) 11 | addr = @client.request 'getnewaddress', name 12 | 13 | self[name] = Account.new @client, name 14 | 15 | self[name] 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/bitcoind/client.rb: -------------------------------------------------------------------------------- 1 | module Bitcoind 2 | class Client 3 | def initialize(user, pass) 4 | @endpoint = "http://#{user}:#{pass}@localhost:8332" 5 | end 6 | 7 | def balance 8 | request 'getbalance' 9 | end 10 | 11 | def accounts 12 | balance_hash = request 'listaccounts' 13 | AccountHash.new self, balance_hash 14 | end 15 | 16 | def request(method, *args) 17 | body = { 'id'=>'jsonrpc', 'method'=>method} 18 | body['params'] = args unless args.empty? 19 | response_json = RestClient.post @endpoint, body.to_json 20 | response = JSON.parse response_json 21 | return response['result'] 22 | end 23 | 24 | def inspect 25 | "#" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/bitcoind/transaction.rb: -------------------------------------------------------------------------------- 1 | module Bitcoind 2 | class Transaction 3 | extend ActiveSupport::Memoizable 4 | attr_accessor :id, :account 5 | def initialize(client, account, id) 6 | @client = client 7 | self.account = account 8 | self.id = id 9 | end 10 | 11 | def detail_hash 12 | @client.request 'gettransaction', self.id 13 | end 14 | memoize :detail_hash 15 | 16 | def inspect 17 | "#" 18 | rescue RestClient::InternalServerError 19 | "#" 20 | end 21 | 22 | def amount 23 | detail_hash['amount'] 24 | end 25 | 26 | def confirmations 27 | detail_hash['confirmations'] rescue 0 28 | end 29 | 30 | def time 31 | Time.at detail_hash['time'] 32 | end 33 | memoize :time 34 | 35 | def confirmed? 36 | confirmations > 6 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/bitcoind/version.rb: -------------------------------------------------------------------------------- 1 | module Bitcoind 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /test/account_hash_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class AccountHashTest < Test::Unit::TestCase 4 | context 'an AccountHash' do 5 | setup do 6 | @client = stub 7 | @ach = Bitcoind::AccountHash.new @client, ""=>0.0, "pi"=>3.14, "benjamin"=>100.0 8 | end 9 | 10 | should 'access accounts like a normal hash' do 11 | assert_equal 'pi', @ach['pi'].name 12 | assert_equal 'benjamin', @ach['benjamin'].name 13 | end 14 | 15 | context 'new method' do 16 | should 'make a new account with a given name' do 17 | @client.expects(:request). 18 | once. 19 | with('getnewaddress', 'new test account'). 20 | returns('xxxnewtestaddress') 21 | 22 | @act = @ach.new 'new test account' 23 | assert_equal 'new test account', @act.name 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/account_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class AccountTest < Test::Unit::TestCase 4 | context 'an Account' do 5 | setup do 6 | @client = stub 7 | @acc = Bitcoind::Account.new @client, 'pi' 8 | end 9 | 10 | should 'have name' do 11 | assert_equal 'pi', @acc.name 12 | end 13 | 14 | should 'have a short but useful inspect' do 15 | assert_equal "#", @acc.inspect 16 | end 17 | 18 | should 'ask the client for a balance' do 19 | @client.expects(:request). 20 | once. 21 | with('getbalance', 'pi'). 22 | returns(3.14) 23 | 24 | @balance = @acc.balance 25 | 26 | assert_equal 3.14, @balance 27 | end 28 | 29 | should 'ask the client for an address' do 30 | @client.expects(:request). 31 | once. 32 | with('getaccountaddress', 'pi'). 33 | returns('testaddress') 34 | 35 | @address = @acc.address 36 | 37 | assert_equal 'testaddress', @address 38 | end 39 | 40 | should 'have a list of transactions' do 41 | @client.expects(:request). 42 | once. 43 | with('listtransactions', 'pi'). 44 | returns [ 45 | { 46 | 'account'=>'pi', 47 | 'address'=>'testaddress', 48 | 'category'=>'receive', 49 | 'amount'=>3.10, 50 | 'confirmations'=>310, 51 | 'txid'=>'310', 52 | 'time'=>1234567310 53 | }, 54 | { 55 | 'account'=>'pi', 56 | 'address'=>'testaddress', 57 | 'category'=>'receive', 58 | 'amount'=>3.11, 59 | 'confirmations'=>311, 60 | 'txid'=>'311', 61 | 'time'=>1234567311 62 | }, 63 | { 64 | 'account'=>'pi', 65 | 'address'=>'testaddress', 66 | 'category'=>'receive', 67 | 'amount'=>3.12, 68 | 'confirmations'=>312, 69 | 'txid'=>'312', 70 | 'time'=>1234567312 71 | }, 72 | ] 73 | 74 | @txns = @acc.transactions 75 | 76 | @txns.each do |t| 77 | assert_kind_of Bitcoind::Transaction, t 78 | end 79 | assert_equal ['310', '311', '312'], @txns.map(&:id) 80 | end 81 | 82 | context 'transactions' do 83 | should 'send money' do 84 | @client.expects(:request). 85 | once. 86 | with('sendfrom', @acc.name, 'testdestinationaddress', 5). 87 | returns('sentmoneytransactionid') 88 | 89 | @txn = @acc.send_to('testdestinationaddress', 5) 90 | 91 | assert_equal 'sentmoneytransactionid', @txn.id 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/client_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class ClientTest < Test::Unit::TestCase 4 | context 'a Bitcoind client' do 5 | setup do 6 | @bcd = Bitcoind.new 'testuser', 'testpass' 7 | end 8 | 9 | should 'have a simple and useful inspect' do 10 | assert_equal "#", @bcd.inspect 11 | end 12 | 13 | context 'balance operation' do 14 | setup do 15 | RestClient.expects(:post). 16 | returns("{\"result\":12.34000000,\"error\":null,\"id\":\"jsonrpc\"}\n"). 17 | with do |e, a| 18 | e == 'http://testuser:testpass@localhost:8332' && 19 | JSON.parse(a) == {'method'=>'getbalance', 'id'=>'jsonrpc'} 20 | end 21 | end 22 | 23 | should 'get the balance' do 24 | @result = @bcd.balance 25 | assert_equal 12.34, @result 26 | end 27 | end 28 | 29 | context 'accounts operation' do 30 | setup do 31 | RestClient.expects(:post). 32 | returns("{\"result\":{\"\":0.0,\"Your Address\":0.0,\"pi\":3.14,\"ben\":100.00},\"error\":null,\"id\":\"jsonrpc\"}\n"). 33 | with do |e, a| 34 | e == 'http://testuser:testpass@localhost:8332' && 35 | JSON.parse(a) == {'method'=>'listaccounts', 'id'=>'jsonrpc'} 36 | end 37 | end 38 | 39 | should 'return a hash of Account objects' do 40 | @result = @bcd.accounts 41 | assert_kind_of Hash, @result 42 | @result.each do |k, a| 43 | assert_kind_of Bitcoind::Account, a 44 | assert_equal k, a.name 45 | end 46 | 47 | assert_equal 'pi', @result['pi'].name 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda' 2 | require 'mocha' 3 | require File.join(File.dirname(__FILE__), '..', 'lib', 'bitcoind') 4 | -------------------------------------------------------------------------------- /test/transaction_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TransactionTest < Test::Unit::TestCase 4 | context 'a transaction' do 5 | setup do 6 | @client = stub 7 | @acc = Bitcoind::Account.new @client, 'pi' 8 | @txn = Bitcoind::Transaction.new @client, @acc, 'testtxnid' 9 | end 10 | 11 | context 'with a detail_hash' do 12 | setup do 13 | @client.expects(:request). 14 | once. 15 | with('gettransaction', 'testtxnid'). 16 | returns('amount'=>3.14, 'confirmations'=>420, 'txid'=>'testtxnid', 17 | 'time'=>1234567890, 'details'=>{ 18 | 'account'=> 'pi', 19 | 'address'=>'testaddress', 20 | 'category'=>'receive', 21 | 'amount'=>3.14 22 | }) 23 | end 24 | 25 | should 'have an amount and time' do 26 | assert_equal 3.14, @txn.amount 27 | assert_equal Time.at(1234567890), @txn.time 28 | end 29 | 30 | should 'have confirmations and be confirmed' do 31 | assert_equal 420, @txn.confirmations 32 | assert @txn.confirmed? 33 | end 34 | end 35 | 36 | context 'without a detail_hash' do 37 | setup do 38 | @client.expects(:request). 39 | at_least_once. 40 | with('gettransaction', 'testtxnid'). 41 | raises(RestClient::InternalServerError) 42 | end 43 | 44 | should 'have a sane inspect' do 45 | assert_equal "#", @txn.inspect 46 | end 47 | 48 | should 'have zero confirmations and be unconfirmed' do 49 | assert_equal 0, @txn.confirmations 50 | assert !@txn.confirmed? 51 | end 52 | end 53 | end 54 | end 55 | --------------------------------------------------------------------------------