pochette 0.1.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32a5e22b912d750c86f5ce857e8fd6c13e0be105
4
- data.tar.gz: b4b5176cfaabc855f88c692d17f721699ff4dc68
3
+ metadata.gz: c505a9f75cdfc003b454e87cd97c48f8fc11aeb7
4
+ data.tar.gz: 9e6c94633014b5d0f1ab9328dd1d9f0edea20ca1
5
5
  SHA512:
6
- metadata.gz: a9e1050c5b93cf85eea51bbcfb3e16468a6331c5ff49b1e5d4ef9c9bdd9bfe1bc5a3fedcc0c98db244eec3121fe63a7de1a1ca6801fbe083935edf2f4fd0c7b1
7
- data.tar.gz: 9d806da72bafad0279da22e85e3375a36957a8d667cb994361cfb1071eee80cabd0d46d1740b6313b7ca4955529838372415c872b4ea3e5541f2e5cddad3e414
6
+ metadata.gz: ba821bb23b235bb6ca4b9a2249c6ca6a3844665bcfe1a967e16066da4976e37d1cf01ce7e7004cc83c522fc84359d0a419e14a352264c1a4a5aa570a8b3b55cf
7
+ data.tar.gz: 2b312820cb91d983c0638473d957a13b4f45fe99044aa4361eede24d61d1ec638b2da51fcc4243e1f8c9d13a2d96107e0f687e0a9a9a8ea6869e76ab348f3f3e
data/README.md CHANGED
@@ -8,14 +8,38 @@ bitcoin deposits to preparing transactions with bip32 paths instead of input add
8
8
  ready to be signed with a [Trezor Device](https://www.bitcointrezor.com/)
9
9
 
10
10
  Pochette offers a common interface to full bitcoin nodes like
11
- [Bitcoin Core](https://bitcoin.org/en/download) or [Toshi](http://toshi.io)
11
+ [Bitcoin Core](https://bitcoin.org/en/download)
12
+ [Blockchain.info](https://blockchain.info/api) or [Toshi](http://toshi.io)
12
13
  and will let you run several instances of each one of them simultaneously
13
- always choosing the most recent node to query.
14
+ always choosing the most recent node to query.
14
15
 
15
16
  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
17
+ of 'source' addresses and a list of recipients as "address/amount" pairs and
18
+ uses them to select unspent outputs and build a raw transaction to be signed
19
+ and broadcasted.
20
+
21
+ The Pochette::TrezorTransactionBuilder class extends Pochette::TransactionBuilder
22
+ including transactions, inputs and outputs that are formatted in a way they can
23
+ be passed directly to a Trezor device for signing.
24
+
25
+ ## Table of contents
26
+ - [Installation and Setup](#installation-and-setup)
27
+ - [Pochette::TransactionBuilder](#the-pochettetransactionbuilder)
28
+ - [Pochette::TrezorTransactionBuilder](#the-pochettetrezortransactionbuilder)
29
+ - [Backend API](#backend-api)
30
+ - [incoming_for(addresses, min_date)](#incoming_foraddresses-min_date)
31
+ - [balances_for(addresses, confirmations)](#balances_foraddresses-confirmations)
32
+ - [list_unspent(addresses)](#list_unspentaddresses)
33
+ - [list_transactions(txids)](#list_transactionstxids)
34
+ - [block_height](#block_height)
35
+ - [pushtx(hex)](#pushtxhex)
36
+ - Supported Backends
37
+ - [BitcoinCore Backend](#bitcoincore-backend)
38
+ - [BlockchainInfo Backend](#blockchaininfo-backend)
39
+ - [Toshi Backend](#toshi-backend)
40
+ - [Trendy Backend](#trendy-backend)
41
+
42
+ ## Installation and Setup
19
43
 
20
44
  Add this line to your application's Gemfile:
21
45
 
@@ -31,55 +55,522 @@ Or install it yourself as:
31
55
 
32
56
  $ gem install pochette
33
57
 
58
+ You will probably want to setup Pochette with a global default backend,
59
+ the backend can also be configured separately for each instance of
60
+ a Pochette::TransactionBuilder
61
+
62
+ ```ruby
63
+ >>> Pochette.backend = Pochette::Backends::BlockchainInfo.new
64
+ >>> Pochette::TransactionBuilder.backend = Pochette::Backends::BlockchainInfo.new
65
+ >>> Pochette::TrezorTransactionBuilder.backend = Pochette::Backends::BlockchainInfo.new
66
+ ```
67
+
68
+ Pochette can also be configured to use the bitcoin testnet, this will change
69
+ the default network used by the Bitcoin gem and may alter the way backends work too.
70
+
71
+ ```ruby
72
+ >>> Pochette.testnet = true
73
+ ```
74
+
34
75
  ## The Pochette::TransactionBuilder
35
76
 
36
77
  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.
78
+ using a configured backend to fetch unspent outputs and related transaction data.
38
79
  Instantiating will perform all the required queries, you'll be left with a
39
80
  TransactionBuilder object that is either valid? or not, and if valid,
40
81
  you can query the results via to_hash.
41
82
 
83
+ #### Receives
42
84
  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.
85
+
86
+ <dl>
87
+ <dt>addresses:</dt>
88
+ <dd>
89
+ List of addresses in wallet.
90
+ We will be spending their unspent outputs.
91
+ </dd>
92
+ <dt>outputs:</dt>
93
+ <dd>
94
+ List of pairs [recipient_address, amount]
95
+ This will not be all the final outputs in the transaction,
96
+ as a 'change' output may be adted if needed.
97
+ </dd>
98
+ <dt>utxo_blacklist:</dt>
99
+ <dd>
100
+ Optional. List of utxos to ignore, a list of pairs [transaction hash, position]
101
+ </dd>
102
+ <dt>change_address:</dt>
103
+ <dd>
104
+ Optional. Change address to use. Will default to the first source address.
105
+ </dd>
106
+ <dt>fee_per_kb:</dt>
107
+ <dd>
108
+ Optional. Defaults to 10000 satoshis.
109
+ </dd>
110
+ <dt>spend_all:</dt>
111
+ <dd>
112
+ Optional. Boolean. Wether to spend all available utxos or just select enough to
113
+ cover the given outputs.
114
+ </dd>
115
+ </dl>
116
+
117
+ #### Returns
118
+
119
+ A hash with
120
+
121
+ <dl>
122
+ <dt>input_total:</dt>
123
+ <dd>The sum of all input amounts, in satoshis.</dd>
124
+ <dt>output_total:</dt>
125
+ <dd>The sum of all outputs, in satoshis.</dd>
126
+ <dt>fee:</dt>
127
+ <dd>fee to pay (input_total - output_total).</dd>
128
+ <dt>outputs:</dt>
129
+ <dd>Array of [destination address, amount in satoshis]</dd>
130
+ <dt>inputs:</dt>
131
+ <dd>Array of [input address, utxo transaction hash, utxo position, amount]</dd>
132
+ <dt>utxos_to_blacklist:</dt>
133
+ <dd>
134
+ Transaction inputs formatted to be used as utxo_blacklist on another
135
+ TransactionBuilder.
136
+ </dd>
137
+ </dl>
138
+
139
+
140
+ ```ruby
141
+ >>> require 'pochette'
142
+ >>> backend = Pochette::Backends::BlockchainInfo.new
143
+ >>> transaction = Pochette::TransactionBuilder.new({
144
+ addresses: ["2NAHscN6XVqUPzBSJHC3fhkeF5SQVxiR9p9"],
145
+ outputs: [
146
+ ["mvtUvWSWCU7knrcMVzjcKJgjL1LdekLK5q", 1_0000_0000],
147
+ ],
148
+ utxo_blacklist: [
149
+ ["0ded7f014fa3213e9b000bc81b8151bc6f2f926b9afea6e3643c8ad658353c72", 1]
150
+ ],
151
+ change_address: 'mgaTEUM4ZE9kLiK58FcffwHfvxpte5CfvE',
152
+ fee_per_kb: 10000,
153
+ spend_all: false,
154
+ backend: backend
155
+ })
156
+ >>> transaction.valid?
157
+ => true
158
+ >>> transaction.as_hash
159
+ => {
160
+ input_total: 2_0000_0000,
161
+ output_total: 1_9999_0000,
162
+ fee: 10000,
163
+ outputs: [
164
+ ["mvtUvWSWCU7knrcMVzjcKJgjL1LdekLK5q", 1_0000_0000],
165
+ ["mgaTEUM4ZE9kLiK58FcffwHfvxpte5CfvE", 9999_0000],
166
+ ],
167
+ inputs: [
168
+ [ "2NAHscN6XVqUPzBSJHC3fhkeF5SQVxiR9p9",
169
+ "956b30c3c4335f019dbee60c60d76994319473acac356f774c7858cd5c968e40",
170
+ 1,
171
+ 200000000
172
+ ],
173
+ ],
174
+ utxos_to_blacklist: [
175
+ ["956b30c3c4335f019dbee60c60d76994319473acac356f774c7858cd5c968e40", 1]
176
+ ],
177
+ }
178
+ ```
179
+
180
+ ## The Pochette::TrezorTransactionBuilder
181
+
182
+ Builds a transaction like TransactionBuilder but includes transaction data
183
+ and formats inputs and outputs in a way that can be sent directly to your trezor
184
+ device.
185
+ If you're using [Trezor Connect](https://github.com/trezor/connect) for signing
186
+ then you won't need to pass in the transactions.
187
+
188
+ #### Receives
189
+ The TransactionBuilder's initializer receives a single options hash with:
190
+
191
+ <dl>
192
+ <dt>addresses:</dt>
193
+ <dd>
194
+ List of addresses in wallet. We will be spending their unspent outputs.
68
195
  Each address is represented as a pair, with the public address string
69
196
  and the BIP32 path as a list of integers, for example:
70
197
  ['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.
198
+ </dd>
199
+ <dt>outputs:</dt>
200
+ <dd>
201
+ List of pairs [recipient_address, amount]
202
+ This will not be all the final outputs in the transaction,
203
+ as a 'change' output may be adted if needed.
204
+ </dd>
205
+ <dt>utxo_blacklist:</dt>
206
+ <dd>
207
+ Optional. List of utxos to ignore, a list of pairs [transaction hash, position]
208
+ </dd>
209
+ <dt>change_address:</dt>
210
+ <dd>
211
+ Optional. Change address to use. Will default to the first source address.
212
+ </dd>
213
+ <dt>fee_per_kb:</dt>
214
+ <dd>
215
+ Optional. Defaults to 10000 satoshis.
216
+ </dd>
217
+ <dt>spend_all:</dt>
218
+ <dd>
219
+ Optional. Boolean. Wether to spend all available utxos or just select enough to
220
+ cover the given outputs.
221
+ </dd>
222
+ </dl>
223
+
224
+ #### Returns
225
+
226
+ A hash with
227
+
228
+ <dl>
229
+ <dt>input_total:</dt>
230
+ <dd>The sum of all input amounts, in satoshis.</dd>
231
+ <dt>output_total:</dt>
232
+ <dd>The sum of all outputs, in satoshis.</dd>
233
+ <dt>fee:</dt>
234
+ <dd>fee to pay (input_total - output_total).</dd>
235
+ <dt>outputs:</dt>
236
+ <dd>Array of [destination address, amount in satoshis]</dd>
237
+ <dt>inputs:</dt>
238
+ <dd>Array of [input address, utxo transaction hash, utxo position, amount]</dd>
239
+ <dt>utxos_to_blacklist:</dt>
240
+ <dd>
241
+ Transaction inputs formatted to be used as utxo_blacklist on another
242
+ TransactionBuilder.
243
+ </dd>
244
+ <dt>transactions:</dt>
245
+ <dd>Transaction data for each input.</dd>
246
+ <dt>trezor_inputs:</dt>
247
+ <dd>
248
+ List of inputs as hashes with bip32 paths instead of addresses
249
+ { address_n: [42,1,1],
250
+ prev_hash: "956b30c3c4335f019dbee60c60d76994319473acac356f774c7858cd5c968e40",
251
+ prev_index: 1}
252
+ </dd>
253
+ <dt>trezor_outputs:</dt>
254
+ <dd>
255
+ List of outputs as Hashes with:
76
256
  { script_type: 'PAYTOADDRESS',
77
257
  address: '1address-as-string',
78
- amount: amount_in_satoshis }
258
+ amount: amount in satoshis }
259
+ </dd>
260
+ </dl>
79
261
 
80
- TODO: Document as_hash output.
262
+ ```ruby
263
+ >>> require 'pochette'
264
+ >>> backend = Pochette::Backends::BlockchainInfo.new
265
+ >>> transaction = Pochette::TransactionBuilder.new({
266
+ addresses: [
267
+ ["2NAHscN6XVqUPzBSJHC3fhkeF5SQVxiR9p9", [44, 1, 3, 11]]
268
+ ],
269
+ outputs: [
270
+ ["mvtUvWSWCU7knrcMVzjcKJgjL1LdekLK5q", 1_0000_0000],
271
+ ],
272
+ utxo_blacklist: [
273
+ ["0ded7f014fa3213e9b000bc81b8151bc6f2f926b9afea6e3643c8ad658353c72", 1]
274
+ ],
275
+ change_address: 'mgaTEUM4ZE9kLiK58FcffwHfvxpte5CfvE',
276
+ fee_per_kb: 10000,
277
+ spend_all: false
278
+ })
279
+ >>> transaction.valid?
280
+ => true
281
+ >>> transaction.as_hash
282
+ => {
283
+ input_total: 2_0000_0000,
284
+ output_total: 1_9999_0000,
285
+ fee: 10000,
286
+ outputs: [
287
+ ["mvtUvWSWCU7knrcMVzjcKJgjL1LdekLK5q", 1_0000_0000],
288
+ ["mgaTEUM4ZE9kLiK58FcffwHfvxpte5CfvE", 9999_0000],
289
+ ],
290
+ trezor_outputs: [
291
+ { script_type: 'PAYTOADDRESS',
292
+ address: "mvtUvWSWCU7knrcMVzjcKJgjL1LdekLK5q",
293
+ amount: 1_0000_0000 },
294
+ { script_type: 'PAYTOADDRESS',
295
+ address: "mgaTEUM4ZE9kLiK58FcffwHfvxpte5CfvE",
296
+ amount: 9999_0000 },
297
+ ],
298
+ inputs: [
299
+ [ "2NAHscN6XVqUPzBSJHC3fhkeF5SQVxiR9p9",
300
+ "956b30c3c4335f019dbee60c60d76994319473acac356f774c7858cd5c968e40",
301
+ 1,
302
+ 200000000
303
+ ],
304
+ ],
305
+ trezor_inputs: [
306
+ { address_n: [43,1,3,11],
307
+ prev_hash: "956b30c3c4335f019dbee60c60d76994319473acac356f774c7858cd5c968e40",
308
+ prev_index: 1 },
309
+ ],
310
+ transactions: [
311
+ { hash: "956b30c3c4335f019dbee60c60d76994319473acac356f774c7858cd5c968e40",
312
+ version: "1",
313
+ lock_time: "0",
314
+ inputs: [
315
+ { prev_hash: "158d6bbe586b4e00347f992e8296532d69f902d0ead32d964b6c87d4f8f0d3ea",
316
+ prev_index: 0,
317
+ sequence: 4294967295,
318
+ script_sig: "SCRIPTSCRIPTSCRIPT"
319
+ }
320
+ ],
321
+ bin_outputs: [
322
+ { amount: 1234568, script_pubkey: "76a914988cb8253f4e28be6e8bfded1b4aa11c646e1a8588ac" },
323
+ { amount: 200000000, script_pubkey: "76a914988cb8253f4e28be6e8bfded1b4aa11c646e1a8588ac"}
324
+ ]
325
+ }
326
+ ],
327
+ utxos_to_blacklist: [
328
+ ["956b30c3c4335f019dbee60c60d76994319473acac356f774c7858cd5c968e40", 1]
329
+ ],
330
+ }
331
+ ```
332
+
333
+ ## Backend API
334
+
335
+ Pochette offers a common interface to full bitcoin nodes like
336
+ [Bitcoin Core](https://bitcoin.org/en/download)
337
+ [Blockchain.info](https://blockchain.info/api) or [Toshi](http://toshi.io)
338
+ and will let you run several instances of each one of them simultaneously
339
+ always choosing the most recent node to query.
340
+
341
+ ## incoming_for(addresses, min_date)
342
+
343
+ The incoming_for method is useful when registering deposits received to
344
+ a number of bitcoin addresses.
345
+
346
+ #### Receives
347
+ - addresses: A list of public bitcoin addresses to check for incoming transactions.
348
+ - min_date: Do not check for deposits earlier than this date. This is only to prevent
349
+ fetching too many results if the backend was to return too many,
350
+ each backend may apply its own limits so higher value here is not guaranteed to
351
+ fetch more results.
352
+
353
+ #### Returns
354
+ A list with
355
+
356
+ - Amount received, in satoshis.
357
+ - Address which received the deposit.
358
+ - Transaction hash for the deposit.
359
+ - Confirmations for the transaction.
360
+ - Position of this deposit in the transaction outputs list.
361
+ - Senders, as a comma-separated list of addresses (no whitespaces)
362
+
363
+ ```ruby
364
+ >>> require 'pochette'
365
+ >>> backend = Pochette::Backends::BlockchainInfo.new
366
+ >>> addresses = [
367
+ 'mjfa56Keq7PXRKgdPSDB6eWLp4aaAVcj6L',
368
+ 'mwZE4QfzzriE7nsgHSWbgmtT7s6SDysYvP',
369
+ 'mvrDG7Ts6Mq9ejhZxdsQLjbScycVaktqsg',
370
+ 'mxYzRdJfPk8PcaKSsSzNkX85mMfNcr2CGr',
371
+ 'mzbXim4u1Nq4J2kVggu471pZL3ahxNkmE9',
372
+ 'mhLAgRz5f1YogfYBZCDFSRt3ceeKBPVEKg',
373
+ ]
374
+ >>> Pochette.backend.incoming_for(addresses, 1.day.ago)
375
+ => [
376
+ [ 500000, "mjfa56Keq7PXRKgdPSDB6eWLp4aaAVcj6L",
377
+ "fb401691795a73e0160252c00af18327a15006fcdf877ccca0c116809669032e", 1629, 0,
378
+ "my2hmDuD9XjmtQWFu9HyyNAsE5WGSDBKpQ"],
379
+ [100000, "mxYzRdJfPk8PcaKSsSzNkX85mMfNcr2CGr",
380
+ "250978b77fe1310d6c72239d9e9589d7ac3dc6edf1b2412806ace5104553da34", 1648, 1,
381
+ "mv9ES7SmQQQ8dpMravBKsLWukgxU2DXfFs"],
382
+ [500000, "mvrDG7Ts6Mq9ejhZxdsQLjbScycVaktqsg",
383
+ "d9afd460b0a5e065fdd87bf97cb1843a29ea588c59daabd1609794e8166bb75f", 1648, 0,
384
+ "my2hmDuD9XjmtQWFu9HyyNAsE5WGSDBKpQ"],
385
+ [ 100000, "mxYzRdJfPk8PcaKSsSzNkX85mMfNcr2CGr",
386
+ "5bd72a4aa7818f47ac8943e3e17519be00c46530760860e608d898d728b9d46e", 553, 1,
387
+ "mvUhgW1ZcUju181bvwEhmZu2x2sRdbV4y2"],
388
+ [ 500000, "mwZE4QfzzriE7nsgHSWbgmtT7s6SDysYvP",
389
+ "b252037526ecb616ab5901552abb903f00bf73400a1fc49b5b5bd699b84bce77", 1632, 0,
390
+ "my2hmDuD9XjmtQWFu9HyyNAsE5WGSDBKpQ"],
391
+ [ 500000, "mzbXim4u1Nq4J2kVggu471pZL3ahxNkmE9",
392
+ "ff768084764a05d1de72628432c0a4419538b2786089ec8ad009f6096bc69fe1", 1660, 0,
393
+ "my2hmDuD9XjmtQWFu9HyyNAsE5WGSDBKpQ"]
394
+ ]
395
+ ```
396
+
397
+ ## balances_for(addresses, confirmations)
398
+
399
+ Gets confirmed and unconfirmed sent, received and total balances for the given
400
+ addresses. It's useful for payment processing as you can see what's the total
401
+ amount seen on the network for a given address, and the final confirmed amount as well.
402
+
403
+ #### Receives
404
+ - addresses: A list of public bitcoin addresses to check balances for.
405
+ - confirmations: How many confirmations to use for the 'confirmed' amounts.
406
+
407
+ #### Returns
408
+ A hash where keys are public addresses and values ara a list of
409
+
410
+ - Confirmed received
411
+ - Confirmed sent
412
+ - Confirmed balance
413
+ - Unconfirmed received
414
+ - Unconfirmed sent
415
+ - Unconfirmed balance
416
+
417
+ ```ruby
418
+ >>> require 'pochette'
419
+ >>> backend = Pochette::Backends::BlockchainInfo.new
420
+ >>> addresses = [
421
+ 'mjfa56Keq7PXRKgdPSDB6eWLp4aaAVcj6L',
422
+ 'mwZE4QfzzriE7nsgHSWbgmtT7s6SDysYvP',
423
+ 'mvrDG7Ts6Mq9ejhZxdsQLjbScycVaktqsg',
424
+ 'mxYzRdJfPk8PcaKSsSzNkX85mMfNcr2CGr',
425
+ 'mzbXim4u1Nq4J2kVggu471pZL3ahxNkmE9',
426
+ 'mhLAgRz5f1YogfYBZCDFSRt3ceeKBPVEKg',
427
+ ]
428
+ >>> backend.balances_for(addresses, 6)
429
+ >>> [
430
+ "mhLAgRz5f1YogfYBZCDFSRt3ceeKBPVEKg" =>
431
+ [0.00544426, 0.00544426, 0.0, 0.00544426, 0.00544426, 0.0],
432
+ "mjfa56Keq7PXRKgdPSDB6eWLp4aaAVcj6L" =>
433
+ [0.005, 0.0, 0.005, 0.006, 0.0, 0.006],
434
+ "mvrDG7Ts6Mq9ejhZxdsQLjbScycVaktqsg" =>
435
+ [0.005, 0.0, 0.005, 0.005, 0.0, 0.005],
436
+ "mwZE4QfzzriE7nsgHSWbgmtT7s6SDysYvP" =>
437
+ [0.005, 0.0, 0.005, 0.005, 0.0, 0.005],
438
+ "mxYzRdJfPk8PcaKSsSzNkX85mMfNcr2CGr" =>
439
+ [0.002, 0.0, 0.002, 0.002, 0.0, 0.002],
440
+ "mzbXim4u1Nq4J2kVggu471pZL3ahxNkmE9" =>
441
+ [0.005, 0.0, 0.005, 0.005, 0.0, 0.005],
442
+ ```
81
443
 
82
- ## Using a Bitcoin-Core backend.
444
+ ## list_unspent(addresses)
445
+
446
+ Gets unspent transaction outputs (a.k.a. utxos) for all the given addresses.
447
+ You may not need to use this directly, but rather through a
448
+ Pochette::TransactionBuilder which is smart about selecting utxos.
449
+
450
+ #### Receives
451
+ - addresses: A list of public bitcoin addresses to check balances for.
452
+
453
+ #### Returns
454
+ A list of list, each of them is
455
+
456
+ - Address
457
+ - Transaction Hash
458
+ - Output position in transaction
459
+ - Unspent amount
460
+
461
+ ```ruby
462
+ >>> require 'pochette'
463
+ >>> backend = Pochette::Backends::BlockchainInfo.new
464
+ >>> addresses = [
465
+ 'mjfa56Keq7PXRKgdPSDB6eWLp4aaAVcj6L',
466
+ 'mwZE4QfzzriE7nsgHSWbgmtT7s6SDysYvP',
467
+ 'mvrDG7Ts6Mq9ejhZxdsQLjbScycVaktqsg',
468
+ 'mxYzRdJfPk8PcaKSsSzNkX85mMfNcr2CGr',
469
+ 'mzbXim4u1Nq4J2kVggu471pZL3ahxNkmE9',
470
+ 'mhLAgRz5f1YogfYBZCDFSRt3ceeKBPVEKg',
471
+ ]
472
+ >>> backend.list_unspent(addresses).should == [
473
+ ["mjfa56Keq7PXRKgdPSDB6eWLp4aaAVcj6L",
474
+ "fb401691795a73e0160252c00af18327a15006fcdf877ccca0c116809669032e", 0, 500000],
475
+ ["mxYzRdJfPk8PcaKSsSzNkX85mMfNcr2CGr",
476
+ "250978b77fe1310d6c72239d9e9589d7ac3dc6edf1b2412806ace5104553da34", 1, 100000],
477
+ ["mvrDG7Ts6Mq9ejhZxdsQLjbScycVaktqsg",
478
+ "d9afd460b0a5e065fdd87bf97cb1843a29ea588c59daabd1609794e8166bb75f", 0, 500000],
479
+ ["mxYzRdJfPk8PcaKSsSzNkX85mMfNcr2CGr",
480
+ "5bd72a4aa7818f47ac8943e3e17519be00c46530760860e608d898d728b9d46e", 1, 100000],
481
+ ["mwZE4QfzzriE7nsgHSWbgmtT7s6SDysYvP",
482
+ "b252037526ecb616ab5901552abb903f00bf73400a1fc49b5b5bd699b84bce77", 0, 500000],
483
+ ["mjfa56Keq7PXRKgdPSDB6eWLp4aaAVcj6L",
484
+ "9142d7a8e96124a36db9708dd21afa4ac81f15a77bd85c06f16e808a4d700da2", 1, 100000],
485
+ ["mzbXim4u1Nq4J2kVggu471pZL3ahxNkmE9",
486
+ "ff768084764a05d1de72628432c0a4419538b2786089ec8ad009f6096bc69fe1", 0, 500000]
487
+ ]
488
+ ```
489
+
490
+ ## list_transactions(txids)
491
+
492
+ List full transaction data for the given transaction hashes. The output
493
+ is formatted for Trezor as required by their multi-step signature process.
494
+ You may not want to use this directly and use Pochette::TrezorTransactionBuilder
495
+ instead.
496
+
497
+ #### Receives
498
+ - Transactions: A list of Transaction ids
499
+
500
+ #### Returns
501
+ A list of ruby hashes will transaction data for each id passed.
502
+
503
+ ```ruby
504
+ >>> require 'pochette'
505
+ >>> backend = Pochette::Backends::BlockchainInfo.new
506
+ >>> transactions = [
507
+ "fb401691795a73e0160252c00af18327a15006fcdf877ccca0c116809669032e",
508
+ "250978b77fe1310d6c72239d9e9589d7ac3dc6edf1b2412806ace5104553da34",
509
+ ]
510
+ >>> backend.list_transactions(transactions)
511
+ => [{ hash: "fb401691795a73e0160252c00af18327a15006fcdf877ccca0c116809669032e",
512
+ version: 1,
513
+ lock_time: 0,
514
+ inputs: [
515
+ { prev_hash: "f948600a52719ec63947bd47105411c2cb032bda91b08b25bb7b1de22f1f7458",
516
+ prev_index: 1,
517
+ sequence: 4294967295,
518
+ script_sig: "483045022100898b773c1cf095d5c1608a892673ad067387663e00327e7b88dd48cc4eb9cf2a02203a53336afd4440ea52c6a55e07ac60035dc386cb01b5b6810efac018f0346fad01210316cff587a01a2736d5e12e53551b18d73780b83c3bfb4fcf209c869b11b6415e"
519
+ }
520
+ ],
521
+ bin_outputs: [
522
+ { amount: 500000, script_pubkey: "76a9142d81b210deb7e22475cd7f2fda0bf582dddc9da788ac" },
523
+ { amount: 276607256, script_pubkey: "76a914c01a7ca16b47be50cbdbc60724f701d52d75156688ac"}
524
+ ]
525
+ },
526
+ { hash: "250978b77fe1310d6c72239d9e9589d7ac3dc6edf1b2412806ace5104553da34",
527
+ version: 1,
528
+ lock_time: 0,
529
+ inputs: [
530
+ { prev_hash: "8505c663d0414b678827eed85ba9e7652e616c19ff1be871f6b083d5ed400a20",
531
+ prev_index: 0,
532
+ sequence: 4294967295,
533
+ script_sig: "47304402203ac7cb9afe1a14189b63807aff301ef6bb8507f6dc0471bcee5362282a8c3e38022012c3bfe9680f013735f0d6d012d6420383c0e3a1dbf010f97be9cc834a93d1f601210221af8672ff613d2ea198d8dadcc387a36ef47d7cba6f541221db61b96aa20149"
534
+ }
535
+ ],
536
+ bin_outputs: [
537
+ { amount: 999293000, script_pubkey: "76a91426beab63a5fb7b2103929e91b65f339a2c5b285088ac"},
538
+ { amount: 100000, script_pubkey: "76a914badcbfae4d83a52dc2c8f68605663adc9d4922a688ac"}
539
+ ]
540
+ }
541
+ ]
542
+ ```
543
+
544
+ ## block_height
545
+
546
+ Get the latest block height for this backend. Always in the main branch.
547
+
548
+ ```ruby
549
+ >>> require 'pochette'
550
+ >>> backend = Pochette::Backends::BlockchainInfo.new
551
+ >>> backend.get_height
552
+ => 376152
553
+ ```
554
+
555
+ ## pushtx
556
+
557
+ Propagates a raw transaction to the network.
558
+
559
+ #### Receives
560
+ - transaction: A raw transaction in hex format
561
+
562
+ #### Returns
563
+ The transaction id (hash)
564
+
565
+ ```ruby
566
+ >>> require 'pochette'
567
+ >>> hex = "0100000001d11a6cc978fc41aaf5b24fc5c8ddde71fb91ffdba9579cd62ba20fc284b2446c000000008a47304402206d2f98829a9e5017ade2c084a8b821625c35aeaa633f718b1c348906afbe68b00220094cb8ee519adcebe866e655532abab818aa921144bd98a12491481931d2383a014104e318459c24b89c0928cec3c9c7842ae749dcca78673149179af9155c80f3763642989df3ffe34ab205d02e2efd07e9a34db2f00ed8b821dd5bb087ff64df2c9effffffff0280f0fa02000000001976a9149b754a70b9a3dbb64f65db01d164ef51101c18d788ac40aeeb02000000001976a914aadf5d54eda13070d39af72eb5ce40b1d3b8282588ac00000000"
568
+ >>> backend = Pochette::Backends::BlockchainInfo.new
569
+ >>> backend.pushtx(hex)
570
+ => 'fb92420f73af6d25f5fab93435bc6b8ebfff3a07c02abd053f0923ae296fe380'
571
+ ```
572
+
573
+ ## BitcoinCore backend
83
574
 
84
575
  Pochette will connect to your bitcoin-core node via JSON-RPC, using the
85
576
  [bitcoin-rpc gem](https://github.com/bitex-la/bitcoin-rpc)
@@ -91,13 +582,41 @@ To properly use Pochette you need to be running your bitcoin node with setting t
91
582
  Also, if you're creating new addresses and want bitcoin-core to track them you'll want to import
92
583
  them using the bitcoin-rpc gem, like so:
93
584
 
94
- >>> BitcoinRpc::Client.new('http://user:pass@your_server').importaddress('1PUBLICADDRESS', '', false)
585
+ ```ruby
586
+ >>> BitcoinRpc::Client.new('http://user:pass@your_server').importaddress('1PUBLICADDRESS', '', false)
587
+ ```
95
588
 
96
589
  Setting up bitcoin-core as a backend can be done like this:
97
590
 
98
- >>> Pochette.backend = Pochette::Backends::BitcoinCore.new('http://user:pass@your_server')
591
+ ```ruby
592
+ >>> Pochette.backend = Pochette::Backends::BitcoinCore.new('http://user:pass@your_server')
593
+ ```
594
+
595
+ ## BlockchainInfo backend
596
+
597
+ Pochette can use blockchain.info's public API to fetch unspent outputs,
598
+ sleeping a bit after each request to prevent blockchain.info from banning your IP.
599
+ This backend is probably the slowest one but also the one that's more convenient for
600
+ testing and managing small wallets.
601
+
602
+ This backend is not usable for testnet transactions, all queries will be done to the
603
+ main network.
99
604
 
100
- ## Using a Toshi backend.
605
+ You can create a blockchain.info backend like this
606
+
607
+ ```ruby
608
+ >>> Pochette::Backends::BlockchainInfo.new
609
+ ```
610
+
611
+ The default cooldown time is 1 second after each request, but if you have a Blockchain.info
612
+ API key you can configure your backend like so:
613
+
614
+ ```ruby
615
+ >>> Pochette::Backends::BlockchainInfo.cooldown = 0.1 # Make the cooldown a tenth of a second
616
+ >>> Pochette.backend = Pochette::Backends::BlockchainInfo.new("your_api_key")
617
+ ```
618
+
619
+ ## Toshi backend
101
620
 
102
621
  Pochette will connect to your Toshi node's postgres database directly.
103
622
  It's provided as a separate gem as it depends on the pg gem which needs local
@@ -105,7 +624,7 @@ postgres extensions to work. You may need to add some extra indexes to your post
105
624
  to speed things up when using Pochette.
106
625
  [See the gem readme](https://github.com/bitex-la/pochette-toshi) for more info.
107
626
 
108
- ## Using the best of many available backends
627
+ ## Trendy Backend
109
628
 
110
629
  Pochette provides a higher level Backend Pochette::Backends::Trendy which chooses
111
630
  between a pool of available backends always using the one at the highest block height,
@@ -114,9 +633,11 @@ between a pool of available backends always using the one at the highest block h
114
633
  This is useful for automatic fallbacks and redundancy, you could also mix Toshi and Bitcoin-Core
115
634
  backends and use whatever looks more up to date.
116
635
 
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])
636
+ ```ruby
637
+ >>> alpha = Pochette::Backends::BitcoinCore.new('http://user:pass@alpha_host')
638
+ >>> beta = Pochette::Backends::BitcoinCore.new('http://user:pass@beta_host')
639
+ >>> Pochette.backend = Pochette::Backends::Trendy.new([alpha, beta])
640
+ ```
120
641
 
121
642
  ## Development
122
643
 
@@ -6,12 +6,24 @@ require "bitcoin"
6
6
 
7
7
  module Pochette
8
8
  mattr_accessor :backend
9
+
10
+ def self.testnet=(v)
11
+ @testnet = v
12
+ Bitcoin.network = v ? :testnet : :bitcoin
13
+ end
14
+ def self.testnet
15
+ @testnet
16
+ end
17
+ def self.testnet?
18
+ self.testnet
19
+ end
9
20
 
10
21
  module Backends
11
22
  end
12
23
  end
13
24
 
14
25
  require "pochette/backends/bitcoin_core"
26
+ require "pochette/backends/blockchain_info"
15
27
  require "pochette/backends/trendy"
16
28
  require "pochette/transaction_builder"
17
29
  require "pochette/trezor_transaction_builder"
@@ -102,4 +102,8 @@ class Pochette::Backends::BitcoinCore
102
102
  def block_height
103
103
  client.getinfo[:blocks]
104
104
  end
105
+
106
+ def pushtx(hex)
107
+ client.sendrawtransaction(hex)
108
+ end
105
109
  end
@@ -0,0 +1,111 @@
1
+ # A bitcoin backend that uses blockchain.info to retrieve information.
2
+ # See Pochette::Backends::Trendy to learn more about the backend
3
+ # interface and contract.
4
+ require 'open-uri'
5
+
6
+ class Pochette::Backends::BlockchainInfo
7
+ cattr_accessor(:cooldown){1}
8
+ attr_accessor :api_key
9
+
10
+ def initialize(key = nil)
11
+ self.api_key = key
12
+ end
13
+
14
+ def list_unspent(addresses)
15
+ json = get_json("unspent", {active: addresses.join('|'), format: 'json'})
16
+ json['unspent_outputs'].collect do |utxo|
17
+ address = Bitcoin::Script.new(utxo['script'].htb).get_address
18
+ [address, utxo['tx_hash_big_endian'], utxo['tx_output_n'].to_i, utxo['value']]
19
+ end
20
+ end
21
+
22
+ def balances_for(addresses, confirmations)
23
+ json = get_json("multiaddr", {active: addresses.join('|'), format: 'json'})
24
+ result = {}
25
+ json['addresses'].collect do |a|
26
+ address = a['address']
27
+ sent = get_json("q/getsentbyaddress/#{address}",
28
+ {confirmations: confirmations, format: 'json'})
29
+ received = get_json("q/getreceivedbyaddress/#{address}",
30
+ {confirmations: confirmations, format: 'json'})
31
+ result[address] = [
32
+ sat(received), sat(sent), sat(received - sent),
33
+ sat(a['total_received']), sat(a['total_sent']), sat(a['final_balance'])]
34
+ end
35
+ result
36
+ end
37
+
38
+ def sat(x)
39
+ x.to_d / 1_0000_0000
40
+ end
41
+
42
+ def incoming_for(addresses, min_date)
43
+ return unless latest_block = block_height
44
+ addresses.in_groups_of(50, false).collect do |group|
45
+ incoming_for_helper(group, latest_block)
46
+ end.flatten(1)
47
+ end
48
+
49
+ def incoming_for_helper(addresses, latest_block)
50
+ json = get_json("multiaddr", {active: addresses.join('|'), format: 'json'})
51
+
52
+ json['txs'].collect do |transaction|
53
+ transaction['out'].collect do |out|
54
+ next unless addresses.include? out['addr']
55
+ confirmations = latest_block - transaction['block_height'].to_i
56
+ senders = transaction['inputs'].collect{ |i| i['prev_out']['addr'] }.join(',')
57
+ [ out['value'].to_d, out['addr'], transaction['hash'], confirmations, out['n'], senders ]
58
+ end.compact
59
+ end.flatten(1)
60
+ end
61
+
62
+ def list_transactions(txids)
63
+ return nil if txids.empty?
64
+ txids.collect do |txid|
65
+ tx = get_json("rawtx/#{txid}", {format: 'json'})
66
+ inputs = tx['inputs'].collect do |i|
67
+ prevhash = get_json("rawtx/#{i['prev_out']['tx_index']}", {format: 'json'})['hash']
68
+ { prev_hash: prevhash,
69
+ prev_index: i['prev_out']['n'],
70
+ sequence: i['sequence'],
71
+ script_sig: i['script']
72
+ }
73
+ end
74
+
75
+ outputs = tx['out'].collect do |o|
76
+ { amount: o['value'].to_i, script_pubkey: o['script'] }
77
+ end
78
+
79
+ { hash: tx['hash'], version: tx['ver'], lock_time: tx['lock_time'],
80
+ inputs: inputs, bin_outputs: outputs}
81
+ end
82
+ end
83
+
84
+ def block_height
85
+ get_json("latestblock", {format: 'json'})['height'].to_i
86
+ end
87
+
88
+ def pushtx(hex)
89
+ uri = URI.parse("https://blockchain.info/pushtx")
90
+ params = { "tx" => hex }
91
+ params['api_code'] = api_key if api_key
92
+ Net::HTTP.post_form(uri, params)
93
+ Bitcoin::Protocol::Tx.new(hex.htb).hash
94
+ end
95
+
96
+ def get_json(path, params={})
97
+ params['api_code'] = api_key if api_key
98
+ query = params.empty? ? '' : "?#{params.to_query}"
99
+ retries = 30
100
+ begin
101
+ raw_response = open("https://blockchain.info/#{path}#{query}").read
102
+ sleep cooldown
103
+ Oj.load(raw_response)
104
+ rescue OpenURI::HTTPError => e
105
+ raise if retries < 0 || e.message.to_i != 429
106
+ retries -= 1
107
+ sleep (cooldown * 5)
108
+ retry
109
+ end
110
+ end
111
+ end
@@ -42,6 +42,13 @@ class Pochette::Backends::Trendy
42
42
  # [[address, txid, position (vout), amount (in satoshis)], ...]
43
43
  def list_unspent(addresses)
44
44
  backend.list_unspent(addresses)
45
+ rescue OpenURI::HTTPError => e
46
+ # Blockchain.info returns 500 when there are no unspent outputs
47
+ if e.io.read == "No free outputs to spend"
48
+ return []
49
+ else
50
+ raise
51
+ end
45
52
  end
46
53
 
47
54
  # Gets information for the given transactions
@@ -3,25 +3,13 @@
3
3
  # Instantiating will perform all the given queries, you'll be left with a
4
4
  # TransactionBuilder object that is either valid? or not, and if valid
5
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
6
 
24
7
  class Pochette::TransactionBuilder
8
+ # Backend can be set globally, or independently for each class and instance.
9
+ class_attribute :backend
10
+ def self.backend
11
+ @backend || Pochette.backend
12
+ end
25
13
 
26
14
  cattr_accessor(:dust_size){ 546 }
27
15
  cattr_accessor(:output_size){ 149 }
@@ -30,6 +18,7 @@ class Pochette::TransactionBuilder
30
18
  cattr_accessor(:default_fee_per_kb){ 10000 }
31
19
 
32
20
  def initialize(options)
21
+ self.backend = options[:backend] if options[:backend]
33
22
  initialize_options(options)
34
23
  return unless valid?
35
24
  initialize_fee
@@ -42,10 +31,13 @@ class Pochette::TransactionBuilder
42
31
 
43
32
  def as_hash
44
33
  return nil unless valid?
45
- { amount: inputs_amount,
34
+ { input_total: inputs_amount,
35
+ output_total: outputs_amount,
46
36
  fee: inputs_amount - outputs_amount,
47
37
  inputs: inputs,
48
- outputs: outputs}
38
+ outputs: outputs,
39
+ utxos_to_blacklist: inputs.collect{|i| [i[1], i[2]] },
40
+ }
49
41
  end
50
42
 
51
43
  def valid?
@@ -44,11 +44,10 @@ class Pochette::TrezorTransactionBuilder < Pochette::TransactionBuilder
44
44
 
45
45
  def as_hash
46
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}
47
+ super.merge(
48
+ trezor_inputs: trezor_inputs,
49
+ trezor_outputs: trezor_outputs,
50
+ transactions: transactions)
52
51
  end
53
52
 
54
53
  protected
@@ -1,3 +1,3 @@
1
1
  module Pochette
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.3"
3
3
  end
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency "rspec", "~> 3"
30
30
  spec.add_development_dependency "webmock", "~> 1.21"
31
31
  spec.add_development_dependency "timecop", "~> 0.8.0"
32
+ spec.add_development_dependency "byebug"
32
33
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pochette
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nubis
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-09-22 00:00:00.000000000 Z
12
+ date: 2015-09-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -123,6 +123,20 @@ dependencies:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
125
  version: 0.8.0
126
+ - !ruby/object:Gem::Dependency
127
+ name: byebug
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
126
140
  description: "Pochette is a Bitcoin Wallet backend offering a common\n interface
127
141
  to several bitcoin nodes so you can build single purpose wallets.\n You can pass
128
142
  in a bunch of addresses and outputs and it will select the\n appropriate unspent
@@ -146,6 +160,7 @@ files:
146
160
  - bin/setup
147
161
  - lib/pochette.rb
148
162
  - lib/pochette/backends/bitcoin_core.rb
163
+ - lib/pochette/backends/blockchain_info.rb
149
164
  - lib/pochette/backends/trendy.rb
150
165
  - lib/pochette/transaction_builder.rb
151
166
  - lib/pochette/trezor_transaction_builder.rb