├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── README.md ├── README.rst ├── RELEASE ├── bitcoingraph ├── __init__.py ├── bitcoind.py ├── bitcoingraph.py ├── blockchain.py ├── entities.py ├── graphdb.py ├── helper.py ├── model.py ├── neo4j.py └── writer.py ├── docs ├── Makefile ├── bitcoingraph.rst ├── conf.py ├── index.rst └── modules.rst ├── requirements.txt ├── scripts ├── bcgraph-compute-entities ├── bcgraph-export └── bcgraph-synchronize ├── setup.py ├── tests ├── __init__.py ├── data │ ├── block_100000.json │ ├── block_100001.json │ ├── block_99999.json │ ├── tx_110ed92f558a1e3a94976ddea5c32f030670b5c58c3cc4d857ac14d7a1547a90.json │ ├── tx_4a1817bf3dacbdb2f726e551979a4a1d27ec3ba9e4dda14fd90d0312f1cf068b.json │ ├── tx_5ca3c87c85e58830b8ee87cbf22ed61a51ccabbd1bfcf5a8e3f381ae7a7fe31f.json │ ├── tx_6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4.json │ ├── tx_87a08fe3e74fa0a6a386c196938594ce0b928bf1550fb2004b02d68b1e7bd893.json │ ├── tx_87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03.json │ ├── tx_8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87.json │ ├── tx_a288fec5559c3f73fd3d93db8e8460562ebfe2fcf04a5114e8d0f2920a6270dc.json │ ├── tx_cf4e2978d0611ce46592e02d7e7daf8627a316ab69759a9f3df109a7f2bf3ec3.json │ ├── tx_d5f013abf2cf4af6d68bcacd675c91f19bab5b7103b4ac2f4941686eb47da1f0.json │ ├── tx_d8066858142abfae59964da1ec29c26e30af52091dd7b5145fa9a953aa1f072e.json │ ├── tx_e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d.json │ ├── tx_f4515fed3dc4a19b90a317b9840c243bac26114cf637522373a7d486b372600b.json │ └── tx_fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4.json ├── rpc_mock.py └── test_blockchain.py └── utils ├── identities_blockchain.py └── identities_pools.py /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE settings 2 | /.idea/ 3 | 4 | # Generated dirs 5 | 6 | 7 | # Backup files 8 | *.~ 9 | .*.swp 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | # bin/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Sphinx documentation 49 | docs/_build/ 50 | 51 | # Test log 52 | /testlog.log 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | # command to install dependencies 5 | # need "python setup.py install" to install bitcoingraph so that tests work 6 | install: 7 | - pip install -r requirements.txt 8 | - pip install coveralls 9 | - python setup.py install 10 | script: 11 | - py.test 12 | - coverage run --source=bitcoingraph setup.py test 13 | after_success: 14 | - coveralls 15 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | = v0.3 2015-11-13 = 2 | 3 | * Added Neo4J graph database backend 4 | * Adapted model and path search to operate on Neo4J 5 | * Updated internal data model 6 | * Changed blockchain dump export model 7 | * Implemented identity gathering script 8 | 9 | 10 | = v0.2 2015-03-31 = 11 | 12 | * Added graph abstraction and graph pattern search methods 13 | * Entity graph generation support 14 | * Improved transaction extraction performance 15 | 16 | = v0.1 2015-01-15 = 17 | 18 | * First release supporting transaction graph generation 19 | * Blockchain API supporing navigation along Blocks and Transactions 20 | * Bitcoin client JSON-RPC interface 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Bitcoingraph Software License 2 | 3 | Copyright (c) 2015 Bernhard Haslhofer 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bitcoingraph - A Python library for exploring the Bitcoin transaction graph. 2 | 3 | [![Build Status](https://travis-ci.org/behas/bitcoingraph.svg?branch=master)](https://travis-ci.org/behas/bitcoingraph) 4 | 5 | ## Prerequesites 6 | 7 | ### Bitcoin Core setup and configuration 8 | 9 | First, install the current version of Bitcoin Core (v.11.1), either from [source](https://github.com/bitcoin/bitcoin) or from a [pre-compiled executable](https://bitcoin.org/en/download). 10 | 11 | Once installed, you'll have access to three programs: `bitcoind` (= full peer), `bitcoin-qt` (= peer with GUI), and `bitcoin-cli` (RPC command line interface). The following instructions have been tested with `bitcoind` and assume you can start and run a Bitcoin Core peer as follows: 12 | 13 | bitcoind -printtoconsole 14 | 15 | Second, you must make sure that your bitcoin client accepts JSON-RPC connections by modifying the [Bitcoin Core configuration file][bc_conf] as follows: 16 | 17 | # server=1 tells Bitcoin-QT to accept JSON-RPC commands. 18 | server=1 19 | 20 | # You must set rpcuser and rpcpassword to secure the JSON-RPC api 21 | rpcuser=your_rpcuser 22 | rpcpassword=your_rpcpass 23 | 24 | # How many seconds bitcoin will wait for a complete RPC HTTP request. 25 | # after the HTTP connection is established. 26 | rpctimeout=30 27 | 28 | # Listen for RPC connections on this TCP port: 29 | rpcport=8332 30 | 31 | Test whether the JSON-RPC interface is working by starting your Bitcoin Core peer (...waiting until it finished startup...) and using the following cURL request (with adapted username and password): 32 | 33 | curl --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "getinfo", "params": [] }' -H 'content-type: text/plain;' http://your_rpcuser:your_rpcpass@localhost:8332/ 34 | 35 | 36 | Third, since Bitcoingraph needs to access non-wallet blockchain transactions by their ids, you need to enable the transaction index in the Bitcoin Core database. This can be achieved by adding the following property to your `bitcoin.conf` 37 | 38 | txindex=1 39 | 40 | ... and restarting your Bitcoin core peer as follows (rebuilding the index can take a while): 41 | 42 | bitcoind -reindex 43 | 44 | 45 | Test non-wallet transaction data access by taking an arbitrary transaction id and issuing the following request using cURL: 46 | 47 | curl --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "getrawtransaction", "params": ["110ed92f558a1e3a94976ddea5c32f030670b5c58c3cc4d857ac14d7a1547a90", 1] }' -H 'content-type: text/plain;' http://your_rpcuser:your_rpcpass@localhost:8332/ 48 | 49 | 50 | Finally, bitcoingraph also makes use of Bitcoin Core's HTTP REST interface, which is enabled using the following parameter: 51 | 52 | bitcoind -rest 53 | 54 | Test it using some sample block hash 55 | 56 | http://localhost:8332/rest/block/000000000000000e7ad69c72afc00dc4e05fc15ae3061c47d3591d07c09f2928.json 57 | 58 | 59 | When you reached this point, your Bitcoin Core setup is working. Terminate all running bitcoind instances and launch a new background daemon with enabled REST interface 60 | 61 | bitcoind -daemon -rest 62 | 63 | 64 | ### Bitcoingraph library setup 65 | 66 | Bitcoingraph is being developed in Python 3.4. Make sure it is running on your machine: 67 | 68 | python --version 69 | 70 | 71 | Now clone Bitcoingraph... 72 | 73 | git clone https://github.com/behas/bitcoingraph.git 74 | 75 | 76 | ...test and install the Bitcoingraph library: 77 | 78 | cd bitcoingraph 79 | pip install -r requirements.txt 80 | py.test 81 | python setup.py install 82 | 83 | 84 | ### Mac OSX specifics 85 | 86 | Running bitcoingraph on a Mac requires coreutils to be installed 87 | 88 | homebrew install coreutils 89 | 90 | 91 | ## Boostrapping the underlying graph database (Neo4J) 92 | 93 | bitcoingraph stores Bitcoin transactions as directed labelled graph in a Neo4J graph database instance. This database can be bootstrapped by loading an initial blockchain dump, performing entity computation over the entire dump as described by [Ron and Shamir](https://eprint.iacr.org/2012/584.pdf), and ingesting it into a running Neo4J instance. 94 | 95 | ### Step 1: Create transaction dump from blockchain 96 | 97 | Bitcoingraph provides the `bcgraph-export` tool for exporting transactions in a given block range from the blockchain. The following command exports all transactions contained in block range 0 to 1000 using Neo4Js header format and separate CSV header files: 98 | 99 | bcgraph-export 0 1000 -u your_rpcuser -p your_rpcpass 100 | 101 | The following CSV files are created (with separate header files): 102 | 103 | * addresses.csv: sorted list of Bitcoin addressed 104 | * blocks.csv: list of blocks (hash, height, timestamp) 105 | * transactions.csv: list of transactions (hash, coinbase/non-coinbase) 106 | * outputs.csv: list of transaction outputs (output key, id, value, script type) 107 | * rel_block_tx.csv: relationship between blocks and transactions (block_hash, tx_hash) 108 | * rel_input.csv: relationship between transactions and transaction outputs (tx_hash, output key) 109 | * rel_output_address.csv: relationship between outputs and addresses (output key, address) 110 | * rel_tx_output.csv: relationship between transactions and transaction outputs (tx_hash, output key) 111 | 112 | 113 | ### Step 2: Compute entities over transaction dump 114 | 115 | The following command computes entities for a given blockchain data dump: 116 | 117 | bcgraph-compute-entities -i blocks_0_1000 118 | 119 | Two additional files are created: 120 | 121 | * entities.csv: list of entity identifiers (entity_id) 122 | * rel_address_entity.csv: assignment of addresses to entities (address, entity_id) 123 | 124 | 125 | ### Step 3: Ingest pre-computed dump into Neo4J 126 | 127 | Download and install [Neo4J][neo4j] community edition (>= 2.3.0): 128 | 129 | tar xvfz neo4j-community-2.3.0-unix.tar.gz 130 | export NEO4J_HOME=[PATH_TO_NEO4J_INSTALLATION] 131 | 132 | Test Neo4J installation: 133 | 134 | $NEO4J_HOME/bin/neo4j start 135 | http://localhost:7474/ 136 | 137 | 138 | Install and make sure is not running and pre-existing databases are removed: 139 | 140 | $NEO4J_HOME/bin/neo4j stop 141 | rm -rf $NEO4J_HOME/data/* 142 | 143 | 144 | Switch back into the dump directory and create a new database using Neo4J's CSV importer tool: 145 | 146 | $NEO4J_HOME/bin/neo4j-import --into $NEO4J_HOME/data/graph.db \ 147 | --nodes:Block blocks_header.csv,blocks.csv \ 148 | --nodes:Transaction transactions_header.csv,transactions.csv \ 149 | --nodes:Output outputs_header.csv,outputs.csv \ 150 | --nodes:Address addresses_header.csv,addresses.csv \ 151 | --nodes:Entity entities.csv \ 152 | --relationships:CONTAINS rel_block_tx_header.csv,rel_block_tx.csv \ 153 | --relationships:OUTPUT rel_tx_output_header.csv,rel_tx_output.csv \ 154 | --relationships:INPUT rel_input_header.csv,rel_input.csv \ 155 | --relationships:USES rel_output_address_header.csv,rel_output_address.csv \ 156 | --relationships:BELONGS_TO rel_address_entity.csv 157 | 158 | 159 | Then, start the Neo4J shell...: 160 | 161 | $NEO4J_HOME/bin/neo4j-shell -path $NEO4J_HOME/data 162 | 163 | and create the following uniquness constraints: 164 | 165 | CREATE CONSTRAINT ON (a:Address) ASSERT a.address IS UNIQUE; 166 | 167 | CREATE CONSTRAINT ON (o:Output) ASSERT o.txid_n IS UNIQUE; 168 | 169 | 170 | Finally start Neo4J 171 | 172 | $NEO4J_HOME/bin/neo4j start 173 | 174 | 175 | ### Step 4: Enrich transaction graph with identity information 176 | 177 | Some bitcoin addresses have associated public identity information. Bitcoingraph provides an example script which collects information from blockchain.info. 178 | 179 | utils/identity_information.py 180 | 181 | The resulting CSV file can be imported into Neo4j with the Cypher statement: 182 | 183 | LOAD CSV WITH HEADERS FROM "file:///identities.csv" AS row 184 | MERGE (a:Address {address: row.address}) 185 | CREATE a-[:HAS]->(i:Identity 186 | {name: row.tag, link: row.link, source: "https://blockchain.info/"}) 187 | 188 | 189 | ### Step 5: Install Neo4J entity computation plugin 190 | 191 | Clone the git repository and compile from source. This requires Maven and Java JDK to be installed. 192 | 193 | git clone https://github.com/romankarl/entity-plugin.git 194 | cd entity-plugin 195 | mvn package 196 | 197 | Copy the JAR package into Neo4j's plugin directory. 198 | 199 | service neo4j-service stop 200 | cp target/entities-plugin-0.0.1-SNAPSHOT.jar $NEO4J_HOME/plugins/ 201 | service neo4j-service start 202 | 203 | 204 | 205 | ### Step 6: Enable synchronization with Bitcoin block chain 206 | 207 | Bitcoingraph provides a synchronisation script, which reads blocks from bitcoind and writes them into Neo4j. It is intended to be called by a cron job which runs daily or more frequent. For performance reasons it is no substitution for steps 1-3. 208 | 209 | bcgraph-synchronize -s localhost -u RPC_USER -p RPC_PASS -S localhost -U NEO4J_USER -P NEO4J_PASS --rest 210 | 211 | 212 | ## Contributors 213 | 214 | * [Bernhard Haslhofer](mailto:bernhard.haslhofer@ait.ac.at) 215 | * [Roman Karl](mailto:roman.karl@ait.ac.at) 216 | 217 | 218 | ## License 219 | 220 | This library is release Open Source under the [MIT license](http://opensource.org/licenses/MIT). 221 | 222 | [bc_core]: https://github.com/bitcoin/bitcoin "Bitcoin Core" 223 | [bc_conf]: https://en.bitcoin.it/wiki/Running_Bitcoin#Bitcoin.conf_Configuration_File "Bitcoin Core configuration file" 224 | [neo4j]: http://neo4j.com/ "Neo4J" 225 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Bitcoingraph - A Python library for exploring the Bitcoin transaction 2 | graph. 3 | 4 | |Build Status| 5 | 6 | Prerequesites 7 | ------------- 8 | 9 | Bitcoin Core setup and configuration 10 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | First, install the current version of Bitcoin Core (v.11.1), either from 13 | `source `__ or from a `pre-compiled 14 | executable `__. 15 | 16 | Once installed, you'll have access to three programs: ``bitcoind`` (= 17 | full peer), ``bitcoin-qt`` (= peer with GUI), and ``bitcoin-cli`` (RPC 18 | command line interface). The following instructions have been tested 19 | with ``bitcoind`` and assume you can start and run a Bitcoin Core peer 20 | as follows: 21 | 22 | :: 23 | 24 | bitcoind -printtoconsole 25 | 26 | Second, you must make sure that your bitcoin client accepts JSON-RPC 27 | connections by modifying the `Bitcoin Core configuration 28 | file `__ 29 | as follows: 30 | 31 | :: 32 | 33 | # server=1 tells Bitcoin-QT to accept JSON-RPC commands. 34 | server=1 35 | 36 | # You must set rpcuser and rpcpassword to secure the JSON-RPC api 37 | rpcuser=your_rpcuser 38 | rpcpassword=your_rpcpass 39 | 40 | # How many seconds bitcoin will wait for a complete RPC HTTP request. 41 | # after the HTTP connection is established. 42 | rpctimeout=30 43 | 44 | # Listen for RPC connections on this TCP port: 45 | rpcport=8332 46 | 47 | Test whether the JSON-RPC interface is working by starting your Bitcoin 48 | Core peer (...waiting until it finished startup...) and using the 49 | following cURL request (with adapted username and password): 50 | 51 | :: 52 | 53 | curl --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "getinfo", "params": [] }' -H 'content-type: text/plain;' http://your_rpcuser:your_rpcpass@localhost:8332/ 54 | 55 | Third, since Bitcoingraph needs to access non-wallet blockchain 56 | transactions by their ids, you need to enable the transaction index in 57 | the Bitcoin Core database. This can be achieved by adding the following 58 | property to your ``bitcoin.conf`` 59 | 60 | :: 61 | 62 | txindex=1 63 | 64 | ... and restarting your Bitcoin core peer as follows (rebuilding the 65 | index can take a while): 66 | 67 | :: 68 | 69 | bitcoind -reindex 70 | 71 | Test non-wallet transaction data access by taking an arbitrary 72 | transaction id and issuing the following request using cURL: 73 | 74 | :: 75 | 76 | curl --data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "getrawtransaction", "params": ["110ed92f558a1e3a94976ddea5c32f030670b5c58c3cc4d857ac14d7a1547a90", 1] }' -H 'content-type: text/plain;' http://your_rpcuser:your_rpcpass@localhost:8332/ 77 | 78 | Finally, bitcoingraph also makes use of Bitcoin Core's HTTP REST 79 | interface, which is enabled using the following parameter: 80 | 81 | :: 82 | 83 | bitcoind -rest 84 | 85 | Test it using some sample block hash 86 | 87 | :: 88 | 89 | http://localhost:8332/rest/block/000000000000000e7ad69c72afc00dc4e05fc15ae3061c47d3591d07c09f2928.json 90 | 91 | When you reached this point, your Bitcoin Core setup is working. 92 | Terminate all running bitcoind instances and launch a new background 93 | daemon with enabled REST interface 94 | 95 | :: 96 | 97 | bitcoind -daemon -rest 98 | 99 | Bitcoingraph library setup 100 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | 102 | Bitcoingraph is being developed in Python 3.4. Make sure it is running 103 | on your machine: 104 | 105 | :: 106 | 107 | python --version 108 | 109 | Now clone Bitcoingraph... 110 | 111 | :: 112 | 113 | git clone https://github.com/behas/bitcoingraph.git 114 | 115 | ...test and install the Bitcoingraph library: 116 | 117 | :: 118 | 119 | cd bitcoingraph 120 | pip install -r requirements.txt 121 | py.test 122 | python setup.py install 123 | 124 | Mac OSX specifics 125 | ~~~~~~~~~~~~~~~~~ 126 | 127 | Running bitcoingraph on a Mac requires coreutils to be installed 128 | 129 | :: 130 | 131 | homebrew install coreutils 132 | 133 | Boostrapping the underlying graph database (Neo4J) 134 | -------------------------------------------------- 135 | 136 | bitcoingraph stores Bitcoin transactions as directed labelled graph in a 137 | Neo4J graph database instance. This database can be bootstrapped by 138 | loading an initial blockchain dump, performing entity computation over 139 | the entire dump as described by `Ron and 140 | Shamir `__, and ingesting it into 141 | a running Neo4J instance. 142 | 143 | Step 1: Create transaction dump from blockchain 144 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 145 | 146 | Bitcoingraph provides the ``bcgraph-export`` tool for exporting 147 | transactions in a given block range from the blockchain. The following 148 | command exports all transactions contained in block range 0 to 1000 149 | using Neo4Js header format and separate CSV header files: 150 | 151 | :: 152 | 153 | bcgraph-export 0 1000 -u your_rpcuser -p your_rpcpass 154 | 155 | The following CSV files are created (with separate header files): 156 | 157 | - addresses.csv: sorted list of Bitcoin addressed 158 | - blocks.csv: list of blocks (hash, height, timestamp) 159 | - transactions.csv: list of transactions (hash, coinbase/non-coinbase) 160 | - outputs.csv: list of transaction outputs (output key, id, value, 161 | script type) 162 | - rel\_block\_tx.csv: relationship between blocks and transactions 163 | (block\_hash, tx\_hash) 164 | - rel\_input.csv: relationship between transactions and transaction 165 | outputs (tx\_hash, output key) 166 | - rel\_output\_address.csv: relationship between outputs and addresses 167 | (output key, address) 168 | - rel\_tx\_output.csv: relationship between transactions and 169 | transaction outputs (tx\_hash, output key) 170 | 171 | Step 2: Compute entities over transaction dump 172 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 173 | 174 | The following command computes entities for a given blockchain data 175 | dump: 176 | 177 | :: 178 | 179 | bcgraph-compute-entities -i blocks_0_1000 180 | 181 | Two additional files are created: 182 | 183 | - entities.csv: list of entity identifiers (entity\_id) 184 | - rel\_address\_entity.csv: assignment of addresses to entities 185 | (address, entity\_id) 186 | 187 | Step 3: Ingest pre-computed dump into Neo4J 188 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 189 | 190 | Download and install `Neo4J `__ community edition (>= 191 | 2.3.0): 192 | 193 | :: 194 | 195 | tar xvfz neo4j-community-2.3.0-unix.tar.gz 196 | export NEO4J_HOME=[PATH_TO_NEO4J_INSTALLATION] 197 | 198 | Test Neo4J installation: 199 | 200 | :: 201 | 202 | $NEO4J_HOME/bin/neo4j start 203 | http://localhost:7474/ 204 | 205 | Install and make sure is not running and pre-existing databases are 206 | removed: 207 | 208 | :: 209 | 210 | $NEO4J_HOME/bin/neo4j stop 211 | rm -rf $NEO4J_HOME/data/* 212 | 213 | Switch back into the dump directory and create a new database using 214 | Neo4J's CSV importer tool: 215 | 216 | :: 217 | 218 | $NEO4J_HOME/bin/neo4j-import --into $NEO4J_HOME/data/graph.db \ 219 | --nodes:Block blocks_header.csv,blocks.csv \ 220 | --nodes:Transaction transactions_header.csv,transactions.csv \ 221 | --nodes:Output outputs_header.csv,outputs.csv \ 222 | --nodes:Address addresses_header.csv,addresses.csv \ 223 | --nodes:Entity entities.csv \ 224 | --relationships:CONTAINS rel_block_tx_header.csv,rel_block_tx.csv \ 225 | --relationships:OUTPUT rel_tx_output_header.csv,rel_tx_output.csv \ 226 | --relationships:INPUT rel_input_header.csv,rel_input.csv \ 227 | --relationships:USES rel_output_address_header.csv,rel_output_address.csv \ 228 | --relationships:BELONGS_TO rel_address_entity.csv 229 | 230 | Then, start the Neo4J shell...: 231 | 232 | :: 233 | 234 | $NEO4J_HOME/bin/neo4j-shell -path $NEO4J_HOME/data 235 | 236 | and create the following uniquness constraints: 237 | 238 | :: 239 | 240 | CREATE CONSTRAINT ON (a:Address) ASSERT a.address IS UNIQUE; 241 | 242 | CREATE CONSTRAINT ON (o:Output) ASSERT o.txid_n IS UNIQUE; 243 | 244 | Finally start Neo4J 245 | 246 | :: 247 | 248 | $NEO4J_HOME/bin/neo4j start 249 | 250 | Step 4: Enrich transaction graph with identity information 251 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 252 | 253 | Some bitcoin addresses have associated public identity information. 254 | Bitcoingraph provides an example script which collects information from 255 | blockchain.info. 256 | 257 | :: 258 | 259 | utils/identity_information.py 260 | 261 | The resulting CSV file can be imported into Neo4j with the Cypher 262 | statement: 263 | 264 | :: 265 | 266 | LOAD CSV WITH HEADERS FROM "file:///identities.csv" AS row 267 | MERGE (a:Address {address: row.address}) 268 | CREATE a-[:HAS]->(i:Identity 269 | {name: row.tag, link: row.link, source: "https://blockchain.info/"}) 270 | 271 | Step 5: Install Neo4J entity computation plugin 272 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 273 | 274 | Clone the git repository and compile from source. This requires Maven 275 | and Java JDK to be installed. 276 | 277 | :: 278 | 279 | git clone https://github.com/romankarl/entity-plugin.git 280 | cd entity-plugin 281 | mvn package 282 | 283 | Copy the JAR package into Neo4j's plugin directory. 284 | 285 | :: 286 | 287 | service neo4j-service stop 288 | cp target/entities-plugin-0.0.1-SNAPSHOT.jar $NEO4J_HOME/plugins/ 289 | service neo4j-service start 290 | 291 | Step 6: Enable synchronization with Bitcoin block chain 292 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 293 | 294 | Bitcoingraph provides a synchronisation script, which reads blocks from 295 | bitcoind and writes them into Neo4j. It is intended to be called by a 296 | cron job which runs daily or more frequent. For performance reasons it 297 | is no substitution for steps 1-3. 298 | 299 | :: 300 | 301 | bcgraph-synchronize -s localhost -u RPC_USER -p RPC_PASS -S localhost -U NEO4J_USER -P NEO4J_PASS --rest 302 | 303 | Contributors 304 | ------------ 305 | 306 | - `Bernhard Haslhofer `__ 307 | - `Roman Karl `__ 308 | 309 | License 310 | ------- 311 | 312 | This library is release Open Source under the `MIT 313 | license `__. 314 | 315 | .. |Build Status| image:: https://travis-ci.org/behas/bitcoingraph.svg?branch=master 316 | :target: https://travis-ci.org/behas/bitcoingraph 317 | -------------------------------------------------------------------------------- /RELEASE: -------------------------------------------------------------------------------- 1 | # How to release 2 | 3 | * git flow release start v0.X (start a release by creating release branch from develop) 4 | * Update CHANGELOG 5 | * Update version in __init__.py 6 | * Update README.md -> change button URIs to master branch 7 | * pandoc README.md -o README.rst 8 | * sphinx-apidoc -f -o docs bitcoingraph 9 | * cd docs && make html 10 | * commit changes 11 | * git flow release finish 'v0.X' (finishes release) 12 | * git push --tags 13 | * git checkout develop 14 | * Update version in __init__.py to v0.X+1 15 | -------------------------------------------------------------------------------- /bitcoingraph/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | bitcoingraph 3 | 4 | A Python library for exploring the Bitcoin transaction graph. 5 | 6 | """ 7 | 8 | from bitcoingraph.bitcoingraph import BitcoinGraph 9 | 10 | __author__ = 'Bernhard Haslhofer, Roman Karl' 11 | __license__ = "MIT" 12 | __version__ = '0.3.2dev' 13 | -------------------------------------------------------------------------------- /bitcoingraph/bitcoind.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bitcoin Core JSON-RPC interface. 3 | 4 | """ 5 | 6 | import requests 7 | import json 8 | 9 | import time 10 | 11 | 12 | __author__ = 'Bernhard Haslhofer (bernhard.haslhofer@ait.ac.at)' 13 | __copyright__ = 'Copyright 2015, Bernhard Haslhofer' 14 | __license__ = "MIT" 15 | 16 | 17 | class BitcoindException(Exception): 18 | """ 19 | Exception raised when accessing Bitcoin Core via JSON-RPCS. 20 | """ 21 | pass 22 | 23 | 24 | class JSONRPCInterface: 25 | """ 26 | A generic JSON-RPC interface with keep-alive session reuse. 27 | """ 28 | 29 | def __init__(self, url): 30 | """ 31 | Creates a generic JSON-RPC interface object. 32 | 33 | :param str url: URL of JSON-RPC endpoint 34 | :return: JSON-RPC proxy object 35 | :rtype: JSONRPCInterface 36 | """ 37 | self._session = requests.Session() 38 | self._url = url 39 | self._headers = {'content-type': 'application/json'} 40 | 41 | def call(self, rpcMethod, *params): 42 | """ 43 | Execute a single request against a JSON-RPC interface 44 | """ 45 | request = {"jsonrpc": "2.0", 46 | "method": rpcMethod, 47 | "params": list(params)} 48 | responseJSON = self._execute(request) 49 | return responseJSON['result'] 50 | 51 | def batch(self, calls): 52 | """ 53 | Executes a batch request (with same method) but different parameters 54 | against a JSON-RPC interface 55 | """ 56 | requests = [] 57 | for call in calls: 58 | request = {"jsonrpc": "2.0", 59 | "method": call['method'], 60 | "params": call['params'], 61 | "id": call['id']} 62 | requests.append(request) 63 | responseJSON = self._execute(requests) 64 | return responseJSON 65 | 66 | def _execute(self, request): 67 | payload = json.dumps(request) 68 | 69 | tries = 5 70 | hadConnectionFailures = False 71 | while True: 72 | try: 73 | response = self._session.post(self._url, headers=self._headers, 74 | data=payload) 75 | except requests.exceptions.ConnectionError as e: 76 | print(self._url) 77 | print(e) 78 | tries -= 1 79 | if tries == 0: 80 | raise BitcoindException('Failed to connect for RPC call.') 81 | hadConnectionFailures = True 82 | print("Couldn't connect for remote procedure call.", 83 | "will sleep for ten seconds and then try again...") 84 | time.sleep(10) 85 | else: 86 | if hadConnectionFailures: 87 | print("Connected for RPC call after retry.") 88 | break 89 | if response.status_code not in (200, 500): 90 | raise BitcoindException("RPC connection failure: " + 91 | str(response.status_code) + ' ' + 92 | response.reason) 93 | responseJSON = response.json() 94 | if 'error' in responseJSON and responseJSON['error'] is not None: 95 | raise BitcoindException('Error in RPC call: ' + 96 | str(responseJSON['error'])) 97 | return responseJSON 98 | 99 | 100 | class RESTInterface: 101 | 102 | def __init__(self, url): 103 | self._session = requests.Session() 104 | self._url = url 105 | 106 | def get_block(self, hash): 107 | r = self._session.get(self._url + 'block/{}.json'.format(hash)) 108 | if r.status_code != 200: 109 | raise Exception('REST request was not successful') 110 | return r.json() 111 | 112 | 113 | class BitcoinProxy: 114 | """ 115 | Proxy to Bitcoin JSON RPC Service. 116 | 117 | Implements a subset of call list described 118 | `here `_ 119 | """ 120 | 121 | def __init__(self, host, port, rpc_user=None, rpc_pass=None, method='RPC'): 122 | """ 123 | Creates a Bitcoin JSON RPC Service object. 124 | 125 | :param str url: URL of Bitcoin Core JSON-RPC endpoint 126 | :return: bitcoin proxy object 127 | :rtype: BitcoinProxy 128 | """ 129 | self.method = method 130 | rest_url = 'http://{}:{}/rest/'.format(host, port) 131 | rpc_url = 'http://{}:{}@{}:{}/'.format(rpc_user, rpc_pass, host, port) 132 | self._jsonrpc_proxy = JSONRPCInterface(rpc_url) 133 | if method == 'REST': 134 | self._rest_proxy = RESTInterface(rest_url) 135 | 136 | def getblock(self, block_hash): 137 | """ 138 | Returns information about the block with the given hash. 139 | 140 | :param str block_hash: the block hash 141 | :return: block as JSON 142 | :rtype: str 143 | """ 144 | if self.method == 'REST': 145 | r = self._rest_proxy.get_block(block_hash) 146 | else: 147 | r = self._jsonrpc_proxy.call('getblock', block_hash) 148 | return r 149 | 150 | def getblockcount(self): 151 | """ 152 | Returns the number of blocks in the longest block chain. 153 | 154 | :return: number of blocks in block chain 155 | :rtype: int 156 | """ 157 | r = self._jsonrpc_proxy.call('getblockcount') 158 | return int(r) 159 | 160 | def getblockhash(self, height): 161 | """ 162 | Returns hash of block in best-block-chain at given height. 163 | 164 | :param str height: the block height 165 | :return: block hash 166 | :rtype: str 167 | """ 168 | r = self._jsonrpc_proxy.call('getblockhash', height) 169 | return r 170 | 171 | def getinfo(self): 172 | """ 173 | Returns an object containing various state info. 174 | 175 | :return: JSON string with state info 176 | :rtype: str 177 | """ 178 | r = self._jsonrpc_proxy.call('getinfo') 179 | return r 180 | 181 | def getrawtransaction(self, tx_id, verbose=1): 182 | """ 183 | Returns raw transaction representation for given transaction id. 184 | 185 | :param str tx_id: transaction id 186 | :param int verbose: complete transaction record (0 = false, 1 = true) 187 | :return: raw transaction data as JSON 188 | :rtype: str 189 | """ 190 | r = self._jsonrpc_proxy.call('getrawtransaction', tx_id, verbose) 191 | return r 192 | 193 | def getrawtransactions(self, tx_ids, verbose=1): 194 | """ 195 | Returns raw transaction representation for a given list of transaction 196 | ids. 197 | 198 | :param tx_ids: list of transaction ids 199 | :param int verbose: complete transaction record (0 = false, 1 = true) 200 | :return: array of raw transaction data as JSON 201 | :rtype: dictionary (key=id, value=result) 202 | """ 203 | calls = [] 204 | for tx_id in tx_ids: 205 | call = {'method': 'getrawtransaction', 206 | 'params': [tx_id, verbose], 207 | 'id': tx_id} 208 | calls.append(call) 209 | r = self._jsonrpc_proxy.batch(calls) 210 | 211 | results = [] 212 | for entry in r: 213 | results.append(entry['result']) 214 | return results 215 | -------------------------------------------------------------------------------- /bitcoingraph/bitcoingraph.py: -------------------------------------------------------------------------------- 1 | """ 2 | bitcoingraph 3 | 4 | A Python library for extracting and navigating graph structures from 5 | the Bitcoin block chain. 6 | 7 | """ 8 | 9 | import logging 10 | 11 | from bitcoingraph.bitcoind import BitcoinProxy, BitcoindException 12 | from bitcoingraph.blockchain import Blockchain 13 | from bitcoingraph import entities 14 | from bitcoingraph.graphdb import GraphController 15 | from bitcoingraph.helper import sort 16 | from bitcoingraph.writer import CSVDumpWriter 17 | 18 | logger = logging.getLogger('bitcoingraph') 19 | 20 | 21 | class BitcoingraphException(Exception): 22 | """ 23 | Top-level exception raised when interacting with bitcoingraph 24 | library. 25 | """ 26 | 27 | def __init__(self, msg, inner_exc): 28 | self.msg = msg 29 | self.inner_exc = inner_exc 30 | 31 | def __str__(self): 32 | return self.msg 33 | 34 | 35 | class BitcoinGraph: 36 | """Facade which provides the main access to this package.""" 37 | 38 | def __init__(self, **config): 39 | """Create an instance based on the configuration.""" 40 | self.blockchain = self.__get_blockchain(config['blockchain']) 41 | if 'neo4j' in config: 42 | nc = config['neo4j'] 43 | self.graph_db = GraphController(nc['host'], nc['port'], nc['user'], nc['pass']) 44 | 45 | @staticmethod 46 | def __get_blockchain(config): 47 | """Connect to Bitcoin Core (via JSON-RPC) and return a 48 | Blockchain object. 49 | """ 50 | try: 51 | logger.debug("Connecting to Bitcoin Core at {}".format(config['host'])) 52 | bc_proxy = BitcoinProxy(**config) 53 | bc_proxy.getinfo() 54 | logger.debug("Connection successful.") 55 | blockchain = Blockchain(bc_proxy) 56 | return blockchain 57 | except BitcoindException as exc: 58 | raise BitcoingraphException("Couldn't connect to {}.".format(config['host']), exc) 59 | 60 | def get_transaction(self, tx_id): 61 | """Return a transaction.""" 62 | return self.blockchain.get_transaction(tx_id) 63 | 64 | def incoming_addresses(self, address, date_from, date_to): 65 | return self.graph_db.incoming_addresses(address, date_from, date_to) 66 | 67 | def outgoing_addresses(self, address, date_from, date_to): 68 | return self.graph_db.outgoing_addresses(address, date_from, date_to) 69 | 70 | def transaction_relations(self, address1, address2, date_from=None, date_to=None): 71 | return self.graph_db.transaction_relations(address1, address2, date_from, date_to) 72 | 73 | def get_block_by_height(self, height): 74 | """Return the block for a given height.""" 75 | return self.blockchain.get_block_by_height(height) 76 | 77 | def get_block_by_hash(self, hash): 78 | """Return a block.""" 79 | return self.blockchain.get_block_by_hash(hash) 80 | 81 | def search_address_by_identity_name(self, term): 82 | """Return an address that has an associated identity with 83 | the given name. 84 | """ 85 | return self.graph_db.search_address_by_identity_name(term) 86 | 87 | def get_address_info(self, address, date_from, date_to): 88 | """Return basic address information for the given 89 | time period. 90 | """ 91 | return self.graph_db.get_address_info(address, date_from, date_to) 92 | 93 | def get_address(self, address, current_page, date_from, date_to, 94 | rows_per_page=GraphController.rows_per_page_default): 95 | """Return an address with its transaction uses in a given 96 | time period. 97 | """ 98 | return self.graph_db.get_address(address, current_page, date_from, date_to, rows_per_page) 99 | 100 | def get_identities(self, address): 101 | """Return a list of identities.""" 102 | return self.graph_db.get_identities(address) 103 | 104 | def add_identity(self, address, name, link, source): 105 | """Add an identity to an address.""" 106 | self.graph_db.add_identity(address, name, link, source) 107 | 108 | def delete_identity(self, identity_id): 109 | """Delete an identity.""" 110 | return self.graph_db.delete_identity(identity_id) 111 | 112 | def get_entity(self, id): 113 | """Return an entity.""" 114 | return self.graph_db.get_entity(id) 115 | 116 | def get_path(self, start, end): 117 | """Return a path between addresses.""" 118 | return self.graph_db.get_path(start, end) 119 | 120 | def get_received_bitcoins(self, address): 121 | """Return the total number of bitcoins received by this address.""" 122 | return self.graph_db.get_received_bitcoins(address) 123 | 124 | def get_unspent_bitcoins(self, address): 125 | """Return the current balance of this address.""" 126 | return self.graph_db.get_unspent_bitcoins(address) 127 | 128 | def export(self, start, end, output_path=None, plain_header=False, separate_header=True, 129 | progress=None, deduplicate_transactions=True): 130 | """Export the blockchain into CSV files.""" 131 | if output_path is None: 132 | output_path = 'blocks_{}_{}'.format(start, end) 133 | 134 | number_of_blocks = end - start + 1 135 | with CSVDumpWriter(output_path, plain_header, separate_header) as writer: 136 | for block in self.blockchain.get_blocks_in_range(start, end): 137 | writer.write(block) 138 | if progress: 139 | processed_blocks = block.height - start + 1 140 | last_percentage = ((processed_blocks - 1) * 100) // number_of_blocks 141 | percentage = (processed_blocks * 100) // number_of_blocks 142 | if percentage > last_percentage: 143 | progress(processed_blocks / number_of_blocks) 144 | if separate_header: 145 | sort(output_path, 'addresses.csv', '-u') 146 | if deduplicate_transactions: 147 | for base_name in ['transactions', 'rel_tx_output', 148 | 'outputs', 'rel_output_address']: 149 | sort(output_path, base_name + '.csv', '-u') 150 | 151 | def synchronize(self, max_blocks=None): 152 | """Synchronise the graph database with the blockchain 153 | information from the bitcoin client. 154 | """ 155 | start = self.graph_db.get_max_block_height() + 1 156 | blockchain_end = self.blockchain.get_max_block_height() - 2 157 | if start > blockchain_end: 158 | print('Already up-to-date.') 159 | else: 160 | if max_blocks is None: 161 | end = blockchain_end 162 | else: 163 | end = min(start + max_blocks - 1, blockchain_end) 164 | print('add blocks', start, 'to', end) 165 | for block in self.blockchain.get_blocks_in_range(start, end): 166 | self.graph_db.add_block(block) 167 | 168 | 169 | def compute_entities(input_path, sort_input=False): 170 | """Read exported CSV files containing blockchain information and 171 | export entities into CSV files. 172 | """ 173 | if sort_input: 174 | sort(input_path, 'rel_output_address.csv') 175 | sort(input_path, 'rel_input.csv', '-k 2 -t ,') 176 | entities.calculate_input_addresses(input_path) 177 | sort(input_path, 'input_addresses.csv') 178 | entities.compute_entities(input_path) 179 | -------------------------------------------------------------------------------- /bitcoingraph/blockchain.py: -------------------------------------------------------------------------------- 1 | """ 2 | blockchain 3 | 4 | An API for traversing the Bitcoin blockchain 5 | 6 | """ 7 | 8 | from bitcoingraph.model import Block, Transaction 9 | from bitcoingraph.bitcoind import BitcoindException 10 | 11 | __author__ = 'Bernhard Haslhofer (bernhard.haslhofer@ait.ac.at)' 12 | __copyright__ = 'Copyright 2015, Bernhard Haslhofer' 13 | __license__ = "MIT" 14 | 15 | 16 | class BlockchainException(Exception): 17 | """ 18 | Exception raised when accessing or navigating the block chain. 19 | """ 20 | 21 | def __init__(self, msg, inner_exc): 22 | self.msg = msg 23 | self.inner_exc = inner_exc 24 | 25 | def __str__(self): 26 | return repr(self.msg) 27 | 28 | 29 | class Blockchain: 30 | 31 | """ 32 | Bitcoin block chain. 33 | """ 34 | 35 | def __init__(self, bitcoin_proxy): 36 | """ 37 | Creates a block chain object. 38 | 39 | :param BitcoinProxy bitcoin_proxy: reference to Bitcoin proxy 40 | :return: block chain object 41 | :rtype: Blockchain 42 | """ 43 | self._bitcoin_proxy = bitcoin_proxy 44 | 45 | def get_block_by_hash(self, block_hash): 46 | """ 47 | Returns a block by given block hash. 48 | 49 | :param str block_hash: hash of block to be returned 50 | :return: the requested block 51 | :rtype: Block 52 | :raises BlockchainException: if block cannot be retrieved 53 | """ 54 | # Returns block by hash 55 | try: 56 | raw_block_data = self._bitcoin_proxy.getblock(block_hash) 57 | return Block(self, json_data=raw_block_data) 58 | except BitcoindException as exc: 59 | raise BlockchainException('Cannot retrieve block {}'.format(block_hash), exc) 60 | 61 | def get_block_by_height(self, block_height): 62 | """ 63 | Returns a block by given block height. 64 | 65 | :param int block_height: height of block to be returned 66 | :return: the requested block 67 | :rtype: Block 68 | :raises BlockchainException: if block cannot be retrieved 69 | """ 70 | # Returns block by height 71 | try: 72 | block_hash = self._bitcoin_proxy.getblockhash(block_height) 73 | return self.get_block_by_hash(block_hash) 74 | except BitcoindException as exc: 75 | raise BlockchainException( 76 | 'Cannot retrieve block with height {}'.format(block_height), exc) 77 | 78 | def get_blocks_in_range(self, start_height=0, end_height=0): 79 | """ 80 | Generates blocks in a given range. 81 | 82 | :param int start_height: first block height in range 83 | :param int end_height: last block height in range 84 | :yield: the requested blocks 85 | :rtype: Block 86 | """ 87 | block = self.get_block_by_height(start_height) 88 | while block.height <= end_height: 89 | yield block 90 | if block.has_next_block(): 91 | block = block.next_block 92 | else: 93 | break 94 | 95 | def get_transaction(self, tx_id): 96 | """ 97 | Returns a transaction by given transaction id. 98 | 99 | :param str tx_id: transaction id 100 | :return: the requested transaction 101 | :rtype: Transaction 102 | """ 103 | try: 104 | raw_tx_data = self._bitcoin_proxy.getrawtransaction(tx_id) 105 | return Transaction(self, json_data=raw_tx_data) 106 | except BitcoindException as exc: 107 | raise BlockchainException('Cannot retrieve transaction with id {}'.format(tx_id), exc) 108 | 109 | def get_transactions(self, tx_ids): 110 | """ 111 | Returns transactions for given transaction ids. 112 | 113 | :param tx_ids: list of transaction ids 114 | :return: list of transaction objects 115 | :rtype: Transaction list 116 | """ 117 | try: 118 | txs = [] 119 | raw_txs_data = self._bitcoin_proxy.getrawtransactions(tx_ids) 120 | for raw_tx_data in raw_txs_data: 121 | txs.append(Transaction(raw_tx_data, self)) 122 | return txs 123 | except BitcoindException as exc: 124 | raise BlockchainException('Cannot retrieve transactions {}'.format(tx_ids), exc) 125 | 126 | def get_max_block_height(self): 127 | """ 128 | Returns maximum known block height. 129 | 130 | :return: maximum block height 131 | :rtype: int 132 | """ 133 | try: 134 | max_height = self._bitcoin_proxy.getblockcount() 135 | return max_height 136 | except BitcoindException as exc: 137 | raise BlockchainException("Error when retrieving maximum\ 138 | block height", exc) 139 | -------------------------------------------------------------------------------- /bitcoingraph/entities.py: -------------------------------------------------------------------------------- 1 | import bisect 2 | import csv 3 | import os 4 | 5 | 6 | class Address: 7 | 8 | counter = 0 9 | 10 | def __init__(self, address, assign_number=False): 11 | self.address = address 12 | self.representative = None 13 | self.height = 0 14 | if assign_number: 15 | self.number = Address.counter 16 | Address.counter += 1 17 | 18 | def get_representative(self): 19 | r = self 20 | while r.representative is not None: 21 | r = r.representative 22 | return r 23 | 24 | def set_representative(self, address): 25 | self.representative = address 26 | if self.height >= address.height: 27 | address.height = self.height + 1 28 | 29 | def __lt__(self, other): 30 | return self.address < other.address 31 | 32 | def __eq__(self, other): 33 | return self.address == other.address 34 | 35 | def __hash__(self): 36 | return hash(self.address) 37 | 38 | 39 | class AddressList: 40 | 41 | def __init__(self): 42 | self.addresses = [] 43 | 44 | def add(self, address_string): 45 | self.addresses.append(Address(address_string, True)) 46 | 47 | def group(self, address_strings): 48 | if len(address_strings) >= 2: 49 | addresses = list(map(self.search, address_strings)) 50 | representatives = {address.get_representative() for address in addresses} 51 | highest_representative = None 52 | for representative in representatives: 53 | if (highest_representative is None or 54 | representative.height > highest_representative.height): 55 | highest_representative = representative 56 | representatives.remove(highest_representative) 57 | for representative in representatives: 58 | representative.set_representative(highest_representative) 59 | 60 | def search(self, address_string): 61 | index = bisect.bisect_left(self.addresses, Address(address_string)) 62 | return self.addresses[index] 63 | 64 | def export(self, path): 65 | with open(os.path.join(path, 'entities.csv'), 'w') as entity_csv_file, \ 66 | open(os.path.join(path, 'rel_address_entity.csv'), 'w') as entity_rel_csv_file: 67 | entity_writer = csv.writer(entity_csv_file) 68 | entity_rel_writer = csv.writer(entity_rel_csv_file) 69 | entity_writer.writerow(['id:ID(Entity)']) 70 | entity_rel_writer.writerow([':START_ID(Address)', ':END_ID(Entity)']) 71 | for address in self.addresses: 72 | representative = address.get_representative() 73 | if address == representative: 74 | entity_writer.writerow([representative.number]) 75 | entity_rel_writer.writerow([address.address, representative.number]) 76 | 77 | def print(self): 78 | for address in self.addresses: 79 | print(address.address, address.get_representative().address) 80 | 81 | 82 | def compute_entities(input_path): 83 | address_list = AddressList() 84 | print('reading addresses') 85 | with open(os.path.join(input_path, 'addresses.csv'), 'r') as address_file: 86 | for line in address_file: 87 | line = line.strip() 88 | address_list.add(line) 89 | print('reading inputs') 90 | input_counter = 0 91 | with open(os.path.join(input_path, 'input_addresses.csv'), 'r') as input_file: 92 | input_addresses = set() 93 | transaction = None 94 | for line in input_file: 95 | entries = line.strip().split(',') 96 | address = entries[1] 97 | if transaction is None or transaction == entries[0]: 98 | input_addresses.add(address) 99 | else: 100 | address_list.group(input_addresses) 101 | input_addresses = {address} 102 | transaction = entries[0] 103 | input_counter += 1 104 | if input_counter % (1000 * 1000) == 0: 105 | print('processed inputs:', input_counter) 106 | address_list.group(input_addresses) 107 | print('write to file') 108 | address_list.export(input_path) 109 | 110 | 111 | def open_csv(input_path, base_name, mode): 112 | return open(os.path.join(input_path, base_name + '.csv'), mode, newline='') 113 | 114 | 115 | def calculate_input_addresses(input_path): 116 | print('calculating input addresses') 117 | with open_csv(input_path, 'rel_input', 'r') as input_file, \ 118 | open_csv(input_path, 'rel_output_address', 'r') as output_address_file, \ 119 | open_csv(input_path, 'input_addresses', 'w') as input_addresses_file: 120 | input_reader = csv.reader(input_file) 121 | output_address_reader = csv.reader(output_address_file) 122 | input_address_writer = csv.writer(input_addresses_file) 123 | last_output = '' 124 | last_address = '' 125 | for input_row in input_reader: 126 | txid = input_row[0] 127 | output_ref = input_row[1] 128 | 129 | match_address = last_address if output_ref == last_output else None 130 | 131 | if output_ref >= last_output: 132 | for output_row in output_address_reader: 133 | output = output_row[0] 134 | address = output_row[1] 135 | if output_ref == output: 136 | if match_address is None: 137 | match_address = address 138 | else: 139 | match_address = None 140 | last_output = output 141 | last_address = address 142 | break 143 | elif output_ref < output: 144 | last_output = output 145 | last_address = address 146 | break 147 | last_output = output 148 | last_address = address 149 | 150 | if match_address is not None: 151 | input_address_writer.writerow([txid, match_address]) 152 | -------------------------------------------------------------------------------- /bitcoingraph/graphdb.py: -------------------------------------------------------------------------------- 1 | 2 | from bitcoingraph.neo4j import Neo4jController 3 | from bitcoingraph.helper import to_time, to_json 4 | 5 | 6 | def round_value(bitcoin_value): 7 | return round(bitcoin_value, 8) 8 | 9 | 10 | class GraphController: 11 | 12 | rows_per_page_default = 20 13 | 14 | def __init__(self, host, port, user, password): 15 | self.graph_db = Neo4jController(host, port, user, password) 16 | 17 | def get_address_info(self, address, date_from=None, date_to=None, 18 | rows_per_page=rows_per_page_default): 19 | result = self.graph_db.address_stats_query(address).single_row() 20 | if result['num_transactions'] == 0: 21 | return {'transactions': 0} 22 | if date_from is None and date_to is None: 23 | count = result['num_transactions'] 24 | else: 25 | count = self.graph_db.address_count_query(address, date_from, date_to).single_result() 26 | entity = self.graph_db.entity_query(address).single_result() 27 | return {'transactions': result['num_transactions'], 28 | 'first': to_time(result['first'], True), 29 | 'last': to_time(result['last'], True), 30 | 'entity': entity, 31 | 'pages': (count + rows_per_page - 1) // rows_per_page} 32 | 33 | def get_received_bitcoins(self, address): 34 | return self.graph_db.get_received_bitcoins(address) 35 | 36 | def get_unspent_bitcoins(self, address): 37 | return self.graph_db.get_unspent_bitcoins(address) 38 | 39 | def get_address(self, address, page, date_from=None, date_to=None, 40 | rows_per_page=rows_per_page_default): 41 | if rows_per_page is None: 42 | query = self.graph_db.address_query(address, date_from, date_to) 43 | else: 44 | query = self.graph_db.paginated_address_query(address, date_from, date_to, 45 | page * rows_per_page, rows_per_page) 46 | return Address(address, self.get_identities(address), query.get()) 47 | 48 | def incoming_addresses(self, address, date_from, date_to): 49 | return self.graph_db.incoming_addresses(address, date_from, date_to) 50 | 51 | def outgoing_addresses(self, address, date_from, date_to): 52 | return self.graph_db.outgoing_addresses(address, date_from, date_to) 53 | 54 | def transaction_relations(self, address1, address2, date_from, date_to): 55 | trs = self.graph_db.transaction_relations(address1, address2, date_from, date_to) 56 | transaction_relations = [{'txid': tr['txid'], 'in': round_value(tr['in']), 57 | 'out': round_value(tr['out']), 58 | 'timestamp': to_time(tr['timestamp'])} 59 | for tr in trs] 60 | return transaction_relations 61 | 62 | def get_identities(self, address): 63 | identities = self.graph_db.identity_query(address).single_result() 64 | return identities 65 | 66 | def get_entity(self, id, max_addresses=rows_per_page_default): 67 | count = self.graph_db.get_number_of_addresses_for_entity(id) 68 | result = self.graph_db.entity_address_query(id, max_addresses) 69 | entity = {'id': id, 'addresses': result.get(), 'number_of_addresses': count} 70 | return entity 71 | 72 | def search_address_by_identity_name(self, name): 73 | address = self.graph_db.reverse_identity_query(name).single_result() 74 | return address 75 | 76 | def add_identity(self, address, name, link, source): 77 | self.graph_db.identity_add_query(address, name, link, source) 78 | 79 | def delete_identity(self, id): 80 | self.graph_db.identity_delete_query(id) 81 | 82 | def get_path(self, address1, address2): 83 | return Path(self.graph_db.path_query(address1, address2)) 84 | 85 | def get_max_block_height(self): 86 | return self.graph_db.get_max_block_height() 87 | 88 | def add_block(self, block): 89 | print('add block', block.height) 90 | with self.graph_db.transaction() as db_transaction: 91 | block_node_id = db_transaction.add_block(block) 92 | for index, tx in enumerate(block.transactions): 93 | print('add transaction {} of {} (txid: {})'.format( 94 | index + 1, len(block.transactions), tx.txid)) 95 | tx_node_id = db_transaction.add_transaction(block_node_id, tx) 96 | if not tx.is_coinbase(): 97 | for input in tx.inputs: 98 | db_transaction.add_input(tx_node_id, input.output_reference) 99 | for output in tx.outputs: 100 | output_node_id = db_transaction.add_output(tx_node_id, output) 101 | for address in output.addresses: 102 | db_transaction.add_address(output_node_id, address) 103 | print('create entities for block (node id: {})'.format(block_node_id)) 104 | self.graph_db.create_entities(block_node_id) 105 | 106 | 107 | class Address: 108 | 109 | def __init__(self, address, identities, outputs): 110 | self.address = address 111 | self.identities = identities 112 | self.outputs = [{'txid': o['txid'], 'value': round_value(o['value']), 113 | 'timestamp': to_time(o['timestamp'])} 114 | for o in outputs] 115 | 116 | def get_incoming_transactions(self): 117 | for output in self.outputs: 118 | if output['value'] > 0: 119 | yield output 120 | 121 | def get_outgoing_transactions(self): 122 | for output in self.outputs: 123 | if output['value'] < 0: 124 | yield {'txid': output['txid'], 'value': -output['value'], 125 | 'timestamp': output['timestamp']} 126 | 127 | 128 | class Path: 129 | 130 | def __init__(self, raw_path): 131 | self.raw_path = raw_path 132 | 133 | @property 134 | def path(self): 135 | if self.raw_path: 136 | path = [] 137 | for idx, row in enumerate(self.raw_path): 138 | if 'txid' in row: 139 | path.append(row) 140 | else: 141 | output = row 142 | if idx != 0: 143 | path.append(output) 144 | path.append({'address': row['addresses'][0]}) 145 | if idx != len(self.raw_path) - 1: 146 | path.append(output) 147 | return path 148 | else: 149 | return None 150 | -------------------------------------------------------------------------------- /bitcoingraph/helper.py: -------------------------------------------------------------------------------- 1 | 2 | import datetime 3 | import json 4 | import os 5 | import subprocess 6 | import sys 7 | 8 | 9 | def to_time(numeric_string, as_date=False): 10 | """ 11 | Converts UTC timestamp to string-formatted data time. 12 | 13 | :param int numeric_string: UTC timestamp 14 | """ 15 | if as_date: 16 | date_format = '%Y-%m-%d' 17 | else: 18 | date_format = '%Y-%m-%d %H:%M:%S' 19 | time_as_date = datetime.datetime.utcfromtimestamp(int(numeric_string)) 20 | return time_as_date.strftime(date_format) 21 | 22 | 23 | def to_json(raw_data): 24 | """ 25 | Pretty-prints JSON data 26 | 27 | :param str raw_data: raw JSON data 28 | """ 29 | return json.dumps(raw_data, sort_keys=True, 30 | indent=4, separators=(',', ': ')) 31 | 32 | 33 | def sort(path, filename, args=''): 34 | if sys.platform == 'darwin': 35 | s = 'LC_ALL=C gsort -S 50% --parallel=4 {0} {1} -o {1}' 36 | else: 37 | s = 'LC_ALL=C sort -S 50% --parallel=4 {0} {1} -o {1}' 38 | status = subprocess.call(s.format(args, os.path.join(path, filename)), shell=True) 39 | if status != 0: 40 | raise Exception('unable to sort file: {}'.format(filename)) 41 | -------------------------------------------------------------------------------- /bitcoingraph/model.py: -------------------------------------------------------------------------------- 1 | 2 | from bitcoingraph.helper import to_time 3 | 4 | 5 | class Block: 6 | 7 | def __init__(self, blockchain, hash=None, height=None, json_data=None): 8 | self._blockchain = blockchain 9 | if json_data is None: 10 | self.__hash = hash 11 | self.__height = height 12 | self.__timestamp = None 13 | self.__has_previous_block = None 14 | self.__has_next_block = None 15 | self.__transactions = None 16 | else: 17 | self.__hash = json_data['hash'] 18 | self.__height = json_data['height'] 19 | self.__timestamp = json_data['time'] 20 | if 'previousblockhash' in json_data: 21 | self.__has_previous_block = True 22 | self.__previous_block = Block(blockchain, json_data['previousblockhash'], 23 | self.height - 1) 24 | else: 25 | self.__has_previous_block = False 26 | self.__previous_block = None 27 | if 'nextblockhash' in json_data: 28 | self.__has_next_block = True 29 | self.__next_block = Block(blockchain, json_data['nextblockhash'], self.height + 1) 30 | else: 31 | self.__has_next_block = False 32 | self.__next_block = None 33 | self.__transactions = [ 34 | Transaction(blockchain, self, tx) if isinstance(tx, str) 35 | else Transaction(blockchain, self, json_data=tx) 36 | for tx in json_data['tx']] 37 | 38 | @property 39 | def hash(self): 40 | if self.__hash is None: 41 | self._load() 42 | return self.__hash 43 | 44 | @property 45 | def height(self): 46 | if self.__height is None: 47 | self._load() 48 | return self.__height 49 | 50 | @property 51 | def timestamp(self): 52 | if self.__timestamp is None: 53 | self._load() 54 | return self.__timestamp 55 | 56 | def formatted_time(self): 57 | return to_time(self.timestamp) 58 | 59 | @property 60 | def previous_block(self): 61 | self.has_previous_block() 62 | return self.__previous_block 63 | 64 | def has_previous_block(self): 65 | if self.__has_previous_block is None: 66 | self._load() 67 | return self.__has_previous_block 68 | 69 | @property 70 | def next_block(self): 71 | self.has_next_block() 72 | return self.__next_block 73 | 74 | def has_next_block(self): 75 | if self.__has_next_block is None: 76 | self._load() 77 | return self.__has_next_block 78 | 79 | @property 80 | def transactions(self): 81 | if self.__transactions is None: 82 | self._load() 83 | return self.__transactions 84 | 85 | def _load(self): 86 | if self.__hash is None: 87 | block = self._blockchain.get_block_by_height(self.__height) 88 | else: 89 | block = self._blockchain.get_block_by_hash(self.__hash) 90 | self.__height = block.height 91 | self.__hash = block.hash 92 | self.__timestamp = block.timestamp 93 | self.__has_previous_block = block.has_previous_block() 94 | self.__previous_block = block.previous_block 95 | self.__has_next_block = block.has_next_block() 96 | self.__next_block = block.next_block 97 | self.__transactions = block.transactions 98 | 99 | 100 | class Transaction: 101 | 102 | def __init__(self, blockchain, block=None, txid=None, json_data=None): 103 | self._blockchain = blockchain 104 | self.block = block 105 | if json_data is None: 106 | self.txid = txid 107 | self.__inputs = None 108 | self.__outputs = None 109 | else: 110 | self.txid = json_data['txid'] 111 | if block is None: 112 | self.block = Block(blockchain, json_data['blockhash']) 113 | self.__inputs = [ 114 | Input(blockchain, is_coinbase=True) if 'coinbase' in vin 115 | else Input(blockchain, vin) 116 | for vin in json_data['vin']] 117 | self.__outputs = [Output(self, i, vout) for i, vout in enumerate(json_data['vout'])] 118 | 119 | @property 120 | def inputs(self): 121 | if self.__inputs is None: 122 | self._load() 123 | return self.__inputs 124 | 125 | @property 126 | def outputs(self): 127 | if self.__outputs is None: 128 | self._load() 129 | return self.__outputs 130 | 131 | def _load(self): 132 | transaction = self._blockchain.get_transaction(self.txid) 133 | self.__inputs = transaction.inputs 134 | self.__outputs = transaction.outputs 135 | 136 | def is_coinbase(self): 137 | return self.inputs[0].is_coinbase 138 | 139 | def input_sum(self): 140 | return sum([input.output.value for input in self.inputs]) 141 | 142 | def output_sum(self): 143 | return sum([output.value for output in self.outputs]) 144 | 145 | def aggregated_inputs(self): 146 | aggregated_inputs = {} 147 | for input in self.inputs: 148 | output = input.output 149 | if input.is_coinbase: 150 | aggregated_inputs['COINBASE'] = self.output_sum() 151 | elif output.addresses[0] in aggregated_inputs: 152 | aggregated_inputs[output.addresses[0]] += output.value 153 | else: 154 | aggregated_inputs[output.addresses[0]] = output.value 155 | return aggregated_inputs 156 | 157 | def aggregated_outputs(self): 158 | aggregated_outputs = {} 159 | for output in self.outputs: 160 | if output.addresses: 161 | if output.addresses[0] in aggregated_outputs: 162 | aggregated_outputs[output.addresses[0]] += output.value 163 | else: 164 | aggregated_outputs[output.addresses[0]] = output.value 165 | return aggregated_outputs 166 | 167 | @staticmethod 168 | def _reduced_values(values, other_values): 169 | reduced_values = {} 170 | for address, value in values.items(): 171 | if address in other_values: 172 | other_value = other_values[address] 173 | if value > other_value: 174 | reduced_values[address] = value - other_value 175 | else: 176 | reduced_values[address] = value 177 | return reduced_values 178 | 179 | def reduced_inputs(self): 180 | return self._reduced_values(self.aggregated_inputs(), self.aggregated_outputs()) 181 | 182 | def reduced_outputs(self): 183 | return self._reduced_values(self.aggregated_outputs(), self.aggregated_inputs()) 184 | 185 | 186 | class Input: 187 | 188 | def __init__(self, blockchain, output_reference=None, is_coinbase=False): 189 | self._blockchain = blockchain 190 | self.output_reference = output_reference 191 | self.is_coinbase = is_coinbase 192 | self.__output = None 193 | 194 | @property 195 | def output(self): 196 | if self.is_coinbase: 197 | return None 198 | if self.__output is None: 199 | self._load() 200 | return self.__output 201 | 202 | def _load(self): 203 | transaction = self._blockchain.get_transaction(self.output_reference['txid']) 204 | self.__output = transaction.outputs[self.output_reference['vout']] 205 | 206 | 207 | class Output: 208 | 209 | def __init__(self, transaction, index, json_data): 210 | self.transaction = transaction 211 | self.index = index 212 | self.value = json_data['value'] 213 | self.type = json_data['scriptPubKey']['type'] 214 | if 'addresses' in json_data['scriptPubKey']: 215 | self.addresses = json_data['scriptPubKey']['addresses'] 216 | else: 217 | self.addresses = [] 218 | -------------------------------------------------------------------------------- /bitcoingraph/neo4j.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import requests 4 | from datetime import date, datetime, timezone 5 | 6 | 7 | def lb_join(*lines): 8 | return '\n'.join(lines) 9 | 10 | 11 | class Neo4jException(Exception): 12 | 13 | def __init__(self, msg): 14 | self.msg = msg 15 | 16 | def __str__(self): 17 | return repr(self.msg) 18 | 19 | 20 | class Neo4jController: 21 | 22 | def __init__(self, host, port, user, password): 23 | self.host = host 24 | self.port = port 25 | self.user = user 26 | self.password = password 27 | self.url_base = 'http://{}:{}/db/data/'.format(host, port) 28 | self.url = self.url_base + 'transaction/commit' 29 | self.headers = { 30 | 'Accept': 'application/json; charset=UTF-8', 31 | 'Content-Type': 'application/json', 32 | 'max-execution-time': 30000 33 | } 34 | self._session = requests.Session() 35 | 36 | address_match = lb_join( 37 | 'MATCH (a:Address {address: {address}})<-[:USES]-(o),', 38 | ' (o)-[r:INPUT|OUTPUT]-(t)<-[:CONTAINS]-(b)', 39 | 'WITH a, t, b,', 40 | 'CASE type(r) WHEN "OUTPUT" THEN sum(o.value) ELSE -sum(o.value) END AS value') 41 | reduced_address_match = lb_join( 42 | address_match, 43 | 'WITH a, t, b, sum(value) AS value') 44 | address_period_match = lb_join( 45 | reduced_address_match, 46 | 'WHERE b.timestamp > {from} AND b.timestamp < {to}') 47 | address_statement = lb_join( 48 | address_period_match, 49 | 'RETURN t.txid as txid, value, b.timestamp as timestamp', 50 | 'ORDER BY b.timestamp desc') 51 | 52 | def address_stats_query(self, address): 53 | s = lb_join( 54 | self.reduced_address_match, 55 | 'RETURN count(*) as num_transactions, ' 56 | 'min(b.timestamp) as first, max(b.timestamp) as last') 57 | return self.query(s, {'address': address}) 58 | 59 | def get_received_bitcoins(self, address): 60 | s = lb_join( 61 | self.reduced_address_match, 62 | 'WHERE value > 0', 63 | 'RETURN sum(value)') 64 | return self.query(s, {'address': address}).single_result() 65 | 66 | def get_unspent_bitcoins(self, address): 67 | s = lb_join( 68 | 'MATCH (a:Address {address: {address}})<-[:USES]-(o)', 69 | 'WHERE NOT (o)-[:INPUT]->()', 70 | 'RETURN sum(o.value)') 71 | return self.query(s, {'address': address}).single_result() 72 | 73 | def address_count_query(self, address, date_from, date_to): 74 | s = lb_join( 75 | self.address_period_match, 76 | 'RETURN count(*)') 77 | return self.query(s, self.as_address_query_parameter(address, date_from, date_to)) 78 | 79 | def address_query(self, address, date_from, date_to): 80 | return self.query(self.address_statement, 81 | self.as_address_query_parameter(address, date_from, date_to)) 82 | 83 | def paginated_address_query(self, address, date_from, date_to, skip, limit): 84 | s = lb_join( 85 | self.address_statement, 86 | 'SKIP {skip} LIMIT {limit}') 87 | p = self.as_address_query_parameter(address, date_from, date_to) 88 | p['skip'] = skip 89 | p['limit'] = limit 90 | return self.query(s, p) 91 | 92 | def incoming_addresses(self, address, date_from, date_to): 93 | return self._related_addresses(address, date_from, date_to, '<-[:OUTPUT]-(t)<-[:INPUT]-') 94 | 95 | def outgoing_addresses(self, address, date_from, date_to): 96 | return self._related_addresses(address, date_from, date_to, '-[:INPUT]->(t)-[:OUTPUT]->') 97 | 98 | def _related_addresses(self, address, date_from, date_to, output_relation): 99 | s = lb_join( 100 | 'MATCH (a:Address {address: {address}})<-[:USES]-(o),', 101 | ' (o)' + output_relation + '(o2)-[:USES]->(a2),', 102 | ' (t)<-[:CONTAINS]-(b)', 103 | 'WITH DISTINCT a, a2, t, b', 104 | 'WHERE a2 <> a', 105 | 'AND b.timestamp > {from} AND b.timestamp < {to}', 106 | 'RETURN a2.address as address, count(t) as transactions', 107 | 'ORDER BY transactions desc') 108 | return self.query(s, self.as_address_query_parameter(address, date_from, date_to)).get() 109 | 110 | def transaction_relations(self, address, address2, date_from, date_to): 111 | s = lb_join( 112 | 'MATCH (a:Address {address: {address}})<-[:USES]-(o),', 113 | ' (o)-[:INPUT]->(t)-[:OUTPUT]->(o2),', 114 | ' (o2)-[:USES]->(a2:Address {address: {address2}}),', 115 | ' (t)<-[:CONTAINS]-(b)', 116 | 'WHERE b.timestamp > {from} AND b.timestamp < {to}', 117 | 'WITH a, a2, t, b, collect(DISTINCT o) as ins, collect(DISTINCT o2) as outs', 118 | 'RETURN t.txid as txid, reduce(sum=0, o in ins | sum+o.value) as in,', 119 | ' reduce(sum=0, o in outs | sum+o.value) as out, b.timestamp as timestamp', 120 | 'ORDER BY b.timestamp desc') 121 | p = self.as_address_query_parameter(address, date_from, date_to) 122 | p['address2'] = address2 123 | return self.query(s, p).get() 124 | 125 | def entity_query(self, address): 126 | s = lb_join( 127 | 'MATCH (a:Address {address: {address}})-[:BELONGS_TO]->(e)', 128 | 'RETURN {id: id(e)}') 129 | return self.query(s, {'address': address}) 130 | 131 | def get_number_of_addresses_for_entity(self, id): 132 | s = lb_join( 133 | 'MATCH (e:Entity)', 134 | 'WHERE id(e) = {id}', 135 | 'RETURN size((e)<-[:BELONGS_TO]-())') 136 | return self.query(s, {'id': id}).single_result() 137 | 138 | def entity_address_query(self, id, limit): 139 | s = lb_join( 140 | 'MATCH (e:Entity)<-[:BELONGS_TO]-(a)', 141 | 'WHERE id(e) = {id}', 142 | 'OPTIONAL MATCH (a)-[:HAS]->(i)', 143 | 'WITH e, a, collect(i) as is', 144 | 'ORDER BY length(is) desc', 145 | 'LIMIT {limit}', 146 | 'RETURN a.address as address, is as identities') 147 | return self.query(s, {'id': id, 'limit': limit}) 148 | 149 | def identity_query(self, address): 150 | s = lb_join( 151 | 'MATCH (a:Address {address: {address}})-[:HAS]->(i)', 152 | 'RETURN collect({id: id(i), name: i.name, link: i.link, source: i.source})') 153 | return self.query(s, {'address': address}) 154 | 155 | def reverse_identity_query(self, name): 156 | s = lb_join( 157 | 'MATCH (i:Identity {name: {name}})<-[:HAS]-(a)', 158 | 'RETURN a.address') 159 | return self.query(s, {'name': name}) 160 | 161 | def identity_add_query(self, address, name, link, source): 162 | s = lb_join( 163 | 'MATCH (a:Address {address: {address}})', 164 | 'CREATE (a)-[:HAS]->(i:Identity {name: {name}, link: {link}, source: {source}})') 165 | return self.query(s, {'address': address, 'name': name, 'link': link, 'source': source}) 166 | 167 | def identity_delete_query(self, id): 168 | s = lb_join( 169 | 'MATCH (i:Identity)', 170 | 'WHERE id(i) = {id}', 171 | 'DETACH DELETE i') 172 | return self.query(s, {'id': id}) 173 | 174 | def path_query_old(self, address1, address2): 175 | s = lb_join( 176 | 'MATCH (start:Address {address: {address1}})<-[:USES]-(o1:Output)', 177 | ' -[:INPUT|OUTPUT*]->(o2:Output)-[:USES]->(end:Address {address: {address2}}),', 178 | ' p = shortestpath((o1)-[:INPUT|OUTPUT*]->(o2))', 179 | 'WITH p', 180 | 'LIMIT 1', 181 | 'UNWIND nodes(p) as n', 182 | 'OPTIONAL MATCH (n)-[:USES]->(a)', 183 | 'RETURN n as node, a as address') 184 | return self.query(s, {'address1': address1, 'address2': address2}) 185 | 186 | def path_query(self, address1, address2): 187 | source = self.get_id_of_address_node(address1) 188 | if source is None: 189 | raise Neo4jException("address {} doesn't exist".format(address1)) 190 | target = self.get_id_of_address_node(address2) 191 | if target is None: 192 | raise Neo4jException("address {} doesn't exist".format(address2)) 193 | url = self.url_base + 'ext/Entity/node/{}/findPathWithBidirectionalStrategy'.format(source) 194 | payload = {'target': self.url_base + 'node/{}'.format(target)} 195 | r = self._session.post(url, auth=(self.user, self.password), json=payload, 196 | headers=self.headers) 197 | result_obj = r.json() 198 | result = json.loads(result_obj) if type(result_obj) is str else result_obj 199 | if 'errors' in result: 200 | raise Neo4jException(result['errors'][0]['message']) 201 | elif 'path' in result: 202 | return result['path'] 203 | else: 204 | return None 205 | 206 | def get_id_of_address_node(self, address): 207 | s = lb_join( 208 | 'MATCH (a:Address {address: {address}})', 209 | 'RETURN id(a)') 210 | return self.query(s, {'address': address}).single_result() 211 | 212 | def get_max_block_height(self): 213 | s = lb_join( 214 | 'MATCH (b:Block)', 215 | 'RETURN max(b.height)') 216 | return self.query(s).single_result() 217 | 218 | def add_block(self, block): 219 | s = lb_join( 220 | 'CREATE (b:Block {hash: {hash}, height: {height}, timestamp: {timestamp}})', 221 | 'RETURN id(b)') 222 | p = {'hash': block.hash, 'height': block.height, 'timestamp': block.timestamp} 223 | return self.query(s, p).single_result() 224 | 225 | def add_transaction(self, block_node_id, tx): 226 | s = lb_join( 227 | 'MATCH (b) WHERE id(b) = {id}', 228 | 'CREATE (b)-[:CONTAINS]->(t:Transaction {txid: {txid}, coinbase: {coinbase}})', 229 | 'RETURN id(t)') 230 | p = {'id': block_node_id, 'txid': tx.txid, 'coinbase': tx.is_coinbase()} 231 | return self.query(s, p).single_result() 232 | 233 | def add_input(self, tx_node_id, output_reference): 234 | s = lb_join( 235 | 'MATCH (o:Output {txid_n: {txid_n}}), (t)', 236 | 'WHERE id(t) = {id}', 237 | 'CREATE (o)-[:INPUT]->(t)') 238 | p = {'txid_n': '{}_{}'.format(output_reference['txid'], output_reference['vout']), 239 | 'id': tx_node_id} 240 | return self.query(s, p).single_result() 241 | 242 | def add_output(self, tx_node_id, output): 243 | s = lb_join( 244 | 'MATCH (t) WHERE id(t) = {id}', 245 | 'CREATE (t)-[:OUTPUT]->' 246 | '(o:Output {txid_n: {txid_n}, n: {n}, value: {value}, type: {type}})', 247 | 'RETURN id(o)') 248 | p = {'id': tx_node_id, 'txid_n': '{}_{}'.format(output.transaction.txid, output.index), 249 | 'n': output.index, 'value': output.value, 'type': output.type} 250 | return self.query(s, p).single_result() 251 | 252 | def add_address(self, output_node_id, address): 253 | s = lb_join( 254 | 'MATCH (o) WHERE id(o) = {id}', 255 | 'MERGE (a:Address {address: {address}})', 256 | 'CREATE (o)-[:USES]->(a)', 257 | 'RETURN id(a)') 258 | return self.query(s, {'id': output_node_id, 'address': address}).single_result() 259 | 260 | def create_entity(self, transaction_node_id): 261 | url = self.url_base + 'ext/Entity/node/{}/createEntity'.format(transaction_node_id) 262 | self._session.post(url, auth=(self.user, self.password)) 263 | 264 | def create_entities(self, block_node_id): 265 | url = self.url_base + 'ext/Entity/node/{}/createEntities'.format(block_node_id) 266 | self._session.post(url, auth=(self.user, self.password)) 267 | 268 | def query(self, statement, parameters=None): 269 | #print(statement, '||', parameters) 270 | statement_json = {'statement': statement} 271 | if parameters is not None: 272 | statement_json['parameters'] = parameters 273 | payload = {'statements': [statement_json]} 274 | r = self._session.post(self.url, auth=(self.user, self.password), 275 | headers=self.headers, json=payload) 276 | result = r.json() 277 | if result['errors']: 278 | raise Neo4jException(result['errors'][0]['message']) 279 | #print(result) 280 | return QueryResult(result) 281 | 282 | @staticmethod 283 | def as_address_query_parameter(address, date_from=None, date_to=None): 284 | if date_from is None: 285 | timestamp_from = 0 286 | else: 287 | timestamp_from = datetime.strptime(date_from, '%Y-%m-%d').replace( 288 | tzinfo=timezone.utc).timestamp() 289 | if date_to is None: 290 | timestamp_to = 2 ** 31 - 1 291 | else: 292 | d = datetime.strptime(date_to, '%Y-%m-%d').replace(tzinfo=timezone.utc) 293 | d += date.resolution 294 | timestamp_to = d.timestamp() 295 | return {'address': address, 'from': timestamp_from, 'to': timestamp_to} 296 | 297 | def transaction(self): 298 | return DBTransaction(self.host, self.port, self.user, self.password) 299 | 300 | 301 | class DBTransaction(Neo4jController): 302 | 303 | def __enter__(self): 304 | transaction_begin_url = self.url_base + 'transaction' 305 | r = self._session.post(transaction_begin_url, auth=(self.user, self.password), 306 | headers=self.headers) 307 | self.url = r.headers['Location'] 308 | return self 309 | 310 | def __exit__(self, type, value, traceback): 311 | transaction_commit_url = self.url + '/commit' 312 | r = self._session.post(transaction_commit_url, auth=(self.user, self.password), 313 | headers=self.headers) 314 | self._session.close() 315 | 316 | 317 | class QueryResult: 318 | 319 | def __init__(self, raw_data): 320 | self._raw_data = raw_data 321 | 322 | def data(self): 323 | if self._raw_data['results']: 324 | return self._raw_data['results'][0]['data'] 325 | else: 326 | return [] 327 | 328 | def columns(self): 329 | return self._raw_data['results'][0]['columns'] 330 | 331 | def get(self): 332 | return [dict(zip(self.columns(), r['row'])) for r in self.data()] 333 | 334 | def list(self): 335 | return [r['row'][0] for r in self.data()] 336 | 337 | def single_result(self): 338 | if self.data(): 339 | return self.data()[0]['row'][0] 340 | else: 341 | return None 342 | 343 | def single_row(self): 344 | rows = self.get() 345 | if self.get(): 346 | return list(self.get())[0] 347 | else: 348 | return None 349 | -------------------------------------------------------------------------------- /bitcoingraph/writer.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | 4 | 5 | class CSVDumpWriter: 6 | 7 | def __init__(self, output_path, plain_header=False, separate_header=True): 8 | self._output_path = output_path 9 | self._plain_header = plain_header 10 | self._separate_header = separate_header 11 | 12 | if not os.path.exists(output_path): 13 | os.makedirs(output_path) 14 | 15 | self._write_header('blocks', ['hash:ID(Block)', 'height:int', 'timestamp:int']) 16 | self._write_header('transactions', ['txid:ID(Transaction)', 'coinbase:boolean']) 17 | self._write_header('outputs', ['txid_n:ID(Output)', 'n:int', 'value:double', 'type']) 18 | self._write_header('addresses', ['address:ID(Address)']) 19 | self._write_header('rel_block_tx', ['hash:START_ID(Block)', 'txid:END_ID(Transaction)']) 20 | self._write_header('rel_tx_output', 21 | ['txid:START_ID(Transaction)', 'txid_n:END_ID(Output)']) 22 | self._write_header('rel_input', ['txid:END_ID(Transaction)', 'txid_n:START_ID(Output)']) 23 | self._write_header('rel_output_address', 24 | ['txid_n:START_ID(Output)', 'address:END_ID(Address)']) 25 | 26 | def __enter__(self): 27 | self._blocks_file = open(self._get_path('blocks'), 'a') 28 | self._transactions_file = open(self._get_path('transactions'), 'a') 29 | self._outputs_file = open(self._get_path('outputs'), 'a') 30 | self._addresses_file = open(self._get_path('addresses'), 'a') 31 | self._rel_block_tx_file = open(self._get_path('rel_block_tx'), 'a') 32 | self._rel_tx_output_file = open(self._get_path('rel_tx_output'), 'a') 33 | self._rel_input_file = open(self._get_path('rel_input'), 'a') 34 | self._rel_output_address_file = open(self._get_path('rel_output_address'), 'a') 35 | 36 | self._block_writer = csv.writer(self._blocks_file) 37 | self._transaction_writer = csv.writer(self._transactions_file) 38 | self._output_writer = csv.writer(self._outputs_file) 39 | self._address_writer = csv.writer(self._addresses_file) 40 | self._rel_block_tx_writer = csv.writer(self._rel_block_tx_file) 41 | self._rel_tx_output_writer = csv.writer(self._rel_tx_output_file) 42 | self._rel_input_writer = csv.writer(self._rel_input_file) 43 | self._rel_output_address_writer = csv.writer(self._rel_output_address_file) 44 | return self 45 | 46 | def __exit__(self, type, value, traceback): 47 | self._blocks_file.close() 48 | self._transactions_file.close() 49 | self._outputs_file.close() 50 | self._addresses_file.close() 51 | self._rel_block_tx_file.close() 52 | self._rel_tx_output_file.close() 53 | self._rel_input_file.close() 54 | self._rel_output_address_file.close() 55 | 56 | def _write_header(self, filename, row): 57 | if self._separate_header: 58 | filename += '_header' 59 | with open(self._get_path(filename), 'w') as f: 60 | writer = csv.writer(f) 61 | if self._plain_header: 62 | header = [entry.partition(':')[0] for entry in row] 63 | else: 64 | header = row 65 | writer.writerow(header) 66 | 67 | def _get_path(self, filename): 68 | return os.path.join(self._output_path, filename + '.csv') 69 | 70 | def write(self, block): 71 | def a_b(a, b): 72 | return '{}_{}'.format(a, b) 73 | 74 | self._block_writer.writerow([block.hash, block.height, block.timestamp]) 75 | for tx in block.transactions: 76 | self._transaction_writer.writerow([tx.txid, tx.is_coinbase()]) 77 | self._rel_block_tx_writer.writerow([block.hash, tx.txid]) 78 | if not tx.is_coinbase(): 79 | for input in tx.inputs: 80 | self._rel_input_writer.writerow( 81 | [tx.txid, 82 | a_b(input.output_reference['txid'], input.output_reference['vout'])]) 83 | for output in tx.outputs: 84 | self._output_writer.writerow([a_b(tx.txid, output.index), output.index, 85 | output.value, output.type]) 86 | self._rel_tx_output_writer.writerow([tx.txid, a_b(tx.txid, output.index)]) 87 | for address in output.addresses: 88 | self._address_writer.writerow([address]) 89 | self._rel_output_address_writer.writerow([a_b(tx.txid, output.index), address]) 90 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/bitcoingraph.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/bitcoingraph.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/bitcoingraph" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/bitcoingraph" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/bitcoingraph.rst: -------------------------------------------------------------------------------- 1 | bitcoingraph package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | bitcoingraph.bitcoind module 8 | ---------------------------- 9 | 10 | .. automodule:: bitcoingraph.bitcoind 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | bitcoingraph.bitcoingraph module 16 | -------------------------------- 17 | 18 | .. automodule:: bitcoingraph.bitcoingraph 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | bitcoingraph.blockchain module 24 | ------------------------------ 25 | 26 | .. automodule:: bitcoingraph.blockchain 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | bitcoingraph.entities module 32 | ---------------------------- 33 | 34 | .. automodule:: bitcoingraph.entities 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | bitcoingraph.graphdb module 40 | --------------------------- 41 | 42 | .. automodule:: bitcoingraph.graphdb 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | bitcoingraph.helper module 48 | -------------------------- 49 | 50 | .. automodule:: bitcoingraph.helper 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | bitcoingraph.model module 56 | ------------------------- 57 | 58 | .. automodule:: bitcoingraph.model 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | bitcoingraph.neo4j module 64 | ------------------------- 65 | 66 | .. automodule:: bitcoingraph.neo4j 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | bitcoingraph.writer module 72 | -------------------------- 73 | 74 | .. automodule:: bitcoingraph.writer 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | 80 | Module contents 81 | --------------- 82 | 83 | .. automodule:: bitcoingraph 84 | :members: 85 | :undoc-members: 86 | :show-inheritance: 87 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # bitcoingraph documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Jan 26 10:58:27 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.viewcode', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | # templates_path = ['_templates'] 39 | 40 | # The suffix of source filenames. 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | #source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = 'bitcoingraph' 51 | copyright = '2015, Bernhard Haslhofer' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | # version = '' 59 | # The full version, including alpha/beta/rc tags. 60 | # release = '' 61 | 62 | try: 63 | import bitcoingraph as bg 64 | # The short X.Y version. 65 | version = bg.__version__ 66 | # The full version, including alpha/beta/rc tags. 67 | release = bg.__version__ 68 | except ImportError: 69 | version = 'latest' 70 | release = 'latest' 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | #language = None 75 | 76 | # There are two options for replacing |today|: either, you set today to some 77 | # non-false value, then it is used: 78 | #today = '' 79 | # Else, today_fmt is used as the format for a strftime call. 80 | #today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | exclude_patterns = ['_build'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | #default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | #add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | #add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | #show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = 'sphinx' 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | #modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | #keep_warnings = False 109 | 110 | 111 | # -- Options for HTML output ---------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. See the documentation for 114 | # a list of builtin themes. 115 | html_theme = 'default' 116 | 117 | # Theme options are theme-specific and customize the look and feel of a theme 118 | # further. For a list of options available for each theme, see the 119 | # documentation. 120 | #html_theme_options = {} 121 | 122 | # Add any paths that contain custom themes here, relative to this directory. 123 | #html_theme_path = [] 124 | 125 | # The name for this set of Sphinx documents. If None, it defaults to 126 | # " v documentation". 127 | #html_title = None 128 | 129 | # A shorter title for the navigation bar. Default is the same as html_title. 130 | #html_short_title = None 131 | 132 | # The name of an image file (relative to this directory) to place at the top 133 | # of the sidebar. 134 | #html_logo = None 135 | 136 | # The name of an image file (within the static path) to use as favicon of the 137 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 138 | # pixels large. 139 | #html_favicon = None 140 | 141 | # Add any paths that contain custom static files (such as style sheets) here, 142 | # relative to this directory. They are copied after the builtin static files, 143 | # so a file named "default.css" will overwrite the builtin "default.css". 144 | # html_static_path = ['_static'] 145 | 146 | # Add any extra paths that contain custom files (such as robots.txt or 147 | # .htaccess) here, relative to this directory. These files are copied 148 | # directly to the root of the documentation. 149 | #html_extra_path = [] 150 | 151 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 152 | # using the given strftime format. 153 | #html_last_updated_fmt = '%b %d, %Y' 154 | 155 | # If true, SmartyPants will be used to convert quotes and dashes to 156 | # typographically correct entities. 157 | #html_use_smartypants = True 158 | 159 | # Custom sidebar templates, maps document names to template names. 160 | #html_sidebars = {} 161 | 162 | # Additional templates that should be rendered to pages, maps page names to 163 | # template names. 164 | #html_additional_pages = {} 165 | 166 | # If false, no module index is generated. 167 | #html_domain_indices = True 168 | 169 | # If false, no index is generated. 170 | #html_use_index = True 171 | 172 | # If true, the index is split into individual pages for each letter. 173 | #html_split_index = False 174 | 175 | # If true, links to the reST sources are added to the pages. 176 | #html_show_sourcelink = True 177 | 178 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 179 | #html_show_sphinx = True 180 | 181 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 182 | #html_show_copyright = True 183 | 184 | # If true, an OpenSearch description file will be output, and all pages will 185 | # contain a tag referring to it. The value of this option must be the 186 | # base URL from which the finished HTML is served. 187 | #html_use_opensearch = '' 188 | 189 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 190 | #html_file_suffix = None 191 | 192 | # Output file base name for HTML help builder. 193 | htmlhelp_basename = 'bitcoingraphdoc' 194 | 195 | 196 | # -- Options for LaTeX output --------------------------------------------- 197 | 198 | latex_elements = { 199 | # The paper size ('letterpaper' or 'a4paper'). 200 | #'papersize': 'letterpaper', 201 | 202 | # The font size ('10pt', '11pt' or '12pt'). 203 | #'pointsize': '10pt', 204 | 205 | # Additional stuff for the LaTeX preamble. 206 | #'preamble': '', 207 | } 208 | 209 | # Grouping the document tree into LaTeX files. List of tuples 210 | # (source start file, target name, title, 211 | # author, documentclass [howto, manual, or own class]). 212 | latex_documents = [ 213 | ('index', 'bitcoingraph.tex', 'bitcoingraph Documentation', 214 | 'Author', 'manual'), 215 | ] 216 | 217 | # The name of an image file (relative to this directory) to place at the top of 218 | # the title page. 219 | #latex_logo = None 220 | 221 | # For "manual" documents, if this is true, then toplevel headings are parts, 222 | # not chapters. 223 | #latex_use_parts = False 224 | 225 | # If true, show page references after internal links. 226 | #latex_show_pagerefs = False 227 | 228 | # If true, show URL addresses after external links. 229 | #latex_show_urls = False 230 | 231 | # Documents to append as an appendix to all manuals. 232 | #latex_appendices = [] 233 | 234 | # If false, no module index is generated. 235 | #latex_domain_indices = True 236 | 237 | 238 | # -- Options for manual page output --------------------------------------- 239 | 240 | # One entry per manual page. List of tuples 241 | # (source start file, name, description, authors, manual section). 242 | man_pages = [ 243 | ('index', 'bitcoingraph', 'bitcoingraph Documentation', 244 | ['Author'], 1) 245 | ] 246 | 247 | # If true, show URL addresses after external links. 248 | #man_show_urls = False 249 | 250 | 251 | # -- Options for Texinfo output ------------------------------------------- 252 | 253 | # Grouping the document tree into Texinfo files. List of tuples 254 | # (source start file, target name, title, author, 255 | # dir menu entry, description, category) 256 | texinfo_documents = [ 257 | ('index', 'bitcoingraph', 'bitcoingraph Documentation', 258 | 'Author', 'bitcoingraph', 'One line description of project.', 259 | 'Miscellaneous'), 260 | ] 261 | 262 | # Documents to append as an appendix to all manuals. 263 | #texinfo_appendices = [] 264 | 265 | # If false, no module index is generated. 266 | #texinfo_domain_indices = True 267 | 268 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 269 | #texinfo_show_urls = 'footnote' 270 | 271 | # If true, do not generate a @detailmenu in the "Top" node's menu. 272 | #texinfo_no_detailmenu = False 273 | 274 | 275 | # -- Options for Epub output ---------------------------------------------- 276 | 277 | # Bibliographic Dublin Core info. 278 | epub_title = 'bitcoingraph' 279 | epub_author = 'Author' 280 | epub_publisher = 'Author' 281 | epub_copyright = '2015, Author' 282 | 283 | # The basename for the epub file. It defaults to the project name. 284 | #epub_basename = 'bitcoingraph' 285 | 286 | # The HTML theme for the epub output. Since the default themes are not optimized 287 | # for small screen space, using the same theme for HTML and epub output is 288 | # usually not wise. This defaults to 'epub', a theme designed to save visual 289 | # space. 290 | #epub_theme = 'epub' 291 | 292 | # The language of the text. It defaults to the language option 293 | # or en if the language is not set. 294 | #epub_language = '' 295 | 296 | # The scheme of the identifier. Typical schemes are ISBN or URL. 297 | #epub_scheme = '' 298 | 299 | # The unique identifier of the text. This can be a ISBN number 300 | # or the project homepage. 301 | #epub_identifier = '' 302 | 303 | # A unique identification for the text. 304 | #epub_uid = '' 305 | 306 | # A tuple containing the cover image and cover page html template filenames. 307 | #epub_cover = () 308 | 309 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 310 | #epub_guide = () 311 | 312 | # HTML files that should be inserted before the pages created by sphinx. 313 | # The format is a list of tuples containing the path and title. 314 | #epub_pre_files = [] 315 | 316 | # HTML files shat should be inserted after the pages created by sphinx. 317 | # The format is a list of tuples containing the path and title. 318 | #epub_post_files = [] 319 | 320 | # A list of files that should not be packed into the epub file. 321 | epub_exclude_files = ['search.html'] 322 | 323 | # The depth of the table of contents in toc.ncx. 324 | #epub_tocdepth = 3 325 | 326 | # Allow duplicate toc entries. 327 | #epub_tocdup = True 328 | 329 | # Choose between 'default' and 'includehidden'. 330 | #epub_tocscope = 'default' 331 | 332 | # Fix unsupported image types using the PIL. 333 | #epub_fix_images = False 334 | 335 | # Scale large images. 336 | #epub_max_image_width = 0 337 | 338 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 339 | #epub_show_urls = 'inline' 340 | 341 | # If false, no index is generated. 342 | #epub_use_index = True 343 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. bitcoingraph documentation master file, created by 2 | sphinx-quickstart on Mon Jan 26 10:58:27 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to bitcoingraph's documentation! 7 | ======================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 4 13 | 14 | bitcoingraph 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | bitcoingraph 2 | ============ 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | bitcoingraph 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.5.0 2 | beautifulsoup4 3 | pytest 4 | -------------------------------------------------------------------------------- /scripts/bcgraph-compute-entities: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | from bitcoingraph import bitcoingraph 5 | 6 | parser = argparse.ArgumentParser( 7 | description='Compute entities from exported block chain data') 8 | parser.add_argument('-i', '--input_path', required=True, 9 | help='Input path') 10 | parser.add_argument('--sort-input', action='store_true', 11 | help='Sort all input files. This is necessary if ' 12 | 'the transaction deduplication was skipped on export.') 13 | 14 | args = parser.parse_args() 15 | bitcoingraph.compute_entities(args.input_path, args.sort_input) 16 | -------------------------------------------------------------------------------- /scripts/bcgraph-export: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import sys 5 | from bitcoingraph import BitcoinGraph 6 | 7 | 8 | def progress(p=0): 9 | p = int(p * 100) 10 | sys.stdout.write('\rProgress: {}%'.format(p)) 11 | sys.stdout.flush() 12 | 13 | parser = argparse.ArgumentParser( 14 | description='Export transactions from blockchain') 15 | parser.add_argument('startheight', type=int, 16 | help='Start block height') 17 | parser.add_argument('endheight', type=int, 18 | help='End block height') 19 | parser.add_argument('-o', '--output_path', type=str, 20 | help='Output path') 21 | parser.add_argument('--plain-header', action='store_true', 22 | help='Create header without Neo4J field types') 23 | parser.add_argument('--no-separate-header', action='store_true', 24 | help='Write header and data into one CSV file') 25 | parser.add_argument('--no-transaction-deduplication', action='store_true', 26 | help='Skip deduplication of transactions') 27 | parser.add_argument("-u", "--user", required=True, 28 | help="Bitcoin Core RPC username") 29 | parser.add_argument("-p", "--password", required=True, 30 | help="Bitcoin Core RPC password") 31 | 32 | 33 | if len(sys.argv) <= 1: 34 | parser.print_help() 35 | sys.exit(1) 36 | 37 | args = parser.parse_args() 38 | bcgraph = BitcoinGraph( 39 | blockchain={'host': 'localhost', 'port': 8332, 40 | 'rpc_user': args.user, 'rpc_pass': args.password, 41 | 'method': 'REST'}) 42 | bcgraph.export( 43 | args.startheight, 44 | args.endheight, 45 | args.output_path, 46 | args.plain_header, 47 | not args.no_separate_header, 48 | progress, 49 | not args.no_transaction_deduplication) 50 | -------------------------------------------------------------------------------- /scripts/bcgraph-synchronize: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | from bitcoingraph import BitcoinGraph 5 | 6 | parser = argparse.ArgumentParser( 7 | description='Synchronise database with blockchain') 8 | parser.add_argument('-s', '--bc-host', required=True, 9 | help='Bitcoin Core host') 10 | parser.add_argument('--bc-port', default='8332', 11 | help='Bitcoin Core port') 12 | parser.add_argument('-u', '--bc-user', required=True, 13 | help='Bitcoin Core RPC username') 14 | parser.add_argument('-p', '--bc-password', required=True, 15 | help='Bitcoin Core RPC password') 16 | parser.add_argument('--rest', action='store_true', 17 | help='Prefer REST API over RPC. This is only possible on localhost.') 18 | parser.add_argument('-S', '--neo4j-host', required=True, 19 | help='Neo4j host') 20 | parser.add_argument('--neo4j-port', default='7474', 21 | help='Neo4j port') 22 | parser.add_argument('-U', '--neo4j-user', required=True, 23 | help='Neo4j username') 24 | parser.add_argument('-P', '--neo4j-password', required=True, 25 | help='Neo4j password') 26 | parser.add_argument('-b', '--max-blocks', type=int, 27 | help='Enforce a limit on the number of blocks that are synchronised') 28 | 29 | 30 | args = parser.parse_args() 31 | blockchain = {'host': args.bc_host, 'port': args.bc_port, 32 | 'rpc_user': args.bc_user, 'rpc_pass': args.bc_password} 33 | if args.rest: 34 | blockchain['method'] = 'REST' 35 | neo4j = {'host': args.neo4j_host, 'port': args.neo4j_port, 36 | 'user': args.neo4j_user, 'pass': args.neo4j_password} 37 | bcgraph = BitcoinGraph(blockchain=blockchain, neo4j=neo4j) 38 | bcgraph.synchronize(args.max_blocks) 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from setuptools import setup 3 | from setuptools.command.test import test as TestCommand 4 | import io 5 | import sys 6 | 7 | import bitcoingraph 8 | 9 | 10 | def read(*filenames, **kwargs): 11 | encoding = kwargs.get('encoding', 'utf-8') 12 | sep = kwargs.get('sep', '\n') 13 | buf = [] 14 | for filename in filenames: 15 | with io.open(filename, encoding=encoding) as f: 16 | buf.append(f.read()) 17 | return sep.join(buf) 18 | 19 | 20 | long_description = read('README.rst') 21 | 22 | 23 | class PyTest(TestCommand): 24 | def finalize_options(self): 25 | TestCommand.finalize_options(self) 26 | self.test_args = [] 27 | self.test_suite = True 28 | 29 | def run_tests(self): 30 | import pytest 31 | errcode = pytest.main(self.test_args) 32 | sys.exit(errcode) 33 | 34 | 35 | setup( 36 | # Basic info 37 | name='bitcoingraph', 38 | version=bitcoingraph.__version__, 39 | url='https://github.com/behas/bitcoingraph.git', 40 | 41 | # Author 42 | author='Bernhard Haslhofer', 43 | author_email='bernhard.haslhofer@ait.ac.at', 44 | 45 | # Description 46 | description="""A Python library for extracting and navigating 47 | graphstructures from the Bitcoin block chain.""", 48 | long_description=long_description, 49 | 50 | # Package information 51 | packages=['bitcoingraph'], 52 | scripts=['scripts/bcgraph-export', 53 | 'scripts/bcgraph-compute-entities', 54 | 'scripts/bcgraph-synchronize'], 55 | platforms='any', 56 | install_requires=['requests>=2.5.0'], 57 | 58 | classifiers=[ 59 | 'Development Status :: 4 - Beta', 60 | 'Environment :: Console', 61 | 'Intended Audience :: Developers', 62 | 'License :: OSI Approved :: MIT License', 63 | 'Operating System :: OS Independent', 64 | 'Programming Language :: Python :: 3', 65 | 'Topic :: Software Development :: Libraries', 66 | ], 67 | 68 | # Testing 69 | test_suite='tests', 70 | tests_require=['pytest'], 71 | cmdclass={'test': PyTest}, 72 | extras_require={ 73 | 'testing': ['pytest'], 74 | }, 75 | 76 | # Legal info 77 | license='MIT License', 78 | ) 79 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behas/bitcoingraph/9039dcc255165f2a5d8a2b016b85839989b8e2c0/tests/__init__.py -------------------------------------------------------------------------------- /tests/data/block_100000.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506", 3 | "confirmations": 233539, 4 | "size": 957, 5 | "height": 100000, 6 | "version": 1, 7 | "merkleroot": "f3e94742aca4b5ef85488dc37c06c3282295ffec960994b2c0d5ac2a25a95766", 8 | "tx": [ 9 | "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87", 10 | "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4", 11 | "6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4", 12 | "e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d" 13 | ], 14 | "time": 1293623863, 15 | "nonce": 274148111, 16 | "bits": "1b04864c", 17 | "difficulty": 14484.16236123, 18 | "chainwork": "0000000000000000000000000000000000000000000000000644cb7f5234089e", 19 | "previousblockhash": "000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250", 20 | "nextblockhash": "00000000000080b66c911bd5ba14a74260057311eaeb1982802f7010f1a9f090" 21 | } 22 | -------------------------------------------------------------------------------- /tests/data/block_100001.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "00000000000080b66c911bd5ba14a74260057311eaeb1982802f7010f1a9f090", 3 | "confirmations": 233538, 4 | "size": 3308, 5 | "height": 100001, 6 | "version": 1, 7 | "merkleroot": "7fe79307aeb300d910d9c4bec5bacb4c7e114c7dfd6789e19f3a733debb3bb6a", 8 | "tx": [ 9 | "bb28a1a5b3a02e7657a81c38355d56c6f05e80b9219432e3352ddcfc3cb6304c", 10 | "fbde5d03b027d2b9ba4cf5d4fecab9a99864df2637b25ea4cbcb1796ff6550ca", 11 | "8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb", 12 | "d6c7cb254aa7a5fd446e8b48c307890a2d4e426da8ad2e1191cc1d8bbe0677d7", 13 | "ce29e5407f5e4c9ad581c337a639f3041b24220d5aa60370d96a39335538810b", 14 | "45a38677e1be28bd38b51bc1a1c0280055375cdf54472e04c590a989ead82515", 15 | "c5abc61566dbb1c4bce5e1fda7b66bed22eb2130cea4b721690bc1488465abc9", 16 | "a71f74ab78b564004fffedb2357fb4059ddfc629cb29ceeb449fafbf272104ca", 17 | "fda204502a3345e08afd6af27377c052e77f1fefeaeb31bdd45f1e1237ca5470", 18 | "d3cd1ee6655097146bdae1c177eb251de92aed9045a0959edc6b91d7d8c1f158", 19 | "cb00f8a0573b18faa8c4f467b049f5d202bf1101d9ef2633bc611be70376a4b4", 20 | "05d07bb2de2bda1115409f99bf6b626d23ecb6bed810d8be263352988e4548cb" 21 | ], 22 | "time": 1293624404, 23 | "nonce": 2613872960, 24 | "bits": "1b04864c", 25 | "difficulty": 14484.16236123, 26 | "chainwork": "00000000000000000000000000000000000000000000000006450413b458ec1c", 27 | "previousblockhash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506" 28 | } 29 | -------------------------------------------------------------------------------- /tests/data/block_99999.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250", 3 | "confirmations": 233540, 4 | "size": 215, 5 | "height": 99999, 6 | "version": 1, 7 | "merkleroot": "110ed92f558a1e3a94976ddea5c32f030670b5c58c3cc4d857ac14d7a1547a90", 8 | "tx": [ 9 | "110ed92f558a1e3a94976ddea5c32f030670b5c58c3cc4d857ac14d7a1547a90" 10 | ], 11 | "time": 1293623731, 12 | "nonce": 3892545714, 13 | "bits": "1b04864c", 14 | "difficulty": 14484.16236123, 15 | "chainwork": "000000000000000000000000000000000000000000000000064492eaf00f2520", 16 | "previousblockhash": "0000000000002103637910d267190996687fb095880d432c6531a527c8ec53d1", 17 | "nextblockhash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506" 18 | } 19 | -------------------------------------------------------------------------------- /tests/data/tx_110ed92f558a1e3a94976ddea5c32f030670b5c58c3cc4d857ac14d7a1547a90.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff07044c86041b013effffffff0100f2052a01000000434104d190840cfdae05af3d2febca52b0466b6efb02b44036a5d0d70659a53f7b84b736c5a05ed81e90af70985d59ffb3d1b91364f70b4d2b3b7553e177b1ceaff322ac00000000", 3 | "txid": "110ed92f558a1e3a94976ddea5c32f030670b5c58c3cc4d857ac14d7a1547a90", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "coinbase": "044c86041b013e", 9 | "sequence": 4294967295 10 | } 11 | ], 12 | "vout": [ 13 | { 14 | "value": 50.00000000, 15 | "n": 0, 16 | "scriptPubKey": { 17 | "asm": "04d190840cfdae05af3d2febca52b0466b6efb02b44036a5d0d70659a53f7b84b736c5a05ed81e90af70985d59ffb3d1b91364f70b4d2b3b7553e177b1ceaff322 OP_CHECKSIG", 18 | "hex": "4104d190840cfdae05af3d2febca52b0466b6efb02b44036a5d0d70659a53f7b84b736c5a05ed81e90af70985d59ffb3d1b91364f70b4d2b3b7553e177b1ceaff322ac", 19 | "reqSigs": 1, 20 | "type": "pubkey", 21 | "addresses": [ 22 | "1XPLDXBheQyN2JCujEYTdHHxz66i3QJJA" 23 | ] 24 | } 25 | } 26 | ], 27 | "blockhash": "000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250", 28 | "confirmations": 233618, 29 | "time": 1293623731, 30 | "blocktime": 1293623731 31 | } 32 | -------------------------------------------------------------------------------- /tests/data/tx_4a1817bf3dacbdb2f726e551979a4a1d27ec3ba9e4dda14fd90d0312f1cf068b.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "01000000025bb2bc80ae23b0c3d9ddf8fbb3457cd9a06c71ff369b477018c6a18f6b73a00d00000000494830450220690414b0cece222dd5fece6c0459d460f4f19e56e6e778d5837e964ae17ae8a6022100d60e1fec5f83496bc24ecd99861e09cebaf29d2690069a909b01c5bd4bf828b101ffffffff424739050927e6d7320df58eee636f0473d5e8678d92d9aa8e86a79fd01cb95700000000494830450221009ad66755338edfca8b713149b3a17cd69166b4c6804530644171c5d6c8fb74ab022011530cb894ee873db5010f9a3a28cf3a8e71ddcade634510e6a47a0fcdbc46d001ffffffff02c0cf6a00000000001976a9146dc5889a5b0e0d3d38b951f5ef2a701ec98a891488ac00e40b54020000001976a914f4b004c3ca2e7f96f9fc5bca767708967af67a4488ac00000000", 3 | "txid": "4a1817bf3dacbdb2f726e551979a4a1d27ec3ba9e4dda14fd90d0312f1cf068b", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "0da0736b8fa1c61870479b36ff716ca0d97c45b3fbf8ddd9c3b023ae80bcb25b", 9 | "vout": 0, 10 | "scriptSig": { 11 | "asm": "30450220690414b0cece222dd5fece6c0459d460f4f19e56e6e778d5837e964ae17ae8a6022100d60e1fec5f83496bc24ecd99861e09cebaf29d2690069a909b01c5bd4bf828b101", 12 | "hex": "4830450220690414b0cece222dd5fece6c0459d460f4f19e56e6e778d5837e964ae17ae8a6022100d60e1fec5f83496bc24ecd99861e09cebaf29d2690069a909b01c5bd4bf828b101" 13 | }, 14 | "sequence": 4294967295 15 | }, 16 | { 17 | "txid": "57b91cd09fa7868eaad9928d67e8d573046f63ee8ef50d32d7e6270905394742", 18 | "vout": 0, 19 | "scriptSig": { 20 | "asm": "30450221009ad66755338edfca8b713149b3a17cd69166b4c6804530644171c5d6c8fb74ab022011530cb894ee873db5010f9a3a28cf3a8e71ddcade634510e6a47a0fcdbc46d001", 21 | "hex": "4830450221009ad66755338edfca8b713149b3a17cd69166b4c6804530644171c5d6c8fb74ab022011530cb894ee873db5010f9a3a28cf3a8e71ddcade634510e6a47a0fcdbc46d001" 22 | }, 23 | "sequence": 4294967295 24 | } 25 | ], 26 | "vout": [ 27 | { 28 | "value": 0.07000000, 29 | "n": 0, 30 | "scriptPubKey": { 31 | "asm": "OP_DUP OP_HASH160 6dc5889a5b0e0d3d38b951f5ef2a701ec98a8914 OP_EQUALVERIFY OP_CHECKSIG", 32 | "hex": "76a9146dc5889a5b0e0d3d38b951f5ef2a701ec98a891488ac", 33 | "reqSigs": 1, 34 | "type": "pubkeyhash", 35 | "addresses": [ 36 | "1B1RHnXpachCAVHpQt81rZ17z9yYoipPBp" 37 | ] 38 | } 39 | }, 40 | { 41 | "value": 100.00000000, 42 | "n": 1, 43 | "scriptPubKey": { 44 | "asm": "OP_DUP OP_HASH160 f4b004c3ca2e7f96f9fc5bca767708967af67a44 OP_EQUALVERIFY OP_CHECKSIG", 45 | "hex": "76a914f4b004c3ca2e7f96f9fc5bca767708967af67a4488ac", 46 | "reqSigs": 1, 47 | "type": "pubkeyhash", 48 | "addresses": [ 49 | "1PJnjo4n2Rt5jWTUrCRr4inK2XmFPXqFC7" 50 | ] 51 | } 52 | } 53 | ], 54 | "blockhash": "00000000000092de6cd6e12e089c82d0e0ccda29c3311000b2947e9bef27fb2c", 55 | "confirmations": 224799, 56 | "time": 1303363832, 57 | "blocktime": 1303363832 58 | } 59 | -------------------------------------------------------------------------------- /tests/data/tx_6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "0100000001c33ebff2a709f13d9f9a7569ab16a32786af7d7e2de09265e41c61d078294ecf010000008a4730440220032d30df5ee6f57fa46cddb5eb8d0d9fe8de6b342d27942ae90a3231e0ba333e02203deee8060fdc70230a7f5b4ad7d7bc3e628cbe219a886b84269eaeb81e26b4fe014104ae31c31bf91278d99b8377a35bbce5b27d9fff15456839e919453fc7b3f721f0ba403ff96c9deeb680e5fd341c0fc3a7b90da4631ee39560639db462e9cb850fffffffff0240420f00000000001976a914b0dcbf97eabf4404e31d952477ce822dadbe7e1088acc060d211000000001976a9146b1281eec25ab4e1e0793ff4e08ab1abb3409cd988ac00000000", 3 | "txid": "6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "cf4e2978d0611ce46592e02d7e7daf8627a316ab69759a9f3df109a7f2bf3ec3", 9 | "vout": 1, 10 | "scriptSig": { 11 | "asm": "30440220032d30df5ee6f57fa46cddb5eb8d0d9fe8de6b342d27942ae90a3231e0ba333e02203deee8060fdc70230a7f5b4ad7d7bc3e628cbe219a886b84269eaeb81e26b4fe01 04ae31c31bf91278d99b8377a35bbce5b27d9fff15456839e919453fc7b3f721f0ba403ff96c9deeb680e5fd341c0fc3a7b90da4631ee39560639db462e9cb850f", 12 | "hex": "4730440220032d30df5ee6f57fa46cddb5eb8d0d9fe8de6b342d27942ae90a3231e0ba333e02203deee8060fdc70230a7f5b4ad7d7bc3e628cbe219a886b84269eaeb81e26b4fe014104ae31c31bf91278d99b8377a35bbce5b27d9fff15456839e919453fc7b3f721f0ba403ff96c9deeb680e5fd341c0fc3a7b90da4631ee39560639db462e9cb850f" 13 | }, 14 | "sequence": 4294967295 15 | } 16 | ], 17 | "vout": [ 18 | { 19 | "value": 0.01000000, 20 | "n": 0, 21 | "scriptPubKey": { 22 | "asm": "OP_DUP OP_HASH160 b0dcbf97eabf4404e31d952477ce822dadbe7e10 OP_EQUALVERIFY OP_CHECKSIG", 23 | "hex": "76a914b0dcbf97eabf4404e31d952477ce822dadbe7e1088ac", 24 | "reqSigs": 1, 25 | "type": "pubkeyhash", 26 | "addresses": [ 27 | "1H8ANdafjpqYntniT3Ddxh4xPBMCSz33pj" 28 | ] 29 | } 30 | }, 31 | { 32 | "value": 2.99000000, 33 | "n": 1, 34 | "scriptPubKey": { 35 | "asm": "OP_DUP OP_HASH160 6b1281eec25ab4e1e0793ff4e08ab1abb3409cd9 OP_EQUALVERIFY OP_CHECKSIG", 36 | "hex": "76a9146b1281eec25ab4e1e0793ff4e08ab1abb3409cd988ac", 37 | "reqSigs": 1, 38 | "type": "pubkeyhash", 39 | "addresses": [ 40 | "1Am9UTGfdnxabvcywYG2hvzr6qK8T3oUZT" 41 | ] 42 | } 43 | } 44 | ], 45 | "blockhash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506", 46 | "confirmations": 233539, 47 | "time": 1293623863, 48 | "blocktime": 1293623863 49 | } 50 | -------------------------------------------------------------------------------- /tests/data/tx_87a08fe3e74fa0a6a386c196938594ce0b928bf1550fb2004b02d68b1e7bd893.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "01000000014bac7b686e11eb138e67710158b76cbddb9c1cd0adb5d839775277f4591e0708000000004948304502206f5e76037d5c774443c195f1754364611499016dfdb3dee97a78d8b6b83262d7022100b6a0b0cd2d561cf7cd3fe908accf1ee354cfa448eccceada9818f359b1f8895d01ffffffff0200093d00000000001976a914116fae0885d98037c847bb8feaa0f6a7759cceed88ac00f2052a010000001976a914f4b004c3ca2e7f96f9fc5bca767708967af67a4488ac00000000", 3 | "txid": "87a08fe3e74fa0a6a386c196938594ce0b928bf1550fb2004b02d68b1e7bd893", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "08071e59f477527739d8b5add01c9cdbbd6cb7580171678e13eb116e687bac4b", 9 | "vout": 0, 10 | "scriptSig": { 11 | "asm": "304502206f5e76037d5c774443c195f1754364611499016dfdb3dee97a78d8b6b83262d7022100b6a0b0cd2d561cf7cd3fe908accf1ee354cfa448eccceada9818f359b1f8895d01", 12 | "hex": "48304502206f5e76037d5c774443c195f1754364611499016dfdb3dee97a78d8b6b83262d7022100b6a0b0cd2d561cf7cd3fe908accf1ee354cfa448eccceada9818f359b1f8895d01" 13 | }, 14 | "sequence": 4294967295 15 | } 16 | ], 17 | "vout": [ 18 | { 19 | "value": 0.04000000, 20 | "n": 0, 21 | "scriptPubKey": { 22 | "asm": "OP_DUP OP_HASH160 116fae0885d98037c847bb8feaa0f6a7759cceed OP_EQUALVERIFY OP_CHECKSIG", 23 | "hex": "76a914116fae0885d98037c847bb8feaa0f6a7759cceed88ac", 24 | "reqSigs": 1, 25 | "type": "pubkeyhash", 26 | "addresses": [ 27 | "12bCGuqBso4K8pKoNFXHUR4eMZg8j4xqN3" 28 | ] 29 | } 30 | }, 31 | { 32 | "value": 50.00000000, 33 | "n": 1, 34 | "scriptPubKey": { 35 | "asm": "OP_DUP OP_HASH160 f4b004c3ca2e7f96f9fc5bca767708967af67a44 OP_EQUALVERIFY OP_CHECKSIG", 36 | "hex": "76a914f4b004c3ca2e7f96f9fc5bca767708967af67a4488ac", 37 | "reqSigs": 1, 38 | "type": "pubkeyhash", 39 | "addresses": [ 40 | "1PJnjo4n2Rt5jWTUrCRr4inK2XmFPXqFC7" 41 | ] 42 | } 43 | } 44 | ], 45 | "blockhash": "00000000000058e5fee294c63ad70dcc160b27662571c41720b6595f77a38ee7", 46 | "confirmations": 224881, 47 | "time": 1303323136, 48 | "blocktime": 1303323136 49 | } 50 | -------------------------------------------------------------------------------- /tests/data/tx_87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "01000000012935b177236ec1cb75cd9fba86d84acac9d76ced9c1b22ba8de4cd2de85a8393000000004948304502200f653627aff050093a83dabc12a2a9b627041d424f2eb18849a2d587f1acd38f022100a23f94acd94a4d24049140d5fbe12448a880fd8f8c1c2b4141f83bef2be409be01ffffffff0100f2052a010000001976a91471d7dd96d9edda09180fe9d57a477b5acc9cad1188ac00000000", 3 | "txid": "87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "93835ae82dcde48dba221b9ced6cd7c9ca4ad886ba9fcd75cbc16e2377b13529", 9 | "vout": 0, 10 | "scriptSig": { 11 | "asm": "304502200f653627aff050093a83dabc12a2a9b627041d424f2eb18849a2d587f1acd38f022100a23f94acd94a4d24049140d5fbe12448a880fd8f8c1c2b4141f83bef2be409be01", 12 | "hex": "48304502200f653627aff050093a83dabc12a2a9b627041d424f2eb18849a2d587f1acd38f022100a23f94acd94a4d24049140d5fbe12448a880fd8f8c1c2b4141f83bef2be409be01" 13 | }, 14 | "sequence": 4294967295 15 | } 16 | ], 17 | "vout": [ 18 | { 19 | "value": 50.00000000, 20 | "n": 0, 21 | "scriptPubKey": { 22 | "asm": "OP_DUP OP_HASH160 71d7dd96d9edda09180fe9d57a477b5acc9cad11 OP_EQUALVERIFY OP_CHECKSIG", 23 | "hex": "76a91471d7dd96d9edda09180fe9d57a477b5acc9cad1188ac", 24 | "reqSigs": 1, 25 | "type": "pubkeyhash", 26 | "addresses": [ 27 | "1BNwxHGaFbeUBitpjy2AsKpJ29Ybxntqvb" 28 | ] 29 | } 30 | } 31 | ], 32 | "blockhash": "00000000000081a7e51283a1fc26bfe8ff7c5b7bd031dc5613395590d6220a3d", 33 | "confirmations": 234480, 34 | "time": 1293112858, 35 | "blocktime": 1293112858 36 | } 37 | -------------------------------------------------------------------------------- /tests/data/tx_8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84ac00000000", 3 | "txid": "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "coinbase": "044c86041b020602", 9 | "sequence": 4294967295 10 | } 11 | ], 12 | "vout": [ 13 | { 14 | "value": 50.00000000, 15 | "n": 0, 16 | "scriptPubKey": { 17 | "asm": "041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84 OP_CHECKSIG", 18 | "hex": "41041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84ac", 19 | "reqSigs": 1, 20 | "type": "pubkey", 21 | "addresses": [ 22 | "1HWqMzw1jfpXb3xyuUZ4uWXY4tqL2cW47J" 23 | ] 24 | } 25 | } 26 | ], 27 | "blockhash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506", 28 | "confirmations": 233539, 29 | "time": 1293623863, 30 | "blocktime": 1293623863 31 | } 32 | -------------------------------------------------------------------------------- /tests/data/tx_cf4e2978d0611ce46592e02d7e7daf8627a316ab69759a9f3df109a7f2bf3ec3.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "0100000001f4e7d8e6c04e24674492b678ff3db17689bcd88250547820e48cf11d8eda2b10010000008b483045022100a09b9e9ac574157551ecc78f22428b40660c7b4bf299dc77fc53a6a7e74c13ec02201e92a1e55f23ad6c54ab37790cd4bdc9606d09890442e8b5321e7c613f67c5e30141048d7f5b7f17f13a647dc84ed230518b73d964c9e6ee324cb3954bbd26cfa794666d7fd2d0aab5e3ddf36146b2957f54ea651b10664f9ab81742ecbc920c0dc01affffffff02c0fc9b01000000001976a914bd8f6f48907227d8bd07c342ea5c1c463051abde88ac00a3e111000000001976a91435fbee6a3bf8d99f17724ec54787567393a8a6b188ac00000000", 3 | "txid": "cf4e2978d0611ce46592e02d7e7daf8627a316ab69759a9f3df109a7f2bf3ec3", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "102bda8e1df18ce42078545082d8bc8976b13dff78b6924467244ec0e6d8e7f4", 9 | "vout": 1, 10 | "scriptSig": { 11 | "asm": "3045022100a09b9e9ac574157551ecc78f22428b40660c7b4bf299dc77fc53a6a7e74c13ec02201e92a1e55f23ad6c54ab37790cd4bdc9606d09890442e8b5321e7c613f67c5e301 048d7f5b7f17f13a647dc84ed230518b73d964c9e6ee324cb3954bbd26cfa794666d7fd2d0aab5e3ddf36146b2957f54ea651b10664f9ab81742ecbc920c0dc01a", 12 | "hex": "483045022100a09b9e9ac574157551ecc78f22428b40660c7b4bf299dc77fc53a6a7e74c13ec02201e92a1e55f23ad6c54ab37790cd4bdc9606d09890442e8b5321e7c613f67c5e30141048d7f5b7f17f13a647dc84ed230518b73d964c9e6ee324cb3954bbd26cfa794666d7fd2d0aab5e3ddf36146b2957f54ea651b10664f9ab81742ecbc920c0dc01a" 13 | }, 14 | "sequence": 4294967295 15 | } 16 | ], 17 | "vout": [ 18 | { 19 | "value": 0.27000000, 20 | "n": 0, 21 | "scriptPubKey": { 22 | "asm": "OP_DUP OP_HASH160 bd8f6f48907227d8bd07c342ea5c1c463051abde OP_EQUALVERIFY OP_CHECKSIG", 23 | "hex": "76a914bd8f6f48907227d8bd07c342ea5c1c463051abde88ac", 24 | "reqSigs": 1, 25 | "type": "pubkeyhash", 26 | "addresses": [ 27 | "1JHJYGshG8Ds9XXHbXuTrDkf8XAXzNhi5c" 28 | ] 29 | } 30 | }, 31 | { 32 | "value": 3.00000000, 33 | "n": 1, 34 | "scriptPubKey": { 35 | "asm": "OP_DUP OP_HASH160 35fbee6a3bf8d99f17724ec54787567393a8a6b1 OP_EQUALVERIFY OP_CHECKSIG", 36 | "hex": "76a91435fbee6a3bf8d99f17724ec54787567393a8a6b188ac", 37 | "reqSigs": 1, 38 | "type": "pubkeyhash", 39 | "addresses": [ 40 | "15vScfMHNrXN4QvWe54q5hwfVoYwG79CS1" 41 | ] 42 | } 43 | } 44 | ], 45 | "blockhash": "000000000002e399a140b0440a1f86a978a4cdadb1d054c68ade774ed53c88b5", 46 | "confirmations": 236536, 47 | "time": 1292066700, 48 | "blocktime": 1292066700 49 | } 50 | -------------------------------------------------------------------------------- /tests/data/tx_d5f013abf2cf4af6d68bcacd675c91f19bab5b7103b4ac2f4941686eb47da1f0.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "01000000021fe37f7aae81f3e3a8f5fc1bbdabcc511ad62ef2cb87eeb83088e5857cc8a35c3e0000008c493046022100af1a2cabf97f6d78a26f9544e445ad8269289cfb67d144905734d58a64e82467022100960f349808d3a8514b4ea8aec4a77e69f7e353635d6b2277b46f13965e61bfc50141044474591f6071614f98bf13f91756cc0a68d8d4a061b0203b3c548093520829943360c7b1fff040bca0fab6cd79f30b7281ef6ed969ad8b000ed7e79a9204e4e9ffffffff2e071faa53a9a95f14b5d71d0952af306ec229eca14d9659aebf2a14586806d82c0000008c493046022100cadc3187d6f65dd07d4c2e5fc082a990a482dd66f1bcc5b192d42fdb7a9a5680022100ba1ba42d6ff44b9c1f4dc34c441107c8180ee365c75a533cee43b34eeaaf6a60014104924067169e2579a4a0be724d5b8ccf4a1878fd8ff29867d20d1636c2f3b4075d877ea6e0a2033d362ec00005b220a2ad612adb5164f97a55acd5381f7b3bc5a3ffffffff14402c4206000000001976a914735aab94846992b307504b63687288aa830dbbc088ac40420f00000000001976a914a39fcf895f7c9fb88a6f19733c941e04bed5401e88ac404b4c00000000001976a91442eee18c92f0b915f6f30125683ad08ba4a41fdf88ac80778e06000000001976a914360a69efc728b6d1ed5fe3f60f62dc8c1556cb9788ac40230506000000001976a914740fa2fce9b3b34775f81335aba8b216ea50f51a88ac80969800000000001976a9149ea26718877da9171a8f14cccbccdc6d3c1a063a88ac80b14f01000000001976a914f4be058ccc345d557574b461eb41ca118d3dba5788ac80969800000000001976a914a8193fc5392bef28aad8d7ad793510adc4dd098e88ac80969800000000001976a914e978df283dd633b9d70fbb57232bbe8312e6147988ac00fcac06000000001976a914185f394e6d556dfe8b2ccec766e41a289fc240ad88ac40230506000000001976a91443713cb62d876c1d74b7cee5785e4a6a37814a4f88ac80969800000000001976a91405ca39cfbba25b384182bf9e35bfbaea9eb38b0488acc0cf6a00000000001976a9146abb64c4751ede732ba3a584de223dbd8989531288ac00ea3206000000001976a9147bc54ce6282e7ef48480cef15a2174bf036681aa88acc0d8a700000000001976a914332e8e34541560458687a5357693c1185b077a8688ac80969800000000001976a914bd4b19a9dea94472c9d1ffd4fe604ccdf560eee588ac00093d00000000001976a914ee4697eb4d3e8ca572da04b5e6c363586bda81f088ac80841e00000000001976a9149711461ec302d68573b80f28bb360fe34c47376f88acc0cf6a00000000001976a9144ea2cc288c1c871f9be60fb600c294b75fb83b4088ac40b72803000000001976a91472113668daf57fdb282985be164ec0b1f5a07eeb88ac00000000", 3 | "txid": "d5f013abf2cf4af6d68bcacd675c91f19bab5b7103b4ac2f4941686eb47da1f0", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "5ca3c87c85e58830b8ee87cbf22ed61a51ccabbd1bfcf5a8e3f381ae7a7fe31f", 9 | "vout": 62, 10 | "scriptSig": { 11 | "asm": "3046022100af1a2cabf97f6d78a26f9544e445ad8269289cfb67d144905734d58a64e82467022100960f349808d3a8514b4ea8aec4a77e69f7e353635d6b2277b46f13965e61bfc501 044474591f6071614f98bf13f91756cc0a68d8d4a061b0203b3c548093520829943360c7b1fff040bca0fab6cd79f30b7281ef6ed969ad8b000ed7e79a9204e4e9", 12 | "hex": "493046022100af1a2cabf97f6d78a26f9544e445ad8269289cfb67d144905734d58a64e82467022100960f349808d3a8514b4ea8aec4a77e69f7e353635d6b2277b46f13965e61bfc50141044474591f6071614f98bf13f91756cc0a68d8d4a061b0203b3c548093520829943360c7b1fff040bca0fab6cd79f30b7281ef6ed969ad8b000ed7e79a9204e4e9" 13 | }, 14 | "sequence": 4294967295 15 | }, 16 | { 17 | "txid": "d8066858142abfae59964da1ec29c26e30af52091dd7b5145fa9a953aa1f072e", 18 | "vout": 44, 19 | "scriptSig": { 20 | "asm": "3046022100cadc3187d6f65dd07d4c2e5fc082a990a482dd66f1bcc5b192d42fdb7a9a5680022100ba1ba42d6ff44b9c1f4dc34c441107c8180ee365c75a533cee43b34eeaaf6a6001 04924067169e2579a4a0be724d5b8ccf4a1878fd8ff29867d20d1636c2f3b4075d877ea6e0a2033d362ec00005b220a2ad612adb5164f97a55acd5381f7b3bc5a3", 21 | "hex": "493046022100cadc3187d6f65dd07d4c2e5fc082a990a482dd66f1bcc5b192d42fdb7a9a5680022100ba1ba42d6ff44b9c1f4dc34c441107c8180ee365c75a533cee43b34eeaaf6a60014104924067169e2579a4a0be724d5b8ccf4a1878fd8ff29867d20d1636c2f3b4075d877ea6e0a2033d362ec00005b220a2ad612adb5164f97a55acd5381f7b3bc5a3" 22 | }, 23 | "sequence": 4294967295 24 | } 25 | ], 26 | "vout": [ 27 | { 28 | "value": 1.05000000, 29 | "n": 0, 30 | "scriptPubKey": { 31 | "asm": "OP_DUP OP_HASH160 735aab94846992b307504b63687288aa830dbbc0 OP_EQUALVERIFY OP_CHECKSIG", 32 | "hex": "76a914735aab94846992b307504b63687288aa830dbbc088ac", 33 | "reqSigs": 1, 34 | "type": "pubkeyhash", 35 | "addresses": [ 36 | "1BWwKwTM6phe45zwUVGQq6WipmWZsVbK8h" 37 | ] 38 | } 39 | }, 40 | { 41 | "value": 0.01000000, 42 | "n": 1, 43 | "scriptPubKey": { 44 | "asm": "OP_DUP OP_HASH160 a39fcf895f7c9fb88a6f19733c941e04bed5401e OP_EQUALVERIFY OP_CHECKSIG", 45 | "hex": "76a914a39fcf895f7c9fb88a6f19733c941e04bed5401e88ac", 46 | "reqSigs": 1, 47 | "type": "pubkeyhash", 48 | "addresses": [ 49 | "1FvAb7NDfcwQPTso8MLgx1Q4UZpVpYk3VE" 50 | ] 51 | } 52 | }, 53 | { 54 | "value": 0.05000000, 55 | "n": 2, 56 | "scriptPubKey": { 57 | "asm": "OP_DUP OP_HASH160 42eee18c92f0b915f6f30125683ad08ba4a41fdf OP_EQUALVERIFY OP_CHECKSIG", 58 | "hex": "76a91442eee18c92f0b915f6f30125683ad08ba4a41fdf88ac", 59 | "reqSigs": 1, 60 | "type": "pubkeyhash", 61 | "addresses": [ 62 | "176umQcueELRQzRLG51UPNRJVPmUiuVS3S" 63 | ] 64 | } 65 | }, 66 | { 67 | "value": 1.10000000, 68 | "n": 3, 69 | "scriptPubKey": { 70 | "asm": "OP_DUP OP_HASH160 360a69efc728b6d1ed5fe3f60f62dc8c1556cb97 OP_EQUALVERIFY OP_CHECKSIG", 71 | "hex": "76a914360a69efc728b6d1ed5fe3f60f62dc8c1556cb9788ac", 72 | "reqSigs": 1, 73 | "type": "pubkeyhash", 74 | "addresses": [ 75 | "15vjxvZHpjkc1Bor1TFJA7s34VFQpoQk2Z" 76 | ] 77 | } 78 | }, 79 | { 80 | "value": 1.01000000, 81 | "n": 4, 82 | "scriptPubKey": { 83 | "asm": "OP_DUP OP_HASH160 740fa2fce9b3b34775f81335aba8b216ea50f51a OP_EQUALVERIFY OP_CHECKSIG", 84 | "hex": "76a914740fa2fce9b3b34775f81335aba8b216ea50f51a88ac", 85 | "reqSigs": 1, 86 | "type": "pubkeyhash", 87 | "addresses": [ 88 | "1Bag7i2RtYNDaSmg5HxUbpkjQ2n5Lgv5XW" 89 | ] 90 | } 91 | }, 92 | { 93 | "value": 0.10000000, 94 | "n": 5, 95 | "scriptPubKey": { 96 | "asm": "OP_DUP OP_HASH160 9ea26718877da9171a8f14cccbccdc6d3c1a063a OP_EQUALVERIFY OP_CHECKSIG", 97 | "hex": "76a9149ea26718877da9171a8f14cccbccdc6d3c1a063a88ac", 98 | "reqSigs": 1, 99 | "type": "pubkeyhash", 100 | "addresses": [ 101 | "1FTnKFNrdQtLHJSKKucLA1TL6SVxEfd3FT" 102 | ] 103 | } 104 | }, 105 | { 106 | "value": 0.22000000, 107 | "n": 6, 108 | "scriptPubKey": { 109 | "asm": "OP_DUP OP_HASH160 f4be058ccc345d557574b461eb41ca118d3dba57 OP_EQUALVERIFY OP_CHECKSIG", 110 | "hex": "76a914f4be058ccc345d557574b461eb41ca118d3dba5788ac", 111 | "reqSigs": 1, 112 | "type": "pubkeyhash", 113 | "addresses": [ 114 | "1PK5WkB7xyczmazfyYpRyCtfwRdMssMHDM" 115 | ] 116 | } 117 | }, 118 | { 119 | "value": 0.10000000, 120 | "n": 7, 121 | "scriptPubKey": { 122 | "asm": "OP_DUP OP_HASH160 a8193fc5392bef28aad8d7ad793510adc4dd098e OP_EQUALVERIFY OP_CHECKSIG", 123 | "hex": "76a914a8193fc5392bef28aad8d7ad793510adc4dd098e88ac", 124 | "reqSigs": 1, 125 | "type": "pubkeyhash", 126 | "addresses": [ 127 | "1GKpmZps56HFN4MGkSEiwQgi3NQYshsdVR" 128 | ] 129 | } 130 | }, 131 | { 132 | "value": 0.10000000, 133 | "n": 8, 134 | "scriptPubKey": { 135 | "asm": "OP_DUP OP_HASH160 e978df283dd633b9d70fbb57232bbe8312e61479 OP_EQUALVERIFY OP_CHECKSIG", 136 | "hex": "76a914e978df283dd633b9d70fbb57232bbe8312e6147988ac", 137 | "reqSigs": 1, 138 | "type": "pubkeyhash", 139 | "addresses": [ 140 | "1NHVF1aZfXtXcx9sga7FAQs26w4TzQbRd3" 141 | ] 142 | } 143 | }, 144 | { 145 | "value": 1.12000000, 146 | "n": 9, 147 | "scriptPubKey": { 148 | "asm": "OP_DUP OP_HASH160 185f394e6d556dfe8b2ccec766e41a289fc240ad OP_EQUALVERIFY OP_CHECKSIG", 149 | "hex": "76a914185f394e6d556dfe8b2ccec766e41a289fc240ad88ac", 150 | "reqSigs": 1, 151 | "type": "pubkeyhash", 152 | "addresses": [ 153 | "13DsHseXX5f8eY6EVwws6QvtPUgUjMoxgk" 154 | ] 155 | } 156 | }, 157 | { 158 | "value": 1.01000000, 159 | "n": 10, 160 | "scriptPubKey": { 161 | "asm": "OP_DUP OP_HASH160 43713cb62d876c1d74b7cee5785e4a6a37814a4f OP_EQUALVERIFY OP_CHECKSIG", 162 | "hex": "76a91443713cb62d876c1d74b7cee5785e4a6a37814a4f88ac", 163 | "reqSigs": 1, 164 | "type": "pubkeyhash", 165 | "addresses": [ 166 | "179bvi51LJszF7tyMbJghPtgzbQuipDkWP" 167 | ] 168 | } 169 | }, 170 | { 171 | "value": 0.10000000, 172 | "n": 11, 173 | "scriptPubKey": { 174 | "asm": "OP_DUP OP_HASH160 05ca39cfbba25b384182bf9e35bfbaea9eb38b04 OP_EQUALVERIFY OP_CHECKSIG", 175 | "hex": "76a91405ca39cfbba25b384182bf9e35bfbaea9eb38b0488ac", 176 | "reqSigs": 1, 177 | "type": "pubkeyhash", 178 | "addresses": [ 179 | "1Xcdre9pAipV9kiSrSgssEpQPAruzMFzr" 180 | ] 181 | } 182 | }, 183 | { 184 | "value": 0.07000000, 185 | "n": 12, 186 | "scriptPubKey": { 187 | "asm": "OP_DUP OP_HASH160 6abb64c4751ede732ba3a584de223dbd89895312 OP_EQUALVERIFY OP_CHECKSIG", 188 | "hex": "76a9146abb64c4751ede732ba3a584de223dbd8989531288ac", 189 | "reqSigs": 1, 190 | "type": "pubkeyhash", 191 | "addresses": [ 192 | "1AjM7fvtZz7Si2rEjPLfdD7461p5hGzn1H" 193 | ] 194 | } 195 | }, 196 | { 197 | "value": 1.04000000, 198 | "n": 13, 199 | "scriptPubKey": { 200 | "asm": "OP_DUP OP_HASH160 7bc54ce6282e7ef48480cef15a2174bf036681aa OP_EQUALVERIFY OP_CHECKSIG", 201 | "hex": "76a9147bc54ce6282e7ef48480cef15a2174bf036681aa88ac", 202 | "reqSigs": 1, 203 | "type": "pubkeyhash", 204 | "addresses": [ 205 | "1CHSUGT1or76y4DEoGkRD94QeuUeuC1ukn" 206 | ] 207 | } 208 | }, 209 | { 210 | "value": 0.11000000, 211 | "n": 14, 212 | "scriptPubKey": { 213 | "asm": "OP_DUP OP_HASH160 332e8e34541560458687a5357693c1185b077a86 OP_EQUALVERIFY OP_CHECKSIG", 214 | "hex": "76a914332e8e34541560458687a5357693c1185b077a8688ac", 215 | "reqSigs": 1, 216 | "type": "pubkeyhash", 217 | "addresses": [ 218 | "15fdEWtDcc6THx9YMfVm4vLKEVcLc61C8s" 219 | ] 220 | } 221 | }, 222 | { 223 | "value": 0.10000000, 224 | "n": 15, 225 | "scriptPubKey": { 226 | "asm": "OP_DUP OP_HASH160 bd4b19a9dea94472c9d1ffd4fe604ccdf560eee5 OP_EQUALVERIFY OP_CHECKSIG", 227 | "hex": "76a914bd4b19a9dea94472c9d1ffd4fe604ccdf560eee588ac", 228 | "reqSigs": 1, 229 | "type": "pubkeyhash", 230 | "addresses": [ 231 | "1JFtgK8r1jd9me53hTqs5zo5HjuRzEdYyQ" 232 | ] 233 | } 234 | }, 235 | { 236 | "value": 0.04000000, 237 | "n": 16, 238 | "scriptPubKey": { 239 | "asm": "OP_DUP OP_HASH160 ee4697eb4d3e8ca572da04b5e6c363586bda81f0 OP_EQUALVERIFY OP_CHECKSIG", 240 | "hex": "76a914ee4697eb4d3e8ca572da04b5e6c363586bda81f088ac", 241 | "reqSigs": 1, 242 | "type": "pubkeyhash", 243 | "addresses": [ 244 | "1NitPaG33RFJZYvUt2cm7Gqka77e4jsCP6" 245 | ] 246 | } 247 | }, 248 | { 249 | "value": 0.02000000, 250 | "n": 17, 251 | "scriptPubKey": { 252 | "asm": "OP_DUP OP_HASH160 9711461ec302d68573b80f28bb360fe34c47376f OP_EQUALVERIFY OP_CHECKSIG", 253 | "hex": "76a9149711461ec302d68573b80f28bb360fe34c47376f88ac", 254 | "reqSigs": 1, 255 | "type": "pubkeyhash", 256 | "addresses": [ 257 | "1EmmjBMRa2yfq2KiTwzqEkqNrqxkAaeBu6" 258 | ] 259 | } 260 | }, 261 | { 262 | "value": 0.07000000, 263 | "n": 18, 264 | "scriptPubKey": { 265 | "asm": "OP_DUP OP_HASH160 4ea2cc288c1c871f9be60fb600c294b75fb83b40 OP_EQUALVERIFY OP_CHECKSIG", 266 | "hex": "76a9144ea2cc288c1c871f9be60fb600c294b75fb83b4088ac", 267 | "reqSigs": 1, 268 | "type": "pubkeyhash", 269 | "addresses": [ 270 | "18AnjLCGk6P5jtAUdcqHYv78Ny3LXXuD9u" 271 | ] 272 | } 273 | }, 274 | { 275 | "value": 0.53000000, 276 | "n": 19, 277 | "scriptPubKey": { 278 | "asm": "OP_DUP OP_HASH160 72113668daf57fdb282985be164ec0b1f5a07eeb OP_EQUALVERIFY OP_CHECKSIG", 279 | "hex": "76a91472113668daf57fdb282985be164ec0b1f5a07eeb88ac", 280 | "reqSigs": 1, 281 | "type": "pubkeyhash", 282 | "addresses": [ 283 | "1BQ8epNWFF6UxvcpB43e5CNF5PVXgYozkL" 284 | ] 285 | } 286 | } 287 | ], 288 | "blockhash": "000000000000591b8b7b8e8c5aa90ecf594d044f74c2b1f4df46d6e6f045edb3", 289 | "confirmations": 219885, 290 | "time": 1303387869, 291 | "blocktime": 1303387869 292 | } 293 | -------------------------------------------------------------------------------- /tests/data/tx_d8066858142abfae59964da1ec29c26e30af52091dd7b5145fa9a953aa1f072e.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "010000000293d87b1e8bd6024b00b20f55f18b920bce94859396c186a3a6a04fe7e38fa087010000008b483045022100ee1fc01edab54f5f5fde5be769c7f648e0a83d14de30d99ec2bb9d7bb0c1d1c602202cba8444c9c8efcfc25ba48558be0f3ab115fddaeb7f073c1911eb3e007f013e014104e6723fb5cb12c4f8bec7f1f9a507b3e080f5063da279187cbc9a6b879ab8ce2dc7de7cccfaacd44a5874b3255045c7c2a0102e3f073e4d3c86f73e5a1b03fc78ffffffff8b06cff112030dd94fa1dde4a93bec271d4a9a9751e526f7b2bdac3dbf17184a010000008b483045022027b8747399b96a95ce4ea8d476d6699682d8e7c22bf0996c7d862b12c7ec69fe022100e2a464bf4a5d80264d9c136bae52b777a43f7c92a3eefe98dde52fda8841c072014104e6723fb5cb12c4f8bec7f1f9a507b3e080f5063da279187cbc9a6b879ab8ce2dc7de7cccfaacd44a5874b3255045c7c2a0102e3f073e4d3c86f73e5a1b03fc78ffffffff37c03b4703000000001976a91449b58166f37e8ca2362fbb98ebf4d87ff218cd3688ac80779743000000001976a914fa5fb9fb9c2741574fbd298d5bcc617b33a5701b88acc0a72306000000001976a914693cb8dd3974a81807290b6bdf65c1193641338288ac402c4206000000001976a914ce08b2f103e0917cdba722ff1ecc75be7981979588ac8080cb06000000001976a914739d1ad7162dccf0d2812c7513b4f8c14baaacb188acc07fdc0b000000001976a9149c93a46cc82b3ba0185840703034c41039052fa688ac00093d00000000001976a91442eee18c92f0b915f6f30125683ad08ba4a41fdf88ac00093d00000000001976a914fc7d77c372ddb8c8049b36da6b4fdcf7bda9916688ac80e40117000000001976a9143cae7a1911e472278cfc3a39a166eb5c7cda53dc88ac4062b007000000001976a9144a36cd4b90328d7c5a4b311a8b88f32fa5a0c48f88ac406f4001000000001976a9146abb64c4751ede732ba3a584de223dbd8989531288ac40357f06000000001976a914e466707187d38ee6f4c2ac67b9c21cbc17de395588acc0a72306000000001976a914235751c3139e8498c9c496c6c81fef568a86edc488ac002d3101000000001976a914ee4697eb4d3e8ca572da04b5e6c363586bda81f088ac80c8b308000000001976a914c5b3a9f6cddfc506edb3a9fd8ef533d51f0c54da88ac00990d04000000001976a91472113668daf57fdb282985be164ec0b1f5a07eeb88ac404b4c00000000001976a91433d352b32af864447ea3a5397a26414ab3eeb9b688ac00f2052a010000001976a9146385e05f35ca874dddb45a3ca3a45120c84b203188ac00b0710b000000001976a914acd5f88f4958dcbf89de52b3a5c7bea3ff85df6388ac00ea3206000000001976a91455cf6da1a8d381c7019c1d375ed36e2e65075f7d88acc0cb1707000000001976a9147eeca8e4c523e16746e5a0a5e1ea77f41e88f7bd88acc0c7c40d000000001976a914360a69efc728b6d1ed5fe3f60f62dc8c1556cb9788ac40cbd01e000000001976a9146b8e262762c6bbecd36d1ae14b770f86c917739088acc0b99d06000000001976a91487ea46e0f742b95b7ab51bdda5b644a522e5f78588acc0cf6a00000000001976a9147610422f995540b0914efba5b79a4fcd61b5286288ac80841e00000000001976a9147ed0e5d65823ca4ea4bbdbfb92776cfe3239f21588ac80969800000000001976a91405ca39cfbba25b384182bf9e35bfbaea9eb38b0488ac40548900000000001976a9145e320e294012f86459676ff96cb0e99f5d42be4288ac403ebc06000000001976a914bb2a62f4f130d5d7c8f33802492457a70cc9f7d388ac00f36f06000000001976a91430f8c38f48efd02636b27cb4f4c4d5cd9aead55188ac00093d00000000001976a914a39fcf895f7c9fb88a6f19733c941e04bed5401e88ac80651406000000001976a914fc24668134da1e804db1e386ea8cb18f18bd4f8888ac004dd208000000001976a914465941501190171805e96a5e8d8bc1794fd4737888acc0e1e400000000001976a9144ea2cc288c1c871f9be60fb600c294b75fb83b4088ac40420f00000000001976a9148d7991c145ef34234238f8d1d8898717faf8acb988ac8061c10c000000001976a9148f9046b56fd50f8434b261bf96e83e7778c0b4f188acc0c62d00000000001976a914ca670e37b8170e91426f19669da92dca3dbd18aa88ac00a3e111000000001976a914b79506588a14a0c0be9c4ad2a29c8ad58590ec1a88ac80969800000000001976a914aea6c870f027d74b0e064e1b119200c2b72075d688ac80890807000000001976a9147f3a49e14b2e5861cb8218552fc7e67ac28a085b88acc041c817000000001976a9140da116fe94af16a4597ae4febd7fee0c6467ce6488acc04fef1e000000001976a914916698eefead5f75029d9ea56bf3a00104dc615188acc040310a000000001976a91467ad45dcfc30e5c92b827c7a0767705cab2fcca088acc0f35e01000000001976a914fa6a5019983b848e64f6858e23db27b4f171729b88ac804a5d05000000001976a914d88bba412363fd3986513038007b2d7b8ee0232088ac009f8e18000000001976a91419e4fe7403d4cdf4e7a44c080aab114520b1e88e88ac00ea3206000000001976a9141b0ee86a1a9845766dc9d7da998d2ddea9d3dd8588ac80f00340000000001976a9142dcaf496772a8da8ae6560a523eae02dd431ae3088ac80b64245000000001976a914acfe60621ae22430f78bd3d2437de0f3ec39b9b188ac00e1f505000000001976a914daa2fb05680d1c01bccd29d8d545e4732f960f1688ac8080cb06000000001976a914c3ba83d99a86f6aa65f57baeaeb7ef45faf4bfee88acc0a72306000000001976a914e76d750418680b018b921e20463927d1909a69d988ac80f0fa02000000001976a9142168960c49d62a680abc9104bbf4a9c6253463ba88ac406f4001000000001976a914ff258a3d0c3e7a3a2f70ce17988e46a23ce40c7f88ac0080841e000000001976a914fd5680eaa0214d4d86cb87654600b7113e65236f88ac00000000", 3 | "txid": "d8066858142abfae59964da1ec29c26e30af52091dd7b5145fa9a953aa1f072e", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "87a08fe3e74fa0a6a386c196938594ce0b928bf1550fb2004b02d68b1e7bd893", 9 | "vout": 1, 10 | "scriptSig": { 11 | "asm": "3045022100ee1fc01edab54f5f5fde5be769c7f648e0a83d14de30d99ec2bb9d7bb0c1d1c602202cba8444c9c8efcfc25ba48558be0f3ab115fddaeb7f073c1911eb3e007f013e01 04e6723fb5cb12c4f8bec7f1f9a507b3e080f5063da279187cbc9a6b879ab8ce2dc7de7cccfaacd44a5874b3255045c7c2a0102e3f073e4d3c86f73e5a1b03fc78", 12 | "hex": "483045022100ee1fc01edab54f5f5fde5be769c7f648e0a83d14de30d99ec2bb9d7bb0c1d1c602202cba8444c9c8efcfc25ba48558be0f3ab115fddaeb7f073c1911eb3e007f013e014104e6723fb5cb12c4f8bec7f1f9a507b3e080f5063da279187cbc9a6b879ab8ce2dc7de7cccfaacd44a5874b3255045c7c2a0102e3f073e4d3c86f73e5a1b03fc78" 13 | }, 14 | "sequence": 4294967295 15 | }, 16 | { 17 | "txid": "4a1817bf3dacbdb2f726e551979a4a1d27ec3ba9e4dda14fd90d0312f1cf068b", 18 | "vout": 1, 19 | "scriptSig": { 20 | "asm": "3045022027b8747399b96a95ce4ea8d476d6699682d8e7c22bf0996c7d862b12c7ec69fe022100e2a464bf4a5d80264d9c136bae52b777a43f7c92a3eefe98dde52fda8841c07201 04e6723fb5cb12c4f8bec7f1f9a507b3e080f5063da279187cbc9a6b879ab8ce2dc7de7cccfaacd44a5874b3255045c7c2a0102e3f073e4d3c86f73e5a1b03fc78", 21 | "hex": "483045022027b8747399b96a95ce4ea8d476d6699682d8e7c22bf0996c7d862b12c7ec69fe022100e2a464bf4a5d80264d9c136bae52b777a43f7c92a3eefe98dde52fda8841c072014104e6723fb5cb12c4f8bec7f1f9a507b3e080f5063da279187cbc9a6b879ab8ce2dc7de7cccfaacd44a5874b3255045c7c2a0102e3f073e4d3c86f73e5a1b03fc78" 22 | }, 23 | "sequence": 4294967295 24 | } 25 | ], 26 | "vout": [ 27 | { 28 | "value": 0.55000000, 29 | "n": 0, 30 | "scriptPubKey": { 31 | "asm": "OP_DUP OP_HASH160 49b58166f37e8ca2362fbb98ebf4d87ff218cd36 OP_EQUALVERIFY OP_CHECKSIG", 32 | "hex": "76a91449b58166f37e8ca2362fbb98ebf4d87ff218cd3688ac", 33 | "reqSigs": 1, 34 | "type": "pubkeyhash", 35 | "addresses": [ 36 | "17ijmE4nZstNnevbHLawnhCW9nZ7PN8CSg" 37 | ] 38 | } 39 | }, 40 | { 41 | "value": 11.34000000, 42 | "n": 1, 43 | "scriptPubKey": { 44 | "asm": "OP_DUP OP_HASH160 fa5fb9fb9c2741574fbd298d5bcc617b33a5701b OP_EQUALVERIFY OP_CHECKSIG", 45 | "hex": "76a914fa5fb9fb9c2741574fbd298d5bcc617b33a5701b88ac", 46 | "reqSigs": 1, 47 | "type": "pubkeyhash", 48 | "addresses": [ 49 | "1Pprc9mMddEssywtyjyppUcuYvXxS5gKi2" 50 | ] 51 | } 52 | }, 53 | { 54 | "value": 1.03000000, 55 | "n": 2, 56 | "scriptPubKey": { 57 | "asm": "OP_DUP OP_HASH160 693cb8dd3974a81807290b6bdf65c11936413382 OP_EQUALVERIFY OP_CHECKSIG", 58 | "hex": "76a914693cb8dd3974a81807290b6bdf65c1193641338288ac", 59 | "reqSigs": 1, 60 | "type": "pubkeyhash", 61 | "addresses": [ 62 | "1AbShBzJLwHUt7DVk3azwgFpUqZszD7QFn" 63 | ] 64 | } 65 | }, 66 | { 67 | "value": 1.05000000, 68 | "n": 3, 69 | "scriptPubKey": { 70 | "asm": "OP_DUP OP_HASH160 ce08b2f103e0917cdba722ff1ecc75be79819795 OP_EQUALVERIFY OP_CHECKSIG", 71 | "hex": "76a914ce08b2f103e0917cdba722ff1ecc75be7981979588ac", 72 | "reqSigs": 1, 73 | "type": "pubkeyhash", 74 | "addresses": [ 75 | "1KnQcw9H1jvQF5cytF8qvi6kF83wnx94EU" 76 | ] 77 | } 78 | }, 79 | { 80 | "value": 1.14000000, 81 | "n": 4, 82 | "scriptPubKey": { 83 | "asm": "OP_DUP OP_HASH160 739d1ad7162dccf0d2812c7513b4f8c14baaacb1 OP_EQUALVERIFY OP_CHECKSIG", 84 | "hex": "76a914739d1ad7162dccf0d2812c7513b4f8c14baaacb188ac", 85 | "reqSigs": 1, 86 | "type": "pubkeyhash", 87 | "addresses": [ 88 | "1BYJutzgtg3cGKPYT7xnJcP91kQRiCjQ4y" 89 | ] 90 | } 91 | }, 92 | { 93 | "value": 1.99000000, 94 | "n": 5, 95 | "scriptPubKey": { 96 | "asm": "OP_DUP OP_HASH160 9c93a46cc82b3ba0185840703034c41039052fa6 OP_EQUALVERIFY OP_CHECKSIG", 97 | "hex": "76a9149c93a46cc82b3ba0185840703034c41039052fa688ac", 98 | "reqSigs": 1, 99 | "type": "pubkeyhash", 100 | "addresses": [ 101 | "1FGuHHu4xucr5PU3AWL32F6gTbxz6gQsSe" 102 | ] 103 | } 104 | }, 105 | { 106 | "value": 0.04000000, 107 | "n": 6, 108 | "scriptPubKey": { 109 | "asm": "OP_DUP OP_HASH160 42eee18c92f0b915f6f30125683ad08ba4a41fdf OP_EQUALVERIFY OP_CHECKSIG", 110 | "hex": "76a91442eee18c92f0b915f6f30125683ad08ba4a41fdf88ac", 111 | "reqSigs": 1, 112 | "type": "pubkeyhash", 113 | "addresses": [ 114 | "176umQcueELRQzRLG51UPNRJVPmUiuVS3S" 115 | ] 116 | } 117 | }, 118 | { 119 | "value": 0.04000000, 120 | "n": 7, 121 | "scriptPubKey": { 122 | "asm": "OP_DUP OP_HASH160 fc7d77c372ddb8c8049b36da6b4fdcf7bda99166 OP_EQUALVERIFY OP_CHECKSIG", 123 | "hex": "76a914fc7d77c372ddb8c8049b36da6b4fdcf7bda9916688ac", 124 | "reqSigs": 1, 125 | "type": "pubkeyhash", 126 | "addresses": [ 127 | "1Q23azvTqcKifpb9zqZqrF9BkY2La4kHfz" 128 | ] 129 | } 130 | }, 131 | { 132 | "value": 3.86000000, 133 | "n": 8, 134 | "scriptPubKey": { 135 | "asm": "OP_DUP OP_HASH160 3cae7a1911e472278cfc3a39a166eb5c7cda53dc OP_EQUALVERIFY OP_CHECKSIG", 136 | "hex": "76a9143cae7a1911e472278cfc3a39a166eb5c7cda53dc88ac", 137 | "reqSigs": 1, 138 | "type": "pubkeyhash", 139 | "addresses": [ 140 | "16XrZP4z3Q6JtBMC8MgKg9p7x3UnU3YSVF" 141 | ] 142 | } 143 | }, 144 | { 145 | "value": 1.29000000, 146 | "n": 9, 147 | "scriptPubKey": { 148 | "asm": "OP_DUP OP_HASH160 4a36cd4b90328d7c5a4b311a8b88f32fa5a0c48f OP_EQUALVERIFY OP_CHECKSIG", 149 | "hex": "76a9144a36cd4b90328d7c5a4b311a8b88f32fa5a0c48f88ac", 150 | "reqSigs": 1, 151 | "type": "pubkeyhash", 152 | "addresses": [ 153 | "17mQeuEcCZvJvBK8cfh3aHbVzC9zNq66GW" 154 | ] 155 | } 156 | }, 157 | { 158 | "value": 0.21000000, 159 | "n": 10, 160 | "scriptPubKey": { 161 | "asm": "OP_DUP OP_HASH160 6abb64c4751ede732ba3a584de223dbd89895312 OP_EQUALVERIFY OP_CHECKSIG", 162 | "hex": "76a9146abb64c4751ede732ba3a584de223dbd8989531288ac", 163 | "reqSigs": 1, 164 | "type": "pubkeyhash", 165 | "addresses": [ 166 | "1AjM7fvtZz7Si2rEjPLfdD7461p5hGzn1H" 167 | ] 168 | } 169 | }, 170 | { 171 | "value": 1.09000000, 172 | "n": 11, 173 | "scriptPubKey": { 174 | "asm": "OP_DUP OP_HASH160 e466707187d38ee6f4c2ac67b9c21cbc17de3955 OP_EQUALVERIFY OP_CHECKSIG", 175 | "hex": "76a914e466707187d38ee6f4c2ac67b9c21cbc17de395588ac", 176 | "reqSigs": 1, 177 | "type": "pubkeyhash", 178 | "addresses": [ 179 | "1MpfnLigtuU1JTbmvVvbaac7dVrtao3AX4" 180 | ] 181 | } 182 | }, 183 | { 184 | "value": 1.03000000, 185 | "n": 12, 186 | "scriptPubKey": { 187 | "asm": "OP_DUP OP_HASH160 235751c3139e8498c9c496c6c81fef568a86edc4 OP_EQUALVERIFY OP_CHECKSIG", 188 | "hex": "76a914235751c3139e8498c9c496c6c81fef568a86edc488ac", 189 | "reqSigs": 1, 190 | "type": "pubkeyhash", 191 | "addresses": [ 192 | "14DsFmj9dhpcFjs5R3CsMSrx8vnxGcQLk8" 193 | ] 194 | } 195 | }, 196 | { 197 | "value": 0.20000000, 198 | "n": 13, 199 | "scriptPubKey": { 200 | "asm": "OP_DUP OP_HASH160 ee4697eb4d3e8ca572da04b5e6c363586bda81f0 OP_EQUALVERIFY OP_CHECKSIG", 201 | "hex": "76a914ee4697eb4d3e8ca572da04b5e6c363586bda81f088ac", 202 | "reqSigs": 1, 203 | "type": "pubkeyhash", 204 | "addresses": [ 205 | "1NitPaG33RFJZYvUt2cm7Gqka77e4jsCP6" 206 | ] 207 | } 208 | }, 209 | { 210 | "value": 1.46000000, 211 | "n": 14, 212 | "scriptPubKey": { 213 | "asm": "OP_DUP OP_HASH160 c5b3a9f6cddfc506edb3a9fd8ef533d51f0c54da OP_EQUALVERIFY OP_CHECKSIG", 214 | "hex": "76a914c5b3a9f6cddfc506edb3a9fd8ef533d51f0c54da88ac", 215 | "reqSigs": 1, 216 | "type": "pubkeyhash", 217 | "addresses": [ 218 | "1K2MM4QeHgtER2HScsDYs5Hjy3DzdokKjV" 219 | ] 220 | } 221 | }, 222 | { 223 | "value": 0.68000000, 224 | "n": 15, 225 | "scriptPubKey": { 226 | "asm": "OP_DUP OP_HASH160 72113668daf57fdb282985be164ec0b1f5a07eeb OP_EQUALVERIFY OP_CHECKSIG", 227 | "hex": "76a91472113668daf57fdb282985be164ec0b1f5a07eeb88ac", 228 | "reqSigs": 1, 229 | "type": "pubkeyhash", 230 | "addresses": [ 231 | "1BQ8epNWFF6UxvcpB43e5CNF5PVXgYozkL" 232 | ] 233 | } 234 | }, 235 | { 236 | "value": 0.05000000, 237 | "n": 16, 238 | "scriptPubKey": { 239 | "asm": "OP_DUP OP_HASH160 33d352b32af864447ea3a5397a26414ab3eeb9b6 OP_EQUALVERIFY OP_CHECKSIG", 240 | "hex": "76a91433d352b32af864447ea3a5397a26414ab3eeb9b688ac", 241 | "reqSigs": 1, 242 | "type": "pubkeyhash", 243 | "addresses": [ 244 | "15j2cmRuWUgNiXgimwQAJ588SV4V9HoVWM" 245 | ] 246 | } 247 | }, 248 | { 249 | "value": 50.00000000, 250 | "n": 17, 251 | "scriptPubKey": { 252 | "asm": "OP_DUP OP_HASH160 6385e05f35ca874dddb45a3ca3a45120c84b2031 OP_EQUALVERIFY OP_CHECKSIG", 253 | "hex": "76a9146385e05f35ca874dddb45a3ca3a45120c84b203188ac", 254 | "reqSigs": 1, 255 | "type": "pubkeyhash", 256 | "addresses": [ 257 | "1A5EGtdiXTb2cnWYvjJZjM1cy5fBQggXL1" 258 | ] 259 | } 260 | }, 261 | { 262 | "value": 1.92000000, 263 | "n": 18, 264 | "scriptPubKey": { 265 | "asm": "OP_DUP OP_HASH160 acd5f88f4958dcbf89de52b3a5c7bea3ff85df63 OP_EQUALVERIFY OP_CHECKSIG", 266 | "hex": "76a914acd5f88f4958dcbf89de52b3a5c7bea3ff85df6388ac", 267 | "reqSigs": 1, 268 | "type": "pubkeyhash", 269 | "addresses": [ 270 | "1GksYxTQsaKuLEkeNzTzzKS9DDwCwksELF" 271 | ] 272 | } 273 | }, 274 | { 275 | "value": 1.04000000, 276 | "n": 19, 277 | "scriptPubKey": { 278 | "asm": "OP_DUP OP_HASH160 55cf6da1a8d381c7019c1d375ed36e2e65075f7d OP_EQUALVERIFY OP_CHECKSIG", 279 | "hex": "76a91455cf6da1a8d381c7019c1d375ed36e2e65075f7d88ac", 280 | "reqSigs": 1, 281 | "type": "pubkeyhash", 282 | "addresses": [ 283 | "18pivg2hxAGEHNSVMwXjpWMyKEJvFPW6gC" 284 | ] 285 | } 286 | }, 287 | { 288 | "value": 1.19000000, 289 | "n": 20, 290 | "scriptPubKey": { 291 | "asm": "OP_DUP OP_HASH160 7eeca8e4c523e16746e5a0a5e1ea77f41e88f7bd OP_EQUALVERIFY OP_CHECKSIG", 292 | "hex": "76a9147eeca8e4c523e16746e5a0a5e1ea77f41e88f7bd88ac", 293 | "reqSigs": 1, 294 | "type": "pubkeyhash", 295 | "addresses": [ 296 | "1Ca7eaWMo1ot3C5DRGDwx92hNpFePUrpzy" 297 | ] 298 | } 299 | }, 300 | { 301 | "value": 2.31000000, 302 | "n": 21, 303 | "scriptPubKey": { 304 | "asm": "OP_DUP OP_HASH160 360a69efc728b6d1ed5fe3f60f62dc8c1556cb97 OP_EQUALVERIFY OP_CHECKSIG", 305 | "hex": "76a914360a69efc728b6d1ed5fe3f60f62dc8c1556cb9788ac", 306 | "reqSigs": 1, 307 | "type": "pubkeyhash", 308 | "addresses": [ 309 | "15vjxvZHpjkc1Bor1TFJA7s34VFQpoQk2Z" 310 | ] 311 | } 312 | }, 313 | { 314 | "value": 5.17000000, 315 | "n": 22, 316 | "scriptPubKey": { 317 | "asm": "OP_DUP OP_HASH160 6b8e262762c6bbecd36d1ae14b770f86c9177390 OP_EQUALVERIFY OP_CHECKSIG", 318 | "hex": "76a9146b8e262762c6bbecd36d1ae14b770f86c917739088ac", 319 | "reqSigs": 1, 320 | "type": "pubkeyhash", 321 | "addresses": [ 322 | "1AohbDRZhMfEcgRjD6gs3m7B3NuLsdcJii" 323 | ] 324 | } 325 | }, 326 | { 327 | "value": 1.11000000, 328 | "n": 23, 329 | "scriptPubKey": { 330 | "asm": "OP_DUP OP_HASH160 87ea46e0f742b95b7ab51bdda5b644a522e5f785 OP_EQUALVERIFY OP_CHECKSIG", 331 | "hex": "76a91487ea46e0f742b95b7ab51bdda5b644a522e5f78588ac", 332 | "reqSigs": 1, 333 | "type": "pubkeyhash", 334 | "addresses": [ 335 | "1DPesjtQPqDsyyrgse8LsWUDvgSZGYU7CG" 336 | ] 337 | } 338 | }, 339 | { 340 | "value": 0.07000000, 341 | "n": 24, 342 | "scriptPubKey": { 343 | "asm": "OP_DUP OP_HASH160 7610422f995540b0914efba5b79a4fcd61b52862 OP_EQUALVERIFY OP_CHECKSIG", 344 | "hex": "76a9147610422f995540b0914efba5b79a4fcd61b5286288ac", 345 | "reqSigs": 1, 346 | "type": "pubkeyhash", 347 | "addresses": [ 348 | "1BmGDJM57bNST4j8WseghFVe7ViiNwej9K" 349 | ] 350 | } 351 | }, 352 | { 353 | "value": 0.02000000, 354 | "n": 25, 355 | "scriptPubKey": { 356 | "asm": "OP_DUP OP_HASH160 7ed0e5d65823ca4ea4bbdbfb92776cfe3239f215 OP_EQUALVERIFY OP_CHECKSIG", 357 | "hex": "76a9147ed0e5d65823ca4ea4bbdbfb92776cfe3239f21588ac", 358 | "reqSigs": 1, 359 | "type": "pubkeyhash", 360 | "addresses": [ 361 | "1CZYPeNe5KkyvbUaQQ2RKYHZNnAoi29FwL" 362 | ] 363 | } 364 | }, 365 | { 366 | "value": 0.10000000, 367 | "n": 26, 368 | "scriptPubKey": { 369 | "asm": "OP_DUP OP_HASH160 05ca39cfbba25b384182bf9e35bfbaea9eb38b04 OP_EQUALVERIFY OP_CHECKSIG", 370 | "hex": "76a91405ca39cfbba25b384182bf9e35bfbaea9eb38b0488ac", 371 | "reqSigs": 1, 372 | "type": "pubkeyhash", 373 | "addresses": [ 374 | "1Xcdre9pAipV9kiSrSgssEpQPAruzMFzr" 375 | ] 376 | } 377 | }, 378 | { 379 | "value": 0.09000000, 380 | "n": 27, 381 | "scriptPubKey": { 382 | "asm": "OP_DUP OP_HASH160 5e320e294012f86459676ff96cb0e99f5d42be42 OP_EQUALVERIFY OP_CHECKSIG", 383 | "hex": "76a9145e320e294012f86459676ff96cb0e99f5d42be4288ac", 384 | "reqSigs": 1, 385 | "type": "pubkeyhash", 386 | "addresses": [ 387 | "19b4UwKDvFnKcSbY8Q4CGtAMPkwXX2u8eK" 388 | ] 389 | } 390 | }, 391 | { 392 | "value": 1.13000000, 393 | "n": 28, 394 | "scriptPubKey": { 395 | "asm": "OP_DUP OP_HASH160 bb2a62f4f130d5d7c8f33802492457a70cc9f7d3 OP_EQUALVERIFY OP_CHECKSIG", 396 | "hex": "76a914bb2a62f4f130d5d7c8f33802492457a70cc9f7d388ac", 397 | "reqSigs": 1, 398 | "type": "pubkeyhash", 399 | "addresses": [ 400 | "1J4e8wbvFgfGZk1PPevWDv3L54iauYFPug" 401 | ] 402 | } 403 | }, 404 | { 405 | "value": 1.08000000, 406 | "n": 29, 407 | "scriptPubKey": { 408 | "asm": "OP_DUP OP_HASH160 30f8c38f48efd02636b27cb4f4c4d5cd9aead551 OP_EQUALVERIFY OP_CHECKSIG", 409 | "hex": "76a91430f8c38f48efd02636b27cb4f4c4d5cd9aead55188ac", 410 | "reqSigs": 1, 411 | "type": "pubkeyhash", 412 | "addresses": [ 413 | "15TwSdNEozxL9vYmJQEqmtxV6bzJSPbaQf" 414 | ] 415 | } 416 | }, 417 | { 418 | "value": 0.04000000, 419 | "n": 30, 420 | "scriptPubKey": { 421 | "asm": "OP_DUP OP_HASH160 a39fcf895f7c9fb88a6f19733c941e04bed5401e OP_EQUALVERIFY OP_CHECKSIG", 422 | "hex": "76a914a39fcf895f7c9fb88a6f19733c941e04bed5401e88ac", 423 | "reqSigs": 1, 424 | "type": "pubkeyhash", 425 | "addresses": [ 426 | "1FvAb7NDfcwQPTso8MLgx1Q4UZpVpYk3VE" 427 | ] 428 | } 429 | }, 430 | { 431 | "value": 1.02000000, 432 | "n": 31, 433 | "scriptPubKey": { 434 | "asm": "OP_DUP OP_HASH160 fc24668134da1e804db1e386ea8cb18f18bd4f88 OP_EQUALVERIFY OP_CHECKSIG", 435 | "hex": "76a914fc24668134da1e804db1e386ea8cb18f18bd4f8888ac", 436 | "reqSigs": 1, 437 | "type": "pubkeyhash", 438 | "addresses": [ 439 | "1PzCtVBzBFHX1L2gujLQ9RHJ77ZGkajkJc" 440 | ] 441 | } 442 | }, 443 | { 444 | "value": 1.48000000, 445 | "n": 32, 446 | "scriptPubKey": { 447 | "asm": "OP_DUP OP_HASH160 465941501190171805e96a5e8d8bc1794fd47378 OP_EQUALVERIFY OP_CHECKSIG", 448 | "hex": "76a914465941501190171805e96a5e8d8bc1794fd4737888ac", 449 | "reqSigs": 1, 450 | "type": "pubkeyhash", 451 | "addresses": [ 452 | "17QyDz2J54J58oUbRkV1TC8MdZXCKSTZcz" 453 | ] 454 | } 455 | }, 456 | { 457 | "value": 0.15000000, 458 | "n": 33, 459 | "scriptPubKey": { 460 | "asm": "OP_DUP OP_HASH160 4ea2cc288c1c871f9be60fb600c294b75fb83b40 OP_EQUALVERIFY OP_CHECKSIG", 461 | "hex": "76a9144ea2cc288c1c871f9be60fb600c294b75fb83b4088ac", 462 | "reqSigs": 1, 463 | "type": "pubkeyhash", 464 | "addresses": [ 465 | "18AnjLCGk6P5jtAUdcqHYv78Ny3LXXuD9u" 466 | ] 467 | } 468 | }, 469 | { 470 | "value": 0.01000000, 471 | "n": 34, 472 | "scriptPubKey": { 473 | "asm": "OP_DUP OP_HASH160 8d7991c145ef34234238f8d1d8898717faf8acb9 OP_EQUALVERIFY OP_CHECKSIG", 474 | "hex": "76a9148d7991c145ef34234238f8d1d8898717faf8acb988ac", 475 | "reqSigs": 1, 476 | "type": "pubkeyhash", 477 | "addresses": [ 478 | "1Du3uqRt1oMuHmESR6VE8pjkwtHSAsiHYQ" 479 | ] 480 | } 481 | }, 482 | { 483 | "value": 2.14000000, 484 | "n": 35, 485 | "scriptPubKey": { 486 | "asm": "OP_DUP OP_HASH160 8f9046b56fd50f8434b261bf96e83e7778c0b4f1 OP_EQUALVERIFY OP_CHECKSIG", 487 | "hex": "76a9148f9046b56fd50f8434b261bf96e83e7778c0b4f188ac", 488 | "reqSigs": 1, 489 | "type": "pubkeyhash", 490 | "addresses": [ 491 | "1E66TvG7JhhRNTNBgU57qmCyzzBb3CMbyV" 492 | ] 493 | } 494 | }, 495 | { 496 | "value": 0.03000000, 497 | "n": 36, 498 | "scriptPubKey": { 499 | "asm": "OP_DUP OP_HASH160 ca670e37b8170e91426f19669da92dca3dbd18aa OP_EQUALVERIFY OP_CHECKSIG", 500 | "hex": "76a914ca670e37b8170e91426f19669da92dca3dbd18aa88ac", 501 | "reqSigs": 1, 502 | "type": "pubkeyhash", 503 | "addresses": [ 504 | "1KTCxB6TNDrAdhaAgNdCPpd77eEQwNtHpG" 505 | ] 506 | } 507 | }, 508 | { 509 | "value": 3.00000000, 510 | "n": 37, 511 | "scriptPubKey": { 512 | "asm": "OP_DUP OP_HASH160 b79506588a14a0c0be9c4ad2a29c8ad58590ec1a OP_EQUALVERIFY OP_CHECKSIG", 513 | "hex": "76a914b79506588a14a0c0be9c4ad2a29c8ad58590ec1a88ac", 514 | "reqSigs": 1, 515 | "type": "pubkeyhash", 516 | "addresses": [ 517 | "1HjhBXfshDn73mKGLN9VAFDWD4vGTmbc3w" 518 | ] 519 | } 520 | }, 521 | { 522 | "value": 0.10000000, 523 | "n": 38, 524 | "scriptPubKey": { 525 | "asm": "OP_DUP OP_HASH160 aea6c870f027d74b0e064e1b119200c2b72075d6 OP_EQUALVERIFY OP_CHECKSIG", 526 | "hex": "76a914aea6c870f027d74b0e064e1b119200c2b72075d688ac", 527 | "reqSigs": 1, 528 | "type": "pubkeyhash", 529 | "addresses": [ 530 | "1GvUNfS6rwrjfMWGZQmCEi91KddTNNzxNh" 531 | ] 532 | } 533 | }, 534 | { 535 | "value": 1.18000000, 536 | "n": 39, 537 | "scriptPubKey": { 538 | "asm": "OP_DUP OP_HASH160 7f3a49e14b2e5861cb8218552fc7e67ac28a085b OP_EQUALVERIFY OP_CHECKSIG", 539 | "hex": "76a9147f3a49e14b2e5861cb8218552fc7e67ac28a085b88ac", 540 | "reqSigs": 1, 541 | "type": "pubkeyhash", 542 | "addresses": [ 543 | "1CbieKrCy6FKa9KhepNSbr14qFXxDr4VcC" 544 | ] 545 | } 546 | }, 547 | { 548 | "value": 3.99000000, 549 | "n": 40, 550 | "scriptPubKey": { 551 | "asm": "OP_DUP OP_HASH160 0da116fe94af16a4597ae4febd7fee0c6467ce64 OP_EQUALVERIFY OP_CHECKSIG", 552 | "hex": "76a9140da116fe94af16a4597ae4febd7fee0c6467ce6488ac", 553 | "reqSigs": 1, 554 | "type": "pubkeyhash", 555 | "addresses": [ 556 | "12F4mDEKZG9ggqLXvBDYceNEmMpxUxNv3d" 557 | ] 558 | } 559 | }, 560 | { 561 | "value": 5.19000000, 562 | "n": 41, 563 | "scriptPubKey": { 564 | "asm": "OP_DUP OP_HASH160 916698eefead5f75029d9ea56bf3a00104dc6151 OP_EQUALVERIFY OP_CHECKSIG", 565 | "hex": "76a914916698eefead5f75029d9ea56bf3a00104dc615188ac", 566 | "reqSigs": 1, 567 | "type": "pubkeyhash", 568 | "addresses": [ 569 | "1EFotQdsKSR1dbDRdH4iFbHmtVax1TjNBi" 570 | ] 571 | } 572 | }, 573 | { 574 | "value": 1.71000000, 575 | "n": 42, 576 | "scriptPubKey": { 577 | "asm": "OP_DUP OP_HASH160 67ad45dcfc30e5c92b827c7a0767705cab2fcca0 OP_EQUALVERIFY OP_CHECKSIG", 578 | "hex": "76a91467ad45dcfc30e5c92b827c7a0767705cab2fcca088ac", 579 | "reqSigs": 1, 580 | "type": "pubkeyhash", 581 | "addresses": [ 582 | "1ATCAyK9EM5hME5qQ58FUmL3v7JkEkhoLJ" 583 | ] 584 | } 585 | }, 586 | { 587 | "value": 0.23000000, 588 | "n": 43, 589 | "scriptPubKey": { 590 | "asm": "OP_DUP OP_HASH160 fa6a5019983b848e64f6858e23db27b4f171729b OP_EQUALVERIFY OP_CHECKSIG", 591 | "hex": "76a914fa6a5019983b848e64f6858e23db27b4f171729b88ac", 592 | "reqSigs": 1, 593 | "type": "pubkeyhash", 594 | "addresses": [ 595 | "1Pq5HhzU7wVQPhffPw55EGD7UxENTZ1RPc" 596 | ] 597 | } 598 | }, 599 | { 600 | "value": 0.90000000, 601 | "n": 44, 602 | "scriptPubKey": { 603 | "asm": "OP_DUP OP_HASH160 d88bba412363fd3986513038007b2d7b8ee02320 OP_EQUALVERIFY OP_CHECKSIG", 604 | "hex": "76a914d88bba412363fd3986513038007b2d7b8ee0232088ac", 605 | "reqSigs": 1, 606 | "type": "pubkeyhash", 607 | "addresses": [ 608 | "1LjzLspWYZHgBECKvrMKDY5myM2kkCrtKu" 609 | ] 610 | } 611 | }, 612 | { 613 | "value": 4.12000000, 614 | "n": 45, 615 | "scriptPubKey": { 616 | "asm": "OP_DUP OP_HASH160 19e4fe7403d4cdf4e7a44c080aab114520b1e88e OP_EQUALVERIFY OP_CHECKSIG", 617 | "hex": "76a91419e4fe7403d4cdf4e7a44c080aab114520b1e88e88ac", 618 | "reqSigs": 1, 619 | "type": "pubkeyhash", 620 | "addresses": [ 621 | "13MvDaLttfseTAJnSXkApAvHhvVpSFsPzX" 622 | ] 623 | } 624 | }, 625 | { 626 | "value": 1.04000000, 627 | "n": 46, 628 | "scriptPubKey": { 629 | "asm": "OP_DUP OP_HASH160 1b0ee86a1a9845766dc9d7da998d2ddea9d3dd85 OP_EQUALVERIFY OP_CHECKSIG", 630 | "hex": "76a9141b0ee86a1a9845766dc9d7da998d2ddea9d3dd8588ac", 631 | "reqSigs": 1, 632 | "type": "pubkeyhash", 633 | "addresses": [ 634 | "13U56zncvzmXFySZZgaQJSuzwEHeZJGwkG" 635 | ] 636 | } 637 | }, 638 | { 639 | "value": 10.74000000, 640 | "n": 47, 641 | "scriptPubKey": { 642 | "asm": "OP_DUP OP_HASH160 2dcaf496772a8da8ae6560a523eae02dd431ae30 OP_EQUALVERIFY OP_CHECKSIG", 643 | "hex": "76a9142dcaf496772a8da8ae6560a523eae02dd431ae3088ac", 644 | "reqSigs": 1, 645 | "type": "pubkeyhash", 646 | "addresses": [ 647 | "15B8YDtb8AUvSKmWXDaQ44ox6sjLoamM1e" 648 | ] 649 | } 650 | }, 651 | { 652 | "value": 11.62000000, 653 | "n": 48, 654 | "scriptPubKey": { 655 | "asm": "OP_DUP OP_HASH160 acfe60621ae22430f78bd3d2437de0f3ec39b9b1 OP_EQUALVERIFY OP_CHECKSIG", 656 | "hex": "76a914acfe60621ae22430f78bd3d2437de0f3ec39b9b188ac", 657 | "reqSigs": 1, 658 | "type": "pubkeyhash", 659 | "addresses": [ 660 | "1GmhxPFkKN7fDatoXVwg9GUvsE7yukfQHw" 661 | ] 662 | } 663 | }, 664 | { 665 | "value": 1.00000000, 666 | "n": 49, 667 | "scriptPubKey": { 668 | "asm": "OP_DUP OP_HASH160 daa2fb05680d1c01bccd29d8d545e4732f960f16 OP_EQUALVERIFY OP_CHECKSIG", 669 | "hex": "76a914daa2fb05680d1c01bccd29d8d545e4732f960f1688ac", 670 | "reqSigs": 1, 671 | "type": "pubkeyhash", 672 | "addresses": [ 673 | "1Lw3YuZeYX3otNK1LFgHTao6azEQrgTnnC" 674 | ] 675 | } 676 | }, 677 | { 678 | "value": 1.14000000, 679 | "n": 50, 680 | "scriptPubKey": { 681 | "asm": "OP_DUP OP_HASH160 c3ba83d99a86f6aa65f57baeaeb7ef45faf4bfee OP_EQUALVERIFY OP_CHECKSIG", 682 | "hex": "76a914c3ba83d99a86f6aa65f57baeaeb7ef45faf4bfee88ac", 683 | "reqSigs": 1, 684 | "type": "pubkeyhash", 685 | "addresses": [ 686 | "1JqvChYaN24Xj5gFLQ3LSfnqhFnZ9T7yfu" 687 | ] 688 | } 689 | }, 690 | { 691 | "value": 1.03000000, 692 | "n": 51, 693 | "scriptPubKey": { 694 | "asm": "OP_DUP OP_HASH160 e76d750418680b018b921e20463927d1909a69d9 OP_EQUALVERIFY OP_CHECKSIG", 695 | "hex": "76a914e76d750418680b018b921e20463927d1909a69d988ac", 696 | "reqSigs": 1, 697 | "type": "pubkeyhash", 698 | "addresses": [ 699 | "1N6gDXTCKmQuxyibDNxufjr4gfPHduSQ2r" 700 | ] 701 | } 702 | }, 703 | { 704 | "value": 0.50000000, 705 | "n": 52, 706 | "scriptPubKey": { 707 | "asm": "OP_DUP OP_HASH160 2168960c49d62a680abc9104bbf4a9c6253463ba OP_EQUALVERIFY OP_CHECKSIG", 708 | "hex": "76a9142168960c49d62a680abc9104bbf4a9c6253463ba88ac", 709 | "reqSigs": 1, 710 | "type": "pubkeyhash", 711 | "addresses": [ 712 | "143eb6uLSwcJ8HJJzR9ffPmETBqEEdcL38" 713 | ] 714 | } 715 | }, 716 | { 717 | "value": 0.21000000, 718 | "n": 53, 719 | "scriptPubKey": { 720 | "asm": "OP_DUP OP_HASH160 ff258a3d0c3e7a3a2f70ce17988e46a23ce40c7f OP_EQUALVERIFY OP_CHECKSIG", 721 | "hex": "76a914ff258a3d0c3e7a3a2f70ce17988e46a23ce40c7f88ac", 722 | "reqSigs": 1, 723 | "type": "pubkeyhash", 724 | "addresses": [ 725 | "1QG6HEzdL1hg7ZTj465UH74Go7FxruSPVy" 726 | ] 727 | } 728 | }, 729 | { 730 | "value": 5.12000000, 731 | "n": 54, 732 | "scriptPubKey": { 733 | "asm": "OP_DUP OP_HASH160 fd5680eaa0214d4d86cb87654600b7113e65236f OP_EQUALVERIFY OP_CHECKSIG", 734 | "hex": "76a914fd5680eaa0214d4d86cb87654600b7113e65236f88ac", 735 | "reqSigs": 1, 736 | "type": "pubkeyhash", 737 | "addresses": [ 738 | "1Q6XaucrfM7NKZH1KyM4sYFvmwX3gs7jDv" 739 | ] 740 | } 741 | } 742 | ], 743 | "blockhash": "0000000000005417e8026d94c1d4b7a22777dbc5ad9ffe6d0edfad70fcee92a1", 744 | "confirmations": 220061, 745 | "time": 1303371183, 746 | "blocktime": 1303371183 747 | } 748 | -------------------------------------------------------------------------------- /tests/data/tx_e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "01000000010b6072b386d4a773235237f64c1126ac3b240c84b917a3909ba1c43ded5f51f4000000008c493046022100bb1ad26df930a51cce110cf44f7a48c3c561fd977500b1ae5d6b6fd13d0b3f4a022100c5b42951acedff14abba2736fd574bdb465f3e6f8da12e2c5303954aca7f78f3014104a7135bfe824c97ecc01ec7d7e336185c81e2aa2c41ab175407c09484ce9694b44953fcb751206564a9c24dd094d42fdbfdd5aad3e063ce6af4cfaaea4ea14fbbffffffff0140420f00000000001976a91439aa3d569e06a1d7926dc4be1193c99bf2eb9ee088ac00000000", 3 | "txid": "e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "f4515fed3dc4a19b90a317b9840c243bac26114cf637522373a7d486b372600b", 9 | "vout": 0, 10 | "scriptSig": { 11 | "asm": "3046022100bb1ad26df930a51cce110cf44f7a48c3c561fd977500b1ae5d6b6fd13d0b3f4a022100c5b42951acedff14abba2736fd574bdb465f3e6f8da12e2c5303954aca7f78f301 04a7135bfe824c97ecc01ec7d7e336185c81e2aa2c41ab175407c09484ce9694b44953fcb751206564a9c24dd094d42fdbfdd5aad3e063ce6af4cfaaea4ea14fbb", 12 | "hex": "493046022100bb1ad26df930a51cce110cf44f7a48c3c561fd977500b1ae5d6b6fd13d0b3f4a022100c5b42951acedff14abba2736fd574bdb465f3e6f8da12e2c5303954aca7f78f3014104a7135bfe824c97ecc01ec7d7e336185c81e2aa2c41ab175407c09484ce9694b44953fcb751206564a9c24dd094d42fdbfdd5aad3e063ce6af4cfaaea4ea14fbb" 13 | }, 14 | "sequence": 4294967295 15 | } 16 | ], 17 | "vout": [ 18 | { 19 | "value": 0.01000000, 20 | "n": 0, 21 | "scriptPubKey": { 22 | "asm": "OP_DUP OP_HASH160 39aa3d569e06a1d7926dc4be1193c99bf2eb9ee0 OP_EQUALVERIFY OP_CHECKSIG", 23 | "hex": "76a91439aa3d569e06a1d7926dc4be1193c99bf2eb9ee088ac", 24 | "reqSigs": 1, 25 | "type": "pubkeyhash", 26 | "addresses": [ 27 | "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ" 28 | ] 29 | } 30 | } 31 | ], 32 | "blockhash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506", 33 | "confirmations": 233539, 34 | "time": 1293623863, 35 | "blocktime": 1293623863 36 | } 37 | -------------------------------------------------------------------------------- /tests/data/tx_f4515fed3dc4a19b90a317b9840c243bac26114cf637522373a7d486b372600b.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "0100000001e7fcf39ee6b86f1595c55b16b60bf4f297988cb9519f5d42597e7fb721e591c6010000008b483045022100ac572b43e78089851202cfd9386750b08afc175318c537f04eb364bf5a0070d402203f0e829d4baea982feaf987cb9f14c85097d2fbe89fba3f283f6925b3214a97e0141048922fa4dc891f9bb39f315635c03e60e019ff9ec1559c8b581324b4c3b7589a57550f9b0b80bc72d0f959fddf6ca65f07223c37a8499076bd7027ae5c325fac5ffffffff0140420f00000000001976a914c4eb47ecfdcf609a1848ee79acc2fa49d3caad7088ac00000000", 3 | "txid": "f4515fed3dc4a19b90a317b9840c243bac26114cf637522373a7d486b372600b", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "c691e521b77f7e59425d9f51b98c9897f2f40bb6165bc595156fb8e69ef3fce7", 9 | "vout": 1, 10 | "scriptSig": { 11 | "asm": "3045022100ac572b43e78089851202cfd9386750b08afc175318c537f04eb364bf5a0070d402203f0e829d4baea982feaf987cb9f14c85097d2fbe89fba3f283f6925b3214a97e01 048922fa4dc891f9bb39f315635c03e60e019ff9ec1559c8b581324b4c3b7589a57550f9b0b80bc72d0f959fddf6ca65f07223c37a8499076bd7027ae5c325fac5", 12 | "hex": "483045022100ac572b43e78089851202cfd9386750b08afc175318c537f04eb364bf5a0070d402203f0e829d4baea982feaf987cb9f14c85097d2fbe89fba3f283f6925b3214a97e0141048922fa4dc891f9bb39f315635c03e60e019ff9ec1559c8b581324b4c3b7589a57550f9b0b80bc72d0f959fddf6ca65f07223c37a8499076bd7027ae5c325fac5" 13 | }, 14 | "sequence": 4294967295 15 | } 16 | ], 17 | "vout": [ 18 | { 19 | "value": 0.01000000, 20 | "n": 0, 21 | "scriptPubKey": { 22 | "asm": "OP_DUP OP_HASH160 c4eb47ecfdcf609a1848ee79acc2fa49d3caad70 OP_EQUALVERIFY OP_CHECKSIG", 23 | "hex": "76a914c4eb47ecfdcf609a1848ee79acc2fa49d3caad7088ac", 24 | "reqSigs": 1, 25 | "type": "pubkeyhash", 26 | "addresses": [ 27 | "1JxDJCyWNakZ5kECKdCU9Zka6mh34mZ7B2" 28 | ] 29 | } 30 | } 31 | ], 32 | "blockhash": "0000000000004273d89d3f220a9d3dbd45a4fd1c028b51e170f478da11352187", 33 | "confirmations": 233728, 34 | "time": 1293528528, 35 | "blocktime": 1293528528 36 | } 37 | -------------------------------------------------------------------------------- /tests/data/tx_fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4.json: -------------------------------------------------------------------------------- 1 | { 2 | "hex": "0100000001032e38e9c0a84c6046d687d10556dcacc41d275ec55fc00779ac88fdf357a187000000008c493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3ffffffff0200e32321000000001976a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac000fe208010000001976a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac00000000", 3 | "txid": "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4", 4 | "version": 1, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03", 9 | "vout": 0, 10 | "scriptSig": { 11 | "asm": "3046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af774801 04f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3", 12 | "hex": "493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3" 13 | }, 14 | "sequence": 4294967295 15 | } 16 | ], 17 | "vout": [ 18 | { 19 | "value": 5.56000000, 20 | "n": 0, 21 | "scriptPubKey": { 22 | "asm": "OP_DUP OP_HASH160 c398efa9c392ba6013c5e04ee729755ef7f58b32 OP_EQUALVERIFY OP_CHECKSIG", 23 | "hex": "76a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac", 24 | "reqSigs": 1, 25 | "type": "pubkeyhash", 26 | "addresses": [ 27 | "1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn" 28 | ] 29 | } 30 | }, 31 | { 32 | "value": 44.44000000, 33 | "n": 1, 34 | "scriptPubKey": { 35 | "asm": "OP_DUP OP_HASH160 948c765a6914d43f2a7ac177da2c2f6b52de3d7c OP_EQUALVERIFY OP_CHECKSIG", 36 | "hex": "76a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac", 37 | "reqSigs": 1, 38 | "type": "pubkeyhash", 39 | "addresses": [ 40 | "1EYTGtG4LnFfiMvjJdsU7GMGCQvsRSjYhx" 41 | ] 42 | } 43 | } 44 | ], 45 | "blockhash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506", 46 | "confirmations": 233539, 47 | "time": 1293623863, 48 | "blocktime": 1293623863 49 | } 50 | -------------------------------------------------------------------------------- /tests/rpc_mock.py: -------------------------------------------------------------------------------- 1 | from bitcoingraph.bitcoind import BitcoinProxy, BitcoindException 2 | 3 | from pathlib import Path 4 | 5 | import json 6 | 7 | 8 | TEST_DATA_PATH = "tests/data" 9 | 10 | 11 | class BitcoinProxyMock(BitcoinProxy): 12 | 13 | def __init__(self, host=None, port=None): 14 | super().__init__(host, port) 15 | self.heights = {} 16 | self.blocks = {} 17 | self.txs = {} 18 | self.load_testdata() 19 | 20 | # Load test data into local dicts 21 | def load_testdata(self): 22 | p = Path(TEST_DATA_PATH) 23 | files = [x for x in p.iterdir() 24 | if x.is_file() and x.name.endswith('json')] 25 | for f in files: 26 | if f.name.startswith("block"): 27 | height = f.name[6:-5] 28 | with f.open() as jf: 29 | raw_block = json.load(jf) 30 | block_hash = raw_block['hash'] 31 | self.heights[int(height)] = block_hash 32 | self.blocks[block_hash] = raw_block 33 | elif f.name.startswith("tx"): 34 | tx_hash = f.name[3:-5] 35 | with f.open() as jf: 36 | raw_block = json.load(jf) 37 | self.txs[tx_hash] = raw_block 38 | 39 | # Override production proxy methods 40 | 41 | def getblock(self, block_hash): 42 | if block_hash not in self.blocks: 43 | raise BitcoindException("Unknown block", block_hash) 44 | else: 45 | return self.blocks[block_hash] 46 | 47 | def getblockcount(self): 48 | return max(self.heights.keys()) 49 | 50 | def getblockhash(self, block_height): 51 | if block_height not in self.heights: 52 | raise BitcoindException("Unknown height", block_height) 53 | else: 54 | return self.heights[block_height] 55 | 56 | def getinfo(self): 57 | print("No info") 58 | 59 | def getrawtransaction(self, tx_id, verbose=1): 60 | if tx_id not in self.txs: 61 | raise BitcoindException("Unknown transaction", tx_id) 62 | else: 63 | return self.txs[tx_id] 64 | 65 | def getrawtransactions(self, tx_ids, verbose=1): 66 | results = [] 67 | for tx_id in tx_ids: 68 | results.append(self.getrawtransaction(tx_id, verbose)) 69 | return results 70 | -------------------------------------------------------------------------------- /tests/test_blockchain.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tests.rpc_mock import BitcoinProxyMock 4 | 5 | from bitcoingraph.blockchain import Blockchain, BlockchainException 6 | from bitcoingraph.model import Input, Output 7 | 8 | BH1 = "000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250" 9 | BH1_HEIGHT = 99999 10 | BH2 = "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506" 11 | BH2_HEIGHT = 100000 12 | BH3 = "00000000000080b66c911bd5ba14a74260057311eaeb1982802f7010f1a9f090" 13 | BH3_HEIGHT = 100001 14 | 15 | # standard transactions 16 | TX1 = "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87" 17 | TX2 = "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4" 18 | TX3 = "87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03" 19 | 20 | # transaction with unknown output 21 | TXE = "a288fec5559c3f73fd3d93db8e8460562ebfe2fcf04a5114e8d0f2920a6270dc" 22 | 23 | # transaction with multiple in and outputs 24 | TXM = "d5f013abf2cf4af6d68bcacd675c91f19bab5b7103b4ac2f4941686eb47da1f0" 25 | 26 | 27 | class TestBlockchainObject(unittest.TestCase): 28 | 29 | def setUp(self): 30 | self.bitcoin_proxy = BitcoinProxyMock() 31 | self.blockchain = Blockchain(self.bitcoin_proxy) 32 | 33 | def test_init(self): 34 | self.assertIsNotNone(self.blockchain) 35 | self.assertIsNotNone(self.bitcoin_proxy) 36 | 37 | 38 | class TestBlock(TestBlockchainObject): 39 | 40 | def test_time(self): 41 | block = self.blockchain.get_block_by_hash(BH1) 42 | self.assertEqual(block.timestamp, 1293623731) 43 | 44 | def test_time_as_dt(self): 45 | block = self.blockchain.get_block_by_hash(BH1) 46 | self.assertEqual(block.formatted_time(), "2010-12-29 11:55:31") 47 | 48 | def test_height(self): 49 | block = self.blockchain.get_block_by_hash(BH1) 50 | self.assertEqual(block.height, BH1_HEIGHT) 51 | 52 | def test_hash(self): 53 | block = self.blockchain.get_block_by_hash(BH1) 54 | self.assertEqual(block.hash, BH1) 55 | 56 | def test_nextblockhash(self): 57 | block = self.blockchain.get_block_by_hash(BH1) 58 | self.assertTrue(block.has_next_block()) 59 | block = self.blockchain.get_block_by_hash(BH3) 60 | self.assertFalse(block.has_next_block()) 61 | 62 | def hasnextblock(self): 63 | block = self.blockchain.get_block_by_hash(BH1) 64 | self.assertTrue(block.has_next_block()) 65 | block = self.blockchain.get_block_by_hash(BH3) 66 | self.assertFalse(block.has_next_block()) 67 | 68 | def test_nextblock(self): 69 | block = self.blockchain.get_block_by_hash(BH1) 70 | self.assertEqual(block.next_block.height, BH2_HEIGHT) 71 | block = self.blockchain.get_block_by_hash(BH3) 72 | self.assertIsNone(block.next_block) 73 | 74 | def test_tx_count(self): 75 | block = self.blockchain.get_block_by_hash(BH1) 76 | self.assertEqual(len(block.transactions), 1) 77 | block = self.blockchain.get_block_by_hash(BH2) 78 | self.assertEqual(len(block.transactions), 4) 79 | 80 | def test_tx_ids(self): 81 | block = self.blockchain.get_block_by_hash(BH2) 82 | self.assertTrue(TX1 in [transaction.txid for transaction in block.transactions]) 83 | 84 | def test_transactions(self): 85 | block = self.blockchain.get_block_by_hash(BH1) 86 | txs = [tx for tx in block.transactions] 87 | self.assertEqual(len(txs), 1) 88 | for tx in txs: 89 | self.assertIsNotNone(tx.txid) 90 | block = self.blockchain.get_block_by_hash(BH2) 91 | txs = [tx for tx in block.transactions] 92 | self.assertEqual(len(txs), 4) 93 | for tx in txs: 94 | self.assertIsNotNone(tx.txid) 95 | 96 | 97 | class TestTxInput(TestBlockchainObject): 98 | 99 | def test_is_coinbase(self): 100 | tx = self.blockchain.get_transaction(TX1) 101 | tx_input = tx.inputs[0] 102 | self.assertTrue(tx_input.is_coinbase) 103 | 104 | def test_is_not_coinbase(self): 105 | tx = self.blockchain.get_transaction(TX2) 106 | tx_input = tx.inputs[0] 107 | self.assertFalse(tx_input.is_coinbase) 108 | 109 | def test_prev_tx_hash(self): 110 | tx = self.blockchain.get_transaction(TX2) 111 | tx_input = tx.inputs[0] 112 | self.assertEqual(tx_input.output_reference['txid'], TX3) 113 | 114 | def test_prev_tx_coinbase(self): 115 | tx = self.blockchain.get_transaction(TX1) 116 | tx_input = tx.inputs[0] 117 | self.assertIsNone(tx_input.output_reference) 118 | 119 | def test_tx_output_index(self): 120 | tx = self.blockchain.get_transaction(TX2) 121 | tx_input = tx.inputs[0] 122 | self.assertEqual(tx_input.output_reference['vout'], 0) 123 | 124 | def test_prev_tx_output(self): 125 | tx = self.blockchain.get_transaction(TX2) 126 | tx_input = tx.inputs[0] 127 | prev_tx_output = tx_input.output 128 | self.assertIsNotNone(prev_tx_output) 129 | 130 | def test_addresses(self): 131 | tx = self.blockchain.get_transaction(TX2) 132 | tx_input = tx.inputs[0] 133 | self.assertEqual("1BNwxHGaFbeUBitpjy2AsKpJ29Ybxntqvb", 134 | tx_input.output.addresses[0]) 135 | 136 | 137 | class TestTxOutput(TestBlockchainObject): 138 | 139 | def test_index(self): 140 | tx = self.blockchain.get_transaction(TX2) 141 | self.assertEqual(0, tx.outputs[0].index) 142 | self.assertEqual(1, tx.outputs[1].index) 143 | 144 | def test_value(self): 145 | tx = self.blockchain.get_transaction(TX2) 146 | self.assertEqual(5.56000000, tx.outputs[0].value) 147 | self.assertEqual(44.44000000, tx.outputs[1].value) 148 | 149 | def test_addresses(self): 150 | tx = self.blockchain.get_transaction(TX2) 151 | self.assertEqual("1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn", 152 | tx.outputs[0].addresses[0]) 153 | self.assertEqual("1EYTGtG4LnFfiMvjJdsU7GMGCQvsRSjYhx", 154 | tx.outputs[1].addresses[0]) 155 | 156 | def test_empty_addresses(self): 157 | """ 158 | Test if empty list is return when no output addresses are present. 159 | """ 160 | tx = self.blockchain.get_transaction(TXE) 161 | self.assertEqual(["152hHAq6kHoLw2FCT8G37uLEts6oFVjZKt"], 162 | tx.outputs[0].addresses) 163 | self.assertFalse(tx.outputs[1].addresses) 164 | 165 | 166 | class TestTransaction(TestBlockchainObject): 167 | 168 | def test_blocktime(self): 169 | tx = self.blockchain.get_transaction(TX1) 170 | self.assertEqual(tx.block.timestamp, 1293623863) 171 | 172 | def test_blocktime_as_dt(self): 173 | tx = self.blockchain.get_transaction(TX1) 174 | self.assertEqual(tx.block.formatted_time(), "2010-12-29 11:57:43") 175 | 176 | def test_id(self): 177 | tx = self.blockchain.get_transaction(TX1) 178 | self.assertEqual(tx.txid, TX1) 179 | 180 | def test_get_input_count(self): 181 | tx = self.blockchain.get_transaction(TX1) 182 | self.assertEqual(len(tx.inputs), 1) 183 | tx = self.blockchain.get_transaction(TX2) 184 | self.assertEqual(len(tx.inputs), 1) 185 | 186 | def test_get_inputs(self): 187 | tx = self.blockchain.get_transaction(TX1) 188 | for tx_input in tx.inputs: 189 | self.assertIsInstance(tx_input, Input) 190 | 191 | def test_is_coinbase_tx(self): 192 | self.assertTrue(self.blockchain.get_transaction(TX1).is_coinbase()) 193 | self.assertFalse(self.blockchain.get_transaction(TX2).is_coinbase()) 194 | 195 | def test_get_output_count(self): 196 | tx = self.blockchain.get_transaction(TX1) 197 | self.assertEqual(len(tx.outputs), 1) 198 | tx = self.blockchain.get_transaction(TX2) 199 | self.assertEqual(len(tx.outputs), 2) 200 | 201 | def test_get_outputs(self): 202 | tx = self.blockchain.get_transaction(TX1) 203 | for tx_output in tx.outputs: 204 | self.assertIsInstance(tx_output, Output) 205 | 206 | 207 | class TestBlockchain(TestBlockchainObject): 208 | 209 | def test_get_block_by_hash(self): 210 | block = self.blockchain.get_block_by_hash(BH1) 211 | self.assertEqual(block.hash, BH1) 212 | 213 | def test_get_block_by_height(self): 214 | block = self.blockchain.get_block_by_height(BH1_HEIGHT) 215 | self.assertEqual(block.height, BH1_HEIGHT) 216 | 217 | def test_get_blocks_in_range(self): 218 | blocks = [block for block in self.blockchain.get_blocks_in_range( 219 | 99999, 100001)] 220 | self.assertEqual(len(blocks), 3) 221 | self.assertEqual(blocks[0].height, 99999) 222 | self.assertEqual(blocks[1].height, 100000) 223 | self.assertEqual(blocks[2].height, 100001) 224 | 225 | def test_get_transaction(self): 226 | tx = self.blockchain.get_transaction(TX1) 227 | self.assertEqual(tx.txid, TX1) 228 | 229 | def test_get_transactions(self): 230 | tx_ids = [TX1, TX2] 231 | txs = self.blockchain.get_transactions(tx_ids) 232 | self.assertEqual(2, len(txs)) 233 | 234 | def test_get_max_blockheight(self): 235 | max_height = self.blockchain.get_max_block_height() 236 | self.assertEqual(max_height, 100001) 237 | 238 | def test_exceptions(self): 239 | with self.assertRaises(BlockchainException) as cm: 240 | self.blockchain.get_block_by_hash("aa") 241 | self.assertEqual("Cannot retrieve block aa", cm.exception.msg) 242 | 243 | with self.assertRaises(BlockchainException) as cm: 244 | self.blockchain.get_block_by_height(123) 245 | self.assertEqual("Cannot retrieve block with height 123", 246 | cm.exception.msg) 247 | 248 | with self.assertRaises(BlockchainException) as cm: 249 | self.blockchain.get_transaction("bb") 250 | self.assertEqual("Cannot retrieve transaction with id bb", 251 | cm.exception.msg) 252 | -------------------------------------------------------------------------------- /utils/identities_blockchain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from bs4 import BeautifulSoup 4 | import csv 5 | import requests 6 | import urllib 7 | 8 | 9 | def scrape_page(writer, list_key, offset): 10 | r = requests.get('https://blockchain.info/de/tags', 11 | params={'filter': list_key, 'offset': offset}) 12 | soup = BeautifulSoup(r.text, 'html.parser') 13 | for tr in soup.tbody.find_all('tr'): 14 | if tr.img['src'].endswith('green_tick.png'): 15 | tds = tr.find_all('td') 16 | address = tds[0].get_text() 17 | tag = tds[1].get_text() 18 | link = urllib.parse.unquote(tds[2].a['href'].rpartition('=')[2]) 19 | writer.writerow([address, tag, link]) 20 | page_links = soup.find_all('div', class_='pagination')[0].find_all('a') 21 | last_offset = int(page_links[-2]['href'].rpartition('=')[2]) 22 | return last_offset 23 | 24 | 25 | with open('identities.csv', 'w') as identities_file: 26 | identities_writer = csv.writer(identities_file) 27 | identities_writer.writerow(['address', 'tag', 'link']) 28 | for list_key in [8, 16, 2, 4]: 29 | last_offset = scrape_page(identities_writer, list_key, 0) 30 | for off in range(200, last_offset + 1, 200): 31 | print('(list {}) scrape identity {} of {}'.format(list_key, off, last_offset)) 32 | scrape_page(identities_writer, list_key, off) 33 | -------------------------------------------------------------------------------- /utils/identities_pools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import csv 4 | import requests 5 | 6 | 7 | def scrape_page(writer): 8 | url = 'https://raw.githubusercontent.com/blockchain/Blockchain-Known-Pools/master/pools.json' 9 | r = requests.get(url) 10 | payout_addresses = r.json()['payout_addresses'] 11 | for payout_address, info in payout_addresses.items(): 12 | address = payout_address 13 | tag = info['name'] 14 | link = info['link'] 15 | writer.writerow([address, tag, link]) 16 | 17 | 18 | with open('identities_pools.csv', 'w') as identities_file: 19 | identities_writer = csv.writer(identities_file) 20 | identities_writer.writerow(['address', 'tag', 'link']) 21 | scrape_page(identities_writer) 22 | --------------------------------------------------------------------------------