Compare commits

..
Author SHA1 Message Date
dni ⚡andGitHub 92d3269a85 chore: update to v1.3.0-rc8 (#3359) 2025-09-12 14:29:42 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>dni ⚡
183b84c167 build(deps): bump axios from 1.8.2 to 1.12.0 (#3357)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2025-09-12 12:53:27 +01:00
60f50a71a2 feat: Adds stripe tap to pay flow for TPoS running on Android PoS/phone devices (#3334)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-09-12 12:50:24 +01:00
10 changed files with 692 additions and 379 deletions
+2
View File
@@ -139,7 +139,9 @@ async def create_fiat_invoice(
payment_hash=internal_payment.payment_hash,
currency=invoice_data.unit,
memo=invoice_data.memo,
extra=invoice_data.extra or {},
)
if fiat_invoice.failed:
logger.warning(fiat_invoice.error_message)
internal_payment.status = PaymentState.FAILED
+28 -1
View File
@@ -1,10 +1,11 @@
from http import HTTPStatus
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.services.fiat_providers import test_connection
from lnbits.decorators import check_admin
from lnbits.fiat import StripeWallet, get_fiat_provider
fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
@@ -16,3 +17,29 @@ fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
)
async def api_test_fiat_provider(provider: str) -> SimpleStatus:
return await test_connection(provider)
@fiat_router.post(
"/{provider}/connection_token",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)],
)
async def connection_token(provider: str):
provider_wallet = await get_fiat_provider(provider)
if provider == "stripe":
if not isinstance(provider_wallet, StripeWallet):
raise HTTPException(
status_code=500, detail="Stripe wallet/provider not configured"
)
try:
tok = await provider_wallet.create_terminal_connection_token()
secret = tok.get("secret")
if not secret:
raise HTTPException(
status_code=502, detail="Stripe returned no connection token"
)
return {"secret": secret}
except Exception as e:
raise HTTPException(
status_code=500, detail="Failed to create connection token"
) from e
+2 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator, Coroutine
from typing import TYPE_CHECKING, NamedTuple
from typing import TYPE_CHECKING, Any, NamedTuple
if TYPE_CHECKING:
pass
@@ -106,6 +106,7 @@ class FiatProvider(ABC):
payment_hash: str,
currency: str,
memo: str | None = None,
extra: dict[str, Any] | None = None,
**kwargs,
) -> Coroutine[None, None, FiatInvoiceResponse]:
pass
+220 -59
View File
@@ -2,10 +2,12 @@ import asyncio
import json
from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, timezone
from typing import Any, Literal
from urllib.parse import urlencode
import httpx
from loguru import logger
from pydantic import BaseModel, Field, ValidationError
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
@@ -21,6 +23,34 @@ from .base import (
FiatStatusResponse,
)
FiatMethod = Literal["checkout", "terminal"]
class StripeTerminalOptions(BaseModel):
class Config:
extra = "ignore"
capture_method: Literal["automatic", "manual"] = "automatic"
metadata: dict[str, str] = Field(default_factory=dict)
class StripeCheckoutOptions(BaseModel):
class Config:
extra = "ignore"
success_url: str | None = None
metadata: dict[str, str] = Field(default_factory=dict)
line_item_name: str | None = None
class StripeCreateInvoiceOptions(BaseModel):
class Config:
extra = "ignore"
fiat_method: FiatMethod = "checkout"
terminal: StripeTerminalOptions | None = None
checkout: StripeCheckoutOptions | None = None
class StripeWallet(FiatProvider):
"""https://docs.stripe.com/api"""
@@ -30,9 +60,9 @@ class StripeWallet(FiatProvider):
self._settings_fields = self._settings_connection_fields()
if not settings.stripe_api_endpoint:
raise ValueError("Cannot initialize StripeWallet: missing endpoint.")
if not settings.stripe_api_secret_key:
raise ValueError("Cannot initialize StripeWallet: missing API secret key.")
self.endpoint = normalize_endpoint(settings.stripe_api_endpoint)
self.headers = {
"Authorization": f"Bearer {settings.stripe_api_secret_key}",
@@ -60,8 +90,10 @@ class StripeWallet(FiatProvider):
r.raise_for_status()
data = r.json()
available_balance = data.get("available", [{}])[0].get("amount", 0)
# pending_balance = data.get("pending", {}).get("amount", 0)
available = data.get("available") or []
available_balance = 0
if available and isinstance(available, list):
available_balance = int(available[0].get("amount", 0))
return FiatStatusResponse(balance=available_balance)
except json.JSONDecodeError:
@@ -76,54 +108,25 @@ class StripeWallet(FiatProvider):
payment_hash: str,
currency: str,
memo: str | None = None,
extra: dict[str, Any] | None = None,
**kwargs,
) -> FiatInvoiceResponse:
amount_cents = int(amount * 100)
form_data = [
("mode", "payment"),
(
"success_url",
settings.stripe_payment_success_url or "https://lnbits.com",
),
("metadata[payment_hash]", payment_hash),
("line_items[0][price_data][currency]", currency.lower()),
("line_items[0][price_data][product_data][name]", memo or "LNbits Invoice"),
("line_items[0][price_data][unit_amount]", amount_cents),
("line_items[0][quantity]", "1"),
]
encoded_data = urlencode(form_data)
opts = self._parse_create_opts(extra or {})
if not opts:
return FiatInvoiceResponse(ok=False, error_message="Invalid Stripe options")
try:
headers = self.headers.copy()
headers["Content-Type"] = "application/x-www-form-urlencoded"
r = await self.client.post(
url="/v1/checkout/sessions", headers=headers, content=encoded_data
if opts.fiat_method == "checkout":
return await self._create_checkout_invoice(
amount_cents, currency, payment_hash, memo, opts
)
r.raise_for_status()
data = r.json()
session_id = data.get("id")
if not session_id:
return FiatInvoiceResponse(
ok=False, error_message="Server error: 'missing session id'"
)
payment_request = data.get("url")
if not payment_request:
return FiatInvoiceResponse(
ok=False, error_message="Server error: 'missing payment URL'"
if opts.fiat_method == "terminal":
return await self._create_terminal_invoice(
amount_cents, currency, payment_hash, opts
)
return FiatInvoiceResponse(
ok=True, checking_id=session_id, payment_request=payment_request
)
except json.JSONDecodeError:
return FiatInvoiceResponse(
ok=False, error_message="Server error: 'invalid json response'"
)
except Exception as exc:
logger.warning(exc)
return FiatInvoiceResponse(
ok=False, error_message=f"Unable to connect to {self.endpoint}."
ok=False, error_message=f"Unsupported fiat_method: {opts.fiat_method}"
)
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
@@ -131,26 +134,21 @@ class StripeWallet(FiatProvider):
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try:
r = await self.client.get(
url=f"/v1/checkout/sessions/{checking_id}",
)
stripe_id = self._normalize_stripe_id(checking_id)
if stripe_id.startswith("cs_"):
r = await self.client.get(f"/v1/checkout/sessions/{stripe_id}")
r.raise_for_status()
return self._status_from_checkout_session(r.json())
data = r.json()
payment_status = data.get("payment_status")
if not payment_status:
if stripe_id.startswith("pi_"):
r = await self.client.get(f"/v1/payment_intents/{stripe_id}")
r.raise_for_status()
return self._status_from_payment_intent(r.json())
logger.debug(f"Unknown Stripe id prefix: {checking_id}")
return FiatPaymentPendingStatus()
if payment_status == "paid":
# todo: handle fee
return FiatPaymentSuccessStatus()
expires_at = data.get("expires_at")
_24_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
if expires_at and expires_at < _24_hours_ago.timestamp():
# be defensive: add a 24 hour buffer
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
except Exception as exc:
logger.debug(f"Error getting invoice status: {exc}")
return FiatPaymentPendingStatus()
@@ -167,6 +165,169 @@ class StripeWallet(FiatProvider):
value = await mock_queue.get()
yield value
async def create_terminal_connection_token(self) -> dict:
r = await self.client.post("/v1/terminal/connection_tokens")
r.raise_for_status()
return r.json()
async def _create_checkout_invoice(
self,
amount_cents: int,
currency: str,
payment_hash: str,
memo: str | None,
opts: StripeCreateInvoiceOptions,
) -> FiatInvoiceResponse:
co = opts.checkout or StripeCheckoutOptions()
success_url = (
co.success_url
or settings.stripe_payment_success_url
or "https://lnbits.com"
)
line_item_name = co.line_item_name or memo or "LNbits Invoice"
form_data: list[tuple[str, str]] = [
("mode", "payment"),
("success_url", success_url),
("metadata[payment_hash]", payment_hash),
("line_items[0][price_data][currency]", currency.lower()),
("line_items[0][price_data][product_data][name]", line_item_name),
("line_items[0][price_data][unit_amount]", str(amount_cents)),
("line_items[0][quantity]", "1"),
]
form_data += self._encode_metadata("metadata", co.metadata)
try:
r = await self.client.post(
"/v1/checkout/sessions",
headers=self._build_headers_form(),
content=urlencode(form_data),
)
r.raise_for_status()
data = r.json()
session_id, url = data.get("id"), data.get("url")
if not session_id or not url:
return FiatInvoiceResponse(
ok=False, error_message="Server error: missing id or url"
)
return FiatInvoiceResponse(
ok=True, checking_id=session_id, payment_request=url
)
except json.JSONDecodeError:
return FiatInvoiceResponse(
ok=False, error_message="Server error: invalid json response"
)
except Exception as exc:
logger.warning(exc)
return FiatInvoiceResponse(
ok=False, error_message=f"Unable to connect to {self.endpoint}."
)
async def _create_terminal_invoice(
self,
amount_cents: int,
currency: str,
payment_hash: str,
opts: StripeCreateInvoiceOptions,
) -> FiatInvoiceResponse:
term = opts.terminal or StripeTerminalOptions()
data: dict[str, str] = {
"amount": str(amount_cents),
"currency": currency.lower(),
"payment_method_types[]": "card_present",
"capture_method": term.capture_method,
"metadata[payment_hash]": payment_hash,
"metadata[source]": "lnbits",
}
for k, v in (term.metadata or {}).items():
data[f"metadata[{k}]"] = str(v)
try:
r = await self.client.post("/v1/payment_intents", data=data)
r.raise_for_status()
pi = r.json()
pi_id, client_secret = pi.get("id"), pi.get("client_secret")
if not pi_id or not client_secret:
return FiatInvoiceResponse(
ok=False,
error_message="Error: missing PaymentIntent or client_secret",
)
return FiatInvoiceResponse(
ok=True, checking_id=pi_id, payment_request=client_secret
)
except json.JSONDecodeError:
return FiatInvoiceResponse(
ok=False, error_message="Error: invalid json response"
)
except Exception as exc:
logger.warning(exc)
return FiatInvoiceResponse(
ok=False, error_message=f"Unable to connect to {self.endpoint}."
)
def _normalize_stripe_id(self, checking_id: str) -> str:
"""Remove our internal prefix so Stripe sees a real id."""
return (
checking_id.replace("fiat_stripe_", "", 1)
if checking_id.startswith("fiat_stripe_")
else checking_id
)
def _status_from_checkout_session(self, data: dict) -> FiatPaymentStatus:
"""Map a Checkout Session to LNbits fiat status."""
if data.get("payment_status") == "paid":
return FiatPaymentSuccessStatus()
# Consider an expired session a fail (existing 24h rule).
expires_at = data.get("expires_at")
_24h_ago = datetime.now(timezone.utc) - timedelta(hours=24)
if expires_at and float(expires_at) < _24h_ago.timestamp():
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _status_from_payment_intent(self, pi: dict) -> FiatPaymentStatus:
"""Map a PaymentIntent to LNbits fiat status (card_present friendly)."""
status = pi.get("status")
if status == "succeeded":
return FiatPaymentSuccessStatus()
if status in ("canceled", "payment_failed"):
return FiatPaymentFailedStatus()
if status == "requires_payment_method":
if pi.get("last_payment_error"):
return FiatPaymentFailedStatus()
now_ts = datetime.now(timezone.utc).timestamp()
created_ts = float(pi.get("created") or now_ts)
is_stale = (now_ts - created_ts) > 300
if is_stale:
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _build_headers_form(self) -> dict[str, str]:
return {**self.headers, "Content-Type": "application/x-www-form-urlencoded"}
def _encode_metadata(
self, prefix: str, md: dict[str, Any]
) -> list[tuple[str, str]]:
out: list[tuple[str, str]] = []
for k, v in (md or {}).items():
out.append((f"{prefix}[{k}]", str(v)))
return out
def _parse_create_opts(
self, raw_opts: dict[str, Any]
) -> StripeCreateInvoiceOptions | None:
try:
return StripeCreateInvoiceOptions.parse_obj(raw_opts)
except ValidationError as e:
logger.warning(f"Invalid Stripe options: {e}")
return None
def _settings_connection_fields(self) -> str:
return "-".join(
[str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)]
+2 -2
View File
File diff suppressed because one or more lines are too long
+255 -133
View File
@@ -1,4 +1,4 @@
/*! Axios v1.8.2 Copyright (c) 2025 Matt Zabriskie and contributors */
/*! Axios v1.12.0 Copyright (c) 2025 Matt Zabriskie and contributors */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
@@ -585,9 +585,6 @@
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
function _toArray(arr) {
return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest();
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
@@ -681,6 +678,8 @@
var toString = Object.prototype.toString;
var getPrototypeOf = Object.getPrototypeOf;
var iterator = Symbol.iterator,
toStringTag = Symbol.toStringTag;
var kindOf = function (cache) {
return function (thing) {
var str = toString.call(thing);
@@ -725,7 +724,7 @@
* @returns {boolean} True if value is a Buffer, otherwise false
*/
function isBuffer(val) {
return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) && isFunction(val.constructor.isBuffer) && val.constructor.isBuffer(val);
return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) && isFunction$1(val.constructor.isBuffer) && val.constructor.isBuffer(val);
}
/**
@@ -769,7 +768,7 @@
* @param {*} val The value to test
* @returns {boolean} True if value is a Function, otherwise false
*/
var isFunction = typeOfTest('function');
var isFunction$1 = typeOfTest('function');
/**
* Determine if a value is a Number
@@ -813,7 +812,27 @@
return false;
}
var prototype = getPrototypeOf(val);
return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in val) && !(Symbol.iterator in val);
return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(toStringTag in val) && !(iterator in val);
};
/**
* Determine if a value is an empty object (safely handles Buffers)
*
* @param {*} val The value to test
*
* @returns {boolean} True if value is an empty object, otherwise false
*/
var isEmptyObject = function isEmptyObject(val) {
// Early return for non-objects or Buffers to prevent RangeError
if (!isObject(val) || isBuffer(val)) {
return false;
}
try {
return Object.keys(val).length === 0 && Object.getPrototypeOf(val) === Object.prototype;
} catch (e) {
// Fallback for any other objects that might cause RangeError with Object.keys()
return false;
}
};
/**
@@ -860,7 +879,7 @@
* @returns {boolean} True if value is a Stream, otherwise false
*/
var isStream = function isStream(val) {
return isObject(val) && isFunction(val.pipe);
return isObject(val) && isFunction$1(val.pipe);
};
/**
@@ -872,9 +891,9 @@
*/
var isFormData = function isFormData(thing) {
var kind;
return thing && (typeof FormData === 'function' && thing instanceof FormData || isFunction(thing.append) && ((kind = kindOf(thing)) === 'formdata' ||
return thing && (typeof FormData === 'function' && thing instanceof FormData || isFunction$1(thing.append) && ((kind = kindOf(thing)) === 'formdata' ||
// detect form-data instance
kind === 'object' && isFunction(thing.toString) && thing.toString() === '[object FormData]'));
kind === 'object' && isFunction$1(thing.toString) && thing.toString() === '[object FormData]'));
};
/**
@@ -940,6 +959,11 @@
fn.call(null, obj[i], i, obj);
}
} else {
// Buffer check
if (isBuffer(obj)) {
return;
}
// Iterate over object keys
var keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
var len = keys.length;
@@ -951,6 +975,9 @@
}
}
function findKey(obj, key) {
if (isBuffer(obj)) {
return null;
}
key = key.toLowerCase();
var keys = Object.keys(obj);
var i = keys.length;
@@ -993,7 +1020,8 @@
function merge( /* obj1, obj2, obj3, ... */
) {
var _ref2 = isContextDefined(this) && this || {},
caseless = _ref2.caseless;
caseless = _ref2.caseless,
skipUndefined = _ref2.skipUndefined;
var result = {};
var assignValue = function assignValue(val, key) {
var targetKey = caseless && findKey(result, key) || key;
@@ -1004,8 +1032,10 @@
} else if (isArray(val)) {
result[targetKey] = val.slice();
} else {
if (!skipUndefined || !isUndefined(val)) {
result[targetKey] = val;
}
}
};
for (var i = 0, l = arguments.length; i < l; i++) {
arguments[i] && forEach(arguments[i], assignValue);
@@ -1027,7 +1057,7 @@
var _ref3 = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {},
allOwnKeys = _ref3.allOwnKeys;
forEach(b, function (val, key) {
if (thisArg && isFunction(val)) {
if (thisArg && isFunction$1(val)) {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
@@ -1165,10 +1195,10 @@
* @returns {void}
*/
var forEachEntry = function forEachEntry(obj, fn) {
var generator = obj && obj[Symbol.iterator];
var iterator = generator.call(obj);
var generator = obj && obj[iterator];
var _iterator = generator.call(obj);
var result;
while ((result = iterator.next()) && !result.done) {
while ((result = _iterator.next()) && !result.done) {
var pair = result.value;
fn.call(obj, pair[0], pair[1]);
}
@@ -1235,11 +1265,11 @@
var freezeMethods = function freezeMethods(obj) {
reduceDescriptors(obj, function (descriptor, name) {
// skip restricted props in strict mode
if (isFunction(obj) && ['arguments', 'caller', 'callee'].indexOf(name) !== -1) {
if (isFunction$1(obj) && ['arguments', 'caller', 'callee'].indexOf(name) !== -1) {
return false;
}
var value = obj[name];
if (!isFunction(value)) return;
if (!isFunction$1(value)) return;
descriptor.enumerable = false;
if ('writable' in descriptor) {
descriptor.writable = false;
@@ -1275,7 +1305,7 @@
* @returns {boolean}
*/
function isSpecCompliantForm(thing) {
return !!(thing && isFunction(thing.append) && thing[Symbol.toStringTag] === 'FormData' && thing[Symbol.iterator]);
return !!(thing && isFunction$1(thing.append) && thing[toStringTag] === 'FormData' && thing[iterator]);
}
var toJSONObject = function toJSONObject(obj) {
var stack = new Array(10);
@@ -1284,6 +1314,11 @@
if (stack.indexOf(source) >= 0) {
return;
}
//Buffer check
if (isBuffer(source)) {
return source;
}
if (!('toJSON' in source)) {
stack[i] = source;
var target = isArray(source) ? [] : {};
@@ -1301,7 +1336,7 @@
};
var isAsyncFn = kindOfTest('AsyncFunction');
var isThenable = function isThenable(thing) {
return thing && (isObject(thing) || isFunction(thing)) && isFunction(thing.then) && isFunction(thing["catch"]);
return thing && (isObject(thing) || isFunction$1(thing)) && isFunction$1(thing.then) && isFunction$1(thing["catch"]);
};
// original code
@@ -1326,11 +1361,14 @@
}("axios@".concat(Math.random()), []) : function (cb) {
return setTimeout(cb);
};
}(typeof setImmediate === 'function', isFunction(_global.postMessage));
}(typeof setImmediate === 'function', isFunction$1(_global.postMessage));
var asap = typeof queueMicrotask !== 'undefined' ? queueMicrotask.bind(_global) : typeof process !== 'undefined' && process.nextTick || _setImmediate;
// *********************
var isIterable = function isIterable(thing) {
return thing != null && isFunction$1(thing[iterator]);
};
var utils$1 = {
isArray: isArray,
isArrayBuffer: isArrayBuffer,
@@ -1342,6 +1380,7 @@
isBoolean: isBoolean,
isObject: isObject,
isPlainObject: isPlainObject,
isEmptyObject: isEmptyObject,
isReadableStream: isReadableStream,
isRequest: isRequest,
isResponse: isResponse,
@@ -1351,7 +1390,7 @@
isFile: isFile,
isBlob: isBlob,
isRegExp: isRegExp,
isFunction: isFunction,
isFunction: isFunction$1,
isStream: isStream,
isURLSearchParams: isURLSearchParams,
isTypedArray: isTypedArray,
@@ -1387,7 +1426,8 @@
isAsyncFn: isAsyncFn,
isThenable: isThenable,
setImmediate: _setImmediate,
asap: asap
asap: asap,
isIterable: isIterable
};
/**
@@ -1461,9 +1501,20 @@
}, function (prop) {
return prop !== 'isAxiosError';
});
AxiosError.call(axiosError, error.message, code, config, request, response);
axiosError.cause = error;
axiosError.name = error.name;
var msg = error && error.message ? error.message : 'Error';
// Prefer explicit code; otherwise copy the low-level error's code (e.g. ECONNREFUSED)
var errCode = code == null && error ? error.code : code;
AxiosError.call(axiosError, msg, errCode, config, request, response);
// Chain the original error on the standard field; non-enumerable to avoid JSON noise
if (error && axiosError.cause == null) {
Object.defineProperty(axiosError, 'cause', {
value: error,
configurable: true
});
}
axiosError.name = error && error.name || 'Error';
customProps && Object.assign(axiosError, customProps);
return axiosError;
};
@@ -1580,6 +1631,9 @@
if (utils$1.isDate(value)) {
return value.toISOString();
}
if (utils$1.isBoolean(value)) {
return value.toString();
}
if (!useBlob && utils$1.isBlob(value)) {
throw new AxiosError('Blob is not supported. Use a Buffer instead.');
}
@@ -1708,7 +1762,7 @@
* @returns {string} The encoded value.
*/
function encode(val) {
return encodeURIComponent(val).replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']');
return encodeURIComponent(val).replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+');
}
/**
@@ -1898,7 +1952,7 @@
var platform = _objectSpread2(_objectSpread2({}, utils), platform$1);
function toURLEncodedForm(data, options) {
return toFormData(data, new platform.classes.URLSearchParams(), Object.assign({
return toFormData(data, new platform.classes.URLSearchParams(), _objectSpread2({
visitor: function visitor(value, key, path, helpers) {
if (platform.isNode && utils$1.isBuffer(value)) {
this.append(key, value.toString('base64'));
@@ -2063,7 +2117,7 @@
var silentJSONParsing = transitional && transitional.silentJSONParsing;
var strictJSONParsing = !silentJSONParsing && JSONRequested;
try {
return JSON.parse(data);
return JSON.parse(data, this.parseReviver);
} catch (e) {
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
@@ -2227,21 +2281,26 @@
setHeaders(header, valueOrRewrite);
} else if (utils$1.isString(header) && (header = header.trim()) && !isValidHeaderName(header)) {
setHeaders(parseHeaders(header), valueOrRewrite);
} else if (utils$1.isHeaders(header)) {
var _iterator = _createForOfIteratorHelper(header.entries()),
} else if (utils$1.isObject(header) && utils$1.isIterable(header)) {
var obj = {},
dest,
key;
var _iterator = _createForOfIteratorHelper(header),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _step$value = _slicedToArray(_step.value, 2),
key = _step$value[0],
value = _step$value[1];
setHeader(value, key, rewrite);
var entry = _step.value;
if (!utils$1.isArray(entry)) {
throw TypeError('Object iterator must return a key-value pair');
}
obj[key = entry[0]] = (dest = obj[key]) ? utils$1.isArray(dest) ? [].concat(_toConsumableArray(dest), [entry[1]]) : [dest, entry[1]] : entry[1];
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
setHeaders(obj, valueOrRewrite);
} else {
header != null && setHeader(valueOrRewrite, header, rewrite);
}
@@ -2372,6 +2431,11 @@
return header + ': ' + value;
}).join('\n');
}
}, {
key: "getSetCookie",
value: function getSetCookie() {
return this.get("set-cookie") || [];
}
}, {
key: _Symbol$toStringTag,
get: function get() {
@@ -2557,7 +2621,7 @@
clearTimeout(timer);
timer = null;
}
fn.apply(null, args);
fn.apply(void 0, _toConsumableArray(args));
};
var throttled = function throttled() {
var now = Date.now();
@@ -2703,7 +2767,7 @@
*/
function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
var isRelativeUrl = !isAbsoluteURL(requestedURL);
if (baseURL && isRelativeUrl || allowAbsoluteUrls == false) {
if (baseURL && (isRelativeUrl || allowAbsoluteUrls == false)) {
return combineURLs(baseURL, requestedURL);
}
return requestedURL;
@@ -2805,7 +2869,7 @@
return mergeDeepProperties(headersToObject(a), headersToObject(b), prop, true);
}
};
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
utils$1.forEach(Object.keys(_objectSpread2(_objectSpread2({}, config1), config2)), function computeConfigValue(prop) {
var merge = mergeMap[prop] || mergeDeepProperties;
var configValue = merge(config1[prop], config2[prop], prop);
utils$1.isUndefined(configValue) && merge !== mergeDirectKeys || (config[prop] = configValue);
@@ -2822,25 +2886,28 @@
headers = newConfig.headers,
auth = newConfig.auth;
newConfig.headers = headers = AxiosHeaders$1.from(headers);
newConfig.url = buildURL(buildFullPath(newConfig.baseURL, newConfig.url), config.params, config.paramsSerializer);
newConfig.url = buildURL(buildFullPath(newConfig.baseURL, newConfig.url, newConfig.allowAbsoluteUrls), config.params, config.paramsSerializer);
// HTTP basic authentication
if (auth) {
headers.set('Authorization', 'Basic ' + btoa((auth.username || '') + ':' + (auth.password ? unescape(encodeURIComponent(auth.password)) : '')));
}
var contentType;
if (utils$1.isFormData(data)) {
if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
headers.setContentType(undefined); // Let the browser set it
} else if ((contentType = headers.getContentType()) !== false) {
// fix semicolon duplication issue for ReactNative FormData implementation
var _ref = contentType ? contentType.split(';').map(function (token) {
return token.trim();
}).filter(Boolean) : [],
_ref2 = _toArray(_ref),
type = _ref2[0],
tokens = _ref2.slice(1);
headers.setContentType([type || 'multipart/form-data'].concat(_toConsumableArray(tokens)).join('; '));
headers.setContentType(undefined); // browser handles it
} else if (utils$1.isFunction(data.getHeaders)) {
// Node.js FormData (like form-data package)
var formHeaders = data.getHeaders();
// Only set safe headers to avoid overwriting security headers
var allowedHeaders = ['content-type', 'content-length'];
Object.entries(formHeaders).forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
key = _ref2[0],
val = _ref2[1];
if (allowedHeaders.includes(key.toLowerCase())) {
headers.set(key, val);
}
});
}
}
@@ -2946,12 +3013,15 @@
};
// Handle low level network errors
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
// Clean up request
request.onerror = function handleError(event) {
// Browsers deliver a ProgressEvent in XHR onerror
// (message may be empty; when present, surface it)
// See https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/error_event
var msg = event && event.message ? event.message : 'Network Error';
var err = new AxiosError(msg, AxiosError.ERR_NETWORK, config, request);
// attach the underlying event for consumers who want details
err.event = event || null;
reject(err);
request = null;
};
@@ -3280,35 +3350,21 @@
});
};
var isFetchSupported = typeof fetch === 'function' && typeof Request === 'function' && typeof Response === 'function';
var isReadableStreamSupported = isFetchSupported && typeof ReadableStream === 'function';
// used only inside the fetch adapter
var encodeText = isFetchSupported && (typeof TextEncoder === 'function' ? function (encoder) {
return function (str) {
return encoder.encode(str);
var DEFAULT_CHUNK_SIZE = 64 * 1024;
var isFunction = utils$1.isFunction;
var globalFetchAPI = function (_ref) {
var fetch = _ref.fetch,
Request = _ref.Request,
Response = _ref.Response;
return {
fetch: fetch,
Request: Request,
Response: Response
};
}(new TextEncoder()) : ( /*#__PURE__*/function () {
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(str) {
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
_context.t0 = Uint8Array;
_context.next = 3;
return new Response(str).arrayBuffer();
case 3:
_context.t1 = _context.sent;
return _context.abrupt("return", new _context.t0(_context.t1));
case 5:
case "end":
return _context.stop();
}
}, _callee);
}));
return function (_x) {
return _ref.apply(this, arguments);
};
}()));
}(utils$1.global);
var _utils$global = utils$1.global,
ReadableStream$1 = _utils$global.ReadableStream,
TextEncoder = _utils$global.TextEncoder;
var test = function test(fn) {
try {
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
@@ -3319,10 +3375,47 @@
return false;
}
};
var supportsRequestStream = isReadableStreamSupported && test(function () {
var factory = function factory(env) {
var _Object$assign = Object.assign({}, globalFetchAPI, env),
fetch = _Object$assign.fetch,
Request = _Object$assign.Request,
Response = _Object$assign.Response;
var isFetchSupported = isFunction(fetch);
var isRequestSupported = isFunction(Request);
var isResponseSupported = isFunction(Response);
if (!isFetchSupported) {
return false;
}
var isReadableStreamSupported = isFetchSupported && isFunction(ReadableStream$1);
var encodeText = isFetchSupported && (typeof TextEncoder === 'function' ? function (encoder) {
return function (str) {
return encoder.encode(str);
};
}(new TextEncoder()) : ( /*#__PURE__*/function () {
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(str) {
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
_context.t0 = Uint8Array;
_context.next = 3;
return new Request(str).arrayBuffer();
case 3:
_context.t1 = _context.sent;
return _context.abrupt("return", new _context.t0(_context.t1));
case 5:
case "end":
return _context.stop();
}
}, _callee);
}));
return function (_x) {
return _ref2.apply(this, arguments);
};
}()));
var supportsRequestStream = isRequestSupported && isReadableStreamSupported && test(function () {
var duplexAccessed = false;
var hasContentType = new Request(platform.origin, {
body: new ReadableStream(),
body: new ReadableStream$1(),
method: 'POST',
get duplex() {
duplexAccessed = true;
@@ -3331,8 +3424,7 @@
}).headers.has('Content-Type');
return duplexAccessed && !hasContentType;
});
var DEFAULT_CHUNK_SIZE = 64 * 1024;
var supportsResponseStream = isReadableStreamSupported && test(function () {
var supportsResponseStream = isResponseSupported && isReadableStreamSupported && test(function () {
return utils$1.isReadableStream(new Response('').body);
});
var resolvers = {
@@ -3340,17 +3432,19 @@
return res.body;
}
};
isFetchSupported && function (res) {
isFetchSupported && function () {
['text', 'arrayBuffer', 'blob', 'formData', 'stream'].forEach(function (type) {
!resolvers[type] && (resolvers[type] = utils$1.isFunction(res[type]) ? function (res) {
return res[type]();
} : function (_, config) {
!resolvers[type] && (resolvers[type] = function (res, config) {
var method = res && res[type];
if (method) {
return method.call(res);
}
throw new AxiosError("Response type '".concat(type, "' is not supported"), AxiosError.ERR_NOT_SUPPORT, config);
});
});
}(new Response());
}();
var getBodyLength = /*#__PURE__*/function () {
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(body) {
var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(body) {
var _request;
return _regeneratorRuntime().wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
@@ -3404,11 +3498,11 @@
}, _callee2);
}));
return function getBodyLength(_x2) {
return _ref2.apply(this, arguments);
return _ref3.apply(this, arguments);
};
}();
var resolveBodyLength = /*#__PURE__*/function () {
var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(headers, body) {
var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(headers, body) {
var length;
return _regeneratorRuntime().wrap(function _callee3$(_context3) {
while (1) switch (_context3.prev = _context3.next) {
@@ -3422,35 +3516,36 @@
}, _callee3);
}));
return function resolveBodyLength(_x3, _x4) {
return _ref3.apply(this, arguments);
return _ref4.apply(this, arguments);
};
}();
var fetchAdapter = isFetchSupported && ( /*#__PURE__*/function () {
var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(config) {
var _resolveConfig, url, method, data, signal, cancelToken, timeout, onDownloadProgress, onUploadProgress, responseType, headers, _resolveConfig$withCr, withCredentials, fetchOptions, composedSignal, request, unsubscribe, requestContentLength, _request, contentTypeHeader, _progressEventDecorat, _progressEventDecorat2, onProgress, flush, isCredentialsSupported, response, isStreamResponse, options, responseContentLength, _ref5, _ref6, _onProgress, _flush, responseData;
return /*#__PURE__*/function () {
var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(config) {
var _resolveConfig, url, method, data, signal, cancelToken, timeout, onDownloadProgress, onUploadProgress, responseType, headers, _resolveConfig$withCr, withCredentials, fetchOptions, composedSignal, request, unsubscribe, requestContentLength, _request, contentTypeHeader, _progressEventDecorat, _progressEventDecorat2, onProgress, flush, isCredentialsSupported, resolvedOptions, response, isStreamResponse, options, responseContentLength, _ref6, _ref7, _onProgress, _flush, responseData;
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
while (1) switch (_context4.prev = _context4.next) {
case 0:
_resolveConfig = resolveConfig(config), url = _resolveConfig.url, method = _resolveConfig.method, data = _resolveConfig.data, signal = _resolveConfig.signal, cancelToken = _resolveConfig.cancelToken, timeout = _resolveConfig.timeout, onDownloadProgress = _resolveConfig.onDownloadProgress, onUploadProgress = _resolveConfig.onUploadProgress, responseType = _resolveConfig.responseType, headers = _resolveConfig.headers, _resolveConfig$withCr = _resolveConfig.withCredentials, withCredentials = _resolveConfig$withCr === void 0 ? 'same-origin' : _resolveConfig$withCr, fetchOptions = _resolveConfig.fetchOptions;
responseType = responseType ? (responseType + '').toLowerCase() : 'text';
composedSignal = composeSignals$1([signal, cancelToken && cancelToken.toAbortSignal()], timeout);
request = null;
unsubscribe = composedSignal && composedSignal.unsubscribe && function () {
composedSignal.unsubscribe();
};
_context4.prev = 4;
_context4.prev = 5;
_context4.t0 = onUploadProgress && supportsRequestStream && method !== 'get' && method !== 'head';
if (!_context4.t0) {
_context4.next = 11;
_context4.next = 12;
break;
}
_context4.next = 9;
_context4.next = 10;
return resolveBodyLength(headers, data);
case 9:
case 10:
_context4.t1 = requestContentLength = _context4.sent;
_context4.t0 = _context4.t1 !== 0;
case 11:
case 12:
if (!_context4.t0) {
_context4.next = 15;
_context4.next = 16;
break;
}
_request = new Request(url, {
@@ -3465,25 +3560,26 @@
_progressEventDecorat = progressEventDecorator(requestContentLength, progressEventReducer(asyncDecorator(onUploadProgress))), _progressEventDecorat2 = _slicedToArray(_progressEventDecorat, 2), onProgress = _progressEventDecorat2[0], flush = _progressEventDecorat2[1];
data = trackStream(_request.body, DEFAULT_CHUNK_SIZE, onProgress, flush);
}
case 15:
case 16:
if (!utils$1.isString(withCredentials)) {
withCredentials = withCredentials ? 'include' : 'omit';
}
// Cloudflare Workers throws when credentials are defined
// see https://github.com/cloudflare/workerd/issues/902
isCredentialsSupported = "credentials" in Request.prototype;
request = new Request(url, _objectSpread2(_objectSpread2({}, fetchOptions), {}, {
isCredentialsSupported = isRequestSupported && "credentials" in Request.prototype;
resolvedOptions = _objectSpread2(_objectSpread2({}, fetchOptions), {}, {
signal: composedSignal,
method: method.toUpperCase(),
headers: headers.normalize().toJSON(),
body: data,
duplex: "half",
credentials: isCredentialsSupported ? withCredentials : undefined
}));
_context4.next = 20;
return fetch(request);
case 20:
});
request = isRequestSupported && new Request(url, resolvedOptions);
_context4.next = 22;
return isRequestSupported ? fetch(request, fetchOptions) : fetch(url, resolvedOptions);
case 22:
response = _context4.sent;
isStreamResponse = supportsResponseStream && (responseType === 'stream' || responseType === 'response');
if (supportsResponseStream && (onDownloadProgress || isStreamResponse && unsubscribe)) {
@@ -3492,19 +3588,19 @@
options[prop] = response[prop];
});
responseContentLength = utils$1.toFiniteNumber(response.headers.get('content-length'));
_ref5 = onDownloadProgress && progressEventDecorator(responseContentLength, progressEventReducer(asyncDecorator(onDownloadProgress), true)) || [], _ref6 = _slicedToArray(_ref5, 2), _onProgress = _ref6[0], _flush = _ref6[1];
_ref6 = onDownloadProgress && progressEventDecorator(responseContentLength, progressEventReducer(asyncDecorator(onDownloadProgress), true)) || [], _ref7 = _slicedToArray(_ref6, 2), _onProgress = _ref7[0], _flush = _ref7[1];
response = new Response(trackStream(response.body, DEFAULT_CHUNK_SIZE, _onProgress, function () {
_flush && _flush();
unsubscribe && unsubscribe();
}), options);
}
responseType = responseType || 'text';
_context4.next = 26;
_context4.next = 28;
return resolvers[utils$1.findKey(resolvers, responseType) || 'text'](response, config);
case 26:
case 28:
responseData = _context4.sent;
!isStreamResponse && unsubscribe && unsubscribe();
_context4.next = 30;
_context4.next = 32;
return new Promise(function (resolve, reject) {
settle(resolve, reject, {
data: responseData,
@@ -3515,36 +3611,62 @@
request: request
});
});
case 30:
case 32:
return _context4.abrupt("return", _context4.sent);
case 33:
_context4.prev = 33;
_context4.t2 = _context4["catch"](4);
case 35:
_context4.prev = 35;
_context4.t2 = _context4["catch"](5);
unsubscribe && unsubscribe();
if (!(_context4.t2 && _context4.t2.name === 'TypeError' && /fetch/i.test(_context4.t2.message))) {
_context4.next = 38;
if (!(_context4.t2 && _context4.t2.name === 'TypeError' && /Load failed|fetch/i.test(_context4.t2.message))) {
_context4.next = 40;
break;
}
throw Object.assign(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request), {
cause: _context4.t2.cause || _context4.t2
});
case 38:
case 40:
throw AxiosError.from(_context4.t2, _context4.t2 && _context4.t2.code, config, request);
case 39:
case 41:
case "end":
return _context4.stop();
}
}, _callee4, null, [[4, 33]]);
}, _callee4, null, [[5, 35]]);
}));
return function (_x5) {
return _ref4.apply(this, arguments);
return _ref5.apply(this, arguments);
};
}());
}();
};
var seedCache = new Map();
var getFetch = function getFetch(config) {
var env = utils$1.merge.call({
skipUndefined: true
}, globalFetchAPI, config ? config.env : null);
var fetch = env.fetch,
Request = env.Request,
Response = env.Response;
var seeds = [Request, Response, fetch];
var len = seeds.length,
i = len,
seed,
target,
map = seedCache;
while (i--) {
seed = seeds[i];
target = map.get(seed);
target === undefined && map.set(seed, target = i ? new Map() : factory(env));
map = target;
}
return target;
};
getFetch();
var knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter,
fetch: fetchAdapter
fetch: {
get: getFetch
}
};
utils$1.forEach(knownAdapters, function (fn, value) {
if (fn) {
@@ -3567,7 +3689,7 @@
return utils$1.isFunction(adapter) || adapter === null || adapter === false;
};
var adapters = {
getAdapter: function getAdapter(adapters) {
getAdapter: function getAdapter(adapters, config) {
adapters = utils$1.isArray(adapters) ? adapters : [adapters];
var _adapters = adapters,
length = _adapters.length;
@@ -3584,7 +3706,7 @@
throw new AxiosError("Unknown adapter '".concat(id, "'"));
}
}
if (adapter) {
if (adapter && (utils$1.isFunction(adapter) || (adapter = adapter.get(config)))) {
break;
}
rejectedReasons[id || '#' + i] = adapter;
@@ -3636,7 +3758,7 @@
if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
config.headers.setContentType('application/x-www-form-urlencoded', false);
}
var adapter = adapters.getAdapter(config.adapter || defaults$1.adapter);
var adapter = adapters.getAdapter(config.adapter || defaults$1.adapter, config);
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
@@ -3658,7 +3780,7 @@
});
}
var VERSION = "1.8.2";
var VERSION = "1.12.0";
var validators$1 = {};
@@ -3754,7 +3876,7 @@
var Axios = /*#__PURE__*/function () {
function Axios(instanceConfig) {
_classCallCheck(this, Axios);
this.defaults = instanceConfig;
this.defaults = instanceConfig || {};
this.interceptors = {
request: new InterceptorManager$1(),
response: new InterceptorManager$1()
+5 -5
View File
@@ -6,7 +6,7 @@
"": {
"name": "lnbits",
"dependencies": {
"axios": "^1.8.2",
"axios": "^1.12.0",
"chart.js": "^4.4.4",
"moment": "^2.30.1",
"nostr-tools": "^2.7.2",
@@ -402,13 +402,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
+1 -1
View File
@@ -20,7 +20,7 @@
"sass": "^1.78.0"
},
"dependencies": {
"axios": "^1.8.2",
"axios": "^1.12.0",
"chart.js": "^4.4.4",
"moment": "^2.30.1",
"qrcode.vue": "^3.4.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.3.0-rc7"
version = "1.3.0-rc8"
requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
Generated
+1 -1
View File
@@ -1260,7 +1260,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.3.0rc7"
version = "1.3.0rc8"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },