├── .gitignore ├── images ├── channel.png └── open_tx.png ├── scripts ├── sender.js ├── config.js ├── funding.js ├── htlc │ ├── 0-config.js │ ├── 1-funding.js │ ├── 2-funding.js │ └── 3-open.js ├── server.js ├── spend.js ├── client.js └── rxserver.js ├── architecture.md ├── package.json ├── lib ├── index.js ├── channel.js ├── utils.js └── messages.js ├── verify.js ├── double-spend.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /images/channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yemel/bitcore-stream/HEAD/images/channel.png -------------------------------------------------------------------------------- /images/open_tx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yemel/bitcore-stream/HEAD/images/open_tx.png -------------------------------------------------------------------------------- /scripts/sender.js: -------------------------------------------------------------------------------- 1 | 2 | var bitcore = require('bitcore-lib'); 3 | var SenderChannel = require('../lib/channel'); 4 | 5 | 6 | var privateKey = new bitcore.PrivateKey(); 7 | 8 | var channel = new SenderChannel({ 9 | privateKey: privateKey 10 | }); 11 | 12 | console.log(channel); 13 | console.log('Balance', channel.balance); 14 | console.log('Status', channel.status); 15 | console.log('Expiration', channel.expiration); 16 | -------------------------------------------------------------------------------- /architecture.md: -------------------------------------------------------------------------------- 1 | 2 | # Architecture 3 | 4 | 5 | There are several things going on here. 6 | 7 | Systems: 8 | * Channel Store / Wallet 9 | * WebSocket Connection Pull 10 | * Blockchain Network 11 | * Social Media Notifications 12 | 13 | 14 | Server: 15 | * Open Channel Request -> Response with channel parameters. 16 | * Funded Channel Request -> Verify UTXO -> Add UTXO -> Confirm UTXO. 17 | * Payment Data -> Verify Transaction -> Confirm payment received. 18 | * Channel Close Request -> Broadcast Transaction. 19 | * Social Media Request -> Create Payment Request -> Store Payment Request 20 | 21 | OPEN_REQUEST -> STORES_CHANNEL -> EMIT 22 | 23 | 24 | channelRequests.subscribe() -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bitcore = require('bitcore-lib'); 4 | var PrivateKey = bitcore.PrivateKey; 5 | var Address = bitcore.Address; 6 | 7 | bitcore.Networks.defaultNetwork = bitcore.Networks.testnet; 8 | 9 | module.exports.LOCK_UNTIL_BLOCK = 626816; 10 | 11 | module.exports.senderPrivkey = new PrivateKey('3e05cfe5b6feabed18b14816925067a3e414f4aaab3192aecd3b95a9f7f5434e') 12 | module.exports.senderPubkey = module.exports.senderPrivkey.toPublicKey(); 13 | 14 | module.exports.receiverPrivkey = new PrivateKey('e2540c54bedf9b1d455edb4a2aed83c250148194eb44fb965fdba66c5b86ac2a') 15 | module.exports.receiverPubkey = module.exports.receiverPrivkey.toPublicKey(); 16 | 17 | module.exports.refundAddress = new Address('mnhDieWznFmaE32zwowJJz65PgTPr2aVEw'); -------------------------------------------------------------------------------- /scripts/funding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Get Funding Address 4 | 5 | var bitcore = require('bitcore-lib'); 6 | var PrivateKey = bitcore.PrivateKey; 7 | 8 | var bitcoreStream = require('../lib/utils'); 9 | var config = require('./config'); 10 | 11 | var redeemScript = bitcoreStream.buildFundingScript( 12 | config.senderPubkey, 13 | config.receiverPubkey, 14 | config.LOCK_UNTIL_BLOCK 15 | ); 16 | 17 | var script = redeemScript.toScriptHashOut(); 18 | var address = script.toAddress(); 19 | 20 | console.log('Funding Address:', address.toString()); 21 | console.log('Funding Script Hash:', address.hashBuffer.toString('hex')); 22 | console.log('Redeem Script\n', redeemScript.toString()); 23 | 24 | module.exports.address = address; 25 | module.exports.redeemScript = redeemScript; 26 | -------------------------------------------------------------------------------- /scripts/htlc/0-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bitcore = require('bitcore-lib'); 4 | var HDPrivateKey = bitcore.HDPrivateKey; 5 | 6 | bitcore.Networks.defaultNetwork = bitcore.Networks.testnet; 7 | 8 | module.exports.LOCK_TIME_DELTA = 30; // Block 9 | module.exports.SERVER_MIN_BALANCE = 800000; // Satoshis 10 | module.exports.CLIENT_MIN_BALANCE = 2000000; 11 | module.exports.MIN_TX_FEE = 150000; 12 | 13 | module.exports.CLIENT_CHANNEL_BALANCE = 500000; 14 | module.exports.SERVER_CHANNEL_BALANCE = 400000; 15 | 16 | 17 | module.exports.senderPrivkey = new HDPrivateKey('tprv8ZgxMBicQKsPf3TTSUjcoqrRhzpw2RUD3jjp7WhiYiFphZ8Cbf9QketFtG3pXviZ57PAkHZzWqwy6kBhb5VVaMH5FqQFZ8NRJ2i8MdJiJE4') 18 | module.exports.serverPrivkey = new HDPrivateKey('tprv8ZgxMBicQKsPf1eimndX2DGkWF4U16Aeu5tX6nuQ8zVFjC6RW3HES3Le1vXJgEyNyabfpNETMTZ2KxS237fkZ9LYkHKut1ygSSAiBEE87gn') 19 | module.exports.receiverPrivkey = new HDPrivateKey('tprv8ZgxMBicQKsPeyeUtkX5pc6EVGtq5ddbEKMGJ75MMCcV9g8oaKsJCA9bXu1ZnYTEx8Dit7ZesYNdaHjLZv9qdAMGNPvk1JuhqECvHc6cur6') 20 | -------------------------------------------------------------------------------- /scripts/htlc/1-funding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Get some funds 4 | 5 | var bitcore = require('bitcore-lib'); 6 | var Insight = require('bitcore-explorers').Insight; 7 | 8 | var bitcoreStream = require('../../lib/utils'); 9 | var config = require('./0-config'); 10 | 11 | 12 | var fundingAddress = config.senderPrivkey.derive('m/0/0').privateKey.toAddress(); 13 | 14 | 15 | var insight = new Insight(bitcore.Networks.defaultNetwork); 16 | insight.getUnspentUtxos(fundingAddress, function(err, utxos) { 17 | if (err) utxos = []; 18 | 19 | var balance = utxos.reduce((total, x) => total + x.satoshis, 0) 20 | 21 | if (balance < config.CLIENT_MIN_BALANCE) { 22 | var requiredBTC = bitcore.Unit.fromSatoshis(config.CLIENT_MIN_BALANCE - balance).toBTC() + ' BTC'; 23 | console.log('Client #1 needs at least', requiredBTC); 24 | console.log('Please send', requiredBTC, 'to:', fundingAddress.toString()); 25 | } else { 26 | console.log('We have enough money for Client #1 (', bitcore.Unit.fromSatoshis(balance).toBTC(), 'BTC )'); 27 | } 28 | }); 29 | 30 | -------------------------------------------------------------------------------- /scripts/htlc/2-funding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Get some funds 4 | 5 | var bitcore = require('bitcore-lib'); 6 | var Insight = require('bitcore-explorers').Insight; 7 | 8 | var bitcoreStream = require('../../lib/utils'); 9 | var config = require('./0-config'); 10 | 11 | 12 | var fundingAddress = config.serverPrivkey.derive('m/0/0').privateKey.toAddress(); 13 | 14 | 15 | var insight = new Insight(bitcore.Networks.defaultNetwork); 16 | insight.getUnspentUtxos(fundingAddress, function(err, utxos) { 17 | if (err) utxos = []; 18 | 19 | var balance = utxos.reduce((total, x) => total + x.satoshis, 0); 20 | 21 | if (balance < config.SERVER_MIN_BALANCE) { 22 | var requiredBTC = bitcore.Unit.fromSatoshis(config.SERVER_MIN_BALANCE - balance).toBTC() + ' BTC'; 23 | console.log('Server needs at least', requiredBTC); 24 | console.log('Please send', requiredBTC, 'to:', fundingAddress.toString()); 25 | } else { 26 | console.log('We have enough money for the Server (', bitcore.Unit.fromSatoshis(balance).toBTC(), 'BTC )'); 27 | } 28 | }); 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcore-stream", 3 | "version": "1.0.0", 4 | "description": "Payment Channels for Bitcore", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/yemel/bitcore-stream.git" 12 | }, 13 | "keywords": [ 14 | "bitcoin" 15 | ], 16 | "author": "Yemel Jardi", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/yemel/bitcore-stream/issues" 20 | }, 21 | "homepage": "https://github.com/yemel/bitcore-stream#readme", 22 | "dependencies": { 23 | "bitcore-explorers": "^1.0.1", 24 | "bitcore-lib": "^0.13.11", 25 | "bitcore-p2p": "^1.1.0", 26 | "inherits": "^2.0.1", 27 | "lodash": "^3.10.1", 28 | "redux": "^3.0.5", 29 | "redux-rx": "^0.5.0", 30 | "request": "^2.69.0", 31 | "rx": "^4.0.7", 32 | "socket.io": "^1.3.7", 33 | "socket.io-client": "^1.3.7" 34 | }, 35 | "devDependencies": { 36 | "assert": "^1.1.2", 37 | "bitcore-build": "git://github.com/bitpay/bitcore-build.git", 38 | "brfs": "^1.2.0", 39 | "chai": "^1.10.0", 40 | "gulp": "^3.8.10", 41 | "mocha": "^2.2.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bitcore = require('bitcore-lib'); 4 | var SenderChannel = require('./lib/channel').SenderChannel; 5 | var ReceiverChannel = require('./lib/channel').ReceiverChannel; 6 | 7 | 8 | // === CLIENT ==== 9 | var channel = SenderChannel(); 10 | 11 | channel.id; 12 | channel.balance; 13 | channel.expiration; 14 | channel.status; // new, open, closing, closed, expired 15 | channel.fundingAddress; 16 | 17 | channel.fund(utxo); 18 | channel.pay(100, {data: 'some_data'}); 19 | channel.close(); // naming 20 | 21 | channel.on('connected', {}); 22 | channel.on('disconnected', {}); 23 | 24 | channel.on('open', {}); 25 | channel.on('charge', {}); 26 | channel.on('expired', {}); 27 | channel.on('closed', {}); 28 | 29 | 30 | // === SERVER ==== 31 | var channel = ReceiverChannel(); 32 | 33 | channel.id; 34 | channel.balance; 35 | channel.expiration; 36 | channel.status; 37 | channel.fundingAddress; 38 | 39 | channel.charge(100, {data: 'some_data'}); 40 | channel.close(); 41 | 42 | channel.on('connected', {}); 43 | channel.on('disconnected', {}); 44 | 45 | channel.on('open'); 46 | channel.on('payment'); 47 | channel.on('expired'); 48 | channel.on('closing'); 49 | channel.on('closed'); 50 | 51 | 52 | // === Connection === 53 | 54 | var channel = Channel(socket); 55 | channel.connect(socket); 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /lib/channel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bitcore = require('bitcore-lib'); 4 | var Networks = bitcore.Networks; 5 | var PrivateKey = bitcore.PrivateKey; 6 | var Address = bitcore.Address; 7 | 8 | var HOURS_IN_DAY = 24; 9 | var MINUTES_IN_HOUR = 60; 10 | var SECONDS_IN_MINUTE = 60; 11 | 12 | var ONE_DAY = SECONDS_IN_MINUTE * MINUTES_IN_HOUR * HOURS_IN_DAY; 13 | 14 | var STATUS = { 15 | NEW: 'new', 16 | OPEN: 'open', 17 | CLOSING: 'closing', 18 | CLOSED: 'closed', 19 | EXPIRED: 'expired' 20 | } 21 | 22 | function SenderChannel(opts) { 23 | if (!(this instanceof SenderChannel)) { 24 | return new SenderChannel(opts); 25 | } 26 | 27 | opts = opts || {}; 28 | 29 | // Validate Required Args 30 | // Validate Network Compatibility 31 | 32 | this.network = Networks.get(opts.network || 'livenet'); 33 | this.expiration = opts.expiration || Math.round(new Date().getTime() / 1000) + ONE_DAY; 34 | this.privateKey = new PrivateKey(opts.privateKey); 35 | this.fundingAddress = null; 36 | 37 | if (opts.refundAddress) { 38 | this.refundAddress = new Address(opts.refundAddress); 39 | } else { 40 | this.refundAddress = this.privateKey.toAddress(); 41 | } 42 | 43 | this.balance = 0; 44 | this.status = STATUS.NEW; 45 | } 46 | 47 | 48 | SenderChannel.prototype.inspect = function() { 49 | return ''; 50 | }; 51 | 52 | module.exports = SenderChannel; 53 | -------------------------------------------------------------------------------- /verify.js: -------------------------------------------------------------------------------- 1 | var bitcore = require('bitcore-lib') 2 | 3 | var Script = bitcore.Script; 4 | var Interpreter = bitcore.Script.Interpreter; 5 | var Transaction = bitcore.Transaction 6 | 7 | var interpreter = new Interpreter() 8 | 9 | var scriptPubkey = new Script() 10 | .add('OP_HASH160') 11 | .add(new Buffer('9d591f30cbf7ccb6a992e19d6a22e9ee6595925b', 'hex')) 12 | .add('OP_EQUAL') 13 | 14 | var tx = new Transaction('0100000001855dfaa480bdc0e359e52ba1b470b7ef2d0fab6c7b7e2f220b582dbe04360c2601000000be473044022001b1cd154552a6bb153aac12f2f9d8d78d18e027e9cb5259d868b0a5d55853cd02200c29973c19fe13745cf42d532bdd3b10f4721489e6d6a0445d1cef59e7d5b13f01004c73635221033b29afadb7f4ef05659d5a2862af44cbe713490c28ae1bd704f02446490a19fa21021c308b874b69df67ef67795243ee81cbbe7c32e22d92e5127c253504599138ba52ae6703c09009b17521033b29afadb7f4ef05659d5a2862af44cbe713490c28ae1bd704f02446490a19faac680000000001905f0100000000001976a9144eb9ea59ef457ee87b3a73f2dfec47faac36eb7688acc0900900') 15 | 16 | var scriptSig = tx.inputs[0].script 17 | console.log(scriptSig) 18 | 19 | var flags = Interpreter.SCRIPT_VERIFY_P2SH 20 | | Interpreter.SCRIPT_VERIFY_STRICTENC 21 | | Interpreter.SCRIPT_VERIFY_SIGPUSHONLY 22 | | Interpreter.SCRIPT_VERIFY_NULLDUMMY 23 | // | Interpreter.SCRIPT_VERIFY_DERSIG 24 | // | Interpreter.SCRIPT_VERIFY_LOW_S 25 | | Interpreter.SCRIPT_VERIFY_MINIMALDATA 26 | | Interpreter.SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY 27 | 28 | var verified = interpreter.verify(scriptSig, scriptPubkey, tx, 0, flags); 29 | 30 | console.log(verified, interpreter) 31 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var bitcore = require('bitcore-lib'); 5 | 6 | var BN = bitcore.crypto.BN; 7 | var PublicKey = bitcore.PublicKey; 8 | var Transaction = bitcore.Transaction; 9 | var Script = bitcore.Script; 10 | var $ = bitcore.util.preconditions; 11 | 12 | /** 13 | * Create an output script to fund a payment channel. 14 | * 15 | * @param {PublicKey} sender's public key 16 | * @param {PublicKey} receiver's public key 17 | * @param {Number} height 18 | * @return {Script} a an output script requiring both sender and receiver public keys, 19 | * or just the sender's one after a given block height 20 | */ 21 | function buildFundingScript(senderPubkey, receiverPubkey, height) { 22 | // Validate public keys 23 | $.checkArgumentType(senderPubkey, PublicKey); 24 | $.checkArgumentType(receiverPubkey, PublicKey); 25 | 26 | // Validate block height 27 | $.checkArgument(_.isNumber(height)); 28 | // $.checkArgument(height < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT); 29 | 30 | return Script.empty() 31 | .add('OP_IF') 32 | .add('OP_2') 33 | .add(senderPubkey.toBuffer()) 34 | .add(receiverPubkey.toBuffer()) 35 | .add('OP_2') 36 | .add('OP_CHECKMULTISIG') 37 | .add('OP_ELSE') 38 | .add(BN.fromNumber(height).toScriptNumBuffer()) 39 | .add('OP_CHECKLOCKTIMEVERIFY').add('OP_DROP') 40 | .add(senderPubkey.toBuffer()) 41 | .add('OP_CHECKSIG') 42 | .add('OP_ENDIF'); 43 | } 44 | 45 | function buildFundingAddress(senderPubkey, receiverPubkey, height) { 46 | // TODO: Apply network 47 | var redeemScript = buildFundingScript(senderPubkey, receiverPubkey, height); 48 | return redeemScript.toScriptHashOut().toAddress(senderPubkey.network); 49 | } 50 | 51 | function buildSpendingTx(utxo, sender, receiver, amount) { 52 | return new Transaction().from(utxo).to(receiver, amount).change(sender).fee(); 53 | } 54 | 55 | module.exports.buildFundingScript = buildFundingScript; 56 | module.exports.buildFundingAddress = buildFundingAddress; 57 | 58 | -------------------------------------------------------------------------------- /double-spend.js: -------------------------------------------------------------------------------- 1 | var bitcore = require('bitcore-lib'); 2 | var Insight = require('bitcore-explorers').Insight; 3 | 4 | var hdkey = bitcore.HDPrivateKey.fromString('xprv9s21ZrQH143K2f1h6EitwQqsaujnzvVQ8c2nsVt9MmgXS7RWjw68YQdqtW6xUYFTN8QXcHB3obEgsiL1icLRbcVKTkfAafZ2KHs6m3trBWD'); 5 | 6 | 7 | var insight = new Insight(); 8 | 9 | var privateKey = hdkey.derive(1).privateKey; 10 | var changeAddress = hdkey.derive(100).privateKey.toAddress(); 11 | 12 | var fakePayment = '1L8bCB3nHLgEEDsZyYdxYRRWFiqpN4VTnp'; 13 | var fakeAmount = 5430000; 14 | 15 | insight.getUnspentUtxos(privateKey.toAddress(), function(err, utxos) { 16 | console.log('Address 1', privateKey.toAddress().toString()); 17 | if (err || utxos.length === 0) return console.log('Address 1', privateKey.toAddress().toString(), 'No funding yet!'); 18 | 19 | var tx = bitcore.Transaction().from(utxos).to(fakePayment, 1000).change(changeAddress).addData('xD').fee(0); 20 | tx.sign(privateKey); 21 | 22 | console.log('Payment Tx 1', tx.isFullySigned()); 23 | console.log(tx.serialize(true)); 24 | 25 | var tx2 = bitcore.Transaction().from(utxos).change(changeAddress).fee(20000); 26 | tx.sign(privateKey); 27 | 28 | console.log('Payment Tx 2', tx2.isFullySigned()); 29 | console.log(tx2.serialize(true)); 30 | 31 | }); 32 | 33 | 34 | // console.log('Address 2', hdkey.derive(2).privateKey.toAddress().toString()); 35 | // console.log('Address 3', hdkey.derive(3).privateKey.toAddress().toString()); 36 | // console.log('Address 4', hdkey.derive(4).privateKey.toAddress().toString()); 37 | // console.log('Address 5', hdkey.derive(5).privateKey.toAddress().toString()); 38 | // console.log('Address 6', hdkey.derive(6).privateKey.toAddress().toString()); 39 | // console.log('Address 7', hdkey.derive(7).privateKey.toAddress().toString()); 40 | // console.log('Address 8', hdkey.derive(8).privateKey.toAddress().toString()); 41 | // console.log('Address 9', hdkey.derive(9).privateKey.toAddress().toString()); 42 | // console.log('Address 10', hdkey.derive(10).privateKey.toAddress().toString()); 43 | 44 | -------------------------------------------------------------------------------- /scripts/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var bitcore = require('bitcore-lib'); 5 | var bitcoreStream = require('../lib/utils'); 6 | 7 | var app = require('express')(); 8 | var http = require('http').Server(app); 9 | var io = require('socket.io')(http); 10 | 11 | var state = {}; 12 | 13 | io.on('connection', function(socket) { 14 | console.log('a user connected', socket.id); 15 | 16 | 17 | // Event: HELLO 18 | // Description: Request for creating a payment channel 19 | // Result: Emit OPEN with our pubkey and funding address 20 | socket.on('hello', function(data) { 21 | console.log('hello', data); 22 | 23 | state.params = {}; 24 | state.params.network = bitcore.Networks.get(data.network); 25 | state.params.expiration = data.expiration; 26 | 27 | state.keys = {}; 28 | state.keys.senderPubkey = new bitcore.PublicKey(data.senderPubkey, {network: state.params.network}); 29 | state.keys.receiverPrivkey = new bitcore.PrivateKey('e2540c54bedf9b1d455edb4a2aed83c250148194eb44fb965fdba66c5b86ac2a', state.params.network); 30 | 31 | state.fundingAddress = bitcoreStream.buildFundingAddress( 32 | state.keys.senderPubkey, 33 | state.keys.receiverPrivkey.publicKey, 34 | state.params.expiration 35 | ) 36 | 37 | socket.emit('open', { 38 | receiverPubkey: state.keys.receiverPrivkey.publicKey.toString(), 39 | fundingAddress: state.fundingAddress.toString() 40 | }); 41 | 42 | }); 43 | 44 | // Event: FUNDED 45 | // Description: A new UTXO has been added 46 | // Result: Validate them, waits for confirmation and emits confirmed list. 47 | socket.on('funded', function(data) { 48 | console.log('funded', data); 49 | 50 | // TODO: Validate and confirm UTXO 51 | 52 | state.utxos = {}; 53 | state.utxos.pending = []; 54 | state.utxos.confirmed = data.utxos.map(function(utxo) { 55 | return bitcore.Transaction.UnspentOutput.fromObject(utxo); 56 | }); 57 | 58 | socket.emit('confirmed', { 59 | utxos: state.utxos.confirmed.map(function(utxo) { return utxo.toJSON(); }) 60 | }); 61 | 62 | }); 63 | 64 | }); 65 | 66 | http.listen(3000, function() { 67 | console.log('listening on *:3000'); 68 | }); 69 | 70 | -------------------------------------------------------------------------------- /scripts/spend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bitcore = require('bitcore-lib'); 4 | var Insight = require('bitcore-explorers').Insight; 5 | 6 | var config = require('./config'); 7 | var funding = require('./funding'); 8 | 9 | var Address = bitcore.Address; 10 | var Transaction = bitcore.Transaction; 11 | var Signature = bitcore.crypto.Signature; 12 | var Script = bitcore.Script; 13 | 14 | 15 | // Spending Bitcoin Test 16 | 17 | var insight = new Insight(); 18 | 19 | insight.getUnspentUtxos(funding.address, function(err, utxos) { 20 | if (err || utxos.length == 0) throw new Error('No UTXOs available for ' + funding.address); 21 | 22 | var utxo = utxos[0]; 23 | console.log('Using UTXO:', utxo); 24 | 25 | // Cooperative Transaction 26 | 27 | var coopTransaction = new Transaction().from({ 28 | txid: utxo.txId, 29 | vout: utxo.outputIndex, 30 | scriptPubKey: utxo.script, 31 | satoshis: utxo.satoshis, 32 | }) 33 | .to(config.refundAddress, utxo.satoshis - 10000); // 100000 34 | 35 | var senderSig = Transaction.sighash.sign(coopTransaction, config.senderPrivkey, Signature.SIGHASH_ALL, 0, funding.redeemScript); 36 | var receiverSig = Transaction.sighash.sign(coopTransaction, config.receiverPrivkey, Signature.SIGHASH_ALL, 0, funding.redeemScript); 37 | 38 | coopTransaction.inputs[0].setScript( 39 | Script.empty() 40 | .add('OP_0') 41 | .add(senderSig.toTxFormat()) 42 | .add(receiverSig.toTxFormat()) 43 | .add('OP_TRUE') // choose the multisig path 44 | .add(funding.redeemScript.toBuffer()) 45 | ); 46 | 47 | console.log('\n\nCooperative Transaction'); 48 | console.log(coopTransaction.serialize(true)); 49 | 50 | 51 | // Timeout Transaction 52 | 53 | var timeTransaction = new Transaction().from({ 54 | txid: utxo.txId, 55 | vout: utxo.outputIndex, 56 | scriptPubKey: utxo.script, 57 | satoshis: utxo.satoshis, 58 | }) 59 | .to(config.refundAddress, utxo.satoshis - 10000) // 100000 60 | .lockUntilBlockHeight(config.LOCK_UNTIL_BLOCK); 61 | 62 | var signature = Transaction.sighash.sign(timeTransaction, config.senderPrivkey, Signature.SIGHASH_ALL, 0, funding.redeemScript); 63 | 64 | timeTransaction.inputs[0].setScript( 65 | Script.empty() 66 | .add(signature.toTxFormat()) 67 | .add('OP_FALSE') 68 | .add(funding.redeemScript.toBuffer()) 69 | ); 70 | 71 | console.log('\n\nTimeout Transaction'); 72 | console.log(timeTransaction.id); 73 | console.log(timeTransaction.serialize(true)); 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /scripts/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var bitcore = require('bitcore-lib'); 5 | var bitcoreStream = require('../lib/utils'); 6 | var Insight = require('bitcore-explorers').Insight; 7 | 8 | var socket = require('socket.io-client')('http://localhost:3000'); 9 | 10 | var state = {}; 11 | var UTXO = [bitcore.Transaction.UnspentOutput.fromObject({ 12 | address: '2MxuSvjesue263L2sdQoqQckhDcvBvjibou', 13 | txid: 'db21a8ab8790f8282b3614bed501ee881ba368b773ee939dd7277c6bb29d76c6', 14 | vout: 0, 15 | scriptPubKey: 'a9143e133c7b88fdc464766a7e0bd48d4969047d9ca087', 16 | amount: 0.003 17 | })] 18 | 19 | 20 | 21 | socket.on('connect', function(){ 22 | console.log('I am connected, sending hello'); 23 | 24 | state = {}; 25 | 26 | state.params = {}; 27 | state.params.network = bitcore.Networks.get('testnet'); 28 | state.params.expiration = 1450881924; // ONE_DAY 29 | 30 | state.keys = {}; 31 | state.keys.senderPrivkey = new bitcore.PrivateKey('3e05cfe5b6feabed18b14816925067a3e414f4aaab3192aecd3b95a9f7f5434e', state.params.network); 32 | 33 | socket.emit('hello', { 34 | senderPubkey: state.keys.senderPrivkey.publicKey.toString(), 35 | expiration: state.params.expiration, 36 | network: state.params.network.name 37 | }); 38 | }); 39 | 40 | socket.on('open', function(data){ 41 | console.log('Open', data); 42 | 43 | state.keys.receiverPubkey = new bitcore.PublicKey(data.receiverPubkey, {network: state.params.network}); 44 | state.fundingAddress = new bitcore.Address(data.fundingAddress); 45 | 46 | // TODO: Validate Funding Address is OK 47 | console.log('Waiting for funds on', state.fundingAddress.toString()); 48 | 49 | // var insight = new Insight(state.params.network); 50 | // insight.getUnspentUtxos(state.fundingAddress, function(err, utxos) { 51 | // if (err || utxos.length == 0) { 52 | // throw new Error('No UTXOs available for ' + state.fundingAddress); 53 | // } 54 | var utxos = UTXO; 55 | 56 | state.utxos = {}; 57 | state.utxos.pending = utxos; 58 | state.utxos.confirmed = []; 59 | 60 | console.log('The channel has been funded'); 61 | 62 | socket.emit('funded', { 63 | utxos: state.utxos.pending.map(function(utxo) { return utxo.toJSON(); }) 64 | }); 65 | // }); 66 | }); 67 | 68 | socket.on('confirmed', function(data) { 69 | console.log('Confirmed', data); 70 | 71 | state.utxos.pending = []; 72 | state.utxos.confirmed = data.utxos.map(function(utxo) { 73 | return bitcore.Transaction.UnspentOutput.fromObject(utxo); 74 | }); 75 | 76 | var balance = state.utxos.confirmed.reduce(function(total, utxo){ return total + utxo.satoshis; }, 0); 77 | console.log('Balance confirmed', balance); 78 | 79 | // Init first payment 80 | 81 | }); 82 | 83 | socket.on('disconnect', function(){ 84 | console.log('Disconnect!'); 85 | }); 86 | 87 | 88 | // FUND -> UTXO 89 | // UTXO-ACK 90 | 91 | // UTXO 92 | // { address: '2MxuSvjesue263L2sdQoqQckhDcvBvjibou', 93 | // txid: 'db21a8ab8790f8282b3614bed501ee881ba368b773ee939dd7277c6bb29d76c6', 94 | // vout: 0, 95 | // scriptPubKey: 'a9143e133c7b88fdc464766a7e0bd48d4969047d9ca087', 96 | // amount: 0.003 } 97 | 98 | 99 | -------------------------------------------------------------------------------- /scripts/rxserver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Rx = require('rx') 4 | 5 | var _ = require('lodash') 6 | var bitcore = require('bitcore-lib') 7 | var bitcoreStream = require('../lib/utils') 8 | 9 | 10 | var connectionStream = Rx.Observable.create(observer => { 11 | var app = require('express')() 12 | var http = require('http').Server(app) 13 | var io = require('socket.io')(http) 14 | 15 | io.on('connection', socket => observer.onNext(socket)) 16 | http.listen(3000, () => console.log('listening on *:3000')) 17 | 18 | return () => http.close() 19 | }) 20 | 21 | // var messageStream = connectionStream.flatMap() 22 | 23 | function getMessageStream(socket, event) { 24 | return Rx.Observable.create(observer => { 25 | socket.on(event, data => observer.onNext({ 26 | type: event, 27 | data: data, 28 | })) 29 | }) 30 | } 31 | 32 | function logMessage(message) { 33 | console.log('===', message.type, '===') 34 | console.log(message.data) 35 | console.log('\n') 36 | } 37 | 38 | 39 | connectionStream.subscribe(socket => { 40 | console.log('a user connected', socket.id) 41 | 42 | var state = {} 43 | 44 | // Supported Messages 45 | var helloStream = getMessageStream(socket, 'hello') 46 | var fundingStream = getMessageStream(socket, 'funded') 47 | var paymentStream = getMessageStream(socket, 'payment') 48 | var closeRequestStream = getMessageStream(socket, 'close') 49 | var disconnectStream = getMessageStream(socket, 'disconnect') 50 | 51 | // helloStream.subscribe(logMessage) 52 | // helloStream 53 | // .map(Messages.decrypt) 54 | // .map(Messages.logMessage) 55 | // .map(Messages.Hello.fromObject) 56 | // .zip(Channel.pull.getState) 57 | 58 | helloStream.tap(logMessage).subscribe(message => { 59 | // Validate Message Hello 60 | 61 | // Marshaling 62 | var network = bitcore.Networks.get(message.data.network) 63 | var expiration = message.data.expiration 64 | var senderPubkey = new bitcore.PublicKey(message.data.senderPubkey, {network: network}) 65 | 66 | // State Handling 67 | state.network = network 68 | state.expiration = expiration; 69 | state.senderPubkey = senderPubkey 70 | state.receiverPrivkey = new bitcore.PrivateKey('e2540c54bedf9b1d455edb4a2aed83c250148194eb44fb965fdba66c5b86ac2a', state.network) 71 | state.fundingAddress = bitcoreStream.buildFundingAddress(state.senderPubkey, state.receiverPrivkey.publicKey, state.expiration) 72 | 73 | // Emmit Response --> TODO: Make it an exit stream 74 | socket.emit('open', { 75 | receiverPubkey: state.receiverPrivkey.publicKey.toString(), 76 | fundingAddress: state.fundingAddress.toString() 77 | }) 78 | }) 79 | 80 | 81 | fundingStream.tap(logMessage).subscribe(message => { 82 | 83 | // Marshaling 84 | var utxos = message.data.utxos.map(utxo => { 85 | return bitcore.Transaction.UnspentOutput.fromObject(utxo) 86 | }) 87 | 88 | state.utxos = {} 89 | state.utxos.pending = [] 90 | state.utxos.confirmed = [] 91 | 92 | socket.emit('confirmed', { 93 | utxos: state.utxos.confirmed.map(function(utxo) { return utxo.toJSON(); }) 94 | }); 95 | }) 96 | 97 | disconnectStream.subscribe(logMessage) 98 | 99 | }); 100 | 101 | 102 | /* 103 | Playing with RxJX 104 | 105 | InBound 106 | > HELLO, FUND, PAY, CLOSE 107 | 108 | OutBound 109 | > OPEN, CONFIRM, PAY-REQ, CLOSE 110 | 111 | 112 | Pending Concerns: 113 | * Encryption 114 | * Package Validations // Serialization 115 | * Return Errors 116 | 117 | Message: 118 | {TYPE: HELLO, DATA: {}, SIGNATURE: ''} 119 | 120 | 121 | */ 122 | -------------------------------------------------------------------------------- /lib/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var bitcore = require('bitcore-lib'); 4 | var $ = bitcore.util.preconditions; 5 | 6 | 7 | /* Message Flow Example 8 | 9 | // PROTOCOL: WEBSOCKETS 10 | 11 | // CLIENT :: Request to open a channel 12 | { 13 | command: 'open_channel_request', 14 | payload: { 15 | network: 'livenet', 16 | public_key: '03bb3c511621f39cc07d6e2e901067225071a8d0e540c22d4528d85e86473e9227', 17 | } 18 | } 19 | 20 | // SERVER :: Responses with channel parameters 21 | { 22 | command: 'open_channel', 23 | payload: { 24 | relative_locktime: 391626, 25 | public_key: '02f31a3b7952499d38351e853ad4c90eb721351b7e3f034a4a80dcc80d9dae6de1', 26 | final_address: '1GoiTY9WHcZL8UHD6yLLCSjCBYAscHkCiA', 27 | min_depth: 3, 28 | close_fee: 2000 29 | } 30 | } 31 | 32 | // CLIENT :: The Channel has been funded 33 | { 34 | command: 'open_anchor', 35 | payload: { 36 | txid: 'fc070b01262df4af111bb1d2e49faa5877426eecd20dac7df20858edd7ef9a0f', 37 | output_index: 2, 38 | amount: 40000, 39 | commitment_sig: '3045022100b1a954d3acedf4f461c4336180cff6b3aa945c9175e4948d47663f8474c49c7802202800182de810f6a52ff39eb9eb5afd3867a372c4192740ac7d7d788d2796e2ac' 40 | } 41 | } 42 | 43 | // SERVER :: Confirm the channel is open 44 | { 45 | command: 'open_complete', 46 | payload: { 47 | block_id: '000000000000000005045d776f86ff5976dd24808e19ed71cb9166c0a7e4a9bd' 48 | } 49 | } 50 | 51 | // SERVER :: Payment Request 52 | { 53 | command: 'payment_request', 54 | payload: { 55 | id: '12312321', 56 | amount: 200, 57 | amount_display: '$3 USD', 58 | moniker: 'beer', 59 | destination: '@bitmapped', 60 | network: 'twitter', 61 | url: 'https://twitter.com/JVWVU1/status/680158684987658240', 62 | } 63 | } 64 | 65 | // CLIENT :: Update Balance 66 | { 67 | command: 'send_payment', 68 | payload: { 69 | id: '12312321', 70 | refund_address: '1PjppcWq3pZNMW6YxSVLvcVHJMXaVNj3SK', 71 | signature: '3045022100b1a954d3acedf4f461c4336180cff6b3aa945c9175e4948d47663f8474c49c7802202800182de810f6a52ff39eb9eb5afd3867a372c4192740ac7d7d788d2796e2ac' 72 | } 73 | } 74 | 75 | // CLIENT :: Confirm payment 76 | { 77 | command: 'send_payment_ack', 78 | payload: { 79 | id: '1231232132', 80 | } 81 | } 82 | 83 | // SERVER :: Try to open a new channel 84 | { 85 | command: 'send_payment_ack', 86 | payload: { 87 | id: '1231232132', 88 | } 89 | } 90 | 91 | 92 | 93 | // UI FLOW 94 | 95 | * Write down your mnemonic 96 | * Add balance (USD/BTC) - QR 97 | * 98 | 99 | 100 | 101 | 102 | /* List of commands 103 | * Hello 104 | * Ping 105 | */ 106 | 107 | 108 | 109 | 110 | /* Parameters of Lighting Network 111 | message open_channel 112 | locktime_value 113 | locktime_type // block or seconds 114 | commit_key // to send the funds 115 | final_key // to 116 | min_depth // anchor buried to consider channel live 117 | commitment_fee // 118 | 119 | message open_anchor 120 | txid = 1; 121 | output_index = 2; 122 | amount = 3; 123 | commit_sig = 4; 124 | 125 | message open_complete 126 | block_id 127 | 128 | message update 129 | delta_msat = 2; 130 | signature 131 | 132 | message close_channel 133 | 134 | message close_channel_complete 135 | 136 | message error 137 | problem 138 | 139 | 140 | 141 | * funding = how much we will charge? 142 | 143 | */ 144 | 145 | 146 | 147 | function Message(options) { 148 | this.command = options.command; 149 | this.network = options.network; 150 | } 151 | 152 | 153 | Message.prototype.toBuffer = Message.prototype.serialize = function() { 154 | $.checkState(this.network, 'Need to have a defined network to serialize message'); 155 | var commandBuf = new Buffer(Array(12)); 156 | commandBuf.write(this.command, 'ascii'); 157 | 158 | var payload = this.getPayload(); 159 | var checksum = Hash.sha256sha256(payload).slice(0, 4); 160 | 161 | var bw = new BufferWriter(); 162 | bw.write(this.network.networkMagic); 163 | bw.write(commandBuf); 164 | bw.writeUInt32LE(payload.length); 165 | bw.write(checksum); 166 | bw.write(payload); 167 | 168 | return bw.concat(); 169 | }; 170 | -------------------------------------------------------------------------------- /scripts/htlc/3-open.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Get some funds 4 | var request = require('request'); 5 | 6 | var bitcore = require('bitcore-lib'); 7 | var Insight = require('bitcore-explorers').Insight; 8 | var Signature = bitcore.crypto.Signature; 9 | 10 | var bitcoreStream = require('../../lib/utils'); 11 | var config = require('./0-config'); 12 | 13 | 14 | var clientFundingAddress = config.senderPrivkey.derive('m/0/0').privateKey.toAddress(); 15 | var serverFundingAddress = config.serverPrivkey.derive('m/0/0').privateKey.toAddress(); 16 | 17 | var insight = new Insight(bitcore.Networks.defaultNetwork); 18 | insight.getUnspentUtxos(clientFundingAddress, function(err, clientUtxos) { 19 | if (err || clientUtxos.length == 0) throw Error('No funds for client'); 20 | 21 | insight.getUnspentUtxos(serverFundingAddress, function(err, serverUtxos) { 22 | if (err || serverUtxos.length == 0) throw Error('No funds for server'); 23 | 24 | var urls = 'https://api.blockcypher.com/v1/btc/' + (bitcore.Networks.defaultNetwork.name == 'testnet' ? 'test3' : 'main'); 25 | request.get({url: 'https://api.blockcypher.com/v1/btc/test3', json: true}, function(e, b, data) { 26 | openChannel(clientUtxos, serverUtxos, data.height); 27 | }); 28 | }); 29 | }); 30 | 31 | function openChannel(clientUtxos, serverUtxos, blockHeight) { 32 | // Signing Key 33 | var clientPrivkey = config.senderPrivkey.derive('m/0/0').privateKey; 34 | 35 | // Change Outputs 36 | var clientChange = config.senderPrivkey.derive('m/0/1').privateKey.toAddress(); 37 | var serverChange = config.serverPrivkey.derive('m/0/1').privateKey.toAddress(); 38 | 39 | var clientBalance = clientUtxos.reduce((total, x) => total + x.satoshis, 0); 40 | var serverBalance = serverUtxos.reduce((total, x) => total + x.satoshis, 0); 41 | 42 | var clientChangeAmount = clientBalance - config.CLIENT_CHANNEL_BALANCE - config.MIN_TX_FEE; 43 | var serverChangeAmount = serverBalance - config.SERVER_CHANNEL_BALANCE; 44 | 45 | // Channel Outputs 46 | var clientPubkey = config.senderPrivkey.derive('m/1/0').privateKey.publicKey; 47 | var serverPubkey = config.serverPrivkey.derive('m/1/0').privateKey.publicKey; 48 | 49 | var clientChannelAddress = channelScript(clientPubkey, serverPubkey, blockHeight).toScriptHashOut().toAddress(); 50 | var serverChannelAddress = channelScript(serverPubkey, clientPubkey, blockHeight).toScriptHashOut().toAddress(); 51 | 52 | // This Tx is generated on client side, serialized and sent to the server. 53 | var openTx = new bitcore.Transaction() 54 | .from(clientUtxos) 55 | .to(clientChange, clientChangeAmount) 56 | .to(serverChange, serverChangeAmount) 57 | .to(clientChannelAddress, config.CLIENT_CHANNEL_BALANCE) 58 | .to(serverChannelAddress, config.SERVER_CHANNEL_BALANCE) 59 | .sign(clientPrivkey, Signature.SIGHASH_ALL | Signature.SIGHASH_ANYONECANPAY) 60 | 61 | 62 | // The server completes the transaction 63 | var serverPrivkey = config.serverPrivkey.derive('m/0/0').privateKey; 64 | openTx 65 | .from(serverUtxos) 66 | .sign(serverPrivkey) 67 | 68 | broadcastTx(openTx); 69 | } 70 | 71 | function channelScript(pubkey1, pubkey2, height) { 72 | var sortedPubkeys = [pubkey1, pubkey2].sort(function(k1, k2) { 73 | return k1.toString() > k2.toString() ? 1 : -1; 74 | }); 75 | 76 | return bitcore.Script.empty() 77 | .add('OP_IF') 78 | .add('OP_2') 79 | .add(sortedPubkeys[0].toBuffer()) 80 | .add(sortedPubkeys[1].toBuffer()) 81 | .add('OP_2') 82 | .add('OP_CHECKMULTISIG') 83 | .add('OP_ELSE') 84 | .add(bitcore.crypto.BN.fromNumber(height).toScriptNumBuffer()) 85 | .add('OP_CHECKLOCKTIMEVERIFY').add('OP_DROP') 86 | .add(pubkey1.toBuffer()) 87 | .add('OP_CHECKSIG') 88 | .add('OP_ENDIF'); 89 | } 90 | 91 | function broadcastTx(transaction) { 92 | request.post({ 93 | url: 'https://api.blockcypher.com/v1/btc/test3/txs/push', 94 | body: {tx: transaction.serialize()}, 95 | json: true 96 | }, function(err, resp, data) { 97 | if (err) { 98 | console.log('Error broadcasting tx:', resp); 99 | } else { 100 | var url = 'https://live.blockcypher.com/btc-testnet/tx/' + data.tx.hash; 101 | console.log('Transaction has been broadcasted', data.tx.hash); 102 | } 103 | }); 104 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcore Stream 2 | 3 | Bidirectional payment channels for Bitcore. 4 | 5 | 6 | # Sender API 7 | 8 | ```javascript 9 | var channel = SenderChannel(); 10 | 11 | channel.id; 12 | channel.balance; 13 | channel.expiration; 14 | channel.status; // new, open, closing, closed, expired 15 | channel.fundingAddress; 16 | 17 | channel.fund(utxo); 18 | channel.pay(100, {data: 'some_data'}); 19 | channel.close(); // naming 20 | 21 | channel.on('connected', {}); 22 | channel.on('disconnected', {}); 23 | 24 | channel.on('open', {}); 25 | channel.on('charge', {}); 26 | channel.on('expired', {}); 27 | channel.on('closed', {}); 28 | ``` 29 | 30 | 31 | # Sender API 32 | 33 | ```javascript 34 | var channel = ReceiverChannel(); 35 | 36 | channel.id; 37 | channel.balance; 38 | channel.expiration; 39 | channel.status; 40 | channel.fundingAddress; 41 | 42 | channel.charge(100, {data: 'some_data'}); 43 | channel.close(); 44 | 45 | channel.on('connected', {}); 46 | channel.on('disconnected', {}); 47 | 48 | channel.on('open'); 49 | channel.on('payment'); 50 | channel.on('expired'); 51 | channel.on('closing'); 52 | channel.on('closed'); 53 | ``` 54 | 55 | 56 | # Payment Channel 57 | 58 | ## Payment Script 59 | ``` 60 | OP_IF 61 | 2 2 OP_CHECKMULTISIG 62 | OP_ELSE 63 |