mirror of
https://github.com/lnbits/lnbits.git
synced 2025-10-10 20:42:32 +02:00
feat: initial support for bolt12
rebase of #3092 special thanks to @21M4TW
This commit is contained in:
225
lnbits/core/crud/offers.py
Normal file
225
lnbits/core/crud/offers.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
from time import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from lnbits.core.db import db
|
||||||
|
from lnbits.core.models.offers import (
|
||||||
|
CreateOffer,
|
||||||
|
Offer,
|
||||||
|
OfferFilters,
|
||||||
|
OffersStatusCount,
|
||||||
|
OfferState,
|
||||||
|
)
|
||||||
|
from lnbits.db import Connection, Filters, Page
|
||||||
|
|
||||||
|
|
||||||
|
async def get_offer(offer_id: str, conn: Optional[Connection] = None) -> Offer:
|
||||||
|
return await (conn or db).fetchone(
|
||||||
|
"SELECT * FROM apioffers WHERE offer_id = :offer_id",
|
||||||
|
{"offer_id": offer_id},
|
||||||
|
Offer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_standalone_offer(
|
||||||
|
offer_id: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
wallet_id: Optional[str] = None,
|
||||||
|
) -> Optional[Offer]:
|
||||||
|
clause: str = "offer_id = :offer_id"
|
||||||
|
values = {
|
||||||
|
"wallet_id": wallet_id,
|
||||||
|
"offer_id": offer_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if wallet_id:
|
||||||
|
clause = f"({clause}) AND wallet_id = :wallet_id"
|
||||||
|
|
||||||
|
row = await (conn or db).fetchone(
|
||||||
|
"""
|
||||||
|
SELECT * FROM apioffers
|
||||||
|
WHERE {clause}
|
||||||
|
""",
|
||||||
|
values,
|
||||||
|
Offer,
|
||||||
|
)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
async def get_offers_paginated(
|
||||||
|
*,
|
||||||
|
wallet_id: Optional[str] = None,
|
||||||
|
active: Optional[bool] = None,
|
||||||
|
single_use: Optional[bool] = None,
|
||||||
|
since: Optional[int] = None,
|
||||||
|
filters: Optional[Filters[OfferFilters]] = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> Page[Offer]:
|
||||||
|
"""
|
||||||
|
Filters offers to be returned by:
|
||||||
|
- active | single_use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
values: dict = {
|
||||||
|
"wallet_id": wallet_id,
|
||||||
|
"created_at": since,
|
||||||
|
}
|
||||||
|
clause: list[str] = []
|
||||||
|
|
||||||
|
if since is not None:
|
||||||
|
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||||
|
|
||||||
|
if wallet_id:
|
||||||
|
clause.append("wallet_id = :wallet_id")
|
||||||
|
|
||||||
|
if active is not None:
|
||||||
|
clause.append("active = :active")
|
||||||
|
|
||||||
|
if single_use is not None:
|
||||||
|
clause.append("single_use = :single_use")
|
||||||
|
|
||||||
|
return await (conn or db).fetch_page(
|
||||||
|
"SELECT * FROM apioffers",
|
||||||
|
clause,
|
||||||
|
values,
|
||||||
|
filters=filters,
|
||||||
|
model=Offer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_offers(
|
||||||
|
*,
|
||||||
|
wallet_id: Optional[str] = None,
|
||||||
|
active: Optional[bool] = None,
|
||||||
|
single_use: Optional[bool] = None,
|
||||||
|
since: Optional[int] = None,
|
||||||
|
filters: Optional[Filters[OfferFilters]] = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
offset: Optional[int] = None,
|
||||||
|
) -> list[Offer]:
|
||||||
|
"""
|
||||||
|
Filters offers to be returned by active | single_use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filters = filters or Filters()
|
||||||
|
|
||||||
|
filters.sortby = filters.sortby or "time"
|
||||||
|
filters.direction = filters.direction or "desc"
|
||||||
|
filters.limit = limit or filters.limit
|
||||||
|
filters.offset = offset or filters.offset
|
||||||
|
|
||||||
|
page = await get_offers_paginated(
|
||||||
|
wallet_id=wallet_id,
|
||||||
|
active=active,
|
||||||
|
single_use=single_use,
|
||||||
|
since=since,
|
||||||
|
filters=filters,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
|
return page.data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_offers_status_count() -> OffersStatusCount:
|
||||||
|
empty_page: Filters = Filters(limit=0)
|
||||||
|
active_offers = await get_offers_paginated(active=True, filters=empty_page)
|
||||||
|
single_use_offers = await get_offers_paginated(single_use=True, filters=empty_page)
|
||||||
|
|
||||||
|
return OffersStatusCount(
|
||||||
|
active=active_offers.total,
|
||||||
|
single_use=single_use_offers.total,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_expired_offers(
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> None:
|
||||||
|
# We delete all offers whose expiry date is in the past
|
||||||
|
await (conn or db).execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM apioffers
|
||||||
|
WHERE expiry < {db.timestamp_placeholder("now")}
|
||||||
|
""",
|
||||||
|
{"now": int(time())},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_offer(
|
||||||
|
offer_id: str,
|
||||||
|
data: CreateOffer,
|
||||||
|
active: OfferState,
|
||||||
|
single_use: OfferState,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> Offer:
|
||||||
|
# we don't allow the creation of the same offer twice
|
||||||
|
# note: this can be removed if the db uniqueness constraints are set appropriately
|
||||||
|
previous_offer = await get_standalone_offer(offer_id, conn=conn)
|
||||||
|
assert previous_offer is None, "Offer already exists"
|
||||||
|
extra = data.extra or {}
|
||||||
|
|
||||||
|
offer = Offer(
|
||||||
|
offer_id=offer_id,
|
||||||
|
wallet_id=data.wallet_id,
|
||||||
|
amount=data.amount_msat,
|
||||||
|
active=active,
|
||||||
|
single_use=single_use,
|
||||||
|
bolt12=data.bolt12,
|
||||||
|
memo=data.memo,
|
||||||
|
expiry=data.expiry,
|
||||||
|
webhook=data.webhook,
|
||||||
|
tag=extra.get("tag", None),
|
||||||
|
extra=extra,
|
||||||
|
)
|
||||||
|
print(offer)
|
||||||
|
|
||||||
|
await (conn or db).insert("apioffers", offer)
|
||||||
|
|
||||||
|
return offer
|
||||||
|
|
||||||
|
|
||||||
|
async def update_offer(
|
||||||
|
offer: Offer,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> None:
|
||||||
|
await (conn or db).update("apioffers", offer, "WHERE offer_id = :offer_id")
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_wallet_offer(
|
||||||
|
offer_id: str, wallet_id: str, conn: Optional[Connection] = None
|
||||||
|
) -> None:
|
||||||
|
await (conn or db).execute(
|
||||||
|
"DELETE FROM apioffers WHERE offer_id = :offer_id AND wallet = :wallet",
|
||||||
|
{"offer_id": offer_id, "wallet": wallet_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def enable_offer(offer_id: str) -> None:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE apioffers
|
||||||
|
SET active = :active, updated_at = {db.timestamp_placeholder("now")}
|
||||||
|
WHERE offer_id = :offer_id
|
||||||
|
""",
|
||||||
|
{"now": time(), "offer_id": offer_id, "active": OfferState.TRUE},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def disable_offer(offer_id: str) -> None:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE apioffers
|
||||||
|
SET active = :active, updated_at = {db.timestamp_placeholder("now")}
|
||||||
|
WHERE offer_id = :offer_id
|
||||||
|
""",
|
||||||
|
{"now": time(), "offer_id": offer_id, "active": OfferState.FALSE},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_webhook_offer_sent(offer_id: str, status: int) -> None:
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE apioffers SET webhook_status = :status
|
||||||
|
WHERE offer_id = :offer_id
|
||||||
|
""",
|
||||||
|
{"status": status, "offer_id": offer_id},
|
||||||
|
)
|
@@ -275,7 +275,9 @@ async def create_payment(
|
|||||||
payment_hash=data.payment_hash,
|
payment_hash=data.payment_hash,
|
||||||
bolt11=data.bolt11,
|
bolt11=data.bolt11,
|
||||||
amount=data.amount_msat,
|
amount=data.amount_msat,
|
||||||
|
offer_id=data.offer_id,
|
||||||
memo=data.memo,
|
memo=data.memo,
|
||||||
|
payer_note=data.payer_note,
|
||||||
preimage=data.preimage,
|
preimage=data.preimage,
|
||||||
expiry=data.expiry,
|
expiry=data.expiry,
|
||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
|
@@ -735,3 +735,31 @@ async def m032_add_external_id_to_accounts(db: Connection):
|
|||||||
|
|
||||||
async def m033_update_payment_table(db: Connection):
|
async def m033_update_payment_table(db: Connection):
|
||||||
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m034_create_offer_table(db: Connection):
|
||||||
|
await db.execute("ALTER TABLE apipayments ADD COLUMN payer_note TEXT")
|
||||||
|
await db.execute("ALTER TABLE apipayments ADD COLUMN offer_id TEXT")
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS apioffers (
|
||||||
|
offer_id TEXT NOT NULL,
|
||||||
|
amount INT NOT NULL,
|
||||||
|
wallet_id TEXT NOT NULL,
|
||||||
|
memo TEXT,
|
||||||
|
bolt12 TEXT,
|
||||||
|
extra TEXT,
|
||||||
|
webhook TEXT,
|
||||||
|
webhook_status TEXT,
|
||||||
|
expiry TIMESTAMP,
|
||||||
|
active INT NOT NULL DEFAULT 0 CHECK(ABS(active)<=1),
|
||||||
|
single_use INT NOT NULL DEFAULT 0 CHECK(ABS(single_use)<=1),
|
||||||
|
tag TEXT,
|
||||||
|
extension TEXT,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
UNIQUE (wallet_id, offer_id)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
await db.execute("CREATE INDEX by_offer ON apioffers (offer_id)")
|
||||||
|
100
lnbits/core/models/offers.py
Normal file
100
lnbits/core/models/offers.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from lnbits.db import FilterModel
|
||||||
|
from lnbits.wallets import get_funding_source
|
||||||
|
from lnbits.wallets.base import (
|
||||||
|
OfferStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OfferState(int, Enum):
|
||||||
|
TRUE = 1
|
||||||
|
FALSE = 0
|
||||||
|
|
||||||
|
def __int__(self) -> int:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class CreateOffer(BaseModel):
|
||||||
|
wallet_id: str
|
||||||
|
bolt12: str
|
||||||
|
amount_msat: int
|
||||||
|
memo: str
|
||||||
|
extra: dict | None = {}
|
||||||
|
expiry: datetime | None = None
|
||||||
|
webhook: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Offer(BaseModel):
|
||||||
|
offer_id: str
|
||||||
|
wallet_id: str
|
||||||
|
amount: int
|
||||||
|
active: int
|
||||||
|
single_use: int
|
||||||
|
bolt12: str
|
||||||
|
memo: str | None = None
|
||||||
|
expiry: datetime | None = None
|
||||||
|
webhook: str | None = None
|
||||||
|
webhook_status: str | None = None
|
||||||
|
tag: str | None = None
|
||||||
|
extension: str | None = None
|
||||||
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
extra: dict = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self) -> bool:
|
||||||
|
return not self.active == OfferState.FALSE.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_inactive(self) -> bool:
|
||||||
|
return self.active == OfferState.FALSE.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_single_use(self) -> bool:
|
||||||
|
return not self.single_use == OfferState.FALSE.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_multiple_use(self) -> bool:
|
||||||
|
return self.single_use == OfferState.FALSE.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def msat(self) -> int:
|
||||||
|
return self.amount
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sat(self) -> int:
|
||||||
|
return self.amount // 1000
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
return self.expiry < datetime.now(timezone.utc) if self.expiry else False
|
||||||
|
|
||||||
|
async def check_status(self) -> OfferStatus:
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
status = await funding_source.get_offer_status(self.offer_id)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
class OfferFilters(FilterModel):
|
||||||
|
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "active", "single_use"]
|
||||||
|
|
||||||
|
__sort_fields__ = ["created_at", "amount", "fee", "memo", "tag"]
|
||||||
|
|
||||||
|
active: int | None
|
||||||
|
single_use: int | None
|
||||||
|
tag: str | None
|
||||||
|
offer_id: str | None
|
||||||
|
amount: int
|
||||||
|
memo: str | None
|
||||||
|
wallet_id: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class OffersStatusCount(BaseModel):
|
||||||
|
active: int = 0
|
||||||
|
single_use: int = 0
|
@@ -52,10 +52,14 @@ class CreatePayment(BaseModel):
|
|||||||
payment_hash: str
|
payment_hash: str
|
||||||
bolt11: str
|
bolt11: str
|
||||||
amount_msat: int
|
amount_msat: int
|
||||||
|
offer_id: str | None = None
|
||||||
memo: str
|
memo: str
|
||||||
|
payer_note: str | None = None
|
||||||
extra: dict | None = {}
|
extra: dict | None = {}
|
||||||
preimage: str | None = None
|
preimage: str | None = None
|
||||||
expiry: datetime | None = None
|
expiry: datetime | None = None
|
||||||
|
created_at: datetime | None = None
|
||||||
|
updated_at: datetime | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
fee: int = 0
|
fee: int = 0
|
||||||
|
|
||||||
@@ -70,7 +74,9 @@ class Payment(BaseModel):
|
|||||||
# payment_request: str | None
|
# payment_request: str | None
|
||||||
fiat_provider: str | None = None
|
fiat_provider: str | None = None
|
||||||
status: str = PaymentState.PENDING
|
status: str = PaymentState.PENDING
|
||||||
|
offer_id: str | None = None
|
||||||
memo: str | None = None
|
memo: str | None = None
|
||||||
|
payer_note: str | None = None
|
||||||
expiry: datetime | None = None
|
expiry: datetime | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
webhook_status: str | None = None
|
webhook_status: str | None = None
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import secrets
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -16,7 +17,7 @@ from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
|||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
from lnbits.exceptions import InvoiceError, OfferError, PaymentError, UnsupportedError
|
||||||
from lnbits.fiat import get_fiat_provider
|
from lnbits.fiat import get_fiat_provider
|
||||||
from lnbits.helpers import check_callback_url
|
from lnbits.helpers import check_callback_url
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -42,8 +43,19 @@ from ..crud import (
|
|||||||
is_internal_status_success,
|
is_internal_status_success,
|
||||||
update_payment,
|
update_payment,
|
||||||
)
|
)
|
||||||
|
from ..crud import (
|
||||||
|
create_offer as crud_create_offer,
|
||||||
|
)
|
||||||
|
from ..crud import (
|
||||||
|
disable_offer as crud_disable_offer,
|
||||||
|
)
|
||||||
|
from ..crud import (
|
||||||
|
enable_offer as crud_enable_offer,
|
||||||
|
)
|
||||||
from ..models import (
|
from ..models import (
|
||||||
|
CreateOffer,
|
||||||
CreatePayment,
|
CreatePayment,
|
||||||
|
Offer,
|
||||||
Payment,
|
Payment,
|
||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -54,6 +66,155 @@ payment_lock = asyncio.Lock()
|
|||||||
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def create_offer(
|
||||||
|
*,
|
||||||
|
wallet_id: str,
|
||||||
|
amount_sat: int,
|
||||||
|
memo: str,
|
||||||
|
absolute_expiry: Optional[int] = None,
|
||||||
|
single_use: Optional[bool] = None,
|
||||||
|
extra: Optional[dict] = None,
|
||||||
|
webhook: Optional[str] = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> Offer:
|
||||||
|
if amount_sat < 0:
|
||||||
|
raise OfferError("Offers with negative amounts are not valid.", status="failed")
|
||||||
|
|
||||||
|
user_wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not user_wallet:
|
||||||
|
raise OfferError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
|
||||||
|
offer_memo = memo[:640]
|
||||||
|
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
|
||||||
|
# How should this be handled with offers?
|
||||||
|
if amount_sat > settings.lnbits_max_incoming_payment_amount_sats:
|
||||||
|
raise OfferError(
|
||||||
|
f"Offer amount {amount_sat} sats is too high. Max allowed: "
|
||||||
|
f"{settings.lnbits_max_incoming_payment_amount_sats} sats.",
|
||||||
|
status="failed",
|
||||||
|
)
|
||||||
|
if settings.is_wallet_max_balance_exceeded(
|
||||||
|
user_wallet.balance_msat / 1000 + amount_sat
|
||||||
|
):
|
||||||
|
raise OfferError(
|
||||||
|
f"Wallet balance cannot exceed "
|
||||||
|
f"{settings.lnbits_wallet_limit_max_balance} sats.",
|
||||||
|
status="failed",
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
offer_resp = await funding_source.create_offer(
|
||||||
|
amount=amount_sat,
|
||||||
|
issuer=secrets.token_hex(8),
|
||||||
|
memo=offer_memo,
|
||||||
|
absolute_expiry=absolute_expiry,
|
||||||
|
single_use=single_use,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not offer_resp.ok or not offer_resp.invoice_offer or not offer_resp.offer_id:
|
||||||
|
raise OfferError(
|
||||||
|
offer_resp.error_message or "unexpected backend error.",
|
||||||
|
status="pending",
|
||||||
|
)
|
||||||
|
|
||||||
|
if offer_resp.created:
|
||||||
|
break
|
||||||
|
|
||||||
|
create_offer_model = CreateOffer(
|
||||||
|
wallet_id=wallet_id,
|
||||||
|
bolt12=offer_resp.invoice_offer,
|
||||||
|
amount_msat=amount_sat * 1000,
|
||||||
|
memo=memo,
|
||||||
|
extra=extra,
|
||||||
|
expiry=absolute_expiry,
|
||||||
|
webhook=webhook,
|
||||||
|
)
|
||||||
|
|
||||||
|
offer = await crud_create_offer(
|
||||||
|
offer_id=offer_resp.offer_id,
|
||||||
|
data=create_offer_model,
|
||||||
|
active=offer_resp.active,
|
||||||
|
single_use=offer_resp.single_use,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
|
||||||
|
return offer
|
||||||
|
|
||||||
|
|
||||||
|
async def enable_offer(
|
||||||
|
*,
|
||||||
|
wallet_id: str,
|
||||||
|
offer_id: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
user_wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not user_wallet:
|
||||||
|
raise OfferError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
|
||||||
|
offer_resp = await funding_source.enable_offer(offer_id=offer_id)
|
||||||
|
|
||||||
|
if not offer_resp.ok:
|
||||||
|
raise OfferError(
|
||||||
|
offer_resp.error_message or "unexpected backend error.", status="pending"
|
||||||
|
)
|
||||||
|
|
||||||
|
if offer_resp.created:
|
||||||
|
|
||||||
|
if (
|
||||||
|
not offer_resp.invoice_offer
|
||||||
|
or not offer_resp.offer_id == offer_id
|
||||||
|
or not offer_resp.active
|
||||||
|
):
|
||||||
|
raise OfferError(
|
||||||
|
offer_resp.error_message or "unexpected backend state.", status="error"
|
||||||
|
)
|
||||||
|
|
||||||
|
await crud_enable_offer(offer_resp.offer_id)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def disable_offer(
|
||||||
|
*,
|
||||||
|
wallet_id: str,
|
||||||
|
offer_id: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
user_wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not user_wallet:
|
||||||
|
raise OfferError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
|
||||||
|
offer_resp = await funding_source.disable_offer(offer_id=offer_id)
|
||||||
|
|
||||||
|
if not offer_resp.ok:
|
||||||
|
raise OfferError(
|
||||||
|
offer_resp.error_message or "unexpected backend error.", status="pending"
|
||||||
|
)
|
||||||
|
|
||||||
|
if offer_resp.created:
|
||||||
|
|
||||||
|
if (
|
||||||
|
not offer_resp.invoice_offer
|
||||||
|
or not offer_resp.offer_id == offer_id
|
||||||
|
or offer_resp.active
|
||||||
|
):
|
||||||
|
raise OfferError(
|
||||||
|
offer_resp.error_message or "unexpected backend state.", status="error"
|
||||||
|
)
|
||||||
|
|
||||||
|
await crud_disable_offer(offer_resp.offer_id)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
*,
|
*,
|
||||||
wallet_id: str,
|
wallet_id: str,
|
||||||
|
@@ -14,6 +14,12 @@ from lnbits.settings import settings
|
|||||||
from .helpers import path_segments, template_renderer
|
from .helpers import path_segments, template_renderer
|
||||||
|
|
||||||
|
|
||||||
|
class OfferError(Exception):
|
||||||
|
def __init__(self, message: str, status: str = "pending"):
|
||||||
|
self.message = message
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
|
||||||
class PaymentError(Exception):
|
class PaymentError(Exception):
|
||||||
def __init__(self, message: str, status: str = "pending"):
|
def __init__(self, message: str, status: str = "pending"):
|
||||||
self.message = message
|
self.message = message
|
||||||
|
@@ -14,7 +14,9 @@ from lnbits.core.crud import (
|
|||||||
update_payment,
|
update_payment,
|
||||||
)
|
)
|
||||||
from lnbits.core.models import Payment, PaymentState
|
from lnbits.core.models import Payment, PaymentState
|
||||||
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
|
from lnbits.core.services.fiat_providers import (
|
||||||
|
handle_fiat_payment_confirmation,
|
||||||
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
|
|
||||||
@@ -191,3 +193,66 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
|
|||||||
for name, send_chan in invoice_listeners.items():
|
for name, send_chan in invoice_listeners.items():
|
||||||
logger.trace(f"invoice listeners: sending to `{name}`")
|
logger.trace(f"invoice listeners: sending to `{name}`")
|
||||||
await send_chan.put(payment)
|
await send_chan.put(payment)
|
||||||
|
|
||||||
|
# if payment:
|
||||||
|
# if payment.is_in:
|
||||||
|
# status = await payment.check_status()
|
||||||
|
# payment.fee = status.fee_msat or 0
|
||||||
|
# payment.preimage = status.preimage
|
||||||
|
# payment.status = PaymentState.SUCCESS
|
||||||
|
# await update_payment(payment)
|
||||||
|
# internal = "internal" if is_internal else ""
|
||||||
|
# logger.success(f"{internal} invoice {checking_id} settled")
|
||||||
|
# for name, send_chan in invoice_listeners.items():
|
||||||
|
# logger.trace(f"invoice listeners: sending to `{name}`")
|
||||||
|
# await send_chan.put(payment)
|
||||||
|
# else:
|
||||||
|
# funding_source = get_funding_source()
|
||||||
|
# data = await funding_source.get_bolt12_invoice_main_data(checking_id)
|
||||||
|
# logger.debug(f"Returned main invoice data is {data}")
|
||||||
|
|
||||||
|
# if data and data.success:
|
||||||
|
# logger.success(f"Bolt12
|
||||||
|
# data successfully recovered for invoice {checking_id}")
|
||||||
|
# offer = await get_standalone_offer(data.offer_id)
|
||||||
|
|
||||||
|
# if offer:
|
||||||
|
# logger.success(f"Offer {data.offer_id} was found in db")
|
||||||
|
# description = data.description or f"Offer {data.offer_id} payment"
|
||||||
|
# invoice = Bolt11(
|
||||||
|
# currency="bc",
|
||||||
|
# amount_msat=data.amount_msat,
|
||||||
|
# date=data.created_at,
|
||||||
|
# tags=Tags.from_dict(
|
||||||
|
# {
|
||||||
|
# "payment_hash": data.payment_hash,
|
||||||
|
# "payment_secret": data.payment_preimage,
|
||||||
|
# "description": description,
|
||||||
|
# "expire_time": data.expires_at-data.created_at,
|
||||||
|
# } ),
|
||||||
|
# )
|
||||||
|
# privkey = fake_privkey(settings.fake_wallet_secret)
|
||||||
|
# bolt11 = bolt11_encode(invoice, privkey)
|
||||||
|
|
||||||
|
# create_payment_model = CreatePayment(
|
||||||
|
# wallet_id=offer.wallet_id,
|
||||||
|
# bolt11=bolt11,
|
||||||
|
# payment_hash=data.payment_hash,
|
||||||
|
# preimage=data.payment_preimage,
|
||||||
|
# amount_msat=data.amount_msat,
|
||||||
|
# offer_id=data.offer_id,
|
||||||
|
# expiry=data.expires_at,
|
||||||
|
# created_at=data.created_at,
|
||||||
|
# updated_at=data.paid_at,
|
||||||
|
# memo=description,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# payment = await create_payment(
|
||||||
|
# checking_id=checking_id,
|
||||||
|
# data=create_payment_model,
|
||||||
|
# status = PaymentState.SUCCESS
|
||||||
|
# )
|
||||||
|
# logger.success(f"invoice {checking_id} settled")
|
||||||
|
# for name, send_chan in invoice_listeners.items():
|
||||||
|
# logger.trace(f"invoice listeners: sending to `{name}`")
|
||||||
|
# await send_chan.put(payment)
|
||||||
|
@@ -26,6 +26,91 @@ class StatusResponse(NamedTuple):
|
|||||||
balance_msat: int
|
balance_msat: int
|
||||||
|
|
||||||
|
|
||||||
|
class OfferResponse(NamedTuple):
|
||||||
|
ok: bool
|
||||||
|
offer_id: str | None = None
|
||||||
|
active: bool | None = None
|
||||||
|
single_use: bool | None = None
|
||||||
|
invoice_offer: str | None = None
|
||||||
|
used: bool | None = None
|
||||||
|
created: bool | None = None
|
||||||
|
label: str | None = None
|
||||||
|
error_message: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.ok is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.ok is not True
|
||||||
|
|
||||||
|
|
||||||
|
class OfferStatus(NamedTuple):
|
||||||
|
active: bool | None = None
|
||||||
|
used: bool | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active(self) -> bool:
|
||||||
|
return self.active is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def used(self) -> bool:
|
||||||
|
return self.used is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error(self) -> bool:
|
||||||
|
return self.active is None
|
||||||
|
|
||||||
|
|
||||||
|
class OfferErrorStatus(OfferStatus):
|
||||||
|
active = None
|
||||||
|
used = None
|
||||||
|
|
||||||
|
|
||||||
|
class FetchInvoiceResponse(NamedTuple):
|
||||||
|
ok: bool
|
||||||
|
payment_request: str | None = None
|
||||||
|
error_message: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.ok is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.ok is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.ok is False
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceMainData(NamedTuple):
|
||||||
|
paid: bool | None = None
|
||||||
|
payment_hash: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
payer_note: str | None = None
|
||||||
|
amount_msat: int | None = None
|
||||||
|
offer_id: str | None = None
|
||||||
|
expires_at: int | None = None
|
||||||
|
created_at: int | None = None
|
||||||
|
paid_at: int | None = None
|
||||||
|
payment_preimage: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.paid is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pending(self) -> bool:
|
||||||
|
return self.paid is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self) -> bool:
|
||||||
|
return self.paid is False
|
||||||
|
|
||||||
|
|
||||||
class InvoiceResponse(NamedTuple):
|
class InvoiceResponse(NamedTuple):
|
||||||
ok: bool
|
ok: bool
|
||||||
checking_id: str | None = None # payment_hash, rpc_id
|
checking_id: str | None = None # payment_hash, rpc_id
|
||||||
@@ -176,6 +261,11 @@ class Wallet(ABC):
|
|||||||
message="Hold invoices are not supported by this wallet.", status="failed"
|
message="Hold invoices are not supported by this wallet.", status="failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_bolt12_invoice_main_data(
|
||||||
|
self, checking_id: str
|
||||||
|
) -> InvoiceMainData | None:
|
||||||
|
return None
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
for invoice in self.pending_invoices:
|
for invoice in self.pending_invoices:
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import random
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
@@ -15,7 +16,12 @@ from lnbits.utils.crypto import random_secret_and_hash
|
|||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
Feature,
|
Feature,
|
||||||
|
FetchInvoiceResponse,
|
||||||
|
InvoiceMainData,
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
|
OfferErrorStatus,
|
||||||
|
OfferResponse,
|
||||||
|
OfferStatus,
|
||||||
PaymentFailedStatus,
|
PaymentFailedStatus,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
@@ -89,6 +95,218 @@ class CoreLightningWallet(Wallet):
|
|||||||
logger.warning(f"Failed to connect, got: '{exc}'")
|
logger.warning(f"Failed to connect, got: '{exc}'")
|
||||||
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
|
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
|
||||||
|
|
||||||
|
async def create_offer(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
issuer: Optional[str] = None,
|
||||||
|
absolute_expiry: Optional[int] = None,
|
||||||
|
single_use: Optional[bool] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> OfferResponse:
|
||||||
|
label = kwargs.get("label", f"lbl{random.random()}") # noqa: S311
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"amount": int(amount * 1000) if amount > 0 else "any",
|
||||||
|
"description": memo,
|
||||||
|
"issuer": issuer,
|
||||||
|
"label": label,
|
||||||
|
"absolute_expiry": absolute_expiry,
|
||||||
|
"single_use": single_use,
|
||||||
|
}
|
||||||
|
r: dict = self.ln.call("offer", payload)
|
||||||
|
|
||||||
|
if r.get("code") and r.get("code") < 0: # type: ignore
|
||||||
|
raise Exception(r.get("message"))
|
||||||
|
|
||||||
|
return OfferResponse(
|
||||||
|
True,
|
||||||
|
r["offer_id"],
|
||||||
|
r["active"],
|
||||||
|
r["single_use"],
|
||||||
|
r["bolt12"],
|
||||||
|
r["used"],
|
||||||
|
r["created"],
|
||||||
|
r["label"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
except RpcError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
||||||
|
return OfferResponse(
|
||||||
|
False, None, None, None, None, None, None, None, error_message
|
||||||
|
)
|
||||||
|
except KeyError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return OfferResponse(
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"Server error: 'missing required fields'",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
return OfferResponse(
|
||||||
|
False, None, None, None, None, None, None, None, str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def enable_offer(
|
||||||
|
self,
|
||||||
|
offer_id: str,
|
||||||
|
) -> OfferResponse:
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"offer_id": offer_id,
|
||||||
|
}
|
||||||
|
r: dict = self.ln.call("enableoffer", payload)
|
||||||
|
|
||||||
|
if r.get("code"):
|
||||||
|
|
||||||
|
if r.get("code") == 1006:
|
||||||
|
return OfferResponse(
|
||||||
|
True, None, None, None, None, None, False, None, None
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # type: ignore
|
||||||
|
raise Exception(r.get("message"))
|
||||||
|
|
||||||
|
return OfferResponse(
|
||||||
|
True,
|
||||||
|
r["offer_id"],
|
||||||
|
r["active"],
|
||||||
|
r["single_use"],
|
||||||
|
r["bolt12"],
|
||||||
|
r["used"],
|
||||||
|
True,
|
||||||
|
r["label"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
except RpcError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
||||||
|
return OfferResponse(
|
||||||
|
False, None, None, None, None, None, None, None, error_message
|
||||||
|
)
|
||||||
|
except KeyError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return OfferResponse(
|
||||||
|
False, None, None, None, "Server error: 'missing required fields'"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
return OfferResponse(
|
||||||
|
False, None, None, None, None, None, None, None, str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def disable_offer(
|
||||||
|
self,
|
||||||
|
offer_id: str,
|
||||||
|
) -> OfferResponse:
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"offer_id": offer_id,
|
||||||
|
}
|
||||||
|
r: dict = self.ln.call("disableoffer", payload)
|
||||||
|
|
||||||
|
if r.get("code"):
|
||||||
|
|
||||||
|
if r.get("code") == 1001:
|
||||||
|
return OfferResponse(
|
||||||
|
True, None, None, None, None, None, False, None, None
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # type: ignore
|
||||||
|
raise Exception(r.get("message"))
|
||||||
|
|
||||||
|
return OfferResponse(
|
||||||
|
True,
|
||||||
|
r["offer_id"],
|
||||||
|
r["active"],
|
||||||
|
r["single_use"],
|
||||||
|
r["bolt12"],
|
||||||
|
r["used"],
|
||||||
|
True,
|
||||||
|
r["label"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
except RpcError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
||||||
|
return OfferResponse(
|
||||||
|
False, None, None, None, None, None, None, None, error_message
|
||||||
|
)
|
||||||
|
except KeyError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return OfferResponse(
|
||||||
|
False, None, None, None, "Server error: 'missing required fields'"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
return OfferResponse(
|
||||||
|
False, None, None, None, None, None, None, None, str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_offer_status(
|
||||||
|
self, offer_id: str, active_only: bool = False
|
||||||
|
) -> OfferStatus:
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"offer_id": offer_id,
|
||||||
|
"active_only": active_only,
|
||||||
|
}
|
||||||
|
r: dict = self.ln.call("listoffers", payload)
|
||||||
|
|
||||||
|
if not r["offers"]:
|
||||||
|
return OfferErrorStatus()
|
||||||
|
|
||||||
|
offer_resp = r["offers"][-1]
|
||||||
|
|
||||||
|
if offer_resp["offer_id"] == offer_id:
|
||||||
|
return OfferStatus(offer_resp["active"], offer_resp["used"])
|
||||||
|
else:
|
||||||
|
logger.warning(f"supplied an invalid offer_id: {offer_id}")
|
||||||
|
return OfferErrorStatus()
|
||||||
|
except RpcError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return OfferErrorStatus()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return OfferErrorStatus()
|
||||||
|
|
||||||
|
async def fetch_invoice(
|
||||||
|
self,
|
||||||
|
offer_id: str,
|
||||||
|
amount: Optional[int] = None,
|
||||||
|
) -> FetchInvoiceResponse:
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"offer": offer_id,
|
||||||
|
"amount_msat": int(amount * 1000) if amount else None,
|
||||||
|
}
|
||||||
|
r: dict = self.ln.call("fetchinvoice", payload)
|
||||||
|
|
||||||
|
if r.get("code") and r.get("code") < 0: # type: ignore
|
||||||
|
raise Exception(r.get("message"))
|
||||||
|
|
||||||
|
return FetchInvoiceResponse(True, r["invoice"], None)
|
||||||
|
except RpcError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
||||||
|
return FetchInvoiceResponse(False, None, error_message)
|
||||||
|
except KeyError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return FetchInvoiceResponse(
|
||||||
|
False, None, "Server error: 'missing required fields'"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
return FetchInvoiceResponse(False, None, str(e))
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
self,
|
self,
|
||||||
amount: int,
|
amount: int,
|
||||||
@@ -205,6 +423,70 @@ class CoreLightningWallet(Wallet):
|
|||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(error_message=f"Payment failed: '{exc}'.")
|
return PaymentResponse(error_message=f"Payment failed: '{exc}'.")
|
||||||
|
|
||||||
|
async def get_bolt12_invoice_main_data(
|
||||||
|
self, checking_id: str
|
||||||
|
) -> Optional[InvoiceMainData]:
|
||||||
|
try:
|
||||||
|
r: dict = self.ln.listinvoices(payment_hash=checking_id)
|
||||||
|
|
||||||
|
if not r["invoices"]:
|
||||||
|
raise Exception(f"Invoice with checking_id {checking_id}")
|
||||||
|
|
||||||
|
invoice_resp = r["invoices"][-1]
|
||||||
|
logger.debug(f"Returned invoice response is {invoice_resp}")
|
||||||
|
|
||||||
|
if invoice_resp["payment_hash"] != checking_id:
|
||||||
|
raise Exception(f"Supplied an invalid checking_id: {checking_id}")
|
||||||
|
|
||||||
|
if not invoice_resp["bolt12"]:
|
||||||
|
raise Exception("Invoice does not contain bolt12 data")
|
||||||
|
|
||||||
|
payload = {"string": invoice_resp["bolt12"]}
|
||||||
|
r2: dict = self.ln.call("decode", payload)
|
||||||
|
|
||||||
|
logger.debug(f"Returned decoded bolt12 invoice is {r2}")
|
||||||
|
|
||||||
|
if not r2["type"] == "bolt12 invoice":
|
||||||
|
raise Exception("Provided string is not a bolt12 invoice")
|
||||||
|
|
||||||
|
if r2["valid"] is False:
|
||||||
|
raise Exception("Provided bolt12 invoice is invalid")
|
||||||
|
|
||||||
|
if invoice_resp["status"] == "paid":
|
||||||
|
return InvoiceMainData(
|
||||||
|
paid=True,
|
||||||
|
payment_hash=invoice_resp["payment_hash"],
|
||||||
|
description=invoice_resp.get("description"),
|
||||||
|
payer_note=r.get("invreq_payer_note"),
|
||||||
|
amount_msat=invoice_resp["amount_msat"],
|
||||||
|
offer_id=invoice_resp["local_offer_id"],
|
||||||
|
expires_at=invoice_resp["expires_at"],
|
||||||
|
created_at=r["invoice_created_at"],
|
||||||
|
paid_at=invoice_resp["paid_at"],
|
||||||
|
payment_preimage=invoice_resp["payment_preimage"],
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return InvoiceMainData(
|
||||||
|
paid=None if invoice_resp["status"] == "unpaid" else False,
|
||||||
|
payment_hash=invoice_resp["payment_hash"],
|
||||||
|
description=invoice_resp.get("description"),
|
||||||
|
payer_note=r.get("invreq_payer_note"),
|
||||||
|
amount_msat=invoice_resp["amount_msat"],
|
||||||
|
offer_id=invoice_resp["local_offer_id"],
|
||||||
|
expires_at=invoice_resp["expires_at"],
|
||||||
|
created_at=r["invoice_created_at"],
|
||||||
|
paid_at=None,
|
||||||
|
payment_preimage=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
except RpcError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return None
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
r: dict = self.ln.listinvoices(payment_hash=checking_id) # type: ignore
|
r: dict = self.ln.listinvoices(payment_hash=checking_id) # type: ignore
|
||||||
|
34
tests/regtest/test_services_create_offer.py
Normal file
34
tests/regtest/test_services_create_offer.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from lnbits.core.services import create_offer, disable_offer
|
||||||
|
from lnbits.wallets import get_funding_source, set_funding_source
|
||||||
|
from lnbits.wallets.base import OfferStatus
|
||||||
|
|
||||||
|
description = "test create offer"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_offer():
|
||||||
|
set_funding_source("CoreLightningWallet")
|
||||||
|
|
||||||
|
invoice_offer = await create_offer(
|
||||||
|
wallet_id="10c8094f9d1c4ab4b9e3dfe964527297",
|
||||||
|
amount_sat=1000,
|
||||||
|
memo=description,
|
||||||
|
single_use=True,
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
offer = decode(invoice_offer.bolt12)
|
||||||
|
assert offer.amount_msat == 1000000
|
||||||
|
assert offer.description == description
|
||||||
|
"""
|
||||||
|
|
||||||
|
await disable_offer(
|
||||||
|
wallet_id="10c8094f9d1c4ab4b9e3dfe964527297", offer_id=invoice_offer.offer_id
|
||||||
|
)
|
||||||
|
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
status = await funding_source.get_offer_status(invoice_offer.offer_id)
|
||||||
|
assert isinstance(status, OfferStatus)
|
||||||
|
assert not status.active
|
||||||
|
assert not status.used
|
Reference in New Issue
Block a user