straight 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +76 -0
- data/LICENSE.txt +20 -0
- data/README.md +132 -0
- data/Rakefile +25 -0
- data/VERSION +1 -0
- data/lib/straight/blockchain_adapter.rb +49 -0
- data/lib/straight/blockchain_adapters/blockchain_info_adapter.rb +84 -0
- data/lib/straight/blockchain_adapters/helloblock_io_adapter.rb +53 -0
- data/lib/straight/exchange_rate_adapter.rb +45 -0
- data/lib/straight/exchange_rate_adapters/bitpay_adapter.rb +19 -0
- data/lib/straight/exchange_rate_adapters/bitstamp_adapter.rb +17 -0
- data/lib/straight/exchange_rate_adapters/coinbase_adapter.rb +19 -0
- data/lib/straight/gateway.rb +182 -0
- data/lib/straight/order.rb +195 -0
- data/lib/straight.rb +18 -0
- data/spec/lib/blockchain_adapters/blockchain_info_spec.rb +52 -0
- data/spec/lib/blockchain_adapters/helloblock_io_spec.rb +43 -0
- data/spec/lib/exchange_rate_adapter_spec.rb +50 -0
- data/spec/lib/exchange_rate_adapters/bitpay_adapter_spec.rb +14 -0
- data/spec/lib/exchange_rate_adapters/bitstamp_adapter_spec.rb +14 -0
- data/spec/lib/exchange_rate_adapters/coinbase_adapter_spec.rb +14 -0
- data/spec/lib/gateway_spec.rb +75 -0
- data/spec/lib/order_spec.rb +112 -0
- data/spec/spec_helper.rb +1 -0
- metadata +144 -0
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
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
|