Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
3f131a7
feat: add `webhook` path
motorina0 May 28, 2025
154f10a
feat: basic functionality for `stripe`
motorina0 May 29, 2025
09a4344
feat: add fiat providers
motorina0 May 29, 2025
e7c59a7
feat: add pay with stripe button
motorina0 May 29, 2025
bc161ba
feat: select payment provider
motorina0 May 29, 2025
16c6845
feat: create checkout session
motorina0 May 29, 2025
6b57bc7
refactor: extract `amount_cents`
motorina0 May 30, 2025
84fb36a
feat: show stripe payment request
motorina0 May 30, 2025
4490673
feat: check payment with stripe
motorina0 May 30, 2025
7ea1417
fix: lint
motorina0 May 30, 2025
b796274
fix: error message
motorina0 May 30, 2025
1c8b097
refactor: simplify logic
motorina0 May 30, 2025
c5f3c48
feat: extra checks for paid incoming invoice
motorina0 May 30, 2025
6e884c7
chore: code clean-up
motorina0 May 30, 2025
b59c604
fix: notify for successful invoices
motorina0 May 30, 2025
6b4dda9
fix: the previous fix
motorina0 May 30, 2025
ddb38e6
fix: rever tasks logic
motorina0 May 30, 2025
f7d197b
feat: notify listeners when fiat payment is successfull
motorina0 May 30, 2025
31c9348
fix: var name
motorina0 May 30, 2025
a013b5f
feat: handle `stripe` notifications
motorina0 May 30, 2025
77fe046
chore: add todo
motorina0 Jun 2, 2025
d41df01
refactor: more generic fiat providers
motorina0 Jun 2, 2025
70cb58a
refactor: rename classes
motorina0 Jun 2, 2025
fe608c1
feat: check if stripe is enabled
motorina0 Jun 2, 2025
c610018
feat: add fiat providers tab
motorina0 Jun 2, 2025
9d9079f
feat: stripe placeholder settings
motorina0 Jun 2, 2025
74ff39b
feat: settings UI for `stripe`
motorina0 Jun 2, 2025
a777944
refactor: rename vars
motorina0 Jun 2, 2025
0ffcec6
feat: prepare to test connection
motorina0 Jun 2, 2025
0d6ce01
feat: extra properties
motorina0 Jun 2, 2025
966b106
feat: polish settings UI
motorina0 Jun 2, 2025
baff1fa
feat: add check buttons
motorina0 Jun 3, 2025
85adc51
feat: useful label
motorina0 Jun 3, 2025
37a8fbe
feat: check fit provider
motorina0 Jun 4, 2025
23d2dac
feat: check stale connection
motorina0 Jun 6, 2025
84deb9c
feat: add faucet wallet setting an UI
motorina0 Jun 6, 2025
d9c1e59
feat: only show enabled fiat providers
motorina0 Jun 6, 2025
8239da5
refactor: extract `_api_payments_create_fiat_invoice `
motorina0 Jun 6, 2025
7004611
refactor: simplify signature
motorina0 Jun 6, 2025
6dfc72d
refactor: extract functions to services
motorina0 Jun 6, 2025
ada2485
feat: use `get_fiat_provider()`
motorina0 Jun 12, 2025
6f2d12e
refactor: rename base classes
motorina0 Jun 12, 2025
bb9ca5b
refactor: simplify stale connection check
motorina0 Jun 12, 2025
f88409b
feat: better webhook secplainer
motorina0 Jun 12, 2025
e3c44b7
refactor: param order
motorina0 Jun 12, 2025
62f42bd
feat: check fiat payment limits
motorina0 Jun 12, 2025
c6cdf7e
fix: preserve payment fee
motorina0 Jun 12, 2025
3db2862
fix: default value for `fiat_provider`
motorina0 Jun 12, 2025
27f67f0
refactor: use FiatStatu
motorina0 Jun 12, 2025
f12482d
feat: add `handle_fiat_payment_confirmation`
motorina0 Jun 13, 2025
0919f80
fix: credit fiat fee wallet
motorina0 Jun 13, 2025
e761abb
chore: better error message
motorina0 Jun 13, 2025
c7a1556
feat: disable webhook URL input
motorina0 Jun 13, 2025
aa22787
feat: deduct from faucet wallet
motorina0 Jun 13, 2025
a703847
doc: extra description for fiat wallet
motorina0 Jun 13, 2025
dafd8d8
refactor: small changes
motorina0 Jun 18, 2025
dbce21b
test: add basic tests
motorina0 Jun 18, 2025
226c33e
test: add basic tests
motorina0 Jun 18, 2025
60d04b1
test: bypass weird github rule?
motorina0 Jun 18, 2025
39084df
test: limits validation
motorina0 Jun 18, 2025
597bb77
refactor: reorder tests
motorina0 Jun 18, 2025
2c1eb7f
test: fiat provider create invoice failed
motorina0 Jun 18, 2025
ebc7122
test: service_fee_fiat
motorina0 Jun 18, 2025
1e8f383
test: failed invoice exits
motorina0 Jun 18, 2025
3316c87
refactor: file rename
motorina0 Jun 18, 2025
9e687cc
test: add more assertions
motorina0 Jun 18, 2025
0a05032
test: fiat status update
motorina0 Jun 18, 2025
4757a4a
chore: clean-up
motorina0 Jun 18, 2025
ead4d42
refactor: rename webhook to callback
motorina0 Jun 18, 2025
76ffd75
test: allowed users
motorina0 Jun 18, 2025
23b33ed
chore: code format
motorina0 Jun 18, 2025
3db0657
test: api for fiat_provider
motorina0 Jun 18, 2025
7789ce1
test: add tests for faucet
motorina0 Jun 20, 2025
e8194e1
fet: better explanation for the webhook
motorina0 Jun 20, 2025
e236110
feat: check stripe signature
motorina0 Jun 20, 2025
36ca14d
refactor: rename test file
motorina0 Jun 20, 2025
c2221ad
test: check stripe signature (100% github copilot)
motorina0 Jun 20, 2025
1504561
refactor: introduce the `fiat_` prefix
motorina0 Jun 23, 2025
70debe6
refactor: clean-up
motorina0 Jun 23, 2025
8c2d48b
test: extra logging
motorina0 Jun 23, 2025
0da2bc4
refactor: less indent
motorina0 Jun 23, 2025
55afeff
chore: extra log
motorina0 Jun 23, 2025
3f7c333
test: small diferentiators
motorina0 Jun 23, 2025
400a081
fix: ids
motorina0 Jun 23, 2025
53cc75c
chore: extra logs
motorina0 Jun 23, 2025
eca59ca
fix: faucet payment
motorina0 Jun 23, 2025
d87bc28
chore: code clean-up
motorina0 Jun 23, 2025
ab36735
chore: extra log
motorina0 Jun 23, 2025
04e6a2d
test: enable stripe for api test
motorina0 Jun 23, 2025
f21c8b7
chore: code clean-up
motorina0 Jun 23, 2025
040d16b
chore: make bundle
motorina0 Jun 25, 2025
cc4f7f6
refactor: move `UnsupportedError`
motorina0 Jun 30, 2025
b9bb46c
refactor: rename `FiatWallet` to `FiatProvider`
motorina0 Jun 30, 2025
54f5353
chore: code clean-up
motorina0 Jun 30, 2025
82514ce
refactor: extract `normalize_endpoint` to `helpers`
motorina0 Jun 30, 2025
87e018f
refactor: rename function
motorina0 Jun 30, 2025
9a0899e
fix: typo
motorina0 Jun 30, 2025
28ad815
chore: make bundle
motorina0 Jun 30, 2025
8f3b8ce
refactor: rename package
motorina0 Jun 30, 2025
cdc8ff2
refactor: rename var
motorina0 Jun 30, 2025
188fb3a
feat: dense fiat provider list
motorina0 Jun 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lnbits/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from .views.api import api_router
from .views.audit_api import audit_router
from .views.auth_api import auth_router
from .views.callback_api import callback_router
from .views.extension_api import extension_router
from .views.fiat_api import fiat_router

# this compat is needed for usermanager extension
from .views.generic import generic_router
Expand Down Expand Up @@ -34,10 +36,12 @@ def init_core_routers(app: FastAPI):
app.include_router(wallet_router)
app.include_router(api_router)
app.include_router(websocket_router)
app.include_router(callback_router)
app.include_router(tinyurl_router)
app.include_router(webpush_router)
app.include_router(users_router)
app.include_router(audit_router)
app.include_router(fiat_router)


__all__ = ["core_app", "core_app_extra", "db"]
1 change: 1 addition & 0 deletions lnbits/core/crud/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ async def get_user_from_account(
wallets=wallets,
admin=account.is_admin,
super_user=account.is_super_user,
fiat_providers=account.fiat_providers,
has_password=account.password_hash is not None,
)

Expand Down
4 changes: 4 additions & 0 deletions lnbits/core/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,7 @@ async def m032_add_external_id_to_accounts(db: Connection):
Used for external account linking.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN external_id TEXT")


async def m033_update_payment_table(db: Connection):
await db.execute("ALTER TABLE apipayments ADD COLUMN fiat_provider TEXT")
56 changes: 54 additions & 2 deletions lnbits/core/models/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
from pydantic import BaseModel, Field, validator

from lnbits.db import FilterModel
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.utils.exchange_rates import allowed_currencies
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import (
Expand Down Expand Up @@ -60,6 +67,8 @@ class Payment(BaseModel):
amount: int
fee: int
bolt11: str
# payment_request: str | None
fiat_provider: str | None = None
status: str = PaymentState.PENDING
memo: str | None = None
expiry: datetime | None = None
Expand Down Expand Up @@ -107,14 +116,23 @@ def is_expired(self) -> bool:

@property
def is_internal(self) -> bool:
return self.checking_id.startswith("internal_")
return self.checking_id.startswith("internal_") or self.checking_id.startswith(
"fiat_"
Copy link
Member

@dni dni Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does is_internal also check if its fiat?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fiat payments are always internal payments, but I wanted to have an explicit prefix for it

)

async def check_status(self) -> PaymentStatus:
async def check_status(
self, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
if self.is_internal:
if self.success:
return PaymentSuccessStatus()
if self.failed:
return PaymentFailedStatus()
if self.is_in and self.fiat_provider:
fiat_status = await self.check_fiat_status(
skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if self.is_out:
Expand All @@ -123,6 +141,39 @@ async def check_status(self) -> PaymentStatus:
status = await funding_source.get_invoice_status(self.checking_id)
return status

async def check_fiat_status(
self, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
if not self.is_internal:
return FiatPaymentPendingStatus()
if self.success:
return FiatPaymentSuccessStatus()
if self.failed:
return FiatPaymentFailedStatus()

if not self.fiat_provider:
return FiatPaymentPendingStatus()

checking_id = self.extra.get("fiat_checking_id")
if not checking_id:
return FiatPaymentPendingStatus()

fiat_provider = await get_fiat_provider(self.fiat_provider)
if not fiat_provider:
return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id)

if skip_internal_payment_notifications:
return fiat_status

if fiat_status.success:
# notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue

await internal_invoice_queue.put(self.checking_id)

return fiat_status


class PaymentFilters(FilterModel):
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
Expand Down Expand Up @@ -206,6 +257,7 @@ class CreateInvoice(BaseModel):
webhook: str | None = None
bolt11: str | None = None
lnurl_callback: str | None = None
fiat_provider: str | None = None

@validator("unit")
@classmethod
Expand Down
3 changes: 3 additions & 0 deletions lnbits/core/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,13 @@ class Account(BaseModel):

is_super_user: bool = Field(default=False, no_database=True)
is_admin: bool = Field(default=False, no_database=True)
fiat_providers: list[str] = Field(default=[], no_database=True)

def __init__(self, **data):
super().__init__(**data)
self.is_super_user = settings.is_super_user(self.id)
self.is_admin = settings.is_admin_user(self.id)
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)

def hash_password(self, password: str) -> str:
"""sets and returns the hashed password"""
Expand Down Expand Up @@ -191,6 +193,7 @@ class User(BaseModel):
wallets: list[Wallet] = []
admin: bool = False
super_user: bool = False
fiat_providers: list[str] = []
has_password: bool = False
extra: UserExtra = UserExtra()

Expand Down
8 changes: 8 additions & 0 deletions lnbits/core/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
calculate_fiat_amounts,
check_transaction_status,
check_wallet_limits,
create_fiat_invoice,
create_invoice,
create_wallet_invoice,
fee_reserve,
fee_reserve_total,
get_payments_daily_stats,
pay_invoice,
service_fee,
update_pending_payment,
update_pending_payments,
update_wallet_balance,
)
Expand Down Expand Up @@ -44,10 +48,14 @@
"check_transaction_status",
"check_wallet_limits",
"create_invoice",
"create_wallet_invoice",
"create_fiat_invoice",
"fee_reserve",
"fee_reserve_total",
"get_payments_daily_stats",
"pay_invoice",
"service_fee",
"update_pending_payment",
"update_pending_payments",
"update_wallet_balance",
# settings
Expand Down
92 changes: 92 additions & 0 deletions lnbits/core/services/fiat_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import hashlib
import hmac
import time
from typing import Optional

from loguru import logger

from lnbits.core.crud.payments import get_standalone_payment
from lnbits.core.models.misc import SimpleStatus
from lnbits.fiat import get_fiat_provider


async def handle_stripe_event(event: dict):
event_id = event.get("id")
event_object = event.get("data", {}).get("object", {})
object_type = event_object.get("object")
payment_hash = event_object.get("metadata", {}).get("payment_hash")
logger.debug(
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
f" Payment hash: '{payment_hash}'."
)
if not payment_hash:
logger.warning("Stripe event does not contain a payment hash.")
return

payment = await get_standalone_payment(payment_hash)
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await payment.check_fiat_status()


def check_stripe_signature(
payload: bytes,
sig_header: Optional[str],
secret: Optional[str],
tolerance_seconds=300,
):
if not sig_header:
logger.warning("Stripe-Signature header is missing.")
raise ValueError("Stripe-Signature header is missing.")

if not secret:
logger.warning("Stripe webhook signing secret is not set.")
raise ValueError("Stripe webhook cannot be verified.")

# Split the Stripe-Signature header
items = dict(i.split("=") for i in sig_header.split(","))
timestamp = int(items["t"])
signature = items["v1"]

# Check timestamp tolerance
if abs(time.time() - timestamp) > tolerance_seconds:
logger.warning("Timestamp outside tolerance.")
logger.debug(
f"Current time: {time.time()}, "
f"Timestamp: {timestamp}, "
f"Tolerance: {tolerance_seconds} seconds"
)

raise ValueError("Timestamp outside tolerance." f"Timestamp: {timestamp}")

signed_payload = f"{timestamp}.{payload.decode()}"

# Compute HMAC SHA256 using the webhook secret
computed_signature = hmac.new(
key=secret.encode(), msg=signed_payload.encode(), digestmod=hashlib.sha256
).hexdigest()

# Compare signatures using constant time comparison
if hmac.compare_digest(computed_signature, signature) is not True:
logger.warning("Stripe signature verification failed.")
raise ValueError("Stripe signature verification failed.")


async def test_connection(provider: str) -> SimpleStatus:
"""
Test the connection to Stripe by checking if the API key is valid.
This function should be called when setting up or testing the Stripe integration.
"""
fiat_provider = await get_fiat_provider(provider)
status = await fiat_provider.status()
if status.error_message:
return SimpleStatus(
success=False,
message=f"Cconnection test failed: {status.error_message}",
)

return SimpleStatus(
success=True,
message="Connection test successful." f" Balance: {status.balance}.",
)
Loading