straight 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2b001bb2871da76e4b657fc5d3e8812ee1e829f3
4
+ data.tar.gz: ff86dddd47ec116ee7f2f8de5c799aaedd872473
5
+ SHA512:
6
+ metadata.gz: b5fc18e1b66e8c2548dc0d515374437f467ccb8821ae45228e562ad0676568ff3ee7fd30468e50302b7cd9d294b881766f83ce45349bae78337e986bbc83c8c2
7
+ data.tar.gz: b15b7209e215e81bfa18bb260ec358368640122d83718591b3b1a09cb409f056b52badae4a2506582f8477caeea174798978f25d473422254e8fdfce3b88f13e
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Used to generate bip32 addresses
4
+ gem 'money-tree'
5
+
6
+ # Used in exchange rate adapters
7
+ gem 'satoshi-unit'
8
+
9
+ group :development do
10
+ gem "bundler", "~> 1.0"
11
+ gem "jeweler", "~> 2.0.1"
12
+ gem "github_api", "0.11.3"
13
+ end
14
+
15
+ group :test do
16
+ gem 'rspec'
17
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,76 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.3.6)
5
+ builder (3.2.2)
6
+ descendants_tracker (0.0.4)
7
+ thread_safe (~> 0.3, >= 0.3.1)
8
+ diff-lcs (1.2.5)
9
+ faraday (0.9.0)
10
+ multipart-post (>= 1.2, < 3)
11
+ ffi (1.9.3)
12
+ git (1.2.8)
13
+ github_api (0.11.3)
14
+ addressable (~> 2.3)
15
+ descendants_tracker (~> 0.0.1)
16
+ faraday (~> 0.8, < 0.10)
17
+ hashie (>= 1.2)
18
+ multi_json (>= 1.7.5, < 2.0)
19
+ nokogiri (~> 1.6.0)
20
+ oauth2
21
+ hashie (3.3.1)
22
+ highline (1.6.21)
23
+ jeweler (2.0.1)
24
+ builder
25
+ bundler (>= 1.0)
26
+ git (>= 1.2.5)
27
+ github_api
28
+ highline (>= 1.6.15)
29
+ nokogiri (>= 1.5.10)
30
+ rake
31
+ rdoc
32
+ json (1.8.1)
33
+ jwt (1.0.0)
34
+ mini_portile (0.6.0)
35
+ money-tree (0.8.7)
36
+ ffi
37
+ multi_json (1.10.1)
38
+ multi_xml (0.5.5)
39
+ multipart-post (2.0.0)
40
+ nokogiri (1.6.3.1)
41
+ mini_portile (= 0.6.0)
42
+ oauth2 (1.0.0)
43
+ faraday (>= 0.8, < 0.10)
44
+ jwt (~> 1.0)
45
+ multi_json (~> 1.3)
46
+ multi_xml (~> 0.5)
47
+ rack (~> 1.2)
48
+ rack (1.5.2)
49
+ rake (10.3.2)
50
+ rdoc (4.1.2)
51
+ json (~> 1.4)
52
+ rspec (3.1.0)
53
+ rspec-core (~> 3.1.0)
54
+ rspec-expectations (~> 3.1.0)
55
+ rspec-mocks (~> 3.1.0)
56
+ rspec-core (3.1.2)
57
+ rspec-support (~> 3.1.0)
58
+ rspec-expectations (3.1.0)
59
+ diff-lcs (>= 1.2.0, < 2.0)
60
+ rspec-support (~> 3.1.0)
61
+ rspec-mocks (3.1.0)
62
+ rspec-support (~> 3.1.0)
63
+ rspec-support (3.1.0)
64
+ satoshi-unit (0.1.6)
65
+ thread_safe (0.3.4)
66
+
67
+ PLATFORMS
68
+ ruby
69
+
70
+ DEPENDENCIES
71
+ bundler (~> 1.0)
72
+ github_api (= 0.11.3)
73
+ jeweler (~> 2.0.1)
74
+ money-tree
75
+ rspec
76
+ satoshi-unit
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Roman Snitko
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.
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ Straight
2
+ ========
3
+ > Receive bitcoin payments directly into your wallet
4
+ > Website: http://straight.romansnitko.com
5
+
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.
10
+
11
+ Straight cares about security and privacy. No private keys are stored on the server,
12
+ each order uses unique payment address. Straight notifies your application when payment is
13
+ confirmed so you can ship away.
14
+
15
+ IMPORTANT: this is a gem, not a server. It has no state and is intended to use within
16
+ an application, such as Ruby On Rails. Most likely, you want
17
+ [straight-server](https://github.com/snitko/straight-server), it is a server,
18
+ 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
+
21
+ Bitcoin donations are appreciated: 1D3PknG4Lw1gFuJ9SYenA7pboF9gtXtdcD
22
+
23
+ How it works
24
+ ------------
25
+ 1. Get a wallet that supports BIP32 keychains (e.g. bitWallet for iOS).
26
+ 2. Create an "account" and export its root extended public key (looks like xpub572b9e85...).
27
+ 3. Install Straight via Gemfile into your Ruby application.
28
+ 4. Create new Gateway with `Straight::Gateway.new`, set its properties.
29
+ 5. Start creating bitcoin orders by calling `gateway.order_for_keychain_id(...)``
30
+ 6. Set callbacks to get notified when payment is confirmed.
31
+ 7. Your wallet automatically detects incoming funds. Profit!
32
+
33
+ A little bit of explanation is due here. *Gateway* is a class, which instances are payment processors for
34
+ each online store. That is, if you have 2 online stores, you'll probably want to have a Gateway for each.
35
+
36
+ This is because each instance would have different properties specific for each store (see Usage section).
37
+ For example, each gateway may have a different callback or a different number of transaction confirmations
38
+ required to set the order status to PAID.
39
+
40
+ A new *Order* is created when you would like to give your customer an address to pay for your product or service.
41
+ It will track whether new transactions arrived at the address, check how much money was sent and change its status
42
+ accordingly.
43
+
44
+ Installation
45
+ ------------
46
+
47
+ gem install straight
48
+
49
+ Usage
50
+ -----
51
+
52
+ require 'straight'
53
+
54
+ # Create a new gateway first and configure all the settings
55
+ #
56
+ gateway = Gateway.new
57
+ gateway.pubkey = 'xpub12345'
58
+ gateway.confirmations_required = 0
59
+ gateway.order_class = 'Straight::Order'
60
+ gateway.default_currency = 'BTC'
61
+ gateway.name = 'my gateway'
62
+
63
+ # Set the callback for orders' status changes
64
+ #
65
+ gateway.order_callbacks = [
66
+ lambda { |order| puts "Order status changed to #{order.status}" }
67
+ ]
68
+
69
+ # Create a new order
70
+ #
71
+ # Remember you should always use a new, unique keychain_id, should preferably
72
+ # be consecutive.
73
+ #
74
+ order = gateway.order_for_keychain_id(amount: 1, keychain_id: 1)
75
+
76
+ # Start tracking the order
77
+ #
78
+ Thread.new { order.start_periodic_status_check }
79
+
80
+
81
+ Including Straight::Module vs Using Straight::Order class
82
+ ---------------------------------------------------------
83
+ As this library is intended to use within an application and is not a standalone software itself,
84
+ I made a decision to provide a simple way to integrate it into existing ORMs. While there is currently
85
+ no official documentation as to how integrate it into ActiveRecord, you should be able to easily do it
86
+ like this:
87
+
88
+ class Order < ActiveRecord::Base
89
+ include Straight::OrderModule
90
+ ...
91
+ end
92
+
93
+ Same goes for the `GatewayModule`. It works the same way with other ORMs, such as Sequel (on which
94
+ StraightServer is built).
95
+
96
+ The right way to implement this would be to do it the other way: inherit from `Straight::Order`, then
97
+ include `ActiveRecord`, but at this point `ActiveRecord` doesn't work this way. Furthermore, some other libraries, like `Sequel`,
98
+ also require you to inherit from them. Thus, the module.
99
+
100
+ When this module is included, it doesn't actually *include* all the methods, some are prepended (see Ruby docs on #prepend).
101
+ It is important specifically for getters and setters and as a general rule only getters and setters are prepended.
102
+
103
+ 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
+
107
+
108
+ Important Considerations
109
+ ------------------------
110
+ There is no magical link between the wallet and your server. Server creates new addresses for each order
111
+ based on sequential indexes. Your wallet scans blockchain generating the same sequential addresses too
112
+ (using a sliding window of several addresses). In order for this to work, as you may have guessed already,
113
+ all orders should be indexed sequentially, not randomly.
114
+
115
+ Why can't we just derive new addresses from order UUID, or assign them to orders? The reason is that your
116
+ wallet will have to integrate with your very own database and it may be enormously cumbersome to implement
117
+ 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
+
121
+ Requirements
122
+ ------------
123
+ Ruby 2.1 or later.
124
+
125
+
126
+ Credits
127
+ -------
128
+ Authors:
129
+ [Roman Snitko](http://romansnitko.com) and
130
+ [Oleg Andreev](http://oleganza.com)
131
+
132
+ Licence: MIT (see the LICENCE file)
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
17
+ gem.name = "straight"
18
+ gem.homepage = "http://github.com/snitko/straight"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{An engine for the Straight payment gateway software}
21
+ gem.description = %Q{An engine for the Straight payment gateway software. Requires no state to be saved (that is, no storage or DB). Its responsibilities only include processing data coming from an actual gateway.}
22
+ gem.email = "roman.snitko@gmail.com"
23
+ gem.authors = ["Roman Snitko"]
24
+ end
25
+ Jeweler::RubygemsDotOrgTasks.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,49 @@
1
+ module Straight
2
+
3
+ module Blockchain
4
+ # A base class, providing guidance for the interfaces of
5
+ # all blockchain adapters as well as supplying some useful methods.
6
+ class Adapter
7
+
8
+ # Raised when blockchain data cannot be retrived for any reason.
9
+ # We're not really intereste in the precise reason, although it is
10
+ # stored in the message.
11
+ class RequestError < Exception; end
12
+
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
+ def fetch_transaction(tid)
27
+ raise "Please implement #fetch_transaction in #{self.to_s}"
28
+ end
29
+
30
+ def fetch_transactions_for(address)
31
+ raise "Please implement #fetch_transactions_for in #{self.to_s}"
32
+ end
33
+
34
+ def fetch_balance_for(address)
35
+ raise "Please implement #fetch_balance_for in #{self.to_s}"
36
+ end
37
+
38
+ private
39
+
40
+ # Converts transaction info received from the source into the
41
+ # unified format expected by users of BlockchainAdapter instances.
42
+ def straighten_transaction(transaction)
43
+ raise "Please implement #straighten_transaction in #{self.to_s}"
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,84 @@
1
+ module Straight
2
+ module Blockchain
3
+
4
+ class BlockchainInfoAdapter < Adapter
5
+
6
+ def self.mainnet_adapter
7
+ self.new("http://blockchain.info")
8
+ end
9
+
10
+ def self.testnet_adapter
11
+ raise "Not Supported Yet"
12
+ end
13
+
14
+ def initialize(base_url)
15
+ @latest_block = { cache_timestamp: nil, block: nil }
16
+ @base_url = base_url
17
+ end
18
+
19
+ # Returns transaction info for the tid
20
+ def fetch_transaction(tid, address: nil)
21
+ straighten_transaction JSON.parse(http_request("#{@base_url}/rawtx/#{tid}"), address: address)
22
+ end
23
+
24
+ # Returns all transactions for the address
25
+ def fetch_transactions_for(address)
26
+ transactions = JSON.parse(http_request("#{@base_url}/rawaddr/#{address}"))['txs']
27
+ transactions.map { |t| straighten_transaction(t, address: address) }
28
+ end
29
+
30
+ # Returns the current balance of the address
31
+ def fetch_balance_for(address)
32
+ JSON.parse(http_request("#{@base_url}/rawaddr/#{address}"))['final_balance']
33
+ end
34
+
35
+ private
36
+
37
+ # Converts transaction info received from the source into the
38
+ # unified format expected by users of BlockchainAdapter instances.
39
+ def straighten_transaction(transaction, address: nil)
40
+ outs = []
41
+ total_amount = 0
42
+ transaction['out'].each do |out|
43
+ total_amount += out['value'] if address.nil? || address == out['addr']
44
+ outs << { amount: out['value'], receiving_address: out['addr'] }
45
+ end
46
+
47
+ {
48
+ tid: transaction['hash'],
49
+ total_amount: total_amount,
50
+ confirmations: calculate_confirmations(transaction),
51
+ outs: outs
52
+ }
53
+ end
54
+
55
+
56
+ # When we call #calculate_confirmations, it doesn't always make a new
57
+ # request to the blockchain API. Instead, it checks if cached_id matches the one in
58
+ # the hash. It's useful when we want to calculate confirmations for all transactions for
59
+ # a certain address without making any new requests to the Blockchain API.
60
+ def calculate_confirmations(transaction, force_latest_block_reload: false)
61
+
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
+ if transaction["block_height"]
74
+ @latest_block[:block]["height"] - transaction["block_height"] + 1
75
+ else
76
+ 0
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,53 @@
1
+ module Straight
2
+ module Blockchain
3
+
4
+ class HelloblockIoAdapter < Adapter
5
+
6
+ def self.mainnet_adapter
7
+ self.new("https://mainnet.helloblock.io/v1")
8
+ end
9
+
10
+ def self.testnet_adapter
11
+ raise "Not Supported Yet"
12
+ end
13
+
14
+ def initialize(base_url)
15
+ @base_url = base_url
16
+ end
17
+
18
+ # Returns transaction info for the tid
19
+ def fetch_transaction(tid, address: nil)
20
+ straighten_transaction JSON.parse(http_request("#{@base_url}/transactions/#{tid}"))['data']['transaction'], address: address
21
+ end
22
+
23
+ # Returns all transactions for the address
24
+ def fetch_transactions_for(address)
25
+ transactions = JSON.parse(http_request("#{@base_url}/addresses/#{address}/transactions"))['data']['transactions']
26
+ transactions.map { |t| straighten_transaction(t, address: address) }
27
+ end
28
+
29
+ # Returns the current balance of the address
30
+ def fetch_balance_for(address)
31
+ JSON.parse(http_request("#{@base_url}/addresses/#{address}"))['data']['address']['balance']
32
+ end
33
+
34
+ private
35
+
36
+ # Converts transaction info received from the source into the
37
+ # unified format expected by users of BlockchainAdapter instances.
38
+ def straighten_transaction(transaction, address: nil)
39
+ outs = transaction['outputs'].map do |out|
40
+ { amount: out['value'], receiving_address: out['address'] } if address.nil? || address == out['address']
41
+ end.compact
42
+ {
43
+ tid: transaction['txHash'],
44
+ total_amount: outs.inject(0) { |sum, o| sum + o[:amount] },
45
+ confirmations: transaction['confirmations'],
46
+ outs: outs
47
+ }
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class Adapter
5
+
6
+ class CurrencyNotFound < Exception; end
7
+ class FetchingFailed < Exception; end
8
+ class CurrencyNotSupported < Exception; end
9
+
10
+ def initialize(rates_expire_in: 1800)
11
+ @rates_expire_in = rates_expire_in # in seconds
12
+ end
13
+
14
+ def convert_from_currency(amount_in_currency, btc_denomination: :satoshi, currency: 'USD')
15
+ btc_amount = amount_in_currency.to_f/rate_for(currency)
16
+ Satoshi.new(btc_amount, from_unit: :btc, to_unit: btc_denomination).to_unit
17
+ end
18
+
19
+ def convert_to_currency(amount, btc_denomination: :satoshi, currency: 'USD')
20
+ amount_in_btc = Satoshi.new(amount.to_f, from_unit: btc_denomination).to_btc
21
+ amount_in_btc*rate_for(currency)
22
+ end
23
+
24
+ def fetch_rates!
25
+ raise "FETCH_URL is not defined!" unless self.class::FETCH_URL
26
+ uri = URI.parse(self.class::FETCH_URL)
27
+ begin
28
+ @rates = JSON.parse(uri.read(read_timeout: 4))
29
+ @rates_updated_at = Time.now
30
+ rescue OpenURI::HTTPError => e
31
+ raise FetchingFailed
32
+ end
33
+ end
34
+
35
+ def rate_for(currency_code)
36
+ if !@rates_updated_at || (Time.now - @rates_updated_at) > @rates_expire_in
37
+ fetch_rates!
38
+ end
39
+ nil # this should be changed in descendant classes
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,19 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class BitpayAdapter < Adapter
5
+
6
+ FETCH_URL = 'https://bitpay.com/api/rates'
7
+
8
+ def rate_for(currency_code)
9
+ super
10
+ @rates.each do |r|
11
+ return r['rate'].to_f if r['code'] == currency_code
12
+ end
13
+ raise CurrencyNotSupported
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class BitstampAdapter < Adapter
5
+
6
+ FETCH_URL = 'https://www.bitstamp.net/api/ticker/'
7
+
8
+ def rate_for(currency_code)
9
+ super
10
+ raise CurrencyNotSupported if currency_code != 'USD'
11
+ @rates['last'].to_f
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class CoinbaseAdapter < Adapter
5
+
6
+ FETCH_URL = 'https://coinbase.com/api/v1/currencies/exchange_rates'
7
+
8
+ def rate_for(currency_code)
9
+ super
10
+ if rate = @rates["btc_to_#{currency_code.downcase}"]
11
+ return rate.to_f
12
+ end
13
+ raise CurrencyNotSupported
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+ end