mirror of
https://github.com/lnbits/lnbits.git
synced 2025-06-28 17:50:56 +02:00
Merge branch 'main' into switchupdate
This commit is contained in:
commit
a37bec80cf
@ -79,3 +79,8 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
|
|||||||
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
||||||
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
||||||
- `OPENNODE_KEY`: opennodeAdminApiKey
|
- `OPENNODE_KEY`: opennodeAdminApiKey
|
||||||
|
|
||||||
|
|
||||||
|
### Cliche Wallet
|
||||||
|
|
||||||
|
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
|
||||||
|
@ -91,7 +91,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
# app.add_middleware(ASGIProxyFix)
|
|
||||||
|
|
||||||
check_funding_source(app)
|
check_funding_source(app)
|
||||||
register_assets(app)
|
register_assets(app)
|
||||||
|
@ -361,6 +361,35 @@ new Vue({
|
|||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onInitQR: async function (promise) {
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
} catch (error) {
|
||||||
|
let mapping = {
|
||||||
|
NotAllowedError: 'ERROR: you need to grant camera access permission',
|
||||||
|
NotFoundError: 'ERROR: no camera on this device',
|
||||||
|
NotSupportedError:
|
||||||
|
'ERROR: secure context required (HTTPS, localhost)',
|
||||||
|
NotReadableError: 'ERROR: is the camera already in use?',
|
||||||
|
OverconstrainedError: 'ERROR: installed cameras are not suitable',
|
||||||
|
StreamApiNotSupportedError:
|
||||||
|
'ERROR: Stream API is not supported in this browser',
|
||||||
|
InsecureContextError:
|
||||||
|
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
|
||||||
|
}
|
||||||
|
let valid_error = Object.keys(mapping).filter(key => {
|
||||||
|
return error.name === key
|
||||||
|
})
|
||||||
|
let camera_error = valid_error
|
||||||
|
? mapping[valid_error]
|
||||||
|
: `ERROR: Camera error (${error.name})`
|
||||||
|
this.parse.camera.show = false
|
||||||
|
this.$q.notify({
|
||||||
|
message: camera_error,
|
||||||
|
type: 'negative'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
decodeQR: function (res) {
|
decodeQR: function (res) {
|
||||||
this.parse.data.request = res
|
this.parse.data.request = res
|
||||||
this.decodeRequest()
|
this.decodeRequest()
|
||||||
|
@ -653,6 +653,7 @@
|
|||||||
<q-responsive :ratio="1">
|
<q-responsive :ratio="1">
|
||||||
<qrcode-stream
|
<qrcode-stream
|
||||||
@decode="decodeQR"
|
@decode="decodeQR"
|
||||||
|
@init="onInitQR"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode-stream>
|
></qrcode-stream>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
@ -671,6 +672,7 @@
|
|||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<qrcode-stream
|
<qrcode-stream
|
||||||
@decode="decodeQR"
|
@decode="decodeQR"
|
||||||
|
@init="onInitQR"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode-stream>
|
></qrcode-stream>
|
||||||
</div>
|
</div>
|
||||||
|
@ -476,7 +476,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
|||||||
except:
|
except:
|
||||||
# parse internet identifier (user@domain.com)
|
# parse internet identifier (user@domain.com)
|
||||||
name_domain = code.split("@")
|
name_domain = code.split("@")
|
||||||
if len(name_domain) == 2 and len(name_domain[1].split(".")) == 2:
|
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
|
||||||
name, domain = name_domain
|
name, domain = name_domain
|
||||||
url = (
|
url = (
|
||||||
("http://" if domain.endswith(".onion") else "https://")
|
("http://" if domain.endswith(".onion") else "https://")
|
||||||
|
@ -57,7 +57,7 @@ async def check_for_pending_swaps():
|
|||||||
swap_status = get_swap_status(swap)
|
swap_status = get_swap_status(swap)
|
||||||
# should only happen while development when regtest is reset
|
# should only happen while development when regtest is reset
|
||||||
if swap_status.exists is False:
|
if swap_status.exists is False:
|
||||||
logger.warning(f"Boltz - swap: {swap.boltz_id} does not exist.")
|
logger.debug(f"Boltz - swap: {swap.boltz_id} does not exist.")
|
||||||
await update_swap_status(swap.id, "failed")
|
await update_swap_status(swap.id, "failed")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ async def check_for_pending_swaps():
|
|||||||
else:
|
else:
|
||||||
if swap_status.hit_timeout:
|
if swap_status.hit_timeout:
|
||||||
if not swap_status.has_lockup:
|
if not swap_status.has_lockup:
|
||||||
logger.warning(
|
logger.debug(
|
||||||
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
|
f"Boltz - swap: {swap.id} hit timeout, but no lockup tx..."
|
||||||
)
|
)
|
||||||
await update_swap_status(swap.id, "timeout")
|
await update_swap_status(swap.id, "timeout")
|
||||||
|
@ -10,7 +10,7 @@ from .models import Copilots, CreateCopilotData
|
|||||||
|
|
||||||
async def create_copilot(
|
async def create_copilot(
|
||||||
data: CreateCopilotData, inkey: Optional[str] = ""
|
data: CreateCopilotData, inkey: Optional[str] = ""
|
||||||
) -> Copilots:
|
) -> Optional[Copilots]:
|
||||||
copilot_id = urlsafe_short_hash()
|
copilot_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
@ -67,19 +67,19 @@ async def create_copilot(
|
|||||||
|
|
||||||
|
|
||||||
async def update_copilot(
|
async def update_copilot(
|
||||||
data: CreateCopilotData, copilot_id: Optional[str] = ""
|
data: CreateCopilotData, copilot_id: str
|
||||||
) -> Optional[Copilots]:
|
) -> Optional[Copilots]:
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||||
items = [f"{field[1]}" for field in data]
|
items = [f"{field[1]}" for field in data]
|
||||||
items.append(copilot_id)
|
items.append(copilot_id)
|
||||||
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items))
|
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items,))
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
||||||
)
|
)
|
||||||
return Copilots(**row) if row else None
|
return Copilots(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_copilot(copilot_id: str) -> Copilots:
|
async def get_copilot(copilot_id: str) -> Optional[Copilots]:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,7 @@ async def wait_for_paid_invoices():
|
|||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
webhook = None
|
webhook = None
|
||||||
data = None
|
data = None
|
||||||
if payment.extra.get("tag") != "copilot":
|
if not payment.extra or payment.extra.get("tag") != "copilot":
|
||||||
# not an copilot invoice
|
# not an copilot invoice
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -71,12 +71,12 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||||
payment.extra["wh_status"] = status
|
if payment.extra:
|
||||||
|
payment.extra["wh_status"] = status
|
||||||
await core_db.execute(
|
await core_db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE apipayments SET extra = ?
|
UPDATE apipayments SET extra = ?
|
||||||
WHERE hash = ?
|
WHERE hash = ?
|
||||||
""",
|
""",
|
||||||
(json.dumps(payment.extra), payment.payment_hash),
|
(json.dumps(payment.extra), payment.payment_hash),
|
||||||
)
|
)
|
||||||
|
@ -15,7 +15,9 @@ templates = Jinja2Templates(directory="templates")
|
|||||||
|
|
||||||
|
|
||||||
@copilot_ext.get("/", response_class=HTMLResponse)
|
@copilot_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||||
|
):
|
||||||
return copilot_renderer().TemplateResponse(
|
return copilot_renderer().TemplateResponse(
|
||||||
"copilot/index.html", {"request": request, "user": user.dict()}
|
"copilot/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
@ -44,7 +46,7 @@ class ConnectionManager:
|
|||||||
|
|
||||||
async def connect(self, websocket: WebSocket, copilot_id: str):
|
async def connect(self, websocket: WebSocket, copilot_id: str):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
websocket.id = copilot_id
|
websocket.id = copilot_id # type: ignore
|
||||||
self.active_connections.append(websocket)
|
self.active_connections.append(websocket)
|
||||||
|
|
||||||
def disconnect(self, websocket: WebSocket):
|
def disconnect(self, websocket: WebSocket):
|
||||||
@ -52,7 +54,7 @@ class ConnectionManager:
|
|||||||
|
|
||||||
async def send_personal_message(self, message: str, copilot_id: str):
|
async def send_personal_message(self, message: str, copilot_id: str):
|
||||||
for connection in self.active_connections:
|
for connection in self.active_connections:
|
||||||
if connection.id == copilot_id:
|
if connection.id == copilot_id: # type: ignore
|
||||||
await connection.send_text(message)
|
await connection.send_text(message)
|
||||||
|
|
||||||
async def broadcast(self, message: str):
|
async def broadcast(self, message: str):
|
||||||
|
@ -23,7 +23,7 @@ from .views import updater
|
|||||||
|
|
||||||
@copilot_ext.get("/api/v1/copilot")
|
@copilot_ext.get("/api/v1/copilot")
|
||||||
async def api_copilots_retrieve(
|
async def api_copilots_retrieve(
|
||||||
req: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
req: Request, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
wallet_user = wallet.wallet.user
|
wallet_user = wallet.wallet.user
|
||||||
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
||||||
@ -37,7 +37,7 @@ async def api_copilots_retrieve(
|
|||||||
async def api_copilot_retrieve(
|
async def api_copilot_retrieve(
|
||||||
req: Request,
|
req: Request,
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
):
|
):
|
||||||
copilot = await get_copilot(copilot_id)
|
copilot = await get_copilot(copilot_id)
|
||||||
if not copilot:
|
if not copilot:
|
||||||
@ -54,7 +54,7 @@ async def api_copilot_retrieve(
|
|||||||
async def api_copilot_create_or_update(
|
async def api_copilot_create_or_update(
|
||||||
data: CreateCopilotData,
|
data: CreateCopilotData,
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||||
):
|
):
|
||||||
data.user = wallet.wallet.user
|
data.user = wallet.wallet.user
|
||||||
data.wallet = wallet.wallet.id
|
data.wallet = wallet.wallet.id
|
||||||
@ -67,7 +67,8 @@ async def api_copilot_create_or_update(
|
|||||||
|
|
||||||
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
||||||
async def api_copilot_delete(
|
async def api_copilot_delete(
|
||||||
copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
|
copilot_id: str = Query(None),
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||||
):
|
):
|
||||||
copilot = await get_copilot(copilot_id)
|
copilot = await get_copilot(copilot_id)
|
||||||
|
|
||||||
|
@ -98,21 +98,21 @@ async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]:
|
|||||||
return Wallets(**row) if row else None
|
return Wallets(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_wallets(admin_id: str) -> Optional[Wallets]:
|
async def get_discordbot_wallets(admin_id: str) -> List[Wallets]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
|
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
|
||||||
)
|
)
|
||||||
return [Wallets(**row) for row in rows]
|
return [Wallets(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_users_wallets(user_id: str) -> Optional[Wallets]:
|
async def get_discordbot_users_wallets(user_id: str) -> List[Wallets]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
|
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
|
||||||
)
|
)
|
||||||
return [Wallets(**row) for row in rows]
|
return [Wallets(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_wallet_transactions(wallet_id: str) -> Optional[Payment]:
|
async def get_discordbot_wallet_transactions(wallet_id: str) -> List[Payment]:
|
||||||
return await get_payments(
|
return await get_payments(
|
||||||
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,9 @@ from . import discordbot_ext, discordbot_renderer
|
|||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/", response_class=HTMLResponse)
|
@discordbot_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||||
|
):
|
||||||
return discordbot_renderer().TemplateResponse(
|
return discordbot_renderer().TemplateResponse(
|
||||||
"discordbot/index.html", {"request": request, "user": user.dict()}
|
"discordbot/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
@ -27,32 +27,37 @@ from .models import CreateUserData, CreateUserWallet
|
|||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
||||||
async def api_discordbot_users(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_users(
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
):
|
||||||
user_id = wallet.wallet.user
|
user_id = wallet.wallet.user
|
||||||
return [user.dict() for user in await get_discordbot_users(user_id)]
|
return [user.dict() for user in await get_discordbot_users(user_id)]
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_user(
|
||||||
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
user = await get_discordbot_user(user_id)
|
user = await get_discordbot_user(user_id)
|
||||||
return user.dict()
|
if user:
|
||||||
|
return user.dict()
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
||||||
async def api_discordbot_users_create(
|
async def api_discordbot_users_create(
|
||||||
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
|
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await create_discordbot_user(data)
|
user = await create_discordbot_user(data)
|
||||||
full = user.dict()
|
full = user.dict()
|
||||||
full["wallets"] = [
|
wallets = await get_discordbot_users_wallets(user.id)
|
||||||
wallet.dict() for wallet in await get_discordbot_users_wallets(user.id)
|
if wallets:
|
||||||
]
|
full["wallets"] = [wallet for wallet in wallets]
|
||||||
return full
|
return full
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
||||||
async def api_discordbot_users_delete(
|
async def api_discordbot_users_delete(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await get_discordbot_user(user_id)
|
user = await get_discordbot_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
@ -75,7 +80,7 @@ async def api_discordbot_activate_extension(
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||||
)
|
)
|
||||||
update_user_extension(user_id=userid, extension=extension, active=active)
|
await update_user_extension(user_id=userid, extension=extension, active=active)
|
||||||
return {"extension": "updated"}
|
return {"extension": "updated"}
|
||||||
|
|
||||||
|
|
||||||
@ -84,7 +89,7 @@ async def api_discordbot_activate_extension(
|
|||||||
|
|
||||||
@discordbot_ext.post("/api/v1/wallets")
|
@discordbot_ext.post("/api/v1/wallets")
|
||||||
async def api_discordbot_wallets_create(
|
async def api_discordbot_wallets_create(
|
||||||
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
|
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await create_discordbot_wallet(
|
user = await create_discordbot_wallet(
|
||||||
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
||||||
@ -93,28 +98,30 @@ async def api_discordbot_wallets_create(
|
|||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/wallets")
|
@discordbot_ext.get("/api/v1/wallets")
|
||||||
async def api_discordbot_wallets(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_wallets(
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
):
|
||||||
admin_id = wallet.wallet.user
|
admin_id = wallet.wallet.user
|
||||||
return [wallet.dict() for wallet in await get_discordbot_wallets(admin_id)]
|
return await get_discordbot_wallets(admin_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
||||||
async def api_discordbot_wallet_transactions(
|
async def api_discordbot_wallet_transactions(
|
||||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
return await get_discordbot_wallet_transactions(wallet_id)
|
return await get_discordbot_wallet_transactions(wallet_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
||||||
async def api_discordbot_users_wallets(
|
async def api_discordbot_users_wallets(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)]
|
return await get_discordbot_users_wallets(user_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
||||||
async def api_discordbot_wallets_delete(
|
async def api_discordbot_wallets_delete(
|
||||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
get_wallet = await get_discordbot_wallet(wallet_id)
|
get_wallet = await get_discordbot_wallet(wallet_id)
|
||||||
if not get_wallet:
|
if not get_wallet:
|
||||||
|
@ -12,7 +12,10 @@ templates = Jinja2Templates(directory="templates")
|
|||||||
|
|
||||||
|
|
||||||
@example_ext.get("/", response_class=HTMLResponse)
|
@example_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request,
|
||||||
|
user: User = Depends(check_user_exists), # type: ignore
|
||||||
|
):
|
||||||
return example_renderer().TemplateResponse(
|
return example_renderer().TemplateResponse(
|
||||||
"example/index.html", {"request": request, "user": user.dict()}
|
"example/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
@ -4,10 +4,10 @@ import json
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core import db as core_db
|
from lnbits.core import db as core_db
|
||||||
from lnbits.core.crud import create_payment
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
|
from lnbits.core.services import create_invoice, pay_invoice
|
||||||
from lnbits.tasks import internal_invoice_listener, register_invoice_listener
|
from lnbits.helpers import get_current_extension_name
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_livestream_by_track, get_producer, get_track
|
from .crud import get_livestream_by_track, get_producer, get_track
|
||||||
|
|
||||||
@ -44,44 +44,20 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
# now we make a special kind of internal transfer
|
# now we make a special kind of internal transfer
|
||||||
amount = int(payment.amount * (100 - ls.fee_pct) / 100)
|
amount = int(payment.amount * (100 - ls.fee_pct) / 100)
|
||||||
|
|
||||||
# mark the original payment with two extra keys, "shared_with" and "received"
|
payment_hash, payment_request = await create_invoice(
|
||||||
# (this prevents us from doing this process again and it's informative)
|
wallet_id=tpos.tip_wallet,
|
||||||
# and reduce it by the amount we're going to send to the producer
|
amount=amount, # sats
|
||||||
await core_db.execute(
|
internal=True,
|
||||||
"""
|
|
||||||
UPDATE apipayments
|
|
||||||
SET extra = ?, amount = ?
|
|
||||||
WHERE hash = ?
|
|
||||||
AND checking_id NOT LIKE 'internal_%'
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
json.dumps(
|
|
||||||
dict(
|
|
||||||
**payment.extra,
|
|
||||||
shared_with=[producer.name, producer.id],
|
|
||||||
received=payment.amount,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
payment.amount - amount,
|
|
||||||
payment.payment_hash,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# perform an internal transfer using the same payment_hash to the producer wallet
|
|
||||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
|
||||||
await create_payment(
|
|
||||||
wallet_id=producer.wallet,
|
|
||||||
checking_id=internal_checking_id,
|
|
||||||
payment_request="",
|
|
||||||
payment_hash=payment.payment_hash,
|
|
||||||
amount=amount,
|
|
||||||
memo=f"Revenue from '{track.name}'.",
|
memo=f"Revenue from '{track.name}'.",
|
||||||
pending=False,
|
|
||||||
)
|
)
|
||||||
|
logger.debug(f"livestream: producer invoice created: {payment_hash}")
|
||||||
|
|
||||||
# manually send this for now
|
checking_id = await pay_invoice(
|
||||||
# await internal_invoice_paid.send(internal_checking_id)
|
payment_request=payment_request,
|
||||||
await internal_invoice_listener.put(internal_checking_id)
|
wallet_id=payment.wallet_id,
|
||||||
|
extra={"tag": "livestream"},
|
||||||
|
)
|
||||||
|
logger.debug(f"livestream: producer invoice paid: {checking_id}")
|
||||||
|
|
||||||
# so the flow is the following:
|
# so the flow is the following:
|
||||||
# - we receive, say, 1000 satoshis
|
# - we receive, say, 1000 satoshis
|
||||||
|
@ -102,7 +102,7 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
|
|||||||
charge = await get_charge(charge_id)
|
charge = await get_charge(charge_id)
|
||||||
if not charge.paid:
|
if not charge.paid:
|
||||||
if charge.onchainaddress:
|
if charge.onchainaddress:
|
||||||
config = await get_config(charge.user)
|
config = await get_charge_config(charge_id)
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
@ -122,3 +122,10 @@ async def check_address_balance(charge_id: str) -> List[Charges]:
|
|||||||
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 satspay.charges WHERE id = ?", (charge_id,))
|
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
||||||
return Charges.from_row(row) if row else None
|
return Charges.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_charge_config(charge_id: str):
|
||||||
|
row = await db.fetchone(
|
||||||
|
"""SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,)
|
||||||
|
)
|
||||||
|
return await get_config(row.user)
|
||||||
|
17
lnbits/extensions/satspay/helpers.py
Normal file
17
lnbits/extensions/satspay/helpers.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from .models import Charges
|
||||||
|
|
||||||
|
|
||||||
|
def compact_charge(charge: Charges):
|
||||||
|
return {
|
||||||
|
"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, # should be secret?
|
||||||
|
}
|
@ -19,7 +19,6 @@ class CreateCharge(BaseModel):
|
|||||||
|
|
||||||
class Charges(BaseModel):
|
class Charges(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
user: str
|
|
||||||
description: Optional[str]
|
description: Optional[str]
|
||||||
onchainwallet: Optional[str]
|
onchainwallet: Optional[str]
|
||||||
onchainaddress: Optional[str]
|
onchainaddress: Optional[str]
|
||||||
|
@ -328,7 +328,7 @@
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
checkBalances: async function () {
|
checkBalances: async function () {
|
||||||
if (!this.charge.hasStaleBalance) await this.refreshCharge()
|
if (this.charge.hasStaleBalance) return
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
@ -339,18 +339,9 @@
|
|||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshCharge: async function () {
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/satspay/api/v1/charge/${this.charge.id}`
|
|
||||||
)
|
|
||||||
this.charge = mapCharge(data, this.charge)
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
checkPendingOnchain: async function () {
|
checkPendingOnchain: async function () {
|
||||||
|
if (!this.charge.onchainaddress) return
|
||||||
|
|
||||||
const {
|
const {
|
||||||
bitcoin: {addresses: addressesAPI}
|
bitcoin: {addresses: addressesAPI}
|
||||||
} = mempoolJS({
|
} = mempoolJS({
|
||||||
|
@ -9,10 +9,9 @@ from starlette.responses import HTMLResponse
|
|||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
from lnbits.extensions.watchonly.crud import get_config
|
|
||||||
|
|
||||||
from . import satspay_ext, satspay_renderer
|
from . import satspay_ext, satspay_renderer
|
||||||
from .crud import get_charge
|
from .crud import get_charge, get_charge_config
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ async def display(request: Request, charge_id: str):
|
|||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||||
)
|
)
|
||||||
wallet = await get_wallet(charge.lnbitswallet)
|
wallet = await get_wallet(charge.lnbitswallet)
|
||||||
onchainwallet_config = await get_config(charge.user)
|
onchainwallet_config = await get_charge_config(charge_id)
|
||||||
inkey = wallet.inkey if wallet else None
|
inkey = wallet.inkey if wallet else None
|
||||||
mempool_endpoint = (
|
mempool_endpoint = (
|
||||||
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
||||||
|
@ -20,6 +20,7 @@ from .crud import (
|
|||||||
get_charges,
|
get_charges,
|
||||||
update_charge,
|
update_charge,
|
||||||
)
|
)
|
||||||
|
from .helpers import compact_charge
|
||||||
from .models import CreateCharge
|
from .models import CreateCharge
|
||||||
|
|
||||||
#############################CHARGES##########################
|
#############################CHARGES##########################
|
||||||
@ -123,25 +124,13 @@ async def api_charge_balance(charge_id):
|
|||||||
try:
|
try:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
charge.webhook,
|
charge.webhook,
|
||||||
json={
|
json=compact_charge(charge),
|
||||||
"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,
|
timeout=40,
|
||||||
)
|
)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
charge.webhook = None
|
charge.webhook = None
|
||||||
return {
|
return {
|
||||||
**charge.dict(),
|
**compact_charge(charge),
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
**{"time_elapsed": charge.time_elapsed},
|
||||||
**{"time_left": charge.time_left},
|
**{"time_left": charge.time_left},
|
||||||
**{"paid": charge.paid},
|
**{"paid": charge.paid},
|
||||||
|
@ -14,7 +14,7 @@ class Target(BaseModel):
|
|||||||
class TargetPutList(BaseModel):
|
class TargetPutList(BaseModel):
|
||||||
wallet: str = Query(...)
|
wallet: str = Query(...)
|
||||||
alias: str = Query("")
|
alias: str = Query("")
|
||||||
percent: float = Query(..., ge=0.01)
|
percent: float = Query(..., ge=0.01, lt=100)
|
||||||
|
|
||||||
|
|
||||||
class TargetPut(BaseModel):
|
class TargetPut(BaseModel):
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core import db as core_db
|
|
||||||
from lnbits.core.crud import create_payment
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
|
from lnbits.core.services import create_invoice, pay_invoice
|
||||||
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
|
from lnbits.helpers import get_current_extension_name
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_targets
|
from .crud import get_targets
|
||||||
|
|
||||||
@ -22,60 +20,36 @@ async def wait_for_paid_invoices():
|
|||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if payment.extra.get("tag") == "splitpayments" or payment.extra.get("splitted"):
|
if payment.extra.get("tag") == "splitpayments":
|
||||||
# already splitted, ignore
|
# already a splitted payment, ignore
|
||||||
return
|
return
|
||||||
|
|
||||||
# now we make some special internal transfers (from no one to the receiver)
|
|
||||||
targets = await get_targets(payment.wallet_id)
|
targets = await get_targets(payment.wallet_id)
|
||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
return
|
return
|
||||||
|
|
||||||
transfers = [
|
total_percent = sum([target.percent for target in targets])
|
||||||
(target.wallet, int(target.percent * payment.amount / 100))
|
|
||||||
for target in targets
|
|
||||||
]
|
|
||||||
transfers = [(wallet, amount) for wallet, amount in transfers if amount > 0]
|
|
||||||
amount_left = payment.amount - sum([amount for _, amount in transfers])
|
|
||||||
|
|
||||||
if amount_left < 0:
|
if total_percent > 100:
|
||||||
logger.error(
|
logger.error("splitpayment failure: total percent adds up to more than 100%")
|
||||||
"splitpayments failure: amount_left is negative.", payment.payment_hash
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# mark the original payment with one extra key, "splitted"
|
logger.debug(f"performing split payments to {len(targets)} targets")
|
||||||
# (this prevents us from doing this process again and it's informative)
|
for target in targets:
|
||||||
# and reduce it by the amount we're going to send to the producer
|
amount = int(payment.amount * target.percent / 100) # msats
|
||||||
await core_db.execute(
|
payment_hash, payment_request = await create_invoice(
|
||||||
"""
|
wallet_id=target.wallet,
|
||||||
UPDATE apipayments
|
amount=int(amount / 1000), # sats
|
||||||
SET extra = ?, amount = ?
|
internal=True,
|
||||||
WHERE hash = ?
|
memo=f"split payment: {target.percent}% for {target.alias or target.wallet}",
|
||||||
AND checking_id NOT LIKE 'internal_%'
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
json.dumps(dict(**payment.extra, splitted=True)),
|
|
||||||
amount_left,
|
|
||||||
payment.payment_hash,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# perform the internal transfer using the same payment_hash
|
|
||||||
for wallet, amount in transfers:
|
|
||||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
|
||||||
await create_payment(
|
|
||||||
wallet_id=wallet,
|
|
||||||
checking_id=internal_checking_id,
|
|
||||||
payment_request="",
|
|
||||||
payment_hash=payment.payment_hash,
|
|
||||||
amount=amount,
|
|
||||||
memo=payment.memo,
|
|
||||||
pending=False,
|
|
||||||
extra={"tag": "splitpayments"},
|
extra={"tag": "splitpayments"},
|
||||||
)
|
)
|
||||||
|
logger.debug(f"created split invoice: {payment_hash}")
|
||||||
|
|
||||||
# manually send this for now
|
checking_id = await pay_invoice(
|
||||||
await internal_invoice_queue.put(internal_checking_id)
|
payment_request=payment_request,
|
||||||
return
|
wallet_id=payment.wallet_id,
|
||||||
|
extra={"tag": "splitpayments"},
|
||||||
|
)
|
||||||
|
logger.debug(f"paid split invoice: {checking_id}")
|
||||||
|
@ -3,10 +3,10 @@ from typing import List, Optional, Union
|
|||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import CreateDomain, Domains, Subdomains
|
from .models import CreateDomain, CreateSubdomain, Domains, Subdomains
|
||||||
|
|
||||||
|
|
||||||
async def create_subdomain(payment_hash, wallet, data: CreateDomain) -> Subdomains:
|
async def create_subdomain(payment_hash, wallet, data: CreateSubdomain) -> Subdomains:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
|
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
|
||||||
|
@ -3,24 +3,24 @@ from pydantic.main import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
class CreateDomain(BaseModel):
|
class CreateDomain(BaseModel):
|
||||||
wallet: str = Query(...)
|
wallet: str = Query(...) # type: ignore
|
||||||
domain: str = Query(...)
|
domain: str = Query(...) # type: ignore
|
||||||
cf_token: str = Query(...)
|
cf_token: str = Query(...) # type: ignore
|
||||||
cf_zone_id: str = Query(...)
|
cf_zone_id: str = Query(...) # type: ignore
|
||||||
webhook: str = Query("")
|
webhook: str = Query("") # type: ignore
|
||||||
description: str = Query(..., min_length=0)
|
description: str = Query(..., min_length=0) # type: ignore
|
||||||
cost: int = Query(..., ge=0)
|
cost: int = Query(..., ge=0) # type: ignore
|
||||||
allowed_record_types: str = Query(...)
|
allowed_record_types: str = Query(...) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class CreateSubdomain(BaseModel):
|
class CreateSubdomain(BaseModel):
|
||||||
domain: str = Query(...)
|
domain: str = Query(...) # type: ignore
|
||||||
subdomain: str = Query(...)
|
subdomain: str = Query(...) # type: ignore
|
||||||
email: str = Query(...)
|
email: str = Query(...) # type: ignore
|
||||||
ip: str = Query(...)
|
ip: str = Query(...) # type: ignore
|
||||||
sats: int = Query(..., ge=0)
|
sats: int = Query(..., ge=0) # type: ignore
|
||||||
duration: int = Query(...)
|
duration: int = Query(...) # type: ignore
|
||||||
record_type: str = Query(...)
|
record_type: str = Query(...) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Domains(BaseModel):
|
class Domains(BaseModel):
|
||||||
|
@ -20,7 +20,7 @@ async def wait_for_paid_invoices():
|
|||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if payment.extra.get("tag") != "lnsubdomain":
|
if not payment.extra or payment.extra.get("tag") != "lnsubdomain":
|
||||||
# not an lnurlp invoice
|
# not an lnurlp invoice
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
### Use webhook to notify about cloudflare registration
|
### Use webhook to notify about cloudflare registration
|
||||||
if domain.webhook:
|
if domain and domain.webhook:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
|
@ -16,7 +16,9 @@ templates = Jinja2Templates(directory="templates")
|
|||||||
|
|
||||||
|
|
||||||
@subdomains_ext.get("/", response_class=HTMLResponse)
|
@subdomains_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request, user: User = Depends(check_user_exists) # type:ignore
|
||||||
|
):
|
||||||
return subdomains_renderer().TemplateResponse(
|
return subdomains_renderer().TemplateResponse(
|
||||||
"subdomains/index.html", {"request": request, "user": user.dict()}
|
"subdomains/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
@ -29,12 +29,15 @@ from .crud import (
|
|||||||
|
|
||||||
@subdomains_ext.get("/api/v1/domains")
|
@subdomains_ext.get("/api/v1/domains")
|
||||||
async def api_domains(
|
async def api_domains(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
g: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
all_wallets: bool = Query(False),
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
if user is not None:
|
||||||
|
wallet_ids = user.wallet_ids
|
||||||
|
|
||||||
return [domain.dict() for domain in await get_domains(wallet_ids)]
|
return [domain.dict() for domain in await get_domains(wallet_ids)]
|
||||||
|
|
||||||
@ -42,7 +45,9 @@ async def api_domains(
|
|||||||
@subdomains_ext.post("/api/v1/domains")
|
@subdomains_ext.post("/api/v1/domains")
|
||||||
@subdomains_ext.put("/api/v1/domains/{domain_id}")
|
@subdomains_ext.put("/api/v1/domains/{domain_id}")
|
||||||
async def api_domain_create(
|
async def api_domain_create(
|
||||||
data: CreateDomain, domain_id=None, g: WalletTypeInfo = Depends(get_key_type)
|
data: CreateDomain,
|
||||||
|
domain_id=None,
|
||||||
|
g: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
):
|
):
|
||||||
if domain_id:
|
if domain_id:
|
||||||
domain = await get_domain(domain_id)
|
domain = await get_domain(domain_id)
|
||||||
@ -63,7 +68,9 @@ async def api_domain_create(
|
|||||||
|
|
||||||
|
|
||||||
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
|
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
|
||||||
async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)):
|
async def api_domain_delete(
|
||||||
|
domain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
domain = await get_domain(domain_id)
|
domain = await get_domain(domain_id)
|
||||||
|
|
||||||
if not domain:
|
if not domain:
|
||||||
@ -82,12 +89,14 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
|
|||||||
|
|
||||||
@subdomains_ext.get("/api/v1/subdomains")
|
@subdomains_ext.get("/api/v1/subdomains")
|
||||||
async def api_subdomains(
|
async def api_subdomains(
|
||||||
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type)
|
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
if user is not None:
|
||||||
|
wallet_ids = user.wallet_ids
|
||||||
|
|
||||||
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
|
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
|
||||||
|
|
||||||
@ -173,7 +182,9 @@ async def api_subdomain_send_subdomain(payment_hash):
|
|||||||
|
|
||||||
|
|
||||||
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
|
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
|
||||||
async def api_subdomain_delete(subdomain_id, g: WalletTypeInfo = Depends(get_key_type)):
|
async def api_subdomain_delete(
|
||||||
|
subdomain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
subdomain = await get_subdomain(subdomain_id)
|
subdomain = await get_subdomain(subdomain_id)
|
||||||
|
|
||||||
if not subdomain:
|
if not subdomain:
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
|
|
||||||
from lnbits.core import db as core_db
|
from loguru import logger
|
||||||
from lnbits.core.crud import create_payment
|
|
||||||
from lnbits.core.models import Payment
|
from lnbits.core.models import Payment
|
||||||
from lnbits.helpers import get_current_extension_name, urlsafe_short_hash
|
from lnbits.core.services import create_invoice, pay_invoice
|
||||||
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
|
from lnbits.helpers import get_current_extension_name
|
||||||
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
from .crud import get_tpos
|
from .crud import get_tpos
|
||||||
|
|
||||||
@ -20,11 +20,9 @@ async def wait_for_paid_invoices():
|
|||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if payment.extra.get("tag") == "tpos" and payment.extra.get("tipSplitted"):
|
if payment.extra.get("tag") != "tpos":
|
||||||
# already splitted, ignore
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# now we make some special internal transfers (from no one to the receiver)
|
|
||||||
tpos = await get_tpos(payment.extra.get("tposId"))
|
tpos = await get_tpos(payment.extra.get("tposId"))
|
||||||
tipAmount = payment.extra.get("tipAmount")
|
tipAmount = payment.extra.get("tipAmount")
|
||||||
|
|
||||||
@ -32,39 +30,17 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||||||
# no tip amount
|
# no tip amount
|
||||||
return
|
return
|
||||||
|
|
||||||
tipAmount = tipAmount * 1000
|
payment_hash, payment_request = await create_invoice(
|
||||||
amount = payment.amount - tipAmount
|
|
||||||
|
|
||||||
# mark the original payment with one extra key, "splitted"
|
|
||||||
# (this prevents us from doing this process again and it's informative)
|
|
||||||
# and reduce it by the amount we're going to send to the producer
|
|
||||||
await core_db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE apipayments
|
|
||||||
SET extra = ?, amount = ?
|
|
||||||
WHERE hash = ?
|
|
||||||
AND checking_id NOT LIKE 'internal_%'
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
json.dumps(dict(**payment.extra, tipSplitted=True)),
|
|
||||||
amount,
|
|
||||||
payment.payment_hash,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# perform the internal transfer using the same payment_hash
|
|
||||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
|
||||||
await create_payment(
|
|
||||||
wallet_id=tpos.tip_wallet,
|
wallet_id=tpos.tip_wallet,
|
||||||
checking_id=internal_checking_id,
|
amount=int(tipAmount), # sats
|
||||||
payment_request="",
|
internal=True,
|
||||||
payment_hash=payment.payment_hash,
|
memo=f"tpos tip",
|
||||||
amount=tipAmount,
|
|
||||||
memo=f"Tip for {payment.memo}",
|
|
||||||
pending=False,
|
|
||||||
extra={"tipSplitted": True},
|
|
||||||
)
|
)
|
||||||
|
logger.debug(f"tpos: tip invoice created: {payment_hash}")
|
||||||
|
|
||||||
# manually send this for now
|
checking_id = await pay_invoice(
|
||||||
await internal_invoice_queue.put(internal_checking_id)
|
payment_request=payment_request,
|
||||||
return
|
wallet_id=payment.wallet_id,
|
||||||
|
extra={"tag": "tpos"},
|
||||||
|
)
|
||||||
|
logger.debug(f"tpos: tip invoice paid: {checking_id}")
|
||||||
|
@ -10,7 +10,7 @@ from .models import Address, Config, WalletAccount
|
|||||||
##########################WALLETS####################
|
##########################WALLETS####################
|
||||||
|
|
||||||
|
|
||||||
async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
|
async def create_watch_wallet(user: str, w: WalletAccount) -> WalletAccount:
|
||||||
wallet_id = urlsafe_short_hash()
|
wallet_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
@ -30,7 +30,7 @@ async def create_watch_wallet(w: WalletAccount) -> WalletAccount:
|
|||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
wallet_id,
|
wallet_id,
|
||||||
w.user,
|
user,
|
||||||
w.masterpub,
|
w.masterpub,
|
||||||
w.fingerprint,
|
w.fingerprint,
|
||||||
w.title,
|
w.title,
|
||||||
|
@ -14,7 +14,6 @@ class CreateWallet(BaseModel):
|
|||||||
|
|
||||||
class WalletAccount(BaseModel):
|
class WalletAccount(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
user: str
|
|
||||||
masterpub: str
|
masterpub: str
|
||||||
fingerprint: str
|
fingerprint: str
|
||||||
title: str
|
title: str
|
||||||
|
@ -86,7 +86,6 @@ async def api_wallet_create_or_update(
|
|||||||
|
|
||||||
new_wallet = WalletAccount(
|
new_wallet = WalletAccount(
|
||||||
id="none",
|
id="none",
|
||||||
user=w.wallet.user,
|
|
||||||
masterpub=data.masterpub,
|
masterpub=data.masterpub,
|
||||||
fingerprint=descriptor.keys[0].fingerprint.hex(),
|
fingerprint=descriptor.keys[0].fingerprint.hex(),
|
||||||
type=descriptor.scriptpubkey_type(),
|
type=descriptor.scriptpubkey_type(),
|
||||||
@ -115,7 +114,7 @@ async def api_wallet_create_or_update(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
wallet = await create_watch_wallet(new_wallet)
|
wallet = await create_watch_wallet(w.wallet.user, new_wallet)
|
||||||
|
|
||||||
await api_get_addresses(wallet.id, w)
|
await api_get_addresses(wallet.id, w)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
from functools import partial
|
|
||||||
from typing import Callable, List, Optional
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from urllib.request import parse_http_list as _parse_list_header
|
|
||||||
|
|
||||||
from quart import Request
|
|
||||||
from quart_trio.asgi import TrioASGIHTTPConnection
|
|
||||||
from werkzeug.datastructures import Headers
|
|
||||||
|
|
||||||
|
|
||||||
class ASGIProxyFix(TrioASGIHTTPConnection):
|
|
||||||
def _create_request_from_scope(self, send: Callable) -> Request:
|
|
||||||
headers = Headers()
|
|
||||||
headers["Remote-Addr"] = (self.scope.get("client") or ["<local>"])[0]
|
|
||||||
for name, value in self.scope["headers"]:
|
|
||||||
headers.add(name.decode("latin1").title(), value.decode("latin1"))
|
|
||||||
if self.scope["http_version"] < "1.1":
|
|
||||||
headers.setdefault("Host", self.app.config["SERVER_NAME"] or "")
|
|
||||||
|
|
||||||
path = self.scope["path"]
|
|
||||||
path = path if path[0] == "/" else urlparse(path).path
|
|
||||||
|
|
||||||
x_proto = self._get_real_value(1, headers.get("X-Forwarded-Proto"))
|
|
||||||
if x_proto:
|
|
||||||
self.scope["scheme"] = x_proto
|
|
||||||
|
|
||||||
x_host = self._get_real_value(1, headers.get("X-Forwarded-Host"))
|
|
||||||
if x_host:
|
|
||||||
headers["host"] = x_host.lower()
|
|
||||||
|
|
||||||
return self.app.request_class(
|
|
||||||
self.scope["method"],
|
|
||||||
self.scope["scheme"],
|
|
||||||
path,
|
|
||||||
self.scope["query_string"],
|
|
||||||
headers,
|
|
||||||
self.scope.get("root_path", ""),
|
|
||||||
self.scope["http_version"],
|
|
||||||
max_content_length=self.app.config["MAX_CONTENT_LENGTH"],
|
|
||||||
body_timeout=self.app.config["BODY_TIMEOUT"],
|
|
||||||
send_push_promise=partial(self._send_push_promise, send),
|
|
||||||
scope=self.scope,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_real_value(self, trusted: int, value: Optional[str]) -> Optional[str]:
|
|
||||||
"""Get the real value from a list header based on the configured
|
|
||||||
number of trusted proxies.
|
|
||||||
:param trusted: Number of values to trust in the header.
|
|
||||||
:param value: Comma separated list header value to parse.
|
|
||||||
:return: The real value, or ``None`` if there are fewer values
|
|
||||||
than the number of trusted proxies.
|
|
||||||
.. versionchanged:: 1.0
|
|
||||||
Renamed from ``_get_trusted_comma``.
|
|
||||||
.. versionadded:: 0.15
|
|
||||||
"""
|
|
||||||
if not (trusted and value):
|
|
||||||
return None
|
|
||||||
|
|
||||||
values = self.parse_list_header(value)
|
|
||||||
if len(values) >= trusted:
|
|
||||||
return values[-trusted]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def parse_list_header(self, value: str) -> List[str]:
|
|
||||||
result = []
|
|
||||||
for item in _parse_list_header(value):
|
|
||||||
if item[:1] == item[-1:] == '"':
|
|
||||||
item = self.unquote_header_value(item[1:-1])
|
|
||||||
result.append(item)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def unquote_header_value(self, value: str, is_filename: bool = False) -> str:
|
|
||||||
r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
|
|
||||||
This does not use the real unquoting but what browsers are actually
|
|
||||||
using for quoting.
|
|
||||||
.. versionadded:: 0.5
|
|
||||||
:param value: the header value to unquote.
|
|
||||||
:param is_filename: The value represents a filename or path.
|
|
||||||
"""
|
|
||||||
if value and value[0] == value[-1] == '"':
|
|
||||||
# this is not the real unquoting, but fixing this so that the
|
|
||||||
# RFC is met will result in bugs with internet explorer and
|
|
||||||
# probably some other browsers as well. IE for example is
|
|
||||||
# uploading files with "C:\foo\bar.txt" as filename
|
|
||||||
value = value[1:-1]
|
|
||||||
|
|
||||||
# if this is a filename and the starting characters look like
|
|
||||||
# a UNC path, then just return the value without quotes. Using the
|
|
||||||
# replace sequence below on a UNC path has the effect of turning
|
|
||||||
# the leading double slash into a single slash and then
|
|
||||||
# _fix_ie_filename() doesn't work correctly. See #458.
|
|
||||||
if not is_filename or value[:2] != "\\\\":
|
|
||||||
return value.replace("\\\\", "\\").replace('\\"', '"')
|
|
||||||
return value
|
|
@ -89,8 +89,34 @@ profile = "black"
|
|||||||
ignore_missing_imports = "True"
|
ignore_missing_imports = "True"
|
||||||
files = "lnbits"
|
files = "lnbits"
|
||||||
exclude = """(?x)(
|
exclude = """(?x)(
|
||||||
^lnbits/extensions.
|
^lnbits/extensions/bleskomat.
|
||||||
| ^lnbits/wallets/lnd_grpc_files.
|
| ^lnbits/extensions/boltz.
|
||||||
|
| ^lnbits/extensions/boltcards.
|
||||||
|
| ^lnbits/extensions/events.
|
||||||
|
| ^lnbits/extensions/hivemind.
|
||||||
|
| ^lnbits/extensions/invoices.
|
||||||
|
| ^lnbits/extensions/jukebox.
|
||||||
|
| ^lnbits/extensions/livestream.
|
||||||
|
| ^lnbits/extensions/lnaddress.
|
||||||
|
| ^lnbits/extensions/lndhub.
|
||||||
|
| ^lnbits/extensions/lnticket.
|
||||||
|
| ^lnbits/extensions/lnurldevice.
|
||||||
|
| ^lnbits/extensions/lnurlp.
|
||||||
|
| ^lnbits/extensions/lnurlpayout.
|
||||||
|
| ^lnbits/extensions/ngrok.
|
||||||
|
| ^lnbits/extensions/offlineshop.
|
||||||
|
| ^lnbits/extensions/paywall.
|
||||||
|
| ^lnbits/extensions/satsdice.
|
||||||
|
| ^lnbits/extensions/satspay.
|
||||||
|
| ^lnbits/extensions/scrub.
|
||||||
|
| ^lnbits/extensions/splitpayments.
|
||||||
|
| ^lnbits/extensions/streamalerts.
|
||||||
|
| ^lnbits/extensions/tipjar.
|
||||||
|
| ^lnbits/extensions/tpos.
|
||||||
|
| ^lnbits/extensions/usermanager.
|
||||||
|
| ^lnbits/extensions/watchonly.
|
||||||
|
| ^lnbits/extensions/withdraw.
|
||||||
|
| ^lnbits/wallets/lnd_grpc_files.
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user