SatsPayServer works apart from updating charges

This commit is contained in:
Ben Arc 2021-04-07 22:51:33 +01:00
parent 90319bfd8c
commit 2d8f85f77c
8 changed files with 151 additions and 117 deletions

View File

@ -1,5 +1,5 @@
{ {
"name": "SatsPay Server", "name": "SatsPayServer",
"short_description": "Create onchain and LN charges", "short_description": "Create onchain and LN charges",
"icon": "payment", "icon": "payment",
"contributors": [ "contributors": [

View File

@ -9,17 +9,19 @@ from lnbits.helpers import urlsafe_short_hash
from quart import jsonify from quart import jsonify
import httpx import httpx
from lnbits.core.services import create_invoice, check_invoice_status from lnbits.core.services import create_invoice, check_invoice_status
from ..watchonly.crud import get_watch_wallet, get_derive_address, get_mempool from ..watchonly.crud import get_watch_wallet, get_derive_address, get_mempool, update_watch_wallet
###############CHARGES########################## ###############CHARGES##########################
async def create_charge(user: str, description: Optional[str] = None, onchainwallet: Optional[str] = None, lnbitswallet: Optional[str] = None, webhook: Optional[str] = None, time: Optional[int] = None, amount: Optional[int] = None) -> Charges: async def create_charge(user: str, description: str = None, onchainwallet: Optional[str] = None, lnbitswallet: Optional[str] = None, webhook: Optional[str] = None, completelink: Optional[str] = None, completelinktext: Optional[str] = None, time: Optional[int] = None, amount: Optional[int] = None) -> Charges:
charge_id = urlsafe_short_hash() charge_id = urlsafe_short_hash()
if onchainwallet: if onchainwallet:
wallet = await get_watch_wallet(onchainwallet) wallet = await get_watch_wallet(onchainwallet)
onchainaddress = await get_derive_address(onchainwallet, wallet[4] + 1) onchainaddress = await get_derive_address(onchainwallet, int(wallet[4]) + 1)
await update_watch_wallet(wallet_id=onchainwallet, address_no=int(wallet[4]) + 1)
print(onchainaddress)
else: else:
onchainaddress = None onchainaddress = None
if lnbitswallet: if lnbitswallet:
@ -42,14 +44,16 @@ async def create_charge(user: str, description: Optional[str] = None, onchainwal
payment_request, payment_request,
payment_hash, payment_hash,
webhook, webhook,
completelink,
completelinktext,
time, time,
amount, amount,
balance balance
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(charge_id, user, description, onchainwallet, onchainaddress, lnbitswallet, (charge_id, user, description, onchainwallet, onchainaddress, lnbitswallet,
payment_request, payment_hash, webhook, time, amount, 0), payment_request, payment_hash, webhook, completelink, completelinktext, time, amount, 0),
) )
return await get_charge(charge_id) return await get_charge(charge_id)
@ -77,7 +81,6 @@ async def delete_charge(charge_id: str) -> None:
async def check_address_balance(charge_id: str) -> List[Charges]: async def check_address_balance(charge_id: str) -> List[Charges]:
charge = await get_charge(charge_id) charge = await get_charge(charge_id)
print(charge.balance)
if not charge.paid: if not charge.paid:
if charge.onchainaddress: if charge.onchainaddress:
mempool = await get_mempool(charge.user) mempool = await get_mempool(charge.user)
@ -85,16 +88,13 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.get(mempool.endpoint + "/api/address/" + charge.onchainaddress) r = await client.get(mempool.endpoint + "/api/address/" + charge.onchainaddress)
respAmount = r.json()['chain_stats']['funded_txo_sum'] respAmount = r.json()['chain_stats']['funded_txo_sum']
print(respAmount)
if respAmount >= charge.balance: if respAmount >= charge.balance:
await update_charge(charge_id=charge_id, balance=respAmount) await update_charge(charge_id=charge_id, balance=respAmount)
except Exception: except Exception:
pass pass
if charge.lnbitswallet: if charge.lnbitswallet:
invoice_status = await check_invoice_status(charge.lnbitswallet, charge.payment_hash) invoice_status = await check_invoice_status(charge.lnbitswallet, charge.payment_hash)
print(invoice_status)
if invoice_status.paid: if invoice_status.paid:
print("paid")
return await update_charge(charge_id=charge_id, balance=charge.amount) return await update_charge(charge_id=charge_id, balance=charge.amount)
row = await db.fetchone("SELECT * FROM charges WHERE id = ?", (charge_id,)) row = await db.fetchone("SELECT * FROM charges WHERE id = ?", (charge_id,))
return Charges.from_row(row) if row else None return Charges.from_row(row) if row else None

View File

@ -15,6 +15,8 @@ async def m001_initial(db):
payment_request TEXT, payment_request TEXT,
payment_hash TEXT, payment_hash TEXT,
webhook TEXT, webhook TEXT,
completelink TEXT,
completelinktext TEXT,
time INTEGER, time INTEGER,
amount INTEGER, amount INTEGER,
balance INTEGER DEFAULT 0, balance INTEGER DEFAULT 0,

View File

@ -13,6 +13,8 @@ class Charges(NamedTuple):
payment_request: str payment_request: str
payment_hash: str payment_hash: str
webhook: str webhook: str
completelink: str
completelinktext: str
time: int time: int
amount: int amount: int
balance: int balance: int

View File

@ -1,7 +1,9 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<p> <p>
SatsPay: Create Onchain/LN charges. Includes webhooks!<br /> SatsPayServer, create Onchain/LN charges.<br />WARNING: If using with the
WatchOnly extension, we highly reccomend using a fresh extended public Key
specifically for SatsPayServer!<br />
<small> <small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small Created by, <a href="https://github.com/benarc">Ben Arc</a></small
> >

View File

@ -81,6 +81,7 @@
</div> </div>
<div v-else-if="charge_paid == 'True'"> <div v-else-if="charge_paid == 'True'">
<q-icon name="check" style="color:green; font-size: 21.4em;" ></q-icon> <q-icon name="check" style="color:green; font-size: 21.4em;" ></q-icon>
<q-btn outline v-if="'{{ charge.webhook }}' != 'None'" type="a" href="{{ charge.completelink }}" label="{{ charge.completelinktext }}"></q-btn>
</div> </div>
<div v-else> <div v-else>
<center> <center>
@ -115,6 +116,7 @@
</div> </div>
<div v-else-if="charge_paid == 'True'"> <div v-else-if="charge_paid == 'True'">
<q-icon name="check" style="color:green; font-size: 21.4em;" ></q-icon> <q-icon name="check" style="color:green; font-size: 21.4em;" ></q-icon>
<q-btn outline v-if="'{{ charge.webhook }}' != 'None'" type="a" href="{{ charge.completelink }}" label="{{ charge.completelinktext }}"></q-btn>
</div> </div>
<div v-else> <div v-else>
<center> <center>
@ -217,7 +219,11 @@
timerCount: function () { timerCount: function () {
self = this self = this
setInterval(function () { var refreshIntervalId = setInterval(function () {
if(self.charge_paid == "True"){
console.log("did this work?")
clearInterval(refreshIntervalId)
}
self.getTheTime() self.getTheTime()
self.getThePercentage() self.getThePercentage()
self.counter++ self.counter++
@ -228,6 +234,10 @@
} }
}, },
created: function () { created: function () {
if('{{ charge.lnbitswallet }}' == 'None'){
this.lnbtc = false
this.onbtc = true
}
this.getTheTime() this.getTheTime()
this.getThePercentage() this.getThePercentage()
var timerCount = this.timerCount var timerCount = this.timerCount

View File

@ -104,11 +104,10 @@
size="xs" size="xs"
icon="cached" icon="cached"
flat flat
@click="getBalance(props.row.id)"
:color="($q.dark.isActive) ? 'blue' : 'blue'" :color="($q.dark.isActive) ? 'blue' : 'blue'"
> >
<q-tooltip> <q-tooltip>
Check balance Processing
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn <q-btn
@ -118,15 +117,23 @@
@click="openUpdateDialog(props.row.id)" @click="openUpdateDialog(props.row.id)"
icon="edit" icon="edit"
color="light-blue" color="light-blue"
></q-btn> >
<q-tooltip>
Edit charge
</q-tooltip>
</q-btn>
<q-btn <q-btn
flat flat
dense dense
size="xs" size="xs"
@click="deleteWalletLink(props.row.id)" @click="deleteChargeLink(props.row.id)"
icon="cancel" icon="cancel"
color="pink" color="pink"
></q-btn> >
<q-tooltip>
Delete charge
</q-tooltip>
</q-btn>
</q-td> </q-td>
@ -179,7 +186,7 @@
dense dense
v-model.trim="formDialogCharge.data.description" v-model.trim="formDialogCharge.data.description"
type="text" type="text"
label="Description" label="*Description"
></q-input> ></q-input>
<q-input <q-input
@ -187,7 +194,7 @@
dense dense
v-model.trim="formDialogCharge.data.amount" v-model.trim="formDialogCharge.data.amount"
type="number" type="number"
label="Amount (sats)" label="*Amount (sats)"
></q-input> ></q-input>
<q-input <q-input
@ -196,7 +203,7 @@
v-model.trim="formDialogCharge.data.time" v-model.trim="formDialogCharge.data.time"
type="number" type="number"
max="1440" max="1440"
label="Mins valid for (max 1440)" label="*Mins valid for (max 1440)"
> </q-input> > </q-input>
<q-input <q-input
@ -204,7 +211,22 @@
dense dense
v-model.trim="formDialogCharge.data.webhook" v-model.trim="formDialogCharge.data.webhook"
type="url" type="url"
label="Webhook" label="Webhook (URL to send transaction data to once paid)"
> </q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelink"
type="url"
label="Completed button URL"
> </q-input>
<q-input
filled
dense
v-model.trim="formDialogCharge.data.completelinktext"
type="text"
label="Completed button text (ie 'Back to merchant')"
> </q-input> > </q-input>
<div class="row"> <div class="row">
@ -263,9 +285,9 @@
formDialogCharge.data.time == null || formDialogCharge.data.time == null ||
formDialogCharge.data.amount == null" formDialogCharge.data.amount == null"
type="submit" type="submit"
>Create Paylink</q-btn >Create Charge</q-btn
> >
<q-btn v-close-popup flat color="grey" class="q-ml-auto" <q-btn @click="cancelCharge" flat color="grey" class="q-ml-auto"
>Cancel</q-btn >Cancel</q-btn
> >
</div> </div>
@ -283,30 +305,9 @@
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)
Vue.filter('reverse', function(value) {
// slice to make a copy of array, then reverse the copy
return value.slice().reverse();
});
var locationPath = [
window.location.protocol,
'//',
window.location.hostname,
window.location.pathname
].join('')
var mapWalletLink = function (obj) {
obj._data = _.clone(obj)
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
return obj
}
var mapCharge = obj => { var mapCharge = obj => {
obj._data = _.clone(obj) obj._data = _.clone(obj)
obj.theTime = (obj.time * 60) - (Date.now()/1000 - obj.timestamp) obj.theTime = (obj.time * 60) - (Date.now()/1000 - obj.timestamp)
console.log(obj.theTime)
obj.time = obj.time + "mins" obj.time = obj.time + "mins"
if(obj.time_elapsed){ if(obj.time_elapsed){
@ -318,7 +319,6 @@
'HH:mm:ss' 'HH:mm:ss'
)} )}
obj.displayUrl = ['/satspay/', obj.id].join('') obj.displayUrl = ['/satspay/', obj.id].join('')
console.log(obj.date)
return obj return obj
} }
@ -396,6 +396,18 @@
label: 'LNbits wallet', label: 'LNbits wallet',
field: 'lnbitswallet' field: 'lnbitswallet'
}, },
{
name: 'Webhook link',
align: 'left',
label: 'Webhook link',
field: 'webhook'
},
{
name: 'Paid link',
align: 'left',
label: 'Paid link',
field: 'completelink'
},
], ],
pagination: { pagination: {
rowsPerPage: 10 rowsPerPage: 10
@ -407,7 +419,11 @@
}, },
formDialogCharge: { formDialogCharge: {
show: false, show: false,
data: {onchain: false,lnbits:false} data: {
onchain: false,
lnbits:false,
description: ""
}
}, },
qrCodeDialog: { qrCodeDialog: {
show: false, show: false,
@ -416,6 +432,17 @@
} }
}, },
methods: { methods: {
cancelCharge: function (data) {
var self = this
self.formDialogCharge.data.description = ""
self.formDialogCharge.data.onchainwallet = ""
self.formDialogCharge.data.lnbitswallet = ""
self.formDialogCharge.data.time = null
self.formDialogCharge.data.amount = null
self.formDialogCharge.data.webhook = ""
self.formDialogCharge.data.completelink = ""
self.formDialogCharge.show = false
},
getWalletLinks: function () { getWalletLinks: function () {
var self = this var self = this
@ -464,7 +491,6 @@
this.createWalletLink(wallet, data) this.createWalletLink(wallet, data)
} }
}, },
getCharges: function () { getCharges: function () {
var self = this var self = this
var getAddressBalance = this.getAddressBalance var getAddressBalance = this.getAddressBalance
@ -476,7 +502,6 @@
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
) )
.then(function (response) { .then(function (response) {
console.log(response.data[0].time_elapsed)
self.ChargeLinks = response.data.map(mapCharge) self.ChargeLinks = response.data.map(mapCharge)
}) })
.catch(function (error) { .catch(function (error) {
@ -485,8 +510,8 @@
}, },
sendFormDataCharge: function () { sendFormDataCharge: function () {
var self = this var self = this
var wallet = self.g.user.wallets[0].adminkey var wallet = this.g.user.wallets[0].adminkey
var data = self.formDialogCharge.data var data = this.formDialogCharge.data
data.amount = parseInt(data.amount) data.amount = parseInt(data.amount)
data.time = parseInt(data.time) data.time = parseInt(data.time)
if (data.id) { if (data.id) {
@ -494,9 +519,6 @@
} else { } else {
this.createCharge(wallet, data) this.createCharge(wallet, data)
} }
this.getCharges()
this.formDialogCharge.show = false
this.formDialogCharge.data = null
}, },
updateCharge: function (wallet, data) { updateCharge: function (wallet, data) {
var self = this var self = this
@ -517,20 +539,25 @@
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
}, },
getBalance: function (walletId) { timerCount: function () {
var self = this self = this
var refreshIntervalId = setInterval(function () {
for (i = 0; i < self.ChargeLinks.length - 1; i++) {
if(self.ChargeLinks[i]["paid"] == 'True'){
setTimeout(function(){
LNbits.api LNbits.api
.request( .request(
'GET', 'GET',
'/satspay/api/v1/charges/balance/' + walletId, '/satspay/api/v1/charges/balance/' + self.ChargeLinks[i]["id"],
this.g.user.wallets[0].inkey "filla"
) )
.then(function (response) { .then(function (response) {
this.ChargeLinks = response.data.map(mapCharge)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
}) })
}, 2000);
}
}
self.getCharges()
}, 20000)
}, },
createCharge: function (wallet, data) { createCharge: function (wallet, data) {
var self = this var self = this
@ -538,10 +565,8 @@
LNbits.api LNbits.api
.request('POST', '/satspay/api/v1/charge', wallet, data) .request('POST', '/satspay/api/v1/charge', wallet, data)
.then(function (response) { .then(function (response) {
this.formDialogCharge.show = false
this.formDialogCharge.data = null
self.ChargeLinks.push(mapCharge(response.data)) self.ChargeLinks.push(mapCharge(response.data))
self.formDialogCharge.show = false
}) })
.catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
@ -570,53 +595,21 @@
}) })
}, },
updateWalletLink: function (wallet, data) { deleteChargeLink: function (chargeId) {
var self = this var self = this
var link = _.findWhere(this.chargeLinks, {id: chargeId})
LNbits.api
.request(
'PUT',
'/satspay/api/v1/wallet/' + data.id,
wallet.inkey, data)
.then(function (response) {
self.walletLinks = _.reject(self.walletLinks, function (obj) {
return obj.id === data.id
})
self.walletLinks.push(mapWalletLink(response.data))
self.formDialog.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
createWalletLink: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/satspay/api/v1/wallet', wallet.inkey, data)
.then(function (response) {
self.walletLinks.push(mapWalletLink(response.data))
self.formDialog.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteWalletLink: function (linkId) {
var self = this
var link = _.findWhere(this.walletLinks, {id: linkId})
LNbits.utils LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?') .confirmDialog('Are you sure you want to delete this pay link?')
.onOk(function () { .onOk(function () {
LNbits.api LNbits.api
.request( .request(
'DELETE', 'DELETE',
'/satspay/api/v1/wallet/' + linkId, '/satspay/api/v1/charge/' + chargeId,
self.g.user.wallets[0].inkey self.g.user.wallets[0].adminkey
) )
.then(function (response) { .then(function (response) {
self.walletLinks = _.reject(self.walletLinks, function (obj) { self.chargeLinks = _.reject(self.chargeLinks, function (obj) {
return obj.id === linkId return obj.id === chargeId
})}) })})
.catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
@ -624,7 +617,7 @@
}) })
}, },
exportCSV: function () { exportCSV: function () {
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls) LNbits.utils.exportCSV(this.ChargesTable.columns, this.chargeLinks)
}, },
}, },
@ -634,6 +627,8 @@
getCharges() getCharges()
var getWalletLinks = this.getWalletLinks var getWalletLinks = this.getWalletLinks
getWalletLinks() getWalletLinks()
var timerCount = this.timerCount
timerCount()
} }
}) })
</script> </script>

View File

@ -28,7 +28,9 @@ from .crud import (
"onchainwallet": {"type": "string"}, "onchainwallet": {"type": "string"},
"lnbitswallet": {"type": "string"}, "lnbitswallet": {"type": "string"},
"description": {"type": "string", "empty": False, "required": True}, "description": {"type": "string", "empty": False, "required": True},
"webhook": {"type": "string", "empty": False, "required": True}, "webhook": {"type": "string"},
"completelink": {"type": "string"},
"completelinktext": {"type": "string"},
"time": {"type": "integer", "min": 1, "required": True}, "time": {"type": "integer", "min": 1, "required": True},
"amount": {"type": "integer", "min": 1, "required": True}, "amount": {"type": "integer", "min": 1, "required": True},
} }
@ -85,15 +87,36 @@ async def api_charges_balance(charge_id):
if not charge: if not charge:
return jsonify({"message": "charge does not exist"}), HTTPStatus.NOT_FOUND return jsonify({"message": "charge does not exist"}), HTTPStatus.NOT_FOUND
else: if charge.paid and charge.webhook:
async with httpx.AsyncClient() as client:
try:
r = await client.post(
charge.webhook,
json={
"id": charge.id,
"description": charge.description,
"onchainaddress": charge.onchainaddress,
"payment_request": charge.payment_request,
"payment_hash": charge.payment_hash,
"time": charge.time,
"amount": charge.amount,
"balance": charge.balance,
"paid": charge.paid,
"timestamp": charge.timestamp,
"completelink": charge.completelink,
},
timeout=40,
)
except AssertionError:
charge.webhook = None
return jsonify(charge._asdict()), HTTPStatus.OK return jsonify(charge._asdict()), HTTPStatus.OK
#############################MEMPOOL########################## #############################MEMPOOL##########################
@satspay_ext.route("/api/v1/mempool", methods=["PUT"]) @ satspay_ext.route("/api/v1/mempool", methods=["PUT"])
@api_check_wallet_key("invoice") @ api_check_wallet_key("invoice")
@api_validate_post_request( @ api_validate_post_request(
schema={ schema={
"endpoint": {"type": "string", "empty": False, "required": True}, "endpoint": {"type": "string", "empty": False, "required": True},
} }
@ -103,8 +126,8 @@ async def api_update_mempool():
return jsonify(mempool._asdict()), HTTPStatus.OK return jsonify(mempool._asdict()), HTTPStatus.OK
@satspay_ext.route("/api/v1/mempool", methods=["GET"]) @ satspay_ext.route("/api/v1/mempool", methods=["GET"])
@api_check_wallet_key("invoice") @ api_check_wallet_key("invoice")
async def api_get_mempool(): async def api_get_mempool():
mempool = await get_mempool(g.wallet.user) mempool = await get_mempool(g.wallet.user)
if not mempool: if not mempool: