straight 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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