diff --git a/.gitignore b/.gitignore index 23ee9c3..0748bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ venv database.sqlite3 database.sqlite3* +.pyre* diff --git a/LNbits/__init__.py b/LNbits/__init__.py index fcb0e5f..64f52e1 100644 --- a/LNbits/__init__.py +++ b/LNbits/__init__.py @@ -7,17 +7,12 @@ from flask import Flask, jsonify, render_template, request, redirect, url_for from . import bolt11 from .db import Database -from .settings import DATABASE_PATH, LNBITS_PATH, WALLET, DEFAULT_USER_WALLET_NAME +from .helpers import megajson +from .settings import LNBITS_PATH, WALLET, DEFAULT_USER_WALLET_NAME app = Flask(__name__) - - -def db_connect(db_path=DATABASE_PATH): - import sqlite3 - - con = sqlite3.connect(db_path) - return con +app.jinja_env.filters["megajson"] = megajson @app.before_first_request @@ -50,7 +45,7 @@ def deletewallet(): (thewal, theid), ) - next_wallet = db.fetchone("SELECT hash FROM wallets WHERE user = ?", (theid,)) + next_wallet = db.fetchone("SELECT id FROM wallets WHERE user = ?", (theid,)) if next_wallet: return redirect(url_for("wallet", usr=theid, wal=next_wallet[0])) @@ -164,16 +159,14 @@ def wallet(): (SELECT balance/1000 FROM balances WHERE wallet = wallets.id), 0 ) AS balance, - name, - adminkey, - inkey + * FROM wallets WHERE user = ? AND id = ? """, (usr, wallet_id), ) - transactions = [] + transactions = db.fetchall("SELECT * FROM apipayments WHERE wallet = ?", (wallet_id,)) return render_template( "wallet.html", user_wallets=user_wallets, wallet=wallet, user=usr, transactions=transactions, diff --git a/LNbits/data/schema.sql b/LNbits/data/schema.sql index 7b9f3a4..af1eca9 100644 --- a/LNbits/data/schema.sql +++ b/LNbits/data/schema.sql @@ -18,7 +18,8 @@ CREATE TABLE IF NOT EXISTS apipayments ( fee integer NOT NULL DEFAULT 0, wallet text NOT NULL, pending boolean NOT NULL, - memo text + memo text, + time timestamp NOT NULL DEFAULT (strftime('%s', 'now')) ); CREATE VIEW IF NOT EXISTS balances AS diff --git a/LNbits/helpers.py b/LNbits/helpers.py index e69de29..30e1925 100644 --- a/LNbits/helpers.py +++ b/LNbits/helpers.py @@ -0,0 +1,16 @@ +import json +import sqlite3 + + +class MegaEncoder(json.JSONEncoder): + def default(self, o): + if type(o) == sqlite3.Row: + val = {} + for k in o.keys(): + val[k] = o[k] + return val + return o + + +def megajson(o): + return json.dumps(o, cls=MegaEncoder) diff --git a/LNbits/static/app.js b/LNbits/static/app.js new file mode 100644 index 0000000..3d12314 --- /dev/null +++ b/LNbits/static/app.js @@ -0,0 +1,349 @@ +/** @format */ + +const user = window.user +const user_wallets = window.user_wallets +const wallet = window.wallet +const transactions = window.transactions + +var thehash = '' +var theinvoice = '' +var outamount = '' +var outmemo = '' + +// API CALLS + +function postAjax(url, data, thekey, success) { + var params = + typeof data == 'string' + ? data + : Object.keys(data) + .map(function(k) { + return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) + }) + .join('&') + var xhr = window.XMLHttpRequest + ? new XMLHttpRequest() + : new ActiveXObject('Microsoft.XMLHTTP') + xhr.open('POST', url) + xhr.onreadystatechange = function() { + if (xhr.readyState > 3 && xhr.status == 200) { + success(xhr.responseText) + } + } + xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.send(params) + return xhr +} + +function getAjax(url, thekey, success) { + var xhr = window.XMLHttpRequest + ? new XMLHttpRequest() + : new ActiveXObject('Microsoft.XMLHTTP') + xhr.open('GET', url, true) + xhr.onreadystatechange = function() { + if (xhr.readyState > 3 && xhr.status == 200) { + success(xhr.responseText) + } + } + xhr.setRequestHeader('Grpc-Metadata-macaroon', thekey) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.send() + return xhr +} + +function sendfundsinput() { + document.getElementById('sendfunds').innerHTML = + "

" + + "
" + + "


' + document.getElementById('receive').innerHTML = '' +} + +function sendfundspaste() { + invoice = document.getElementById('pasteinvoice').value + theinvoice = decode(invoice) + outmemo = theinvoice.data.tags[1].value + outamount = Number(theinvoice.human_readable_part.amount) / 1000 + if (outamount > Number(wallet.balance)) { + document.getElementById('sendfunds').innerHTML = + "
" + + "

Not enough funds!

" + + "" + + '

' + } else { + document.getElementById('sendfunds').innerHTML = + "
" + + '

Invoice details
Amount: ' + + outamount + + '
Memo: ' + + outmemo + + '

' + + "

" + + invoice + + '

' + + "" + + "" + + '

' + } +} + +function receive() { + document.getElementById('receive').innerHTML = + "
" + + "
" + + "
" + + "
" + + '

' + document.getElementById('sendfunds').innerHTML = '' +} + +function received() { + memo = document.getElementById('memo').value + amount = document.getElementById('amount').value + postAjax( + '/v1/invoices', + JSON.stringify({value: amount, memo: memo}), + wallet.inkey, + function(data) { + theinvoice = JSON.parse(data).pay_req + thehash = JSON.parse(data).payment_hash + document.getElementById('QRCODE').innerHTML = + "
" + + "
" + + "

" + + theinvoice + + '

' + + new QRCode(document.getElementById('qrcode'), { + text: theinvoice, + width: 300, + height: 300, + colorDark: '#000000', + colorLight: '#ffffff', + correctLevel: QRCode.CorrectLevel.M + }) + getAjax('/v1/invoice/' + thehash, wallet.inkey, function(datab) { + console.log(JSON.parse(datab).PAID) + if (JSON.parse(datab).PAID == 'TRUE') { + window.location.href = 'wallet?wal=' + wallet.id + '&usr=' + user + } + }) + } + ) +} + +function cancelsend() { + window.location.href = 'wallet?wal=' + wallet.id + '&usr=' + user +} + +function sendfunds(invoice) { + var url = '/v1/channels/transactions' + postAjax( + url, + JSON.stringify({payment_request: invoice}), + wallet.adminkey, + function(data) { + thehash = JSON.parse(data).payment_hash + console.log(JSON.parse(data)) + if (JSON.parse(data).PAID == 'TRUE') { + window.location.href = 'wallet?wal=' + wallet.id + '&usr=' + user + } + } + ) +} + +function scanQRsend() { + document.getElementById('sendfunds').innerHTML = + "

" + + "
🎥 Unable to access video stream (please make sure you have a webcam enabled)
" + + "


" + var video = document.createElement('video') + var canvasElement = document.getElementById('canvas') + var canvas = canvasElement.getContext('2d') + var loadingMessage = document.getElementById('loadingMessage') + var outputContainer = document.getElementById('output') + var outputMessage = document.getElementById('outputMessage') + var outputData = document.getElementById('outputData') + function drawLine(begin, end, color) { + canvas.beginPath() + canvas.moveTo(begin.x, begin.y) + canvas.lineTo(end.x, end.y) + canvas.lineWidth = 4 + canvas.strokeStyle = color + canvas.stroke() + } + // Use facingMode: environment to attemt to get the front camera on phones + navigator.mediaDevices + .getUserMedia({video: {facingMode: 'environment'}}) + .then(function(stream) { + video.srcObject = stream + video.setAttribute('playsinline', true) // required to tell iOS safari we don't want fullscreen + video.play() + requestAnimationFrame(tick) + }) + function tick() { + loadingMessage.innerText = '⌛ Loading video...' + if (video.readyState === video.HAVE_ENOUGH_DATA) { + loadingMessage.hidden = true + canvasElement.hidden = false + outputContainer.hidden = false + canvasElement.height = video.videoHeight + canvasElement.width = video.videoWidth + canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height) + var imageData = canvas.getImageData( + 0, + 0, + canvasElement.width, + canvasElement.height + ) + var code = jsQR(imageData.data, imageData.width, imageData.height, { + inversionAttempts: 'dontInvert' + }) + if (code) { + drawLine( + code.location.topLeftCorner, + code.location.topRightCorner, + '#FF3B58' + ) + drawLine( + code.location.topRightCorner, + code.location.bottomRightCorner, + '#FF3B58' + ) + drawLine( + code.location.bottomRightCorner, + code.location.bottomLeftCorner, + '#FF3B58' + ) + drawLine( + code.location.bottomLeftCorner, + code.location.topLeftCorner, + '#FF3B58' + ) + outputMessage.hidden = true + outputData.parentElement.hidden = false + outputData.innerText = JSON.stringify(code.data) + theinvoice = decode(code.data) + outmemo = theinvoice.data.tags[1].value + outamount = Number(theinvoice.human_readable_part.amount) / 1000 + if (outamount > Number(wallet.balance)) { + document.getElementById('sendfunds').innerHTML = + "
" + + "

Not enough funds!

" + + "" + + '

' + } else { + document.getElementById('sendfunds').innerHTML = + "
" + + '

Invoice details
Amount: ' + + outamount + + '
Memo: ' + + outmemo + + '

' + + "

" + + JSON.stringify(code.data) + + '

' + + "" + + "" + + '

' + } + } else { + outputMessage.hidden = false + outputData.parentElement.hidden = true + } + } + requestAnimationFrame(tick) + } +} + +function deletewallet() { + var url = 'deletewallet?wal=' + wallet.id + '&usr=' + user + window.location.href = url +} + +function sidebarmake() { + document.getElementById('sidebarmake').innerHTML = + "
  • " + + "" + + "" + + '


  • ' +} + +function newwallet() { + walname = document.getElementById('walname').value + window.location.href = 'wallet?usr=' + user + '&nme=' + walname +} + +function drawChart(transactions) { + var linechart = [] + var transactionsHTML = '' + var balance = 0 + + for (var i = 0; i < transactions.length; i++) { + var tx = transactions[i] + var datime = convertTimestamp(tx.time) + + // make the transactions table + transactionsHTML += + "" + + tx.memo + + '' + + datime + + '' + + parseFloat(tx.amount / 1000) + + '' + + // make the line chart + balance += parseInt(tx.amount / 1000) + linechart.push({y: datime, balance: balance}) + } + + document.getElementById('transactions').innerHTML = transactionsHTML + + if (linechart[0] != '') { + document.getElementById('satschart').innerHTML = + "
    " + + "

    Spending

    " + + "
    " + } + + console.log(linechart) + var line = new Morris.Line({ + element: 'line-chart', + resize: true, + data: linechart, + xkey: 'y', + ykeys: ['balance'], + labels: ['balance'], + lineColors: ['#3c8dbc'], + hideHover: 'auto' + }) +} + +function convertTimestamp(timestamp) { + var d = new Date(timestamp * 1000), + yyyy = d.getFullYear(), + mm = ('0' + (d.getMonth() + 1)).slice(-2), + dd = ('0' + d.getDate()).slice(-2), + hh = d.getHours(), + h = hh, + min = ('0' + d.getMinutes()).slice(-2), + ampm = 'AM', + time + time = yyyy + '-' + mm + '-' + dd + ' ' + h + ':' + min + return time +} + +if (transactions.length) { + drawChart(transactions) +} diff --git a/LNbits/templates/base.html b/LNbits/templates/base.html index be4d940..93c3089 100644 --- a/LNbits/templates/base.html +++ b/LNbits/templates/base.html @@ -296,4 +296,9 @@ > + + diff --git a/LNbits/templates/index.html b/LNbits/templates/index.html index bc3b4af..844f8f2 100644 --- a/LNbits/templates/index.html +++ b/LNbits/templates/index.html @@ -89,23 +89,4 @@ - - {% endblock %} diff --git a/LNbits/templates/wallet.html b/LNbits/templates/wallet.html index dc46c41..18112e8 100644 --- a/LNbits/templates/wallet.html +++ b/LNbits/templates/wallet.html @@ -63,545 +63,171 @@
    - -
    -
    -

    {{ wallet.balance }} sats

    -

    {{ wallet.name }}

    -
    -
    - -
    +
    +
    +

    {{ wallet.balance }} sats

    +

    {{ wallet.name }}

    +
    +
    + +
    +
    + + + +
    +
    +
    - +
    + +
    +
    +
    - > +
    +
    +
    -
    -
    - -
    -
    - -
    +
    +
    +
    +
    +

    Transactions

    + +
    + + + + + + + +
    Memodateamount
    +
    + +
    + +
    +
    -
    -
    +
    -
    -
    -
    -
    -

    Transactions

    +
    +
    +
    +
    + +
    +
    + +
    + - -
    - - - - - - - -
    Memodateamount
    + -
    - -
    -
    - -
    - -
    - -
    - + + + +
    {% endblock %}