mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2024-11-19 01:43:42 +01:00
feat: Blink funding source (#2477)
* feat: Blink funding source * chore: make bundle * Blink review 01 (#2575) * refactor: mark `graphql_query` as private (`_` prefix) * feat: set default value for `blink_api_endpoint` * fix: raise if HTTP call failed * refactor: move private method to the bottom * refactor: make `wallet_id` a property * fix: key mapping for attribute * chore: fix `mypy` * chore: fix `make check` * refactor: extract query strings * refactor: extract `BlinkGrafqlQueries` class * chore: code clean-up * chore: add `try-catch` * refactor: extract `tx_query` * chore: format grapfhql queries * fix: set funding source class * chore: `make format` * fix: test by following the other patterns * Update docs/guide/wallets.md Co-authored-by: openoms <43343391+openoms@users.noreply.github.com> * feat: add websocket connection to blink (#2577) * feat: add websocket connection to blink * feat: close websocket on shutdown * feat: add `blink_ws_endpoint` to the UI * fix: use `SEND` tx for `settlementFee` * refactor: remove `else` when `if` has `return` * fix: remove test env file --------- Co-authored-by: bitkarrot <73979971+bitkarrot@users.noreply.github.com> Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com> Co-authored-by: openoms <43343391+openoms@users.noreply.github.com>
This commit is contained in:
parent
eda7e35c61
commit
7298c4664b
@ -28,7 +28,7 @@ PORT=5000
|
||||
######################################
|
||||
|
||||
# which fundingsources are allowed in the admin ui
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet"
|
||||
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet"
|
||||
|
||||
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||
@ -91,6 +91,11 @@ ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
|
||||
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
|
||||
ZBD_API_KEY=ZBD_ACCESS_TOKEN
|
||||
|
||||
# BlinkWallet
|
||||
BLINK_API_ENDPOINT=https://api.blink.sv/graphql
|
||||
BLINK_WS_ENDPOINT=wss://ws.blink.sv/graphql
|
||||
BLINK_TOKEN=BLINK_TOKEN
|
||||
|
||||
# PhoenixdWallet
|
||||
PHOENIXD_API_ENDPOINT=http://localhost:9740/
|
||||
PHOENIXD_API_PASSWORD=PHOENIXD_KEY
|
||||
|
@ -76,6 +76,15 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
|
||||
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
||||
- `OPENNODE_KEY`: opennodeAdminApiKey
|
||||
|
||||
### Blink
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate a Blink API key after logging in or creating a new Blink account at: https://dashboard.blink.sv. For more info visit: https://dev.blink.sv/api/auth#create-an-api-key```
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **BlinkWallet**
|
||||
- `BLINK_API_ENDPOINT`: https://api.blink.sv/graphql
|
||||
- `BLINK_WS_ENDPOINT`: wss://ws.blink.sv/graphql
|
||||
- `BLINK_TOKEN`: BlinkToken
|
||||
|
||||
### Alby
|
||||
|
||||
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an alby access token here: https://getalby.com/developer/access_tokens/new
|
||||
|
@ -218,6 +218,12 @@ class LnPayFundingSource(LNbitsSettings):
|
||||
lnpay_admin_key: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class BlinkFundingSource(LNbitsSettings):
|
||||
blink_api_endpoint: Optional[str] = Field(default="https://api.blink.sv/graphql")
|
||||
blink_ws_endpoint: Optional[str] = Field(default="wss://ws.blink.sv/graphql")
|
||||
blink_token: Optional[str] = Field(default=None)
|
||||
|
||||
|
||||
class ZBDFundingSource(LNbitsSettings):
|
||||
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
|
||||
zbd_api_key: Optional[str] = Field(default=None)
|
||||
@ -266,6 +272,7 @@ class FundingSourcesSettings(
|
||||
LndRestFundingSource,
|
||||
LndGrpcFundingSource,
|
||||
LnPayFundingSource,
|
||||
BlinkFundingSource,
|
||||
AlbyFundingSource,
|
||||
ZBDFundingSource,
|
||||
PhoenixdFundingSource,
|
||||
@ -415,14 +422,15 @@ class SuperUserSettings(LNbitsSettings):
|
||||
lnbits_allowed_funding_sources: list[str] = Field(
|
||||
default=[
|
||||
"AlbyWallet",
|
||||
"FakeWallet",
|
||||
"BlinkWallet",
|
||||
"CoreLightningRestWallet",
|
||||
"CoreLightningWallet",
|
||||
"EclairWallet",
|
||||
"LNbitsWallet",
|
||||
"LndRestWallet",
|
||||
"FakeWallet",
|
||||
"LNPayWallet",
|
||||
"LNbitsWallet",
|
||||
"LnTipsWallet",
|
||||
"LndRestWallet",
|
||||
"LndWallet",
|
||||
"OpenNodeWallet",
|
||||
"PhoenixdWallet",
|
||||
|
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@ -110,6 +110,15 @@ Vue.component('lnbits-funding-sources', {
|
||||
lnbits_key: 'Admin Key'
|
||||
}
|
||||
],
|
||||
[
|
||||
'BlinkWallet',
|
||||
'Blink',
|
||||
{
|
||||
blink_api_endpoint: 'Endpoint',
|
||||
blink_ws_endpoint: 'WebSocket',
|
||||
blink_token: 'Key'
|
||||
}
|
||||
],
|
||||
[
|
||||
'AlbyWallet',
|
||||
'Alby',
|
||||
|
@ -8,6 +8,7 @@ from lnbits.settings import settings
|
||||
from lnbits.wallets.base import Wallet
|
||||
|
||||
from .alby import AlbyWallet
|
||||
from .blink import BlinkWallet
|
||||
from .cliche import ClicheWallet
|
||||
from .corelightning import CoreLightningWallet
|
||||
|
||||
|
483
lnbits/wallets/blink.py
Normal file
483
lnbits/wallets/blink.py
Normal file
@ -0,0 +1,483 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from websockets.client import WebSocketClientProtocol, connect
|
||||
from websockets.typing import Subprotocol
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
|
||||
|
||||
class BlinkWallet(Wallet):
|
||||
"""https://dev.blink.sv/"""
|
||||
|
||||
def __init__(self):
|
||||
if not settings.blink_api_endpoint:
|
||||
raise ValueError(
|
||||
"cannot initialize BlinkWallet: missing blink_api_endpoint"
|
||||
)
|
||||
if not settings.blink_ws_endpoint:
|
||||
raise ValueError("cannot initialize BlinkWallet: missing blink_ws_endpoint")
|
||||
if not settings.blink_token:
|
||||
raise ValueError("cannot initialize BlinkWallet: missing blink_token")
|
||||
|
||||
self.endpoint = self.normalize_endpoint(settings.blink_api_endpoint)
|
||||
|
||||
self.auth = {
|
||||
"X-API-KEY": settings.blink_token,
|
||||
"User-Agent": settings.user_agent,
|
||||
}
|
||||
self.ws_endpoint = self.normalize_endpoint(settings.blink_ws_endpoint)
|
||||
self.ws_auth = {
|
||||
"type": "connection_init",
|
||||
"payload": {"X-API-KEY": settings.blink_token},
|
||||
}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||
self.ws: Optional[WebSocketClientProtocol] = None
|
||||
self._wallet_id = None
|
||||
|
||||
@property
|
||||
def wallet_id(self):
|
||||
if self._wallet_id:
|
||||
return self._wallet_id
|
||||
raise ValueError("Wallet id not initialized.")
|
||||
|
||||
async def cleanup(self):
|
||||
try:
|
||||
await self.client.aclose()
|
||||
except RuntimeError as e:
|
||||
logger.warning(f"Error closing wallet connection: {e}")
|
||||
|
||||
try:
|
||||
if self.ws:
|
||||
await self.ws.close(reason="Shutting down.")
|
||||
except RuntimeError as e:
|
||||
logger.warning(f"Error closing websocket connection: {e}")
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
await self._init_wallet_id()
|
||||
|
||||
payload = {"query": q.balance_query, "variables": {}}
|
||||
response = await self._graphql_query(payload)
|
||||
wallets = (
|
||||
response.get("data", {})
|
||||
.get("me", {})
|
||||
.get("defaultAccount", {})
|
||||
.get("wallets", [])
|
||||
)
|
||||
btc_balance = next(
|
||||
(
|
||||
wallet["balance"]
|
||||
for wallet in wallets
|
||||
if wallet["walletCurrency"] == "BTC"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if btc_balance is None:
|
||||
return StatusResponse("No BTC balance", 0)
|
||||
|
||||
# multiply balance by 1000 to get msats balance
|
||||
return StatusResponse(None, btc_balance * 1000)
|
||||
except ValueError as exc:
|
||||
return StatusResponse(str(exc), 0)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
# https://dev.blink.sv/api/btc-ln-receive
|
||||
|
||||
invoice_variables = {
|
||||
"input": {
|
||||
"amount": amount,
|
||||
"recipientWalletId": self.wallet_id,
|
||||
}
|
||||
}
|
||||
if description_hash:
|
||||
invoice_variables["input"]["descriptionHash"] = description_hash.hex()
|
||||
elif unhashed_description:
|
||||
invoice_variables["input"]["descriptionHash"] = hashlib.sha256(
|
||||
unhashed_description
|
||||
).hexdigest()
|
||||
else:
|
||||
invoice_variables["input"]["memo"] = memo or ""
|
||||
|
||||
data = {"query": q.invoice_query, "variables": invoice_variables}
|
||||
|
||||
try:
|
||||
response = await self._graphql_query(data)
|
||||
|
||||
errors = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
|
||||
.get("errors", {})
|
||||
)
|
||||
if len(errors) > 0:
|
||||
error_message = errors[0].get("message")
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
payment_request = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
|
||||
.get("invoice", {})
|
||||
.get("paymentRequest", None)
|
||||
)
|
||||
checking_id = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
|
||||
.get("invoice", {})
|
||||
.get("paymentHash", None)
|
||||
)
|
||||
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def pay_invoice(
|
||||
self, bolt11_invoice: str, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
# https://dev.blink.sv/api/btc-ln-send
|
||||
# Future: add check fee estimate is < fee_limit_msat before paying invoice
|
||||
|
||||
payment_variables = {
|
||||
"input": {
|
||||
"paymentRequest": bolt11_invoice,
|
||||
"walletId": self.wallet_id,
|
||||
"memo": "Payment memo",
|
||||
}
|
||||
}
|
||||
data = {"query": q.payment_query, "variables": payment_variables}
|
||||
try:
|
||||
response = await self._graphql_query(data)
|
||||
|
||||
errors = (
|
||||
response.get("data", {})
|
||||
.get("lnInvoicePaymentSend", {})
|
||||
.get("errors", {})
|
||||
)
|
||||
if len(errors) > 0:
|
||||
error_message = errors[0].get("message")
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
||||
|
||||
payment_status = await self.get_payment_status(checking_id)
|
||||
fee_msat = payment_status.fee_msat
|
||||
preimage = payment_status.preimage
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
statuses = {
|
||||
"EXPIRED": False,
|
||||
"PENDING": None,
|
||||
"PAID": True,
|
||||
}
|
||||
|
||||
variables = {"paymentHash": checking_id, "walletId": self.wallet_id}
|
||||
data = {"query": q.status_query, "variables": variables}
|
||||
|
||||
try:
|
||||
response = await self._graphql_query(data)
|
||||
if response.get("errors") is not None:
|
||||
logger.trace(response.get("errors"))
|
||||
return PaymentStatus(None)
|
||||
|
||||
status = response["data"]["me"]["defaultAccount"]["walletById"][
|
||||
"invoiceByPaymentHash"
|
||||
]["paymentStatus"]
|
||||
return PaymentStatus(statuses[status])
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting invoice status: {e}")
|
||||
return PaymentStatus(None)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
variables = {
|
||||
"walletId": self.wallet_id,
|
||||
"transactionsByPaymentHash": checking_id,
|
||||
}
|
||||
data = {"query": q.tx_query, "variables": variables}
|
||||
|
||||
statuses = {
|
||||
"FAILURE": False,
|
||||
"EXPIRED": False,
|
||||
"PENDING": None,
|
||||
"PAID": True,
|
||||
"SUCCESS": True,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self._graphql_query(data)
|
||||
|
||||
response_data = response.get("data")
|
||||
assert response_data is not None
|
||||
txs_data = (
|
||||
response_data.get("me", {})
|
||||
.get("defaultAccount", {})
|
||||
.get("walletById", {})
|
||||
.get("transactionsByPaymentHash", [])
|
||||
)
|
||||
tx_data = next((t for t in txs_data if t.get("direction") == "SEND"), None)
|
||||
assert tx_data, "No SEND data found."
|
||||
fee = tx_data.get("settlementFee")
|
||||
preimage = tx_data.get("settlementVia", {}).get("preImage")
|
||||
status = tx_data.get("status")
|
||||
|
||||
return PaymentStatus(
|
||||
paid=statuses[status], fee_msat=fee * 1000, preimage=preimage
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment status: {e}")
|
||||
return PaymentStatus(None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
subscription_id = "blink_payment_stream"
|
||||
while settings.lnbits_running:
|
||||
try:
|
||||
async with connect(
|
||||
self.ws_endpoint, subprotocols=[Subprotocol("graphql-transport-ws")]
|
||||
) as ws:
|
||||
logger.info("Connected to blink invoices stream.")
|
||||
self.ws = ws
|
||||
await ws.send(json.dumps(self.ws_auth))
|
||||
confirmation = await ws.recv()
|
||||
ack = json.loads(confirmation)
|
||||
assert (
|
||||
ack.get("type") == "connection_ack"
|
||||
), "Websocket connection not acknowledged."
|
||||
|
||||
logger.info("Websocket connection acknowledged.")
|
||||
subscription_req = {
|
||||
"id": subscription_id,
|
||||
"type": "subscribe",
|
||||
"payload": {"query": q.my_updates_query, "variables": {}},
|
||||
}
|
||||
await ws.send(json.dumps(subscription_req))
|
||||
|
||||
while settings.lnbits_running:
|
||||
message = await ws.recv()
|
||||
resp = json.loads(message)
|
||||
if resp.get("id") != subscription_id:
|
||||
continue
|
||||
tx = (
|
||||
resp.get("payload", {})
|
||||
.get("data", {})
|
||||
.get("myUpdates", {})
|
||||
.get("update", {})
|
||||
.get("transaction", {})
|
||||
)
|
||||
if tx.get("direction") != "RECEIVE":
|
||||
continue
|
||||
|
||||
if not tx.get("initiationVia"):
|
||||
continue
|
||||
|
||||
payment_hash = tx.get("initiationVia").get("paymentHash")
|
||||
if payment_hash:
|
||||
yield payment_hash
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to blink invoices stream: '{exc}'"
|
||||
"retrying in 5 seconds"
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _graphql_query(self, payload) -> dict:
|
||||
response = await self.client.post(self.endpoint, json=payload, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def _init_wallet_id(self) -> str:
|
||||
"""
|
||||
Get the defaultAccount wallet id, required for payments.
|
||||
"""
|
||||
|
||||
if self._wallet_id:
|
||||
return self._wallet_id
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"query": q.wallet_query,
|
||||
"variables": {},
|
||||
}
|
||||
response = await self._graphql_query(payload)
|
||||
wallets = (
|
||||
response.get("data", {})
|
||||
.get("me", {})
|
||||
.get("defaultAccount", {})
|
||||
.get("wallets", [])
|
||||
)
|
||||
btc_wallet_ids = [
|
||||
wallet["id"] for wallet in wallets if wallet["walletCurrency"] == "BTC"
|
||||
]
|
||||
|
||||
if not btc_wallet_ids:
|
||||
raise ValueError("BTC Wallet not found")
|
||||
|
||||
self._wallet_id = btc_wallet_ids[0]
|
||||
return self._wallet_id
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
raise ValueError(f"Unable to connect to '{self.endpoint}'") from exc
|
||||
|
||||
|
||||
class BlinkGrafqlQueries(BaseModel):
|
||||
balance_query: str
|
||||
invoice_query: str
|
||||
payment_query: str
|
||||
status_query: str
|
||||
wallet_query: str
|
||||
tx_query: str
|
||||
my_updates_query: str
|
||||
|
||||
|
||||
q = BlinkGrafqlQueries(
|
||||
balance_query="""
|
||||
query Me {
|
||||
me {
|
||||
defaultAccount {
|
||||
wallets {
|
||||
walletCurrency
|
||||
balance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
invoice_query="""
|
||||
mutation LnInvoiceCreateOnBehalfOfRecipient(
|
||||
$input: LnInvoiceCreateOnBehalfOfRecipientInput!
|
||||
) {
|
||||
lnInvoiceCreateOnBehalfOfRecipient(input: $input) {
|
||||
invoice {
|
||||
paymentRequest
|
||||
paymentHash
|
||||
paymentSecret
|
||||
satoshis
|
||||
}
|
||||
errors {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
payment_query="""
|
||||
mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
||||
lnInvoicePaymentSend(input: $input) {
|
||||
status
|
||||
errors {
|
||||
message
|
||||
path
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
status_query="""
|
||||
query InvoiceByPaymentHash($walletId: WalletId!, $paymentHash: PaymentHash!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
invoiceByPaymentHash(paymentHash: $paymentHash) {
|
||||
... on LnInvoice {
|
||||
paymentStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
wallet_query="""
|
||||
query me {
|
||||
me {
|
||||
defaultAccount {
|
||||
wallets {
|
||||
id
|
||||
walletCurrency
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
tx_query="""
|
||||
query TransactionsByPaymentHash(
|
||||
$walletId: WalletId!
|
||||
$transactionsByPaymentHash: PaymentHash!
|
||||
) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
walletCurrency
|
||||
... on BTCWallet {
|
||||
transactionsByPaymentHash(paymentHash: $transactionsByPaymentHash) {
|
||||
settlementFee
|
||||
status
|
||||
direction
|
||||
settlementVia {
|
||||
... on SettlementViaLn {
|
||||
preImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
my_updates_query="""
|
||||
subscription {
|
||||
myUpdates {
|
||||
update {
|
||||
... on LnUpdate {
|
||||
transaction {
|
||||
initiationVia {
|
||||
... on InitiationViaLn {
|
||||
paymentHash
|
||||
}
|
||||
}
|
||||
direction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
)
|
148
tests/wallets/test_blink.py
Normal file
148
tests/wallets/test_blink.py
Normal file
@ -0,0 +1,148 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import BlinkWallet, get_funding_source, set_funding_source
|
||||
|
||||
settings.lnbits_backend_wallet_class = "BlinkWallet"
|
||||
settings.blink_token = "mock"
|
||||
settings.blink_api_endpoint = "https://api.blink.sv/graphql"
|
||||
|
||||
# Check if BLINK_TOKEN environment variable is set
|
||||
use_real_api = os.environ.get("BLINK_TOKEN") is not None
|
||||
logger.info(f"use_real_api: {use_real_api}")
|
||||
|
||||
if use_real_api:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-KEY": os.environ.get("BLINK_TOKEN"),
|
||||
}
|
||||
settings.blink_token = os.environ.get("BLINK_TOKEN")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"settings.lnbits_backend_wallet_class: {settings.lnbits_backend_wallet_class}"
|
||||
)
|
||||
logger.info(f"settings.blink_api_endpoint: {settings.blink_api_endpoint}")
|
||||
logger.info(f"settings.blink_token: {settings.blink_token}")
|
||||
|
||||
set_funding_source()
|
||||
funding_source = get_funding_source()
|
||||
assert isinstance(funding_source, BlinkWallet)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def payhash():
|
||||
# put your external payment hash here
|
||||
payment_hash = "14d7899c3456bcd78f7f18a70d782b8eadb2de974e80dc5120e133032423dcda"
|
||||
return payment_hash
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def outbound_bolt11():
|
||||
# put your outbound bolt11 here
|
||||
bolt11 = "lnbc1u1pjl0uhypp5yxvdqq923atm9ywkpgtu3yxv9w2n44ensrkwfyagvmzqhml2x9gqdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsssp5h3qlnnlfqekquacwwj9yu7fhujyzxhzqegpxenscw45pgv6xakfq9qyyssqqjruygw0jrcg3365jksxn6yhsxx7c5pdjrjdlyvuhs7xh8r409h4e3kucc54kgh34pscaq3mg7hn55l8a0qszgzex80amwrp4gkdgqcpkse88y" # noqa: E501
|
||||
return bolt11
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_environment_variables():
|
||||
if use_real_api:
|
||||
assert "X-API-KEY" in headers, "X-API-KEY is not present in headers"
|
||||
assert isinstance(headers["X-API-KEY"], str), "X-API-KEY is not a string"
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_wallet_id():
|
||||
if use_real_api:
|
||||
wallet_id = await funding_source._init_wallet_id()
|
||||
logger.info(f"test_get_wallet_id: {wallet_id}")
|
||||
assert wallet_id
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status():
|
||||
if use_real_api:
|
||||
status = await funding_source.status()
|
||||
logger.info(f"test_status: {status}")
|
||||
assert status
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_invoice():
|
||||
if use_real_api:
|
||||
invoice_response = await funding_source.create_invoice(amount=1000, memo="test")
|
||||
assert invoice_response.ok is True
|
||||
assert invoice_response.payment_request
|
||||
assert invoice_response.checking_id
|
||||
logger.info(f"test_create_invoice: ok: {invoice_response.ok}")
|
||||
logger.info(
|
||||
f"test_create_invoice: payment_request: {invoice_response.payment_request}"
|
||||
)
|
||||
|
||||
payment_status = await funding_source.get_invoice_status(
|
||||
invoice_response.checking_id
|
||||
)
|
||||
assert payment_status.paid is None # still pending
|
||||
|
||||
logger.info(
|
||||
f"test_create_invoice: PaymentStatus is Still Pending: {payment_status.paid is None}" # noqa: E501
|
||||
)
|
||||
logger.info(
|
||||
f"test_create_invoice: PaymentStatusfee_msat: {payment_status.fee_msat}"
|
||||
)
|
||||
logger.info(
|
||||
f"test_create_invoice: PaymentStatus preimage: {payment_status.preimage}"
|
||||
)
|
||||
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pay_invoice_self_payment():
|
||||
if use_real_api:
|
||||
invoice_response = await funding_source.create_invoice(amount=100, memo="test")
|
||||
assert invoice_response.ok is True
|
||||
bolt11 = invoice_response.payment_request
|
||||
assert bolt11 is not None
|
||||
payment_response = await funding_source.pay_invoice(bolt11, fee_limit_msat=100)
|
||||
assert payment_response.ok is False # can't pay self
|
||||
assert payment_response.error_message
|
||||
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outbound_invoice_payment(outbound_bolt11):
|
||||
if use_real_api:
|
||||
payment_response = await funding_source.pay_invoice(
|
||||
outbound_bolt11, fee_limit_msat=100
|
||||
)
|
||||
assert payment_response.ok is True
|
||||
assert payment_response.checking_id
|
||||
logger.info(f"test_outbound_invoice_payment: ok: {payment_response.ok}")
|
||||
logger.info(
|
||||
f"test_outbound_invoice_payment: checking_id: {payment_response.checking_id}" # noqa: E501
|
||||
)
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_payment_status(payhash):
|
||||
if use_real_api:
|
||||
payment_status = await funding_source.get_payment_status(payhash)
|
||||
assert payment_status.paid
|
||||
logger.info(f"test_get_payment_status: payment_status: {payment_status.paid}")
|
||||
else:
|
||||
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
|
Loading…
Reference in New Issue
Block a user