Merge pull request #677 from lnbits/SCRUB

SCRUB extension
This commit is contained in:
Arc 2022-07-28 13:29:04 +01:00 committed by GitHub
commit dc501d3f40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 865 additions and 0 deletions

View File

@ -0,0 +1,28 @@
# Scrub
## Automatically forward funds (Scrub) that get paid to the wallet to an LNURLpay or Lightning Address
SCRUB is a small but handy extension that allows a user to take advantage of all the functionalities inside **LNbits** and upon a payment received to your LNbits wallet, automatically forward it to your desired wallet via LNURL or LNAddress!
[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets)
## Usage
1. Create an scrub (New Scrub link)\
![create scrub](https://i.imgur.com/LUeNkzM.jpg)
- select the wallet to be _scrubbed_
- make a small description
- enter either an LNURL pay or a lightning address
Make sure your LNURL or LNaddress is correct!
2. A new scrub will show on the _Scrub links_ section\
![scrub](https://i.imgur.com/LNoFkeu.jpg)
- only one scrub can be created for each wallet!
- You can _edit_ or _delete_ the Scrub at any time\
![edit scrub](https://i.imgur.com/Qu65lGG.jpg)
3. On your wallet, you'll see a transaction of a payment received and another right after it as apayment sent, marked with **#scrubed**\
![wallet view](https://i.imgur.com/S6EWWCP.jpg)

View File

@ -0,0 +1,34 @@
import asyncio
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_scrub")
scrub_static_files = [
{
"path": "/scrub/static",
"app": StaticFiles(directory="lnbits/extensions/scrub/static"),
"name": "scrub_static",
}
]
scrub_ext: APIRouter = APIRouter(prefix="/scrub", tags=["scrub"])
def scrub_renderer():
return template_renderer(["lnbits/extensions/scrub/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def scrub_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View File

@ -0,0 +1,6 @@
{
"name": "Scrub",
"short_description": "Pass payments to LNURLp/LNaddress",
"icon": "send",
"contributors": ["arcbtc", "talvasconcelos"]
}

View File

@ -0,0 +1,80 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import CreateScrubLink, ScrubLink
async def create_scrub_link(data: CreateScrubLink) -> ScrubLink:
scrub_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO scrub.scrub_links (
id,
wallet,
description,
payoraddress
)
VALUES (?, ?, ?, ?)
""",
(
scrub_id,
data.wallet,
data.description,
data.payoraddress,
),
)
link = await get_scrub_link(scrub_id)
assert link, "Newly created link couldn't be retrieved"
return link
async def get_scrub_link(link_id: str) -> Optional[ScrubLink]:
row = await db.fetchone("SELECT * FROM scrub.scrub_links WHERE id = ?", (link_id,))
return ScrubLink(**row) if row else None
async def get_scrub_links(wallet_ids: Union[str, List[str]]) -> List[ScrubLink]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"""
SELECT * FROM scrub.scrub_links WHERE wallet IN ({q})
ORDER BY id
""",
(*wallet_ids,),
)
return [ScrubLink(**row) for row in rows]
async def update_scrub_link(link_id: int, **kwargs) -> Optional[ScrubLink]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE scrub.scrub_links SET {q} WHERE id = ?",
(*kwargs.values(), link_id),
)
row = await db.fetchone("SELECT * FROM scrub.scrub_links WHERE id = ?", (link_id,))
return ScrubLink(**row) if row else None
async def delete_scrub_link(link_id: int) -> None:
await db.execute("DELETE FROM scrub.scrub_links WHERE id = ?", (link_id,))
async def get_scrub_by_wallet(wallet_id) -> Optional[ScrubLink]:
row = await db.fetchone(
"SELECT * from scrub.scrub_links WHERE wallet = ?",
(wallet_id,),
)
return ScrubLink(**row) if row else None
async def unique_scrubed_wallet(wallet_id):
(row,) = await db.fetchone(
"SELECT COUNT(wallet) FROM scrub.scrub_links WHERE wallet = ?",
(wallet_id,),
)
return row

View File

@ -0,0 +1,14 @@
async def m001_initial(db):
"""
Initial scrub table.
"""
await db.execute(
f"""
CREATE TABLE scrub.scrub_links (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
description TEXT NOT NULL,
payoraddress TEXT NOT NULL
);
"""
)

View File

@ -0,0 +1,28 @@
from sqlite3 import Row
from pydantic import BaseModel
from starlette.requests import Request
from lnbits.lnurl import encode as lnurl_encode # type: ignore
class CreateScrubLink(BaseModel):
wallet: str
description: str
payoraddress: str
class ScrubLink(BaseModel):
id: str
wallet: str
description: str
payoraddress: str
@classmethod
def from_row(cls, row: Row) -> "ScrubLink":
data = dict(row)
return cls(**data)
def lnurl(self, req: Request) -> str:
url = req.url_for("scrub.api_lnurl_response", link_id=self.id)
return lnurl_encode(url)

View File

@ -0,0 +1,143 @@
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
Vue.component(VueQrcode.name, VueQrcode)
var locationPath = [
window.location.protocol,
'//',
window.location.host,
window.location.pathname
].join('')
var mapScrubLink = obj => {
obj._data = _.clone(obj)
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.print_url = [locationPath, 'print/', obj.id].join('')
obj.pay_url = [locationPath, obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
checker: null,
payLinks: [],
payLinksTable: {
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
},
qrCodeDialog: {
show: false,
data: null
}
}
},
methods: {
getScrubLinks() {
LNbits.api
.request(
'GET',
'/scrub/api/v1/links?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.payLinks = response.data.map(mapScrubLink)
})
.catch(err => {
clearInterval(this.checker)
LNbits.utils.notifyApiError(err)
})
},
closeFormDialog() {
this.resetFormData()
},
openUpdateDialog(linkId) {
const link = _.findWhere(this.payLinks, {id: linkId})
this.formDialog.data = _.clone(link._data)
this.formDialog.show = true
},
sendFormData() {
const wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
let data = Object.freeze(this.formDialog.data)
console.log(wallet, data)
if (data.id) {
this.updateScrubLink(wallet, data)
} else {
this.createScrubLink(wallet, data)
}
},
resetFormData() {
this.formDialog = {
show: false,
data: {}
}
},
updateScrubLink(wallet, data) {
LNbits.api
.request('PUT', '/scrub/api/v1/links/' + data.id, wallet.adminkey, data)
.then(response => {
this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id)
this.payLinks.push(mapScrubLink(response.data))
this.formDialog.show = false
this.resetFormData()
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
},
createScrubLink(wallet, data) {
LNbits.api
.request('POST', '/scrub/api/v1/links', wallet.adminkey, data)
.then(response => {
console.log('RES', response)
this.getScrubLinks()
this.formDialog.show = false
this.resetFormData()
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
},
deleteScrubLink(linkId) {
var link = _.findWhere(this.payLinks, {id: linkId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?')
.onOk(() => {
LNbits.api
.request(
'DELETE',
'/scrub/api/v1/links/' + linkId,
_.findWhere(this.g.user.wallets, {id: link.wallet}).adminkey
)
.then(response => {
this.payLinks = _.reject(this.payLinks, obj => obj.id === linkId)
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
})
}
},
created() {
if (this.g.user.wallets.length) {
var getScrubLinks = this.getScrubLinks
getScrubLinks()
}
}
})

View File

@ -0,0 +1,85 @@
import asyncio
import json
from http import HTTPStatus
from urllib.parse import urlparse
import httpx
from fastapi import HTTPException
from lnbits import bolt11
from lnbits.core.models import Payment
from lnbits.core.services import pay_invoice
from lnbits.tasks import register_invoice_listener
from .crud import get_scrub_by_wallet
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue)
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
# (avoid loops)
if "scrubed" == payment.extra.get("tag"):
# already scrubbed
return
scrub_link = await get_scrub_by_wallet(payment.wallet_id)
if not scrub_link:
return
from lnbits.core.views.api import api_lnurlscan
# DECODE LNURLP OR LNADDRESS
data = await api_lnurlscan(scrub_link.payoraddress)
# I REALLY HATE THIS DUPLICATION OF CODE!! CORE/VIEWS/API.PY, LINE 267
domain = urlparse(data["callback"]).netloc
async with httpx.AsyncClient() as client:
try:
r = await client.get(
data["callback"],
params={"amount": payment.amount},
timeout=40,
)
if r.is_error:
raise httpx.ConnectError
except (httpx.ConnectError, httpx.RequestError):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Failed to connect to {domain}.",
)
params = json.loads(r.text)
if params.get("status") == "ERROR":
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} said: '{params.get('reason', '')}'",
)
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != payment.amount:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected {payment.amount} msat, got {invoice.amount_msat}.",
)
payment_hash = await pay_invoice(
wallet_id=payment.wallet_id,
payment_request=params["pr"],
description=data["description"],
extra={"tag": "scrubed"},
)
return {
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}

View File

@ -0,0 +1,136 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="List scrubs">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /scrub/api/v1/links</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;pay_link_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}scrub/api/v1/links?all_wallets=true
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get a scrub">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/scrub/api/v1/links/&lt;scrub_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "wallet": &lt;string&gt;, "description":
&lt;string&gt;, "payoraddress": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}scrub/api/v1/links/&lt;pay_id&gt;
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a scrub">
<q-card>
<q-card-section>
<code><span class="text-green">POST</span> /scrub/api/v1/links</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"wallet": &lt;string&gt;, "description": &lt;string&gt;,
"payoraddress": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "wallet": &lt;string&gt;, "description":
&lt;string&gt;, "payoraddress": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}scrub/api/v1/links -d '{"wallet":
&lt;string&gt;, "description": &lt;string&gt;, "payoraddress":
&lt;string&gt;}' -H "Content-type: application/json" -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Update a scrub">
<q-card>
<q-card-section>
<code
><span class="text-green">PUT</span>
/scrub/api/v1/links/&lt;pay_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"wallet": &lt;string&gt;, "description": &lt;string&gt;,
"payoraddress": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "wallet": &lt;string&gt;, "description":
&lt;string&gt;, "payoraddress": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.base_url }}scrub/api/v1/links/&lt;pay_id&gt;
-d '{"wallet": &lt;string&gt;, "description": &lt;string&gt;,
"payoraddress": &lt;string&gt;}' -H "Content-type: application/json"
-H "X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a scrub"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/scrub/api/v1/links/&lt;pay_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}scrub/api/v1/links/&lt;pay_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View File

@ -0,0 +1,28 @@
<q-expansion-item group="extras" icon="info" label="Powered by LNURL">
<q-card>
<q-card-section>
<p>
<b>WARNING: LNURL must be used over https or TOR</b><br />
LNURL is a range of lightning-network standards that allow us to use
lightning-network differently. An LNURL-pay is a link that wallets use
to fetch an invoice from a server on-demand. The link or QR code is
fixed, but each time it is read by a compatible wallet a new QR code is
issued by the service. It can be used to activate machines without them
having to maintain an electronic screen to generate and show invoices
locally, or to sell any predefined good or service automatically.
</p>
<p>
Exploring LNURL and finding use cases, is really helping inform
lightning protocol development, rather than the protocol dictating how
lightning-network should be engaged with.
</p>
<small
>Check
<a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank"
>Awesome LNURL</a
>
for further information.</small
>
</q-card-section>
</q-card>
</q-expansion-item>

View File

@ -0,0 +1,140 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New scrub link</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Scrub links</h5>
</div>
</div>
<q-table
dense
flat
:data="payLinks"
row-key="id"
:pagination.sync="payLinksTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props" style="text-align: left">
<q-th>Wallet</q-th>
<q-th>Description</q-th>
<q-th>LNURLPay/Address</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td>{{ props.row.wallet }}</q-td>
<q-td>{{ props.row.description }}</q-td>
<q-td>{{ props.row.payoraddress }}</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteScrubLink(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Scrub extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "scrub/_api_docs.html" %}
<q-separator></q-separator>
{% include "scrub/_lnurl.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.description"
type="text"
label="Description *"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.payoraddress"
type="text"
label="LNURLPay or LNAdress *"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialog.data.id"
unelevated
color="primary"
type="submit"
>Update pay link</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="
formDialog.data.wallet == null ||
formDialog.data.description == null ||
formDialog.data.payoraddress == null
"
type="submit"
>Create pay link</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="/scrub/static/js/index.js"></script>
{% endblock %}

View File

@ -0,0 +1,18 @@
from fastapi import Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import scrub_ext, scrub_renderer
templates = Jinja2Templates(directory="templates")
@scrub_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return scrub_renderer().TemplateResponse(
"scrub/index.html", {"request": request, "user": user.dict()}
)

View File

@ -0,0 +1,112 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import scrub_ext
from .crud import (
create_scrub_link,
delete_scrub_link,
get_scrub_link,
get_scrub_links,
unique_scrubed_wallet,
update_scrub_link,
)
from .models import CreateScrubLink
@scrub_ext.get("/api/v1/links", status_code=HTTPStatus.OK)
async def api_links(
req: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
all_wallets: bool = Query(False),
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [link.dict() for link in await get_scrub_links(wallet_ids)]
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="No SCRUB links made yet",
)
@scrub_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_link_retrieve(
r: Request, link_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
link = await get_scrub_link(link_id)
if not link:
raise HTTPException(
detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND
)
if link.wallet != wallet.wallet.id:
raise HTTPException(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
)
return link
@scrub_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
@scrub_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
async def api_scrub_create_or_update(
data: CreateScrubLink,
link_id=None,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
if link_id:
link = await get_scrub_link(link_id)
if not link:
raise HTTPException(
detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND
)
if link.wallet != wallet.wallet.id:
raise HTTPException(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
)
link = await update_scrub_link(**data.dict(), link_id=link_id)
else:
wallet_has_scrub = await unique_scrubed_wallet(wallet_id=data.wallet)
if wallet_has_scrub > 0:
raise HTTPException(
detail="Wallet is already being Scrubbed",
status_code=HTTPStatus.FORBIDDEN,
)
link = await create_scrub_link(data=data)
return link
@scrub_ext.delete("/api/v1/links/{link_id}")
async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
link = await get_scrub_link(link_id)
if not link:
raise HTTPException(
detail="Scrub link does not exist.", status_code=HTTPStatus.NOT_FOUND
)
if link.wallet != wallet.wallet.id:
raise HTTPException(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
)
await delete_scrub_link(link_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)

Binary file not shown.

View File

@ -704,6 +704,19 @@ def migrate_ext(sqlite_db_file, schema, ignore_missing=True):
VALUES (%s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "scrub":
# SCRUB LINKS
res = sq.execute("SELECT * FROM scrub_links;")
q = f"""
INSERT INTO scrub.scrub_links (
id,
wallet,
description,
payoraddress
)
VALUES (%s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
else:
print(f"❌ Not implemented: {schema}")
sq.close()