├── .drone.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── bitcoin-transaction-builder.js ├── data-payload.js └── index.js └── test ├── bitcoin-transaction-builder-spec.js ├── data-payload-spec.js ├── index-spec.js └── raw-transactions ├── test0.json └── test1.json /.drone.yml: -------------------------------------------------------------------------------- 1 | image: node0.10 2 | env: 3 | - DEBUG=* 4 | script: 5 | - npm install 6 | - npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 William Cotton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | blockcast 2 | === 3 | 4 | [![Build Status](http://drone.d.blockai.com/api/badge/github.com/blockai/blockcast/status.svg?branch=master)](http://drone.d.blockai.com/github.com/blockai/blockcast) 5 | 6 | A multi-transaction protocol for storing data in the Bitcoin blockchain. 7 | 8 | This protocol is intended for use while **developing** ```OP_RETURN``` based protocols. Mature protocols should switch to a custom ```OP_RETURN``` method that uses as few transactions as possible to store data. 9 | 10 | In the meantime, save yourself from premature optimizations. Wait until you've figured out your protocol's most basic requirements so you don't hold up developing applications that consume your APIs. 11 | 12 | Posting data 13 | --- 14 | 15 | In our examples we're going to use ```bitcoinjs-lib``` to create our wallet. 16 | 17 | ```javascript 18 | var bitcoin = require("bitcoinjs-lib"); 19 | 20 | var seed = bitcoin.crypto.sha256("test"); 21 | var wallet = new bitcoin.Wallet(seed, bitcoin.networks.testnet); 22 | var address = wallet.generateAddress(); 23 | 24 | var signRawTransaction = function(txHex, cb) { 25 | var tx = bitcoin.Transaction.fromHex(txHex); 26 | var signedTx = wallet.signWith(tx, [address]); 27 | var txid = signedTx.getId(); 28 | var signedTxHex = signedTx.toHex(); 29 | cb(false, signedTxHex, txid); 30 | }; 31 | 32 | var commonWallet = { 33 | signRawTransaction: signRawTransaction, 34 | address: address 35 | } 36 | ``` 37 | 38 | We'll need to provide an instance of a commonBlockchain which will provide functions for getting unspent outputs, propagating a trasnaction, and looking up a transaction by ```txid```. 39 | 40 | In this example we're using the in memory version that is provided by ```mem-common-blockchain```. 41 | 42 | 43 | ```javascript 44 | var memCommonBlockchain = require("mem-common-blockchain")({ 45 | type: "local" 46 | }); 47 | 48 | // or local Bitcoin-Qt.app via the JSON-RPC 49 | 50 | var RpcClient = require('bitcoind-rpc') 51 | 52 | var config = { 53 | protocol: 'http', 54 | user: 'rpcuser', 55 | pass: 'rpcpassword', 56 | host: '127.0.0.1', 57 | port: '18332' 58 | } 59 | 60 | var rpc = new RpcClient(config) 61 | 62 | var rpcCommonBlockchain = require('rpc-common-blockchain')({ 63 | rpc: rpc 64 | }) 65 | 66 | // or testnet via blockcypher 67 | 68 | testnetCommonBlockchain = require('blockcypher-unofficial')({ 69 | network: "testnet" 70 | }); 71 | ``` 72 | 73 | And finally we're ready to post. 74 | 75 | ```javascript 76 | blockcast.post({ 77 | data: "Hello, world! I'm posting a message that is compressed and spread out across a number of bitcoin transactions!", 78 | commonWallet: commonWallet, 79 | commonBlockchain: memCommonBlockchain 80 | }, function(error, response) { 81 | console.log(response); 82 | }); 83 | ``` 84 | 85 | Scan for data from a single transaction 86 | --- 87 | 88 | We can also provide the transaction hash from the first transaction's payload. 89 | 90 | ```javascript 91 | blockcast.scanSingle({ 92 | txid: 'fe44cae45f69dd1d6115815356a73b9c5179feff1b276d99ac0e283156e1cd01', 93 | commonBlockchain: testnetCommonBlockchain 94 | }, function(err, body) { 95 | var document = JSON.parse(body); 96 | console.log(document); 97 | }); 98 | ``` 99 | 100 | Scan for data from a range of blocks 101 | --- 102 | 103 | You can use the [```blockcast-state-engine```](https://github.com/blockai/blockcast-state-engine) module to scan the Bitcoin blockchain for an ordered list of Blockcast data. 104 | 105 | How does it work? 106 | --- 107 | 108 | Documents are compressed using DEFLATE and then embedded across up to 16 Bitcoin transactions in OP_RETURN outputs allowing for total compressed size of no larger than 1277 bytes. 109 | 110 | Each blockcast post has a primary transaction identified by the magic header ```0x1f00```. The compressed data can be rebuilt by following the chain of previous transactions back through their inputs and appending data stored in OP_RETURN. 111 | 112 | ```OP_RETURN 0x1f00032d8d5b4bc4301085ff4ac973d14c9a34c9beb982c20aa28222bee5ea06bb6d4cb3ec45fcef4ed59799e19cf39df92253262b52484b6c4d5b3cbb4e704a1def406a6bc1784a95604a071e7aea``` 113 | 114 | The total number of tranactions is stored as the first byte immediately following the magic header. In the above example, ```0x1f0003```, the total transaction count is 3, meaning that two more transactions bust be sequentially scanned in order to reconstruct the full compressed data. 115 | 116 | This is enough space to contain a number of document digest formats, URIs and URNs. This allows for cross-platform content addressable formats between systems like BitTorrent and IPFS. Used by [openpublish](https://github.com/blockai/openpublish/). 117 | 118 | Why Bitcoin? 119 | --- 120 | 121 | The Bitcoin blockchain is the world's first public equal-access data store. Data embedded in the Bitcoin blockchain becomes provably published records signed by recognizable authors. 122 | 123 | Other public data stores are unreliable. Bittorrent, Freenet and public-access DHTs cannot guarantee that data will be retrievable. 124 | 125 | What about polluting the blockchain? 126 | --- 127 | 128 | We will move this protocol to a Bitcoin sidechain designed specifically for public data as soon as the technology for building sidechains becomes available. 129 | 130 | In the meantime we've been using this protocol while prototyping other protocols that rely on storing metadata in the Bitcoin blockchain. 131 | 132 | Woodsy Owl says "Give a Hoot! Don't Pollute!" 133 | 134 | What about an alternative currency like Namecoin? 135 | --- 136 | 137 | Namecoin doesn't match all specific use-cases as documents expire after ~200 days. 138 | 139 | It also lacks the infrastructure of exchanges, APIs, tools, and software that support Bitcoin. 140 | 141 | Ultimately we feel that Bitcoin sidechains are a better approach to crypto-currencies than having competing alt-coins. 142 | 143 | Building any application on top of Bitcoin creates an incentive to own Bitcoin. Incentives to own Bitcoin keep miners happy. Happy miners create happy Bitcoin. 144 | 145 | Was this always intended for prototyping? 146 | --- 147 | 148 | No, this started out as some sort of "Twitter on the Blockchain" protocol but it quickly became clear that the current Bitcoin blockchain is most useful for storing metadata related to digital assets and not as a generic decentralized data store. 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blockcast", 3 | "version": "3.7.0", 4 | "description": "A multi-transaction protocol for storing data in the Bitcoin blockchain.", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/jasmine-node test/ --forceexit --verbose --captureExceptions", 8 | "test-dev": "./node_modules/.bin/jasmine-node test/ --verbose --autotest --captureExceptions --color --watch src/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/blockai/blockcast.git" 13 | }, 14 | "keywords": [ 15 | "bitcoin", 16 | "blockchain", 17 | "message", 18 | "decentralized", 19 | "social", 20 | "crypto", 21 | "cryptocurrency" 22 | ], 23 | "author": "William Cotton", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/blockai/blockcast/issues" 27 | }, 28 | "homepage": "https://github.com/blockai/blockcast", 29 | "devDependencies": { 30 | "blockcypher-unofficial": "^1.5.0", 31 | "jasmine-node": "^1.14.5", 32 | "mem-common-blockchain": "^1.5.0", 33 | "node-env-file": "^0.1.7", 34 | "test-common-wallet": "^1.3.1" 35 | }, 36 | "dependencies": { 37 | "async": "^1.4.2", 38 | "bitcoin-tx-hex-to-json": "^1.2.0", 39 | "bitcoinjs-lib": "^1.5.8" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/bitcoin-transaction-builder.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | var bitcoin = require('bitcoinjs-lib') 3 | var txHexToJSON = require('bitcoin-tx-hex-to-json') 4 | 5 | var dataPayload = require('./data-payload') 6 | 7 | var OP_RETURN_BUFFER = new Buffer(1) 8 | OP_RETURN_BUFFER.writeUInt8(bitcoin.opcodes.OP_RETURN, 0) 9 | 10 | var loadAndSignTransaction = function (options, callback) { 11 | var tx = options.tx 12 | var address = options.address 13 | var fee = options.fee 14 | var destinationValue = options.destinationValue || 0 15 | var unspentOutputs = options.unspentOutputs 16 | var unspentValue = 0 17 | var compare = function (a, b) { 18 | if (a.value < b.value) { 19 | return -1 20 | } 21 | if (a.value > b.value) { 22 | return 1 23 | } 24 | return 0 25 | } 26 | unspentOutputs.sort(compare) 27 | var txInputIndex = tx.inputs.length 28 | for (var i = unspentOutputs.length - 1; i >= 0; i--) { 29 | var unspentOutput = unspentOutputs[i] 30 | if (unspentOutput.value === 0) { 31 | continue 32 | } 33 | unspentValue += unspentOutput.value 34 | tx.addInput(unspentOutput.txid, unspentOutput.vout) 35 | } 36 | var change = unspentValue - fee - destinationValue 37 | tx.addOutput(address, change) 38 | if (options.skipSign) { 39 | return callback(false, tx) 40 | } 41 | options.signTransaction(tx, txInputIndex, function (err, signedTx) { 42 | if (err) { } // TODO 43 | callback(false, signedTx) 44 | }) 45 | } 46 | 47 | var getPrimaryTransactions = function (transactions) { 48 | var primaryTransactions = [] 49 | for (var i = 0; i < transactions.length; i++) { 50 | var transactionHex = transactions[i] 51 | var transaction = txHexToJSON(transactionHex) 52 | var vout = transaction.vout 53 | for (var j = vout.length - 1; j >= 0; j--) { 54 | var output = vout[j] 55 | var scriptPubKeyASM = output.scriptPubKey.asm 56 | if (scriptPubKeyASM.split(' ')[0] === 'OP_RETURN') { 57 | var data = new Buffer(scriptPubKeyASM.split(' ')[1], 'hex') 58 | var length = dataPayload.parse(data) 59 | if (length) { 60 | primaryTransactions.push({tx: transaction, length: length, dataOutput: j, data: data}) 61 | } 62 | } 63 | } 64 | } 65 | return primaryTransactions 66 | } 67 | 68 | var createTransactionWithPayload = function (payload, primaryTxHex) { 69 | var primaryTx = primaryTxHex ? bitcoin.TransactionBuilder.fromTransaction(bitcoin.Transaction.fromHex(primaryTxHex)) : false 70 | var lengthBuffer = new Buffer(1) 71 | lengthBuffer.writeUInt8(payload.length, 0) 72 | var payloadScript = bitcoin.Script.fromChunks([bitcoin.opcodes.OP_RETURN, payload]) 73 | var tx = primaryTx || new bitcoin.TransactionBuilder() 74 | tx.addOutput(payloadScript, 0) 75 | return tx 76 | } 77 | 78 | var getData = function (options, callback) { 79 | var transactions = options.transactions 80 | var primaryTransactions = getPrimaryTransactions(transactions) 81 | var decodedTransactions = [] 82 | async.each(primaryTransactions, function (primaryTx, cb) { 83 | var tx = primaryTx.tx 84 | var data = primaryTx.data 85 | var length = primaryTx.length 86 | var dataOutput = primaryTx.dataOutput 87 | var payloads = [] 88 | for (var i = 0; i < length; i++) { 89 | if (i === 0) { 90 | payloads.push(data) 91 | } else { 92 | var prevTxid = tx.vin[dataOutput].txid 93 | transactions.forEach(function (txHex) { 94 | var _tx = txHexToJSON(txHex) 95 | if (prevTxid === _tx.txid) { 96 | tx = _tx 97 | var scriptPubKeyASM = tx.vout[dataOutput].scriptPubKey.asm 98 | var data = new Buffer(scriptPubKeyASM.split(' ')[1], 'hex') 99 | payloads.push(data) 100 | } 101 | }) 102 | } 103 | } 104 | dataPayload.decode(payloads, function (err, decodedData) { 105 | if (err) { } // TODO 106 | var decodedTransaction = { 107 | data: decodedData 108 | } 109 | decodedTransactions.push(decodedTransaction) 110 | cb() 111 | }) 112 | }, function (err) { 113 | if (err) { } // TODO 114 | callback(false, decodedTransactions) 115 | }) 116 | } 117 | 118 | var signFromTransactionHex = function (signTransactionHex) { 119 | if (!signTransactionHex) { 120 | return false 121 | } 122 | return function (tx, input, callback) { 123 | var txHex = tx.tx.toHex() 124 | signTransactionHex({txHex: txHex, input: input}, function (error, signedTxHex) { 125 | var signedTx = bitcoin.TransactionBuilder.fromTransaction(bitcoin.Transaction.fromHex(signedTxHex)) 126 | callback(error, signedTx) 127 | }) 128 | } 129 | } 130 | 131 | var createSignedTransactionsWithData = function (options, callback) { 132 | var primaryTxHex = options.primaryTxHex 133 | var usedTxids = [] 134 | var destinationAddress = options.destinationAddress 135 | var destinationValue = options.value || 0 136 | if (primaryTxHex) { 137 | var primaryTx = txHexToJSON(primaryTxHex) 138 | usedTxids = primaryTx.vin.map(function (input) { return input.txid }) 139 | } 140 | var signPrimaryTxHex = options.signPrimaryTxHex 141 | var commonWallet = options.commonWallet 142 | var address = commonWallet.address 143 | var returnWithUnsignedPrimary = options.returnWithUnsignedPrimary 144 | var fee = options.fee || 1000 145 | var commonBlockchain = options.commonBlockchain 146 | var buildStatus = options.buildStatus || function () {} 147 | var signTransaction = signFromTransactionHex(commonWallet.signRawTransaction) 148 | options.signTransaction = signTransaction 149 | var data = options.data 150 | commonBlockchain.Addresses.Unspents([address], function (err, addresses_unspents) { 151 | if (err) { } // TODO 152 | var unspentOutputs = addresses_unspents[0] 153 | dataPayload.create({data: data}, function (err, payloads) { 154 | if (err) { } // TODO 155 | var signedTransactions = [] 156 | var signedTransactionsCounter = payloads.length - 1 157 | var payloadsLength = payloads.length 158 | 159 | buildStatus({ 160 | response: 'got payloads', 161 | data: data, 162 | payloadsLength: payloadsLength 163 | }) 164 | 165 | var totalCost = payloadsLength * fee + destinationValue 166 | var existingUnspents = [] 167 | var unspentValue = 0 168 | var compare = function (a, b) { 169 | if (a.value < b.value) { 170 | return -1 171 | } 172 | if (a.value > b.value) { 173 | return 1 174 | } 175 | return 0 176 | } 177 | unspentOutputs.sort(compare) 178 | for (var i = unspentOutputs.length - 1; i >= 0; i--) { 179 | var unspentOutput = unspentOutputs[i] 180 | if (usedTxids.indexOf(unspentOutput.txid) > -1) { 181 | continue 182 | } 183 | unspentValue += unspentOutput.value 184 | existingUnspents.push(unspentOutput) 185 | if (unspentValue >= totalCost) { 186 | break 187 | } 188 | } 189 | 190 | var signedTransactionResponse = function (err, signedTx) { 191 | if (err) { } // TODO 192 | 193 | if (signedTransactionsCounter === 0 && destinationAddress && destinationValue) { 194 | signedTx.addOutput(destinationAddress, destinationValue) 195 | } 196 | 197 | var signedTxBuilt = signedTx.buildIncomplete() 198 | var signedTxHex = signedTxBuilt.toHex() 199 | var signedTxid = signedTxBuilt.getId() 200 | 201 | var onSignedTxHexAndId = function (signedTxHex, signedTxid) { 202 | buildStatus({ 203 | response: 'signed transaction', 204 | txid: signedTxid, 205 | count: signedTransactionsCounter, 206 | payloadsLength: payloadsLength 207 | }) 208 | signedTransactions[signedTransactionsCounter] = signedTxHex 209 | signedTransactionsCounter-- 210 | if (signedTransactionsCounter < 0) { 211 | callback(false, signedTransactions, signedTxid) 212 | } else { 213 | var vout = signedTx.tx.outs.length - 1 214 | 215 | var payload = payloads[signedTransactionsCounter] 216 | var tx 217 | if (signedTransactionsCounter === 0) { 218 | tx = createTransactionWithPayload(payload, primaryTxHex) 219 | } else { 220 | tx = createTransactionWithPayload(payload) 221 | } 222 | 223 | var value = signedTx.tx.outs[vout].value 224 | 225 | var unspent = { 226 | txid: signedTxid, 227 | vout: vout, 228 | value: value 229 | } 230 | 231 | var skipSign = returnWithUnsignedPrimary && signedTransactionsCounter === 0 232 | 233 | loadAndSignTransaction({ 234 | fee: fee, 235 | tx: tx, 236 | unspentOutputs: [unspent], 237 | address: address, 238 | skipSign: skipSign, 239 | signTransaction: options.signTransaction, 240 | destinationValue: (signedTransactionsCounter === 0) ? destinationValue : false 241 | }, signedTransactionResponse) 242 | } 243 | } 244 | 245 | if (signPrimaryTxHex && signedTransactionsCounter === 0) { 246 | signPrimaryTxHex(signedTxHex, function (err, signedTxHex) { 247 | if (err) { } // TODO 248 | var _tx = bitcoin.TransactionBuilder.fromTransaction(bitcoin.Transaction.fromHex(signedTxHex)) 249 | onSignedTxHexAndId(signedTxHex, _tx.build().getId()) 250 | }) 251 | } else { 252 | onSignedTxHexAndId(signedTxHex, signedTxid) 253 | } 254 | } 255 | 256 | var tx 257 | if (signedTransactionsCounter === 0) { 258 | tx = createTransactionWithPayload(payloads[signedTransactionsCounter], primaryTxHex) 259 | } else { 260 | tx = createTransactionWithPayload(payloads[signedTransactionsCounter]) 261 | } 262 | 263 | var signOptions = { 264 | fee: fee, 265 | tx: tx, 266 | unspentOutputs: existingUnspents, 267 | address: address, 268 | signTransaction: options.signTransaction, 269 | signPrimaryTxHex: signPrimaryTxHex, 270 | destinationValue: (signedTransactionsCounter === 0) ? destinationValue : false 271 | } 272 | 273 | loadAndSignTransaction(signOptions, signedTransactionResponse) 274 | }) 275 | }) 276 | } 277 | 278 | module.exports = { 279 | createSignedTransactionsWithData: createSignedTransactionsWithData, 280 | getData: getData 281 | } 282 | -------------------------------------------------------------------------------- /src/data-payload.js: -------------------------------------------------------------------------------- 1 | var zlib = require('zlib') 2 | 3 | var OP_RETURN_SIZE = 80 4 | 5 | var MAGIC_NUMBER = new Buffer('1f', 'hex') 6 | var VERSION = new Buffer('00', 'hex') 7 | 8 | var dth = function (d) { 9 | var h = Number(d).toString(16) 10 | while (h.length < 2) { 11 | h = '0' + h 12 | } 13 | return h 14 | } 15 | 16 | var compress = function (decompressedBuffer, callback) { 17 | zlib.deflateRaw(decompressedBuffer, function (err, compressedBuffer) { 18 | callback(err, compressedBuffer) 19 | }) 20 | } 21 | 22 | var decompress = function (compressedBuffer, callback) { 23 | zlib.inflateRaw(compressedBuffer, function (err, decompressedBuffer) { 24 | callback(err, decompressedBuffer) 25 | }) 26 | } 27 | 28 | var parse = function (payload) { 29 | var length = payload.slice(2, 3)[0] 30 | var valid = payload.slice(0, 1)[0] === MAGIC_NUMBER[0] && payload.slice(1, 2)[0] === VERSION[0] 31 | return valid ? length : false 32 | } 33 | 34 | var create = function (options, callback) { 35 | var data = options.data 36 | var payloads = [] 37 | var buffer = new Buffer(data) 38 | compress(buffer, function (err, compressedBuffer) { 39 | if (err) { } // TODO 40 | var dataLength = compressedBuffer.length 41 | if (dataLength > 1277) { 42 | callback('data payload > 1277', false) 43 | return 44 | } 45 | var count = OP_RETURN_SIZE - 3 46 | while (count < dataLength) { 47 | payload = compressedBuffer.slice(count, count + OP_RETURN_SIZE) 48 | count += OP_RETURN_SIZE 49 | payloads.push(payload) 50 | } 51 | var lengthByte = new Buffer(dth(payloads.length + 1), 'hex') 52 | var dataPayload = compressedBuffer.slice(0, OP_RETURN_SIZE - 3) 53 | var payload = Buffer.concat([MAGIC_NUMBER, VERSION, lengthByte, dataPayload]) 54 | payloads.unshift(payload) 55 | callback(false, payloads) 56 | }) 57 | } 58 | 59 | var decode = function (payloads, callback) { 60 | var firstPayload = payloads[0] 61 | var startHeader = firstPayload.slice(0, 3) 62 | var length = startHeader.slice(2, 3)[0] 63 | if (!length) { 64 | return callback('no start header', false) 65 | } 66 | if (payloads.length !== length) { 67 | return callback('length mismatch', false) 68 | } 69 | var compressedBuffer = new Buffer('') 70 | for (var i = 0; i < length; i++) { 71 | var payload = payloads[i] 72 | var dataPayload = i === 0 ? payload.slice(3, OP_RETURN_SIZE) : payload 73 | if (!dataPayload) { 74 | return callback('missing payload', false) 75 | } 76 | compressedBuffer = Buffer.concat([compressedBuffer, dataPayload]) 77 | } 78 | decompress(compressedBuffer, function (err, data) { 79 | if (err) { } // TODO 80 | if (!data || !data.toString) { 81 | return callback(true, '') 82 | } else { 83 | return callback(false, data.toString()) 84 | } 85 | }) 86 | } 87 | 88 | module.exports = { 89 | create: create, 90 | decode: decode, 91 | parse: parse 92 | } 93 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var txHexToJSON = require('bitcoin-tx-hex-to-json') 2 | 3 | var bitcoinTransactionBuilder = require('./bitcoin-transaction-builder') 4 | var dataPayload = require('./data-payload') 5 | 6 | var post = function (options, callback) { 7 | var commonWallet = options.commonWallet 8 | var commonBlockchain = options.commonBlockchain 9 | var data = options.data 10 | var fee = options.fee 11 | var primaryTxHex = options.primaryTxHex 12 | var destinationAddress = options.destinationAddress 13 | var value = options.value 14 | var signPrimaryTxHex = options.signPrimaryTxHex 15 | var propagationStatus = options.propagationStatus || function () {} 16 | var buildStatus = options.buildStatus || function () {} 17 | var retryMax = options.retryMax || 5 18 | var onSignedTransactions = function (err, signedTransactions, txid) { 19 | if (err) { } // TODO 20 | var reverseSignedTransactions = signedTransactions.reverse() 21 | var transactionTotal = reverseSignedTransactions.length 22 | var propagateCounter = 0 23 | var retryCounter = [] 24 | var propagateResponse = function (err, res) { 25 | propagationStatus({ 26 | response: res, 27 | count: propagateCounter, 28 | transactionTotal: transactionTotal 29 | }) 30 | if (err) { 31 | var rc = retryCounter[propagateCounter] || 0 32 | if (rc < retryMax) { 33 | retryCounter[propagateCounter] = rc + 1 34 | commonBlockchain.Transactions.Propagate(reverseSignedTransactions[propagateCounter], propagateResponse) 35 | } else { 36 | callback(err, false) 37 | } 38 | } 39 | propagateCounter++ 40 | if (propagateCounter < transactionTotal) { 41 | commonBlockchain.Transactions.Propagate(reverseSignedTransactions[propagateCounter], propagateResponse) 42 | } else { 43 | callback(false, { 44 | txid: txid, 45 | data: data, 46 | transactionTotal: transactionTotal 47 | }) 48 | } 49 | } 50 | commonBlockchain.Transactions.Propagate(reverseSignedTransactions[0], propagateResponse) 51 | } 52 | if (options.signedTransactions && options.txid) { 53 | return onSignedTransactions(false, options.signedTransactions, options.txid) 54 | } 55 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 56 | destinationAddress: destinationAddress, 57 | value: value, 58 | primaryTxHex: primaryTxHex, 59 | signPrimaryTxHex: signPrimaryTxHex, 60 | data: data, 61 | fee: fee, 62 | buildStatus: buildStatus, 63 | commonBlockchain: commonBlockchain, 64 | commonWallet: commonWallet 65 | }, onSignedTransactions) 66 | } 67 | 68 | var payloadsLength = function (options, callback) { 69 | dataPayload.create({data: options.data, id: 0}, function (err, payloads) { 70 | if (err) { 71 | callback(err, payloads) 72 | return 73 | } 74 | callback(false, payloads.length) 75 | }) 76 | } 77 | 78 | var scanSingle = function (options, callback) { 79 | var txid = options.txid 80 | var tx = options.tx ? options.tx.txHex ? txHexToJSON(options.tx.txHex) : options.tx : false 81 | var commonBlockchain = options.commonBlockchain 82 | var allTransactions = [] 83 | var payloads = [] 84 | var transactionTotal 85 | var addresses = [] 86 | var primaryTx 87 | var onTransaction = function (err, transactions, tx) { 88 | if (!tx && transactions[0].txHex) { 89 | tx = txHexToJSON(transactions[0].txHex) 90 | } 91 | if (!tx) { 92 | return callback(err, false) 93 | } 94 | if (allTransactions.length === 0) { 95 | primaryTx = tx 96 | tx.vin.forEach(function (vin) { 97 | vin.addresses.forEach(function (address) { 98 | if (addresses.indexOf(address) === -1) { 99 | addresses.push(address) 100 | } 101 | }) 102 | }) 103 | } 104 | var vout = tx.vout 105 | for (var j = vout.length - 1; j >= 0; j--) { 106 | var output = vout[j] 107 | var scriptPubKeyASM = output.scriptPubKey.asm 108 | if (scriptPubKeyASM.split(' ')[0] === 'OP_RETURN') { 109 | var hex = scriptPubKeyASM.split(' ')[1] || '' 110 | var data 111 | try { 112 | data = new Buffer(hex, 'hex') 113 | } catch (e) { 114 | data = new Buffer('', 'hex') 115 | } 116 | var parsedLength = dataPayload.parse(data) 117 | if (!transactionTotal) { 118 | transactionTotal = parsedLength 119 | } 120 | payloads.push(data) 121 | } 122 | } 123 | if (allTransactions.length === 0 && !parsedLength) { 124 | return callback('not blockcast', false) 125 | } 126 | allTransactions.push(tx) 127 | if (allTransactions.length === transactionTotal) { 128 | dataPayload.decode(payloads, function (err, data) { 129 | callback(err, data, addresses, primaryTx) 130 | }) 131 | return 132 | } 133 | var prevTxid = tx.vin[tx.vin.length - 1].txid 134 | if (!prevTxid) { 135 | callback('missing: ' + (allTransactions.length + 1), false) 136 | return 137 | } else { 138 | commonBlockchain.Transactions.Get([prevTxid], onTransaction) 139 | } 140 | } 141 | if (tx) { 142 | onTransaction(false, [], tx) 143 | } else { 144 | commonBlockchain.Transactions.Get([txid], onTransaction) 145 | } 146 | } 147 | 148 | module.exports = { 149 | post: post, 150 | scanSingle: scanSingle, 151 | payloadsLength: payloadsLength, 152 | bitcoinTransactionBuilder: bitcoinTransactionBuilder 153 | } 154 | -------------------------------------------------------------------------------- /test/bitcoin-transaction-builder-spec.js: -------------------------------------------------------------------------------- 1 | /* global jasmine, it, expect, describe */ 2 | 3 | jasmine.getEnv().defaultTimeoutInterval = 50000 4 | 5 | var bitcoinTransactionBuilder = require('../src/bitcoin-transaction-builder') 6 | var dataPayload = require('../src/data-payload') 7 | 8 | var txHexToJSON = require('bitcoin-tx-hex-to-json') 9 | var bitcoin = require('bitcoinjs-lib') 10 | var commonBlockchain = require('mem-common-blockchain')() 11 | 12 | var env = require('node-env-file') 13 | env('./.env', { raise: false }) 14 | 15 | var BLOCKCYPHER_TOKEN = process.env.BLOCKCYPHER_TOKEN 16 | 17 | var testnetCommonBlockchain = require('blockcypher-unofficial')({ 18 | key: BLOCKCYPHER_TOKEN, 19 | network: 'testnet' 20 | }) 21 | 22 | var testCommonWallet = require('test-common-wallet') 23 | 24 | var commonWallet = testCommonWallet({ 25 | seed: 'test', 26 | network: 'testnet', 27 | commonBlockchain: commonBlockchain 28 | }) 29 | 30 | var anotherCommonWallet = testCommonWallet({ 31 | seed: 'test1', 32 | network: 'testnet', 33 | commonBlockchain: testnetCommonBlockchain 34 | }) 35 | 36 | var loremIpsum = 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?' 37 | 38 | var randomString = function (length) { 39 | var characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz' 40 | var output = '' 41 | for (var i = 0; i < length; i++) { 42 | var r = Math.floor(Math.random() * characters.length) 43 | output += characters.substring(r, r + 1) 44 | } 45 | return output 46 | } 47 | 48 | describe('bitcoin transaction builder', function () { 49 | it('should create transaction with test0.data, using data0.utxo, signed with test0.privateKeyWIF and get text0.txHex', function (done) { 50 | var test0 = require('./raw-transactions/test0.json') 51 | 52 | var test0Wallet = { 53 | signRawTransaction: function (txHex, callback) { 54 | var index = 0 55 | var options 56 | if (typeof (txHex) === 'object') { 57 | options = txHex 58 | txHex = options.txHex 59 | index = options.index || 0 60 | } 61 | var tx = bitcoin.Transaction.fromHex(txHex) 62 | var key = bitcoin.ECKey.fromWIF(test0.privateKeyWIF) 63 | tx.sign(index, key) 64 | var signedTx = tx 65 | var txid = signedTx.getId() 66 | var signedTxHex = signedTx.toHex() 67 | callback(false, signedTxHex, txid) 68 | }, 69 | address: test0.address 70 | } 71 | 72 | var test0Blockchain = { 73 | Addresses: { 74 | Unspents: function (addresses, cb) { cb(false, [[test0.utxo]]) } 75 | } 76 | } 77 | 78 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 79 | data: test0.data, 80 | commonWallet: test0Wallet, 81 | commonBlockchain: test0Blockchain 82 | }, function (err, signedTransactions) { 83 | if (err) { } // TODO 84 | expect(signedTransactions.length).toBe(1) 85 | var txHex = signedTransactions[0] 86 | expect(txHex).toBe(test0.txHex) 87 | done() 88 | }) 89 | }) 90 | 91 | it('should create transaction with test1.data, using data1.utxo, signed with test1.privateKeyWIF and get text1.txHex', function (done) { 92 | var test1 = require('./raw-transactions/test1.json') 93 | 94 | var test1Wallet = { 95 | signRawTransaction: function (txHex, callback) { 96 | var index = 0 97 | var options 98 | if (typeof (txHex) === 'object') { 99 | options = txHex 100 | txHex = options.txHex 101 | index = options.index || 0 102 | } 103 | var tx = bitcoin.Transaction.fromHex(txHex) 104 | var key = bitcoin.ECKey.fromWIF(test1.privateKeyWIF) 105 | tx.sign(index, key) 106 | var signedTx = tx 107 | var txid = signedTx.getId() 108 | var signedTxHex = signedTx.toHex() 109 | callback(false, signedTxHex, txid) 110 | }, 111 | address: test1.address 112 | } 113 | 114 | var test1Blockchain = { 115 | Addresses: { 116 | Unspents: function (addresses, cb) { cb(false, [[test1.utxo]]) } 117 | } 118 | } 119 | 120 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 121 | data: test1.data, 122 | commonWallet: test1Wallet, 123 | commonBlockchain: test1Blockchain 124 | }, function (err, signedTransactions) { 125 | if (err) { } // TODO 126 | expect(signedTransactions.length).toBe(4) 127 | expect(signedTransactions).toEqual(test1.txHexes) 128 | done() 129 | }) 130 | }) 131 | 132 | it('should create the transaction for a random string of 30 bytes', function (done) { 133 | var data = randomString(30) 134 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 135 | data: data, 136 | commonWallet: commonWallet, 137 | commonBlockchain: commonBlockchain 138 | }, function (err, signedTransactions, txid) { 139 | if (err) { } // TODO 140 | expect(signedTransactions.length).toBe(1) 141 | var primaryTxHex = signedTransactions[0] 142 | var primaryTx = txHexToJSON(primaryTxHex) 143 | expect(primaryTx.txid).toBe(txid) 144 | var primaryData = new Buffer(primaryTx.vout[0].scriptPubKey.hex, 'hex') 145 | // console.log(primaryData) 146 | var length = dataPayload.parse(primaryData.slice(2, primaryData.length)) 147 | expect(length).toBe(1) 148 | bitcoinTransactionBuilder.getData({transactions: signedTransactions}, function (err, decodedTransactions) { 149 | if (err) { } // TODO 150 | // console.log(err, decodedTransactions) 151 | var decodedData = decodedTransactions[0].data 152 | expect(data).toBe(decodedData) 153 | done() 154 | }) 155 | }) 156 | }) 157 | 158 | it('should create the transaction for a random string of 170 bytes', function (done) { 159 | var data = randomString(170) 160 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 161 | data: data, 162 | commonWallet: commonWallet, 163 | commonBlockchain: commonBlockchain 164 | }, function (err, signedTransactions, txid) { 165 | if (err) { } // TODO 166 | expect(signedTransactions.length).toBe(2) 167 | var primaryTxHex = signedTransactions[0] 168 | var primaryTx = txHexToJSON(primaryTxHex) 169 | expect(primaryTx.txid).toBe(txid) 170 | var primaryData = new Buffer(primaryTx.vout[0].scriptPubKey.hex, 'hex') 171 | var length = dataPayload.parse(primaryData.slice(3, primaryData.length)) 172 | var txHex1 = signedTransactions[1] 173 | var tx1 = txHexToJSON(txHex1) 174 | expect(primaryTx.vin[0].txid).toBe(tx1.txid) 175 | expect(length).toBe(2) 176 | bitcoinTransactionBuilder.getData({transactions: signedTransactions}, function (err, decodedTransactions) { 177 | if (err) { } // TODO 178 | var decodedData = decodedTransactions[0].data 179 | expect(data).toBe(decodedData) 180 | done() 181 | }) 182 | }) 183 | }) 184 | 185 | it('should create the transaction for a random string of 320 bytes', function (done) { 186 | var data = randomString(320) 187 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 188 | data: data, 189 | commonWallet: commonWallet, 190 | commonBlockchain: commonBlockchain 191 | }, function (err, signedTransactions, txid) { 192 | if (err) { } // TODO 193 | expect(signedTransactions.length).toBe(4) 194 | var primaryTxHex = signedTransactions[0] 195 | var primaryTx = txHexToJSON(primaryTxHex) 196 | expect(primaryTx.txid).toBe(txid) 197 | var primaryData = new Buffer(primaryTx.vout[0].scriptPubKey.hex, 'hex') 198 | var length = dataPayload.parse(primaryData.slice(3, primaryData.length)) 199 | expect(length).toBe(4) 200 | var tx1 = txHexToJSON(signedTransactions[1]) 201 | expect(primaryTx.vin[0].txid).toBe(tx1.txid) 202 | var tx2 = txHexToJSON(signedTransactions[2]) 203 | expect(tx1.vin[0].txid).toBe(tx2.txid) 204 | var tx3 = txHexToJSON(signedTransactions[3]) 205 | expect(tx2.vin[0].txid).toBe(tx3.txid) 206 | bitcoinTransactionBuilder.getData({transactions: signedTransactions}, function (err, decodedTransactions) { 207 | if (err) { } // TODO 208 | var decodedData = decodedTransactions[0].data 209 | expect(data).toBe(decodedData) 210 | done() 211 | }) 212 | }) 213 | }) 214 | 215 | it('should create the transaction for a random string of 675 bytes', function (done) { 216 | var data = randomString(675) 217 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 218 | data: data, 219 | commonWallet: commonWallet, 220 | commonBlockchain: commonBlockchain 221 | }, function (err, signedTransactions, txid) { 222 | if (err) { } // TODO 223 | expect(signedTransactions.length).toBe(7) 224 | var primaryTxHex = signedTransactions[0] 225 | var primaryTx = txHexToJSON(primaryTxHex) 226 | expect(primaryTx.txid).toBe(txid) 227 | var primaryData = new Buffer(primaryTx.vout[0].scriptPubKey.hex, 'hex') 228 | var length = dataPayload.parse(primaryData.slice(3, primaryData.length)) 229 | expect(length).toBe(7) 230 | signedTransactions.forEach(function (signedTxHex) { 231 | var signedTx = txHexToJSON(signedTxHex) 232 | signedTx.vin.forEach(function (vin) { 233 | expect(vin.scriptSig.hex).not.toBe('') 234 | }) 235 | }) 236 | bitcoinTransactionBuilder.getData({transactions: signedTransactions}, function (err, decodedTransactions) { 237 | if (err) { } // TODO 238 | var decodedData = decodedTransactions[0].data 239 | expect(data).toBe(decodedData) 240 | done() 241 | }) 242 | }) 243 | }) 244 | 245 | it('should create the transaction for full latin paragraph of 1265 bytes', function (done) { 246 | var data = loremIpsum.slice(0, 1265) 247 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 248 | data: data, 249 | commonWallet: commonWallet, 250 | commonBlockchain: commonBlockchain 251 | }, function (err, signedTransactions, txid) { 252 | if (err) { } // TODO 253 | expect(signedTransactions.length).toBe(6) 254 | signedTransactions.forEach(function (signedTxHex) { 255 | var signedTx = txHexToJSON(signedTxHex) 256 | signedTx.vin.forEach(function (vin) { 257 | expect(vin.scriptSig.hex).not.toBe('') 258 | }) 259 | }) 260 | bitcoinTransactionBuilder.getData({transactions: signedTransactions}, function (err, decodedTransactions) { 261 | if (err) { } // TODO 262 | var decodedData = decodedTransactions[0].data 263 | expect(data).toBe(decodedData) 264 | done() 265 | }) 266 | }) 267 | }) 268 | 269 | it('should create the transaction with a custom primaryTxHex with 30 bytes', function (done) { 270 | var data = randomString(30) 271 | var value = 12345 272 | anotherCommonWallet.createTransaction({ 273 | destinationAddress: commonWallet.address, 274 | value: value, 275 | skipSign: true 276 | }, function (err, primaryTxHex) { 277 | if (err) { } // TODO 278 | var primaryTx = txHexToJSON(primaryTxHex) 279 | expect(primaryTx.vout[0].value).toBe(value) 280 | expect(primaryTx.vin[0].scriptSig.hex).toBe('') 281 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 282 | primaryTxHex: primaryTxHex, 283 | data: data, 284 | commonWallet: commonWallet, 285 | commonBlockchain: testnetCommonBlockchain 286 | }, function (err, signedTransactions, txid) { 287 | if (err) { } // TODO 288 | expect(signedTransactions.length).toBe(1) 289 | var primaryTxHex = signedTransactions[0] 290 | var primaryTx = txHexToJSON(primaryTxHex) 291 | expect(primaryTx.txid).toBe(txid) 292 | expect(primaryTx.vin[0].scriptSig.hex).toBe('') 293 | expect(primaryTx.vout[0].value).toBe(value) 294 | expect(primaryTx.vout[2].value).toBe(0) 295 | var primaryData = new Buffer(primaryTx.vout[2].scriptPubKey.hex, 'hex') 296 | var length = dataPayload.parse(primaryData.slice(2, primaryData.length)) 297 | expect(length).toBe(1) 298 | anotherCommonWallet.signRawTransaction({txHex: primaryTxHex, input: 0}, function (err, signedTxHex) { 299 | if (err) { } // TODO 300 | var signedTx = txHexToJSON(signedTxHex) 301 | signedTx.vin.forEach(function (vin) { 302 | expect(vin.scriptSig.hex).not.toBe('') 303 | }) 304 | testnetCommonBlockchain.Transactions.Propagate(signedTxHex, function (err, res) { 305 | console.log(res.status, '1/1') 306 | if (err) { 307 | return done(err) 308 | } 309 | done() 310 | // var txids = [res.txid] 311 | // testnetCommonBlockchain.Transactions.Get(txids, function(err, transactions) { 312 | // expect(transactions[0].vout[0].value).toBe(value) 313 | // bitcoinTransactionBuilder.getData({commonWallet: commonWallet, transactions:transactions, id:id}, function(error, decodedData) { 314 | // expect(data).toBe(decodedData) 315 | // done() 316 | // }) 317 | // }) 318 | }) 319 | }) 320 | }) 321 | }) 322 | }) 323 | 324 | it('should create the transaction with a custom primaryTxHex with 120 bytes', function (done) { 325 | var data = randomString(120) 326 | var value = 12345 327 | var signPrimaryTxHex = function (txHex, callback) { 328 | anotherCommonWallet.signRawTransaction({txHex: txHex, input: 0}, callback) 329 | } 330 | anotherCommonWallet.createTransaction({ 331 | destinationAddress: commonWallet.address, 332 | value: value, 333 | skipSign: true 334 | }, function (err, primaryTxHex) { 335 | if (err) { } // TODO 336 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 337 | primaryTxHex: primaryTxHex, 338 | signPrimaryTxHex: signPrimaryTxHex, 339 | data: data, 340 | commonWallet: commonWallet, 341 | commonBlockchain: testnetCommonBlockchain 342 | }, function (err, signedTransactions, txid) { 343 | if (err) { } // TODO 344 | expect(signedTransactions.length).toBe(2) 345 | var primaryTxHex = signedTransactions[0] 346 | var primaryTx = txHexToJSON(primaryTxHex) 347 | expect(primaryTx.txid).toBe(txid) 348 | expect(primaryTx.vout[0].value).toBe(value) 349 | expect(primaryTx.vout[2].value).toBe(0) 350 | signedTransactions.forEach(function (signedTxHex) { 351 | var signedTx = txHexToJSON(signedTxHex) 352 | signedTx.vin.forEach(function (vin) { 353 | expect(vin.scriptSig.hex).not.toBe('') 354 | }) 355 | }) 356 | var primaryData = new Buffer(primaryTx.vout[2].scriptPubKey.hex, 'hex') 357 | var length = dataPayload.parse(primaryData.slice(3, primaryData.length)) 358 | expect(length).toBe(2) 359 | var txHex1 = signedTransactions[1] 360 | var tx1 = txHexToJSON(txHex1) 361 | expect(primaryTx.vin[1].txid).toBe(tx1.txid) 362 | done() 363 | }) 364 | }) 365 | }) 366 | 367 | it('should create the transaction with a custom primaryTxHex with 320 bytes', function (done) { 368 | var data = randomString(320) 369 | var value = 12345 370 | var signPrimaryTxHex = function (txHex, callback) { 371 | anotherCommonWallet.signRawTransaction({txHex: txHex, input: 0}, callback) 372 | } 373 | anotherCommonWallet.createTransaction({ 374 | destinationAddress: commonWallet.address, 375 | value: value, 376 | skipSign: true 377 | }, function (err, primaryTxHex) { 378 | if (err) { } // TODO 379 | bitcoinTransactionBuilder.createSignedTransactionsWithData({ 380 | primaryTxHex: primaryTxHex, 381 | signPrimaryTxHex: signPrimaryTxHex, 382 | data: data, 383 | commonWallet: commonWallet, 384 | commonBlockchain: testnetCommonBlockchain 385 | }, function (err, signedTransactions, txid) { 386 | if (err) { } // TODO 387 | expect(signedTransactions.length).toBe(4) 388 | var primaryTxHex = signedTransactions[0] 389 | var primaryTx = txHexToJSON(primaryTxHex) 390 | var primaryData = new Buffer(primaryTx.vout[2].scriptPubKey.hex, 'hex') 391 | var length = dataPayload.parse(primaryData.slice(3, primaryData.length)) 392 | expect(length).toBe(4) 393 | signedTransactions.forEach(function (signedTxHex) { 394 | var signedTx = txHexToJSON(signedTxHex) 395 | signedTx.vin.forEach(function (vin) { 396 | expect(vin.scriptSig.hex).not.toBe('') 397 | }) 398 | }) 399 | expect(primaryTx.txid).toBe(txid) 400 | done() 401 | }) 402 | }) 403 | }) 404 | }) 405 | -------------------------------------------------------------------------------- /test/data-payload-spec.js: -------------------------------------------------------------------------------- 1 | /* global it, expect, describe */ 2 | 3 | var dataPayload = require('../src/data-payload') 4 | 5 | var randomString = function (length) { 6 | var characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz' 7 | var output = '' 8 | for (var i = 0; i < length; i++) { 9 | var r = Math.floor(Math.random() * characters.length) 10 | output += characters.substring(r, r + 1) 11 | } 12 | return output 13 | } 14 | 15 | var OP_RETURN_SIZE = 80 16 | 17 | var loremIpsum = 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?' 18 | 19 | var randomJsonObject = function (messageLength) { 20 | var r = { 21 | 'm': loremIpsum.slice(0, messageLength), 22 | 'i': randomString(36), 23 | 't': +(new Date()) 24 | } 25 | return JSON.stringify(r) 26 | } 27 | 28 | describe('data payload', function () { 29 | it('should parse a data payload', function (done) { 30 | var payload = new Buffer('1f0002', 'hex') 31 | var length = dataPayload.parse(payload) 32 | expect(length).toBe(2) 33 | done() 34 | }) 35 | 36 | it('should no parse a bad data payload', function (done) { 37 | var payload = new Buffer('1f0178', 'hex') 38 | var length = dataPayload.parse(payload) 39 | expect(length).toBe(false) 40 | done() 41 | }) 42 | 43 | it('should create a data payload for some random data of 30 bytes', function (done) { 44 | var data = randomString(30) 45 | dataPayload.create({data: data}, function (err, payloads) { 46 | if (err) { } // TODO 47 | expect(payloads.length).toBe(1) 48 | expect(payloads[0].length).toBeLessThan(OP_RETURN_SIZE + 1) 49 | done() 50 | }) 51 | }) 52 | 53 | it('should create a data payload for a latin sentence sentence of 30 bytes', function (done) { 54 | var data = loremIpsum.slice(0, 30) 55 | dataPayload.create({data: data}, function (err, payloads) { 56 | if (err) { } // TODO 57 | expect(payloads.length).toBe(1) 58 | expect(payloads[0].length).toBeLessThan(OP_RETURN_SIZE + 1) 59 | done() 60 | }) 61 | }) 62 | 63 | it('should create a data payload for some random data of 270 bytes', function (done) { 64 | var data = randomString(270) 65 | dataPayload.create({data: data}, function (err, payloads) { 66 | if (err) { } // TODO 67 | expect(payloads.length).toBe(3) 68 | expect(payloads[0].length).toBe(OP_RETURN_SIZE) 69 | expect(payloads[2].length).toBeLessThan(OP_RETURN_SIZE + 1) 70 | done() 71 | }) 72 | }) 73 | 74 | it('should create a data payload for a latin sentence sentence of 70 bytes', function (done) { 75 | var data = loremIpsum.slice(0, 270) 76 | dataPayload.create({data: data}, function (err, payloads) { 77 | if (err) { } // TODO 78 | expect(payloads.length).toBe(3) 79 | expect(payloads[0].length).toBe(OP_RETURN_SIZE) 80 | expect(payloads[2].length).toBeLessThan(OP_RETURN_SIZE + 1) 81 | done() 82 | }) 83 | }) 84 | 85 | it('should create a data payload for some random data of 110 bytes', function (done) { 86 | var data = randomString(110) 87 | dataPayload.create({data: data}, function (err, payloads) { 88 | if (err) { } // TODO 89 | expect(payloads.length).toBe(2) 90 | expect(payloads[0].length).toBe(OP_RETURN_SIZE) 91 | expect(payloads[1].length).toBeLessThan(OP_RETURN_SIZE + 1) 92 | done() 93 | }) 94 | }) 95 | 96 | it('should create a data payload for a latin sentence sentence of 110 bytes', function (done) { 97 | var data = loremIpsum.slice(0, 110) 98 | dataPayload.create({data: data}, function (err, payloads) { 99 | if (err) { } // TODO 100 | expect(payloads.length).toBe(2) 101 | expect(payloads[0].length).toBe(OP_RETURN_SIZE) 102 | expect(payloads[1].length).toBeLessThan(OP_RETURN_SIZE + 1) 103 | done() 104 | }) 105 | }) 106 | 107 | it('should create a data payload for some random data of 700 bytes', function (done) { 108 | var data = randomString(1600) 109 | dataPayload.create({data: data}, function (err, payloads) { 110 | if (err) { } // TODO 111 | expect(payloads.length).toBe(16) 112 | for (var i = 0; i < payloads.length - 1; i++) { 113 | var payload = payloads[i] 114 | expect(payload.length).toBe(OP_RETURN_SIZE) 115 | } 116 | expect(payloads[15].length).toBeLessThan(OP_RETURN_SIZE + 1) 117 | done() 118 | }) 119 | }) 120 | 121 | it('should create a data payload for some random data of OP_RETURN_SIZE0 bytes', function (done) { 122 | var data = randomString(750) 123 | dataPayload.create({data: data}, function (err, payloads) { 124 | if (err) { } // TODO 125 | expect(payloads.length).toBe(8) 126 | for (var i = 0; i < payloads.length - 1; i++) { 127 | var payload = payloads[i] 128 | expect(payload.length).toBe(OP_RETURN_SIZE) 129 | } 130 | expect(payloads[7].length).toBeLessThan(OP_RETURN_SIZE + 1) 131 | done() 132 | }) 133 | }) 134 | 135 | it('should create a data payload for the full latin paragraph of 865 bytes', function (done) { 136 | var data = loremIpsum.slice(0, 965) 137 | dataPayload.create({data: data}, function (err, payloads) { 138 | if (err) { } // TODO 139 | expect(payloads.length).toBe(6) 140 | for (var i = 0; i < payloads.length - 1; i++) { 141 | var payload = payloads[i] 142 | expect(payload.length).toBe(OP_RETURN_SIZE) 143 | } 144 | expect(payloads[5].length).toBeLessThan(OP_RETURN_SIZE + 1) 145 | done() 146 | }) 147 | }) 148 | 149 | it('should create a data payload for some JSON data', function (done) { 150 | var data = randomJsonObject(865) 151 | dataPayload.create({data: data}, function (err, payloads) { 152 | if (err) { } // TODO 153 | expect(payloads.length).toBe(7) 154 | done() 155 | }) 156 | }) 157 | 158 | it('should create a known data payload and then decode it', function (done) { 159 | var data = '{"op":"t","sha1":"5eac19f495bfa2502b76e39c8b1be7aaf66225d5","value":50000,"ttl":365}' 160 | dataPayload.create({data: data}, function (err, payloads) { 161 | if (err) { } // TODO 162 | dataPayload.decode(payloads, function (err, decodedData) { 163 | if (err) { } // TODO 164 | expect(decodedData).toBe(data) 165 | done() 166 | }) 167 | }) 168 | }) 169 | 170 | it('should create a data payload for some 30 byte data and then decode it', function (done) { 171 | var data = loremIpsum.slice(0, 30) 172 | dataPayload.create({data: data}, function (err, payloads) { 173 | if (err) { } // TODO 174 | dataPayload.decode(payloads, function (err, decodedData) { 175 | if (err) { } // TODO 176 | expect(decodedData).toBe(data) 177 | done() 178 | }) 179 | }) 180 | }) 181 | 182 | it('should create a data payload for some 78 byte data and then decode it', function (done) { 183 | var data = loremIpsum.slice(0, 78) 184 | dataPayload.create({data: data}, function (err, payloads) { 185 | if (err) { } // TODO 186 | dataPayload.decode(payloads, function (err, decodedData) { 187 | if (err) { } // TODO 188 | expect(decodedData).toBe(data) 189 | done() 190 | }) 191 | }) 192 | }) 193 | 194 | it('should create a data payload for some JSON data and then decode it', function (done) { 195 | var data = randomJsonObject(865) 196 | dataPayload.create({data: data}, function (err, payloads) { 197 | if (err) { } // TODO 198 | dataPayload.decode(payloads, function (err, decodedData) { 199 | if (err) { } // TODO 200 | expect(decodedData).toBe(data) 201 | done() 202 | }) 203 | }) 204 | }) 205 | 206 | // it("should not create a data payload for a larger amount of data", function(done) { 207 | // var data = randomString(1200) 208 | // dataPayload.create({data: data}, function(err, payloads) { 209 | // expect(err).toBeDefined() 210 | // expect(payloads).toBe(false) 211 | // done() 212 | // }) 213 | // }) 214 | }) 215 | -------------------------------------------------------------------------------- /test/index-spec.js: -------------------------------------------------------------------------------- 1 | /* global jasmine, it, expect, describe */ 2 | 3 | jasmine.getEnv().defaultTimeoutInterval = 50000 4 | 5 | var blockcast = require('../src/index') 6 | 7 | var env = require('node-env-file') 8 | env('./.env', { raise: false }) 9 | 10 | var loremIpsum = 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?' 11 | 12 | var BLOCKCYPHER_TOKEN = process.env.BLOCKCYPHER_TOKEN 13 | 14 | var commonBlockchain = require('blockcypher-unofficial')({ 15 | key: BLOCKCYPHER_TOKEN, 16 | network: 'testnet' 17 | }) 18 | 19 | var memCommonBlockchain = require('mem-common-blockchain')() 20 | 21 | var randomString = function (length) { 22 | var characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz' 23 | var output = '' 24 | for (var i = 0; i < length; i++) { 25 | var r = Math.floor(Math.random() * characters.length) 26 | output += characters.substring(r, r + 1) 27 | } 28 | return output 29 | } 30 | 31 | var testCommonWallet = require('test-common-wallet') 32 | 33 | var commonWallet = testCommonWallet({ 34 | seed: 'test', 35 | network: 'testnet', 36 | commonBlockchain: commonBlockchain 37 | }) 38 | 39 | var anotherCommonWallet = testCommonWallet({ 40 | seed: 'test1', 41 | network: 'testnet', 42 | commonBlockchain: commonBlockchain 43 | }) 44 | 45 | var JSONdata = JSON.stringify({ 46 | op: 'r', 47 | btih: '335400c43179bb1ad0085289e4e60c0574e6252e', 48 | sha1: 'dc724af18fbdd4e59189f5fe768a5f8311527050', 49 | ipfs: 'QmcJf1w9bVpquGdzCp86pX4K21Zcn7bJBUtrBP1cr2NFuR', 50 | name: 'test.txt', 51 | size: 7, 52 | type: 'text/plain', 53 | title: 'A text file for testing', 54 | keywords: 'test, text, txt' 55 | }) 56 | 57 | describe('blockcast', function () { 58 | it('should post a message of a random string of 170 bytes', function (done) { 59 | var data = randomString(170) 60 | 61 | blockcast.post({ 62 | data: data, 63 | commonWallet: commonWallet, 64 | commonBlockchain: commonBlockchain 65 | }, function (err, blockcastTx) { 66 | if (err) { } // TODO 67 | console.log(blockcastTx) 68 | expect(blockcastTx.data).toBe(data) 69 | expect(blockcastTx.txid).toBeDefined() 70 | expect(blockcastTx.transactionTotal).toBe(2) 71 | done() 72 | }) 73 | }) 74 | 75 | it('should post a message of a random string of 276 bytes', function (done) { 76 | blockcast.post({ 77 | data: JSONdata, 78 | commonWallet: commonWallet, 79 | commonBlockchain: commonBlockchain 80 | }, function (err, blockcastTx) { 81 | if (err) { } // TODO 82 | console.log(blockcastTx) 83 | expect(blockcastTx.data).toBe(JSONdata) 84 | expect(blockcastTx.txid).toBeDefined() 85 | expect(blockcastTx.transactionTotal).toBe(3) 86 | done() 87 | }) 88 | }) 89 | 90 | it('should post a message with a primaryTx', function (done) { 91 | var data = JSON.stringify({ 92 | op: 't', 93 | value: 50000000, 94 | sha1: 'dd09da17ec523e92e38b5f141d9625a5e77bb9fa' 95 | }) 96 | 97 | var signPrimaryTxHex = function (txHex, callback) { 98 | anotherCommonWallet.signRawTransaction({txHex: txHex, input: 0}, callback) 99 | } 100 | 101 | var value = 12345 102 | anotherCommonWallet.createTransaction({ 103 | destinationAddress: commonWallet.address, 104 | value: value, 105 | skipSign: true 106 | }, function (err, primaryTxHex) { 107 | if (err) { } // TODO 108 | blockcast.post({ 109 | primaryTxHex: primaryTxHex, 110 | signPrimaryTxHex: signPrimaryTxHex, 111 | data: data, 112 | commonWallet: commonWallet, 113 | commonBlockchain: commonBlockchain 114 | }, function (err, blockcastTx) { 115 | if (err) { } // TODO 116 | console.log(blockcastTx) 117 | expect(blockcastTx.data).toBe(data) 118 | expect(blockcastTx.txid).toBeDefined() 119 | expect(blockcastTx.transactionTotal).toBe(1) 120 | done() 121 | }) 122 | }) 123 | }) 124 | 125 | it('should get the payloads length', function (done) { 126 | var data = loremIpsum 127 | blockcast.payloadsLength({data: data}, function (err, payloadsLength) { 128 | if (err) { } // TODO 129 | expect(payloadsLength).toBe(6) 130 | done() 131 | }) 132 | }) 133 | 134 | it('should warn when the payloads length is too big', function (done) { 135 | var data = randomString(4200) 136 | blockcast.payloadsLength({data: data}, function (err, payloadsLength) { 137 | expect(err).toBe('data payload > 1277') 138 | expect(payloadsLength).toBe(false) 139 | done() 140 | }) 141 | }) 142 | 143 | it('should scan single txid 7be2dbaab47b7f71d0fb8919824119a3e2ebbff23d0b5d4f15fa023f3d55eb95', function (done) { 144 | var txid = '7be2dbaab47b7f71d0fb8919824119a3e2ebbff23d0b5d4f15fa023f3d55eb95' 145 | blockcast.scanSingle({ 146 | txid: txid, 147 | commonBlockchain: commonBlockchain 148 | }, function (err, data, addresses, primaryTx) { 149 | if (err) { } // TODO 150 | expect(addresses[0]).toBe('mwaj74EideMcpe4cjieuPFpqacmpjtKSk1') 151 | expect(data).toBe('{"op":"t","value":50000000,"sha1":"dd09da17ec523e92e38b5f141d9625a5e77bb9fa"}') 152 | expect(primaryTx.txid).toBe(txid) 153 | expect(primaryTx.vin.length).toBe(2) 154 | done() 155 | }) 156 | }) 157 | 158 | it('should scan single txid 7cf57a5a9c7db909298db28b09271b497039e50ab8a26f200c8edaba68d0a190', function (done) { 159 | var txid = '7cf57a5a9c7db909298db28b09271b497039e50ab8a26f200c8edaba68d0a190' 160 | blockcast.scanSingle({ 161 | txid: txid, 162 | commonBlockchain: commonBlockchain 163 | }, function (err, data, addresses, primaryTx) { 164 | if (err) { } // TODO 165 | expect(addresses[0]).toBe('msLoJikUfxbc2U5UhRSjc2svusBSqMdqxZ') 166 | expect(data).toBe(JSONdata) 167 | expect(primaryTx.vin.length).toBe(1) 168 | done() 169 | }) 170 | }) 171 | 172 | it('should not scan single txid b32192c9d2d75a8a28dd4034ea61eacb0dfe4f226acb502cfe108df20fbddebc', function (done) { 173 | var txid = 'b32192c9d2d75a8a28dd4034ea61eacb0dfe4f226acb502cfe108df20fbddebc' 174 | blockcast.scanSingle({ 175 | txid: txid, 176 | commonBlockchain: commonBlockchain 177 | }, function (err, data) { 178 | if (err) { } // TODO 179 | expect(data).toBe(false) 180 | expect(err).toBe('not blockcast') 181 | done() 182 | }) 183 | }) 184 | 185 | it('should post a message of a random string of 720 bytes and then scan (memCommonBlockchain) ', function (done) { 186 | var randomStringData = randomString(720) 187 | blockcast.post({ 188 | data: randomStringData, 189 | commonWallet: commonWallet, 190 | commonBlockchain: memCommonBlockchain 191 | }, function (err, blockcastTx) { 192 | if (err) { } // TODO 193 | expect(blockcastTx.txid).toBeDefined() 194 | expect(blockcastTx.transactionTotal).toBe(8) 195 | blockcast.scanSingle({ 196 | txid: blockcastTx.txid, 197 | commonBlockchain: memCommonBlockchain 198 | }, function (err, data, addresses) { 199 | if (err) { } // TODO 200 | expect(addresses[0]).toBe('msLoJikUfxbc2U5UhRSjc2svusBSqMdqxZ') 201 | expect(data).toBe(randomStringData) 202 | done() 203 | }) 204 | done() 205 | }) 206 | }) 207 | }) 208 | -------------------------------------------------------------------------------- /test/raw-transactions/test0.json: -------------------------------------------------------------------------------- 1 | { 2 | "utxo": { 3 | "txid": "03af5bf0b3fe25db04b684ab41bea8cdd127e57822602b8545beaf06685967c8", 4 | "vout": 0, 5 | "value": 1000000 6 | }, 7 | "privateKeyWIF": "KyjhazeX7mXpHedQsKMuGh56o3rh8hm8FGhU3H6HPqfP9pA4YeoS", 8 | "address": "n3PDRtKoHXHNt8FU17Uu9Te81AnKLa7oyU", 9 | "data": "testData0", 10 | "txHex": "0100000001c867596806afbe45852b602278e527d1cda8be41ab84b604db25feb3f05baf03000000006b483045022100e4aa91932e04d68887c80fc4be5a6ff31c8c040938983e9b249e1f5885ab86590220627b547b2afaafe40b3e6e3f7aaae6e3d8fed185d2feefc70f7be314e0f24c84012102bb80ce863dbd6132336d7cc915880d8cf5cdbf17787ded978b078f11d9441abfffffffff020000000000000000106a0e1f00012b492d2e71492c49340000583e0f00000000001976a914efdc12d9bd12a9a599d6d44706dd2328760c500188ac00000000" 11 | } -------------------------------------------------------------------------------- /test/raw-transactions/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "utxo": { 3 | "txid": "03af5bf0b3fe25db04b684ab41bea8cdd127e57822602b8545beaf06685967c8", 4 | "vout": 0, 5 | "value": 1000000 6 | }, 7 | "privateKeyWIF": "KyjhazeX7mXpHedQsKMuGh56o3rh8hm8FGhU3H6HPqfP9pA4YeoS", 8 | "address": "n3PDRtKoHXHNt8FU17Uu9Te81AnKLa7oyU", 9 | "data": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", 10 | "txHexes": [ "0100000001b4c7ef2c4774265dc89a7e027cd5fba1cb37130bf14b4352dd8126e5051bb300010000006b4830450221008fa4bc8f747184a58be37a9eeab85ac86f19a4d54661ae83a60745287cc971cb02205a0d903915072626dbc9ad2e6bd3610a72675840554cc5e8803abe73658ea15f012102bb80ce863dbd6132336d7cc915880d8cf5cdbf17787ded978b078f11d9441abfffffffff020000000000000000536a4c501f000455915d6ec3300c83afc203143dca5e7602d5d63a01fe492c39d8f147271dd0bd048e40991fe94fcd98814d876f964cc21cb36545af8d47f3503489e9d031fa805be0e8656e21a11592d2746961a0320f00000000001976a914efdc12d9bd12a9a599d6d44706dd2328760c500188ac00000000", "01000000013e7333b0f67956b74c20461b1a2e65a247eeae3df1bb73f7fdf51bc69a754841010000006a4730440220219b5b637b3e4cc24edd718d209a2c949404f9dd492a1119fb3254fcab23d662022040a67ca264b7d08ebb3ef6ac3f0e8f6a57a2966038376d022aa11e29dcfa10a1012102bb80ce863dbd6132336d7cc915880d8cf5cdbf17787ded978b078f11d9441abfffffffff020000000000000000536a4c50b322f7d287d67d2a8acc7c4d6f881e523196982e261ca92c916d2ed8a728e4012ba5c3daa12d78090e2ae384d1581a37c848df169aa2e3a1125c3b6c7db3a510f86c01fdd98a2579f43b3eb47668b37a88360f00000000001976a914efdc12d9bd12a9a599d6d44706dd2328760c500188ac00000000", "01000000019e8bb9d47a19dc310f6bb988193a3bc48005ebee8da45554effee1946599816c010000006b483045022100cee9a6f3f3cd8a2a7fb19ca830b1ac60cf2597dc6ac22b9d21ebfb7714f087920220247586150f3a3d4e097ebe43a382ef21642ef40d2d1b06fdf3b9019b329e2163012102bb80ce863dbd6132336d7cc915880d8cf5cdbf17787ded978b078f11d9441abfffffffff020000000000000000536a4c50dad477e67d9afcfdfb19489c642be580b08c9ed78c87aff9b4b8c1d9d1b9947a73dd27ada8acf26cf60a4dccee4b8341e8def4dd6fad189a7a326e2eb4957eebec73ad38e355a8d368e95e2d2eecf962703a0f00000000001976a914efdc12d9bd12a9a599d6d44706dd2328760c500188ac00000000", "0100000001c867596806afbe45852b602278e527d1cda8be41ab84b604db25feb3f05baf03000000006b4830450221009dde97b1044fde1cadf209cc8719b0de61d6fb2a5d03a8e982035dcbe49af5ba022067c405eeaae77e1e19a1240e2db3565ffdf4e59ee2879ebdd19fcc62b336020e012102bb80ce863dbd6132336d7cc915880d8cf5cdbf17787ded978b078f11d9441abfffffffff020000000000000000426a403d47176855ca4e8c144a8a1b24db66bc9a95957fb0ad37b459afdb8d0f58990b04a2b3b0ea647975c698858db173367d999fd1b825c5ceedf546ccf596e9fe0b583e0f00000000001976a914efdc12d9bd12a9a599d6d44706dd2328760c500188ac00000000" ] 11 | } --------------------------------------------------------------------------------