├── .dockerignore ├── .gitignore ├── Dockerfile.watcher ├── Dockerfile.webapp ├── README.md ├── build.sh ├── docker-compose.yml ├── watcher ├── .gitignore ├── README.md ├── channels.js ├── config.js ├── litrpc.js ├── package.json ├── payments.js ├── server.js └── util.js └── webapp ├── .bowerrc ├── .gitignore ├── README.md ├── app ├── controllers │ ├── channels.js │ ├── payments.js │ ├── root.js │ ├── users.js │ └── www.js ├── helpers │ └── password.js ├── middleware │ ├── auth.js │ └── authUser.js └── models │ ├── channel.js │ ├── payment.js │ ├── transaction.js │ └── user.js ├── bower.json ├── config └── config.js ├── package.json ├── public ├── css │ └── style.css ├── js │ ├── app.js │ ├── appRoutes.js │ ├── controllers │ │ ├── MainCtrl.js │ │ └── UserCtrl.js │ └── services │ │ ├── AuthService.js │ │ ├── CoinService.js │ │ └── UserService.js └── views │ ├── channels.html │ ├── home.html │ ├── index.html │ ├── login.html │ ├── pay.html │ ├── payments.html │ ├── transactions.html │ └── user.html ├── server.js └── webapp.workspace /.dockerignore: -------------------------------------------------------------------------------- 1 | /data/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /data/ 2 | -------------------------------------------------------------------------------- /Dockerfile.watcher: -------------------------------------------------------------------------------- 1 | FROM node:slim 2 | ENV NODE_ENV dev 3 | RUN mkdir -p /var/app 4 | COPY . /var/app 5 | RUN apt-get update && apt-get install -y git 6 | RUN cd /var/app/watcher && npm update && \ 7 | npm install --silent 8 | RUN cd /var/app/webapp && npm update && \ 9 | npm install --silent 10 | WORKDIR /var/app/watcher 11 | ENV NODE_ENV=production 12 | CMD ["node", "server.js"] 13 | -------------------------------------------------------------------------------- /Dockerfile.webapp: -------------------------------------------------------------------------------- 1 | FROM node:slim 2 | ENV NODE_ENV dev 3 | RUN mkdir -p /var/app 4 | COPY . /var/app 5 | RUN apt-get update && apt-get install -y git python build-essential 6 | RUN npm install -g bower 7 | RUN echo '{ "allow_root": true }' > /root/.bowerrc 8 | RUN cd /var/app/watcher && npm update && \ 9 | npm install --silent 10 | RUN cd /var/app/webapp && npm update && \ 11 | npm install --silent && \ 12 | bower install && \ 13 | npm rebuild bcrypt --build-from-source 14 | WORKDIR /var/app/webapp 15 | ENV NODE_ENV=development 16 | ENV DEBUG=express:* 17 | CMD ["node", "server.js"] 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LitPay 2 | ### Like BitPay, but more 'lit' 3 | 4 | This project is a work in progress. 5 | 6 | Designed to provide a [Lit](https://github.com/mit-dci/lit) based payment gateway. Lit is a multicoin LN payment channel client 7 | (working on BTC, LTC and VTC). A user makes a channel with a LitPay gateway ahead of time, and then makes a free and instant `push` 8 | at the time of payment as opposed to an additional on-chain payment. This would be especially useful for Bitcoin whose transaction 9 | fees and confirmation times are prohibitively high for small payments. The effective fee from using this scheme is `(2*txfee)/nTxs`, or in other words, cheaper than an on-chain transaction as long as the channel is used more than twice. 10 | 11 | #### Components (so far) 12 | 13 | - [litpay-webapp](https://github.com/mit-dci/litpay/tree/master/webapp): an angular/express web application for the frontend control of LitPay 14 | - [litpay-watcher](https://github.com/mit-dci/litpay/tree/master/watcher): a node daemon for managing Lit channels with users and updating their states for the frontend 15 | - [lit](https://github.com/mit-dci/lit): MIT LN daemon 16 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -f Dockerfile.watcher . -t litpay-watcher 3 | docker build -f Dockerfile.webapp . -t litpay-webapp 4 | 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | 4 | lit: 5 | image: lit 6 | restart: always 7 | expose: 8 | - "8001" 9 | volumes: 10 | - ./data/lit:/root/.lit 11 | 12 | webapp: 13 | image: litpay-webapp 14 | depends_on: 15 | - lit 16 | - mongodb 17 | restart: always 18 | environment: 19 | - MONGO_HOST=mongodb 20 | - MONGO_DB=litpay 21 | expose: 22 | - "8080" 23 | 24 | watcher: 25 | image: litpay-watcher 26 | depends_on: 27 | - lit 28 | - mongodb 29 | restart: always 30 | environment: 31 | - MONGO_HOST=mongodb 32 | - MONGO_DB=litpay 33 | - LIT_HOST=lit 34 | - LIT_RPCPORT=8001 35 | 36 | mongodb: 37 | image: mongo 38 | expose: 39 | - "27001" 40 | volumes: 41 | - ./data/mongodb:/data/db 42 | 43 | networks: 44 | default: 45 | external: 46 | name: litpay-network 47 | -------------------------------------------------------------------------------- /watcher/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tern* 3 | -------------------------------------------------------------------------------- /watcher/README.md: -------------------------------------------------------------------------------- 1 | # litpay-watcher 2 | 3 | This is the daemon that monitors Lit for new channels and channel state changes 4 | and writes the details of them to the database. 5 | 6 | #### Install 7 | 8 | Assumes you already have Node.js and NPM installed for your system. 9 | 10 | ``` 11 | npm install 12 | ``` 13 | 14 | #### Run 15 | 16 | ``` 17 | npm start 18 | ``` 19 | -------------------------------------------------------------------------------- /watcher/channels.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose').set('debug', true); 2 | var ChannelSchema = require('../webapp/app/models/channel'); 3 | var TxSchema = require('../webapp/app/models/transaction'); 4 | 5 | var Channel = mongoose.model('Channel', new mongoose.Schema(ChannelSchema)); 6 | 7 | var Util = require('./util'); 8 | 9 | function openChannel(pendingChannel, realChannel) { 10 | return new Promise(function(resolve, reject) { 11 | if(realChannel.StateNum != 0) { 12 | // TODO: Handle this more gracefully than just giving up, it's easily recoverable 13 | return console.error("Channel was pushed to before opened"); 14 | } 15 | 16 | pendingChannel.cointype = realChannel.CoinType; 17 | pendingChannel.capacity = realChannel.Capacity; 18 | pendingChannel.balance = realChannel.MyBalance; 19 | pendingChannel.open = !realChannel.Closed; 20 | pendingChannel.funded = true; 21 | pendingChannel.pkh = Util.toHexString(realChannel.Pkh); 22 | 23 | pendingChannel.save(function(err) { 24 | if(err) { 25 | console.error(err); 26 | } else { 27 | console.log("Found new channel"); 28 | } 29 | 30 | resolve(err); 31 | }); 32 | }); 33 | } 34 | 35 | // Checks the database for unfunded channels, and check if they become funded 36 | function updateNewChannels(rpc) { 37 | // Check database for unfunded channels 38 | return new Promise(function(resolve, reject) { 39 | Channel.find({'funded': false}, function(err, pendingChannels) { 40 | if(err) { 41 | return resolve(err); 42 | } 43 | 44 | // First check the channel list 45 | rpc.call('LitRPC.ChannelList', [null]).then(function(realChannels) { 46 | var promises = []; 47 | 48 | for(var idpc in pendingChannels) { 49 | for(var idrc in realChannels.Channels) { 50 | if(Util.arrCmp(realChannels.Channels[idrc].Data, [...Buffer.from(pendingChannels[idpc].fundData, "hex")])) { 51 | promises.push(openChannel(pendingChannels[idpc], realChannels.Channels[idrc])); 52 | break; 53 | } 54 | } 55 | } 56 | 57 | Promise.all(promises).then(function(err) { 58 | return resolve(err); 59 | }); 60 | }); 61 | }); 62 | }); 63 | } 64 | 65 | // Updates the status of already open channels 66 | function updateOpenChannels(rpc) { 67 | return new Promise(function(resolve, reject) { 68 | Channel.find({'funded': true, 'open': true}, function(err, openChannels) { 69 | if(err) { 70 | return resolve(err); 71 | } 72 | 73 | rpc.call('LitRPC.StateDump', [null]).then(function(states) { 74 | rpc.call('LitRPC.ChannelList', [null]).then(function(channels) { 75 | var promises = []; 76 | for(var ido in openChannels) { 77 | var nTxs = openChannels[ido].transactions.length; 78 | var lastBal = openChannels[ido].balance; 79 | 80 | // Add missing transactions from StateDump 81 | for(var ids in states.Txs) { 82 | if(Util.toHexString(states.Txs[ids].Pkh) == openChannels[ido].pkh) { 83 | var newTx = true; 84 | 85 | if(nTxs > 0) { 86 | if(states.Txs[ids].Idx <= openChannels[ido].transactions[nTxs - 1].idx) { 87 | newTx = false; 88 | } 89 | } 90 | 91 | if(newTx) { 92 | // Figure out delta 93 | var delta = states.Txs[ids].Amt - lastBal; 94 | if(delta != 0) { 95 | var tx = new mongoose.Schema(TxSchema); 96 | 97 | tx.delta = delta; 98 | tx.idx = states.Txs[ids].Idx; 99 | tx.pushData = Util.toHexString(states.Txs[ids].Data); 100 | 101 | openChannels[ido].transactions.push(tx); 102 | openChannels[ido].balance = states.Txs[ids].Amt; 103 | 104 | lastBal = states.Txs[ids].Amt; 105 | nTxs++; 106 | } 107 | } 108 | } 109 | } 110 | 111 | // Update latest channel status 112 | for(var idc in channels.Channels) { 113 | if(Util.toHexString(channels.Channels[idc].Pkh) == openChannels[ido].pkh) { 114 | if(channels.Channels[idc].StateNum >= nTxs) { 115 | var delta = channels.Channels[idc].MyBalance - lastBal; 116 | if(delta != 0) { 117 | var tx = new mongoose.Schema(TxSchema); 118 | 119 | tx.delta = delta; 120 | tx.idx = channels.Channels[idc].StateNum; 121 | tx.pushData = Util.toHexString(channels.Channels[idc].Data); 122 | 123 | openChannels[ido].transactions.push(tx); 124 | 125 | openChannels[ido].balance = channels.Channels[idc].MyBalance; 126 | } 127 | } 128 | 129 | openChannels[ido].open = !channels.Channels[idc].Closed; 130 | 131 | break; 132 | } 133 | } 134 | 135 | promises.push(new Promise(function(resolve, reject) { 136 | openChannels[ido].save(function(err) { 137 | if(err) { 138 | console.error("Failed to update channel"); 139 | } 140 | 141 | console.log("Updated channel"); 142 | 143 | resolve(err); 144 | }); 145 | })); 146 | } 147 | 148 | Promise.all(promises).then(function(err) { 149 | resolve(err); 150 | }); 151 | }); 152 | }); 153 | }); 154 | }); 155 | } 156 | 157 | module.exports = { 158 | updateNewChannels: updateNewChannels, 159 | updateOpenChannels: updateOpenChannels 160 | }; -------------------------------------------------------------------------------- /watcher/config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-dci/litpay/1afef9304901e42591b7bd56da43b6ddc4eedc1c/watcher/config.js -------------------------------------------------------------------------------- /watcher/litrpc.js: -------------------------------------------------------------------------------- 1 | var config = require('./config'); 2 | var JsonRPC = require('simple-jsonrpc-js'); 3 | var WebSocket = require('ws'); 4 | 5 | function connect() { 6 | return new Promise(function(resolve, reject){ 7 | var jrpc = new JsonRPC(); 8 | var litHost = process.env.LIT_HOST || '127.0.0.1'; 9 | var litRpcPort = process.env.LIT_RPCPORT || '8001'; 10 | 11 | var socket = new WebSocket("ws://" + litHost + ":" + litRpcPort + "/ws", 12 | { origin: 'http://localhost' }); 13 | 14 | socket.onmessage = function(event) { 15 | jrpc.messageHandler(event.data); 16 | }; 17 | 18 | jrpc.toStream = function(_msg){ 19 | socket.send(_msg); 20 | }; 21 | 22 | socket.onerror = function(error) { 23 | console.error("Error: " + error.message); 24 | }; 25 | 26 | socket.onclose = function(event) { 27 | if (event.wasClean) { 28 | console.info('Connection close was clean'); 29 | } else { 30 | console.error('Connection suddenly close'); 31 | } 32 | console.info('close code : ' + event.code + ' reason: ' + event.reason); 33 | }; 34 | 35 | socket.onopen = function() { 36 | resolve(jrpc); 37 | }; 38 | }); 39 | } 40 | 41 | module.exports = connect; -------------------------------------------------------------------------------- /watcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "litpay-watcher", 3 | "main": "server.js", 4 | "dependencies": { 5 | "mongoose": "latest", 6 | "simple-jsonrpc-js": "latest", 7 | "ws": "latest", 8 | "when": "latest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /watcher/payments.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose').set('debug', true); 2 | var PaymentSchema = require('../webapp/app/models/payment'); 3 | 4 | var when = require('when'); 5 | 6 | var Payment = mongoose.model('Payment', new mongoose.Schema(PaymentSchema)); 7 | var Channel = mongoose.model('Channel'); 8 | 9 | function processPayment(payment) { 10 | return new Promise(function(resolve, reject) { 11 | // Get the channels for the payer that are compatible with this payment 12 | Channel.find({'user': payment.from, 'cointype': payment.cointype, 'open': true}, function(err, channels) { 13 | if(err) { 14 | return resolve(err); 15 | } 16 | 17 | var promises = []; 18 | 19 | for(var chanIdx in channels) { 20 | // Check for any corresponding transactions in the channel 21 | for(var tx in channels[chanIdx].transactions) { 22 | var curTx = channels[chanIdx].transactions[tx]; 23 | 24 | // If the transaction's pushData matches that of the payment 25 | // then the transaction is accounted 26 | if(curTx.pushData == payment.pushData && !curTx.accounted) { 27 | payment.balance -= curTx.delta; 28 | curTx.accounted = true; 29 | } 30 | } 31 | 32 | promises.push(new Promise(function(resolve, reject) { 33 | channels[chanIdx].save(function(err) { 34 | return resolve(err); 35 | }); 36 | })); 37 | } 38 | 39 | // The channels can be saved in parallel as they don't affect each other 40 | Promise.all(promises).then(function(errs) { 41 | for(var err in errs) { 42 | if(errs[err]) { 43 | console.error("Failed to save channels"); 44 | return resolve(errs[err]); 45 | } 46 | } 47 | 48 | // Payment should only be saved after txs have been marked as accounted 49 | // to prevent double spends due to a race condition 50 | payment.save(function(err) { 51 | if(err) { 52 | console.error("Failed to save payment"); 53 | } 54 | return resolve(err); 55 | }); 56 | }); 57 | }); 58 | }); 59 | } 60 | 61 | function matchPayments() { 62 | return new Promise(function(resolve, reject) { 63 | 64 | // Find payments with an outstanding balance and an un-expired timeout 65 | Payment.find({'balance': {$gt: 0}, 'timeout': {$gt: new Date()}}, function(err, payments) { 66 | if(err) { 67 | return resolve(err); 68 | } 69 | 70 | // These promises need to occur in sequence or txs/payments could be double counted 71 | var chain = when(); 72 | 73 | for(var payment in payments) { 74 | chain = chain.then(function() { 75 | return processPayment(payments[payment]); 76 | }); 77 | } 78 | 79 | chain.then(function(err) { 80 | return resolve(err); 81 | }); 82 | }); 83 | }); 84 | } 85 | 86 | module.exports = { 87 | matchPayments: matchPayments 88 | }; -------------------------------------------------------------------------------- /watcher/server.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose').set('debug', true); 2 | var config = require('../webapp/config/config'); 3 | 4 | var Channels = require('./channels'); 5 | var Payments = require('./payments'); 6 | var LitRPC = require('./litrpc'); 7 | 8 | mongoose.Promise = global.Promise; 9 | mongoose.connect(config.database, function(err) { 10 | if(err) { 11 | return console.error(err); 12 | } 13 | 14 | console.log("Connected to mongodb"); 15 | 16 | LitRPC().then(function(rpc) { 17 | console.log("Connected to Lit"); 18 | 19 | setInterval(function() { 20 | Channels.updateNewChannels(rpc).then(function(err) { 21 | console.log("Checked for new channels"); 22 | Channels.updateOpenChannels(rpc).then(function(err) { 23 | console.log("Updated channels"); 24 | Payments.matchPayments().then(function(err) { 25 | console.log("Matched payments"); 26 | }); 27 | }); 28 | })}, 5000); 29 | }); 30 | }); 31 | 32 | 33 | -------------------------------------------------------------------------------- /watcher/util.js: -------------------------------------------------------------------------------- 1 | function arrCmp(arr1, arr2) { 2 | return (arr1.length == arr2.length 3 | && arr1.every(function(u, i) { 4 | return u === arr2[i]; 5 | })); 6 | } 7 | 8 | function toHexString(byteArray) { 9 | return Array.from(byteArray, function(byte) { 10 | return ('0' + (byte & 0xFF).toString(16)).slice(-2); 11 | }).join('') 12 | } 13 | 14 | module.exports = { 15 | arrCmp: arrCmp, 16 | toHexString: toHexString 17 | }; -------------------------------------------------------------------------------- /webapp/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/libs" 3 | } 4 | -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | .tern-port 2 | node_modules 3 | public/libs 4 | npm-debug.log 5 | .codelite 6 | .tern-project 7 | -------------------------------------------------------------------------------- /webapp/README.md: -------------------------------------------------------------------------------- 1 | # litpay-webapp 2 | 3 | This is the frontend for LitPay and the backend API. 4 | 5 | #### API Endpoints (so far) 6 | 7 | - User Management 8 | - POST /api/authenticate - get a new JWT 9 | - POST /api/users - register as a new user 10 | - POST /api/users/[user_id] - set user password 11 | - GET /api/users/[user_id] - get user info 12 | - DELETE /api/users/[user_id] - delete user 13 | 14 | - Channel Management 15 | - GET /api/users/[user_id]/channels - list channels and their info 16 | - GET /api/users/[user_id]/channels/[channel_id] - get info about a specific channel 17 | - POST /api/users/[user_id]/channels - create a new channel open request 18 | 19 | - Payments 20 | - GET /api/users/[user_id]/payments - list of payments associated with this user and their info 21 | - GET /api/users/[user_id]/payments/[payment_id] - get info about a specific payment 22 | - POST /api/users/[user_id]/payments - create a new payment invoice from this user to the given user 23 | 24 | #### Install 25 | 26 | Assumes you already have Node.js, NPM and MongoDB installed for your platform. 27 | 28 | ``` 29 | npm install 30 | npm install -g bower 31 | bower install 32 | ``` 33 | 34 | #### Run 35 | 36 | ``` 37 | npm start 38 | ``` 39 | -------------------------------------------------------------------------------- /webapp/app/controllers/channels.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router({mergeParams: true}); 3 | 4 | var mongoose = require('mongoose'); 5 | var Schema = mongoose.Schema; 6 | 7 | var CryptoJS = require('crypto-js'); 8 | 9 | var auth = require('../middleware/auth'); 10 | var authUser = require('../middleware/authUser'); 11 | 12 | var ChannelSchema = require('../models/channel'); 13 | var Channel = mongoose.model('Channel', new Schema(ChannelSchema)); 14 | 15 | // Dumps all the channels for a given user 16 | router.get('/', auth, authUser, function(req, res) { 17 | Channel.find({'user': req.params.user_id, 'funded': true}, function(err, channels) { 18 | if(err) { 19 | return res.json({success: false, 20 | message: err 21 | }); 22 | } 23 | 24 | return res.json({success: true, 25 | channels: channels 26 | }); 27 | }); 28 | }); 29 | 30 | // Gets info about a specific channel 31 | router.get('/:channel_id', auth, authUser, function(req, res) { 32 | Channel.findOne({'user': req.params.user_id, '_id': req.params.channel_id}, function(err, channel) { 33 | if(err) { 34 | return res.json({success: false, 35 | message: err 36 | }); 37 | } 38 | 39 | return res.json({success: true, 40 | channel: channel 41 | }); 42 | }); 43 | }); 44 | 45 | // Creates a new channel open request 46 | router.post('/', auth, authUser, function(req, res) { 47 | req.getValidationResult().then(function(errors) { 48 | if(!errors.isEmpty()) { 49 | var errs = []; 50 | for(var i in errors.array()) { 51 | errs.push(errors.array()[i]["msg"]); 52 | } 53 | return res.json({success: false, 54 | message: errs.join('/n') 55 | }); 56 | } 57 | 58 | Channel.find({'user': req.params.user_id, 'funded': false}, function(err, channels) { 59 | if(err) { 60 | return res.json({ 61 | success: false, 62 | message: err 63 | }); 64 | } 65 | 66 | if(channels.length == 0) { 67 | var channel = new Channel(); 68 | 69 | channel.user = req.params.user_id; 70 | channel.fundData = CryptoJS.lib.WordArray.random(32); 71 | 72 | channel.save(function(err) { 73 | if(err) { 74 | return res.json({ 75 | success: false, 76 | message: err 77 | }); 78 | } 79 | 80 | return res.json({ 81 | success: true, 82 | fundData: channel.fundData 83 | }); 84 | }); 85 | } else { 86 | return res.json({ 87 | success: true, 88 | fundData: channels[0].fundData 89 | }); 90 | } 91 | }); 92 | }); 93 | }); 94 | 95 | module.exports = router; 96 | -------------------------------------------------------------------------------- /webapp/app/controllers/payments.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router({mergeParams: true}); 3 | 4 | var mongoose = require('mongoose'); 5 | var Schema = mongoose.Schema; 6 | 7 | var CryptoJS = require('crypto-js'); 8 | 9 | var auth = require('../middleware/auth'); 10 | var authUser = require('../middleware/authUser'); 11 | 12 | var PaymentSchema = require('../models/payment'); 13 | var Payment = mongoose.model('Payment', new Schema(PaymentSchema)); 14 | 15 | var User = mongoose.model('User'); 16 | 17 | // Dumps all the payments for a given user 18 | router.get('/', auth, authUser, function(req, res) { 19 | Payment.find({'to': req.params.user_id}, function(err, receivable) { 20 | if(err) { 21 | return res.json({success: false, 22 | message: err 23 | }); 24 | } 25 | 26 | Payment.find({'from': req.params.user_id}, function(err, payable) { 27 | if(err) { 28 | return res.json({success: false, 29 | message: err 30 | }); 31 | } 32 | 33 | return res.json({success: true, 34 | receivable: receivable, 35 | payable: payable 36 | }); 37 | }); 38 | }); 39 | }); 40 | 41 | // Gets info about a specific payment 42 | router.get('/:payment_id', auth, authUser, function(req, res) { 43 | Payment.findOne({ $or: [{'to': req.params.user_id}, {'from': req.params.user_id}], '_id': req.params.payment_id}, function(err, payment) { 44 | if(err) { 45 | return res.json({success: false, 46 | message: err 47 | }); 48 | } 49 | 50 | return res.json({success: true, 51 | payment: payment 52 | }); 53 | }); 54 | }); 55 | 56 | // Creates a new payment request 57 | router.post('/', auth, authUser, function(req, res) { 58 | req.checkBody('to', 'Recipient is required').notEmpty(); 59 | req.checkBody('amount', 'Amount is required').notEmpty(); 60 | req.checkBody('amount', 'Amount must be an integer').isInt({min: 0}); 61 | req.checkBody('cointype', 'Coin type is required').notEmpty(); 62 | req.checkBody('cointype', 'Coin type must be an integer').isInt({min: 0, max: 65536}); 63 | 64 | req.getValidationResult().then(function(errors) { 65 | if(!errors.isEmpty()) { 66 | var errs = []; 67 | for(var i in errors.array()) { 68 | errs.push(errors.array()[i]["msg"]); 69 | } 70 | return res.json({success: false, 71 | message: errs.join('/n') 72 | }); 73 | } 74 | 75 | switch(parseInt(req.body.cointype)) { 76 | case 0: 77 | case 1: 78 | case 28: 79 | case 65536: 80 | break; 81 | default: 82 | return res.json({success: false, 83 | message: "Unsupported coin type" 84 | }); 85 | } 86 | 87 | // Check recipient user exists 88 | User.findById(req.body.to, function(err, user) { 89 | if(err) { 90 | return res.json({success: false, 91 | message: err 92 | }); 93 | } 94 | 95 | var newPayment = new Payment(); 96 | 97 | newPayment.pushData = CryptoJS.lib.WordArray.random(32); 98 | newPayment.from = req.params.user_id; 99 | newPayment.to = req.body.to; 100 | newPayment.cointype = req.body.cointype; 101 | 102 | // Timeout 15 minutes in the future 103 | var now = new Date(); 104 | now.setMinutes(now.getMinutes() + 15); 105 | newPayment.timeout = now; 106 | 107 | newPayment.amount = req.body.amount; 108 | newPayment.balance = req.body.amount; 109 | 110 | newPayment.save(function(err) { 111 | if(err) { 112 | return res.json({success: false, 113 | message: err 114 | }); 115 | } 116 | 117 | return res.json({success: true, 118 | payment: newPayment 119 | }); 120 | }); 121 | }); 122 | }); 123 | }); 124 | 125 | module.exports = router; -------------------------------------------------------------------------------- /webapp/app/controllers/root.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var pw = require('../helpers/password'); 5 | 6 | var mongoose = require('mongoose'); 7 | var User = mongoose.model('User'); 8 | 9 | var jwt = require('jsonwebtoken'); 10 | 11 | router.post('/authenticate', function(req, res) { 12 | req.checkBody('name', 'Username is required').notEmpty(); 13 | req.checkBody('password', 'Password is required').notEmpty(); 14 | 15 | req.getValidationResult().then(function(errors) { 16 | if(!errors.isEmpty()) { 17 | var errs = []; 18 | for(var i in errors.array()) { 19 | errs.push(errors.array()[i]["msg"]); 20 | } 21 | return res.json({success: false, 22 | message: errs.join('/n') 23 | }); 24 | } 25 | 26 | User.findOne({ 27 | name: req.body.name 28 | }, function(err, user) { 29 | if(err) { 30 | throw err; 31 | } 32 | 33 | if(!user) { 34 | res.json({success: false, 35 | message: 'Authentication failed. User not found.' 36 | }); 37 | } else { 38 | pw.comparePassword(req.body.password, 39 | user.password, 40 | function(err, passwordValid) { 41 | if(err) { 42 | return res.json({success: false, 43 | message: err 44 | }); 45 | } 46 | 47 | if(passwordValid) { 48 | var payload = { 49 | id: user.id 50 | }; 51 | var token = jwt.sign(payload, 52 | req.app.get('superSecret'), { 53 | expiresIn: 1440 * 60 54 | }); 55 | 56 | res.json({ 57 | success: true, 58 | token: token 59 | }); 60 | } else { 61 | return res.json({success: false, 62 | message: 'Authentication failed. Wrong password.' 63 | }); 64 | } 65 | }); 66 | } 67 | }); 68 | }); 69 | }); 70 | 71 | module.exports = router; 72 | -------------------------------------------------------------------------------- /webapp/app/controllers/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var mongoose = require('mongoose'); 5 | var Schema = mongoose.Schema; 6 | 7 | var pw = require("../helpers/password"); 8 | 9 | var auth = require('../middleware/auth'); 10 | var authUser = require('../middleware/authUser'); 11 | 12 | var UserSchema = require('../models/user'); 13 | var User = mongoose.model('User', new Schema(UserSchema)); 14 | 15 | var channels = require('./channels'); 16 | var payments = require('./payments'); 17 | 18 | router.use('/:user_id/channels', channels); 19 | router.use('/:user_id/payments', payments); 20 | 21 | router.post('/', function(req, res) { 22 | req.checkBody('password', 'Password is required').notEmpty(); 23 | req.checkBody('password', 'Password must be at least 8 characters') 24 | .len(8, undefined); 25 | req.checkBody('name', 'Username is required').notEmpty(); 26 | 27 | req.getValidationResult().then(function(errors) { 28 | if(!errors.isEmpty()) { 29 | var errs = []; 30 | for(var i in errors.array()) { 31 | errs.push(errors.array()[i]["msg"]); 32 | } 33 | return res.json({success: false, 34 | message: errs.join('/n') 35 | }); 36 | } 37 | 38 | User.findOne({ 39 | name: req.body.name 40 | }, function(err, user) { 41 | if(err) { 42 | throw err; 43 | } 44 | 45 | if(user) { 46 | return res.json({success: false, 47 | message: 'User already exists' 48 | }); 49 | } else { 50 | pw.cryptPassword(req.body.password, function(err, hash){ 51 | if(err) { 52 | return res.json({success: false, 53 | message: err 54 | }); 55 | } 56 | 57 | var user = new User(); 58 | 59 | user.password = hash; 60 | user.name = req.body.name; 61 | 62 | user.save(function(err){ 63 | if(err) { 64 | return res.json({success: false, 65 | message: err 66 | }); 67 | } 68 | 69 | res.json({success: true, 70 | message: 'User created!' 71 | }); 72 | }); 73 | }); 74 | } 75 | }); 76 | }); 77 | }); 78 | 79 | router.put('/:user_id', auth, authUser, function(req, res) { 80 | req.checkBody('password', 'Password is required').notEmpty(); 81 | req.checkBody('password', 'Password must be at least 8 characters') 82 | .len(8, undefined); 83 | 84 | req.getValidationResult().then(function(errors) { 85 | if(!errors.isEmpty()) { 86 | var errs = []; 87 | for(var i in errors.array()) { 88 | errs.push(errors.array()[i]["msg"]); 89 | } 90 | return res.json({success: false, 91 | message: errs 92 | }); 93 | } 94 | 95 | User.findById(req.params.user_id, function(err, user) { 96 | if(err) { 97 | return res.json({success: false, 98 | message: err 99 | }); 100 | } 101 | 102 | pw.cryptPassword(req.body.password, function(err, hash) { 103 | if(err) { 104 | return res.json({success: false, 105 | message: err 106 | }); 107 | } 108 | 109 | user.password = hash; 110 | }); 111 | 112 | user.save(function(err) { 113 | if(err) { 114 | return res.json({success: false, 115 | message: err 116 | }); 117 | } 118 | 119 | res.json({success: true, 120 | message: 'User updated' 121 | }); 122 | }); 123 | }); 124 | }); 125 | }); 126 | 127 | router.get('/:user_id', auth, authUser, function(req, res) { 128 | User.findById(req.params.user_id, function(err, user) { 129 | if(err) { 130 | return res.json({success: false, 131 | message: err 132 | }); 133 | } 134 | 135 | res.json({success: true, 136 | user: user 137 | }); 138 | }); 139 | }); 140 | 141 | router.delete('/:user_id', auth, authUser, function(req, res) { 142 | User.remove({_id: req.params.user_id}, function(err, user) { 143 | if(err) { 144 | return res.send(err); 145 | } 146 | 147 | res.json({success: true, 148 | message: 'Successfully deleted' 149 | }); 150 | }); 151 | }); 152 | 153 | module.exports = router; 154 | -------------------------------------------------------------------------------- /webapp/app/controllers/www.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var path = require('path'); 4 | 5 | router.get('/*', function(req, res) { 6 | res.sendFile(path.resolve('./public/views/index.html')); 7 | }); 8 | 9 | module.exports = router; -------------------------------------------------------------------------------- /webapp/app/helpers/password.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/14015677/node-js-encryption-of-passwords 2 | 3 | var bcrypt = require('bcrypt'); 4 | 5 | exports.cryptPassword = function(password, callback) { 6 | bcrypt.genSalt(10, function(err, salt) { 7 | if(err) { 8 | return callback(err); 9 | } 10 | 11 | bcrypt.hash(password, salt, function(err, hash) { 12 | return callback(err, hash); 13 | }); 14 | }); 15 | }; 16 | 17 | exports.comparePassword = function(plainPass, hashword, callback) { 18 | bcrypt.compare(plainPass, hashword, function(err, isPasswordMatch) { 19 | return err == null ? 20 | callback(null, isPasswordMatch) : 21 | callback(err); 22 | }); 23 | }; -------------------------------------------------------------------------------- /webapp/app/middleware/auth.js: -------------------------------------------------------------------------------- 1 | var jwt = require('jsonwebtoken'); 2 | 3 | module.exports = function(req, res, next) { 4 | // check header or url parameters or post parameters for token 5 | var token = req.body.token || req.params['token'] || req.headers['x-access-token']; 6 | 7 | // decode token 8 | if(token) { 9 | // verifies secret and checks exp 10 | jwt.verify(token, req.app.get('superSecret'), function(err, decoded) { 11 | if (err) { 12 | return res.json({ success: false, message: 'Failed to authenticate token.' }); 13 | } else { 14 | // if everything is good, save to request for use in other routes 15 | req.decoded = decoded; 16 | next(); 17 | } 18 | }); 19 | } else { 20 | // if there is no token 21 | // return an error 22 | return res.status(401).send({ 23 | success: false, 24 | message: 'No token provided.' 25 | }); 26 | } 27 | }; -------------------------------------------------------------------------------- /webapp/app/middleware/authUser.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, next) { 2 | if(req.decoded.id == req.params.user_id) { 3 | next(); 4 | } else { 5 | return res.status(401).send({ 6 | success: false, 7 | message: 'Insufficient permissions' 8 | }); 9 | } 10 | }; -------------------------------------------------------------------------------- /webapp/app/models/channel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | var TransactionSchema = require('./transaction'); 5 | 6 | module.exports = { 7 | pkh: { 8 | type: String 9 | }, 10 | capacity: { 11 | type: Number, 12 | min: 1 13 | }, 14 | balance: { 15 | type: Number, 16 | min: 0 17 | }, 18 | cointype: { 19 | type: Number, 20 | min: 0, 21 | max: 65536 22 | }, 23 | open: { 24 | type: Boolean, 25 | required: true, 26 | default: false 27 | }, 28 | funded: { 29 | type: Boolean, 30 | required: true, 31 | default: false 32 | }, 33 | fundData: { 34 | type: String, 35 | required: true 36 | }, 37 | user: { 38 | type: mongoose.Schema.Types.ObjectId, 39 | ref: 'User', 40 | required: true, 41 | index: true 42 | }, 43 | transactions: [Schema(TransactionSchema)] 44 | }; 45 | -------------------------------------------------------------------------------- /webapp/app/models/payment.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | module.exports = { 4 | pushData: { 5 | type: String, 6 | required: true 7 | }, 8 | amount: { 9 | type: Number, 10 | required: true, 11 | min: 1 12 | }, 13 | cointype: { 14 | type: Number, 15 | required: true, 16 | min: 0, 17 | max: 65536 18 | }, 19 | balance: { 20 | type: Number, 21 | required: true, 22 | min: 0 23 | }, 24 | from: { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: 'User', 27 | required: true, 28 | index: true 29 | }, 30 | to: { 31 | type: mongoose.Schema.Types.ObjectId, 32 | ref: 'User', 33 | required: true, 34 | index: true 35 | }, 36 | timeout: { 37 | type: Date, 38 | required: true 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /webapp/app/models/transaction.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pushData: { 3 | type: String, 4 | required: true 5 | }, 6 | delta: { 7 | type: Number, 8 | required: true 9 | }, 10 | idx: { 11 | type: Number, 12 | required: true, 13 | min: 0 14 | }, 15 | accounted: { 16 | type: Boolean, 17 | required: true, 18 | default: false 19 | } 20 | }; -------------------------------------------------------------------------------- /webapp/app/models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: { 3 | type: String, 4 | required: true, 5 | min: [3, 'Username too short'], 6 | max: 24, 7 | unique: true 8 | }, 9 | password: { 10 | type: String, 11 | required: true 12 | }, 13 | admin: { 14 | type: Boolean, 15 | required: true, 16 | default: false 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /webapp/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "litpay", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "bootstrap": "latest", 6 | "font-awesome": "latest", 7 | "animate.css": "latest", 8 | "angular": "latest", 9 | "angular-ui-router": "latest", 10 | "angular-cookies": "latest", 11 | "crypto-js": "latest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webapp/config/config.js: -------------------------------------------------------------------------------- 1 | var mongoServer = process.env.MONGO_HOST || 'localhost'; 2 | var mongoDatabase = process.env.MONGO_DB || 'litpay'; 3 | var secret = process.env.RANDOM_SECRET || 'MY_RANDOM_SECRET'; 4 | 5 | module.exports = { 6 | database : 'mongodb://' + mongoServer + '/' + mongoDatabase, 7 | secret: 'MY_RANDOM_SECRET' 8 | }; 9 | -------------------------------------------------------------------------------- /webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "litpay-webapp", 3 | "main": "server.js", 4 | "dependencies": { 5 | "express": "latest", 6 | "express-validator": "latest", 7 | "mongoose": "latest", 8 | "body-parser": "latest", 9 | "method-override": "latest", 10 | "morgan": "latest", 11 | "jsonwebtoken": "latest", 12 | "bcrypt": "latest", 13 | "crypto-js": "latest" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webapp/public/css/style.css: -------------------------------------------------------------------------------- 1 | .full-width { 2 | width: 100%; 3 | text-align: left; 4 | } 5 | 6 | .scrolling-table { 7 | overflow: hidden; 8 | word-break: break-word; 9 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 10 | } 11 | 12 | .scrolling-table tbody { 13 | -ms-overflow-style: none; // IE 10+ 14 | overflow: -moz-scrollbars-none; // Firefox 15 | } 16 | .scrolling-table tbody::-webkit-scrollbar { 17 | display: none; // Safari and Chrome 18 | } 19 | 20 | table { 21 | width: 100%; 22 | } 23 | 24 | .scrolling-table thead { 25 | width: 100%; 26 | } 27 | .scrolling-table tbody { 28 | height: 230px; 29 | overflow-y: auto; 30 | width: 100%; 31 | } 32 | 33 | .scrolling-table thead, .scrolling-table tbody, .scrolling-table tr, .scrolling-table td, .scrolling-table th { 34 | display: block; 35 | } 36 | .scrolling-table tbody td, .scrolling-table thead > tr> th { 37 | float: left; 38 | border-bottom-width: 0; 39 | } 40 | 41 | td, th { 42 | overflow: hidden; 43 | height: 3em; 44 | } 45 | 46 | th { 47 | background-color: #222; 48 | color: white; 49 | } 50 | 51 | td, th { 52 | text-align: left; 53 | } 54 | 55 | .scrolling-table tr:nth-child(even) { 56 | background-color: #f2f2f2; 57 | } 58 | 59 | .scrolling-table tr:hover { 60 | background-color: #f5f5f5; 61 | } 62 | 63 | .section { 64 | margin-bottom: 1em; 65 | } -------------------------------------------------------------------------------- /webapp/public/js/app.js: -------------------------------------------------------------------------------- 1 | angular.module('litpay', ['ngCookies', 'appRoutes', 'MainCtrl', 2 | 'UserCtrl', 'UserService', 'AuthService', 'CoinService']); -------------------------------------------------------------------------------- /webapp/public/js/appRoutes.js: -------------------------------------------------------------------------------- 1 | angular.module('appRoutes', ['ui.router']).config( 2 | function($stateProvider, $urlRouterProvider, $locationProvider) { 3 | 4 | $urlRouterProvider.otherwise('/login'); 5 | 6 | $locationProvider.html5Mode({ 7 | enabled: true, 8 | requireBase: false 9 | }); 10 | 11 | $stateProvider 12 | 13 | .state('newpayment', { 14 | url: '/pay', 15 | controller: 'NewPaymentController' 16 | }) 17 | 18 | .state('pay', { 19 | url: '/users/{user_id}/payments/{payment_id}', 20 | templateUrl: 'views/pay.html', 21 | controller: 'PaymentController' 22 | }) 23 | 24 | .state('user', { 25 | url: '/users/{user_id}', 26 | templateUrl: 'views/user.html', 27 | controller: 'UserController' 28 | }) 29 | 30 | .state('login', { 31 | url: '/login', 32 | templateUrl: 'views/login.html', 33 | controller: 'LoginController' 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /webapp/public/js/controllers/MainCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('MainCtrl', []).controller('MainController', 2 | function($scope, auth, $location) { 3 | $scope.auth = auth; 4 | 5 | if(!auth.isAuthed()) { 6 | auth.saveAttemptUrl(); 7 | $location.path('/login'); 8 | } 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /webapp/public/js/controllers/UserCtrl.js: -------------------------------------------------------------------------------- 1 | angular.module('UserCtrl', []).controller('UserController', 2 | function($scope, User, 3 | $transition$, $location, 4 | $interval, Coin) { 5 | 6 | $scope.channels = []; 7 | $scope.transactions = []; 8 | $scope.payment = {}; 9 | 10 | $scope.coinTypeToName = Coin.coinTypeToName; 11 | 12 | $scope.updateChannels = function() { 13 | User.getChannels($transition$.params().user_id).then(function(res) { 14 | if(res.data.success) { 15 | $scope.channels = res.data.channels; 16 | } else { 17 | $scope.message = res.data.message; 18 | } 19 | }); 20 | }; 21 | 22 | $scope.updateChannels(); 23 | 24 | $scope.newChannel = function() { 25 | User.newChannel($transition$.params().user_id).then(function(res) { 26 | if(res.data.success) { 27 | $scope.fundData = res.data.fundData; 28 | } else { 29 | $scope.message = res.data.message; 30 | } 31 | }); 32 | }; 33 | 34 | $scope.newChannel(); 35 | 36 | $scope.updateTransactions = function() { 37 | var txs = []; 38 | for(var channel in $scope.channels) { 39 | for(var tx in $scope.channels[channel].transactions) { 40 | $scope.channels[channel].transactions[tx].pkh = $scope.channels[channel].pkh; 41 | $scope.channels[channel].transactions[tx].cointype = $scope.channels[channel].cointype; 42 | txs.push($scope.channels[channel].transactions[tx]); 43 | } 44 | } 45 | 46 | $scope.transactions = txs; 47 | }; 48 | 49 | $scope.updateTransactions(); 50 | 51 | $scope.updatePayments = function() { 52 | User.getPayments($transition$.params().user_id).then(function(res) { 53 | if(res.data.success) { 54 | $scope.payable = res.data.payable; 55 | $scope.receivable = res.data.receivable; 56 | for(var i in $scope.payable) { 57 | if(Date.parse($scope.payable[i].timeout) < Date.now()) { 58 | $scope.payable[i].timeout = "Expired"; 59 | } 60 | } 61 | 62 | for(var i in $scope.receivable) { 63 | if(Date.parse($scope.receivable[i].timeout) < Date.now()) { 64 | $scope.receivable[i].timeout = "Expired"; 65 | } 66 | } 67 | } else { 68 | $scope.message = res.data.message; 69 | } 70 | }); 71 | }; 72 | 73 | $scope.updatePayments(); 74 | 75 | $scope.newPayment = function() { 76 | User.newPayment($transition$.params().user_id, $scope.payment).then(function(res) { 77 | return $location.path("/users/" + $transition$.params().user_id + "/payments/" + res.data.payment._id); 78 | }); 79 | }; 80 | 81 | $scope.$on("$destroy", function() { 82 | if(angular.isDefined($scope.updateTimer)) { 83 | $interval.cancel($scope.updateTimer); 84 | } 85 | }); 86 | 87 | $scope.updateTimer = $interval(function(){ 88 | $scope.newChannel(); 89 | $scope.updateChannels(); 90 | $scope.updateTransactions(); 91 | $scope.updatePayments(); 92 | }, 5000); 93 | }) 94 | 95 | .controller('LoginController', function($scope, $http, API, 96 | User, $location, auth, $cookies) { 97 | if(auth.isAuthed()) { 98 | $location.path("/users/" + auth.getToken().id); 99 | } 100 | 101 | $scope.login = function() { 102 | $scope.message = "Logging in..."; 103 | var callback = function(res) { 104 | $scope.message = res.data.message; 105 | if(res.data.success) { 106 | var expiry = new Date(); 107 | expiry.setDate(expiry.getDate() + (1.0/48)); 108 | auth.redirectToAttemptedUrl(); 109 | } 110 | }; 111 | 112 | return $http.post(API + '/authenticate', { 113 | name: $scope.username, 114 | password: CryptoJS.SHA3($scope.password).toString() 115 | }).then(callback, callback); 116 | }; 117 | 118 | $scope.register = function() { 119 | $scope.message = "Registering..."; 120 | var callback = function(res) { 121 | $scope.message = res.data.message; 122 | if(res.data.success) { 123 | $scope.login(); 124 | } 125 | }; 126 | 127 | return User.create({ 128 | name: $scope.username, 129 | password: CryptoJS.SHA3($scope.password).toString() 130 | }).then(callback, callback); 131 | }; 132 | }) 133 | 134 | .controller('PaymentController', function($scope, User, $location, $transition$, $interval, Coin) { 135 | $scope.payment = {}; 136 | $scope.timeout = ""; 137 | $scope.paid = false; 138 | 139 | $scope.updatePayment = function() { 140 | User.getPayment($transition$.params().user_id, $transition$.params().payment_id).then(function(res) { 141 | if(!res.data.success) { 142 | return $location.path("/"); 143 | } 144 | 145 | $scope.payment = res.data.payment; 146 | 147 | if($scope.payment.balance <= 0) { 148 | $scope.payment.balance = "Paid"; 149 | $scope.paid = true; 150 | } 151 | }); 152 | }; 153 | 154 | $scope.updateTimeout = function() { 155 | var distance = Date.parse($scope.payment.timeout) - Date.now(); 156 | var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); 157 | var seconds = Math.floor((distance % (1000 * 60)) / 1000); 158 | 159 | if(distance > 0) { 160 | $scope.timeout = minutes + "m " + seconds + "s"; 161 | } else { 162 | $scope.timeout = "Expired"; 163 | } 164 | }; 165 | 166 | $scope.updatePayment(); 167 | 168 | $scope.$on("$destroy", function() { 169 | if(angular.isDefined($scope.updateTimer)) { 170 | $interval.cancel($scope.updateTimer); 171 | } 172 | 173 | if(angular.isDefined($scope.timeoutTimer)) { 174 | $interval.cancel($scope.timeoutTimer); 175 | } 176 | }); 177 | 178 | $scope.coinTypeToName = Coin.coinTypeToName; 179 | 180 | $scope.updateTimer = $interval($scope.updatePayment, 5000); 181 | $scope.timeoutTimer = $interval($scope.updateTimeout, 500); 182 | }) 183 | 184 | .controller('NewPaymentController', function(User, $location, auth) { 185 | var payment = { 186 | cointype: $location.search().cointype, 187 | to: $location.search().to, 188 | amount: $location.search().amount 189 | }; 190 | 191 | User.newPayment(auth.getToken().id, payment).then(function(res) { 192 | if(res.data.success) { 193 | return $location.path("/users/" + auth.getToken().id + "/payments/" + res.data.payment._id); 194 | } else { 195 | return $location.path("/users/" + auth.getToken().id); 196 | } 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /webapp/public/js/services/AuthService.js: -------------------------------------------------------------------------------- 1 | angular.module('AuthService', []) 2 | 3 | .factory('authInterceptor', function(API, auth, $q, $location) { 4 | return { 5 | // automatically attach Authorization header 6 | request: function(config) { 7 | var token = auth.getRawToken(); 8 | if(config.url.indexOf(API) === 0 && token) { 9 | config.headers['x-access-token'] = token; 10 | } 11 | return config; 12 | }, 13 | 14 | response: function(res) { 15 | if(res.config.url.indexOf(API) === 0 && res.data.token) { 16 | auth.saveToken(res.data.token); 17 | } 18 | 19 | return res; 20 | }, 21 | 22 | responseError: function(res) { 23 | if(res.status == 401) { 24 | if(auth.isAuthed()) { 25 | // Disallowed resource 26 | $location.path('/'); 27 | } else { 28 | // Token expired/logged out 29 | auth.saveAttemptUrl(); 30 | $location.path('/login'); 31 | } 32 | return; 33 | } 34 | 35 | return $q.reject(res); 36 | } 37 | } 38 | }) 39 | 40 | .service('auth', function($window, $location, $cookies) { 41 | this.parseJwt = function(token) { 42 | var base64Url = token.split('.')[1]; 43 | var base64 = base64Url.replace('-', '+').replace('_', '/'); 44 | return JSON.parse($window.atob(base64)); 45 | }; 46 | 47 | this.saveToken = function(token) { 48 | var expiry = new Date(); 49 | expiry.setDate(expiry.getDate() + (1.0/48)); 50 | $cookies.put('jwtToken', token, {expires: expiry}); 51 | }; 52 | 53 | this.logout = function() { 54 | $cookies.remove('jwtToken'); 55 | $location.path("/"); 56 | }; 57 | 58 | this.getRawToken = function() { 59 | return $cookies.get('jwtToken'); 60 | }; 61 | 62 | this.getToken = function() { 63 | return this.parseJwt(this.getRawToken()); 64 | }; 65 | 66 | this.isAuthed = function() { 67 | var token = this.getRawToken(); 68 | if(token) { 69 | var params = this.parseJwt(token); 70 | return Math.round(new Date().getTime() / 1000) <= params.exp; 71 | } else { 72 | return false; 73 | } 74 | }; 75 | 76 | this.saveAttemptUrl = function() { 77 | this.url = $location.path(); 78 | }; 79 | 80 | this.redirectToAttemptedUrl = function() { 81 | if(!angular.isDefined(this.url) || this.url == "/login" || this.url == "/") { 82 | return $location.path("/users/" + this.getToken().id); 83 | } 84 | 85 | return $location.path(this.url); 86 | }; 87 | }) 88 | 89 | .config(function($httpProvider) { 90 | $httpProvider.interceptors.push('authInterceptor'); 91 | }) 92 | 93 | .constant('API', '/api'); 94 | 95 | -------------------------------------------------------------------------------- /webapp/public/js/services/CoinService.js: -------------------------------------------------------------------------------- 1 | angular.module('CoinService', []) 2 | 3 | .factory('Coin', function() { 4 | return { 5 | coinTypeToName: function(cointype) { 6 | switch(cointype) { 7 | case 0: 8 | return {name: "Bitcoin", 9 | ticker: "BTC" 10 | }; 11 | case 1: 12 | return {name: "Bitcoin Testnet", 13 | ticker: "BTCTEST" 14 | }; 15 | case 28: 16 | return {name: "Vertcoin", 17 | ticker: "VTC" 18 | }; 19 | case 65536: 20 | return {name: "Vertcoin Testnet", 21 | ticker: "VTCTEST" 22 | }; 23 | default: 24 | return {name: "Unknown", 25 | ticker: "UNKNOWN" 26 | }; 27 | 28 | } 29 | } 30 | } 31 | }); -------------------------------------------------------------------------------- /webapp/public/js/services/UserService.js: -------------------------------------------------------------------------------- 1 | angular.module('UserService', []) 2 | 3 | .factory('User', function($http) { 4 | return { 5 | get: function(id) { 6 | return $http.get('/api/users/' + id); 7 | }, 8 | 9 | create: function(userData) { 10 | return $http.post('/api/users', userData); 11 | }, 12 | 13 | getChannels: function(id) { 14 | return $http.get('/api/users/' + id + '/channels'); 15 | }, 16 | 17 | newChannel: function(id) { 18 | return $http.post('/api/users/' + id + '/channels'); 19 | }, 20 | 21 | getPayments: function(id) { 22 | return $http.get('/api/users/' + id + '/payments'); 23 | }, 24 | 25 | newPayment: function(id, payment) { 26 | return $http.post('/api/users/' + id + '/payments', payment); 27 | }, 28 | 29 | getPayment: function(id, payment) { 30 | return $http.get('/api/users/' + id + '/payments/' + payment); 31 | } 32 | } 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /webapp/public/views/channels.html: -------------------------------------------------------------------------------- 1 |
3 | New channel ID: {{ fundData }} 4 |
5 |Coin | 10 |Gateway balance | 11 |Capacity | 12 |Open | 13 |PKH | 14 |# txs | 15 |Fee multiplier per tx | 16 |
---|---|---|---|---|---|---|
{{ coinTypeToName(channel.cointype).name }} | 21 |{{ (channel.balance / 100000000) + " " + coinTypeToName(channel.cointype).ticker }} | 22 |{{ (channel.capacity / 100000000) + " " + coinTypeToName(channel.cointype).ticker }} | 23 |{{ channel.open }} | 24 |{{ channel.pkh }} | 25 |{{ channel.transactions.length }} | 26 |{{ (channel.transactions.length === 0) ? "inf" : 2 / channel.transactions.length }} | 27 |
{{ tagline }}
5 |
3 | Payment ID: {{ payment.pushData }}
4 | Recipient: {{ payment.to }}
5 | Timeout: {{ timeout }}
6 | Balance: {{ (payment.balance / 100000000) + " " + coinTypeToName(payment.cointype).ticker }}
7 |
4 | 5 | 7 | 9 | 10 |
11 |Payer | 17 |Push Data | 18 |Amount | 19 |Balance | 20 |Coin | 21 |Timeout | 22 |
---|---|---|---|---|---|
{{ payment.from }} | 27 |{{ payment.pushData }} | 28 |{{ (payment.amount / 100000000) + " " + coinTypeToName(payment.cointype).ticker }} | 29 |{{ (payment.balance <= 0) ? "Paid" : (payment.balance / 100000000) + " " + coinTypeToName(payment.cointype).ticker }} | 30 |{{ coinTypeToName(payment.cointype).name }} | 31 |{{ payment.timeout }} | 32 |
Recipient | 42 |Push Data | 43 |Amount | 44 |Balance | 45 |Coin | 46 |Timeout | 47 |
---|---|---|---|---|---|
{{ payment.from }} | 52 |{{ payment.pushData }} | 53 |{{ (payment.amount / 100000000) + " " + coinTypeToName(payment.cointype).ticker }} | 54 |{{ (payment.balance <= 0) ? "Paid" : (payment.balance / 100000000) + " " + coinTypeToName(payment.cointype).ticker }} | 55 |{{ coinTypeToName(payment.cointype).name }} | 56 |{{ payment.timeout }} | 57 |
Channel PKH | 7 |Push Data | 8 |Delta | 9 |Coin | 10 |
---|---|---|---|
{{ tx.pkh }} | 15 |{{ tx.pushData }} | 16 |{{ (-tx.delta / 100000000) + " " + coinTypeToName(tx.cointype).ticker }} | 17 |{{ coinTypeToName(tx.cointype).name }} | 18 |
{{ message }}
3 |