Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "ATM" option to the TPoS to use LUD-19 withdraw request to top up a wallet (e.g. bolt card) #23

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ async def create_tpos(wallet_id: str, data: CreateTposData) -> TPoS:
tpos_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO tpos.tposs (id, wallet, name, currency, tip_options, tip_wallet)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO tpos.tposs (id, wallet, name, currency, tip_options, tip_wallet, atm)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
tpos_id,
Expand All @@ -20,6 +20,7 @@ async def create_tpos(wallet_id: str, data: CreateTposData) -> TPoS:
data.currency,
data.tip_options,
data.tip_wallet,
data.atm
),
)

Expand Down
10 changes: 10 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,13 @@ async def m003_addtip_options(db):
ALTER TABLE tpos.tposs ADD tip_options TEXT NULL;
"""
)

async def m004_addatm_options(db):
"""
Add tips to tposs table
"""
await db.execute(
"""
ALTER TABLE tpos.tposs ADD atm BOOLEAN NULL;
"""
)
2 changes: 2 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class CreateTposData(BaseModel):
currency: str
tip_options: str = Query(None)
tip_wallet: str = Query(None)
atm: bool = Query(None)


class TPoS(BaseModel):
Expand All @@ -19,6 +20,7 @@ class TPoS(BaseModel):
currency: str
tip_options: Optional[str]
tip_wallet: Optional[str]
atm: Optional[bool]

@classmethod
def from_row(cls, row: Row) -> "TPoS":
Expand Down
17 changes: 14 additions & 3 deletions templates/tpos/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ <h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} TPoS extension</h6>
(round amount to a value)
</template>
</q-select>
<q-checkbox filled dense emit-value v-model="formDialog.data.atm" :options="g.user.walletOptions"
label="ATM"></q-checkbox>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" :disable="formDialog.data.currency == null || formDialog.data.name == null"
type="submit">Create TPoS</q-btn>
Expand Down Expand Up @@ -314,15 +316,23 @@ <h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} TPoS extension</h6>
align: 'left',
label: 'Tip Options %',
field: 'tip_options'
}
},
{
name: 'atm',
align: 'left',
label: 'Allow ATM',
field: 'atm'
},
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
data: {
atm: false
}
}
}
},
Expand Down Expand Up @@ -354,7 +364,8 @@ <h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} TPoS extension</h6>
this.formDialog.data.tip_options.map(str => parseInt(str))
)
: JSON.stringify([]),
tip_wallet: this.formDialog.data.tip_wallet || ''
tip_wallet: this.formDialog.data.tip_wallet || '',
atm: this.formDialog.data.atm || 0
}
const wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
Expand Down
65 changes: 63 additions & 2 deletions templates/tpos/tpos.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ <h5 class="q-mt-none q-mb-sm">
<q-page-sticky position="top-right" :offset="[18, 90]">
<q-btn @click="toggleFullscreen" fab :icon="fullScreenIcon" color="primary" />
</q-page-sticky>
<q-page-sticky position="top-right" :offset="[18, 162]">
<q-btn
:disabled="amount == 0"
@click="readNfcTag('atm')"
fab
color="primary"
/>ATM</q-btn>
</q-page-sticky>
<q-dialog v-model="invoiceDialog.show" position="top" @hide="closeInvoiceDialog">
<q-card v-if="invoiceDialog.data" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
Expand Down Expand Up @@ -187,6 +195,7 @@ <h5 class="q-mt-none">
tposId: '{{ tpos.id }}',
currency: '{{ tpos.currency }}',
tip_options: null,
atm: null,
exchangeRate: null,
stack: [],
tipAmount: 0.0,
Expand Down Expand Up @@ -365,7 +374,7 @@ <h5 class="q-mt-none">
LNbits.utils.notifyApiError(error)
})
},
readNfcTag: function () {
readNfcTag: function (atm=false) {
try {
const self = this

Expand Down Expand Up @@ -414,12 +423,21 @@ <h5 class="q-mt-none">

//User feedback, show loader icon
self.nfcTagReading = false
self.payInvoice(lnurl, readerAbortController)

if (atm){
axios.get(lnurl.replace('lnurlw://', 'https://')).then(function (response) {
console.log(response.data.payLink)
self.makeAtmWithdraw(response.data.payLink, readerAbortController)
})
} else {
self.payInvoice(lnurl, readerAbortController)
}

this.$q.notify({
type: 'positive',
message: 'NFC tag read successfully.'
})
readerAbortController.abort()
}
})
} catch (error) {
Expand All @@ -432,6 +450,49 @@ <h5 class="q-mt-none">
})
}
},
makeAtmWithdraw: function (payLink, readerAbortController) {
const self = this

if (!payLink){
this.$q.notify({
type: 'negative',
message: "Card doesn't support the ATM topup feature (LUD-19)"
})
return
}

return axios
.post(
'/tpos/api/v1/tposs/' +
self.tposId +
'/atm/', null,
{
params: {
amount: this.sat,
memo: this.amountFormatted,
payLink: payLink
}
}
)
.then(response => {
if (!response.data.success) {
this.$q.notify({
type: 'negative',
message: response.data.detail
})
}
else {
this.stack = []
this.tipAmount = 0.0
this.$q.notify({
type: 'positive',
message: "Topup successful!"
})
}

readerAbortController.abort()
})
},
payInvoice: function (lnurl, readerAbortController) {
const self = this

Expand Down
58 changes: 57 additions & 1 deletion views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from lnbits.core.crud import get_latest_payments_by_extension, get_user
from lnbits.core.models import Payment
from lnbits.core.services import create_invoice
from lnbits.core.services import create_invoice, pay_invoice
from lnbits.core.views.api import api_payment
from lnbits.decorators import (
WalletTypeInfo,
Expand Down Expand Up @@ -111,6 +111,62 @@ async def api_tpos_create_invoice(

return {"payment_hash": payment_hash, "payment_request": payment_request}

@tpos_ext.post("/api/v1/tposs/{tpos_id}/atm", status_code=HTTPStatus.OK)
async def api_tpos_make_atm(
tpos_id: str, amount: int = Query(..., ge=1), memo: str = "", payLink: str = ""
) -> dict:

tpos = await get_tpos(tpos_id)

if not tpos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
)
if (tpos.atm == 0 or tpos.atm == None):
return {"success": False, "detail": "ATM mode not allowed"}

payLink = payLink.replace("lnurlp://", "https://") # pointless lnurlp:// -> https://

async with httpx.AsyncClient() as client:
try:
headers = {"user-agent": f"lnbits/tpos"}
r = await client.get(payLink, follow_redirects=True, headers=headers)

if r.is_error:
return {"success": False, "detail": "Error loading"}

resp = r.json()

amount = amount*1000 # convert to msats

if resp["tag"] != "payRequest":
return {"success": False, "detail": "Wrong tag type"}

if amount < resp["minSendable"]:
return {"success": False, "detail": "Amount too low"}

if amount > resp["maxSendable"]:
return {"success": False, "detail": "Amount too high"}

cb_res = await client.get(resp["callback"], follow_redirects=True, headers=headers, params={"amount": amount})
cb_resp = cb_res.json()

if cb_res.is_error:
return {"success": False, "detail": "Error loading callback"}

try:
payment_hash = await pay_invoice(
wallet_id=tpos.wallet,
payment_request=cb_resp["pr"],
description="ATM Withdrawal",
extra={"tag": "tpos_atm", "tpos": tpos.id},
)
return {"success": True, "detail": "Payment successful", "payment_hash": payment_hash}
except Exception as exc:
return {"success": False, "reason": exc, "detail": f"Payment failed - {exc}"}

except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))

@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices")
async def api_tpos_get_latest_invoices(tpos_id: str):
Expand Down