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 +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
|