pochette 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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/pochette/backends/bitcoin_core.rb +105 -0
- data/lib/pochette/backends/trendy.rb +92 -0
- data/lib/pochette/transaction_builder.rb +157 -0
- data/lib/pochette/trezor_transaction_builder.rb +90 -0
- data/lib/pochette/version.rb +3 -0
- data/lib/pochette.rb +17 -0
- data/pochette.gemspec +32 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 65f4b14b12eac44180a3b5b9cdb863ad6c2aa1bc
|
4
|
+
data.tar.gz: 9ccb9cf9b0991fcaab88913f2d3785780cfc99f2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dcc3594a14fde23c00a09465ef3a5678b06aa240e4a723dcbd233564660139cf8e98704e5e5a330e8c08c45eeb2164ac579c2bde9fc4e2d7bab8cc606368b189
|
7
|
+
data.tar.gz: 1959ce4d4eb08d4bb558f11e65272cdb9d9f92c9069caca19d4e90dd3ace44a35059f786754defe2e5da853b5dc87544f4c964d01fc2e281ae0c614e03c05e84
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Nubis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# Pochette
|
2
|
+
|
3
|
+
Pochette is a Bitcoin Wallet for developers, or more accurately a tool for building
|
4
|
+
"single purpose wallets".
|
5
|
+
|
6
|
+
It's used extensively at [Bitex.la](https://bitex.la) from checking and crediting customer
|
7
|
+
bitcoin deposits to preparing transactions with bip32 paths instead of input addresess,
|
8
|
+
ready to be signed with a [Trezor Device](https://www.bitcointrezor.com/)
|
9
|
+
|
10
|
+
Pochette offers a common interface to full bitcoin nodes like
|
11
|
+
[Bitcoin Core](https://bitcoin.org/en/download) or [Toshi](http://toshi.io)
|
12
|
+
and will let you run several instances of each one of them simultaneously
|
13
|
+
always choosing the most recent node to query.
|
14
|
+
|
15
|
+
It also provides a Pochette::TransactionBuilder class which receives a list
|
16
|
+
of 'source' addresses and a list of recipients as "address/amount" pairs.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'pochette'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install pochette
|
33
|
+
|
34
|
+
## The Pochette::TransactionBuilder
|
35
|
+
|
36
|
+
The TransactionBuilder builds transactions from a list of source addresses and a list of recipients,
|
37
|
+
using Pochette.backend to fetch unspent outputs and related transaction data.
|
38
|
+
Instantiating will perform all the required queries, you'll be left with a
|
39
|
+
TransactionBuilder object that is either valid? or not, and if valid,
|
40
|
+
you can query the results via to_hash.
|
41
|
+
|
42
|
+
The TransactionBuilder's initializer receives a single options hash with:
|
43
|
+
addresses:
|
44
|
+
List of addresses in wallet.
|
45
|
+
We will be spending their unspent outputs.
|
46
|
+
outputs:
|
47
|
+
List of pairs [recipient_address, amount]
|
48
|
+
This will not be all the final outputs in the transaction,
|
49
|
+
as a 'change' output may be added if needed.
|
50
|
+
utxo_blacklist:
|
51
|
+
List of utxos to ignore, a list of pairs [transaction hash, position]
|
52
|
+
change_address:
|
53
|
+
Change address to use. Will default to the first source address.
|
54
|
+
fee_per_kb:
|
55
|
+
Defaults to 10000 satoshis.
|
56
|
+
spend_all:
|
57
|
+
Wether to spend all available utxos or just select enough to
|
58
|
+
cover the given outputs.
|
59
|
+
|
60
|
+
TODO: Document as_hash output.
|
61
|
+
|
62
|
+
## Building for Trezor with Pochette::TrezorTransactionBuilder
|
63
|
+
|
64
|
+
Same as TransactionBuilder but outputs a transaction hash with all the
|
65
|
+
required data to create and sign a transaction using a BitcoinTrezor.
|
66
|
+
|
67
|
+
* Uses BIP32 addresses instead of regular strings.
|
68
|
+
Each address is represented as a pair, with the public address string
|
69
|
+
and the BIP32 path as a list of integers, for example:
|
70
|
+
['public-address-as-string', [44, 1, 3, 11]]
|
71
|
+
|
72
|
+
* Includes associated transaction data for each input being spent,
|
73
|
+
ready to be consumed by your Trezor device.
|
74
|
+
|
75
|
+
* Outputs are represented as JSON with script_type as expected by Trezor.
|
76
|
+
{ script_type: 'PAYTOADDRESS',
|
77
|
+
address: '1address-as-string',
|
78
|
+
amount: amount_in_satoshis }
|
79
|
+
|
80
|
+
TODO: Document as_hash output.
|
81
|
+
|
82
|
+
## Using a Bitcoin-Core backend.
|
83
|
+
|
84
|
+
Pochette will connect to your bitcoin-core node via JSON-RPC, using the
|
85
|
+
[bitcoin-rpc gem](https://github.com/bitex-la/bitcoin-rpc)
|
86
|
+
|
87
|
+
To properly use Pochette you need to be running your bitcoin node with setting the
|
88
|
+
"-txindex=1" option to get a full transaction index.
|
89
|
+
[Learn more about -txindex=1](http://bitcoin.stackexchange.com/questions/35707/what-are-pros-and-cons-of-txindex-option)
|
90
|
+
|
91
|
+
Also, if you're creating new addresses and want bitcoin-core to track them you'll want to import
|
92
|
+
them using the bitcoin-rpc gem, like so:
|
93
|
+
|
94
|
+
>>> BitcoinRpc::Client.new('http://user:pass@your_server').importaddress('1PUBLICADDRESS', '', false)
|
95
|
+
|
96
|
+
Setting up bitcoin-core as a backend can be done like this:
|
97
|
+
|
98
|
+
>>> Pochette.backend = Pochette::Backends::BitcoinCore.new('http://user:pass@your_server')
|
99
|
+
|
100
|
+
## Using a Toshi backend.
|
101
|
+
|
102
|
+
Pochette will connect to your Toshi node's postgres database directly.
|
103
|
+
It's provided as a separate gem as it depends on the pg gem which needs local
|
104
|
+
postgres extensions to work. You may need to add some extra indexes to your postgres
|
105
|
+
to speed things up when using Pochette.
|
106
|
+
[See the gem readme](https://github.com/bitex-la/pochette-toshi) for more info.
|
107
|
+
|
108
|
+
## Using the best of many available backends
|
109
|
+
|
110
|
+
Pochette provides a higher level Backend Pochette::Backends::Trendy which chooses
|
111
|
+
between a pool of available backends always using the one at the highest block height,
|
112
|
+
(but biased towards using the incumbent backend).
|
113
|
+
|
114
|
+
This is useful for automatic fallbacks and redundancy, you could also mix Toshi and Bitcoin-Core
|
115
|
+
backends and use whatever looks more up to date.
|
116
|
+
|
117
|
+
>>> alpha = Pochette::Backends::BitcoinCore.new('http://user:pass@alpha_host')
|
118
|
+
>>> beta = Pochette::Backends::BitcoinCore.new('http://user:pass@beta_host')
|
119
|
+
>>> Pochette.backend = Pochette::Backends::Trendy.new([alpha, beta])
|
120
|
+
|
121
|
+
## Development
|
122
|
+
|
123
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
124
|
+
|
125
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
126
|
+
|
127
|
+
## Contributing
|
128
|
+
|
129
|
+
1. Fork it ( https://github.com/[my-github-username]/pochette/fork )
|
130
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
131
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
132
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
133
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pochette"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# A bitcoin backend that uses bitcoin-core to retrieve information.
|
2
|
+
# See Pochette::Backends::Trendy to learn more about the backend
|
3
|
+
# interface and contract.
|
4
|
+
class Pochette::Backends::BitcoinCore
|
5
|
+
def initialize(rpc_url)
|
6
|
+
@rpc_url = rpc_url
|
7
|
+
end
|
8
|
+
|
9
|
+
def client
|
10
|
+
BitcoinRpc::Client.new(@rpc_url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def incoming_for(addresses, min_date)
|
14
|
+
return [] if addresses.empty?
|
15
|
+
|
16
|
+
addresses = addresses.to_set
|
17
|
+
block_height = client.getblockcount
|
18
|
+
from_block = block_height - ((Time.now - min_date) / 60 / 60 * 6).ceil
|
19
|
+
block_hash = client.getblockhash(from_block)
|
20
|
+
|
21
|
+
result = []
|
22
|
+
client.listsinceblock(block_hash, 1, true)[:transactions].each do |t|
|
23
|
+
next unless t[:category] == 'receive'
|
24
|
+
next unless addresses.include?(t[:address])
|
25
|
+
senders = []
|
26
|
+
client.getrawtransaction(t[:txid], 1)[:vin].each do |i|
|
27
|
+
raw_sender = client.getrawtransaction(i[:txid], 1)
|
28
|
+
senders += raw_sender[:vout][i[:vout]][:scriptPubKey][:addresses]
|
29
|
+
end
|
30
|
+
result << [(t[:amount] * 1_0000_0000).to_i, t[:address], t[:txid],
|
31
|
+
t[:confirmations], t[:vout], senders.join(',')]
|
32
|
+
end
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
def balances_for(addresses, confirmations)
|
37
|
+
return {} if addresses.empty?
|
38
|
+
result = addresses.reduce({}) do |accum, address|
|
39
|
+
accum[address] = [0,0,0,0,0,0]
|
40
|
+
accum
|
41
|
+
end
|
42
|
+
|
43
|
+
# Populate confirmed received
|
44
|
+
client.listreceivedbyaddress(confirmations, false, true).each do |a|
|
45
|
+
next unless result[a[:address]]
|
46
|
+
result[a[:address]][0] = a[:amount]
|
47
|
+
result[a[:address]][1] = a[:amount] # Will substract UTXO from it later.
|
48
|
+
end
|
49
|
+
|
50
|
+
# Populate unconfirmed received
|
51
|
+
client.listreceivedbyaddress(0, false, true).each do |a|
|
52
|
+
next unless result[a[:address]]
|
53
|
+
result[a[:address]][3] = a[:amount]
|
54
|
+
result[a[:address]][4] = a[:amount] # Will substract UTXO from it later.
|
55
|
+
end
|
56
|
+
|
57
|
+
# Fix sent amounts to not include amounts which werent actually sent.
|
58
|
+
client.listunspent(0, 99999999, addresses).each do |utxo|
|
59
|
+
if utxo[:confirmations] >= confirmations
|
60
|
+
result[utxo[:address]][1] -= utxo[:amount]
|
61
|
+
end
|
62
|
+
result[utxo[:address]][4] -= utxo[:amount]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Totals are just one substraction away
|
66
|
+
result.each do |k, v|
|
67
|
+
v[2] = v[0] - v[1]
|
68
|
+
v[5] = v[3] - v[4]
|
69
|
+
end
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
def list_unspent(addresses)
|
75
|
+
return nil if addresses.empty?
|
76
|
+
client.listunspent(1, 99999999, addresses).collect do |u|
|
77
|
+
[u[:address], u[:txid], u[:vout], (u[:amount] * 1_0000_0000).to_i]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def list_transactions(txids)
|
82
|
+
return nil if txids.empty?
|
83
|
+
txids.collect do |txid|
|
84
|
+
tx = client.getrawtransaction(txid, 1)
|
85
|
+
inputs = tx[:vin].collect do |i|
|
86
|
+
{ prev_hash: i[:txid],
|
87
|
+
prev_index: i[:vout],
|
88
|
+
sequence: i[:sequence],
|
89
|
+
script_sig: i[:scriptSig][:hex]
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
outputs = tx[:vout].collect do |o|
|
94
|
+
{ amount: (o[:value] * 1_0000_0000).to_i, script_pubkey: o[:scriptPubKey][:hex] }
|
95
|
+
end
|
96
|
+
|
97
|
+
{ hash: tx[:txid], version: tx[:version], lock_time: tx[:locktime],
|
98
|
+
inputs: inputs, bin_outputs: outputs}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def block_height
|
103
|
+
client.getinfo[:blocks]
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# The Trendy backend delegates calls to Toshi or BitcoinCore backends
|
2
|
+
# to list unspent outputs, incoming payments, etcetera.
|
3
|
+
# It chooses the backend to use based on its latest block, trying
|
4
|
+
# to always use the most up to date one.
|
5
|
+
# Its public instance methods are the contract to be used by any
|
6
|
+
# other backend, all Pochette backends must define
|
7
|
+
# thes public methods (Except for the initializer).
|
8
|
+
class Pochette::Backends::Trendy
|
9
|
+
def initialize(backends)
|
10
|
+
@backends = backends
|
11
|
+
end
|
12
|
+
|
13
|
+
# Lists all bitcoins received by a list of addresses
|
14
|
+
# after a given date. Includes both confirmed and unconfirmed
|
15
|
+
# transactions, unconfirmed transactions have a nil block height.
|
16
|
+
# Returns a list of lists as following:
|
17
|
+
# amount: Amount received (in satoshis)
|
18
|
+
# address: Public address receiving the amount.
|
19
|
+
# txid: The hash for the transaction that received it.
|
20
|
+
# confirmations: Transaction confirmations
|
21
|
+
# output position: To disambiguate in case address received more than once.
|
22
|
+
# sender addresses: Comma separated list of input addresses,
|
23
|
+
# used to identify deposits from trusted parties.
|
24
|
+
# can be used to identify deposits from trusted parties.
|
25
|
+
def incoming_for(addresses, min_date)
|
26
|
+
backend.incoming_for(addresses, min_date)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Gets the total received, spent and balance for
|
30
|
+
# a list of addresses. Confirmed balances are enforced to have a number
|
31
|
+
# of confirmation, appearing in a block is not enough.
|
32
|
+
# Returns a hash with:
|
33
|
+
# { address: [received, sent, total,
|
34
|
+
# unconfirmed_received, unconfirmed_sent, unconfirmed_total],
|
35
|
+
# ...}
|
36
|
+
def balances_for(addresses, confirmations)
|
37
|
+
backend.balances_for(addresses, confirmations)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get unspent utxos for the given addresses,
|
41
|
+
# returns a list of lists like so:
|
42
|
+
# [[address, txid, position (vout), amount (in satoshis)], ...]
|
43
|
+
def list_unspent(addresses)
|
44
|
+
backend.list_unspent(addresses)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Gets information for the given transactions
|
48
|
+
# returns a list of objects, like so:
|
49
|
+
# [
|
50
|
+
# { hash: txid,
|
51
|
+
# version: 1,
|
52
|
+
# lock_time: 0,
|
53
|
+
# inputs: [
|
54
|
+
# { prev_hash: txid,
|
55
|
+
# prev_index: 0,
|
56
|
+
# sequence: 0,
|
57
|
+
# script_sig: hex_signature
|
58
|
+
# },
|
59
|
+
# ...
|
60
|
+
# ],
|
61
|
+
# bin_outputs: [
|
62
|
+
# { amount: amount (as satoshis),
|
63
|
+
# script_pubkey: hex_script
|
64
|
+
# },
|
65
|
+
# ...
|
66
|
+
# ]
|
67
|
+
# }
|
68
|
+
def list_transactions(txids)
|
69
|
+
backend.list_transactions(txids)
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
# Chooses a backend to use, gives a small advantage to incumbent backend.
|
75
|
+
def backend
|
76
|
+
if @backend.nil?
|
77
|
+
@last_choice_on = Time.now
|
78
|
+
return @backend = @backends.sort_by(&:block_height).last
|
79
|
+
end
|
80
|
+
|
81
|
+
return @backend if @last_choice_on > 10.minutes.ago
|
82
|
+
|
83
|
+
@last_choice_on = Time.now
|
84
|
+
challenger, height = @backends
|
85
|
+
.reject{|b| b == @backend }
|
86
|
+
.collect{|b| [b, b.block_height] }
|
87
|
+
.sort_by(&:last)
|
88
|
+
.last
|
89
|
+
|
90
|
+
@backend = height > (@backend.block_height + 1) ? challenger : @backend
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# Builds transactions from a list of source addresses and a list of recipients.
|
2
|
+
# Uses Pochette.backend to fetch unspent outputs and related transaction data.
|
3
|
+
# Instantiating will perform all the given queries, you'll be left with a
|
4
|
+
# TransactionBuilder object that is either valid? or not, and if valid
|
5
|
+
# you can query the results via to_hash.
|
6
|
+
# Options:
|
7
|
+
# addresses:
|
8
|
+
# List of addresses in wallet.
|
9
|
+
# We will be spending their unspent outputs.
|
10
|
+
# outputs:
|
11
|
+
# List of pairs [recipient_address, amount]
|
12
|
+
# This will not be all the final outputs in the transaction,
|
13
|
+
# as a 'change' output may be added if needed.
|
14
|
+
# utxo_blacklist:
|
15
|
+
# List of utxos to ignore, a list of pairs [transaction hash, position]
|
16
|
+
# change_address:
|
17
|
+
# Change address to use. Will default to the first source address.
|
18
|
+
# fee_per_kb:
|
19
|
+
# Defaults to 10000 satoshis.
|
20
|
+
# spend_all:
|
21
|
+
# Wether to spend all available utxos or just select enough to
|
22
|
+
# cover the given outputs.
|
23
|
+
|
24
|
+
class Pochette::TransactionBuilder
|
25
|
+
|
26
|
+
cattr_accessor(:dust_size){ 546 }
|
27
|
+
cattr_accessor(:output_size){ 149 }
|
28
|
+
cattr_accessor(:input_size){ 35 }
|
29
|
+
cattr_accessor(:network_minimum_fee){ 10000 }
|
30
|
+
cattr_accessor(:default_fee_per_kb){ 10000 }
|
31
|
+
|
32
|
+
def initialize(options)
|
33
|
+
initialize_options(options)
|
34
|
+
return unless valid?
|
35
|
+
initialize_fee
|
36
|
+
initialize_outputs
|
37
|
+
return unless valid?
|
38
|
+
select_utxos
|
39
|
+
add_change_output
|
40
|
+
validate_final_amounts
|
41
|
+
end
|
42
|
+
|
43
|
+
def as_hash
|
44
|
+
return nil unless valid?
|
45
|
+
{ amount: inputs_amount,
|
46
|
+
fee: inputs_amount - outputs_amount,
|
47
|
+
inputs: inputs,
|
48
|
+
outputs: outputs}
|
49
|
+
end
|
50
|
+
|
51
|
+
def valid?
|
52
|
+
errors.size == 0
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :errors
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
attr_accessor :options
|
60
|
+
attr_accessor :addresses
|
61
|
+
attr_accessor :inputs
|
62
|
+
attr_accessor :outputs
|
63
|
+
attr_writer :errors
|
64
|
+
|
65
|
+
def initialize_options(options)
|
66
|
+
self.options = options
|
67
|
+
self.errors ||= []
|
68
|
+
self.addresses = options[:addresses]
|
69
|
+
if addresses.nil? || addresses.empty?
|
70
|
+
return errors << :no_addresses_given
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize_fee
|
75
|
+
@minimum_fee = fee_for_bytes(10)
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_input_fee(count = 1)
|
79
|
+
@minimum_fee += fee_for_bytes(input_size) * count
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_output_fee(count = 1)
|
83
|
+
@minimum_fee += fee_for_bytes(output_size) * count
|
84
|
+
end
|
85
|
+
|
86
|
+
def minimum_fee(stage=0)
|
87
|
+
[@minimum_fee + stage, network_minimum_fee].max
|
88
|
+
end
|
89
|
+
|
90
|
+
def fee_for_bytes(bytes)
|
91
|
+
bytes.to_d / 1000.to_d * (options[:fee_per_kb] || default_fee_per_kb)
|
92
|
+
end
|
93
|
+
|
94
|
+
def initialize_outputs
|
95
|
+
self.outputs = options[:outputs]
|
96
|
+
if (outputs.nil? || outputs.empty?) && !options[:spend_all]
|
97
|
+
errors << :try_with_spend_all
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
add_output_fee(outputs.size)
|
102
|
+
if outputs.any?{|o| o[1] < dust_size }
|
103
|
+
errors << :dust_in_outputs
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def select_utxos
|
108
|
+
utxo_blacklist = options[:utxo_blacklist] || []
|
109
|
+
all_utxos = Pochette.backend.list_unspent(addresses)
|
110
|
+
available_utxos = all_utxos.reject do |utxo|
|
111
|
+
utxo_blacklist.include?(utxo[1]) || utxo_is_blacklisted?(utxo)
|
112
|
+
end
|
113
|
+
|
114
|
+
self.inputs = []
|
115
|
+
if options[:spend_all]
|
116
|
+
self.inputs = available_utxos
|
117
|
+
add_input_fee(inputs.size)
|
118
|
+
else
|
119
|
+
needed = outputs.collect{|o| o.last }.sum
|
120
|
+
collected = 0.to_d
|
121
|
+
available_utxos.each do |utxo|
|
122
|
+
break if collected >= needed + minimum_fee
|
123
|
+
collected += utxo[3]
|
124
|
+
self.inputs << utxo
|
125
|
+
add_input_fee
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_change_output
|
131
|
+
change = inputs_amount - outputs_amount -
|
132
|
+
minimum_fee(fee_for_bytes(output_size))
|
133
|
+
change_address = options[:change_address] || addresses.first
|
134
|
+
if change > dust_size
|
135
|
+
outputs << [change_address, change]
|
136
|
+
add_output_fee
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def inputs_amount
|
141
|
+
inputs.collect{|x| x[3] }.sum
|
142
|
+
end
|
143
|
+
|
144
|
+
def outputs_amount
|
145
|
+
outputs.collect(&:last).sum
|
146
|
+
end
|
147
|
+
|
148
|
+
def validate_final_amounts
|
149
|
+
if inputs_amount < (outputs_amount + minimum_fee)
|
150
|
+
errors << :insufficient_funds
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def utxo_is_blacklisted?(utxo)
|
155
|
+
false
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# Same as TransactionBuilder but outputs a transaction hash with all the
|
2
|
+
# required data to create and sign a transaction using a BitcoinTrezor.
|
3
|
+
# * Uses BIP32 addresses instead of regular strings.
|
4
|
+
# Each address is represented as a pair, with the public address string
|
5
|
+
# and the BIP32 path as a list of integers, for example:
|
6
|
+
# ['public-address-as-string', [44, 1, 3, 11]]
|
7
|
+
#
|
8
|
+
# * Includes associated transaction data for each input being spent,
|
9
|
+
# ready to be consumed by your Trezor device.
|
10
|
+
#
|
11
|
+
# * Outputs are represented as JSON with script_type as expected by Trezor.
|
12
|
+
# { script_type: 'PAYTOADDRESS',
|
13
|
+
# address: '1address-as-string',
|
14
|
+
# amount: amount_in_satoshis }
|
15
|
+
#
|
16
|
+
# Options:
|
17
|
+
# bip32_addresses:
|
18
|
+
# List of [address, path] pairs in wallet.
|
19
|
+
# We will be spending their unspent outputs.
|
20
|
+
# outputs:
|
21
|
+
# List of pairs [recipient_address, amount]
|
22
|
+
# This will not be all the final outputs in the transaction,
|
23
|
+
# as a 'change' output may be added if needed.
|
24
|
+
# utxo_blacklist:
|
25
|
+
# List of utxos to ignore, a list of pairs [transaction hash, position]
|
26
|
+
# change_address:
|
27
|
+
# Change address to use. Will default to the first source address.
|
28
|
+
# fee_per_kb:
|
29
|
+
# Defaults to 10000 satoshis.
|
30
|
+
# spend_all:
|
31
|
+
# Wether to spend all available utxos or just select enough to
|
32
|
+
# cover the given outputs.
|
33
|
+
|
34
|
+
class Pochette::TrezorTransactionBuilder < Pochette::TransactionBuilder
|
35
|
+
def initialize(options)
|
36
|
+
options = options.dup
|
37
|
+
initialize_bip32_addresses(options)
|
38
|
+
super(options)
|
39
|
+
return unless valid?
|
40
|
+
build_trezor_inputs
|
41
|
+
build_trezor_outputs
|
42
|
+
build_transactions
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_hash
|
46
|
+
return nil unless valid?
|
47
|
+
{ amount: inputs_amount,
|
48
|
+
fee: inputs_amount - outputs_amount,
|
49
|
+
inputs: trezor_inputs,
|
50
|
+
outputs: trezor_outputs,
|
51
|
+
transactions: transactions}
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
attr_accessor :trezor_outputs
|
56
|
+
attr_accessor :trezor_inputs
|
57
|
+
attr_accessor :transactions
|
58
|
+
attr_accessor :bip32_address_lookup
|
59
|
+
|
60
|
+
def initialize_bip32_addresses(options)
|
61
|
+
if options[:bip32_addresses].blank?
|
62
|
+
self.errors = [:no_bip32_addresses_given]
|
63
|
+
return
|
64
|
+
end
|
65
|
+
options[:addresses] = options[:bip32_addresses].collect(&:first)
|
66
|
+
self.bip32_address_lookup = options[:bip32_addresses].reduce({}) do |accum, addr|
|
67
|
+
accum[addr.first] = addr.last
|
68
|
+
accum
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_trezor_inputs
|
73
|
+
self.trezor_inputs = inputs.collect do |input|
|
74
|
+
{ address_n: bip32_address_lookup[input[0]],
|
75
|
+
prev_hash: input[1], prev_index: input[2] }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_trezor_outputs
|
80
|
+
self.trezor_outputs = outputs.collect do |address, amount|
|
81
|
+
type = Bitcoin.address_type(address) == :hash160 ? 'PAYTOADDRESS' : 'PAYTOSCRIPTHASH'
|
82
|
+
{ script_type: type, address: address, amount: amount }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def build_transactions
|
87
|
+
txids = inputs.collect{|i| i[1] }
|
88
|
+
self.transactions = Pochette.backend.list_transactions(txids)
|
89
|
+
end
|
90
|
+
end
|
data/lib/pochette.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "pochette/version"
|
2
|
+
require "bitcoin_rpc"
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext"
|
5
|
+
require "bitcoin"
|
6
|
+
|
7
|
+
module Pochette
|
8
|
+
mattr_accessor :backend
|
9
|
+
|
10
|
+
module Backends
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require "pochette/backends/bitcoin_core"
|
15
|
+
require "pochette/backends/trendy"
|
16
|
+
require "pochette/transaction_builder"
|
17
|
+
require "pochette/trezor_transaction_builder"
|
data/pochette.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pochette/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pochette"
|
8
|
+
spec.version = Pochette::VERSION
|
9
|
+
spec.authors = ["Nubis", "Eromirou"]
|
10
|
+
spec.email = ["nb@bitex.la", "tr@bitex.la"]
|
11
|
+
|
12
|
+
spec.summary = %q{Pochette is a Bitcoin Wallet for developers}
|
13
|
+
spec.description = %q{Pochette is a Bitcoin Wallet backend offering a common
|
14
|
+
interface to several bitcoin nodes so you can build single purpose wallets.
|
15
|
+
You can pass in a bunch of addresses and outputs and it will select the
|
16
|
+
appropriate unspent outputs for each of them, calculate change, fees, etc.
|
17
|
+
}
|
18
|
+
spec.homepage = "http://github.com/bitex-la/pochette"
|
19
|
+
spec.license = "MIT"
|
20
|
+
|
21
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
spec.add_dependency "activesupport", "~> 4.2"
|
24
|
+
spec.add_dependency "bitcoin_rpc", "~> 0.1.1"
|
25
|
+
spec.add_dependency "bitcoin-ruby", "~> 0.0.7"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3"
|
30
|
+
spec.add_development_dependency "webmock", "~> 1.21"
|
31
|
+
spec.add_development_dependency "timecop", "~> 0.8.0"
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pochette
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nubis
|
8
|
+
- Eromirou
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-09-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '4.2'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '4.2'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: bitcoin_rpc
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.1.1
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.1.1
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bitcoin-ruby
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 0.0.7
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 0.0.7
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: bundler
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '1.9'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.9'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rake
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '10.0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '10.0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rspec
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '3'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '3'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: webmock
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '1.21'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '1.21'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: timecop
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: 0.8.0
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 0.8.0
|
126
|
+
description: "Pochette is a Bitcoin Wallet backend offering a common\n interface
|
127
|
+
to several bitcoin nodes so you can build single purpose wallets.\n You can pass
|
128
|
+
in a bunch of addresses and outputs and it will select the\n appropriate unspent
|
129
|
+
outputs for each of them, calculate change, fees, etc.\n "
|
130
|
+
email:
|
131
|
+
- nb@bitex.la
|
132
|
+
- tr@bitex.la
|
133
|
+
executables: []
|
134
|
+
extensions: []
|
135
|
+
extra_rdoc_files: []
|
136
|
+
files:
|
137
|
+
- ".gitignore"
|
138
|
+
- ".rspec"
|
139
|
+
- ".travis.yml"
|
140
|
+
- CODE_OF_CONDUCT.md
|
141
|
+
- Gemfile
|
142
|
+
- LICENSE.txt
|
143
|
+
- README.md
|
144
|
+
- Rakefile
|
145
|
+
- bin/console
|
146
|
+
- bin/setup
|
147
|
+
- lib/pochette.rb
|
148
|
+
- lib/pochette/backends/bitcoin_core.rb
|
149
|
+
- lib/pochette/backends/trendy.rb
|
150
|
+
- lib/pochette/transaction_builder.rb
|
151
|
+
- lib/pochette/trezor_transaction_builder.rb
|
152
|
+
- lib/pochette/version.rb
|
153
|
+
- pochette.gemspec
|
154
|
+
homepage: http://github.com/bitex-la/pochette
|
155
|
+
licenses:
|
156
|
+
- MIT
|
157
|
+
metadata: {}
|
158
|
+
post_install_message:
|
159
|
+
rdoc_options: []
|
160
|
+
require_paths:
|
161
|
+
- lib
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
requirements: []
|
173
|
+
rubyforge_project:
|
174
|
+
rubygems_version: 2.4.5
|
175
|
+
signing_key:
|
176
|
+
specification_version: 4
|
177
|
+
summary: Pochette is a Bitcoin Wallet for developers
|
178
|
+
test_files: []
|