├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── demo.gif ├── package-lock.json ├── package.json ├── src ├── api.js ├── bitcoin-node-connection.js ├── bitcoin-rpc.js ├── bitcoin-zmq.js ├── bus.js ├── config.js ├── crawler.js ├── database.js ├── direct-server-worker.js ├── direct-server.js ├── downloader.js ├── executor.js ├── index.js ├── indexer.js ├── mattercloud.js ├── planaria.js ├── retry-tx.js ├── run-connect.js ├── server.js └── worker.js └── test ├── bitcoin-node-connection-test.js ├── crawler.js ├── index.js ├── indexer.js ├── server.js └── txns.json /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: push 4 | 5 | jobs: 6 | cover_tests: 7 | name: Cover Tests 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 10 10 | steps: 11 | - name: Setup Node 10 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: '10.x' 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Cache Dependencies 20 | id: cache-node-modules 21 | uses: actions/cache@v1 22 | env: 23 | cache-version: 1 24 | with: 25 | path: node_modules 26 | key: v${{ env.cache-version }}-node10-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 27 | 28 | - name: Install 29 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 30 | run: | 31 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.AUTH_TOKEN }}" > ~/.npmrc 32 | npm ci 33 | 34 | - name: Lint 35 | run: npx standard 36 | 37 | - name: Cover Tests 38 | run: 39 | npm run test:cover 40 | 41 | - name: Publish Coverage 42 | uses: codecov/codecov-action@v1 43 | with: 44 | token: ${{ secrets.CODECOV_TOKEN }} 45 | file: coverage/lcov.info 46 | 47 | node_12_tests: 48 | name: Node 12 Tests 49 | runs-on: ubuntu-latest 50 | timeout-minutes: 10 51 | steps: 52 | - name: Setup Node 12 53 | uses: actions/setup-node@v1 54 | with: 55 | node-version: '12.x' 56 | 57 | - name: Checkout 58 | uses: actions/checkout@v2 59 | 60 | - name: Cache Dependencies 61 | id: cache-node-modules 62 | uses: actions/cache@v1 63 | env: 64 | cache-version: 1 65 | with: 66 | path: node_modules 67 | key: v${{ env.cache-version }}-node12-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 68 | 69 | - name: Install 70 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 71 | run: | 72 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.AUTH_TOKEN }}" > ~/.npmrc 73 | npm ci 74 | 75 | - name: Test Node 76 | run: npm run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/target/** 3 | .env 4 | .nyc_output 5 | coverage 6 | run.db* 7 | .idea 8 | *.sqlite3 9 | .runcache -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | test 3 | .env 4 | .nyc_output 5 | coverage 6 | run.db* 7 | *.sqlite3 8 | .idea 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Run Interactive, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Run-DB 2 | 3 | [![tests](https://github.com/runonbitcoin/run-db/workflows/tests/badge.svg)](https://github.com/runonbitcoin/run-db/actions) [![codecov](https://codecov.io/gh/runonbitcoin/run-db/branch/master/graph/badge.svg?token=auXAJR3INN)](https://codecov.io/gh/runonbitcoin/run-db) 4 | 5 | ![](demo.gif) 6 | 7 | Crawls the blockchain and indexes RUN state. 8 | 9 | Using Run-DB, you can self-host the State APIs that Run uses to work well. 10 | 11 | Use Run-DB to: 12 | - Operate a State Server to improve RUN performance by pre-loading jigs 13 | - Query balances, volume, history, and other information across many users and contracts 14 | - Blacklist individual transactions and their descendants in your app 15 | - Create your own local database of transactions your app uses 16 | 17 | ## Requirements 18 | 19 | Node 10+ 20 | 21 | ## Getting started 22 | 23 | 1. Install `npm run install` 24 | 2. Download a db snapshot: `wget https://run.network/run-db-snapshots/main/latest -O run.db` (*optional*) 25 | 3. Run `npm run start` 26 | 4. Install a trustlist: `curl -s https://api.run.network/v1/main/trust | curl -H "Content-Type: application/json" -X POST -d @- http://localhost:8000/trust` (*optional*) 27 | 28 | **Note**: For testnet, you may use `test` in place of `main` in the above commands. 29 | 30 | ## Use with your App Server 31 | 32 | Setup your server's `Run` instance as follows: 33 | 34 | ```javascript 35 | const client = true 36 | const state = new Run.plugins.RunDB('http://localhost:8000') 37 | const trust = ['state'] 38 | const run = new Run({ client, state, trust }) 39 | ``` 40 | 41 | Client mode makes Run-DB the source of truth for your server for all jig information. RUN will not load jigs that are not in your database, and your inventory will only be populated by jig UTXOs known to your database. 42 | 43 | Setting trust to `'state'` makes Run use your database for its trustlist too. This means you only have to setup trust in one place using: 44 | 45 | ``` 46 | curl -X POST localhost:8000/trust/ 47 | ``` 48 | 49 | You may also want to run additional instance of Run-DB in `SERVE_ONLY` mode. That allows you to have an writer that crawls transactions and puts data into the database, and multiple readers that serve your application servers. 50 | 51 | ## Use with a Browser or Mobile Client 52 | 53 | The same approach taken for servers can be used to improve performance of client `Run` instances. You should expose your Run-DB endpoints on a public or private domain rather than connect to `localhost`. If your client connections are not authenticated, be sure to only expose the GET endpoints and never the POST or DELETE endpoints, and use HTTPS to prevent MITM attacks. 54 | 55 | ## Configuration 56 | 57 | Create a .env file or set the following environment variables before running to configure the DB. 58 | 59 | | Name | Description | Default | 60 | | ---- | ----------- | ------- | 61 | | **API**| mattercloud, planaria, bitcoin-node, run, or none | mattercloud 62 | | **MATTERCLOUD_KEY** | Mattercloud API key | undefined 63 | | **PLANARIA_TOKEN** | Planaria API key | undefined 64 | | **ZMQ_URL** | Only for bitcoin-node. ZMQ tcp url | null 65 | | **RPC_URL** | Only for bitcoin-node. bitcoin RPC http url | null 66 | | **NETWORK** | Bitcoin network (main or test) | main 67 | | **DB** | Database file | run.db 68 | | **PORT** | Port used for the REST server | randomly generated 69 | | **WORKERS** | Number of threads used to index | 4 70 | | **FETCH_LIMIT** | Number of parallel downloads | 20 71 | | **START_HEIGHT** | Block height to start indexing | block shortly before sep 2020 72 | | **TIMEOUT** | Network timeout in milliseconds | 10000 73 | | **MEMPOOL_EXPIRATION** | Seconds until transactions are removed from the mempool | 86400 74 | | **DEFAULT_TRUSTLIST** | Comma-separated values of trusted txids | predefined trustlist 75 | | **SERVE_ONLY** | Whether to only serve data and not index transactions | false 76 | 77 | ### Connecting with a bitcoin node 78 | 79 | During development is useful to connect to a local node. In order 80 | to do this you need to provide RUN-db with access to a bitcoin node 81 | trough RPC and ZMQ. 82 | 83 | ``` 84 | export API="bitcoin-node" 85 | export ZMQ_URL="tcp://your-node-uri:port" 86 | export RPC_URL="http://user:password@your-node-uri:port" 87 | ``` 88 | 89 | The only zmq message is needed is `rawtx`. ZMQ is only used to get 90 | the new transactions in the mempool. 91 | 92 | Direct connection with the node is tested in regtest and testnet, but 93 | it's not recommeded for production environments in mainnet at the moment. 94 | 95 | ## Endpoints 96 | 97 | * `GET /jig/:location` - Gets the state for a jig at a particular location 98 | * `GET /berry/:location` - Gets the state for a berry at a particular location 99 | * `GET /tx/:txid` - Gets the raw transaction hex for an added transaction 100 | * `GET /time/:txid` - Gets the block or mempool time of a transaction in seconds since unix epoch 101 | * `GET /spends/:location` - Gets the spending txid for an output at a particular location 102 | * `GET /unspent` - Gets the locations of all unspent jigs that are trusted. You may optionally pass in the following query params: `class` to filter by contract origin, `address` to filter by owner address, `pubkey` to filter by owner pubkey, `scripthash` to filter by hash of the owner script, `lock` to filter by lock class origin. 103 | * `GET /trust/:txid?` - Gets whether a particular txid is trusted, or the entire trust list 104 | * `GET /ban/:txid?` - Gets whether a particular txid is banned, or the entire ban list 105 | * `GET /status` - Prints status information 106 | 107 | * `POST /trust/:txid?` - Trusts a transaction to execute its code, as well as any untrusted ancestors. To trust multiple transactions at once, you may add an array of txids in the body as application/json. 108 | * `POST /ban/:txid` - Bans a transaction from being executed, and unindexes it and its descendents 109 | * `POST /tx/:txid?` - Indexes a transaction and any ancestors. You may optionally add the raw hex data for the transaction in the body as text/plain. 110 | 111 | * `DELETE /trust/:txid` - Removes trust for a transaction, and unindexes it and its descendents 112 | * `DELETE /ban/:txid` - Removes a transaction ban, and reindexes it and its descendents 113 | * `DELETE /tx/:txid` - Removes a transaction, its descendents, and any connected state 114 | 115 | ## Performing Custom Queries 116 | 117 | Run-DB uses SQLite as its underlying database in [WAL](https://sqlite.org/wal.html) mode. SQLite and WAL allows multiple connections to the database so long as there is only one writer, which should be Run-DB. Alternatively, forking Run-DB to create new endpoints for your application may be simpler. 118 | 119 | ### Example Queries 120 | 121 | For some of these queries, you will need the [JSON1](https://www.sqlite.org/json1.html) SQLite extension. 122 | 123 | #### Calculate SHUA supply 124 | 125 | ``` 126 | SELECT SUM(amount) as supply 127 | FROM ( 128 | SELECT 129 | json_extract(jig.state, '$.props.amount') AS amount 130 | FROM jig JOIN spends ON jig.location = spends.location 131 | WHERE spends.spend_txid IS NULL 132 | AND jig.class = 'ce8629aa37a1777d6aa64d0d33cd739fd4e231dc85cfe2f9368473ab09078b78_o1') 133 | ``` 134 | 135 | #### Calculate SHUA token balances by owner 136 | 137 | ``` 138 | SELECT owner, SUM(amount) as amount 139 | FROM (SELECT 140 | json_extract(jig.state, '$.props.owner') AS owner, 141 | json_extract(jig.state, '$.props.amount') AS amount 142 | FROM jig JOIN spends ON jig.location = spends.location 143 | WHERE spends.spend_txid IS NULL 144 | AND jig.class = 'ce8629aa37a1777d6aa64d0d33cd739fd4e231dc85cfe2f9368473ab09078b78_o1') 145 | GROUP BY owner 146 | ORDER BY amount DESC 147 | ``` 148 | 149 | #### Get transaction hex 150 | 151 | ``` 152 | SELECT HEX(bytes) AS hex 153 | FROM tx 154 | WERE txid = 'ce8629aa37a1777d6aa64d0d33cd739fd4e231dc85cfe2f9368473ab09078b78' 155 | ``` 156 | 157 | #### Re-execute all transactions 158 | 159 | ``` 160 | UPDATE tx SET executed = 0; DELETE FROM jig; DELETE FROM berry; 161 | ``` 162 | 163 | ### Database Schema 164 | 165 | There are currently 8 tables updated by Run-DB. 166 | 167 | #### jig 168 | 169 | Stores jig and code states at output locations or destroyed locations. 170 | 171 | | Column | Type | Description | 172 | | ------ | ---- | ----------- | 173 | | location | TEXT | Jig or code location | 174 | | state | TEXT | JSON string describing the object state | 175 | | class | TEXT | Contract origin if this state is a jig | 176 | | scripthash | TEXT | Hex string of the reversed sha256 of the owner script | 177 | | lock | TEXT | Lock class origin if this state has a custom lock | 178 | 179 | #### tx 180 | 181 | Stores all transactions known by Run-DB and their indexing state. 182 | 183 | | Column | Type | Description | 184 | | ------ | ---- | ----------- | 185 | | txid | TEXT | Hex string for the transaction hash | 186 | | height | INTEGER | Block height for this transaction, or `-1` for mempool, or `NULL` for unknown | 187 | | time | INTEGER | Transaction or bock time in seconds since the unix epoch | 188 | | bytes | BLOB | Raw transaction data, or `NULL` if not downloaded | 189 | | has_code | INTEGER | `1` if this transaction deployed or upgraded code and requires trust, `0` otherwise | 190 | | executable | INTEGER | `1` if this transaction is a valid RUN transaction, `0` otherwise | 191 | | executed | INTEGER | `1` if this transaction was executed, even if it failed, `0` otherwise | 192 | | indexed | INTEGER | `1` if this transaction's jig states were calculated successfully, `0` otherwise | 193 | 194 | #### spends 195 | 196 | Stores spend information about transaction outputs. 197 | 198 | | Column | Type | Description | 199 | | ------ | ---- | ----------- | 200 | | location | TEXT | \_o\ string describing an output 201 | | spend_txid| TXID | Hex txid that spent this output, or `NULL` if unspent 202 | 203 | #### deps 204 | 205 | Stores the transaction needed to load a RUN transaction. 206 | 207 | | Column | Type | Description | 208 | | ------ | ---- | ----------- | 209 | | up | TEXT | A transaction ID in hex | 210 | | down | TEXT | Hex txid for a transaction that depends on `up` | 211 | 212 | #### berry 213 | 214 | Stores berry states for third-party protocol data. 215 | 216 | | Column | Type | Description | 217 | | ------ | ---- | ----------- | 218 | | location | TEXT | Berry location without the &hash query param | 219 | | state | TEXT | JSON string describing the object state | 220 | 221 | #### trust 222 | 223 | Stores the transactions which have been trusted and whose code will be executed. 224 | 225 | | Column | Type | Description | 226 | | ------ | ---- | ----------- | 227 | | txid | TEXT | Hex string txid | 228 | | value | INTEGER | `1` if trusted, `0` if untrusted | 229 | 230 | #### ban 231 | 232 | Stores the transactions which have been blacklisted. 233 | 234 | | Column | Type | Description | 235 | | ------ | ---- | ----------- | 236 | | txid | TEXT | Hex string txid | 237 | | value | INTEGER | `1` if blacklisted, `0` otherwise | 238 | 239 | #### crawl 240 | 241 | Stores the crawled block tip height and hash for data in the database. 242 | 243 | | Column | Type | Description | 244 | | ------ | ---- | ----------- | 245 | | key | TEXT | 'height' or 'hash' 246 | | value | TEXT | String value for the key | 247 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runonbitcoin/run-db/4d9e34b90f82c77763c92165cdbf0e7c97df14d3/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-db", 3 | "version": "1.0.32", 4 | "description": "A local database that indexes jig states from RUN transactions", 5 | "keywords": [ 6 | "run", 7 | "run-sdk", 8 | "database", 9 | "indexer", 10 | "jig", 11 | "jigs", 12 | "berry", 13 | "berries", 14 | "token", 15 | "tokens", 16 | "smart contract", 17 | "smart contracts", 18 | "bitcoin", 19 | "bsv", 20 | "bitcoin sv" 21 | ], 22 | "license": "MIT", 23 | "repository": "git://github.com/runonbitcoin/run-db.git", 24 | "main": "src/index.js", 25 | "scripts": { 26 | "lint": "standard --fix", 27 | "start": "node --experimental-worker .", 28 | "retryTx": "node --experimental-worker src/retry-tx.js", 29 | "test": "node --experimental-worker node_modules/mocha/bin/mocha", 30 | "test:cover": "nyc -r=text -r=lcovonly -x=test/** node --experimental-worker node_modules/mocha/bin/mocha", 31 | "bump": "npm version patch && git push --follow-tags && npm publish" 32 | }, 33 | "dependencies": { 34 | "abort-controller": "^3.0.0", 35 | "axios": "^0.21.1", 36 | "better-sqlite3": "^7.4.1", 37 | "body-parser": "^1.19.0", 38 | "cors": "^2.8.5", 39 | "dotenv": "^8.2.0", 40 | "event-stream": "^4.0.1", 41 | "eventsource": "^1.1.0", 42 | "express": "^4.17.1", 43 | "morgan": "^1.10.0", 44 | "node-fetch": "^2.6.1", 45 | "reconnecting-eventsource": "^1.1.0", 46 | "run-sdk": "^0.6.35" 47 | }, 48 | "optionalDependencies": { 49 | "zeromq": "^5.2.8" 50 | }, 51 | "devDependencies": { 52 | "chai": "^4.3.4", 53 | "chai-as-promised": "^7.1.1", 54 | "mocha": "^8.3.2", 55 | "nyc": "^15.1.0", 56 | "standard": "^16.0.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * api.js 3 | * 4 | * API used to get transaction data 5 | */ 6 | 7 | // ------------------------------------------------------------------------------------------------ 8 | // Api 9 | // ------------------------------------------------------------------------------------------------ 10 | 11 | class Api { 12 | // Connect to the API at a particular block height and network 13 | async connect (height, network) { } 14 | 15 | // Stop any connections 16 | async disconnect () { } 17 | 18 | // Returns the rawtx of the txid, or throws an error 19 | async fetch (txid) { throw new Error('Not implemented') } 20 | 21 | // Gets the next relevant block of transactions to add 22 | // currHash may be null 23 | // If there is a next block, return: { height, hash, txids, txhexs? } 24 | // If there is no next block yet, return null 25 | // If the current block passed was reorged, return { reorg: true } 26 | async getNextBlock (currHeight, currHash) { throw new Error('Not implemented') } 27 | 28 | // Begins listening for mempool transactions 29 | // The callback should be called with txid and optionally rawtx when mempool tx is found 30 | // The crawler will call this after the block syncing is up-to-date. 31 | async listenForMempool (mempoolTxCallback) { } 32 | } 33 | 34 | // ------------------------------------------------------------------------------------------------ 35 | 36 | module.exports = Api 37 | -------------------------------------------------------------------------------- /src/bitcoin-node-connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bitcoin node 3 | * 4 | * This connection is meant to connect to a local bitcoin node that you have access to. 5 | */ 6 | 7 | // ------------------------------------------------------------------------------------------------ 8 | // Bitcoin Node 9 | // ------------------------------------------------------------------------------------------------ 10 | 11 | const bsv = require('bsv') 12 | const { metadata } = require('run-sdk').util 13 | 14 | class BitcoinNodeConnection { 15 | constructor (zmq, rpc) { 16 | this.zmq = zmq 17 | this.rpc = rpc 18 | } 19 | 20 | async connect (_height, _network) { 21 | await this.zmq.connect() 22 | } 23 | 24 | async disconnect () { 25 | await this.zmq.disconnect() 26 | } 27 | 28 | async fetch (txid) { 29 | const response = await this.rpc.getRawTransaction(txid) 30 | 31 | return { 32 | hex: response.hex, 33 | time: response.blocktime ? response.blocktime : null, 34 | height: response.blockheight ? response.blockheight : -1 35 | } 36 | } 37 | 38 | async getNextBlock (currentHeight, currentHash) { 39 | const blockCount = await this.rpc.getBlockCount() 40 | 41 | if (blockCount === currentHeight) { 42 | return null 43 | } 44 | 45 | const targetBlockHeight = Number(currentHeight) + 1 46 | const blockData = await this.rpc.getBlockByHeight(targetBlockHeight, true) 47 | 48 | if (currentHash && blockData.previousblockhash !== currentHash) { 49 | return { reorg: true } 50 | } 51 | 52 | if (blockData.size >= 0xf000000) { // Avoids create a string longer than the limit 53 | return this._responsefromBlockData(blockData) 54 | } 55 | 56 | const block = this._parseBlock( 57 | await this.rpc.getBlockByHeight(targetBlockHeight, false), 58 | targetBlockHeight 59 | ) 60 | return this._buildBlockResponse(block, targetBlockHeight) 61 | } 62 | 63 | async listenForMempool (mempoolTxCallback) { 64 | this.zmq.subscribeRawTx((txhex) => { 65 | const tx = bsv.Transaction(txhex) 66 | 67 | if (this._isRunTx(tx.toBuffer().toString('hex'))) { 68 | mempoolTxCallback(tx.hash, tx.toBuffer().toString('hex')) 69 | } 70 | }) 71 | } 72 | 73 | _isRunTx (rawTx) { 74 | try { 75 | metadata(rawTx) 76 | return true 77 | } catch (e) { 78 | return false 79 | } 80 | } 81 | 82 | _buildBlockResponse (block, height) { 83 | const runTxs = block.txs.filter(tx => this._isRunTx(tx.toBuffer().toString('hex'))) 84 | const response = { 85 | height: height, 86 | hash: block.hash, 87 | txids: runTxs.map(tx => tx.hash), 88 | txhexs: runTxs.map(tx => tx.toBuffer().toString('hex')) 89 | } 90 | return response 91 | } 92 | 93 | _parseBlock (rpcResponse, requestedHeight) { 94 | const bsvBlock = new bsv.Block(Buffer.from(rpcResponse, 'hex')) 95 | 96 | return { 97 | height: requestedHeight, 98 | hash: bsvBlock.id.toString('hex'), 99 | previousblockhash: bsvBlock.header.prevHash.reverse().toString('hex'), 100 | time: bsvBlock.header.time, 101 | txs: bsvBlock.transactions 102 | } 103 | } 104 | 105 | async _responsefromBlockData (rpcResponse) { 106 | const runTxs = [] 107 | for (const txid of rpcResponse.tx) { 108 | const hexTx = await this.rpc.getRawTransaction(txid, false) 109 | if (this._isRunTx(hexTx)) { 110 | runTxs.push({ txid, hexTx }) 111 | } 112 | } 113 | return { 114 | height: rpcResponse.height, 115 | hash: rpcResponse.hash, 116 | txids: runTxs.map(tx => tx.txid), 117 | txhexs: runTxs.map(tx => tx.hexTx) 118 | } 119 | } 120 | } 121 | 122 | module.exports = BitcoinNodeConnection 123 | -------------------------------------------------------------------------------- /src/bitcoin-rpc.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | 3 | const httpPost = async (url, jsonBody) => { 4 | const response = await fetch(url, 5 | { 6 | method: 'POST', 7 | headers: { 8 | 'content-type': 'application/json' 9 | }, 10 | body: JSON.stringify(jsonBody) 11 | } 12 | ) 13 | 14 | if (!response.ok) { 15 | throw new Error(`error during rpc call: ${jsonBody.method}, ${jsonBody.params}`) 16 | } 17 | 18 | return response 19 | } 20 | 21 | class BitcoinRpc { 22 | /** 23 | * Creates an instance to connect with a given rpc url. 24 | * 25 | * @param {string} url full connection url, with credentials, port, etc. 26 | */ 27 | constructor (baseUrl) { 28 | this.baseUrl = baseUrl 29 | } 30 | 31 | /** 32 | * @param {String} txid 33 | */ 34 | async getRawTransaction (txid, verbose = true) { 35 | return this._rpcCall('getrawtransaction', [txid, verbose]) 36 | } 37 | 38 | async getBlockCount () { 39 | return this._rpcCall('getblockcount', []) 40 | } 41 | 42 | /** 43 | * @param {Number} targetHeight block height. must be positive int. 44 | * @returns object with needed data. txs are bsv transactions 45 | */ 46 | async getBlockByHeight (targetHeight, verbose = false) { 47 | return this._rpcCall('getblockbyheight', [targetHeight, verbose]) 48 | } 49 | 50 | async _rpcCall (method, params) { 51 | const response = await this._httpPost(this.baseUrl, { 52 | jsonrpc: '1.0', 53 | method: method, 54 | params: params 55 | }) 56 | 57 | const { error, result } = await response.json() 58 | 59 | if (error !== null) { 60 | throw new Error(error) 61 | } 62 | 63 | return result 64 | } 65 | 66 | async _httpPost (url, jsonBody) { 67 | try { 68 | return httpPost(url, jsonBody) 69 | } catch (e) { 70 | // In case of an error requesting to the node we do 1 retry after 2 seconds. 71 | console.error(e.message) 72 | console.log('retrying...') 73 | await new Promise(resolve => setTimeout(resolve, 2000)) 74 | return httpPost(url, jsonBody) 75 | } 76 | } 77 | } 78 | 79 | module.exports = BitcoinRpc 80 | -------------------------------------------------------------------------------- /src/bitcoin-zmq.js: -------------------------------------------------------------------------------- 1 | const zmq = require('zeromq') 2 | 3 | class BitcoinZmq { 4 | constructor (url) { 5 | this.sock = zmq.socket('sub') 6 | this.url = url 7 | } 8 | 9 | async connect () { 10 | this.sock.connect(this.url) 11 | this.sock.subscribe('rawtx') 12 | } 13 | 14 | async disconnect () { 15 | await this.sock.close() 16 | } 17 | 18 | async subscribeRawTx (handler) { 19 | this.sock.on('message', (_topic, message) => { 20 | handler(message) 21 | }) 22 | } 23 | } 24 | 25 | module.exports = BitcoinZmq 26 | -------------------------------------------------------------------------------- /src/bus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bus.js 3 | * 4 | * Communication between the main program and worker threads 5 | */ 6 | 7 | // ------------------------------------------------------------------------------------------------ 8 | // Globals 9 | // ------------------------------------------------------------------------------------------------ 10 | 11 | let messageId = 0 12 | const messageCallbacks = {} 13 | 14 | // ------------------------------------------------------------------------------------------------ 15 | // sendRequest 16 | // ------------------------------------------------------------------------------------------------ 17 | 18 | async function sendRequest (port, func, ...args) { 19 | return await new Promise((resolve, reject) => { 20 | messageCallbacks[messageId] = { resolve, reject } 21 | port.postMessage({ id: messageId, func, args }) 22 | messageId++ 23 | }) 24 | } 25 | 26 | // ------------------------------------------------------------------------------------------------ 27 | // listen 28 | // ------------------------------------------------------------------------------------------------ 29 | 30 | function listen (port, handlers) { 31 | port.on('message', async msg => { 32 | if (msg.response) { 33 | if (msg.err) { 34 | messageCallbacks[msg.id].reject(msg.err) 35 | } else { 36 | messageCallbacks[msg.id].resolve(msg.ret) 37 | } 38 | delete messageCallbacks[msg.id] 39 | return 40 | } 41 | 42 | try { 43 | const handler = handlers[msg.func] 44 | if (typeof handler !== 'function') { 45 | throw new Error('No handler for ' + msg.func) 46 | } 47 | 48 | const ret = await handler(...msg.args) 49 | 50 | port.postMessage({ response: true, id: msg.id, ret }) 51 | } catch (e) { 52 | port.postMessage({ response: true, id: msg.id, err: e.message || e.toString() }) 53 | } 54 | }) 55 | 56 | port.on('error', e => { 57 | console.error('Worker thread error:', e) 58 | process.exit(1) 59 | }) 60 | } 61 | 62 | // ------------------------------------------------------------------------------------------------ 63 | 64 | module.exports = { sendRequest, listen } 65 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * config.js 3 | * 4 | * Configuration from environment variables 5 | */ 6 | 7 | require('dotenv').config() 8 | 9 | // ------------------------------------------------------------------------------------------------ 10 | // Globals 11 | // ------------------------------------------------------------------------------------------------ 12 | 13 | const API = process.env.API || 'mattercloud' 14 | const MATTERCLOUD_KEY = process.env.MATTERCLOUD_KEY 15 | const PLANARIA_TOKEN = process.env.PLANARIA_TOKEN 16 | const NETWORK = process.env.NETWORK || 'main' 17 | const DB = process.env.DB || 'run.db' 18 | const PORT = typeof process.env.PORT !== 'undefined' ? parseInt(process.env.PORT) : 0 19 | const WORKERS = typeof process.env.WORKERS !== 'undefined' ? parseInt(process.env.WORKERS) : 4 20 | const FETCH_LIMIT = typeof process.env.FETCH_LIMIT !== 'undefined' ? parseInt(process.env.FETCH_LIMIT) : 20 21 | const START_HEIGHT = process.env.START_HEIGHT || (NETWORK === 'test' ? 1382000 : 650000) 22 | const TIMEOUT = typeof process.env.TIMEOUT !== 'undefined' ? parseInt(process.env.TIMEOUT) : 10000 23 | const MEMPOOL_EXPIRATION = typeof process.env.MEMPOOL_EXPIRATION !== 'undefined' ? parseInt(process.env.MEMPOOL_EXPIRATION) : 60 * 60 * 24 24 | const ZMQ_URL = process.env.ZMQ_URL || null 25 | const RPC_URL = process.env.RPC_URL || null 26 | const DEBUG = process.env.DEBUG || false 27 | const SERVE_ONLY = process.env.SERVE_ONLY || false 28 | 29 | require('axios').default.defaults.timeout = TIMEOUT 30 | 31 | // ------------------------------------------------------------------------------------------------ 32 | // Default trustlist 33 | // ------------------------------------------------------------------------------------------------ 34 | 35 | const ENV_VAR_DEFAULT_TRUSTLIST = process.env.DEFAULT_TRUSTLIST && process.env.DEFAULT_TRUSTLIST.split(',').filter(t => t) 36 | 37 | const DEFAULT_TRUSTLIST = ENV_VAR_DEFAULT_TRUSTLIST || [ 38 | /** 39 | * RUN ▸ Extras 40 | */ 41 | '61e1265acb3d93f1bf24a593d70b2a6b1c650ec1df90ddece8d6954ae3cdd915', // asm 42 | '49145693676af7567ebe20671c5cb01369ac788c20f3b1c804f624a1eda18f3f', // asm 43 | '284ce17fd34c0f41835435b03eed149c4e0479361f40132312b4001093bb158f', // asm 44 | '6fe169894d313b44bd54154f88e1f78634c7f5a23863d1713342526b86a39b8b', // B 45 | '5332c013476cd2a2c18710a01188695bc27a5ef1748a51d4a5910feb1111dab4', // B (v2) 46 | '81bcef29b0e4ed745f3422c0b764a33c76d0368af2d2e7dd139db8e00ee3d8a6', // Base58 47 | '71fba386341b932380ec5bfedc3a40bce43d4974decdc94c419a94a8ce5dfc23', // expect 48 | '780ab8919cb89323707338070323c24ce42cdec2f57d749bd7aceef6635e7a4d', // Group 49 | '90a3ece416f696731430efac9657d28071cc437ebfff5fb1eaf710fe4b3c8d4e', // Group 50 | '727e7b423b7ee40c0b5be87fba7fa5673ea2d20a74259040a7295d9c32a90011', // Hex 51 | '3b7ef411185bbe3d01caeadbe6f115b0103a546c4ef0ac7474aa6fbb71aff208', // sha256 52 | 'b17a9af70ab0f46809f908b2e900e395ba40996000bf4f00e3b27a1e93280cf1', // Token (v1) 53 | '72a61eb990ffdb6b38e5f955e194fed5ff6b014f75ac6823539ce5613aea0be8', // Token (v2) 54 | '312985bd960ae4c59856b3089b04017ede66506ea181333eec7c9bb88b11c490', // Tx, txo 55 | '05f67252e696160a7c0099ae8d1ec23c39592378773b3a5a55f16bd1286e7dcb', // txo, Tx, B(v2) 56 | 57 | /** 58 | * RelayX 59 | */ 60 | 'd792d10294a0d9b05a30049f187a1704ced14840ecf41d00663d79c695f86633', // USDC 61 | '318d2a009e29cb3a202b2a167773341dcd39809b967889a7e306d504cc266faf', // OKBSV 62 | '5a8d4b4da7c5f27a39adac3a9256a7e15e03a7266c81ac8369a3b634560e7814', // OKBSV 63 | 'd7273b6790a4dec4aa116661aff0ec35381794e552807014ca6a536f4454976d', // OKBSV 64 | 'd6170025a62248d8df6dc14e3806e68b8df3d804c800c7bfb23b0b4232862505', // OrderLock 65 | 66 | /** 67 | * Tokens 68 | */ 69 | 'ce8629aa37a1777d6aa64d0d33cd739fd4e231dc85cfe2f9368473ab09078b78', // SHUA 70 | 'ca1818540d2865c5b6a53e06650eafadc10b478703aa7cf324145f848fec629b', // SHUA 71 | '1de3951603784df7c872519c096445a415d9b0d3dce7bbe3b7a36ca82cf1a91c', // SHUA 72 | '367b4980287f8abae5ee4b0c538232164d5b2463068067ec1e510c91114bced2', // SHUA 73 | 74 | /** 75 | * RUN ▸ Extras (testnet) 76 | */ 77 | '1f0abf8d94477b1cb57629d861376616f6e1d7b78aba23a19da3e6169caf489e', // asm, Hex 78 | '8b9380d445b6fe01ec7230d8363febddc99feee6064d969ae8f98fdb25e1393f', // asm 79 | '03e21aa8fcf08fa6985029ad2e697a2309962527700246d47d891add3cfce3ac', // asm 80 | '5435ae2760dc35f4329501c61c42e24f6a744861c22f8e0f04735637c20ce987', // B 81 | 'b44a203acd6215d2d24b33a41f730e9acf2591c4ae27ecafc8d88ef83da9ddea', // B (v2) 82 | '424abf066be56b9dd5203ed81cf1f536375351d29726d664507fdc30eb589988', // Base58 83 | 'f97d4ac2a3d6f5ed09fad4a4f341619dc5a3773d9844ff95c99c5d4f8388de2f', // expect 84 | '63e0e1268d8ab021d1c578afb8eaa0828ccbba431ffffd9309d04b78ebeb6e56', // Group 85 | '03320f1244e509bb421e6f1ff724bf1156182890c3768cfa4ea127a78f9913d2', // Group 86 | '4a1929527605577a6b30710e6001b9379400421d8089d34bb0404dd558529417', // sha256 87 | '0bdf33a334a60909f4c8dab345500cbb313fbfd50b1d98120227eae092b81c39', // Token (v1) 88 | '7d14c868fe39439edffe6982b669e7b4d3eb2729eee7c262ec2494ee3e310e99', // Token (v2) 89 | '33e78fa7c43b6d7a60c271d783295fa180b7e9fce07d41ff1b52686936b3e6ae', // Tx, txo 90 | 'd476fd7309a0eeb8b92d715e35c6e273ad63c0025ff6cca927bd0f0b64ed88ff', // Tx, txo, B (v2) 91 | 92 | /** 93 | * Other 94 | */ 95 | '24cde3638a444c8ad397536127833878ffdfe1b04d5595489bd294e50d77105a', // B (old) 96 | 'bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d', // Class with logo 97 | 'c0a79e8afb7cabe5f25bdaa398683d6dfe68a2912b29fe948ed130d14e3a2380', // TimeLock 98 | '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64' // Tutorial jigs 99 | ] 100 | 101 | // ------------------------------------------------------------------------------------------------ 102 | 103 | module.exports = { 104 | API, 105 | MATTERCLOUD_KEY, 106 | PLANARIA_TOKEN, 107 | NETWORK, 108 | DB, 109 | PORT, 110 | WORKERS, 111 | FETCH_LIMIT, 112 | START_HEIGHT, 113 | MEMPOOL_EXPIRATION, 114 | DEFAULT_TRUSTLIST, 115 | ZMQ_URL, 116 | RPC_URL, 117 | DEBUG, 118 | SERVE_ONLY 119 | } 120 | -------------------------------------------------------------------------------- /src/crawler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * crawler.js 3 | * 4 | * Generic blockchain crawler that adds and removes transactions to the db 5 | */ 6 | 7 | // ------------------------------------------------------------------------------------------------ 8 | // Crawler 9 | // ------------------------------------------------------------------------------------------------ 10 | 11 | class Crawler { 12 | constructor (api, logger) { 13 | this.api = api 14 | this.logger = logger 15 | this.height = null 16 | this.hash = null 17 | this.pollForNewBlocksInterval = 10000 18 | this.pollForNewBlocksTimerId = null 19 | this.expireMempoolTransactionsInterval = 60000 20 | this.expireMempoolTransactionsTimerId = null 21 | this.rewindCount = 10 22 | this.started = false 23 | this.listeningForMempool = false 24 | 25 | this.onCrawlError = null 26 | this.onCrawlBlockTransactions = null 27 | this.onRewindBlocks = null 28 | this.onMempoolTransaction = null 29 | this.onExpireMempoolTransactions = null 30 | } 31 | 32 | start (height, hash) { 33 | this.logger.debug('Starting crawler') 34 | 35 | if (this.started) return 36 | 37 | this.started = true 38 | this.height = height 39 | this.hash = hash 40 | 41 | this._pollForNewBlocks() 42 | this._expireMempoolTransactions() 43 | } 44 | 45 | stop () { 46 | this.started = false 47 | this.listeningForMempool = false 48 | clearTimeout(this.pollForNewBlocksTimerId) 49 | this.pollForNewBlocksTimerId = null 50 | clearTimeout(this.expireMempoolTransactionsTimerId) 51 | this.expireMempoolTransactionsTimerId = null 52 | } 53 | 54 | _expireMempoolTransactions () { 55 | if (!this.started) return 56 | 57 | this.logger.debug('Expiring mempool transactions') 58 | 59 | if (this.onExpireMempoolTransactions) this.onExpireMempoolTransactions() 60 | 61 | this.expireMempoolTransactionsTimerId = setTimeout( 62 | this._expireMempoolTransactions.bind(this), this.expireMempoolTransactionsInterval) 63 | } 64 | 65 | async _pollForNewBlocks () { 66 | if (!this.started) return 67 | 68 | try { 69 | await this._pollForNextBlock() 70 | } catch (e) { 71 | if (this.onCrawlError) this.onCrawlError(e) 72 | // Swallow, we'll retry 73 | } 74 | 75 | if (!this.started) return 76 | 77 | this.pollForNewBlocksTimerId = setTimeout(this._pollForNewBlocks.bind(this), this.pollForNewBlocksInterval) 78 | } 79 | 80 | async _pollForNextBlock () { 81 | if (!this.started) return 82 | 83 | this.logger.debug('Polling for next block') 84 | 85 | // Save the current query so we can check for a race condition after 86 | const currHeight = this.height 87 | const currHash = this.hash 88 | 89 | const block = this.api.getNextBlock && await this.api.getNextBlock(currHeight, currHash) 90 | 91 | // Case: shutting down 92 | if (!this.started) return 93 | 94 | // Case: race condition, block already updated by another poller 95 | if (this.height !== currHeight) return 96 | 97 | // Case: reorg 98 | if (block && block.reorg) { 99 | this.logger.debug('Reorg detected') 100 | this._rewindAfterReorg() 101 | setTimeout(() => this._pollForNextBlock(), 0) 102 | return 103 | } 104 | 105 | // Case: at the chain tip 106 | if (!block || block.height <= this.height) { 107 | this.logger.debug('No new blocks') 108 | await this._listenForMempool() 109 | return 110 | } 111 | 112 | // Case: received a block 113 | if (block) { 114 | this.logger.debug('Received new block at height', block.height) 115 | if (this.onCrawlBlockTransactions) { 116 | this.onCrawlBlockTransactions(block.height, block.hash, block.time, block.txids, block.txhexs) 117 | } 118 | this.height = block.height 119 | this.hash = block.hash 120 | setTimeout(() => this._pollForNextBlock(), 0) 121 | } 122 | } 123 | 124 | _rewindAfterReorg () { 125 | const newHeight = this.height - this.rewindCount 126 | if (this.onRewindBlocks) this.onRewindBlocks(newHeight) 127 | this.height = newHeight 128 | this.hash = null 129 | } 130 | 131 | async _listenForMempool () { 132 | if (this.listeningForMempool) return 133 | 134 | if (this.api.listenForMempool) { 135 | await this.api.listenForMempool(this._onMempoolRunTransaction.bind(this)) 136 | } 137 | 138 | this.listeningForMempool = true 139 | } 140 | 141 | _onMempoolRunTransaction (txid, rawtx) { 142 | if (this.onMempoolTransaction) this.onMempoolTransaction(txid, rawtx) 143 | } 144 | } 145 | 146 | // ------------------------------------------------------------------------------------------------ 147 | 148 | module.exports = Crawler 149 | -------------------------------------------------------------------------------- /src/database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * database.js 3 | * 4 | * Layer between the database and the application 5 | */ 6 | 7 | const Sqlite3Database = require('better-sqlite3') 8 | const Run = require('run-sdk') 9 | const bsv = require('bsv') 10 | 11 | // ------------------------------------------------------------------------------------------------ 12 | // Globals 13 | // ------------------------------------------------------------------------------------------------ 14 | 15 | const HEIGHT_MEMPOOL = -1 16 | const HEIGHT_UNKNOWN = null 17 | 18 | // The + in the following 2 queries before downloaded improves performance by NOT using the 19 | // tx_downloaded index, which is rarely an improvement over a simple filter for single txns. 20 | // See: https://www.sqlite.org/optoverview.html 21 | const IS_READY_TO_EXECUTE_SQL = ` 22 | SELECT ( 23 | downloaded = 1 24 | AND executable = 1 25 | AND executed = 0 26 | AND (has_code = 0 OR (SELECT COUNT(*) FROM trust WHERE trust.txid = tx.txid AND trust.value = 1) = 1) 27 | AND txid NOT IN ban 28 | AND ( 29 | SELECT COUNT(*) 30 | FROM tx AS tx2 31 | JOIN deps 32 | ON deps.up = tx2.txid 33 | WHERE deps.down = tx.txid 34 | AND (+tx2.downloaded = 0 OR (tx2.executable = 1 AND tx2.executed = 0) OR (tx2.executed = 1 and tx2.indexed = 0)) 35 | ) = 0 36 | ) AS ready 37 | FROM tx 38 | WHERE txid = ? 39 | ` 40 | 41 | const GET_DOWNSTREADM_READY_TO_EXECUTE_SQL = ` 42 | SELECT down 43 | FROM deps 44 | JOIN tx 45 | ON tx.txid = deps.down 46 | WHERE up = ? 47 | AND +downloaded = 1 48 | AND executable = 1 49 | AND executed = 0 50 | AND (has_code = 0 OR (SELECT COUNT(*) FROM trust WHERE trust.txid = tx.txid AND trust.value = 1) = 1) 51 | AND txid NOT IN ban 52 | AND ( 53 | SELECT COUNT(*) 54 | FROM tx AS tx2 55 | JOIN deps 56 | ON deps.up = tx2.txid 57 | WHERE deps.down = tx.txid 58 | AND (+tx2.downloaded = 0 OR (tx2.executable = 1 AND tx2.executed = 0)) 59 | ) = 0 60 | ` 61 | 62 | // ------------------------------------------------------------------------------------------------ 63 | // Database 64 | // ------------------------------------------------------------------------------------------------ 65 | 66 | class Database { 67 | constructor (path, logger, readonly = false) { 68 | this.path = path 69 | this.logger = logger 70 | this.db = null 71 | this.readonly = readonly 72 | 73 | this.onReadyToExecute = null 74 | this.onAddTransaction = null 75 | this.onDeleteTransaction = null 76 | this.onTrustTransaction = null 77 | this.onUntrustTransaction = null 78 | this.onBanTransaction = null 79 | this.onUnbanTransaction = null 80 | this.onUntrustTransaction = null 81 | this.onRequestDownload = null 82 | } 83 | 84 | open () { 85 | this.logger.debug('Opening' + (this.readonly ? ' readonly' : '') + ' database') 86 | 87 | if (this.db) throw new Error('Database already open') 88 | 89 | this.db = new Sqlite3Database(this.path, { readonly: this.readonly }) 90 | 91 | // 100MB cache 92 | this.db.pragma('cache_size = 6400') 93 | this.db.pragma('page_size = 16384') 94 | 95 | // WAL mode allows simultaneous readers 96 | this.db.pragma('journal_mode = WAL') 97 | 98 | // Synchronizes WAL at checkpoints 99 | this.db.pragma('synchronous = NORMAL') 100 | 101 | if (!this.readonly) { 102 | // Initialise and perform upgrades 103 | this.initializeV1() 104 | this.initializeV2() 105 | this.initializeV3() 106 | this.initializeV4() 107 | this.initializeV5() 108 | this.initializeV6() 109 | this.initializeV7() 110 | } 111 | 112 | this.addNewTransactionStmt = this.db.prepare('INSERT OR IGNORE INTO tx (txid, height, time, bytes, has_code, executable, executed, indexed) VALUES (?, null, ?, null, 0, 0, 0, 0)') 113 | this.setTransactionBytesStmt = this.db.prepare('UPDATE tx SET bytes = ? WHERE txid = ?') 114 | this.setTransactionExecutableStmt = this.db.prepare('UPDATE tx SET executable = ? WHERE txid = ?') 115 | this.getTransactionExecutableStmt = this.db.prepare('SELECT executable FROM tx WHERE txid = ?') 116 | this.setTransactionTimeStmt = this.db.prepare('UPDATE tx SET time = ? WHERE txid = ?') 117 | this.setTransactionHeightStmt = this.db.prepare(`UPDATE tx SET height = ? WHERE txid = ? AND (height IS NULL OR height = ${HEIGHT_MEMPOOL})`) 118 | this.setTransactionHasCodeStmt = this.db.prepare('UPDATE tx SET has_code = ? WHERE txid = ?') 119 | this.setTransactionExecutedStmt = this.db.prepare('UPDATE tx SET executed = ? WHERE txid = ?') 120 | this.setTransactionIndexedStmt = this.db.prepare('UPDATE tx SET indexed = ? WHERE txid = ?') 121 | this.hasTransactionStmt = this.db.prepare('SELECT txid FROM tx WHERE txid = ?') 122 | this.getTransactionHexStmt = this.db.prepare('SELECT LOWER(HEX(bytes)) AS hex FROM tx WHERE txid = ?') 123 | this.getTransactionTimeStmt = this.db.prepare('SELECT time FROM tx WHERE txid = ?') 124 | this.getTransactionHeightStmt = this.db.prepare('SELECT height FROM tx WHERE txid = ?') 125 | this.getTransactionHasCodeStmt = this.db.prepare('SELECT has_code FROM tx WHERE txid = ?') 126 | this.getTransactionIndexedStmt = this.db.prepare('SELECT indexed FROM tx WHERE txid = ?') 127 | this.getTransactionFailedStmt = this.db.prepare('SELECT (executed = 1 AND indexed = 0) AS failed FROM tx WHERE txid = ?') 128 | this.getTransactionDownloadedStmt = this.db.prepare('SELECT downloaded FROM tx WHERE txid = ?') 129 | this.deleteTransactionStmt = this.db.prepare('DELETE FROM tx WHERE txid = ?') 130 | this.unconfirmTransactionStmt = this.db.prepare(`UPDATE tx SET height = ${HEIGHT_MEMPOOL} WHERE txid = ?`) 131 | this.getTransactionsAboveHeightStmt = this.db.prepare('SELECT txid FROM tx WHERE height > ?') 132 | this.getMempoolTransactionsBeforeTimeStmt = this.db.prepare(`SELECT txid FROM tx WHERE height = ${HEIGHT_MEMPOOL} AND time < ?`) 133 | this.getTransactionsToDownloadStmt = this.db.prepare('SELECT txid FROM tx WHERE downloaded = 0') 134 | this.getTransactionsDownloadedCountStmt = this.db.prepare('SELECT COUNT(*) AS count FROM tx WHERE downloaded = 1') 135 | this.getTransactionsIndexedCountStmt = this.db.prepare('SELECT COUNT(*) AS count FROM tx WHERE indexed = 1') 136 | this.isReadyToExecuteStmt = this.db.prepare(IS_READY_TO_EXECUTE_SQL) 137 | this.getDownstreamReadyToExecuteStmt = this.db.prepare(GET_DOWNSTREADM_READY_TO_EXECUTE_SQL) 138 | 139 | this.setSpendStmt = this.db.prepare('INSERT OR REPLACE INTO spends (location, spend_txid) VALUES (?, ?)') 140 | this.setUnspentStmt = this.db.prepare('INSERT OR IGNORE INTO spends (location, spend_txid) VALUES (?, null)') 141 | this.getSpendStmt = this.db.prepare('SELECT spend_txid FROM spends WHERE location = ?') 142 | this.unspendOutputsStmt = this.db.prepare('UPDATE spends SET spend_txid = null WHERE spend_txid = ?') 143 | this.deleteSpendsStmt = this.db.prepare('DELETE FROM spends WHERE location LIKE ? || \'%\'') 144 | 145 | this.addDepStmt = this.db.prepare('INSERT OR IGNORE INTO deps (up, down) VALUES (?, ?)') 146 | this.deleteDepsStmt = this.db.prepare('DELETE FROM deps WHERE down = ?') 147 | this.getDownstreamStmt = this.db.prepare('SELECT down FROM deps WHERE up = ?') 148 | this.getUpstreamUnexecutedCodeStmt = this.db.prepare(` 149 | SELECT txdeps.txid as txid 150 | FROM (SELECT up AS txid FROM deps WHERE down = ?) as txdeps 151 | JOIN tx ON tx.txid = txdeps.txid 152 | WHERE tx.executable = 1 AND tx.executed = 0 AND tx.has_code = 1 153 | `) 154 | 155 | this.getUpstreamStmt = this.db.prepare(` 156 | SELECT up as txid FROM tx 157 | JOIN deps ON deps.up = tx.txid 158 | WHERE 159 | deps.down = ? 160 | `) 161 | 162 | this.setJigStateStmt = this.db.prepare('INSERT OR IGNORE INTO jig (location, state, class, lock, scripthash) VALUES (?, ?, null, null, null)') 163 | this.setJigClassStmt = this.db.prepare('UPDATE jig SET class = ? WHERE location = ?') 164 | this.setJigLockStmt = this.db.prepare('UPDATE jig SET lock = ? WHERE location = ?') 165 | this.setJigScripthashStmt = this.db.prepare('UPDATE jig SET scripthash = ? WHERE location = ?') 166 | this.getJigStateStmt = this.db.prepare('SELECT state FROM jig WHERE location = ?') 167 | this.deleteJigStatesStmt = this.db.prepare('DELETE FROM jig WHERE location LIKE ? || \'%\'') 168 | 169 | const getAllUnspentSql = ` 170 | SELECT spends.location AS location FROM spends 171 | JOIN jig ON spends.location = jig.location 172 | WHERE spends.spend_txid IS NULL` 173 | this.getAllUnspentStmt = this.db.prepare(getAllUnspentSql) 174 | this.getAllUnspentByClassStmt = this.db.prepare(`${getAllUnspentSql} AND jig.class = ?`) 175 | this.getAllUnspentByLockStmt = this.db.prepare(`${getAllUnspentSql} AND jig.lock = ?`) 176 | this.getAllUnspentByScripthashStmt = this.db.prepare(`${getAllUnspentSql} AND jig.scripthash = ?`) 177 | this.getAllUnspentByClassLockStmt = this.db.prepare(`${getAllUnspentSql} AND jig.class = ? AND lock = ?`) 178 | this.getAllUnspentByClassScripthashStmt = this.db.prepare(`${getAllUnspentSql} AND jig.class = ? AND scripthash = ?`) 179 | this.getAllUnspentByLockScripthashStmt = this.db.prepare(`${getAllUnspentSql} AND jig.lock = ? AND scripthash = ?`) 180 | this.getAllUnspentByClassLockScripthashStmt = this.db.prepare(`${getAllUnspentSql} AND jig.class = ? AND jig.lock = ? AND scripthash = ?`) 181 | this.getNumUnspentStmt = this.db.prepare('SELECT COUNT(*) as unspent FROM spends JOIN jig ON spends.location = jig.location WHERE spends.spend_txid IS NULL') 182 | 183 | this.setBerryStateStmt = this.db.prepare('INSERT OR IGNORE INTO berry (location, state) VALUES (?, ?)') 184 | this.getBerryStateStmt = this.db.prepare('SELECT state FROM berry WHERE location = ?') 185 | this.deleteBerryStatesStmt = this.db.prepare('DELETE FROM berry WHERE location LIKE ? || \'%\'') 186 | 187 | this.setTrustedStmt = this.db.prepare('INSERT OR REPLACE INTO trust (txid, value) VALUES (?, ?)') 188 | this.getTrustlistStmt = this.db.prepare('SELECT txid FROM trust WHERE value = 1') 189 | this.isTrustedStmt = this.db.prepare('SELECT COUNT(*) FROM trust WHERE txid = ? AND value = 1') 190 | 191 | this.banStmt = this.db.prepare('INSERT OR REPLACE INTO ban (txid) VALUES (?)') 192 | this.unbanStmt = this.db.prepare('DELETE FROM ban WHERE txid = ?') 193 | this.isBannedStmt = this.db.prepare('SELECT COUNT(*) FROM ban WHERE txid = ?') 194 | this.getBanlistStmt = this.db.prepare('SELECT txid FROM ban') 195 | 196 | this.getHeightStmt = this.db.prepare('SELECT value FROM crawl WHERE key = \'height\'') 197 | this.getHashStmt = this.db.prepare('SELECT value FROM crawl WHERE key = \'hash\'') 198 | this.setHeightStmt = this.db.prepare('UPDATE crawl SET value = ? WHERE key = \'height\'') 199 | this.setHashStmt = this.db.prepare('UPDATE crawl SET value = ? WHERE key = \'hash\'') 200 | 201 | this.markExecutingStmt = this.db.prepare('INSERT OR IGNORE INTO executing (txid) VALUES (?)') 202 | this.unmarkExecutingStmt = this.db.prepare('DELETE FROM executing WHERE txid = ?') 203 | } 204 | 205 | initializeV1 () { 206 | if (this.db.pragma('user_version')[0].user_version !== 0) return 207 | 208 | this.logger.info('Setting up database v1') 209 | 210 | this.transaction(() => { 211 | this.db.pragma('user_version = 1') 212 | 213 | this.db.prepare( 214 | `CREATE TABLE IF NOT EXISTS tx ( 215 | txid TEXT NOT NULL, 216 | height INTEGER, 217 | time INTEGER, 218 | hex TEXT, 219 | has_code INTEGER, 220 | executable INTEGER, 221 | executed INTEGER, 222 | indexed INTEGER, 223 | UNIQUE(txid) 224 | )` 225 | ).run() 226 | 227 | this.db.prepare( 228 | `CREATE TABLE IF NOT EXISTS spends ( 229 | location TEXT NOT NULL PRIMARY KEY, 230 | spend_txid TEXT 231 | ) WITHOUT ROWID` 232 | ).run() 233 | 234 | this.db.prepare( 235 | `CREATE TABLE IF NOT EXISTS deps ( 236 | up TEXT NOT NULL, 237 | down TEXT NOT NULL, 238 | UNIQUE(up, down) 239 | )` 240 | ).run() 241 | 242 | this.db.prepare( 243 | `CREATE TABLE IF NOT EXISTS jig ( 244 | location TEXT NOT NULL PRIMARY KEY, 245 | state TEXT NOT NULL, 246 | class TEXT, 247 | scripthash TEXT, 248 | lock TEXT 249 | ) WITHOUT ROWID` 250 | ).run() 251 | 252 | this.db.prepare( 253 | `CREATE TABLE IF NOT EXISTS berry ( 254 | location TEXT NOT NULL PRIMARY KEY, 255 | state TEXT NOT NULL 256 | ) WITHOUT ROWID` 257 | ).run() 258 | 259 | this.db.prepare( 260 | `CREATE TABLE IF NOT EXISTS trust ( 261 | txid TEXT NOT NULL PRIMARY KEY, 262 | value INTEGER 263 | ) WITHOUT ROWID` 264 | ).run() 265 | 266 | this.db.prepare( 267 | `CREATE TABLE IF NOT EXISTS ban ( 268 | txid TEXT NOT NULL PRIMARY KEY 269 | ) WITHOUT ROWID` 270 | ).run() 271 | 272 | this.db.prepare( 273 | `CREATE TABLE IF NOT EXISTS crawl ( 274 | role TEXT UNIQUE, 275 | height INTEGER, 276 | hash TEXT 277 | )` 278 | ).run() 279 | 280 | this.db.prepare( 281 | 'CREATE INDEX IF NOT EXISTS tx_txid_index ON tx (txid)' 282 | ).run() 283 | 284 | this.db.prepare( 285 | 'CREATE INDEX IF NOT EXISTS jig_index ON jig (class)' 286 | ).run() 287 | 288 | this.db.prepare( 289 | 'INSERT OR IGNORE INTO crawl (role, height, hash) VALUES (\'tip\', 0, NULL)' 290 | ).run() 291 | }) 292 | } 293 | 294 | initializeV2 () { 295 | if (this.db.pragma('user_version')[0].user_version !== 1) return 296 | 297 | this.logger.info('Setting up database v2') 298 | 299 | this.transaction(() => { 300 | this.db.pragma('user_version = 2') 301 | 302 | this.db.prepare( 303 | `CREATE TABLE tx_v2 ( 304 | txid TEXT NOT NULL, 305 | height INTEGER, 306 | time INTEGER, 307 | bytes BLOB, 308 | has_code INTEGER, 309 | executable INTEGER, 310 | executed INTEGER, 311 | indexed INTEGER 312 | )` 313 | ).run() 314 | 315 | const txids = this.db.prepare('SELECT txid FROM tx').all().map(row => row.txid) 316 | const gettx = this.db.prepare('SELECT * FROM tx WHERE txid = ?') 317 | const insert = this.db.prepare('INSERT INTO tx_v2 (txid, height, time, bytes, has_code, executable, executed, indexed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)') 318 | 319 | this.logger.info('Migrating data') 320 | for (const txid of txids) { 321 | const row = gettx.get(txid) 322 | const bytes = row.hex ? Buffer.from(row.hex, 'hex') : null 323 | insert.run(row.txid, row.height, row.time, bytes, row.has_code, row.executable, row.executed, row.indexed) 324 | } 325 | 326 | this.db.prepare( 327 | 'DROP INDEX tx_txid_index' 328 | ).run() 329 | 330 | this.db.prepare( 331 | 'DROP TABLE tx' 332 | ).run() 333 | 334 | this.db.prepare( 335 | 'ALTER TABLE tx_v2 RENAME TO tx' 336 | ).run() 337 | 338 | this.db.prepare( 339 | 'CREATE INDEX IF NOT EXISTS tx_txid_index ON tx (txid)' 340 | ).run() 341 | 342 | this.logger.info('Saving results') 343 | }) 344 | 345 | this.logger.info('Optimizing database') 346 | this.db.prepare('VACUUM').run() 347 | } 348 | 349 | initializeV3 () { 350 | if (this.db.pragma('user_version')[0].user_version !== 2) return 351 | 352 | this.logger.info('Setting up database v3') 353 | 354 | this.transaction(() => { 355 | this.db.pragma('user_version = 3') 356 | 357 | this.db.prepare('CREATE INDEX IF NOT EXISTS deps_up_index ON deps (up)').run() 358 | this.db.prepare('CREATE INDEX IF NOT EXISTS deps_down_index ON deps (down)').run() 359 | this.db.prepare('CREATE INDEX IF NOT EXISTS trust_txid_index ON trust (txid)').run() 360 | 361 | this.logger.info('Saving results') 362 | }) 363 | } 364 | 365 | initializeV4 () { 366 | if (this.db.pragma('user_version')[0].user_version !== 3) return 367 | 368 | this.logger.info('Setting up database v4') 369 | 370 | this.transaction(() => { 371 | this.db.pragma('user_version = 4') 372 | 373 | this.db.prepare('ALTER TABLE tx ADD COLUMN downloaded INTEGER GENERATED ALWAYS AS (bytes IS NOT NULL) VIRTUAL').run() 374 | 375 | this.db.prepare('CREATE INDEX IF NOT EXISTS tx_downloaded_index ON tx (downloaded)').run() 376 | 377 | this.logger.info('Saving results') 378 | }) 379 | } 380 | 381 | initializeV5 () { 382 | if (this.db.pragma('user_version')[0].user_version !== 4) return 383 | 384 | this.logger.info('Setting up database v5') 385 | 386 | this.transaction(() => { 387 | this.db.pragma('user_version = 5') 388 | 389 | this.db.prepare('CREATE INDEX IF NOT EXISTS ban_txid_index ON ban (txid)').run() 390 | this.db.prepare('CREATE INDEX IF NOT EXISTS tx_height_index ON tx (height)').run() 391 | 392 | this.logger.info('Saving results') 393 | }) 394 | } 395 | 396 | initializeV6 () { 397 | if (this.db.pragma('user_version')[0].user_version !== 5) return 398 | 399 | this.logger.info('Setting up database v6') 400 | 401 | this.transaction(() => { 402 | this.db.pragma('user_version = 6') 403 | 404 | const height = this.db.prepare('SELECT height FROM crawl WHERE role = \'tip\'').raw(true).all()[0] 405 | const hash = this.db.prepare('SELECT hash FROM crawl WHERE role = \'tip\'').raw(true).all()[0] 406 | 407 | this.db.prepare('DROP TABLE crawl').run() 408 | 409 | this.db.prepare( 410 | `CREATE TABLE IF NOT EXISTS crawl ( 411 | key TEXT UNIQUE, 412 | value TEXT 413 | )` 414 | ).run() 415 | 416 | this.db.prepare('INSERT INTO crawl (key, value) VALUES (\'height\', ?)').run(height.toString()) 417 | this.db.prepare('INSERT INTO crawl (key, value) VALUES (\'hash\', ?)').run(hash) 418 | 419 | this.logger.info('Saving results') 420 | }) 421 | } 422 | 423 | initializeV7 () { 424 | if (this.db.pragma('user_version')[0].user_version !== 6) return 425 | 426 | this.logger.info('Setting up database v7') 427 | 428 | this.transaction(() => { 429 | this.db.pragma('user_version = 7') 430 | 431 | this.logger.info('Getting possible transactions to execute') 432 | const stmt = this.db.prepare(` 433 | SELECT txid 434 | FROM tx 435 | WHERE downloaded = 1 436 | AND executable = 1 437 | AND executed = 0 438 | AND (has_code = 0 OR (SELECT COUNT(*) FROM trust WHERE trust.txid = tx.txid AND trust.value = 1) = 1) 439 | AND txid NOT IN ban 440 | `) 441 | const txids = stmt.raw(true).all().map(x => x[0]) 442 | 443 | const isReadyToExecuteStmt = this.db.prepare(IS_READY_TO_EXECUTE_SQL) 444 | 445 | const ready = [] 446 | for (let i = 0; i < txids.length; i++) { 447 | const txid = txids[i] 448 | const row = isReadyToExecuteStmt.get(txid) 449 | if (row && row.ready) ready.push(txid) 450 | if (i % 1000 === 0) console.log('Checking to execute', i, 'of', txids.length) 451 | } 452 | 453 | this.logger.info('Marking', ready.length, 'transactions to execute') 454 | this.db.prepare('CREATE TABLE IF NOT EXISTS executing (txid TEXT UNIQUE)').run() 455 | const markExecutingStmt = this.db.prepare('INSERT OR IGNORE INTO executing (txid) VALUES (?)') 456 | ready.forEach(txid => markExecutingStmt.run(txid)) 457 | 458 | this.logger.info('Saving results') 459 | }) 460 | } 461 | 462 | async close () { 463 | if (this.worker) { 464 | this.logger.debug('Terminating background loader') 465 | await this.worker.terminate() 466 | this.worker = null 467 | } 468 | 469 | if (this.db) { 470 | this.logger.debug('Closing' + (this.readonly ? ' readonly' : '') + ' database') 471 | this.db.close() 472 | this.db = null 473 | } 474 | } 475 | 476 | transaction (f) { 477 | if (!this.db) return 478 | this.db.transaction(f)() 479 | } 480 | 481 | // -------------------------------------------------------------------------- 482 | // tx 483 | // -------------------------------------------------------------------------- 484 | 485 | addBlock (txids, txhexs, height, hash, time) { 486 | this.transaction(() => { 487 | txids.forEach((txid, i) => { 488 | const txhex = txhexs && txhexs[i] 489 | this.addTransaction(txid, txhex, height, time) 490 | }) 491 | this.setHeight(height) 492 | this.setHash(hash) 493 | }) 494 | } 495 | 496 | addTransaction (txid, txhex, height, time) { 497 | this.transaction(() => { 498 | this.addNewTransaction(txid) 499 | if (height) this.setTransactionHeight(txid, height) 500 | if (time) this.setTransactionTime(txid, time) 501 | }) 502 | 503 | const downloaded = this.isTransactionDownloaded(txid) 504 | if (downloaded) return 505 | 506 | if (txhex) { 507 | this.parseAndStoreTransaction(txid, txhex) 508 | } else { 509 | if (this.onRequestDownload) this.onRequestDownload(txid) 510 | } 511 | } 512 | 513 | parseAndStoreTransaction (txid, hex) { 514 | if (this.isTransactionDownloaded(txid)) return 515 | 516 | let metadata = null 517 | let bsvtx = null 518 | const inputs = [] 519 | const outputs = [] 520 | 521 | try { 522 | if (!hex) throw new Error('No hex') 523 | 524 | bsvtx = new bsv.Transaction(hex) 525 | 526 | bsvtx.inputs.forEach(input => { 527 | const location = `${input.prevTxId.toString('hex')}_o${input.outputIndex}` 528 | inputs.push(location) 529 | }) 530 | 531 | bsvtx.outputs.forEach((output, n) => { 532 | if (output.script.isDataOut() || output.script.isSafeDataOut()) return 533 | outputs.push(`${txid}_o${n}`) 534 | }) 535 | 536 | metadata = Run.util.metadata(hex) 537 | } catch (e) { 538 | this.logger.error(`${txid} => ${e.message}`) 539 | this.storeParsedNonExecutableTransaction(txid, hex, inputs, outputs) 540 | return 541 | } 542 | 543 | const deps = new Set() 544 | 545 | for (let i = 0; i < metadata.in; i++) { 546 | const prevtxid = bsvtx.inputs[i].prevTxId.toString('hex') 547 | deps.add(prevtxid) 548 | } 549 | 550 | for (const ref of metadata.ref) { 551 | if (ref.startsWith('native://')) { 552 | continue 553 | } else if (ref.includes('berry')) { 554 | const reftxid = ref.slice(0, 64) 555 | deps.add(reftxid) 556 | } else { 557 | const reftxid = ref.slice(0, 64) 558 | deps.add(reftxid) 559 | } 560 | } 561 | 562 | const hasCode = metadata.exec.some(cmd => cmd.op === 'DEPLOY' || cmd.op === 'UPGRADE') 563 | 564 | this.storeParsedExecutableTransaction(txid, hex, hasCode, deps, inputs, outputs) 565 | 566 | for (const deptxid of deps) { 567 | if (!this.isTransactionDownloaded(deptxid)) { 568 | if (this.onRequestDownload) this.onRequestDownload(deptxid) 569 | } 570 | } 571 | } 572 | 573 | addNewTransaction (txid) { 574 | if (this.hasTransaction(txid)) return 575 | 576 | const time = Math.round(Date.now() / 1000) 577 | 578 | this.addNewTransactionStmt.run(txid, time) 579 | 580 | if (this.onAddTransaction) this.onAddTransaction(txid) 581 | } 582 | 583 | setTransactionHeight (txid, height) { 584 | this.setTransactionHeightStmt.run(height, txid) 585 | } 586 | 587 | setTransactionTime (txid, time) { 588 | this.setTransactionTimeStmt.run(time, txid) 589 | } 590 | 591 | storeParsedNonExecutableTransaction (txid, hex, inputs, outputs) { 592 | this.transaction(() => { 593 | const bytes = Buffer.from(hex, 'hex') 594 | this.setTransactionBytesStmt.run(bytes, txid) 595 | this.setTransactionExecutableStmt.run(0, txid) 596 | 597 | inputs.forEach(location => this.setSpendStmt.run(location, txid)) 598 | outputs.forEach(location => this.setUnspentStmt.run(location)) 599 | }) 600 | 601 | // Non-executable might be berry data. We execute once we receive them. 602 | const downstreamReadyToExecute = this.getDownstreamReadyToExecuteStmt.raw(true).all(txid).map(x => x[0]) 603 | downstreamReadyToExecute.forEach(downtxid => { 604 | this.markExecutingStmt.run(downtxid) 605 | if (this.onReadyToExecute) this.onReadyToExecute(downtxid) 606 | }) 607 | } 608 | 609 | storeParsedExecutableTransaction (txid, hex, hasCode, deps, inputs, outputs) { 610 | this.transaction(() => { 611 | const bytes = Buffer.from(hex, 'hex') 612 | this.setTransactionBytesStmt.run(bytes, txid) 613 | this.setTransactionExecutableStmt.run(1, txid) 614 | this.setTransactionHasCodeStmt.run(hasCode ? 1 : 0, txid) 615 | 616 | inputs.forEach(location => this.setSpendStmt.run(location, txid)) 617 | outputs.forEach(location => this.setUnspentStmt.run(location)) 618 | 619 | for (const deptxid of deps) { 620 | this.addNewTransaction(deptxid) 621 | this.addDepStmt.run(deptxid, txid) 622 | 623 | if (this.getTransactionFailedStmt.get(deptxid).failed) { 624 | this.setTransactionExecutionFailed(txid) 625 | return 626 | } 627 | } 628 | }) 629 | 630 | this._checkExecutability(txid) 631 | } 632 | 633 | storeExecutedTransaction (txid, result) { 634 | const { cache, classes, locks, scripthashes } = result 635 | 636 | this.transaction(() => { 637 | this.setTransactionExecutedStmt.run(1, txid) 638 | this.setTransactionIndexedStmt.run(1, txid) 639 | this.unmarkExecutingStmt.run(txid) 640 | 641 | for (const key of Object.keys(cache)) { 642 | if (key.startsWith('jig://')) { 643 | const location = key.slice('jig://'.length) 644 | this.setJigStateStmt.run(location, JSON.stringify(cache[key])) 645 | continue 646 | } 647 | 648 | if (key.startsWith('berry://')) { 649 | const location = key.slice('berry://'.length) 650 | this.setBerryStateStmt.run(location, JSON.stringify(cache[key])) 651 | continue 652 | } 653 | } 654 | 655 | for (const [location, cls] of classes) { 656 | this.setJigClassStmt.run(cls, location) 657 | } 658 | 659 | for (const [location, lock] of locks) { 660 | this.setJigLockStmt.run(lock, location) 661 | } 662 | 663 | for (const [location, scripthash] of scripthashes) { 664 | this.setJigScripthashStmt.run(scripthash, location) 665 | } 666 | }) 667 | 668 | const downstreamReadyToExecute = this.getDownstreamReadyToExecuteStmt.raw(true).all(txid).map(x => x[0]) 669 | downstreamReadyToExecute.forEach(downtxid => { 670 | this.markExecutingStmt.run(downtxid) 671 | if (this.onReadyToExecute) this.onReadyToExecute(downtxid) 672 | }) 673 | } 674 | 675 | setTransactionExecutionFailed (txid) { 676 | this.transaction(() => { 677 | this.setTransactionExecutableStmt.run(0, txid) 678 | this.setTransactionExecutedStmt.run(1, txid) 679 | this.setTransactionIndexedStmt.run(0, txid) 680 | this.unmarkExecutingStmt.run(txid) 681 | }) 682 | 683 | // We try executing downstream transactions if this was marked executable but it wasn't. 684 | // This allows an admin to manually change executable status in the database. 685 | 686 | let executable = false 687 | try { 688 | const rawtx = this.getTransactionHex(txid) 689 | Run.util.metadata(rawtx) 690 | executable = true 691 | } catch (e) { } 692 | 693 | if (!executable) { 694 | const downstream = this.getDownstreamStmt.raw(true).all(txid).map(x => x[0]) 695 | downstream.forEach(downtxid => this._checkExecutability(downtxid)) 696 | } 697 | } 698 | 699 | getTransactionHex (txid) { 700 | const row = this.getTransactionHexStmt.raw(true).get(txid) 701 | return row && row[0] 702 | } 703 | 704 | getTransactionTime (txid) { 705 | const row = this.getTransactionTimeStmt.raw(true).get(txid) 706 | return row && row[0] 707 | } 708 | 709 | getTransactionHeight (txid) { 710 | const row = this.getTransactionHeightStmt.raw(true).get(txid) 711 | return row && row[0] 712 | } 713 | 714 | deleteTransaction (txid, deleted = new Set()) { 715 | if (deleted.has(txid)) return 716 | 717 | const txids = [txid] 718 | deleted.add(txid) 719 | 720 | this.transaction(() => { 721 | while (txids.length) { 722 | const txid = txids.shift() 723 | 724 | if (this.onDeleteTransaction) this.onDeleteTransaction(txid) 725 | 726 | this.deleteTransactionStmt.run(txid) 727 | this.deleteJigStatesStmt.run(txid) 728 | this.deleteBerryStatesStmt.run(txid) 729 | this.deleteSpendsStmt.run(txid) 730 | this.unspendOutputsStmt.run(txid) 731 | this.deleteDepsStmt.run(txid) 732 | 733 | const downtxids = this.getDownstreamStmt.raw(true).all(txid).map(row => row[0]) 734 | 735 | for (const downtxid of downtxids) { 736 | if (deleted.has(downtxid)) continue 737 | deleted.add(downtxid) 738 | txids.push(downtxid) 739 | } 740 | } 741 | }) 742 | } 743 | 744 | unconfirmTransaction (txid) { 745 | this.unconfirmTransactionStmt.run(txid) 746 | } 747 | 748 | unindexTransaction (txid) { 749 | this.transaction(() => { 750 | if (this.getTransactionIndexedStmt.raw(true).get(txid)[0]) { 751 | this.setTransactionExecutedStmt.run(0, txid) 752 | this.setTransactionIndexedStmt.run(0, txid) 753 | this.deleteJigStatesStmt.run(txid) 754 | this.deleteBerryStatesStmt.run(txid) 755 | this.unmarkExecutingStmt.run(txid) 756 | 757 | const downtxids = this.getDownstreamStmt.raw(true).all(txid).map(row => row[0]) 758 | downtxids.forEach(downtxid => this.unindexTransaction(downtxid)) 759 | 760 | if (this.onUnindexTransaction) this.onUnindexTransaction(txid) 761 | } 762 | }) 763 | } 764 | 765 | hasTransaction (txid) { return !!this.hasTransactionStmt.get(txid) } 766 | isTransactionDownloaded (txid) { 767 | const result = this.getTransactionDownloadedStmt.raw(true).get(txid) 768 | return result && !!result[0] 769 | } 770 | 771 | getTransactionsAboveHeight (height) { return this.getTransactionsAboveHeightStmt.raw(true).all(height).map(row => row[0]) } 772 | getMempoolTransactionsBeforeTime (time) { return this.getMempoolTransactionsBeforeTimeStmt.raw(true).all(time).map(row => row[0]) } 773 | getTransactionsToDownload () { return this.getTransactionsToDownloadStmt.raw(true).all().map(row => row[0]) } 774 | getDownloadedCount () { return this.getTransactionsDownloadedCountStmt.get().count } 775 | getIndexedCount () { return this.getTransactionsIndexedCountStmt.get().count } 776 | 777 | // -------------------------------------------------------------------------- 778 | // spends 779 | // -------------------------------------------------------------------------- 780 | 781 | getSpend (location) { 782 | const row = this.getSpendStmt.raw(true).get(location) 783 | return row && row[0] 784 | } 785 | 786 | // -------------------------------------------------------------------------- 787 | // deps 788 | // -------------------------------------------------------------------------- 789 | 790 | addDep (txid, deptxid) { 791 | this.addNewTransaction(deptxid) 792 | 793 | this.addDepStmt.run(deptxid, txid) 794 | 795 | if (this.getTransactionFailedStmt.get(deptxid).failed) { 796 | this.setTransactionExecutionFailed(deptxid) 797 | } 798 | } 799 | 800 | addMissingDeps (txid, deptxids) { 801 | this.transaction(() => deptxids.forEach(deptxid => this.addDep(txid, deptxid))) 802 | 803 | this._checkExecutability(txid) 804 | } 805 | 806 | // -------------------------------------------------------------------------- 807 | // jig 808 | // -------------------------------------------------------------------------- 809 | 810 | getJigState (location) { 811 | const row = this.getJigStateStmt.raw(true).get(location) 812 | return row && row[0] 813 | } 814 | 815 | // -------------------------------------------------------------------------- 816 | // unspent 817 | // -------------------------------------------------------------------------- 818 | 819 | getAllUnspent () { 820 | return this.getAllUnspentStmt.raw(true).all().map(row => row[0]) 821 | } 822 | 823 | getAllUnspentByClassOrigin (origin) { 824 | return this.getAllUnspentByClassStmt.raw(true).all(origin).map(row => row[0]) 825 | } 826 | 827 | getAllUnspentByLockOrigin (origin) { 828 | return this.getAllUnspentByLockStmt.raw(true).all(origin).map(row => row[0]) 829 | } 830 | 831 | getAllUnspentByScripthash (scripthash) { 832 | return this.getAllUnspentByScripthashStmt.raw(true).all(scripthash).map(row => row[0]) 833 | } 834 | 835 | getAllUnspentByClassOriginAndLockOrigin (clsOrigin, lockOrigin) { 836 | return this.getAllUnspentByClassLockStmt.raw(true).all(clsOrigin, lockOrigin).map(row => row[0]) 837 | } 838 | 839 | getAllUnspentByClassOriginAndScripthash (clsOrigin, scripthash) { 840 | return this.getAllUnspentByClassScripthashStmt.raw(true).all(clsOrigin, scripthash).map(row => row[0]) 841 | } 842 | 843 | getAllUnspentByLockOriginAndScripthash (lockOrigin, scripthash) { 844 | return this.getAllUnspentByLockScripthashStmt.raw(true).all(lockOrigin, scripthash).map(row => row[0]) 845 | } 846 | 847 | getAllUnspentByClassOriginAndLockOriginAndScripthash (clsOrigin, lockOrigin, scripthash) { 848 | return this.getAllUnspentByClassLockScripthashStmt.raw(true).all(clsOrigin, lockOrigin, scripthash).map(row => row[0]) 849 | } 850 | 851 | getNumUnspent () { 852 | return this.getNumUnspentStmt.get().unspent 853 | } 854 | 855 | // -------------------------------------------------------------------------- 856 | // berry 857 | // -------------------------------------------------------------------------- 858 | 859 | getBerryState (location) { 860 | const row = this.getBerryStateStmt.raw(true).get(location) 861 | return row && row[0] 862 | } 863 | 864 | // -------------------------------------------------------------------------- 865 | // trust 866 | // -------------------------------------------------------------------------- 867 | 868 | isTrusted (txid) { 869 | const row = this.isTrustedStmt.raw(true).get(txid) 870 | return !!row && !!row[0] 871 | } 872 | 873 | trust (txid) { 874 | if (this.isTrusted(txid)) return 875 | 876 | const trusted = [txid] 877 | 878 | // Recursively trust code parents 879 | const queue = this.getUpstreamUnexecutedCodeStmt.raw(true).all(txid).map(x => x[0]) 880 | const visited = new Set() 881 | while (queue.length) { 882 | const uptxid = queue.shift() 883 | if (visited.has(uptxid)) continue 884 | if (this.isTrusted(uptxid)) continue 885 | visited.add(uptxid) 886 | trusted.push(txid) 887 | this.getUpstreamUnexecutedCodeStmt.raw(true).all(txid).forEach(x => queue.push(x[0])) 888 | } 889 | 890 | this.transaction(() => trusted.forEach(txid => this.setTrustedStmt.run(txid, 1))) 891 | 892 | trusted.forEach(txid => this._checkExecutability(txid)) 893 | 894 | if (this.onTrustTransaction) trusted.forEach(txid => this.onTrustTransaction(txid)) 895 | } 896 | 897 | untrust (txid) { 898 | if (!this.isTrusted(txid)) return 899 | this.transaction(() => { 900 | this.unindexTransaction(txid) 901 | this.setTrustedStmt.run(txid, 0) 902 | }) 903 | if (this.onUntrustTransaction) this.onUntrustTransaction(txid) 904 | } 905 | 906 | getTrustlist () { 907 | return this.getTrustlistStmt.raw(true).all().map(x => x[0]) 908 | } 909 | 910 | // -------------------------------------------------------------------------- 911 | // ban 912 | // -------------------------------------------------------------------------- 913 | 914 | isBanned (txid) { 915 | const row = this.isBannedStmt.raw(true).get(txid) 916 | return !!row && !!row[0] 917 | } 918 | 919 | ban (txid) { 920 | this.transaction(() => { 921 | this.unindexTransaction(txid) 922 | this.banStmt.run(txid) 923 | }) 924 | if (this.onBanTransaction) this.onBanTransaction(txid) 925 | } 926 | 927 | unban (txid) { 928 | this.unbanStmt.run(txid) 929 | this._checkExecutability(txid) 930 | if (this.onUnbanTransaction) this.onUnbanTransaction(txid) 931 | } 932 | 933 | getBanlist () { 934 | return this.getBanlistStmt.raw(true).all().map(x => x[0]) 935 | } 936 | 937 | // -------------------------------------------------------------------------- 938 | // crawl 939 | // -------------------------------------------------------------------------- 940 | 941 | getHeight () { 942 | const row = this.getHeightStmt.raw(true).all()[0] 943 | return row && parseInt(row[0]) 944 | } 945 | 946 | getHash () { 947 | const row = this.getHashStmt.raw(true).all()[0] 948 | return row && row[0] 949 | } 950 | 951 | setHeight (height) { 952 | this.setHeightStmt.run(height.toString()) 953 | } 954 | 955 | setHash (hash) { 956 | this.setHashStmt.run(hash) 957 | } 958 | 959 | retryTx (txid) { 960 | const txids = [txid] 961 | const missing = new Set(txids) 962 | this.setTransactionExecutedStmt.run(0, txid) 963 | this.setTransactionIndexedStmt.run(0, txid) 964 | 965 | while (missing.size > 0) { 966 | Array.from(missing).forEach(txid => { 967 | const row = this.isReadyToExecuteStmt.get(txid) 968 | if (row && row.ready) { 969 | missing.delete(txid) 970 | if (this.onReadyToExecute) this.onReadyToExecute(txid) 971 | } else { 972 | this.unmarkExecutingStmt.run(txid) 973 | missing.delete(txid) 974 | const depTxids = this.getUpstreamStmt.raw(true).all(txid).map(r => r[0]) 975 | depTxids.forEach(depTxid => { 976 | // Because we are retrying, we execute again failed deps. 977 | if (this.getTransactionFailedStmt.get(depTxid).failed) { 978 | this.setTransactionExecutedStmt.run(0, depTxid) 979 | this.setTransactionIndexedStmt.run(0, depTxid) 980 | this.setTransactionExecutableStmt.run(1, depTxid) 981 | } 982 | if (this.getTransactionExecutableStmt.get(depTxid).executable) { 983 | missing.add(depTxid) 984 | this.markExecutingStmt.run(depTxid) 985 | } 986 | }) 987 | } 988 | }) 989 | } 990 | } 991 | 992 | // -------------------------------------------------------------------------- 993 | // internal 994 | // -------------------------------------------------------------------------- 995 | 996 | loadTransactionsToExecute () { 997 | this.logger.debug('Loading transactions to execute') 998 | const txids = this.db.prepare('SELECT txid FROM executing').raw(true).all().map(x => x[0]) 999 | txids.forEach(txid => this._checkExecutability(txid)) 1000 | } 1001 | 1002 | _checkExecutability (txid) { 1003 | const row = this.isReadyToExecuteStmt.get(txid) 1004 | if (row && row.ready) { 1005 | this.markExecutingStmt.run(txid) 1006 | if (this.onReadyToExecute) this.onReadyToExecute(txid) 1007 | } 1008 | } 1009 | } 1010 | 1011 | // ------------------------------------------------------------------------------------------------ 1012 | 1013 | Database.HEIGHT_MEMPOOL = HEIGHT_MEMPOOL 1014 | Database.HEIGHT_UNKNOWN = HEIGHT_UNKNOWN 1015 | 1016 | module.exports = Database 1017 | -------------------------------------------------------------------------------- /src/direct-server-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * direct-server-worker.js 3 | * 4 | * Internal worker thread that runs the user server 5 | */ 6 | 7 | const { parentPort, workerData } = require('worker_threads') 8 | const Server = require('./server') 9 | const Bus = require('./bus') 10 | const Database = require('./database') 11 | 12 | const logger = { 13 | info: (...args) => Bus.sendRequest(parentPort, 'info', ...args), 14 | warn: (...args) => Bus.sendRequest(parentPort, 'warn', ...args), 15 | error: (...args) => Bus.sendRequest(parentPort, 'error', ...args), 16 | debug: (...args) => Bus.sendRequest(parentPort, 'debug', ...args) 17 | } 18 | 19 | const readonly = true 20 | const database = new Database(workerData.dbPath, logger, readonly) 21 | const server = new Server(database, logger, workerData.port) 22 | 23 | database.trust = (txid) => Bus.sendRequest(parentPort, 'trust', txid) 24 | database.ban = (txid) => Bus.sendRequest(parentPort, 'ban', txid) 25 | database.addTransaction = (txid, hex) => Bus.sendRequest(parentPort, 'addTransaction', txid, hex) 26 | database.untrust = (txid) => Bus.sendRequest(parentPort, 'untrust', txid) 27 | database.unban = (txid) => Bus.sendRequest(parentPort, 'unban', txid) 28 | database.deleteTransaction = (txid) => Bus.sendRequest(parentPort, 'deleteTransaction', txid) 29 | 30 | Bus.listen(parentPort, { start, stop }) 31 | 32 | async function start () { 33 | await database.open() 34 | await server.start() 35 | } 36 | 37 | async function stop () { 38 | await server.stop() 39 | await database.close() 40 | } 41 | -------------------------------------------------------------------------------- /src/direct-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * direct-server.js 3 | * 4 | * Serves GET requets directly from the database and proxies other requests to the normal server 5 | */ 6 | 7 | const { Worker } = require('worker_threads') 8 | const Bus = require('./bus') 9 | 10 | class DirectServer { 11 | constructor (dbPath, port, logger, database) { 12 | this.dbPath = dbPath 13 | this.port = port 14 | this.logger = logger 15 | this.database = database 16 | this.worker = null 17 | } 18 | 19 | async start () { 20 | if (this.worker) return 21 | const path = require.resolve('./direct-server-worker.js') 22 | const workerData = { dbPath: this.dbPath, port: this.port } 23 | this.worker = new Worker(path, { workerData }) 24 | 25 | const handlers = { 26 | info: this.logger.info.bind(this.logger), 27 | warn: this.logger.warn.bind(this.logger), 28 | error: this.logger.error.bind(this.logger), 29 | debug: this.logger.debug.bind(this.logger), 30 | trust: this.database.trust.bind(this.database), 31 | ban: this.database.ban.bind(this.database), 32 | addTransaction: this.database.addTransaction.bind(this.database), 33 | untrust: this.database.untrust.bind(this.database), 34 | unban: this.database.unban.bind(this.database), 35 | deleteTransaction: this.database.deleteTransaction.bind(this.database) 36 | } 37 | 38 | Bus.listen(this.worker, handlers) 39 | 40 | await Bus.sendRequest(this.worker, 'start') 41 | } 42 | 43 | async stop () { 44 | if (!this.worker) return 45 | await Bus.sendRequest(this.worker, 'stop') 46 | await this.worker.terminate() 47 | this.worker = null 48 | } 49 | } 50 | 51 | module.exports = DirectServer 52 | -------------------------------------------------------------------------------- /src/downloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * downloader.js 3 | * 4 | * Downloads transactions 5 | */ 6 | 7 | // ------------------------------------------------------------------------------------------------ 8 | // Downloader 9 | // ------------------------------------------------------------------------------------------------ 10 | 11 | class Downloader { 12 | constructor (fetchFunction, numParallelDownloads) { 13 | this.onDownloadTransaction = null 14 | this.onFailedToDownloadTransaction = null 15 | this.onRetryingDownload = null 16 | 17 | this.fetchFunction = fetchFunction 18 | this.numParallelDownloads = numParallelDownloads 19 | 20 | this.queued = new Set() // txid 21 | this.fetching = new Set() // txid 22 | this.waitingToRetry = new Set() // txid 23 | this.attempts = new Map() // txid -> attempts 24 | } 25 | 26 | stop () { 27 | this.queued = new Set() 28 | this.fetching = new Set() 29 | this.waitingToRetry = new Set() 30 | this.attempts = new Map() 31 | } 32 | 33 | add (txid) { 34 | if (this.has(txid)) return 35 | if (!this.fetchFunction) return 36 | 37 | this._enqueueFetch(txid) 38 | } 39 | 40 | _enqueueFetch (txid) { 41 | if (this.fetching.size >= this.numParallelDownloads) { 42 | this.queued.add(txid) 43 | } else { 44 | this._fetch(txid) 45 | } 46 | } 47 | 48 | remove (txid) { 49 | if (!this.has(txid)) return 50 | this.queued.delete(txid) 51 | this.fetching.delete(txid) 52 | this.waitingToRetry.delete(txid) 53 | this.attempts.delete(txid) 54 | } 55 | 56 | has (txid) { 57 | return this.queued.has(txid) || this.fetching.has(txid) || this.waitingToRetry.has(txid) 58 | } 59 | 60 | remaining () { 61 | return this.queued.size + this.fetching.size + this.waitingToRetry.size 62 | } 63 | 64 | async _fetch (txid) { 65 | this.fetching.add(txid) 66 | 67 | try { 68 | const { hex, height, time } = await this.fetchFunction(txid) 69 | 70 | this._onFetchSucceed(txid, hex, height, time) 71 | } catch (e) { 72 | this._onFetchFailed(txid, e) 73 | } finally { 74 | this._fetchNextInQueue() 75 | } 76 | } 77 | 78 | _onFetchSucceed (txid, hex, height, time) { 79 | if (!this.fetching.delete(txid)) return 80 | 81 | this.attempts.delete(txid) 82 | 83 | if (this.onDownloadTransaction) this.onDownloadTransaction(txid, hex, height, time) 84 | } 85 | 86 | _onFetchFailed (txid, e) { 87 | if (!this.fetching.delete(txid)) return 88 | 89 | if (this.onFailedToDownloadTransaction) this.onFailedToDownloadTransaction(txid, e) 90 | 91 | const attempts = (this.attempts.get(txid) || 0) + 1 92 | const secondsToRetry = Math.pow(2, attempts) 93 | 94 | if (this.onRetryingDownload) this.onRetryingDownload(txid, secondsToRetry) 95 | 96 | this.attempts.set(txid, attempts) 97 | this.waitingToRetry.add(txid) 98 | 99 | setTimeout(() => { 100 | if (this.waitingToRetry.delete(txid)) { 101 | this._enqueueFetch(txid) 102 | } 103 | }, secondsToRetry * 1000) 104 | } 105 | 106 | _fetchNextInQueue () { 107 | if (!this.queued.size) return 108 | 109 | const txid = this.queued.keys().next().value 110 | this.queued.delete(txid) 111 | 112 | this._fetch(txid) 113 | } 114 | } 115 | 116 | // ------------------------------------------------------------------------------------------------ 117 | 118 | module.exports = Downloader 119 | -------------------------------------------------------------------------------- /src/executor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * executor.js 3 | * 4 | * Executes RUN transactions and calculates state 5 | */ 6 | 7 | const { Worker } = require('worker_threads') 8 | const Bus = require('./bus') 9 | 10 | // ------------------------------------------------------------------------------------------------ 11 | // Executor 12 | // ------------------------------------------------------------------------------------------------ 13 | 14 | class Executor { 15 | constructor (network, numWorkers, database, logger) { 16 | this.network = network 17 | this.numWorkers = numWorkers 18 | this.database = database 19 | this.logger = logger 20 | 21 | this.onIndexed = null 22 | this.onExecuteFailed = null 23 | this.onMissingDeps = null 24 | 25 | this.workers = [] 26 | this.workerRequests = [] 27 | this.executing = new Set() 28 | } 29 | 30 | start () { 31 | for (let i = 0; i < this.numWorkers; i++) { 32 | this.logger.debug('Starting worker', i) 33 | 34 | const path = require.resolve('./worker.js') 35 | 36 | const worker = new Worker(path, { workerData: { id: i, network: this.network } }) 37 | 38 | worker.id = i 39 | worker.available = true 40 | worker.missingDeps = new Set() 41 | 42 | this.workers.push(worker) 43 | 44 | const cacheGet = (txid) => this._onCacheGet(txid) 45 | const blockchainFetch = (txid) => this._onBlockchainFetch(worker, txid) 46 | const handlers = { cacheGet, blockchainFetch } 47 | 48 | Bus.listen(worker, handlers) 49 | 50 | if (this.workerRequests.length) { 51 | worker.available = false 52 | this.workerRequests.shift()(worker) 53 | } 54 | } 55 | } 56 | 57 | async stop () { 58 | this.logger.debug('Stopping all workers') 59 | 60 | await Promise.all(this.workers.map(worker => worker.terminate())) 61 | 62 | this.workers = [] 63 | this.workerRequests = [] 64 | } 65 | 66 | async execute (txid) { 67 | if (this.executing.has(txid)) return 68 | 69 | this.logger.debug('Enqueueing', txid, 'for execution') 70 | 71 | this.executing.add(txid) 72 | 73 | const worker = await this._requestWorker() 74 | 75 | worker.missingDeps = new Set() 76 | 77 | const hex = this.database.getTransactionHex(txid) 78 | const trustlist = this.database.getTrustlist() 79 | 80 | try { 81 | const result = await Bus.sendRequest(worker, 'execute', txid, hex, trustlist) 82 | 83 | if (this.onIndexed) this.onIndexed(txid, result) 84 | } catch (e) { 85 | if (worker.missingDeps.size) { 86 | if (this.onMissingDeps) this.onMissingDeps(txid, Array.from(worker.missingDeps)) 87 | } else { 88 | if (this.onExecuteFailed) this.onExecuteFailed(txid, e) 89 | } 90 | } finally { 91 | this.executing.delete(txid) 92 | 93 | worker.available = true 94 | 95 | if (this.workerRequests.length) { 96 | worker.available = false 97 | this.workerRequests.shift()(worker) 98 | } 99 | } 100 | } 101 | 102 | _requestWorker () { 103 | const worker = this.workers.find(worker => worker.available) 104 | 105 | if (worker) { 106 | worker.available = false 107 | return worker 108 | } 109 | 110 | return new Promise((resolve, reject) => { 111 | this.workerRequests.push(resolve) 112 | }) 113 | } 114 | 115 | _onCacheGet (key) { 116 | if (key.startsWith('jig://')) { 117 | const state = this.database.getJigState(key.slice('jig://'.length)) 118 | if (state) return JSON.parse(state) 119 | } 120 | if (key.startsWith('berry://')) { 121 | const state = this.database.getBerryState(key.slice('berry://'.length)) 122 | if (state) return JSON.parse(state) 123 | } 124 | if (key.startsWith('tx://')) { 125 | return this.database.getTransactionHex(key.slice('tx://'.length)) 126 | } 127 | } 128 | 129 | _onBlockchainFetch (worker, txid) { 130 | const hex = this.database.getTransactionHex(txid) 131 | if (hex) return hex 132 | worker.missingDeps.add(txid) 133 | throw new Error(`Not found: ${txid}`) 134 | } 135 | 136 | _onTrustlistGet () { 137 | return this.database.getTrustlist() 138 | } 139 | } 140 | 141 | // ------------------------------------------------------------------------------------------------ 142 | 143 | module.exports = Executor 144 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | * 4 | * Entry point 5 | */ 6 | 7 | const Indexer = require('./indexer') 8 | const Server = require('./server') 9 | const { 10 | API, DB, NETWORK, PORT, FETCH_LIMIT, WORKERS, MATTERCLOUD_KEY, PLANARIA_TOKEN, START_HEIGHT, 11 | MEMPOOL_EXPIRATION, ZMQ_URL, RPC_URL, DEFAULT_TRUSTLIST, DEBUG, SERVE_ONLY 12 | } = require('./config') 13 | const MatterCloud = require('./mattercloud') 14 | const Planaria = require('./planaria') 15 | const RunConnectFetcher = require('./run-connect') 16 | const BitcoinNodeConnection = require('./bitcoin-node-connection') 17 | const BitcoinRpc = require('./bitcoin-rpc') 18 | const BitcoinZmq = require('./bitcoin-zmq') 19 | const Database = require('./database') 20 | const DirectServer = require('./direct-server') 21 | 22 | // ------------------------------------------------------------------------------------------------ 23 | // Globals 24 | // ------------------------------------------------------------------------------------------------ 25 | 26 | const logger = {} 27 | logger.info = console.info.bind(console) 28 | logger.warn = console.warn.bind(console) 29 | logger.error = console.error.bind(console) 30 | logger.debug = DEBUG ? console.debug.bind(console) : () => {} 31 | 32 | let api = null 33 | switch (API) { 34 | case 'mattercloud': api = new MatterCloud(MATTERCLOUD_KEY, logger); break 35 | case 'planaria': api = new Planaria(PLANARIA_TOKEN, logger); break 36 | case 'bitcoin-node': 37 | if (ZMQ_URL === null) { 38 | throw new Error('please specify ZQM_URL when using bitcoin-node API') 39 | } 40 | 41 | if (RPC_URL === null) { 42 | throw new Error('please specify RPC_URL when using bitcoin-node API') 43 | } 44 | api = new BitcoinNodeConnection(new BitcoinZmq(ZMQ_URL), new BitcoinRpc(RPC_URL)) 45 | break 46 | case 'run': api = new RunConnectFetcher(); break 47 | case 'none': api = {}; break 48 | default: throw new Error(`Unknown API: ${API}`) 49 | } 50 | 51 | const readonly = !!SERVE_ONLY 52 | const database = new Database(DB, logger, readonly) 53 | 54 | const indexer = new Indexer(database, api, NETWORK, FETCH_LIMIT, WORKERS, logger, 55 | START_HEIGHT, MEMPOOL_EXPIRATION, DEFAULT_TRUSTLIST) 56 | 57 | const server = SERVE_ONLY 58 | ? new Server(database, logger, PORT) 59 | : new DirectServer(DB, PORT, logger, database) 60 | 61 | let started = false 62 | 63 | // ------------------------------------------------------------------------------------------------ 64 | // main 65 | // ------------------------------------------------------------------------------------------------ 66 | 67 | async function main () { 68 | database.open() 69 | 70 | if (!SERVE_ONLY) { 71 | await indexer.start() 72 | } 73 | 74 | await server.start() 75 | 76 | started = true 77 | } 78 | 79 | // ------------------------------------------------------------------------------------------------ 80 | // shutdown 81 | // ------------------------------------------------------------------------------------------------ 82 | 83 | async function shutdown () { 84 | if (!started) return 85 | 86 | logger.debug('Shutting down') 87 | 88 | started = false 89 | 90 | await server.stop() 91 | 92 | if (!SERVE_ONLY) { 93 | await indexer.stop() 94 | } 95 | 96 | await database.close() 97 | 98 | process.exit(0) 99 | } 100 | 101 | // ------------------------------------------------------------------------------------------------ 102 | 103 | process.on('SIGTERM', shutdown) 104 | process.on('SIGINT', shutdown) 105 | 106 | main() 107 | -------------------------------------------------------------------------------- /src/indexer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * indexer.js 3 | * 4 | * Main object that discovers, downloads, executes and stores RUN transactions 5 | */ 6 | 7 | const Database = require('./database') 8 | const Downloader = require('./downloader') 9 | const Executor = require('./executor') 10 | const Crawler = require('./crawler') 11 | 12 | // ------------------------------------------------------------------------------------------------ 13 | // Indexer 14 | // ------------------------------------------------------------------------------------------------ 15 | 16 | class Indexer { 17 | constructor (database, api, network, numParallelDownloads, numParallelExecutes, logger, startHeight, mempoolExpiration, defaultTrustlist) { 18 | this.onDownload = null 19 | this.onFailToDownload = null 20 | this.onIndex = null 21 | this.onFailToIndex = null 22 | this.onBlock = null 23 | this.onReorg = null 24 | 25 | this.logger = logger 26 | this.database = database 27 | this.api = api 28 | this.network = network 29 | this.startHeight = startHeight 30 | this.mempoolExpiration = mempoolExpiration 31 | this.defaultTrustlist = defaultTrustlist 32 | 33 | const fetchFunction = this.api.fetch ? this.api.fetch.bind(this.api) : null 34 | 35 | this.downloader = new Downloader(fetchFunction, numParallelDownloads) 36 | this.executor = new Executor(network, numParallelExecutes, this.database, this.logger) 37 | this.crawler = new Crawler(api, this.logger) 38 | 39 | this.database.onReadyToExecute = this._onReadyToExecute.bind(this) 40 | this.database.onAddTransaction = this._onAddTransaction.bind(this) 41 | this.database.onDeleteTransaction = this._onDeleteTransaction.bind(this) 42 | this.database.onTrustTransaction = this._onTrustTransaction.bind(this) 43 | this.database.onUntrustTransaction = this._onUntrustTransaction.bind(this) 44 | this.database.onBanTransaction = this._onBanTransaction.bind(this) 45 | this.database.onUnbanTransaction = this._onUnbanTransaction.bind(this) 46 | this.database.onUnindexTransaction = this._onUnindexTransaction.bind(this) 47 | this.database.onRequestDownload = this._onRequestDownload.bind(this) 48 | this.downloader.onDownloadTransaction = this._onDownloadTransaction.bind(this) 49 | this.downloader.onFailedToDownloadTransaction = this._onFailedToDownloadTransaction.bind(this) 50 | this.downloader.onRetryingDownload = this._onRetryingDownload.bind(this) 51 | this.executor.onIndexed = this._onIndexed.bind(this) 52 | this.executor.onExecuteFailed = this._onExecuteFailed.bind(this) 53 | this.executor.onMissingDeps = this._onMissingDeps.bind(this) 54 | this.crawler.onCrawlError = this._onCrawlError.bind(this) 55 | this.crawler.onCrawlBlockTransactions = this._onCrawlBlockTransactions.bind(this) 56 | this.crawler.onRewindBlocks = this._onRewindBlocks.bind(this) 57 | this.crawler.onMempoolTransaction = this._onMempoolTransaction.bind(this) 58 | this.crawler.onExpireMempoolTransactions = this._onExpireMempoolTransactions.bind(this) 59 | } 60 | 61 | async start () { 62 | this.logger.debug('Starting indexer') 63 | 64 | this.executor.start() 65 | this.defaultTrustlist.forEach(txid => this.database.trust(txid)) 66 | this.database.loadTransactionsToExecute() 67 | const height = this.database.getHeight() || this.startHeight 68 | const hash = this.database.getHash() 69 | if (this.api.connect) await this.api.connect(height, this.network) 70 | 71 | this.logger.debug('Loading transactions to download') 72 | this.database.getTransactionsToDownload().forEach(txid => this.downloader.add(txid)) 73 | 74 | this.crawler.start(height, hash) 75 | } 76 | 77 | async stop () { 78 | this.crawler.stop() 79 | if (this.api.disconnect) await this.api.disconnect() 80 | this.downloader.stop() 81 | await this.executor.stop() 82 | } 83 | 84 | _onDownloadTransaction (txid, hex, height, time) { 85 | this.logger.info(`Downloaded ${txid} (${this.downloader.remaining()} remaining)`) 86 | if (!this.database.hasTransaction(txid)) return 87 | if (height) this.database.setTransactionHeight(txid, height) 88 | if (time) this.database.setTransactionTime(txid, time) 89 | this.database.parseAndStoreTransaction(txid, hex) 90 | if (this.onDownload) this.onDownload(txid) 91 | } 92 | 93 | _onFailedToDownloadTransaction (txid, e) { 94 | this.logger.error('Failed to download', txid, e.toString()) 95 | if (this.onFailToDownload) this.onFailToDownload(txid) 96 | } 97 | 98 | _onRetryingDownload (txid, secondsToRetry) { 99 | this.logger.info('Retrying download', txid, 'after', secondsToRetry, 'seconds') 100 | } 101 | 102 | _onIndexed (txid, result) { 103 | if (!this.database.hasTransaction(txid)) return // Check not re-orged 104 | this.logger.info(`Executed ${txid}`) 105 | this.database.storeExecutedTransaction(txid, result) 106 | if (this.onIndex) this.onIndex(txid) 107 | } 108 | 109 | _onExecuteFailed (txid, e) { 110 | this.logger.error(`Failed to execute ${txid}: ${e.toString()}`) 111 | this.database.setTransactionExecutionFailed(txid) 112 | if (this.onFailToIndex) this.onFailToIndex(txid, e) 113 | } 114 | 115 | _onReadyToExecute (txid) { 116 | this.executor.execute(txid) 117 | } 118 | 119 | _onAddTransaction (txid) { 120 | this.logger.info('Added', txid) 121 | } 122 | 123 | _onDeleteTransaction (txid) { 124 | this.logger.info('Removed', txid) 125 | this.downloader.remove(txid) 126 | } 127 | 128 | _onTrustTransaction (txid) { 129 | this.logger.info('Trusted', txid) 130 | } 131 | 132 | _onUntrustTransaction (txid) { 133 | this.logger.info('Untrusted', txid) 134 | } 135 | 136 | _onBanTransaction (txid) { 137 | this.logger.info('Banned', txid) 138 | } 139 | 140 | _onUnbanTransaction (txid) { 141 | this.logger.info('Unbanned', txid) 142 | } 143 | 144 | _onUnindexTransaction (txid) { 145 | this.logger.info('Unindexed', txid) 146 | } 147 | 148 | _onRequestDownload (txid) { 149 | this.downloader.add(txid) 150 | } 151 | 152 | _onMissingDeps (txid, deptxids) { 153 | this.logger.debug(`Discovered ${deptxids.length} dep(s) for ${txid}`) 154 | this.database.addMissingDeps(txid, deptxids) 155 | deptxids.forEach(deptxid => this.downloader.add(deptxid)) 156 | } 157 | 158 | _onCrawlError (e) { 159 | this.logger.error(`Crawl error: ${e.toString()}`) 160 | } 161 | 162 | _onCrawlBlockTransactions (height, hash, time, txids, txhexs) { 163 | this.logger.info(`Crawled block ${height} for ${txids.length} transactions`) 164 | this.database.addBlock(txids, txhexs, height, hash, time) 165 | if (this.onBlock) this.onBlock(height) 166 | } 167 | 168 | _onRewindBlocks (newHeight) { 169 | this.logger.info(`Rewinding to block ${newHeight}`) 170 | 171 | const txids = this.database.getTransactionsAboveHeight(newHeight) 172 | 173 | this.database.transaction(() => { 174 | // Put all transactions back into the mempool. This is better than deleting them, because 175 | // when we assume they will just go into a different block, we don't need to re-execute. 176 | // If they don't make it into a block, then they will be expired in time. 177 | txids.forEach(txid => this.database.unconfirmTransaction(txid)) 178 | 179 | this.database.setHeight(newHeight) 180 | this.database.setHash(null) 181 | }) 182 | 183 | if (this.onReorg) this.onReorg(newHeight) 184 | } 185 | 186 | _onMempoolTransaction (txid, hex) { 187 | this.database.addTransaction(txid, hex, Database.HEIGHT_MEMPOOL, null) 188 | } 189 | 190 | _onExpireMempoolTransactions () { 191 | const expirationTime = Math.round(Date.now() / 1000) - this.mempoolExpiration 192 | 193 | const expired = this.database.getMempoolTransactionsBeforeTime(expirationTime) 194 | const deleted = new Set() 195 | this.database.transaction(() => expired.forEach(txid => this.database.deleteTransaction(txid, deleted))) 196 | } 197 | } 198 | 199 | // ------------------------------------------------------------------------------------------------ 200 | 201 | module.exports = Indexer 202 | -------------------------------------------------------------------------------- /src/mattercloud.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mattercloud.js 3 | * 4 | * MatterCloud API 5 | */ 6 | 7 | const axios = require('axios') 8 | const bsv = require('bsv') 9 | global.EventSource = require('eventsource') 10 | const { default: ReconnectingEventSource } = require('reconnecting-eventsource') 11 | 12 | // ------------------------------------------------------------------------------------------------ 13 | // Globals 14 | // ------------------------------------------------------------------------------------------------ 15 | 16 | const RUN_0_6_FILTER = '006a0372756e0105' 17 | 18 | // ------------------------------------------------------------------------------------------------ 19 | // MatterCloud 20 | // ------------------------------------------------------------------------------------------------ 21 | 22 | class MatterCloud { 23 | constructor (apiKey, logger) { 24 | this.suffix = apiKey ? `?api_key=${apiKey}` : '' 25 | this.logger = logger 26 | this.mempoolEvents = null 27 | } 28 | 29 | async connect (height, network) { 30 | if (network !== 'main') throw new Error(`Network not supported with MatterCloud: ${network}`) 31 | } 32 | 33 | async disconnect () { 34 | if (this.mempoolEvents) { 35 | this.mempoolEvents.close() 36 | this.mempoolEvents = null 37 | } 38 | } 39 | 40 | async fetch (txid) { 41 | const response = await axios.get(`https://api.mattercloud.net/api/v3/main/tx/${txid}${this.suffix}`) 42 | 43 | const hex = response.data.rawtx 44 | const height = response.data.blockheight === 0 ? -1 : response.data.blockheight 45 | const time = response.data.blocktime === 0 ? null : response.data.blocktime 46 | 47 | if (typeof hex === 'undefined') throw new Error(`MatterCloud API did not return hex for ${txid}`) 48 | if (typeof height === 'undefined') throw new Error(`MatterCloud API did not return blockheight for ${txid}`) 49 | if (typeof time === 'undefined') throw new Error(`MatterCloud API did not return blocktime for ${txid}`) 50 | 51 | return { hex, height, time } 52 | } 53 | 54 | async getNextBlock (currHeight, currHash) { 55 | const height = currHeight + 1 56 | let hash = null 57 | 58 | try { 59 | const response = await axios.get(`https://bfs.mattercloud.io/height/${height}${this.suffix}`) 60 | hash = response.data.blockhash 61 | } catch (e) { 62 | if (e.response && e.response.status === 404) return undefined 63 | throw e 64 | } 65 | 66 | try { 67 | const response = await axios.get(`https://bfs.mattercloud.io/block/${hash}/tx/filter/${RUN_0_6_FILTER}${this.suffix}`) 68 | 69 | const prevHash = response.data.header.prevHash 70 | if (currHash && prevHash !== currHash) return { reorg: true } 71 | 72 | const txhexs = response.data.tx.map(tx => tx.raw) 73 | const txids = txhexs.map(hex => new bsv.Transaction(hex).hash) 74 | const time = response.data.header.time 75 | return { height, hash, time, txids, txhexs } 76 | } catch (e) { 77 | if (e.response && e.response.status === 404) return undefined 78 | throw e 79 | } 80 | } 81 | 82 | async listenForMempool (mempoolTxCallback) { 83 | this.logger.info('Listening for mempool via MatterCloud SSE') 84 | 85 | return new Promise((resolve, reject) => { 86 | this.mempoolEvents = new ReconnectingEventSource(`https://stream.mattercloud.io/mempool?filter=${RUN_0_6_FILTER}`) 87 | 88 | this.mempoolEvents.onerror = (e) => reject(e) 89 | 90 | this.mempoolEvents.onmessage = event => { 91 | if (event.type === 'message') { 92 | const data = JSON.parse(event.data) 93 | 94 | if (data === 'connected') { 95 | resolve() 96 | return 97 | } 98 | 99 | mempoolTxCallback(data.h, data.raw) 100 | } 101 | } 102 | }) 103 | } 104 | } 105 | 106 | // ------------------------------------------------------------------------------------------------ 107 | 108 | module.exports = MatterCloud 109 | -------------------------------------------------------------------------------- /src/planaria.js: -------------------------------------------------------------------------------- 1 | /** 2 | * planaria.js 3 | * 4 | * Bitbus and Bitsocket API. Uses the RUN API to fetch transactions. 5 | * 6 | * Note: Bitbus does not return transactions with more than 100 outputs. Because of this, 7 | * some transactions may get discovered later when they are spent and will not be immediately 8 | * indexed by RUN. They may not also have proper heights. We recommend using MatterCloud for 9 | * production services. 10 | */ 11 | 12 | const fetch = require('node-fetch') 13 | const AbortController = require('abort-controller') 14 | const es = require('event-stream') 15 | global.EventSource = require('eventsource') 16 | const { default: ReconnectingEventSource } = require('reconnecting-eventsource') 17 | const RunConnectFetcher = require('./run-connect') 18 | 19 | // ------------------------------------------------------------------------------------------------ 20 | // Globals 21 | // ------------------------------------------------------------------------------------------------ 22 | 23 | const RUN_PREFIX = 'run' 24 | const RUN_VERSION = '05' 25 | 26 | // ------------------------------------------------------------------------------------------------ 27 | // Planaria 28 | // ------------------------------------------------------------------------------------------------ 29 | 30 | class Planaria { 31 | constructor (token, logger) { 32 | this.token = token 33 | this.logger = logger 34 | this.abortController = new AbortController() 35 | this.recrawlInterveral = 30000 36 | this.maxReorgDepth = 10 37 | this.runConnectFetcher = new RunConnectFetcher() 38 | 39 | this.txns = [] 40 | this.network = null 41 | this.mempoolEvents = null 42 | this.recrawlTimerId = null 43 | this.lastCrawlHeight = null 44 | this.pendingReorg = false 45 | } 46 | 47 | async connect (height, network) { 48 | if (network !== 'main') throw new Error(`Network not supported with Planaria: ${network}`) 49 | 50 | this.runConnectFetcher.connect(height, network) 51 | 52 | this.network = network 53 | this.lastCrawlHeight = height 54 | this.logger.info('Crawling for new blocks via BitBus') 55 | await this._recrawl() 56 | } 57 | 58 | async disconnect () { 59 | clearTimeout(this.recrawlTimerId) 60 | 61 | this.abortController.abort() 62 | 63 | if (this.mempoolEvents) { 64 | this.mempoolEvents.close() 65 | this.mempoolEvents = null 66 | } 67 | } 68 | 69 | async fetch (txid) { 70 | // Planaria doesn't have a fetch endpoint, so we use RUN Connect 71 | return await this.runConnectFetcher.fetch(txid) 72 | } 73 | 74 | async getNextBlock (currHeight, currHash) { 75 | // If we don't have that hash we're looking for next, reorg for safety 76 | if (currHash && this.txns.length && !this.txns.some(txn => txn.hash === currHash)) { 77 | this.logger.info('Reorging due to missing internal block data') 78 | return { reorg: true } 79 | } 80 | 81 | // Notify if we've detected a reorg 82 | if (this.pendingReorg) { 83 | this.logger.info('Detected reorg from planaria transaction data') 84 | this.pendingReorg = false 85 | return { reorg: true } 86 | } 87 | 88 | // Remove all transactions that are not realistically reorg-able 89 | while (this.txns.length && this.txns[0].height <= currHeight - this.maxReorgDepth) { 90 | this.txns.shift() 91 | } 92 | 93 | let i = 0 94 | while (i < this.txns.length && this.txns[i].height <= currHeight) { i++ } 95 | if (i === this.txns.length) return null 96 | 97 | const block = { 98 | height: this.txns[i].height, 99 | hash: this.txns[i].hash, 100 | time: this.txns[i].time, 101 | txids: [] 102 | } 103 | 104 | while (i < this.txns.length && this.txns[i].height === block.height) { 105 | block.txids.push(this.txns[i].txid) 106 | i++ 107 | } 108 | 109 | return block 110 | } 111 | 112 | async listenForMempool (mempoolTxCallback) { 113 | this.logger.info('Listening for mempool via BitSocket') 114 | 115 | const query = { 116 | v: 3, 117 | q: { 118 | find: { 119 | 'out.s2': RUN_PREFIX, 120 | 'out.h3': RUN_VERSION 121 | }, 122 | project: { 'tx.h': 1 } 123 | } 124 | } 125 | 126 | const b64query = Buffer.from(JSON.stringify(query), 'utf8').toString('base64') 127 | 128 | return new Promise((resolve, reject) => { 129 | const url = `https://txo.bitsocket.network/s/${b64query}` 130 | 131 | this.mempoolEvents = new ReconnectingEventSource(url) 132 | 133 | this.mempoolEvents.onerror = (e) => reject(e) 134 | 135 | this.mempoolEvents.onmessage = event => { 136 | if (event.type === 'message') { 137 | const data = JSON.parse(event.data) 138 | 139 | if (data.type === 'open') { 140 | resolve() 141 | } 142 | 143 | if (data.type === 'push') { 144 | for (let i = 0; i < data.data.length; i++) { 145 | mempoolTxCallback(data.data[i].tx.h, null) 146 | } 147 | } 148 | } 149 | } 150 | }) 151 | } 152 | 153 | async _recrawl () { 154 | const scheduleRecrawl = () => { 155 | this.recrawlTimerId = setTimeout(this._recrawl.bind(this), this.recrawlInterveral) 156 | } 157 | 158 | return this._crawl() 159 | .then(() => { 160 | scheduleRecrawl() 161 | }) 162 | .catch(e => { 163 | this.logger.error(e) 164 | this.logger.info('Retrying crawl in ' + this.recrawlInterveral / 1000 + ' seconds') 165 | scheduleRecrawl() 166 | }) 167 | } 168 | 169 | async _crawl () { 170 | this.logger.info('Recrawling planaria') 171 | 172 | const query = { 173 | q: { 174 | find: { 175 | 'out.s2': RUN_PREFIX, 176 | 'out.h3': RUN_VERSION, 177 | 'blk.i': { $gt: this.lastCrawlHeight - this.maxReorgDepth } 178 | }, 179 | sort: { 'blk.i': 1 }, 180 | project: { blk: 1, 'tx.h': 1 } 181 | } 182 | } 183 | 184 | const headers = { 185 | 'Content-type': 'application/json; charset=utf-8', 186 | token: this.token 187 | } 188 | 189 | const options = { 190 | method: 'post', 191 | headers, 192 | body: JSON.stringify(query), 193 | signal: this.abortController.signal 194 | } 195 | 196 | return new Promise((resolve, reject) => { 197 | fetch('https://txo.bitbus.network/block', options) 198 | .then(res => { 199 | // Accumulate a block's transaction into a pending list until we reach the next block 200 | // or the end of the stream. That way, we don't start pulling from the block when it's 201 | // only been partially added and accidentally miss transactions. 202 | let pending = [] 203 | 204 | const addTx = json => { 205 | if (!json.length) return 206 | 207 | const data = JSON.parse(json) 208 | 209 | // If there are pending transactions, check if we are on a new block 210 | if (pending.length && data.blk.i > pending[0].height) { 211 | this.txns = this.txns.concat(pending) 212 | this.lastCrawlHeight = pending[0].height 213 | pending = [] 214 | } 215 | 216 | // Check that the transactions we are adding do not reorg 217 | if (this.txns.length) { 218 | const lastTx = this.txns[this.txns.length - 1] 219 | 220 | // We only add txns that are add to the height 221 | if (data.blk.i < lastTx.height) { 222 | return 223 | } 224 | 225 | // Don't add transactions if we already have them 226 | if (data.blk.i === lastTx.height && data.blk.h === lastTx.hash) { 227 | return 228 | } 229 | 230 | // Check for reorgs 231 | if (data.blk.i === lastTx.height && data.blk.h !== lastTx.hash) { 232 | this.pendingReorg = true 233 | this.txns = this.txns.slice(0, this.txns.findIndex(tx => tx.height === data.blk.h)) 234 | } 235 | } 236 | 237 | pending.push({ height: data.blk.i, hash: data.blk.h, time: data.blk.t, txid: data.tx.h }) 238 | } 239 | 240 | const finish = () => { 241 | if (pending.length) { 242 | this.txns = this.txns.concat(pending) 243 | this.lastCrawlHeight = pending[0].height 244 | pending = [] 245 | } 246 | 247 | resolve() 248 | } 249 | 250 | res.body 251 | .pipe(es.split()) 252 | .pipe(es.mapSync(addTx)) 253 | .on('end', finish) 254 | }) 255 | .catch(e => e.name === 'AbortError' ? resolve() : reject(e)) 256 | }) 257 | }; 258 | } 259 | 260 | // ------------------------------------------------------------------------------------------------ 261 | 262 | module.exports = Planaria 263 | -------------------------------------------------------------------------------- /src/retry-tx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | * 4 | * Entry point 5 | */ 6 | 7 | const Indexer = require('./indexer') 8 | const { 9 | DB, NETWORK, FETCH_LIMIT, WORKERS, START_HEIGHT, 10 | MEMPOOL_EXPIRATION, DEFAULT_TRUSTLIST, DEBUG, SERVE_ONLY 11 | } = require('./config') 12 | const RunConnectFetcher = require('./run-connect') 13 | const Database = require('./database') 14 | 15 | // ------------------------------------------------------------------------------------------------ 16 | // Globals 17 | // ------------------------------------------------------------------------------------------------ 18 | 19 | const logger = {} 20 | logger.info = console.info.bind(console) 21 | logger.warn = console.warn.bind(console) 22 | logger.error = console.error.bind(console) 23 | logger.debug = DEBUG ? console.debug.bind(console) : () => {} 24 | 25 | const api = new RunConnectFetcher() 26 | 27 | const database = new Database(DB, logger, false) 28 | 29 | const indexer = new Indexer(database, api, NETWORK, FETCH_LIMIT, WORKERS, logger, 30 | START_HEIGHT, MEMPOOL_EXPIRATION, DEFAULT_TRUSTLIST) 31 | 32 | // ------------------------------------------------------------------------------------------------ 33 | // main 34 | // ------------------------------------------------------------------------------------------------ 35 | 36 | async function main () { 37 | const targetTxid = process.argv[2] 38 | if (!targetTxid) { 39 | console.log('please specify a txid: "npm run retryTx "') 40 | process.exit(1) 41 | } 42 | console.log(`re executing tx: ${targetTxid}`) 43 | database.open() 44 | 45 | const promise = new Promise(resolve => { 46 | indexer.onIndex = (txid) => txid === targetTxid && resolve() 47 | }) 48 | 49 | if (!SERVE_ONLY) { 50 | await indexer.start() 51 | } 52 | 53 | database.retryTx(targetTxid) 54 | await promise 55 | await indexer.stop() 56 | await database.close() 57 | process.exit(0) 58 | } 59 | 60 | // ------------------------------------------------------------------------------------------------ 61 | // shutdown 62 | // ------------------------------------------------------------------------------------------------ 63 | 64 | async function shutdown () { 65 | await indexer.stop() 66 | await database.close() 67 | process.exit(0) 68 | } 69 | 70 | // ------------------------------------------------------------------------------------------------ 71 | 72 | process.on('SIGTERM', shutdown) 73 | process.on('SIGINT', shutdown) 74 | 75 | main() 76 | -------------------------------------------------------------------------------- /src/run-connect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * run-connect.js 3 | * 4 | * Run Connect API. Currently it only supports fetches. 5 | */ 6 | 7 | const axios = require('axios') 8 | 9 | // ------------------------------------------------------------------------------------------------ 10 | // RunConnectFetcher 11 | // ------------------------------------------------------------------------------------------------ 12 | 13 | class RunConnectFetcher { 14 | async connect (height, network) { 15 | this.network = network 16 | } 17 | 18 | async fetch (txid) { 19 | const response = await axios.get(`https://api.run.network/v1/${this.network}/tx/${txid}`) 20 | const hex = response.data.hex 21 | const height = typeof response.data.blockheight === 'number' ? response.data.blockheight : null 22 | const time = typeof response.data.blocktime === 'number' ? response.data.blocktime : null 23 | return { hex, height, time } 24 | } 25 | } 26 | 27 | // ------------------------------------------------------------------------------------------------ 28 | 29 | module.exports = RunConnectFetcher 30 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * server.js 3 | * 4 | * Express server that exposes the Indexer 5 | */ 6 | 7 | const express = require('express') 8 | const morgan = require('morgan') 9 | const bodyParser = require('body-parser') 10 | const bsv = require('bsv') 11 | const crypto = require('crypto') 12 | const cors = require('cors') 13 | const { Writable } = require('stream') 14 | const Run = require('run-sdk') 15 | 16 | // ------------------------------------------------------------------------------------------------ 17 | // Globals 18 | // ------------------------------------------------------------------------------------------------ 19 | 20 | const calculateScripthash = x => crypto.createHash('sha256').update(Buffer.from(x, 'hex')).digest().reverse().toString('hex') 21 | 22 | // ------------------------------------------------------------------------------------------------ 23 | // Server 24 | // ------------------------------------------------------------------------------------------------ 25 | 26 | class Server { 27 | constructor (database, logger, port) { 28 | this.database = database 29 | this.logger = logger 30 | this.port = port 31 | this.listener = null 32 | this.onListening = null 33 | } 34 | 35 | start () { 36 | this.logger.debug('Starting server') 37 | 38 | const app = express() 39 | 40 | let buffer = '' 41 | const write = (chunk, encoding, callback) => { 42 | buffer = buffer + chunk.toString() 43 | const lines = buffer.split(/\r\n|\n\r|\n|\r/) 44 | for (let i = 0; i < lines.length - 1; i++) { 45 | this.logger.info(lines[i]) 46 | } 47 | buffer = lines[lines.length - 1] 48 | callback() 49 | return true 50 | } 51 | app.use(morgan('tiny', { stream: new Writable({ write }) })) 52 | 53 | app.use(bodyParser.text({ limit: '25mb' })) 54 | app.use(bodyParser.json({ limit: '10mb' })) 55 | 56 | app.use(cors()) 57 | 58 | app.get('/jig/:location', this.getJig.bind(this)) 59 | app.get('/berry/:location', this.getBerry.bind(this)) 60 | app.get('/tx/:txid', this.getTx.bind(this)) 61 | app.get('/time/:txid', this.getTime.bind(this)) 62 | app.get('/spends/:location', this.getSpends.bind(this)) 63 | app.get('/unspent', this.getUnspent.bind(this)) 64 | app.get('/trust/:txid?', this.getTrust.bind(this)) 65 | app.get('/ban/:txid?', this.getBan.bind(this)) 66 | app.get('/status', this.getStatus.bind(this)) 67 | 68 | app.post('/trust/:txid?', this.postTrust.bind(this)) 69 | app.post('/ban/:txid', this.postBan.bind(this)) 70 | app.post('/tx', this.postTx.bind(this)) 71 | app.post('/tx/:txid', this.postTx.bind(this)) // Keeping this for retro compatibility. 72 | 73 | app.delete('/trust/:txid', this.deleteTrust.bind(this)) 74 | app.delete('/ban/:txid', this.deleteBan.bind(this)) 75 | app.delete('/tx/:txid', this.deleteTx.bind(this)) 76 | 77 | app.use((err, req, res, next) => { 78 | if (this.logger) this.logger.error(err.stack) 79 | res.status(500).send('Something broke!') 80 | }) 81 | 82 | this.listener = app.listen(this.port, () => { 83 | if (this.logger) this.logger.info(`Listening at http://localhost:${this.listener.address().port}`) 84 | this.port = this.listener.address().port 85 | if (this.onListening) this.onListening() 86 | }) 87 | } 88 | 89 | stop () { 90 | if (!this.listener) return 91 | this.listener.close() 92 | this.listener = null 93 | } 94 | 95 | async getJig (req, res, next) { 96 | try { 97 | const state = this.database.getJigState(req.params.location) 98 | if (state) { 99 | res.setHeader('Content-Type', 'application/json') 100 | res.send(state) 101 | } else { 102 | res.status(404).send(`Not found: ${req.params.location}\n`) 103 | } 104 | } catch (e) { next(e) } 105 | } 106 | 107 | async getBerry (req, res, next) { 108 | try { 109 | const state = this.database.getBerryState(req.params.location) 110 | if (state) { 111 | res.setHeader('Content-Type', 'application/json') 112 | res.send(state) 113 | } else { 114 | res.status(404).send(`Not found: ${req.params.location}\n`) 115 | } 116 | } catch (e) { next(e) } 117 | } 118 | 119 | async getTx (req, res, next) { 120 | try { 121 | const txid = this._parseTxid(req.params.txid) 122 | const rawtx = this.database.getTransactionHex(txid) 123 | if (rawtx) { 124 | res.send(rawtx) 125 | } else { 126 | res.status(404).send(`Not found: ${req.params.txid}\n`) 127 | } 128 | } catch (e) { next(e) } 129 | } 130 | 131 | async getTime (req, res, next) { 132 | try { 133 | const txid = this._parseTxid(req.params.txid) 134 | const time = this.database.getTransactionTime(txid) 135 | if (time) { 136 | res.json(time) 137 | } else { 138 | res.status(404).send(`Not found: ${req.params.txid}\n`) 139 | } 140 | } catch (e) { next(e) } 141 | } 142 | 143 | async getSpends (req, res, next) { 144 | try { 145 | const txid = this.database.getSpend(req.params.location) 146 | if (txid) { 147 | res.send(txid) 148 | } else { 149 | res.status(404).send(`Not spent: ${req.params.location}\n`) 150 | } 151 | } catch (e) { next(e) } 152 | } 153 | 154 | async getUnspent (req, res, next) { 155 | try { 156 | const cls = req.query.class 157 | const lock = req.query.lock 158 | let scripthash = req.query.scripthash 159 | if (req.query.address) scripthash = calculateScripthash(new Run.util.CommonLock(req.query.address).script()) 160 | if (req.query.pubkey) scripthash = calculateScripthash(new Run.util.CommonLock(req.query.pubkey).script()) 161 | 162 | if (cls && lock && scripthash) { 163 | res.json(this.database.getAllUnspentByClassOriginAndLockOriginAndScripthash(cls, lock, scripthash)) 164 | } else if (cls && lock) { 165 | res.json(this.database.getAllUnspentByClassOriginAndLockOrigin(cls, lock)) 166 | } else if (cls && scripthash) { 167 | res.json(this.database.getAllUnspentByClassOriginAndScripthash(cls, scripthash)) 168 | } else if (lock && scripthash) { 169 | res.json(this.database.getAllUnspentByLockOriginAndScripthash(lock, scripthash)) 170 | } else if (scripthash) { 171 | res.json(this.database.getAllUnspentByScripthash(scripthash)) 172 | } else if (lock) { 173 | res.json(this.database.getAllUnspentByLockOrigin(lock)) 174 | } else if (cls) { 175 | res.json(this.database.getAllUnspentByClassOrigin(cls)) 176 | } else { 177 | res.json(this.database.getAllUnspent()) 178 | } 179 | } catch (e) { next(e) } 180 | } 181 | 182 | async getTrust (req, res, next) { 183 | try { 184 | if (req.params.txid) { 185 | res.json(this.database.isTrusted(req.params.txid)) 186 | } else { 187 | res.json(Array.from(this.database.getTrustlist())) 188 | } 189 | } catch (e) { next(e) } 190 | } 191 | 192 | async getBan (req, res, next) { 193 | try { 194 | if (req.params.txid) { 195 | res.json(this.database.isBanned(req.params.txid)) 196 | } else { 197 | res.json(Array.from(this.database.getBanlist())) 198 | } 199 | } catch (e) { next(e) } 200 | } 201 | 202 | async getStatus (req, res, next) { 203 | try { 204 | const status = { 205 | height: this.database.getHeight(), 206 | hash: this.database.getHash() 207 | } 208 | res.json(status) 209 | } catch (e) { next(e) } 210 | } 211 | 212 | async postTrust (req, res, next) { 213 | try { 214 | if (Array.isArray(req.body)) { 215 | req.body.forEach(txid => { 216 | txid = this._parseTxid(txid) 217 | this.database.trust(txid) 218 | }) 219 | res.send(`Trusted ${req.body.length} transactions\n`) 220 | } else { 221 | const txid = this._parseTxid(req.params.txid) 222 | this.database.trust(txid) 223 | res.send(`Trusted ${req.params.txid}\n`) 224 | } 225 | } catch (e) { next(e) } 226 | } 227 | 228 | async postBan (req, res, next) { 229 | try { 230 | const txid = this._parseTxid(req.params.txid) 231 | this.database.ban(txid) 232 | res.send(`Banned ${req.params.txid}\n`) 233 | } catch (e) { next(e) } 234 | } 235 | 236 | async postTx (req, res, next) { 237 | try { 238 | if (typeof req.body !== 'string') { 239 | throw new Error('missing rawtx') 240 | } 241 | const hex = req.body 242 | const bsvtx = new bsv.Transaction(hex) 243 | 244 | this.database.addTransaction(bsvtx.hash, hex) 245 | res.send(`Added ${bsvtx.hash}\n`) 246 | } catch (e) { next(e) } 247 | } 248 | 249 | async deleteTrust (req, res, next) { 250 | try { 251 | const txid = this._parseTxid(req.params.txid) 252 | this.database.untrust(txid) 253 | res.send(`Untrusted ${req.params.txid}\n`) 254 | } catch (e) { next(e) } 255 | } 256 | 257 | async deleteBan (req, res, next) { 258 | try { 259 | const txid = this._parseTxid(req.params.txid) 260 | this.database.unban(txid) 261 | res.send(`Unbanned ${req.params.txid}\n`) 262 | } catch (e) { next(e) } 263 | } 264 | 265 | async deleteTx (req, res, next) { 266 | try { 267 | const txid = this._parseTxid(req.params.txid) 268 | this.database.deleteTransaction(txid) 269 | res.send(`Removed ${req.params.txid}\n`) 270 | } catch (e) { next(e) } 271 | } 272 | 273 | _parseTxid (txid) { 274 | txid = txid.trim().toLowerCase() 275 | if (!/^[0-9a-f]{64}$/.test(txid)) throw new Error('Not a txid: ' + txid) 276 | return txid 277 | } 278 | } 279 | 280 | // ------------------------------------------------------------------------------------------------ 281 | 282 | module.exports = Server 283 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * worker.js 3 | * 4 | * Background worker that executes RUN transactions 5 | */ 6 | 7 | const { parentPort, workerData } = require('worker_threads') 8 | const crypto = require('crypto') 9 | const Run = require('run-sdk') 10 | const bsv = require('bsv') 11 | const Bus = require('./bus') 12 | 13 | // ------------------------------------------------------------------------------------------------ 14 | // Startup 15 | // ------------------------------------------------------------------------------------------------ 16 | 17 | const network = workerData.network 18 | 19 | Bus.listen(parentPort, { execute }) 20 | 21 | // On Node 15+, when the Blockchain fetch method throws for missing dependencies, it causes 22 | // and unhandled promise rejection error. However, it can't reproduce outside of Run-DB. 23 | // This needs investigation. Perhaps it's related to the worker thread. Perhaps something else. 24 | process.on('unhandledRejection', (e) => { 25 | console.warn('Unhandled promise rejection', e) 26 | }) 27 | 28 | // ------------------------------------------------------------------------------------------------ 29 | // Cache 30 | // ------------------------------------------------------------------------------------------------ 31 | 32 | class Cache { 33 | constructor () { 34 | this.state = {} 35 | } 36 | 37 | async get (key) { 38 | if (key in this.state) { 39 | return this.state[key] 40 | } 41 | 42 | return await Bus.sendRequest(parentPort, 'cacheGet', key) 43 | } 44 | 45 | async set (key, value) { 46 | this.state[key] = value 47 | } 48 | } 49 | 50 | // ------------------------------------------------------------------------------------------------ 51 | // Blockchain 52 | // ------------------------------------------------------------------------------------------------ 53 | 54 | class Blockchain { 55 | constructor (txid) { this.txid = txid } 56 | get network () { return network } 57 | async broadcast (hex) { return this.txid } 58 | async fetch (txid) { return await Bus.sendRequest(parentPort, 'blockchainFetch', txid) } 59 | async utxos (script) { throw new Error('not implemented: utxos') } 60 | async spends (txid, vout) { throw new Error('not implemented: spends') } 61 | async time (txid) { throw new Error('not implemented: time') } 62 | } 63 | 64 | // ------------------------------------------------------------------------------------------------ 65 | // scripthash 66 | // ------------------------------------------------------------------------------------------------ 67 | 68 | const scripthash = x => crypto.createHash('sha256').update(Buffer.from(x, 'hex')).digest().reverse().toString('hex') 69 | 70 | // ------------------------------------------------------------------------------------------------ 71 | // execute 72 | // ------------------------------------------------------------------------------------------------ 73 | 74 | const run = new Run({ network, logger: null, state: new Run.plugins.LocalState() }) 75 | 76 | async function execute (txid, hex, trustlist) { 77 | run.cache = new Cache() 78 | run.blockchain = new Blockchain(txid) 79 | run.timeout = 300000 80 | run.client = false 81 | run.preverify = false 82 | trustlist.forEach(txid => run.trust(txid)) 83 | run.trust('cache') 84 | 85 | const tx = await run.import(hex, { txid }) 86 | 87 | await tx.cache() 88 | 89 | const cache = run.cache.state 90 | const jigs = tx.outputs.filter(creation => creation instanceof Run.Jig) 91 | const classes = jigs.map(jig => [jig.location, jig.constructor.origin]) 92 | const creationsWithLocks = tx.outputs.filter(creation => creation.owner instanceof Run.api.Lock) 93 | const customLocks = creationsWithLocks.map(creation => [creation.location, creation.owner]) 94 | const locks = customLocks.map(([location, lock]) => [location, lock.constructor.origin]) 95 | const creationsWithoutLocks = tx.outputs.filter(creation => typeof creation.owner === 'string') 96 | const addressify = owner => owner.length >= 64 ? new bsv.PublicKey(owner).toAddress().toString() : owner 97 | const addresses = creationsWithoutLocks.map(creation => [creation.location, addressify(creation.owner)]) 98 | const commonLocks = addresses.map(([location, address]) => [location, new Run.util.CommonLock(address)]) 99 | const scripts = customLocks.concat(commonLocks).map(([location, lock]) => [location, lock.script()]) 100 | const scripthashes = scripts.map(([location, script]) => [location, scripthash(script)]) 101 | 102 | return { cache, classes, locks, scripthashes } 103 | } 104 | 105 | // ------------------------------------------------------------------------------------------------ 106 | -------------------------------------------------------------------------------- /test/bitcoin-node-connection-test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, beforeEach } = require('mocha') 2 | const { expect } = require('chai') 3 | require('chai').use(require('chai-as-promised')) 4 | const BitcoinNodeConnection = require('../src/bitcoin-node-connection') 5 | const bsv = require('bsv') 6 | const Run = require('run-sdk') 7 | 8 | class TestBitcoinRpc { 9 | constructor () { 10 | this.knownTxs = new Map() 11 | this.unconfirmedTxs = [] 12 | this.blocks = [ 13 | { 14 | height: 1000, 15 | hash: Buffer.alloc(32).fill(1).toString('hex'), 16 | time: new Date().getTime(), 17 | txs: [], 18 | hex: buildBlock([], Buffer.alloc(32).fill(1)) 19 | } 20 | ] 21 | this.nextBlockHeight = 1001 22 | } 23 | 24 | async getRawTransaction (txid, verbose = true) { 25 | if (verbose) { 26 | return this.knownTxs.get(txid) 27 | } else { 28 | return this.knownTxs.get(txid).hex 29 | } 30 | } 31 | 32 | async getBlockCount () { 33 | return this.blocks[this.blocks.length - 1].height 34 | } 35 | 36 | async getBlockByHeight (targetHeight, verbose) { 37 | const block = this.blocks.find(block => block.height === targetHeight) 38 | if (!verbose) { 39 | return block.hex 40 | } else { 41 | return { 42 | size: block.size || block.hex.length, 43 | previousblockhash: block.previousblockhash, 44 | tx: block.txs.map(tx => tx.hash) 45 | } 46 | } 47 | } 48 | 49 | // Test 50 | 51 | registerConfirmedTx (txid, rawTx) { 52 | this.registerUnconfirmedTx(txid, rawTx) 53 | this.closeBlock(this.nextBlockHeight.toString()) 54 | } 55 | 56 | registerUnconfirmedTx (txid, rawTx) { 57 | this.knownTxs.set(txid, { 58 | hex: rawTx 59 | }) 60 | this.unconfirmedTxs.push({ txid, hex: rawTx }) 61 | } 62 | 63 | closeBlock (size = null) { 64 | const blockTime = new Date().getTime() 65 | const previousBlock = this.blocks[this.blocks.length - 1] 66 | const blockData = { 67 | height: this.nextBlockHeight, 68 | hash: null, 69 | time: blockTime, 70 | previousblockhash: previousBlock.hash, 71 | txs: [] 72 | } 73 | 74 | if (size !== null) { 75 | blockData.size = size 76 | } 77 | 78 | this.nextBlockHeight = this.nextBlockHeight + 1 79 | while (this.unconfirmedTxs.length > 0) { 80 | const { txid, hex } = this.unconfirmedTxs.pop() 81 | const tx = { 82 | txid, 83 | hex, 84 | blockheight: blockData.height, 85 | blocktime: blockData.time 86 | } 87 | this.knownTxs.set(txid, tx) 88 | blockData.txs.push(new bsv.Transaction(tx.hex)) 89 | } 90 | const bsvBlock = buildBlock(blockData.txs, blockData.previousblockhash) 91 | this.blocks.push({ 92 | ...blockData, 93 | hex: bsvBlock.toBuffer().toString('hex'), 94 | hash: bsvBlock.hash 95 | }) 96 | } 97 | } 98 | 99 | class TestZmq { 100 | constructor () { 101 | this.handler = null 102 | this.pendingTxs = [] 103 | } 104 | 105 | subscribeRawTx (handler) { 106 | this.handler = handler 107 | } 108 | 109 | // test 110 | 111 | publishTx (tx) { 112 | this.pendingTxs.push(tx) 113 | } 114 | 115 | async processPendingTxs () { 116 | for (const tx of this.pendingTxs) { 117 | await this.handler(tx.toBuffer().toString('hex')) 118 | } 119 | } 120 | } 121 | 122 | const buildRandomTx = () => { 123 | const tx = bsv.Transaction() 124 | .from({ 125 | txId: Buffer.alloc(32).fill(1).toString('hex'), 126 | outputIndex: 0, 127 | script: bsv.Script.fromASM('0 0'), 128 | satoshis: 2000 129 | }) 130 | .to(bsv.Address.fromPrivateKey(bsv.PrivateKey.fromRandom()), 1000) 131 | 132 | return tx 133 | } 134 | 135 | const buildRandomRunTx = async (run) => { 136 | class Foo extends Run.Jig { 137 | init (attr) { 138 | this.attr = attr 139 | } 140 | } 141 | 142 | const tx = new Run.Transaction() 143 | 144 | const FooDeployed = tx.update(() => run.deploy(Foo)) 145 | tx.update(() => new FooDeployed(Math.random())) 146 | 147 | return new bsv.Transaction(await tx.export()) 148 | } 149 | 150 | const buildBlock = (transactions, prevHash = Buffer.alloc(32).fill('1'), hash) => { 151 | const block = bsv.Block.fromObject({ 152 | transactions, 153 | header: { 154 | hash, 155 | prevHash: prevHash, 156 | merkleRoot: Buffer.alloc(32).fill('2') 157 | } 158 | }) 159 | 160 | return block 161 | } 162 | 163 | describe('BitcoinNodeConnection', () => { 164 | it('initializes', () => { 165 | const bitcoinZmq = new TestZmq() 166 | const bitcoinRpc = new TestBitcoinRpc() 167 | const instance = new BitcoinNodeConnection(bitcoinZmq, bitcoinRpc) 168 | expect(instance).not.to.equal(null) 169 | }) 170 | 171 | let bitcoinZmq = null 172 | let bitcoinRpc = null 173 | let instance = null 174 | let run = null 175 | 176 | beforeEach(() => { 177 | bitcoinZmq = new TestZmq() 178 | bitcoinRpc = new TestBitcoinRpc() 179 | instance = new BitcoinNodeConnection(bitcoinZmq, bitcoinRpc) 180 | 181 | run = new Run({ 182 | purse: { 183 | pay: (rawtx) => rawtx 184 | } 185 | }) 186 | }) 187 | 188 | describe('#fetch', () => { 189 | it('returns the rawTx when the rawTx exists', async () => { 190 | const randomTx = buildRandomTx() 191 | bitcoinRpc.registerConfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 192 | 193 | const resultTxHex = await instance.fetch(randomTx.hash) 194 | expect(resultTxHex.hex).to.eql(randomTx.toBuffer().toString('hex')) 195 | }) 196 | 197 | it('returns the blocktime when the tx was confirmed', async () => { 198 | const randomTx = buildRandomTx() 199 | bitcoinRpc.registerConfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 200 | 201 | const resultTxHex = await instance.fetch(randomTx.hash) 202 | const lastBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 1] 203 | expect(resultTxHex.time).to.eql(lastBlock.time) 204 | }) 205 | 206 | it('returns the blockheight when the tx was confirmed', async () => { 207 | const randomTx = buildRandomTx() 208 | bitcoinRpc.registerConfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 209 | 210 | const resultTxHex = await instance.fetch(randomTx.hash) 211 | const lastBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 1] 212 | expect(resultTxHex.height).to.eql(lastBlock.height) 213 | }) 214 | 215 | it('returns -1 as blockheight when the tx was not confirmed', async () => { 216 | const randomTx = buildRandomTx() 217 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 218 | 219 | const resultTxHex = await instance.fetch(randomTx.hash) 220 | expect(resultTxHex.height).to.eql(-1) 221 | }) 222 | 223 | it('returns null as blocktime when the tx was not confirmed', async () => { 224 | const randomTx = buildRandomTx() 225 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 226 | 227 | const resultTxHex = await instance.fetch(randomTx.hash) 228 | expect(resultTxHex.time).to.equal(null) 229 | }) 230 | 231 | it('throws if the txid does not exist', async () => { 232 | const randomTx = buildRandomTx() 233 | // bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 234 | 235 | expect(instance.fetch(randomTx.hash)).to.eventually.throw() 236 | }) 237 | }) 238 | 239 | describe('#getNextBlock', () => { 240 | it('returns null if the height and the hash sent is the current one', async () => { 241 | const lastBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 1] 242 | const nextBlock = await instance.getNextBlock(lastBlock.height, bitcoinRpc.currentBlockHash) 243 | expect(nextBlock).to.equal(null) 244 | }) 245 | 246 | it('returns block 1001 if 1000 was sent as parameter with the right hash', async () => { 247 | const randomTx = buildRandomTx() 248 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 249 | bitcoinRpc.closeBlock() 250 | const previousBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 2] 251 | const lastBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 1] 252 | 253 | const nextBlock = await instance.getNextBlock(previousBlock.height, previousBlock.hash) 254 | expect(nextBlock.height).to.equal(lastBlock.height) 255 | }) 256 | 257 | it('returns a block with correct attributes', async () => { 258 | const randomTx = await buildRandomRunTx(run) 259 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 260 | bitcoinRpc.closeBlock() 261 | const previousBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 2] 262 | const lastBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 1] 263 | 264 | const nextBlock = await instance.getNextBlock(previousBlock.height, previousBlock.hash) 265 | expect(Object.keys(nextBlock).length).to.eql(4) 266 | expect(nextBlock.height).to.equal(lastBlock.height) 267 | expect(nextBlock.hash).to.equal(lastBlock.hash) 268 | expect(nextBlock.txids).to.eql([randomTx.hash]) 269 | expect(nextBlock.txhexs).to.eql([randomTx.toBuffer().toString('hex')]) 270 | }) 271 | 272 | it('correct block when tons of blocks exists', async () => { 273 | const randomTx = await buildRandomRunTx(run) 274 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 275 | bitcoinRpc.closeBlock() 276 | bitcoinRpc.closeBlock() 277 | bitcoinRpc.closeBlock() 278 | bitcoinRpc.closeBlock() 279 | const firstBlock = bitcoinRpc.blocks[0] 280 | const secondBlock = bitcoinRpc.blocks[1] 281 | 282 | const nextBlock = await instance.getNextBlock(firstBlock.height, null) 283 | expect(Object.keys(nextBlock).length).to.eql(4) 284 | expect(nextBlock.height).to.equal(secondBlock.height) 285 | }) 286 | 287 | it('returns reorg when the hash of the previous block doesnt match', async () => { 288 | const randomTx = buildRandomTx() 289 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 290 | bitcoinRpc.closeBlock() 291 | const previousBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 2] 292 | 293 | const nextBlock = await instance.getNextBlock(previousBlock.height, 'wronghash') 294 | expect(nextBlock).to.eql({ reorg: true }) 295 | }) 296 | 297 | it('does not returns reorg if the prev hash was null', async () => { 298 | const randomTx = buildRandomTx() 299 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 300 | bitcoinRpc.closeBlock() 301 | const previousBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 2] 302 | const lastBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 1] 303 | 304 | const nextBlock = await instance.getNextBlock(previousBlock.height, null) 305 | expect(nextBlock.hash).to.eql(lastBlock.hash) 306 | }) 307 | 308 | it('only includes txids of run txs', async () => { 309 | const randomTx = buildRandomTx() 310 | const randomRunTx = await buildRandomRunTx(run) 311 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 312 | bitcoinRpc.registerUnconfirmedTx(randomRunTx.hash, randomRunTx.toBuffer().toString('hex')) 313 | bitcoinRpc.closeBlock() 314 | const previousBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 2] 315 | 316 | const nextBlock = await instance.getNextBlock(previousBlock.height, null) 317 | expect(nextBlock.txids).to.eql([randomRunTx.hash]) 318 | }) 319 | 320 | it('trows error if block height is longer than the latest block', () => { 321 | const lastBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 1] 322 | expect(instance.getNextBlock(lastBlock.height + 1, null)).to.eventually.throw() 323 | }) 324 | 325 | it('trows error if block height is negative than the latest block', () => { 326 | expect(instance.getNextBlock(-1, null)).to.eventually.throw() 327 | }) 328 | 329 | it('does not process txs with invalid outputs for bsv1.x', async () => { 330 | const tx = bsv.Transaction() 331 | .from({ 332 | txId: Buffer.alloc(32).fill(1).toString('hex'), 333 | outputIndex: 0, 334 | script: bsv.Script.fromASM('0 0'), 335 | satoshis: 20005 336 | }) 337 | .to(bsv.Address.fromPrivateKey(bsv.PrivateKey.fromRandom()), 1000) 338 | .addOutput(new bsv.Transaction.Output({ satoshis: 600, script: Buffer.from('6a304502204b13f000b2f046a17fe77976ad4bc6c6055194745b434757eef9faf8bc5de9a8022100b1c2fdce9df149cc8de3dda5ea680dab46888d28abca9b8abac7a8d6d37e4e6a', 'hex') })) 339 | 340 | bitcoinRpc.registerUnconfirmedTx(tx.hash, tx.toBuffer().toString('hex')) 341 | bitcoinRpc.closeBlock() 342 | const previousBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 2] 343 | const nextBlock = await instance.getNextBlock(previousBlock.height, null) 344 | expect(nextBlock.txids).to.eql([]) 345 | }) 346 | 347 | it('does not consider outptus with less than 4 chunks', async () => { 348 | const tx = bsv.Transaction() 349 | .from({ 350 | txId: Buffer.alloc(32).fill(1).toString('hex'), 351 | outputIndex: 0, 352 | script: bsv.Script.fromASM('0 0'), 353 | satoshis: 20005 354 | }) 355 | .to(bsv.Address.fromPrivateKey(bsv.PrivateKey.fromRandom()), 1000) 356 | .addOutput(new bsv.Transaction.Output({ satoshis: 600, script: Buffer.from('51', 'hex') })) // >> OP_TRUE 357 | 358 | bitcoinRpc.registerUnconfirmedTx(tx.hash, tx.toBuffer().toString('hex')) 359 | bitcoinRpc.closeBlock() 360 | const previousBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 2] 361 | const nextBlock = await instance.getNextBlock(previousBlock.height, null) 362 | expect(nextBlock.txids).to.eql([]) 363 | }) 364 | 365 | it('works for giant blocks', async () => { 366 | const randomTx = buildRandomTx() 367 | const randomRunTx = await buildRandomRunTx(run) 368 | bitcoinRpc.registerUnconfirmedTx(randomTx.hash, randomTx.toBuffer().toString('hex')) 369 | bitcoinRpc.registerUnconfirmedTx(randomRunTx.hash, randomRunTx.toBuffer().toString('hex')) 370 | bitcoinRpc.closeBlock(0x1fffffe8 + 1) 371 | const previousBlock = bitcoinRpc.blocks[bitcoinRpc.blocks.length - 2] 372 | 373 | const nextBlock = await instance.getNextBlock(previousBlock.height, null) 374 | expect(nextBlock.txids).to.eql([randomRunTx.hash]) 375 | }) 376 | }) 377 | 378 | describe('#listenForMempool', () => { 379 | it('calls the handler if the tx is run related', async () => { 380 | let called = false 381 | const handler = async () => { 382 | called = true 383 | } 384 | 385 | await instance.listenForMempool(handler) 386 | 387 | const randomTx = await buildRandomRunTx(run) 388 | 389 | bitcoinZmq.publishTx(randomTx) 390 | await bitcoinZmq.processPendingTxs() 391 | 392 | expect(called).to.equal(true) 393 | }) 394 | 395 | it('does not calls the handler if the tx is not run related', async () => { 396 | let called = false 397 | const handler = async () => { 398 | called = true 399 | } 400 | 401 | await instance.listenForMempool(handler) 402 | 403 | const randomTx = buildRandomTx() 404 | 405 | bitcoinZmq.publishTx(randomTx) 406 | await bitcoinZmq.processPendingTxs() 407 | 408 | expect(called).to.equal(false) 409 | }) 410 | 411 | it('the handler receives the right parameters', async () => { 412 | const randomTx = await buildRandomRunTx(run) 413 | 414 | const handler = async (txid, txHex) => { 415 | expect(txid).to.eql(randomTx.hash) 416 | expect(txHex).to.eql(randomTx.toBuffer().toString('hex')) 417 | } 418 | 419 | await instance.listenForMempool(handler) 420 | 421 | bitcoinZmq.publishTx(randomTx) 422 | await bitcoinZmq.processPendingTxs() 423 | }) 424 | }) 425 | 426 | describe('buildBlock', () => { 427 | it('returns a parseable block', () => { 428 | const hexBlock = buildBlock([buildRandomTx()]).toBuffer().toString('hex') 429 | expect(() => new bsv.Block(Buffer.from(hexBlock, 'hex'))).not.to.throw() 430 | }) 431 | 432 | it('returns a block with correct previous hash', () => { 433 | const prevHash = Buffer.alloc(32).fill('6').toString('hex') 434 | const block = buildBlock([buildRandomTx()], prevHash) 435 | expect(block.header.prevHash.reverse().toString('hex')).to.eql(prevHash) 436 | }) 437 | }) 438 | }) 439 | -------------------------------------------------------------------------------- /test/crawler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * crawler.js 3 | * 4 | * Tests for the crawler and APIs it uses 5 | */ 6 | 7 | const { describe, it, beforeEach, afterEach } = require('mocha') 8 | const { expect } = require('chai') 9 | const Indexer = require('../src/indexer') 10 | const txns = require('./txns.json') 11 | const { DEFAULT_TRUSTLIST } = require('../src/config') 12 | const Database = require('../src/database') 13 | 14 | // ------------------------------------------------------------------------------------------------ 15 | // Globals 16 | // ------------------------------------------------------------------------------------------------ 17 | 18 | const fetch = txid => { return { hex: require('./txns.json')[txid] } } 19 | const indexed = (indexer, txid) => new Promise((resolve, reject) => { indexer.onIndex = x => txid === x && resolve() }) 20 | const crawled = (indexer) => new Promise((resolve, reject) => { indexer.onBlock = height => resolve(height) }) 21 | const reorged = (indexer) => new Promise((resolve, reject) => { indexer.onReorg = newHeight => resolve(newHeight) }) 22 | const logger = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } 23 | const database = new Database(':memory:', logger, false) 24 | 25 | beforeEach(() => database.open()) 26 | afterEach(() => database.close()) 27 | 28 | // ------------------------------------------------------------------------------------------------ 29 | // Crawler 30 | // ------------------------------------------------------------------------------------------------ 31 | 32 | describe('Crawler', () => { 33 | it('add txids', async () => { 34 | const txid = '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64' 35 | function getNextBlock (height, hash) { 36 | return { height: 1, hash: 'abc', txids: [txid] } 37 | } 38 | const api = { getNextBlock, fetch } 39 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 40 | await indexer.start() 41 | database.trust(txid) 42 | await indexed(indexer, txid) 43 | expect(database.getHeight()).to.equal(1) 44 | expect(database.getHash()).to.equal('abc') 45 | await indexer.stop() 46 | }) 47 | 48 | // -------------------------------------------------------------------------- 49 | 50 | it('add block', async () => { 51 | const txid = '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64' 52 | function getNextBlock (height, hash) { 53 | return { height: 1, hash: 'abc', txids: [txid], txhexs: [txns[txid]] } 54 | } 55 | const api = { getNextBlock } 56 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 57 | await indexer.start() 58 | database.trust(txid) 59 | await indexed(indexer, txid) 60 | expect(database.getHeight()).to.equal(1) 61 | expect(database.getHash()).to.equal('abc') 62 | await indexer.stop() 63 | }) 64 | 65 | // -------------------------------------------------------------------------- 66 | 67 | it('add block with already downloaded transactions', async () => { 68 | const txids = [ 69 | '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64', 70 | 'bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d', 71 | 'ca9555f54dd44457d7c912e8eea375a8ed6d8ea1806a206b43af5c7f94ea47e7' 72 | ] 73 | let indexedMiddleTransaction = false 74 | function getNextBlock (height, hash) { 75 | if (!indexedMiddleTransaction) return null 76 | if (height === 1) return null 77 | return { height: 1, hash: 'abc', txids, txhexs: txids.map(txid => txns[txid]) } 78 | } 79 | const api = { getNextBlock, fetch } 80 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, DEFAULT_TRUSTLIST) 81 | indexer.crawler.pollForNewBlocksInterval = 10 82 | await indexer.start() 83 | await database.addTransaction(txids[1]) 84 | database.trust(txids[0]) 85 | database.trust(txids[1]) 86 | database.trust(txids[2]) 87 | await indexed(indexer, txids[1]) 88 | indexedMiddleTransaction = true 89 | await indexed(indexer, txids[0]) 90 | expect(database.getTransactionHex(txids[0])).to.equal(txns[txids[0]]) 91 | expect(database.getTransactionHex(txids[1])).to.equal(txns[txids[1]]) 92 | expect(database.getTransactionHex(txids[2])).to.equal(txns[txids[2]]) 93 | await indexer.stop() 94 | }) 95 | 96 | // -------------------------------------------------------------------------- 97 | 98 | it('reorg blocks', async () => { 99 | const txid = '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64' 100 | let didReorg = false 101 | let didIndex = false 102 | function getNextBlock (height, hash) { 103 | if (didReorg) return { height: 3, hash: 'def', txids: [] } 104 | if (height < 5) return { height: height + 1, hash: 'abc' + height, txids: [] } 105 | if (height === 5) return { height: height + 1, hash: 'abc', txids: [txid] } 106 | if (height < 12) return { height: height + 1, hash: 'abc' + height, txids: [] } 107 | if (!didIndex) { return null } 108 | if (height === 12) { didReorg = true; return { reorg: true } } 109 | } 110 | const api = { getNextBlock, fetch } 111 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 112 | indexer.crawler.pollForNewBlocksInterval = 10 113 | await indexer.start() 114 | database.trust(txid) 115 | await indexed(indexer, txid) 116 | didIndex = true 117 | await reorged(indexer) 118 | expect(indexer.database.getHeight()).to.equal(2) 119 | await crawled(indexer, 3) 120 | expect(database.getHeight()).to.equal(3) 121 | expect(database.getHash()).to.equal('def') 122 | expect(database.getTransactionHex(txid)).not.to.equal(undefined) 123 | expect(database.getJigState(txid + '_o1')).not.to.equal(undefined) 124 | expect(database.getTransactionHeight(txid)).to.equal(-1) 125 | await indexer.stop() 126 | }) 127 | 128 | // -------------------------------------------------------------------------- 129 | 130 | it('reorg while executing', async () => { 131 | const txid = '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64' 132 | let didReorg = false 133 | function getNextBlock (height, hash) { 134 | if (didReorg) return { height: 3, hash: 'def', txids: [] } 135 | if (height < 5) return { height: height + 1, hash: 'abc' + height, txids: [] } 136 | if (height === 5) return { height: height + 1, hash: 'abc', txids: [txid] } 137 | if (height < 12) return { height: height + 1, hash: 'abc' + height, txids: [] } 138 | if (height === 12) { didReorg = true; return { reorg: true } } 139 | } 140 | const api = { getNextBlock, fetch } 141 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 142 | await indexer.start() 143 | database.trust(txid) 144 | await reorged(indexer) 145 | expect(database.getTransactionHex(txid)).not.to.equal(undefined) 146 | expect(database.getJigState(txid + '_o1')).to.equal(undefined) 147 | expect(database.getTransactionHeight(txid)).to.equal(-1) 148 | await indexer.stop() 149 | }) 150 | }) 151 | 152 | // ------------------------------------------------------------------------------------------------ 153 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | * 4 | * Tests for run-db 5 | */ 6 | 7 | require('./crawler') 8 | require('./indexer') 9 | require('./server') 10 | -------------------------------------------------------------------------------- /test/indexer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * indexer.js 3 | * 4 | * Tests for the Indexer 5 | */ 6 | 7 | const { describe, it, beforeEach, afterEach } = require('mocha') 8 | const { expect } = require('chai') 9 | const bsv = require('bsv') 10 | const Indexer = require('../src/indexer') 11 | const Run = require('run-sdk') 12 | const { Jig } = Run 13 | const { DEFAULT_TRUSTLIST } = require('../src/config') 14 | const Database = require('../src/database') 15 | 16 | // ------------------------------------------------------------------------------------------------ 17 | // Globals 18 | // ------------------------------------------------------------------------------------------------ 19 | 20 | const fetch = txid => { return { hex: require('./txns.json')[txid] } } 21 | const api = { fetch } 22 | const indexed = (indexer, txid) => new Promise((resolve, reject) => { indexer.onIndex = x => txid === x && resolve() }) 23 | const failed = (indexer, txid) => new Promise((resolve, reject) => { indexer.onFailToIndex = x => txid === x && resolve() }) 24 | const logger = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } 25 | const database = new Database(':memory:', logger, false) 26 | 27 | beforeEach(() => database.open()) 28 | afterEach(() => database.close()) 29 | 30 | // ------------------------------------------------------------------------------------------------ 31 | // Indexer 32 | // ------------------------------------------------------------------------------------------------ 33 | 34 | describe('Indexer', () => { 35 | it('add and index', async () => { 36 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 37 | await indexer.start() 38 | database.addTransaction('3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64') 39 | database.addTransaction('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 40 | database.trust('3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64') 41 | database.trust('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 42 | await indexed(indexer, '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 43 | const txid = '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102' 44 | expect(database.getTransactionHex(txid)).to.equal(fetch(txid).hex) 45 | expect(database.getTransactionHeight(txid)).to.equal(null) 46 | expect(database.getTransactionTime(txid)).to.be.greaterThan(new Date() / 1000 - 3) 47 | expect(database.getTransactionTime(txid)).to.be.lessThan(new Date() / 1000 + 3) 48 | await indexer.stop() 49 | }) 50 | 51 | // -------------------------------------------------------------------------- 52 | 53 | it('index jig sent to pubkey', async () => { 54 | new Run({ network: 'mock' }) // eslint-disable-line 55 | class A extends Jig { init (owner) { this.owner = owner } } 56 | const tx = new Run.Transaction() 57 | const pubkey = new bsv.PrivateKey('testnet').toPublicKey().toString() 58 | tx.update(() => new A(pubkey)) 59 | const rawtx = await tx.export() 60 | const api = { fetch: txid => { return { hex: rawtx } } } 61 | const indexer = new Indexer(database, api, 'test', 1, 1, logger, 0, Infinity, []) 62 | const txid = new bsv.Transaction(rawtx).hash 63 | await indexer.start() 64 | database.addTransaction(txid) 65 | database.trust(txid) 66 | await indexed(indexer, txid) 67 | await indexer.stop() 68 | }) 69 | 70 | // -------------------------------------------------------------------------- 71 | 72 | it('add in reverse and index', async () => { 73 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 74 | await indexer.start() 75 | database.addTransaction('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 76 | database.addTransaction('3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64') 77 | database.trust('3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64') 78 | database.trust('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 79 | await indexed(indexer, '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 80 | await indexer.stop() 81 | }) 82 | 83 | // -------------------------------------------------------------------------- 84 | 85 | it('fail to index', async () => { 86 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 87 | await indexer.start() 88 | database.trust('b17a9af70ab0f46809f908b2e900e395ba40996000bf4f00e3b27a1e93280cf1') 89 | database.trust('a5291157ab7a2d80d834bbe82c380ce3976f53990d20c62c477ca3a2ac93a7e9') 90 | database.addTransaction('b17a9af70ab0f46809f908b2e900e395ba40996000bf4f00e3b27a1e93280cf1') 91 | database.addTransaction('a5291157ab7a2d80d834bbe82c380ce3976f53990d20c62c477ca3a2ac93a7e9') 92 | await failed(indexer, 'a5291157ab7a2d80d834bbe82c380ce3976f53990d20c62c477ca3a2ac93a7e9') 93 | await indexer.stop() 94 | }) 95 | 96 | // -------------------------------------------------------------------------- 97 | 98 | it('discovered berry transaction', async () => { 99 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 100 | await indexer.start() 101 | database.addTransaction('bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d') // Class with berry image 102 | database.addTransaction('24cde3638a444c8ad397536127833878ffdfe1b04d5595489bd294e50d77105a') // B (old) 103 | database.addTransaction('312985bd960ae4c59856b3089b04017ede66506ea181333eec7c9bb88b11c490') // txo, Tx 104 | database.addTransaction('727e7b423b7ee40c0b5be87fba7fa5673ea2d20a74259040a7295d9c32a90011') // Hex 105 | database.trust('bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d') 106 | database.trust('24cde3638a444c8ad397536127833878ffdfe1b04d5595489bd294e50d77105a') 107 | database.trust('312985bd960ae4c59856b3089b04017ede66506ea181333eec7c9bb88b11c490') 108 | database.trust('727e7b423b7ee40c0b5be87fba7fa5673ea2d20a74259040a7295d9c32a90011') 109 | // Don't index the berry data, because it will be fetched automatically 110 | // database.addTransaction('2f3492ef5401d887a93ca09820dff952f355431cea306841a70d163e32b2acad') // Berry data 111 | await indexed(indexer, 'bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d') 112 | await indexer.stop() 113 | }) 114 | 115 | // -------------------------------------------------------------------------- 116 | 117 | it('add and download dependencies', async () => { 118 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 119 | await indexer.start() 120 | database.addTransaction('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 121 | await new Promise((resolve, reject) => setTimeout(resolve, 1000)) 122 | await indexer.stop() 123 | }) 124 | 125 | // -------------------------------------------------------------------------- 126 | 127 | it('remove discovered dep', async () => { 128 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, DEFAULT_TRUSTLIST) 129 | await indexer.start() 130 | database.addTransaction('bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d') // Class with berry image 131 | database.trust('bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d') 132 | await indexed(indexer, 'bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d') 133 | expect(database.getTransactionHex('bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d')).not.to.equal(undefined) 134 | database.deleteTransaction('2f3492ef5401d887a93ca09820dff952f355431cea306841a70d163e32b2acad') // Berry data 135 | expect(database.getTransactionHex('bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d')).to.equal(undefined) 136 | await indexer.stop() 137 | }) 138 | 139 | // -------------------------------------------------------------------------- 140 | 141 | it('get spent', async function () { 142 | this.timeout(40000) 143 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, DEFAULT_TRUSTLIST) 144 | await indexer.start() 145 | database.addTransaction('11f27cdad53128a4eb14c8328515dfab56b16ea5a71dd26abe9e9d7488f3ab83') 146 | await indexed(indexer, '11f27cdad53128a4eb14c8328515dfab56b16ea5a71dd26abe9e9d7488f3ab83') 147 | expect(database.getSpend('7fa1b0eb8408047e138aadf72ee0980e42afab2208181429b050ad495a384d39_o1')) 148 | .to.equal('11f27cdad53128a4eb14c8328515dfab56b16ea5a71dd26abe9e9d7488f3ab83') 149 | expect(database.getSpend('11f27cdad53128a4eb14c8328515dfab56b16ea5a71dd26abe9e9d7488f3ab83_o1')) 150 | .to.equal(null) 151 | await indexer.stop() 152 | }) 153 | 154 | // -------------------------------------------------------------------------- 155 | 156 | it('mark failed execute as melts', async () => { 157 | const indexer = new Indexer(database, {}, 'test', 1, 1, logger, 0, Infinity, []) 158 | const rawtx1 = '0100000001a11d53c34263d1ea9dec40d3cc5beb7eb461a601d898a8337dea215cd90a9e4a010000006a47304402202f294c5ceca857cfc03e38b1a49a79d6c133e9e6b18047f0301f9f74bb2abdab022027aa6662cd24428106b9f8f2e38d2e5b8f0b7c30929eef6dbc1d013c43b0493f41210211f2cc632921525ec8650cb65c2ed520e400a2644010c1e794203d5823f604c0ffffffff030000000000000000fd0301006a0372756e0105004cf87b22696e223a302c22726566223a5b226e61746976653a2f2f4a6967225d2c226f7574223a5b2238396336653439636532653831373962653138383563396230653032343863363935666130373634343939656665626362363936623238323732366239666165225d2c2264656c223a5b5d2c22637265223a5b226d737138444642455777546166675a6173474c4a386f3338517a456367346267364a225d2c2265786563223a5b7b226f70223a224445504c4f59222c2264617461223a5b22636c617373204120657874656e6473204a6967207b207d222c7b2264657073223a7b224a6967223a7b22246a6967223a307d7d7d5d7d5d7d11010000000000001976a9148711466c1f8b5977cb788485fcb6cc1fb9d0407788acf6def505000000001976a9142208fb2364d1551e2dd26549d7c22eab613a207188ac00000000' 159 | const rawtx2 = '0100000002cb8c61b7d73cf14ed2526f2adcb0ef941563c69fb794a87eb39a94423886d273010000006a4730440220306a24e0464c90889d6fd1580db4420fe9ee1bd8f167ec793d40d2296ff0d8ea02202224f4f13e4c07354478983b2dc88170342a4f1ac3e6cacad8616a92348fc768412103a6fa27cfcda39be6ee9dc5dbd43a44c2c749ca136f7d41cd81468f72cc0fda59ffffffffcb8c61b7d73cf14ed2526f2adcb0ef941563c69fb794a87eb39a94423886d273020000006b483045022100c2b7a660b22dd2c3ac22d47ba16fa3f7df852f5a6cfdec5ce14c734517a0b1900220592da53a61ec1387aa96050c370b7c5ba162ee35e8d30b55d9999f1c2ba06ade41210211f2cc632921525ec8650cb65c2ed520e400a2644010c1e794203d5823f604c0ffffffff030000000000000000ae006a0372756e0105004ca37b22696e223a312c22726566223a5b5d2c226f7574223a5b2264633031326334616436346533626136373632383762323239623865306662303934326448626535303435393036363830616637633937663134666239663433225d2c2264656c223a5b5d2c22637265223a5b5d2c2265786563223a5b7b226f70223a2243414c4c222c2264617461223a5b7b22246a6967223a307d2c2261757468222c5b5d5d7d5d7d11010000000000001976a9148711466c1f8b5977cb788485fcb6cc1fb9d0407788acdeddf505000000001976a9142208fb2364d1551e2dd26549d7c22eab613a207188ac00000000' 160 | const txid1 = new bsv.Transaction(rawtx1).hash 161 | const txid2 = new bsv.Transaction(rawtx2).hash 162 | await indexer.start() 163 | database.addTransaction(txid1, rawtx1) 164 | database.trust(txid1) 165 | await indexed(indexer, txid1) 166 | database.addTransaction(txid2, rawtx2) 167 | await failed(indexer, txid2) 168 | expect(database.getSpend(txid1 + '_o1')).to.equal(txid2) 169 | await indexer.stop() 170 | }) 171 | 172 | // -------------------------------------------------------------------------- 173 | 174 | it('deletes are not included in unspent', async () => { 175 | const indexer = new Indexer(database, {}, 'test', 1, 1, logger, 0, Infinity, []) 176 | const rawtx1 = '01000000016f4f66891029280028bce15768b3fdc385533b0bcc77a029add646176207e77f010000006b483045022100a76777ae759178595cb83ce9473699c9056e32faa8e0d07c2517918744fab9e90220369d7a6a2f52b5ddd9bff4ed659ef5a8e676397dac15e9c5dc6dad09e5eab85e412103ac8a61b3fb98161003daaaa63ec1983dc127f4f978a42f2eefd31a074a814345ffffffff030000000000000000fd0301006a0372756e0105004cf87b22696e223a302c22726566223a5b226e61746976653a2f2f4a6967225d2c226f7574223a5b2237373864313934336265613463353166356561313635666630346335613039323435356365386437343335623936336333613130623961343536633463623330225d2c2264656c223a5b5d2c22637265223a5b226d674671626e5254774c3155436d384a654e6e556d6b7a58665a6f3271385764364c225d2c2265786563223a5b7b226f70223a224445504c4f59222c2264617461223a5b22636c617373204120657874656e6473204a6967207b207d222c7b2264657073223a7b224a6967223a7b22246a6967223a307d7d7d5d7d5d7d11010000000000001976a914081c4c589c062b1b1d4e4b25a8b3096868059d7a88acf6def505000000001976a914146caf0030b67f3fae5d53b7c3fa7e1e6fcaaf3b88ac00000000' 177 | const rawtx2 = '01000000015991661ed379a0d12a68feacdbf7776d82bcffe1761f995cf0412c5ae2d25d28010000006a47304402203776f765d6915431388110a7f4645a61bd8d2f2ab00ade0049f0da95b5455c22022074ca4b6a87891ba852416bf08b64ad3db130a0b780e2a658c451ebacbbcffbf8412103646b0e969bd3825f781f39b737bdfed1e2cd63533301317099e5ac021b4826aaffffffff010000000000000000b1006a0372756e0105004ca67b22696e223a312c22726566223a5b5d2c226f7574223a5b5d2c2264656c223a5b2265386436393434613366383765323936663237326562656437663033623133323962653262313733653732376436623431643632616365343036656434373539225d2c22637265223a5b5d2c2265786563223a5b7b226f70223a2243414c4c222c2264617461223a5b7b22246a6967223a307d2c2264657374726f79222c5b5d5d7d5d7d00000000' 178 | const txid1 = new bsv.Transaction(rawtx1).hash 179 | const txid2 = new bsv.Transaction(rawtx2).hash 180 | await indexer.start() 181 | database.addTransaction(txid1, rawtx1) 182 | database.addTransaction(txid2, rawtx2) 183 | database.trust(txid1) 184 | await indexed(indexer, txid2) 185 | expect(indexer.database.getNumUnspent()).to.equal(0) 186 | await indexer.stop() 187 | }) 188 | }) 189 | 190 | // ------------------------------------------------------------------------------------------------ 191 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * server.js 3 | * 4 | * Tests for src/server.js 5 | */ 6 | 7 | const { describe, it, beforeEach, afterEach } = require('mocha') 8 | require('chai').use(require('chai-as-promised')) 9 | const { expect } = require('chai') 10 | const axios = require('axios') 11 | const Indexer = require('../src/indexer') 12 | const Server = require('../src/server') 13 | const txns = require('./txns.json') 14 | const { DEFAULT_TRUSTLIST } = require('../src/config') 15 | const Database = require('../src/database') 16 | 17 | // ------------------------------------------------------------------------------------------------ 18 | // Globals 19 | // ------------------------------------------------------------------------------------------------ 20 | 21 | const fetch = async txid => { return { hex: require('./txns.json')[txid] } } 22 | const api = { fetch } 23 | const downloaded = (indexer, txid) => new Promise((resolve, reject) => { indexer.onDownload = x => txid === x && resolve() }) 24 | const indexed = (indexer, txid) => new Promise((resolve, reject) => { indexer.onIndex = x => txid === x && resolve() }) 25 | const listening = (server) => new Promise((resolve, reject) => { server.onListening = () => resolve() }) 26 | const logger = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } 27 | const database = new Database(':memory:', logger, false) 28 | 29 | beforeEach(() => database.open()) 30 | afterEach(() => database.close()) 31 | 32 | // ------------------------------------------------------------------------------------------------ 33 | // Server 34 | // ------------------------------------------------------------------------------------------------ 35 | 36 | describe('Server', () => { 37 | // -------------------------------------------------------------------------- 38 | // post tx 39 | // -------------------------------------------------------------------------- 40 | 41 | describe('post tx', () => { 42 | it('add with body', async () => { 43 | const indexer = new Indexer(database, {}, 'main', 1, 1, logger, 0, Infinity, []) 44 | const server = new Server(database, logger, null) 45 | await indexer.start() 46 | server.start() 47 | await listening(server) 48 | const txid = '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64' 49 | const options = { headers: { 'Content-Type': 'text/plain' } } 50 | await axios.post(`http://localhost:${server.port}/tx/${txid}`, txns[txid], options) 51 | await axios.post(`http://localhost:${server.port}/trust/${txid}`) 52 | await indexed(indexer, txid) 53 | server.stop() 54 | await indexer.stop() 55 | }) 56 | 57 | // ------------------------------------------------------------------------ 58 | 59 | it('does not throw if add with rawtx mismatch', async () => { 60 | // Because the "POST /tx/:txid" endpoint is being deprecated we are not doing this 61 | // checking anymore. The txid of the url is ignored. 62 | const indexer = new Indexer(database, {}, 'main', 1, 1, logger, 0, Infinity, []) 63 | const server = new Server(database, logger, null) 64 | await indexer.start() 65 | server.start() 66 | await listening(server) 67 | const txid = '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64' 68 | const otherTxid = 'bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d' 69 | const options = { headers: { 'Content-Type': 'text/plain' } } 70 | await expect(axios.post(`http://localhost:${server.port}/tx/${txid}`, txns[otherTxid], options)).to.be.fulfilled 71 | server.stop() 72 | await indexer.stop() 73 | }) 74 | }) 75 | 76 | // -------------------------------------------------------------------------- 77 | // post trust 78 | // -------------------------------------------------------------------------- 79 | 80 | describe('post trust', () => { 81 | it('trust multiple', async () => { 82 | const indexer = new Indexer(database, {}, 'main', 1, 1, logger, 0, Infinity, []) 83 | const server = new Server(database, logger, null) 84 | await indexer.start() 85 | server.start() 86 | await listening(server) 87 | const trustlist = [ 88 | '3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64', 89 | 'bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d' 90 | ] 91 | const options = { headers: { 'Content-Type': 'application/json' } } 92 | await axios.post(`http://localhost:${server.port}/trust`, trustlist, options) 93 | trustlist.forEach(txid => expect(indexer.database.isTrusted(txid)).to.equal(true)) 94 | server.stop() 95 | await indexer.stop() 96 | }) 97 | }) 98 | 99 | // -------------------------------------------------------------------------- 100 | // get jig 101 | // -------------------------------------------------------------------------- 102 | 103 | describe('get jig', () => { 104 | it('returns state if exists', async () => { 105 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, DEFAULT_TRUSTLIST) 106 | const server = new Server(database, logger, null) 107 | await indexer.start() 108 | server.start() 109 | await listening(server) 110 | database.addTransaction('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 111 | await indexed(indexer, '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 112 | const location = '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102_o1' 113 | const state = (await axios.get(`http://localhost:${server.port}/jig/${location}`)).data 114 | expect(typeof state).to.equal('object') 115 | expect(state.kind).to.equal('jig') 116 | server.stop() 117 | await indexer.stop() 118 | }) 119 | 120 | // ------------------------------------------------------------------------ 121 | 122 | it('returns 404 if missing', async () => { 123 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 124 | const server = new Server(database, logger, null) 125 | await indexer.start() 126 | server.start() 127 | await listening(server) 128 | const location = '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102_o1' 129 | await expect(axios.get(`http://localhost:${server.port}/jig/${location}`)).to.be.rejected 130 | try { 131 | await axios.get(`http://localhost:${server.port}/jig/${location}`) 132 | } catch (e) { 133 | expect(e.response.status).to.equal(404) 134 | } 135 | server.stop() 136 | await indexer.stop() 137 | }) 138 | }) 139 | 140 | // -------------------------------------------------------------------------- 141 | // get berry 142 | // -------------------------------------------------------------------------- 143 | 144 | describe('get berry', () => { 145 | it('returns state if exists', async () => { 146 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, DEFAULT_TRUSTLIST) 147 | const server = new Server(database, logger, null) 148 | await indexer.start() 149 | server.start() 150 | await listening(server) 151 | database.addTransaction('bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d') 152 | await indexed(indexer, 'bfa5180e601e92af23d80782bf625b102ac110105a392e376fe7607e4e87dc8d') 153 | const location = '24cde3638a444c8ad397536127833878ffdfe1b04d5595489bd294e50d77105a_o1?berry=2f3492ef5401d887a93ca09820dff952f355431cea306841a70d163e32b2acad&version=5' 154 | const state = (await axios.get(`http://localhost:${server.port}/berry/${encodeURIComponent(location)}`)).data 155 | expect(typeof state).to.equal('object') 156 | expect(state.kind).to.equal('berry') 157 | server.stop() 158 | await indexer.stop() 159 | }) 160 | 161 | // ------------------------------------------------------------------------ 162 | 163 | it('returns 404 if missing', async () => { 164 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 165 | const server = new Server(database, logger, null) 166 | await indexer.start() 167 | server.start() 168 | await listening(server) 169 | const location = '24cde3638a444c8ad397536127833878ffdfe1b04d5595489bd294e50d77105a_o1?berry=2f3492ef5401d887a93ca09820dff952f355431cea306841a70d163e32b2acad&version=5' 170 | await expect(axios.get(`http://localhost:${server.port}/berry/${location}`)).to.be.rejected 171 | try { 172 | await axios.get(`http://localhost:${server.port}/berry/${location}`) 173 | } catch (e) { 174 | expect(e.response.status).to.equal(404) 175 | } 176 | server.stop() 177 | await indexer.stop() 178 | }) 179 | }) 180 | 181 | // -------------------------------------------------------------------------- 182 | // get tx 183 | // -------------------------------------------------------------------------- 184 | 185 | describe('get tx', () => { 186 | it('returns rawtx if downloaded', async () => { 187 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 188 | const server = new Server(database, logger, null) 189 | await indexer.start() 190 | server.start() 191 | await listening(server) 192 | const txid = '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102' 193 | database.addTransaction(txid) 194 | await downloaded(indexer, txid) 195 | const rawtx = (await axios.get(`http://localhost:${server.port}/tx/${txid}`)).data 196 | expect(typeof rawtx).to.equal('string') 197 | expect(rawtx.length).to.equal(2074) 198 | server.stop() 199 | await indexer.stop() 200 | }) 201 | 202 | // ------------------------------------------------------------------------ 203 | 204 | it('returns 404 if missing', async () => { 205 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 206 | const server = new Server(database, logger, null) 207 | await indexer.start() 208 | server.start() 209 | await listening(server) 210 | const txid = '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102' 211 | await expect(axios.get(`http://localhost:${server.port}/tx/${txid}`)).to.be.rejected 212 | try { 213 | await axios.get(`http://localhost:${server.port}/tx/${txid}`) 214 | } catch (e) { 215 | expect(e.response.status).to.equal(404) 216 | } 217 | server.stop() 218 | await indexer.stop() 219 | }) 220 | 221 | // ------------------------------------------------------------------------ 222 | 223 | it('returns 404 if not downloaded', async () => { 224 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 225 | const server = new Server(database, logger, null) 226 | await indexer.start() 227 | server.start() 228 | await listening(server) 229 | const txid = '1111111111111111111111111111111111111111111111111111111111111111' 230 | database.addTransaction(txid) 231 | await expect(axios.get(`http://localhost:${server.port}/tx/${txid}`)).to.be.rejected 232 | try { 233 | await axios.get(`http://localhost:${server.port}/tx/${txid}`) 234 | } catch (e) { 235 | expect(e.response.status).to.equal(404) 236 | } 237 | server.stop() 238 | await indexer.stop() 239 | }) 240 | }) 241 | 242 | // -------------------------------------------------------------------------- 243 | // get unspent 244 | // -------------------------------------------------------------------------- 245 | 246 | describe('get unspent', () => { 247 | it('query all unspent', async () => { 248 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, DEFAULT_TRUSTLIST) 249 | const server = new Server(database, logger, null) 250 | await indexer.start() 251 | server.start() 252 | await listening(server) 253 | database.addTransaction('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 254 | await indexed(indexer, '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 255 | const unspent = (await axios.get(`http://localhost:${server.port}/unspent`)).data 256 | expect(unspent.length).to.equal(3) 257 | expect(unspent.includes('3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64_o1')).to.equal(true) 258 | expect(unspent.includes('3f9de452f0c3c96be737d42aa0941b27412211976688967adb3174ee18b04c64_o2')).to.equal(true) 259 | expect(unspent.includes('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102_o1')).to.equal(true) 260 | server.stop() 261 | await indexer.stop() 262 | }) 263 | 264 | // ------------------------------------------------------------------------ 265 | 266 | it('query unspent by address', async () => { 267 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, DEFAULT_TRUSTLIST) 268 | const server = new Server(database, logger, null) 269 | await indexer.start() 270 | server.start() 271 | await listening(server) 272 | database.addTransaction('9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 273 | await indexed(indexer, '9bb02c2f34817fec181dcf3f8f7556232d3fac9ef76660326f0583d57bf0d102') 274 | const address = '1Kc8XRNryDycwvfEQiFF2TZwD1CVhgwGy2' 275 | const unspent = (await axios.get(`http://localhost:${server.port}/unspent?address=${address}`)).data 276 | expect(unspent.length).to.equal(3) 277 | server.stop() 278 | await indexer.stop() 279 | }) 280 | }) 281 | 282 | // -------------------------------------------------------------------------- 283 | // misc 284 | // -------------------------------------------------------------------------- 285 | 286 | describe('misc', () => { 287 | it('cors', async () => { 288 | const indexer = new Indexer(database, api, 'main', 1, 1, logger, 0, Infinity, []) 289 | const server = new Server(database, logger, null) 290 | await indexer.start() 291 | server.start() 292 | await listening(server) 293 | const opts = { headers: { Origin: 'https://www.google.com' } } 294 | const resp = (await axios.get(`http://localhost:${server.port}/status`, opts)) 295 | expect(resp.headers['access-control-allow-origin']).to.equal('*') 296 | server.stop() 297 | await indexer.stop() 298 | }) 299 | }) 300 | }) 301 | 302 | // ------------------------------------------------------------------------------------------------ 303 | --------------------------------------------------------------------------------