straight 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/Gemfile.lock +5 -1
  4. data/README.md +20 -10
  5. data/VERSION +1 -1
  6. data/lib/straight.rb +10 -1
  7. data/lib/straight/blockchain_adapter.rb +2 -13
  8. data/lib/straight/blockchain_adapters/biteasy_adapter.rb +74 -0
  9. data/lib/straight/blockchain_adapters/blockchain_info_adapter.rb +37 -17
  10. data/lib/straight/blockchain_adapters/mycelium_adapter.rb +144 -0
  11. data/lib/straight/exchange_rate_adapter.rb +20 -1
  12. data/lib/straight/exchange_rate_adapters/average_rate_adapter.rb +54 -0
  13. data/lib/straight/exchange_rate_adapters/bitpay_adapter.rb +5 -2
  14. data/lib/straight/exchange_rate_adapters/bitstamp_adapter.rb +2 -1
  15. data/lib/straight/exchange_rate_adapters/btce_adapter.rb +18 -0
  16. data/lib/straight/exchange_rate_adapters/coinbase_adapter.rb +2 -4
  17. data/lib/straight/exchange_rate_adapters/kraken_adapter.rb +18 -0
  18. data/lib/straight/exchange_rate_adapters/localbitcoins_adapter.rb +17 -0
  19. data/lib/straight/exchange_rate_adapters/okcoin_adapter.rb +18 -0
  20. data/lib/straight/gateway.rb +20 -9
  21. data/lib/straight/order.rb +46 -13
  22. data/spec/lib/blockchain_adapters/{helloblock_io_spec.rb → biteasy_adapter_spec.rb} +23 -18
  23. data/spec/lib/blockchain_adapters/{blockchain_info_spec.rb → blockchain_info_adapter_spec.rb} +8 -3
  24. data/spec/lib/blockchain_adapters/mycelium_adapter_spec.rb +54 -0
  25. data/spec/lib/exchange_rate_adapter_spec.rb +6 -1
  26. data/spec/lib/exchange_rate_adapters/average_rate_adapter_spec.rb +43 -0
  27. data/spec/lib/exchange_rate_adapters/bitpay_adapter_spec.rb +14 -1
  28. data/spec/lib/exchange_rate_adapters/bitstamp_adapter_spec.rb +14 -1
  29. data/spec/lib/exchange_rate_adapters/btce_adapter_spec.rb +27 -0
  30. data/spec/lib/exchange_rate_adapters/coinbase_adapter_spec.rb +14 -1
  31. data/spec/lib/exchange_rate_adapters/kraken_adapter_spec.rb +27 -0
  32. data/spec/lib/exchange_rate_adapters/localbitcoins_adapter_spec.rb +27 -0
  33. data/spec/lib/exchange_rate_adapters/okcoin_adapter_spec.rb +27 -0
  34. data/spec/lib/gateway_spec.rb +23 -5
  35. data/spec/lib/order_spec.rb +18 -2
  36. data/straight.gemspec +95 -0
  37. metadata +33 -6
  38. data/lib/straight/blockchain_adapters/helloblock_io_adapter.rb +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2b001bb2871da76e4b657fc5d3e8812ee1e829f3
4
- data.tar.gz: ff86dddd47ec116ee7f2f8de5c799aaedd872473
3
+ metadata.gz: 1ce549f0d3b951b8ce6e5d6e35ba868497b977e7
4
+ data.tar.gz: 5c44763aa3b9d35441e0d1bdd4824dac85238320
5
5
  SHA512:
6
- metadata.gz: b5fc18e1b66e8c2548dc0d515374437f467ccb8821ae45228e562ad0676568ff3ee7fd30468e50302b7cd9d294b881766f83ce45349bae78337e986bbc83c8c2
7
- data.tar.gz: b15b7209e215e81bfa18bb260ec358368640122d83718591b3b1a09cb409f056b52badae4a2506582f8477caeea174798978f25d473422254e8fdfce3b88f13e
6
+ metadata.gz: e4632fafae05cd13ab8443a6817806bca0e67fa0f7eddc03027944287e6c57c8437bd914f18589db3b9e39a91ac2a9e796d68015485132ff2e68774fe70f4f86
7
+ data.tar.gz: 0724e768858d85d0b3f9290d6570f75944167554b9b5ed5177cbb97c1ae76f5569b68730e5a77cba88db1b499413e486381b0dfe35681f5c8aab8051cbc0982d
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ gem 'money-tree'
5
5
 
6
6
  # Used in exchange rate adapters
7
7
  gem 'satoshi-unit'
8
+ gem 'httparty'
8
9
 
9
10
  group :development do
10
11
  gem "bundler", "~> 1.0"
data/Gemfile.lock CHANGED
@@ -20,6 +20,9 @@ GEM
20
20
  oauth2
21
21
  hashie (3.3.1)
22
22
  highline (1.6.21)
23
+ httparty (0.13.3)
24
+ json (~> 1.8)
25
+ multi_xml (>= 0.5.2)
23
26
  jeweler (2.0.1)
24
27
  builder
25
28
  bundler (>= 1.0)
@@ -61,7 +64,7 @@ GEM
61
64
  rspec-mocks (3.1.0)
62
65
  rspec-support (~> 3.1.0)
63
66
  rspec-support (3.1.0)
64
- satoshi-unit (0.1.6)
67
+ satoshi-unit (0.1.7)
65
68
  thread_safe (0.3.4)
66
69
 
67
70
  PLATFORMS
@@ -70,6 +73,7 @@ PLATFORMS
70
73
  DEPENDENCIES
71
74
  bundler (~> 1.0)
72
75
  github_api (= 0.11.3)
76
+ httparty
73
77
  jeweler (~> 2.0.1)
74
78
  money-tree
75
79
  rspec
data/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  Straight
2
2
  ========
3
3
  > Receive bitcoin payments directly into your wallet
4
+
4
5
  > Website: http://straight.romansnitko.com
5
6
 
6
- Straight is a built-in stateless gateway to receive bitcoin payments for
7
- your online store. Drop in this library, set your public key and start receiving payments.
8
- Your BIP32-compatible wallet will see payments automatically without any need for integration
9
- with your database.
7
+ Straight is a built-in stateless gateway library written in Ruby.
8
+ It allows you to receive bitcoin payments for your online store. Drop in this library,
9
+ set your public key and start receiving payments. Your BIP32-compatible wallet will
10
+ see payments automatically without any need for integration with your database.
10
11
 
11
12
  Straight cares about security and privacy. No private keys are stored on the server,
12
13
  each order uses unique payment address. Straight notifies your application when payment is
@@ -16,7 +17,7 @@ IMPORTANT: this is a gem, not a server. It has no state and is intended to use w
16
17
  an application, such as Ruby On Rails. Most likely, you want
17
18
  [straight-server](https://github.com/snitko/straight-server), it is a server,
18
19
  which holds the state of all orders for you and has a RESTful API you can use
19
- with any application written in in language or platform.
20
+ with any application written in any language or platform.
20
21
 
21
22
  Bitcoin donations are appreciated: 1D3PknG4Lw1gFuJ9SYenA7pboF9gtXtdcD
22
23
 
@@ -61,6 +62,7 @@ Usage
61
62
  gateway.name = 'my gateway'
62
63
 
63
64
  # Set the callback for orders' status changes
65
+ # (see lib/straight/order.rb for status attribute values and their meanings)
64
66
  #
65
67
  gateway.order_callbacks = [
66
68
  lambda { |order| puts "Order status changed to #{order.status}" }
@@ -101,8 +103,8 @@ When this module is included, it doesn't actually *include* all the methods, som
101
103
  It is important specifically for getters and setters and as a general rule only getters and setters are prepended.
102
104
 
103
105
  If you don't want to bother yourself with modules, please use `Straight::Order` class and simply create new instances of it.
104
- However, if you are contributing to the library, all new functionality should go to either Straight::OrderModule::Includable or
105
- Straight::OrderModule::Prependable (most likely the former).
106
+ However, if you are contributing to the library, all new functionality should go to either `Straight::OrderModule::Includable` or
107
+ `Straight::OrderModule::Prependable` (most likely the former).
106
108
 
107
109
 
108
110
  Important Considerations
@@ -115,14 +117,22 @@ all orders should be indexed sequentially, not randomly.
115
117
  Why can't we just derive new addresses from order UUID, or assign them to orders? The reason is that your
116
118
  wallet will have to integrate with your very own database and it may be enormously cumbersome to implement
117
119
  in a generic way. Alternative would be to create a wallet within Straight and make it generate and keep the
118
- private keys, but this would be highly insecure. Keys stored on popular hosting solutions would quickly invite
119
- all sorts of attacks to get money from them.
120
+ private keys, but this would be highly insecure. Keys stored on popular hosting solutions would quickly
121
+ invite all sorts of attacks to get money from them.
122
+
123
+
124
+ A note about Mycelium blockchain adapter
125
+ ----------------------------------------
126
+ If you wish to use Mycelium blockchain adapter you MUST install bitcoind on your server (you may run it in offline mode, no need to download the whole blockchain!) and have a `bitcoin-cli` in your PATH. This
127
+ requirement is due to the need to parse raw bitcoin transaction received from Mycelium WAPI.
128
+ By default, Mycelium is included as a second (fallback) adapter and will only be used in case
129
+ BlockchainInfo one fails. It will not raise an exception until it actually tries to parse the trasaction
130
+ and finds there is no `bitcoin-cli` in PATH.
120
131
 
121
132
  Requirements
122
133
  ------------
123
134
  Ruby 2.1 or later.
124
135
 
125
-
126
136
  Credits
127
137
  -------
128
138
  Authors:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
data/lib/straight.rb CHANGED
@@ -4,15 +4,24 @@ require 'json'
4
4
  require 'uri'
5
5
  require 'open-uri'
6
6
  require 'yaml'
7
+ require 'singleton'
8
+ require 'httparty'
7
9
 
8
10
  require_relative 'straight/blockchain_adapter'
9
11
  require_relative 'straight/blockchain_adapters/blockchain_info_adapter'
10
- require_relative 'straight/blockchain_adapters/helloblock_io_adapter'
12
+ require_relative 'straight/blockchain_adapters/biteasy_adapter'
13
+ require_relative 'straight/blockchain_adapters/mycelium_adapter'
11
14
 
12
15
  require_relative 'straight/exchange_rate_adapter'
13
16
  require_relative 'straight/exchange_rate_adapters/bitpay_adapter'
14
17
  require_relative 'straight/exchange_rate_adapters/coinbase_adapter'
15
18
  require_relative 'straight/exchange_rate_adapters/bitstamp_adapter'
19
+ require_relative 'straight/exchange_rate_adapters/localbitcoins_adapter'
20
+ require_relative 'straight/exchange_rate_adapters/okcoin_adapter'
21
+ require_relative 'straight/exchange_rate_adapters/btce_adapter'
22
+ require_relative 'straight/exchange_rate_adapters/kraken_adapter'
23
+ require_relative 'straight/exchange_rate_adapters/average_rate_adapter'
24
+
16
25
 
17
26
  require_relative 'straight/order'
18
27
  require_relative 'straight/gateway'
@@ -5,24 +5,13 @@ module Straight
5
5
  # all blockchain adapters as well as supplying some useful methods.
6
6
  class Adapter
7
7
 
8
+ include Singleton
9
+
8
10
  # Raised when blockchain data cannot be retrived for any reason.
9
11
  # We're not really intereste in the precise reason, although it is
10
12
  # stored in the message.
11
13
  class RequestError < Exception; end
12
14
 
13
- # This method is a wrapper for creating an HTTP request
14
- # to various services that ancestors of this class may use
15
- # to retrieve blockchain data. Why do we need a wrapper?
16
- # Because it respects timeouts.
17
- def http_request(url)
18
- uri = URI.parse(url)
19
- begin
20
- http = uri.read(read_timeout: 4)
21
- rescue OpenURI::HTTPError => e
22
- raise RequestError, YAML::dump(e)
23
- end
24
- end
25
-
26
15
  def fetch_transaction(tid)
27
16
  raise "Please implement #fetch_transaction in #{self.to_s}"
28
17
  end
@@ -0,0 +1,74 @@
1
+ module Straight
2
+ module Blockchain
3
+
4
+ class BiteasyAdapter < Adapter
5
+
6
+ def self.mainnet_adapter
7
+ instance = self.instance
8
+ instance._initialize("https://api.biteasy.com/blockchain/v1")
9
+ instance
10
+ end
11
+
12
+ def self.testnet_adapter
13
+ raise "Not Supported Yet"
14
+ end
15
+
16
+ def _initialize(base_url)
17
+ @base_url = base_url
18
+ end
19
+
20
+ # Returns the current balance of the address
21
+ def fetch_balance_for(address)
22
+ JSON.parse(api_request("/addresses/#{address}"))['data']['balance']
23
+ end
24
+
25
+ # Returns transaction info for the tid
26
+ def fetch_transaction(tid, address: nil)
27
+ straighten_transaction JSON.parse(api_request("/transactions/#{tid}"), address: address)
28
+ end
29
+
30
+ # Returns all transactions for the address
31
+ def fetch_transactions_for(address)
32
+ transactions = JSON.parse(api_request("/transactions?address=#{address}"))['data']['transactions']
33
+ transactions.map { |t| straighten_transaction(t, address: address) }
34
+ end
35
+
36
+ private
37
+
38
+ def api_request(url)
39
+ begin
40
+ response = HTTParty.get("#{@base_url}/#{url}", timeout: 4, verify: false)
41
+ unless response.code == 200
42
+ raise RequestError, "Cannot access remote API, response code was #{response.code}"
43
+ end
44
+ response.body
45
+ rescue HTTParty::Error => e
46
+ raise RequestError, YAML::dump(e)
47
+ rescue JSON::ParserError => e
48
+ raise RequestError, YAML::dump(e)
49
+ end
50
+ end
51
+
52
+ # Converts transaction info received from the source into the
53
+ # unified format expected by users of BlockchainAdapter instances.
54
+ def straighten_transaction(transaction, address: nil)
55
+ outs = []
56
+ total_amount = 0
57
+ transaction['data']['outputs'].each do |out|
58
+ total_amount += out['value'] if address.nil? || address == out['to_address']
59
+ outs << { amount: out['value'], receiving_address: out['to_address'] }
60
+ end
61
+
62
+ {
63
+ tid: transaction['data']['hash'],
64
+ total_amount: total_amount,
65
+ confirmations: transaction['data']['confirmations'],
66
+ outs: outs
67
+ }
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -4,36 +4,67 @@ module Straight
4
4
  class BlockchainInfoAdapter < Adapter
5
5
 
6
6
  def self.mainnet_adapter
7
- self.new("http://blockchain.info")
7
+ instance = self.instance
8
+ instance._initialize("https://blockchain.info")
9
+ instance
8
10
  end
9
11
 
10
12
  def self.testnet_adapter
11
13
  raise "Not Supported Yet"
12
14
  end
13
15
 
14
- def initialize(base_url)
16
+ def _initialize(base_url)
15
17
  @latest_block = { cache_timestamp: nil, block: nil }
16
18
  @base_url = base_url
17
19
  end
18
20
 
19
21
  # Returns transaction info for the tid
20
22
  def fetch_transaction(tid, address: nil)
21
- straighten_transaction JSON.parse(http_request("#{@base_url}/rawtx/#{tid}"), address: address)
23
+ straighten_transaction JSON.parse(api_request("/rawtx/#{tid}"), address: address)
22
24
  end
23
25
 
24
26
  # Returns all transactions for the address
25
27
  def fetch_transactions_for(address)
26
- transactions = JSON.parse(http_request("#{@base_url}/rawaddr/#{address}"))['txs']
28
+ transactions = JSON.parse(api_request("/rawaddr/#{address}"))['txs']
27
29
  transactions.map { |t| straighten_transaction(t, address: address) }
28
30
  end
29
31
 
30
32
  # Returns the current balance of the address
31
33
  def fetch_balance_for(address)
32
- JSON.parse(http_request("#{@base_url}/rawaddr/#{address}"))['final_balance']
34
+ JSON.parse(api_request("/rawaddr/#{address}"))['final_balance']
35
+ end
36
+
37
+ def latest_block(force_reload: false)
38
+ # If we checked Blockchain.info latest block data
39
+ # more than a minute ago, check again. Otherwise, use cached version.
40
+ if @latest_block[:cache_timestamp].nil? ||
41
+ @latest_block[:cache_timestamp] < (Time.now - 60) ||
42
+ force_reload
43
+ @latest_block = {
44
+ cache_timestamp: Time.now,
45
+ block: JSON.parse(api_request("/latestblock"))
46
+ }
47
+ else
48
+ @latest_block
49
+ end
33
50
  end
34
51
 
35
52
  private
36
53
 
54
+ def api_request(url)
55
+ begin
56
+ response = HTTParty.get("#{@base_url}/#{url}", timeout: 4, verify: false)
57
+ unless response.code == 200
58
+ raise RequestError, "Cannot access remote API, response code was #{response.code}"
59
+ end
60
+ response.body
61
+ rescue HTTParty::Error => e
62
+ raise RequestError, YAML::dump(e)
63
+ rescue JSON::ParserError => e
64
+ raise RequestError, YAML::dump(e)
65
+ end
66
+ end
67
+
37
68
  # Converts transaction info received from the source into the
38
69
  # unified format expected by users of BlockchainAdapter instances.
39
70
  def straighten_transaction(transaction, address: nil)
@@ -59,19 +90,8 @@ module Straight
59
90
  # a certain address without making any new requests to the Blockchain API.
60
91
  def calculate_confirmations(transaction, force_latest_block_reload: false)
61
92
 
62
- # If we checked Blockchain.info latest block data
63
- # more than a minute ago, check again. Otherwise, use cached version.
64
- if @latest_block[:cache_timestamp].nil? ||
65
- @latest_block[:cache_timestamp] < (Time.now - 60) ||
66
- force_latest_block_reload
67
- @latest_block = {
68
- cache_timestamp: Time.now,
69
- block: JSON.parse(http_request("#{@base_url}/latestblock"))
70
- }
71
- end
72
-
73
93
  if transaction["block_height"]
74
- @latest_block[:block]["height"] - transaction["block_height"] + 1
94
+ latest_block(force_reload: force_latest_block_reload)[:block]["height"] - transaction["block_height"] + 1
75
95
  else
76
96
  0
77
97
  end
@@ -0,0 +1,144 @@
1
+ module Straight
2
+ module Blockchain
3
+
4
+ class MyceliumAdapter < Adapter
5
+
6
+ class NoBitcoindInstalled < Exception
7
+ def message
8
+ "You need to install bitcoind on your server and have a `bitcoin-cli` executable in PATH.\n" +
9
+ "Note that you don't have to download the blockchain and you can run bitcoind in offline mode."
10
+ end
11
+ end
12
+
13
+ require 'base64'
14
+
15
+ def self.mainnet_adapter
16
+ instance = self.instance
17
+ instance._initialize("https://mws2.mycelium.com/wapi/wapi")
18
+ instance
19
+ end
20
+
21
+ def self.testnet_adapter
22
+ instance = self.instance
23
+ instance._initialize("https://node3.mycelium.com/wapitestnet/wapi")
24
+ instance
25
+ end
26
+
27
+ def _initialize(base_url)
28
+ @latest_block = { cache_timestamp: nil, block: nil }
29
+ @base_url = base_url
30
+ end
31
+
32
+ # Returns transaction info for the tid
33
+ def fetch_transaction(tid, address: nil)
34
+ transaction = api_request('getTransactions', { txIds: [tid] })['transactions'].first
35
+ straighten_transaction transaction, address: address
36
+ end
37
+
38
+ # Supposed to returns all transactions for the address, but
39
+ # currently actually returns the first one, since we only need one.
40
+ def fetch_transactions_for(address)
41
+ tid = api_request('queryTransactionInventory', { addresses: [address], limit: 1 })["txIds"].first
42
+ tid ? [fetch_transaction(tid, address: address)] : []
43
+ end
44
+
45
+ # Returns the current balance of the address
46
+ def fetch_balance_for(address)
47
+ unspent = 0
48
+ api_request('queryUnspentOutputs', { addresses: [address]})['unspent'].each do |out|
49
+ unspent += out['value']
50
+ end
51
+ unspent
52
+ end
53
+
54
+ # Here we are using Blockchain.info API at this point because I'm not sure how
55
+ # to get latest block from Mycelium WAPI
56
+ def latest_block(force_reload: false)
57
+ # If we checked Blockchain.info latest block data
58
+ # more than a minute ago, check again. Otherwise, use cached version.
59
+ if @latest_block[:cache_timestamp].nil? ||
60
+ @latest_block[:cache_timestamp] < (Time.now - 60) ||
61
+ force_reload
62
+ @latest_block = {
63
+ cache_timestamp: Time.now,
64
+ block: JSON.parse(HTTParty.get("https://blockchain.info/latestblock", timeout: 4, verify: false).body)
65
+ }
66
+ else
67
+ @latest_block
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def api_request(method, params={})
74
+ begin
75
+ body = JSON.parse(HTTParty.post(
76
+ "#{@base_url}/#{method}",
77
+ body: params.merge({version: 1}).to_json,
78
+ headers: { 'Content-Type' => 'application/json' },
79
+ timeout: 15,
80
+ verify: false
81
+ ).body)["r"]
82
+ rescue HTTParty::Error => e
83
+ raise RequestError, YAML::dump(e)
84
+ rescue JSON::ParserError => e
85
+ raise RequestError, YAML::dump(e)
86
+ end
87
+ end
88
+
89
+ # Converts transaction info received from the source into the
90
+ # unified format expected by users of BlockchainAdapter instances.
91
+ def straighten_transaction(transaction, address: nil)
92
+
93
+ # Get the block number this transaction was included into
94
+ block_height = transaction['height']
95
+ tid = transaction['txid']
96
+
97
+ # Converting from Base64 to hex
98
+ transaction = transaction['binary'].unpack("m0").first.unpack("H*").first
99
+
100
+ # Decoding with bitcoin-cli
101
+ begin
102
+ transaction = JSON.parse(`bitcoin-cli decoderawtransaction #{transaction}`)
103
+ rescue Errno::ENOENT => e
104
+ if e.message == 'No such file or directory - bitcoin-cli'
105
+ raise NoBitcoindInstalled
106
+ else
107
+ raise e
108
+ end
109
+ end
110
+
111
+ outs = []
112
+ total_amount = 0
113
+ transaction['vout'].each do |out|
114
+ out['value'] = out['value']*10**8
115
+ total_amount += out['value'] if address.nil? || address == out['address']
116
+ outs << { amount: out['value'], receiving_address: out['address'] }
117
+ end
118
+
119
+ {
120
+ tid: tid,
121
+ total_amount: total_amount,
122
+ confirmations: calculate_confirmations(block_height),
123
+ outs: outs
124
+ }
125
+ end
126
+
127
+ # When we call #calculate_confirmations, it doesn't always make a new
128
+ # request to the blockchain API. Instead, it checks if cached_id matches the one in
129
+ # the hash. It's useful when we want to calculate confirmations for all transactions for
130
+ # a certain address without making any new requests to the Blockchain API.
131
+ def calculate_confirmations(block_height, force_latest_block_reload: false)
132
+
133
+ if block_height && block_height != -1
134
+ latest_block(force_reload: force_latest_block_reload)[:block]["height"] - block_height + 1
135
+ else
136
+ 0
137
+ end
138
+
139
+ end
140
+
141
+ end
142
+
143
+ end
144
+ end