parent
bd07f7a5ef
commit
b54eedee84
31 changed files with 2078 additions and 163 deletions
|
|
@ -39,7 +39,6 @@ async def get_standalone_payment(
|
||||||
) -> Payment | None:
|
) -> Payment | None:
|
||||||
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
||||||
values = {
|
values = {
|
||||||
"wallet_id": wallet_id,
|
|
||||||
"checking_id": checking_id_or_hash,
|
"checking_id": checking_id_or_hash,
|
||||||
"hash": checking_id_or_hash,
|
"hash": checking_id_or_hash,
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +46,10 @@ async def get_standalone_payment(
|
||||||
clause = f"({clause}) AND amount > 0"
|
clause = f"({clause}) AND amount > 0"
|
||||||
|
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
|
wallet = await get_wallet(wallet_id)
|
||||||
|
if not wallet or not wallet.can_view_payments:
|
||||||
|
return None
|
||||||
|
values["wallet_id"] = wallet.source_wallet_id
|
||||||
clause = f"({clause}) AND wallet_id = :wallet_id"
|
clause = f"({clause}) AND wallet_id = :wallet_id"
|
||||||
|
|
||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
|
|
@ -66,13 +69,16 @@ async def get_standalone_payment(
|
||||||
async def get_wallet_payment(
|
async def get_wallet_payment(
|
||||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
||||||
) -> Payment | None:
|
) -> Payment | None:
|
||||||
|
wallet = await get_wallet(wallet_id)
|
||||||
|
if not wallet or not wallet.can_view_payments:
|
||||||
|
return None
|
||||||
payment = await (conn or db).fetchone(
|
payment = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
WHERE wallet_id = :wallet AND payment_hash = :hash
|
WHERE wallet_id = :wallet AND payment_hash = :hash
|
||||||
""",
|
""",
|
||||||
{"wallet": wallet_id, "hash": payment_hash},
|
{"wallet": wallet.source_wallet_id, "hash": payment_hash},
|
||||||
Payment,
|
Payment,
|
||||||
)
|
)
|
||||||
return payment
|
return payment
|
||||||
|
|
@ -128,7 +134,11 @@ async def get_payments_paginated( # noqa: C901
|
||||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||||
|
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
values["wallet_id"] = wallet_id
|
wallet = await get_wallet(wallet_id)
|
||||||
|
if not wallet or not wallet.can_view_payments:
|
||||||
|
return Page(data=[], total=0)
|
||||||
|
|
||||||
|
values["wallet_id"] = wallet.source_wallet_id
|
||||||
clause.append("wallet_id = :wallet_id")
|
clause.append("wallet_id = :wallet_id")
|
||||||
elif user_id:
|
elif user_id:
|
||||||
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||||
|
|
@ -320,7 +330,7 @@ async def get_payments_history(
|
||||||
|
|
||||||
date_trunc = db.datetime_grouping(group)
|
date_trunc = db.datetime_grouping(group)
|
||||||
|
|
||||||
values = {
|
values: dict[str, Any] = {
|
||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
}
|
}
|
||||||
# count outgoing payments if they are still pending
|
# count outgoing payments if they are still pending
|
||||||
|
|
@ -350,10 +360,10 @@ async def get_payments_history(
|
||||||
)
|
)
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
wallet = await get_wallet(wallet_id)
|
wallet = await get_wallet(wallet_id)
|
||||||
if wallet:
|
if not wallet or not wallet.can_view_payments:
|
||||||
|
return []
|
||||||
balance = wallet.balance_msat
|
balance = wallet.balance_msat
|
||||||
else:
|
values["wallet_id"] = wallet.source_wallet_id
|
||||||
raise ValueError("Unknown wallet")
|
|
||||||
else:
|
else:
|
||||||
balance = await get_total_balance()
|
balance = await get_total_balance()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from time import time
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.wallets import WalletsFilters
|
from lnbits.core.models.wallets import WalletsFilters, WalletType
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
@ -14,17 +14,22 @@ async def create_wallet(
|
||||||
*,
|
*,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
wallet_name: str | None = None,
|
wallet_name: str | None = None,
|
||||||
|
wallet_type: WalletType = WalletType.LIGHTNING,
|
||||||
|
shared_wallet_id: str | None = None,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet_id = uuid4().hex
|
wallet_id = uuid4().hex
|
||||||
wallet = Wallet(
|
wallet = Wallet(
|
||||||
id=wallet_id,
|
id=wallet_id,
|
||||||
name=wallet_name or settings.lnbits_default_wallet_name,
|
name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
|
wallet_type=wallet_type.value,
|
||||||
|
shared_wallet_id=shared_wallet_id,
|
||||||
user=user_id,
|
user=user_id,
|
||||||
adminkey=uuid4().hex,
|
adminkey=uuid4().hex,
|
||||||
inkey=uuid4().hex,
|
inkey=uuid4().hex,
|
||||||
currency=settings.lnbits_default_accounting_currency or "USD",
|
currency=settings.lnbits_default_accounting_currency or "USD",
|
||||||
)
|
)
|
||||||
|
|
||||||
await (conn or db).insert("wallets", wallet)
|
await (conn or db).insert("wallets", wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
|
@ -103,7 +108,7 @@ async def delete_unused_wallets(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_wallet(
|
async def get_standalone_wallet(
|
||||||
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
query = """
|
query = """
|
||||||
|
|
@ -121,8 +126,23 @@ async def get_wallet(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_wallet(
|
||||||
|
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
|
) -> Wallet | None:
|
||||||
|
wallet = await get_standalone_wallet(wallet_id, deleted, conn)
|
||||||
|
if not wallet:
|
||||||
|
return None
|
||||||
|
if wallet.is_lightning_shared_wallet:
|
||||||
|
return await get_source_wallet(wallet, conn)
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets(
|
async def get_wallets(
|
||||||
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
user_id: str,
|
||||||
|
deleted: bool | None = False,
|
||||||
|
wallet_type: WalletType | None = None,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> list[Wallet]:
|
) -> list[Wallet]:
|
||||||
query = """
|
query = """
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
|
|
@ -132,12 +152,20 @@ async def get_wallets(
|
||||||
"""
|
"""
|
||||||
if deleted is not None:
|
if deleted is not None:
|
||||||
query += " AND deleted = :deleted "
|
query += " AND deleted = :deleted "
|
||||||
return await (conn or db).fetchall(
|
if wallet_type is not None:
|
||||||
|
query += " AND wallet_type = :wallet_type "
|
||||||
|
wallets = await (conn or db).fetchall(
|
||||||
query,
|
query,
|
||||||
{"user": user_id, "deleted": deleted},
|
{
|
||||||
|
"user": user_id,
|
||||||
|
"deleted": deleted,
|
||||||
|
"wallet_type": wallet_type.value if wallet_type else None,
|
||||||
|
},
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return await get_source_wallets(wallets, conn)
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_paginated(
|
async def get_wallets_paginated(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|
@ -149,7 +177,7 @@ async def get_wallets_paginated(
|
||||||
deleted = False
|
deleted = False
|
||||||
|
|
||||||
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
||||||
return await (conn or db).fetch_page(
|
wallets = await (conn or db).fetch_page(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
|
|
@ -161,18 +189,24 @@ async def get_wallets_paginated(
|
||||||
model=Wallet,
|
model=Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
wallets.data = await get_source_wallets(wallets.data, conn)
|
||||||
|
return wallets
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_ids(
|
async def get_wallets_ids(
|
||||||
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
query = """SELECT id FROM wallets WHERE "user" = :user"""
|
query = """SELECT * FROM wallets WHERE "user" = :user"""
|
||||||
if deleted is not None:
|
if deleted is not None:
|
||||||
query += " AND deleted = :deleted "
|
query += " AND deleted = :deleted "
|
||||||
result: list[dict] = await (conn or db).fetchall(
|
wallets = await (conn or db).fetchall(
|
||||||
query,
|
query,
|
||||||
{"user": user_id, "deleted": deleted},
|
{"user": user_id, "deleted": deleted},
|
||||||
|
Wallet,
|
||||||
)
|
)
|
||||||
return [row["id"] for row in result]
|
|
||||||
|
wallets = await get_source_wallets(wallets, conn)
|
||||||
|
return [w.source_wallet_id for w in wallets if w.can_view_payments]
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_count():
|
async def get_wallets_count():
|
||||||
|
|
@ -185,7 +219,7 @@ async def get_wallet_for_key(
|
||||||
key: str,
|
key: str,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
return await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
|
|
@ -196,6 +230,39 @@ async def get_wallet_for_key(
|
||||||
{"key": key},
|
{"key": key},
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
if not wallet:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if wallet.is_lightning_shared_wallet:
|
||||||
|
mw = await get_source_wallet(wallet, conn)
|
||||||
|
return mw
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
async def get_source_wallet(
|
||||||
|
wallet: Wallet, conn: Connection | None = None
|
||||||
|
) -> Wallet | None:
|
||||||
|
if not wallet.is_lightning_shared_wallet:
|
||||||
|
return wallet
|
||||||
|
if not wallet.shared_wallet_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
shared_wallet = await get_standalone_wallet(wallet.shared_wallet_id, False, conn)
|
||||||
|
if not shared_wallet:
|
||||||
|
return None
|
||||||
|
wallet.mirror_shared_wallet(shared_wallet)
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
async def get_source_wallets(
|
||||||
|
wallet: list[Wallet], conn: Connection | None = None
|
||||||
|
) -> list[Wallet]:
|
||||||
|
source_wallets = []
|
||||||
|
for w in wallet:
|
||||||
|
source_wallet = await get_source_wallet(w, conn)
|
||||||
|
if source_wallet:
|
||||||
|
source_wallets.append(source_wallet)
|
||||||
|
return source_wallets
|
||||||
|
|
||||||
|
|
||||||
async def get_total_balance(conn: Connection | None = None):
|
async def get_total_balance(conn: Connection | None = None):
|
||||||
|
|
|
||||||
|
|
@ -743,3 +743,19 @@ async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
||||||
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m035_add_wallet_type_column(db: Connection):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m036_add_shared_wallet_column(db: Connection):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ class UserNotifications(BaseModel):
|
||||||
incoming_payments_sats: int = 0
|
incoming_payments_sats: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class WalletInviteRequest(BaseModel):
|
||||||
|
request_id: str
|
||||||
|
from_user_name: str | None = None
|
||||||
|
to_wallet_id: str
|
||||||
|
to_wallet_name: str
|
||||||
|
|
||||||
|
|
||||||
class UserExtra(BaseModel):
|
class UserExtra(BaseModel):
|
||||||
email_verified: bool | None = False
|
email_verified: bool | None = False
|
||||||
first_name: str | None = None
|
first_name: str | None = None
|
||||||
|
|
@ -46,6 +53,41 @@ class UserExtra(BaseModel):
|
||||||
|
|
||||||
notifications: UserNotifications = UserNotifications()
|
notifications: UserNotifications = UserNotifications()
|
||||||
|
|
||||||
|
wallet_invite_requests: list[WalletInviteRequest] = []
|
||||||
|
|
||||||
|
def add_wallet_invite_request(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
to_wallet_id: str,
|
||||||
|
to_wallet_name: str,
|
||||||
|
from_user_name: str | None = None,
|
||||||
|
) -> WalletInviteRequest:
|
||||||
|
self.remove_wallet_invite_request(request_id)
|
||||||
|
invite = WalletInviteRequest(
|
||||||
|
request_id=request_id,
|
||||||
|
from_user_name=from_user_name,
|
||||||
|
to_wallet_id=to_wallet_id,
|
||||||
|
to_wallet_name=to_wallet_name,
|
||||||
|
)
|
||||||
|
self.wallet_invite_requests.append(invite)
|
||||||
|
return invite
|
||||||
|
|
||||||
|
def find_wallet_invite_request(self, request_id: str) -> WalletInviteRequest | None:
|
||||||
|
for invite in self.wallet_invite_requests:
|
||||||
|
if invite.request_id == request_id:
|
||||||
|
return invite
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_wallet_invite_request(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
):
|
||||||
|
self.wallet_invite_requests = [
|
||||||
|
invite
|
||||||
|
for invite in self.wallet_invite_requests
|
||||||
|
if invite.request_id != request_id
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class EndpointAccess(BaseModel):
|
class EndpointAccess(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,95 @@ class BaseWallet(BaseModel):
|
||||||
balance_msat: int
|
balance_msat: int
|
||||||
|
|
||||||
|
|
||||||
|
class WalletType(Enum):
|
||||||
|
LIGHTNING = "lightning"
|
||||||
|
LIGHTNING_SHARED = "lightning-shared"
|
||||||
|
|
||||||
|
|
||||||
|
class WalletPermission(Enum):
|
||||||
|
VIEW_PAYMENTS = "view-payments"
|
||||||
|
RECEIVE_PAYMENTS = "receive-payments"
|
||||||
|
SEND_PAYMENTS = "send-payments"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class WalletShareStatus(Enum):
|
||||||
|
INVITE_SENT = "invite_sent"
|
||||||
|
APPROVED = "approved"
|
||||||
|
|
||||||
|
|
||||||
|
class WalletSharePermission(BaseModel):
|
||||||
|
# unique identifier for this share request
|
||||||
|
request_id: str | None = None
|
||||||
|
# username of the invited user
|
||||||
|
username: str
|
||||||
|
# ID of the wallet being shared with
|
||||||
|
shared_with_wallet_id: str | None = None
|
||||||
|
# permissions being granted
|
||||||
|
permissions: list[WalletPermission] = []
|
||||||
|
# status of the share request
|
||||||
|
status: WalletShareStatus
|
||||||
|
comment: str | None = None
|
||||||
|
|
||||||
|
def approve(
|
||||||
|
self,
|
||||||
|
permissions: list[WalletPermission] | None = None,
|
||||||
|
shared_with_wallet_id: str | None = None,
|
||||||
|
):
|
||||||
|
self.status = WalletShareStatus.APPROVED
|
||||||
|
if permissions is not None:
|
||||||
|
self.permissions = permissions
|
||||||
|
if shared_with_wallet_id is not None:
|
||||||
|
self.shared_with_wallet_id = shared_with_wallet_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_approved(self) -> bool:
|
||||||
|
return self.status == WalletShareStatus.APPROVED
|
||||||
|
|
||||||
|
|
||||||
class WalletExtra(BaseModel):
|
class WalletExtra(BaseModel):
|
||||||
icon: str = "flash_on"
|
icon: str = "flash_on"
|
||||||
color: str = "primary"
|
color: str = "primary"
|
||||||
pinned: bool = False
|
pinned: bool = False
|
||||||
|
# What permissions this wallet grants when it's shared with other users
|
||||||
|
shared_with: list[WalletSharePermission] = []
|
||||||
|
|
||||||
|
def invite_user_to_shared_wallet(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
request_type: WalletShareStatus,
|
||||||
|
username: str,
|
||||||
|
permissions: list[WalletPermission] | None = None,
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
share = WalletSharePermission(
|
||||||
|
request_id=request_id,
|
||||||
|
username=username,
|
||||||
|
status=request_type,
|
||||||
|
permissions=permissions or [],
|
||||||
|
)
|
||||||
|
self.shared_with.append(share)
|
||||||
|
return share
|
||||||
|
|
||||||
|
def find_share_by_id(self, request_id: str) -> WalletSharePermission | None:
|
||||||
|
for share in self.shared_with:
|
||||||
|
if share.request_id == request_id:
|
||||||
|
return share
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_share_for_wallet(
|
||||||
|
self, shared_with_wallet_id: str
|
||||||
|
) -> WalletSharePermission | None:
|
||||||
|
for share in self.shared_with:
|
||||||
|
if share.shared_with_wallet_id == shared_with_wallet_id:
|
||||||
|
return share
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_share_by_id(self, request_id: str):
|
||||||
|
self.shared_with = [
|
||||||
|
share for share in self.shared_with if share.request_id != request_id
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Wallet(BaseModel):
|
class Wallet(BaseModel):
|
||||||
|
|
@ -33,6 +118,9 @@ class Wallet(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
adminkey: str
|
adminkey: str
|
||||||
inkey: str
|
inkey: str
|
||||||
|
wallet_type: str = WalletType.LIGHTNING.value
|
||||||
|
# Must be set only for shared wallets
|
||||||
|
shared_wallet_id: str | None = None
|
||||||
deleted: bool = False
|
deleted: bool = False
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
@ -40,6 +128,65 @@ class Wallet(BaseModel):
|
||||||
balance_msat: int = Field(default=0, no_database=True)
|
balance_msat: int = Field(default=0, no_database=True)
|
||||||
extra: WalletExtra = WalletExtra()
|
extra: WalletExtra = WalletExtra()
|
||||||
stored_paylinks: StoredPayLinks = StoredPayLinks()
|
stored_paylinks: StoredPayLinks = StoredPayLinks()
|
||||||
|
# What permission this wallet has when it's a shared wallet
|
||||||
|
share_permissions: list[WalletPermission] = Field(default=[], no_database=True)
|
||||||
|
|
||||||
|
def __init__(self, **data):
|
||||||
|
super().__init__(**data)
|
||||||
|
self._validate_data()
|
||||||
|
|
||||||
|
def mirror_shared_wallet(
|
||||||
|
self,
|
||||||
|
shared_wallet: Wallet,
|
||||||
|
):
|
||||||
|
if not shared_wallet.is_lightning_wallet:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.wallet_type = WalletType.LIGHTNING_SHARED.value
|
||||||
|
self.shared_wallet_id = shared_wallet.id
|
||||||
|
self.name = shared_wallet.name
|
||||||
|
self.share_permissions = shared_wallet.get_share_permissions(self.id)
|
||||||
|
|
||||||
|
if len(self.share_permissions):
|
||||||
|
self.currency = shared_wallet.currency
|
||||||
|
self.balance_msat = shared_wallet.balance_msat
|
||||||
|
|
||||||
|
self.stored_paylinks = shared_wallet.stored_paylinks
|
||||||
|
self.extra.icon = shared_wallet.extra.icon
|
||||||
|
self.extra.color = shared_wallet.extra.color
|
||||||
|
|
||||||
|
def get_share_permissions(self, wallet_id: str) -> list[WalletPermission]:
|
||||||
|
for share in self.extra.shared_with:
|
||||||
|
if share.shared_with_wallet_id == wallet_id and share.is_approved:
|
||||||
|
return share.permissions
|
||||||
|
return []
|
||||||
|
|
||||||
|
def has_permission(self, permission: WalletPermission) -> bool:
|
||||||
|
if self.is_lightning_wallet:
|
||||||
|
return True
|
||||||
|
if self.is_lightning_shared_wallet:
|
||||||
|
return permission in self.share_permissions
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_wallet_id(self) -> str:
|
||||||
|
"""For shared wallets return the original wallet ID, else return own ID."""
|
||||||
|
if self.is_lightning_shared_wallet and len(self.share_permissions):
|
||||||
|
return self.shared_wallet_id or self.id
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_receive_payments(self) -> bool:
|
||||||
|
return self.has_permission(WalletPermission.RECEIVE_PAYMENTS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_send_payments(self) -> bool:
|
||||||
|
return self.has_permission(WalletPermission.SEND_PAYMENTS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_view_payments(self) -> bool:
|
||||||
|
return self.has_permission(WalletPermission.VIEW_PAYMENTS)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def balance(self) -> int:
|
def balance(self) -> int:
|
||||||
|
|
@ -57,9 +204,24 @@ class Wallet(BaseModel):
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_lightning_wallet(self) -> bool:
|
||||||
|
return self.wallet_type == WalletType.LIGHTNING.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_lightning_shared_wallet(self) -> bool:
|
||||||
|
return self.wallet_type == WalletType.LIGHTNING_SHARED.value
|
||||||
|
|
||||||
|
def _validate_data(self):
|
||||||
|
if self.is_lightning_shared_wallet:
|
||||||
|
if not self.shared_wallet_id:
|
||||||
|
raise ValueError("Shared wallet ID must be set for shared wallets.")
|
||||||
|
|
||||||
|
|
||||||
class CreateWallet(BaseModel):
|
class CreateWallet(BaseModel):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
|
wallet_type: WalletType = WalletType.LIGHTNING
|
||||||
|
shared_wallet_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class KeyType(Enum):
|
class KeyType(Enum):
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from lnbits.core.crud import (
|
||||||
mark_webhook_sent,
|
mark_webhook_sent,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_user
|
from lnbits.core.crud.users import get_user
|
||||||
|
from lnbits.core.crud.wallets import get_wallet
|
||||||
from lnbits.core.models import Payment, Wallet
|
from lnbits.core.models import Payment, Wallet
|
||||||
from lnbits.core.models.notifications import (
|
from lnbits.core.models.notifications import (
|
||||||
NOTIFICATION_TEMPLATES,
|
NOTIFICATION_TEMPLATES,
|
||||||
|
|
@ -257,6 +258,12 @@ async def dispatch_webhook(payment: Payment):
|
||||||
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
try:
|
try:
|
||||||
await send_ws_payment_notification(wallet, payment)
|
await send_ws_payment_notification(wallet, payment)
|
||||||
|
for shared in wallet.extra.shared_with:
|
||||||
|
if not shared.shared_with_wallet_id:
|
||||||
|
continue
|
||||||
|
shared_wallet = await get_wallet(shared.shared_with_wallet_id)
|
||||||
|
if shared_wallet and shared_wallet.can_view_payments:
|
||||||
|
await send_ws_payment_notification(shared_wallet, payment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending websocket payment notification {e!s}")
|
logger.error(f"Error sending websocket payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,11 @@ async def pay_invoice(
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
amount_msat = invoice.amount_msat
|
amount_msat = invoice.amount_msat
|
||||||
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
||||||
|
if not wallet.can_send_payments:
|
||||||
|
raise PaymentError(
|
||||||
|
"Wallet does not have permission to pay invoices.",
|
||||||
|
status="failed",
|
||||||
|
)
|
||||||
|
|
||||||
if await is_internal_status_success(invoice.payment_hash, new_conn):
|
if await is_internal_status_success(invoice.payment_hash, new_conn):
|
||||||
raise PaymentError("Internal invoice already paid.", status="failed")
|
raise PaymentError("Internal invoice already paid.", status="failed")
|
||||||
|
|
@ -79,7 +84,7 @@ async def pay_invoice(
|
||||||
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet.source_wallet_id,
|
||||||
bolt11=payment_request,
|
bolt11=payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
amount_msat=-amount_msat,
|
amount_msat=-amount_msat,
|
||||||
|
|
@ -88,7 +93,7 @@ async def pay_invoice(
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
payment = await _pay_invoice(wallet.source_wallet_id, create_payment_model, conn)
|
||||||
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
await _credit_service_fee_wallet(wallet, payment, new_conn)
|
await _credit_service_fee_wallet(wallet, payment, new_conn)
|
||||||
|
|
@ -250,6 +255,12 @@ async def create_invoice(
|
||||||
if not user_wallet:
|
if not user_wallet:
|
||||||
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
|
||||||
|
if not user_wallet.can_receive_payments:
|
||||||
|
raise InvoiceError(
|
||||||
|
"Wallet does not have permission to create invoices.",
|
||||||
|
status="failed",
|
||||||
|
)
|
||||||
|
|
||||||
invoice_memo = None if description_hash else memo[:640]
|
invoice_memo = None if description_hash else memo[:640]
|
||||||
|
|
||||||
# use the fake wallet if the invoice is for internal use only
|
# use the fake wallet if the invoice is for internal use only
|
||||||
|
|
@ -308,7 +319,7 @@ async def create_invoice(
|
||||||
invoice = bolt11_decode(invoice_response.payment_request)
|
invoice = bolt11_decode(invoice_response.payment_request)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet_id,
|
wallet_id=user_wallet.source_wallet_id,
|
||||||
bolt11=invoice_response.payment_request,
|
bolt11=invoice_response.payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
preimage=invoice_response.preimage,
|
preimage=invoice_response.preimage,
|
||||||
|
|
@ -456,7 +467,7 @@ async def update_wallet_balance(
|
||||||
await create_payment(
|
await create_payment(
|
||||||
checking_id=f"internal_{payment_hash}",
|
checking_id=f"internal_{payment_hash}",
|
||||||
data=CreatePayment(
|
data=CreatePayment(
|
||||||
wallet_id=wallet.id,
|
wallet_id=wallet.source_wallet_id,
|
||||||
bolt11=bolt11,
|
bolt11=bolt11,
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
amount_msat=amount * 1000,
|
amount_msat=amount * 1000,
|
||||||
|
|
@ -475,7 +486,7 @@ async def update_wallet_balance(
|
||||||
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
payment = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=wallet.id,
|
wallet_id=wallet.source_wallet_id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
memo="Admin credit",
|
memo="Admin credit",
|
||||||
internal=True,
|
internal=True,
|
||||||
|
|
@ -910,7 +921,7 @@ async def _credit_service_fee_wallet(
|
||||||
|
|
||||||
memo = f"""
|
memo = f"""
|
||||||
Service fee for payment of {abs(payment.sat)} sats.
|
Service fee for payment of {abs(payment.sat)} sats.
|
||||||
Wallet: '{wallet.name}' ({wallet.id})."""
|
Wallet: '{wallet.name}' ({wallet.source_wallet_id})."""
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=settings.lnbits_service_fee_wallet,
|
wallet_id=settings.lnbits_service_fee_wallet,
|
||||||
|
|
|
||||||
202
lnbits/core/services/wallets.py
Normal file
202
lnbits/core/services/wallets.py
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
from lnbits.core.crud.users import (
|
||||||
|
get_account,
|
||||||
|
get_account_by_username_or_email,
|
||||||
|
update_account,
|
||||||
|
)
|
||||||
|
from lnbits.core.crud.wallets import (
|
||||||
|
create_wallet,
|
||||||
|
force_delete_wallet,
|
||||||
|
get_standalone_wallet,
|
||||||
|
get_wallet,
|
||||||
|
get_wallets,
|
||||||
|
update_wallet,
|
||||||
|
)
|
||||||
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.models.users import Account
|
||||||
|
from lnbits.core.models.wallets import (
|
||||||
|
Wallet,
|
||||||
|
WalletSharePermission,
|
||||||
|
WalletShareStatus,
|
||||||
|
WalletType,
|
||||||
|
)
|
||||||
|
from lnbits.db import Connection
|
||||||
|
from lnbits.helpers import sha256s
|
||||||
|
|
||||||
|
|
||||||
|
async def invite_to_wallet(
|
||||||
|
source_wallet: Wallet, data: WalletSharePermission
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
if not source_wallet.is_lightning_wallet:
|
||||||
|
raise ValueError("Only lightning wallets can be shared.")
|
||||||
|
if not data.username:
|
||||||
|
raise ValueError("Username or email missing.")
|
||||||
|
invited_user = await get_account_by_username_or_email(data.username)
|
||||||
|
if not invited_user:
|
||||||
|
raise ValueError("Invited user not found.")
|
||||||
|
|
||||||
|
request_id = sha256s(invited_user.id + source_wallet.id)
|
||||||
|
share = source_wallet.extra.find_share_by_id(request_id)
|
||||||
|
if share:
|
||||||
|
raise ValueError("User already invited to this wallet.")
|
||||||
|
|
||||||
|
invite_request = source_wallet.extra.invite_user_to_shared_wallet(
|
||||||
|
request_id=request_id,
|
||||||
|
request_type=WalletShareStatus.INVITE_SENT,
|
||||||
|
username=data.username,
|
||||||
|
permissions=data.permissions,
|
||||||
|
)
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
|
||||||
|
wallet_owner = await get_account(source_wallet.user)
|
||||||
|
if not wallet_owner:
|
||||||
|
raise ValueError("Cannot find wallet owner.")
|
||||||
|
invited_user.extra.add_wallet_invite_request(
|
||||||
|
request_id=request_id,
|
||||||
|
from_user_name=wallet_owner.username or wallet_owner.email,
|
||||||
|
to_wallet_id=source_wallet.id,
|
||||||
|
to_wallet_name=source_wallet.name,
|
||||||
|
)
|
||||||
|
await update_account(invited_user)
|
||||||
|
|
||||||
|
return invite_request
|
||||||
|
|
||||||
|
|
||||||
|
async def reject_wallet_invitation(invited_user_id: str, share_request_id: str):
|
||||||
|
invited_user = await get_account(invited_user_id)
|
||||||
|
if not invited_user:
|
||||||
|
raise ValueError("Invited user not found.")
|
||||||
|
|
||||||
|
existing_request = invited_user.extra.find_wallet_invite_request(share_request_id)
|
||||||
|
if not existing_request:
|
||||||
|
raise ValueError("Invitation not found.")
|
||||||
|
|
||||||
|
invited_user.extra.remove_wallet_invite_request(share_request_id)
|
||||||
|
await update_account(invited_user)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_wallet_share_permissions(
|
||||||
|
source_wallet: Wallet, data: WalletSharePermission
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
if not source_wallet.is_lightning_wallet:
|
||||||
|
raise ValueError("Only lightning wallets can be shared.")
|
||||||
|
if not data.shared_with_wallet_id:
|
||||||
|
raise ValueError("Wallet ID missing.")
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_for_wallet(data.shared_with_wallet_id)
|
||||||
|
if not share:
|
||||||
|
raise ValueError("Share not found")
|
||||||
|
|
||||||
|
if not share.shared_with_wallet_id:
|
||||||
|
raise ValueError("Share does not have a mirror wallet ID.")
|
||||||
|
|
||||||
|
mirror_wallet = await get_wallet(share.shared_with_wallet_id)
|
||||||
|
if not mirror_wallet:
|
||||||
|
raise ValueError("Target wallet not found")
|
||||||
|
if not mirror_wallet.is_lightning_shared_wallet:
|
||||||
|
raise ValueError("Target wallet is not a shared wallet.")
|
||||||
|
if mirror_wallet.shared_wallet_id != source_wallet.id:
|
||||||
|
raise ValueError("Not the owner of the shared wallet.")
|
||||||
|
|
||||||
|
share.approve(permissions=data.permissions)
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
return share
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_wallet_share(source_wallet: Wallet, request_id: str) -> SimpleStatus:
|
||||||
|
if not source_wallet.is_lightning_wallet:
|
||||||
|
raise ValueError("Source wallet is not a lightning wallet.")
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_by_id(request_id)
|
||||||
|
if not share:
|
||||||
|
raise ValueError("Wallet share not found.")
|
||||||
|
source_wallet.extra.remove_share_by_id(request_id)
|
||||||
|
|
||||||
|
invited_user = await get_account_by_username_or_email(share.username)
|
||||||
|
if not invited_user:
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True, message="Permission removed. Invited user not found."
|
||||||
|
)
|
||||||
|
if invited_user.extra.find_wallet_invite_request(request_id):
|
||||||
|
invited_user.extra.remove_wallet_invite_request(request_id)
|
||||||
|
await update_account(invited_user)
|
||||||
|
|
||||||
|
mirror_wallets = await get_wallets(
|
||||||
|
invited_user.id, wallet_type=WalletType.LIGHTNING_SHARED
|
||||||
|
)
|
||||||
|
mirror_wallet = next(
|
||||||
|
(w for w in mirror_wallets if w.shared_wallet_id == source_wallet.id), None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not mirror_wallet:
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
return SimpleStatus(
|
||||||
|
success=True, message="Permission removed. Target wallet not found."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not mirror_wallet.is_lightning_shared_wallet:
|
||||||
|
raise ValueError("Target wallet is not a shared lightning wallet.")
|
||||||
|
|
||||||
|
if mirror_wallet.shared_wallet_id != source_wallet.id:
|
||||||
|
raise ValueError("Not the owner of the shared wallet.")
|
||||||
|
|
||||||
|
await force_delete_wallet(mirror_wallet.id)
|
||||||
|
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
return SimpleStatus(success=True, message="Permission removed.")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_lightning_shared_wallet(
|
||||||
|
user_id: str,
|
||||||
|
source_wallet_id: str,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Wallet:
|
||||||
|
source_wallet = await get_standalone_wallet(source_wallet_id, conn=conn)
|
||||||
|
if not source_wallet:
|
||||||
|
raise ValueError("Shared wallet does not exist.")
|
||||||
|
|
||||||
|
if not source_wallet.is_lightning_wallet:
|
||||||
|
raise ValueError("Shared wallet is not a lightning wallet.")
|
||||||
|
|
||||||
|
if source_wallet.user == user_id:
|
||||||
|
raise ValueError("Cannot mirror your own wallet.")
|
||||||
|
|
||||||
|
invited_user = await get_account(user_id, conn=conn)
|
||||||
|
if not invited_user:
|
||||||
|
raise ValueError("Cannot find invited user.")
|
||||||
|
|
||||||
|
return await _accept_invitation_to_shared_wallet(
|
||||||
|
invited_user, source_wallet, conn=conn
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _accept_invitation_to_shared_wallet(
|
||||||
|
invited_user: Account,
|
||||||
|
source_wallet: Wallet,
|
||||||
|
conn: Connection | None = None,
|
||||||
|
) -> Wallet:
|
||||||
|
request_id = sha256s(invited_user.id + source_wallet.id)
|
||||||
|
existing_request = source_wallet.extra.find_share_by_id(request_id)
|
||||||
|
if not existing_request:
|
||||||
|
raise ValueError("No invitation found for this invited user.")
|
||||||
|
if existing_request.status == WalletShareStatus.APPROVED:
|
||||||
|
raise ValueError("This wallet is already shared with you.")
|
||||||
|
if existing_request.status != WalletShareStatus.INVITE_SENT:
|
||||||
|
raise ValueError("Unknown request type.")
|
||||||
|
|
||||||
|
invited_user.extra.remove_wallet_invite_request(request_id)
|
||||||
|
await update_account(invited_user)
|
||||||
|
|
||||||
|
# todo: double check if user already has a mirror wallet for this source wallet
|
||||||
|
|
||||||
|
mirror_wallet = await create_wallet(
|
||||||
|
user_id=invited_user.id,
|
||||||
|
wallet_name=source_wallet.name,
|
||||||
|
wallet_type=WalletType.LIGHTNING_SHARED,
|
||||||
|
shared_wallet_id=source_wallet.id,
|
||||||
|
conn=conn,
|
||||||
|
)
|
||||||
|
existing_request.approve(shared_with_wallet_id=mirror_wallet.id)
|
||||||
|
await update_wallet(source_wallet, conn=conn)
|
||||||
|
mirror_wallet.mirror_shared_wallet(source_wallet)
|
||||||
|
return mirror_wallet
|
||||||
251
lnbits/core/templates/core/_wallet_share.html
Normal file
251
lnbits/core/templates/core/_wallet_share.html
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
<q-expansion-item
|
||||||
|
v-if="wallet.walletType == 'lightning'"
|
||||||
|
group="extras"
|
||||||
|
icon="share"
|
||||||
|
:label="$t('share_wallet')"
|
||||||
|
>
|
||||||
|
<template v-slot:header>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="share" style="margin-left: -5px" />
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
<span v-text="$t('share_wallet')"></span>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section side v-if="walletPendingRequests.length">
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-icon name="hail" color="secondary" size="24px" />
|
||||||
|
<span v-text="walletPendingRequests.length"></span>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</template>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
You can invite other users to have access to this wallet.
|
||||||
|
<br />
|
||||||
|
The access is limitted by the permission you grant.
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5">
|
||||||
|
<q-input
|
||||||
|
v-model="walletShareInvite.username"
|
||||||
|
@keyup.enter="inviteUserToWallet()"
|
||||||
|
label="Username"
|
||||||
|
hint="Invite user to this wallet"
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-select
|
||||||
|
:options="permissionOptions"
|
||||||
|
v-model="walletShareInvite.permissions"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
dense
|
||||||
|
class="q-pl-md"
|
||||||
|
hint="Select permissions for this user"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-1">
|
||||||
|
<q-btn
|
||||||
|
@click="inviteUserToWallet()"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
icon="person_add_alt"
|
||||||
|
class="float-right"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator class="q-mt-lg"></q-separator>
|
||||||
|
<q-expansion-item
|
||||||
|
group="wallet_shares"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
icon="share"
|
||||||
|
:label="'Shared With (' + walletApprovedShares.length + ')'"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section v-if="walletApprovedShares.length">
|
||||||
|
<div v-for="share in walletApprovedShares" class="row q-mb-xs">
|
||||||
|
<div class="col-3 q-mt-md">
|
||||||
|
<strong v-text="share.username"></strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-1 q-mt-sm">
|
||||||
|
<q-icon v-if="share.comment" name="add_comment">
|
||||||
|
<q-tooltip v-text="share.comment"></q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-select
|
||||||
|
v-model="share.permissions"
|
||||||
|
:options="permissionOptions"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
dense
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-1 q-mt-sm">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="red"
|
||||||
|
icon="delete"
|
||||||
|
outline
|
||||||
|
class="full-width"
|
||||||
|
@click="deleteSharePermission(share)"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-1 q-mt-sm">
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
color="primary"
|
||||||
|
icon="check"
|
||||||
|
class="full-width"
|
||||||
|
@click="updateSharePermissions(share)"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-else>
|
||||||
|
<span>This wallet is not shared with anyone.</span>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item
|
||||||
|
group="wallet_shares"
|
||||||
|
dense
|
||||||
|
expand-separator
|
||||||
|
icon="group_add"
|
||||||
|
:label="'Pending Invitations (' + walletPendingInvites.length + ')'"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section v-if="walletPendingInvites.length">
|
||||||
|
<div v-for="share in walletPendingInvites" class="row q-mb-xs">
|
||||||
|
<div class="col-3 q-mt-md">
|
||||||
|
<strong v-text="share.username"></strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-8">
|
||||||
|
<q-select
|
||||||
|
v-model="share.permissions"
|
||||||
|
:options="permissionOptions"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
dense
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-1 q-mt-sm">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="red"
|
||||||
|
icon="delete"
|
||||||
|
outline
|
||||||
|
class="full-width"
|
||||||
|
@click="deleteSharePermission(share)"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-else>
|
||||||
|
<span>No pending invites.</span>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<q-card-section> </q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item
|
||||||
|
v-else-if="wallet.walletType == 'lightning-shared'"
|
||||||
|
group="extras"
|
||||||
|
icon="supervisor_account"
|
||||||
|
:label="$t('shared_wallet')"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
This wallet does not belong to you. It is a shared Lightning wallet.
|
||||||
|
<br />
|
||||||
|
The owner can revoke the permissions at any moment.
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-item dense class="q-pa-none">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
<strong>Shared Wallet ID: </strong
|
||||||
|
><em
|
||||||
|
v-text="walletIdHidden ? '****************' : wallet.sharedWalletId"
|
||||||
|
></em>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<div>
|
||||||
|
<q-icon
|
||||||
|
:name="walletIdHidden ? 'visibility_off' : 'visibility'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="walletIdHidden = !walletIdHidden"
|
||||||
|
></q-icon>
|
||||||
|
<q-icon
|
||||||
|
name="content_copy"
|
||||||
|
class="cursor-pointer q-ml-sm"
|
||||||
|
@click="copyText(wallet.sharedWalletId)"
|
||||||
|
></q-icon>
|
||||||
|
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
|
||||||
|
<q-popup-proxy>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<lnbits-qrcode
|
||||||
|
:value="wallet.sharedWalletId"
|
||||||
|
:show-buttons="false"
|
||||||
|
></lnbits-qrcode>
|
||||||
|
</div>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 q-mt-md">
|
||||||
|
<strong>Permissions:</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<q-select
|
||||||
|
v-model="wallet.sharePermissions"
|
||||||
|
:options="permissionOptions"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
dense
|
||||||
|
disable
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-expansion-item
|
||||||
|
v-else
|
||||||
|
group="extras"
|
||||||
|
icon="question_mark"
|
||||||
|
:label="$t('share_wallet')"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
Unknown wallet type:
|
||||||
|
<strong v-text="wallet.walletType" class="q-ml-md"></strong>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
@ -159,6 +159,7 @@
|
||||||
color="primary"
|
color="primary"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
@click="showParseDialog"
|
@click="showParseDialog"
|
||||||
|
:disable="!this.g.wallet.canSendPayments"
|
||||||
:label="$t('paste_request')"
|
:label="$t('paste_request')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -166,12 +167,14 @@
|
||||||
color="primary"
|
color="primary"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
@click="showReceiveDialog"
|
@click="showReceiveDialog"
|
||||||
|
:disable="!this.g.wallet.canReceivePayments"
|
||||||
:label="$t('create_invoice')"
|
:label="$t('create_invoice')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="secondary"
|
color="secondary"
|
||||||
icon="qr_code_scanner"
|
icon="qr_code_scanner"
|
||||||
|
:disable="!this.g.wallet.canReceivePayments && !this.g.wallet.canSendPayments"
|
||||||
@click="showCamera"
|
@click="showCamera"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
|
|
@ -361,6 +364,8 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
{% include "core/_wallet_share.html" %}
|
||||||
|
<q-separator></q-separator>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="extras"
|
group="extras"
|
||||||
icon="phone_android"
|
icon="phone_android"
|
||||||
|
|
@ -509,7 +514,7 @@
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="charts"
|
group="extras"
|
||||||
icon="insights"
|
icon="insights"
|
||||||
:label="$t('wallet_charts')"
|
:label="$t('wallet_charts')"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,25 @@ from fastapi import (
|
||||||
HTTPException,
|
HTTPException,
|
||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallets_paginated
|
from lnbits.core.crud.wallets import (
|
||||||
|
create_wallet,
|
||||||
|
get_wallets_paginated,
|
||||||
|
)
|
||||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
||||||
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
||||||
from lnbits.core.models.wallets import WalletsFilters
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
|
from lnbits.core.models.wallets import (
|
||||||
|
WalletsFilters,
|
||||||
|
WalletSharePermission,
|
||||||
|
WalletType,
|
||||||
|
)
|
||||||
|
from lnbits.core.services.wallets import (
|
||||||
|
create_lightning_shared_wallet,
|
||||||
|
delete_wallet_share,
|
||||||
|
invite_to_wallet,
|
||||||
|
reject_wallet_invitation,
|
||||||
|
update_wallet_share_permissions,
|
||||||
|
)
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
|
|
@ -22,7 +37,6 @@ from lnbits.decorators import (
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_wallet,
|
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
|
|
@ -62,6 +76,35 @@ async def api_wallets_paginated(
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.put("/share/invite")
|
||||||
|
async def api_invite_wallet_share(
|
||||||
|
data: WalletSharePermission, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
return await invite_to_wallet(key_info.wallet, data)
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.delete("/share/invite/{share_request_id}")
|
||||||
|
async def api_reject_wallet_invitation(
|
||||||
|
share_request_id: str, invited_user: User = Depends(check_user_exists)
|
||||||
|
) -> SimpleStatus:
|
||||||
|
await reject_wallet_invitation(invited_user.id, share_request_id)
|
||||||
|
return SimpleStatus(success=True, message="Invitation rejected.")
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.put("/share")
|
||||||
|
async def api_accept_wallet_share_request(
|
||||||
|
data: WalletSharePermission, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> WalletSharePermission:
|
||||||
|
return await update_wallet_share_permissions(key_info.wallet, data)
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.delete("/share/{share_request_id}")
|
||||||
|
async def api_delete_wallet_share_permissions(
|
||||||
|
share_request_id: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> SimpleStatus:
|
||||||
|
return await delete_wallet_share(key_info.wallet, share_request_id)
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.put("/{new_name}")
|
@wallet_router.put("/{new_name}")
|
||||||
async def api_update_wallet_name(
|
async def api_update_wallet_name(
|
||||||
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
|
@ -70,6 +113,7 @@ async def api_update_wallet_name(
|
||||||
if not wallet:
|
if not wallet:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
wallet.name = new_name
|
wallet.name = new_name
|
||||||
|
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
return {
|
return {
|
||||||
"id": wallet.id,
|
"id": wallet.id,
|
||||||
|
|
@ -124,6 +168,7 @@ async def api_update_wallet(
|
||||||
wallet.extra.color = color or wallet.extra.color
|
wallet.extra.color = color or wallet.extra.color
|
||||||
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
|
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
|
||||||
wallet.currency = currency if currency is not None else wallet.currency
|
wallet.currency = currency if currency is not None else wallet.currency
|
||||||
|
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
|
@ -147,4 +192,21 @@ async def api_create_wallet(
|
||||||
data: CreateWallet,
|
data: CreateWallet,
|
||||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
|
if data.wallet_type == WalletType.LIGHTNING:
|
||||||
return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
|
return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
|
||||||
|
|
||||||
|
if data.wallet_type == WalletType.LIGHTNING_SHARED:
|
||||||
|
if not data.shared_wallet_id:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Shared wallet ID is required for shared wallets.",
|
||||||
|
)
|
||||||
|
return await create_lightning_shared_wallet(
|
||||||
|
user_id=key_info.wallet.user,
|
||||||
|
source_wallet_id=data.shared_wallet_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
f"Unknown wallet type: {data.wallet_type}.",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -594,6 +594,13 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class DbJsonEncoder(json.JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if isinstance(o, Enum):
|
||||||
|
return o.value
|
||||||
|
return super().default(o)
|
||||||
|
|
||||||
|
|
||||||
def insert_query(table_name: str, model: BaseModel) -> str:
|
def insert_query(table_name: str, model: BaseModel) -> str:
|
||||||
"""
|
"""
|
||||||
Generate an insert query with placeholders for a given table and model
|
Generate an insert query with placeholders for a given table and model
|
||||||
|
|
@ -648,7 +655,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
||||||
or type_ is dict
|
or type_ is dict
|
||||||
or get_origin(outertype_) is list
|
or get_origin(outertype_) is list
|
||||||
):
|
):
|
||||||
_dict[key] = json.dumps(value)
|
_dict[key] = json.dumps(value, cls=DbJsonEncoder)
|
||||||
continue
|
continue
|
||||||
_dict[key] = value
|
_dict[key] = value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -399,3 +399,11 @@ def is_snake_case(v: str) -> bool:
|
||||||
|
|
||||||
def lowercase_first_letter(s: str) -> str:
|
def lowercase_first_letter(s: str) -> str:
|
||||||
return s[:1].lower() + s[1:] if s else s
|
return s[:1].lower() + s[1:] if s else s
|
||||||
|
|
||||||
|
|
||||||
|
def sha256s(value: str) -> str:
|
||||||
|
"""
|
||||||
|
SHA256 applied on a string value.
|
||||||
|
Returns the hex as a string.
|
||||||
|
"""
|
||||||
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
||||||
|
|
|
||||||
2
lnbits/static/bundle-components.min.js
vendored
2
lnbits/static/bundle-components.min.js
vendored
File diff suppressed because one or more lines are too long
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
|
|
@ -52,9 +52,17 @@ window.localisation.en = {
|
||||||
stored_paylinks: 'Stored LNURL pay links',
|
stored_paylinks: 'Stored LNURL pay links',
|
||||||
wallet: 'Wallet: ',
|
wallet: 'Wallet: ',
|
||||||
wallet_name: 'Wallet name',
|
wallet_name: 'Wallet name',
|
||||||
|
wallet_type: 'Wallet type',
|
||||||
|
shared_wallet: 'Shared Wallet',
|
||||||
|
share_wallet: 'Share Wallet',
|
||||||
|
update_permissions: 'Update Permissions',
|
||||||
|
shared_wallet_id: 'Shared Wallet ID',
|
||||||
|
shared_wallet_desc:
|
||||||
|
"You have been invited to have access to someone else's wallet.",
|
||||||
wallets: 'Wallets',
|
wallets: 'Wallets',
|
||||||
exclude_wallets: 'Exclude Wallets',
|
exclude_wallets: 'Exclude Wallets',
|
||||||
add_wallet: 'Add wallet',
|
add_wallet: 'Add wallet',
|
||||||
|
reject_wallet: 'Reject wallet',
|
||||||
add_new_wallet: 'Add a new wallet',
|
add_new_wallet: 'Add a new wallet',
|
||||||
pin_wallet: 'Pin wallet',
|
pin_wallet: 'Pin wallet',
|
||||||
delete_wallet: 'Delete wallet',
|
delete_wallet: 'Delete wallet',
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,11 @@ window._lnbitsApi = {
|
||||||
getWallet(wallet) {
|
getWallet(wallet) {
|
||||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||||
},
|
},
|
||||||
createWallet(wallet, name) {
|
createWallet(wallet, name, walletType, ops = {}) {
|
||||||
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
||||||
name: name
|
name: name,
|
||||||
|
wallet_type: walletType,
|
||||||
|
...ops
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
window.location = '/wallet?wal=' + res.data.id
|
window.location = '/wallet?wal=' + res.data.id
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -52,22 +52,33 @@ window.LNbits = {
|
||||||
0,
|
0,
|
||||||
data.wallets.length - data.extra.visible_wallet_count
|
data.wallets.length - data.extra.visible_wallet_count
|
||||||
)
|
)
|
||||||
|
obj.walletInvitesCount = data.extra.wallet_invite_requests?.length || 0
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
wallet(data) {
|
wallet(data) {
|
||||||
newWallet = {
|
newWallet = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
walletType: data.wallet_type,
|
||||||
|
sharePermissions: data.share_permissions,
|
||||||
|
sharedWalletId: data.shared_wallet_id,
|
||||||
adminkey: data.adminkey,
|
adminkey: data.adminkey,
|
||||||
inkey: data.inkey,
|
inkey: data.inkey,
|
||||||
currency: data.currency,
|
currency: data.currency,
|
||||||
extra: data.extra
|
extra: data.extra,
|
||||||
|
canReceivePayments: true,
|
||||||
|
canSendPayments: true
|
||||||
}
|
}
|
||||||
newWallet.msat = data.balance_msat
|
newWallet.msat = data.balance_msat
|
||||||
newWallet.sat = Math.floor(data.balance_msat / 1000)
|
newWallet.sat = Math.floor(data.balance_msat / 1000)
|
||||||
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
|
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
|
||||||
newWallet.sat
|
newWallet.sat
|
||||||
)
|
)
|
||||||
|
if (newWallet.walletType === 'lightning-shared') {
|
||||||
|
const perms = newWallet.sharePermissions
|
||||||
|
newWallet.canReceivePayments = perms.includes('receive-payments')
|
||||||
|
newWallet.canSendPayments = perms.includes('send-payments')
|
||||||
|
}
|
||||||
newWallet.url = `/wallet?&wal=${data.id}`
|
newWallet.url = `/wallet?&wal=${data.id}`
|
||||||
return newWallet
|
return newWallet
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,13 @@ window.app.component('lnbits-wallet-list', {
|
||||||
return {
|
return {
|
||||||
activeWallet: null,
|
activeWallet: null,
|
||||||
balance: 0,
|
balance: 0,
|
||||||
showForm: false,
|
|
||||||
walletName: '',
|
walletName: '',
|
||||||
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
createWallet() {
|
createWallet() {
|
||||||
LNbits.api.createWallet(this.g.user.wallets[0], this.walletName)
|
this.$emit('wallet-action', {action: 'create-wallet'})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
|
||||||
74
lnbits/static/js/components/lnbits-new-user-wallet.js
Normal file
74
lnbits/static/js/components/lnbits-new-user-wallet.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
window.app.component('lnbits-new-user-wallet', {
|
||||||
|
props: ['form-data'],
|
||||||
|
template: '#lnbits-new-user-wallet',
|
||||||
|
mixins: [window.windowMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newWallet: {walletType: 'lightning', name: '', sharedWalletId: ''}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async submitRejectWalletInvitation() {
|
||||||
|
try {
|
||||||
|
const inviteRequests = this.g.user.extra.wallet_invite_requests || []
|
||||||
|
const invite = inviteRequests.find(
|
||||||
|
invite => invite.to_wallet_id === this.newWallet.sharedWalletId
|
||||||
|
)
|
||||||
|
if (!invite) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'Cannot find invitation for the selected wallet.',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
`/api/v1/wallet/share/invite/${invite.request_id}`,
|
||||||
|
this.g.wallet.adminkey
|
||||||
|
)
|
||||||
|
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'Invitation rejected.',
|
||||||
|
type: 'positive'
|
||||||
|
})
|
||||||
|
this.g.user.extra.wallet_invite_requests = inviteRequests.filter(
|
||||||
|
inv => inv.request_id !== invite.request_id
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submitAddWallet() {
|
||||||
|
console.log('### submitAddWallet', this.newWallet)
|
||||||
|
const data = this.newWallet
|
||||||
|
if (data.walletType === 'lightning' && !data.name) {
|
||||||
|
this.$q.notify({
|
||||||
|
message: 'Please enter a name for the wallet',
|
||||||
|
color: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.walletType === 'lightning-shared' && !data.sharedWalletId) {
|
||||||
|
this.$q.notify({
|
||||||
|
message: 'Missing a shared wallet ID',
|
||||||
|
color: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await LNbits.api.createWallet(
|
||||||
|
this.g.user.wallets[0],
|
||||||
|
data.name,
|
||||||
|
data.walletType,
|
||||||
|
{
|
||||||
|
shared_wallet_id: data.sharedWalletId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -6,7 +6,7 @@ window.PageWallets = {
|
||||||
user: null,
|
user: null,
|
||||||
tab: 'wallets',
|
tab: 'wallets',
|
||||||
wallets: [],
|
wallets: [],
|
||||||
showAddWalletDialog: {show: false},
|
addWalletDialog: {show: false},
|
||||||
walletsTable: {
|
walletsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
|
@ -73,6 +73,10 @@ window.PageWallets = {
|
||||||
this.walletsTable.loading = false
|
this.walletsTable.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showNewWalletDialog() {
|
||||||
|
this.addWalletDialog = {show: true, walletType: 'lightning'}
|
||||||
|
this.showAddNewWalletDialog() // from base.js
|
||||||
|
},
|
||||||
|
|
||||||
goToWallet(walletId) {
|
goToWallet(walletId) {
|
||||||
window.location = `/wallet?wal=${walletId}`
|
window.location = `/wallet?wal=${walletId}`
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ window.WalletPageLogic = {
|
||||||
walletBalanceChart: null,
|
walletBalanceChart: null,
|
||||||
inkeyHidden: true,
|
inkeyHidden: true,
|
||||||
adminkeyHidden: true,
|
adminkeyHidden: true,
|
||||||
|
walletIdHidden: true,
|
||||||
hasNfc: false,
|
hasNfc: false,
|
||||||
nfcReaderAbortController: null,
|
nfcReaderAbortController: null,
|
||||||
isFiatPriority: false,
|
isFiatPriority: false,
|
||||||
|
|
@ -125,7 +126,16 @@ window.WalletPageLogic = {
|
||||||
showBalanceInOut: true,
|
showBalanceInOut: true,
|
||||||
showPaymentCountInOut: true
|
showPaymentCountInOut: true
|
||||||
},
|
},
|
||||||
paymentsFilter: {}
|
paymentsFilter: {},
|
||||||
|
permissionOptions: [
|
||||||
|
{label: 'View', value: 'view-payments'},
|
||||||
|
{label: 'Receive', value: 'receive-payments'},
|
||||||
|
{label: 'Send', value: 'send-payments'}
|
||||||
|
],
|
||||||
|
walletShareInvite: {
|
||||||
|
unsername: '',
|
||||||
|
permissions: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -168,6 +178,21 @@ window.WalletPageLogic = {
|
||||||
wallet() {
|
wallet() {
|
||||||
return this.g.wallet
|
return this.g.wallet
|
||||||
},
|
},
|
||||||
|
walletApprovedShares() {
|
||||||
|
return this.g.wallet.extra.shared_with.filter(
|
||||||
|
s => s.status === 'approved'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
walletPendingRequests() {
|
||||||
|
return this.g.wallet.extra.shared_with.filter(
|
||||||
|
s => s.status === 'request_access'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
walletPendingInvites() {
|
||||||
|
return this.g.wallet.extra.shared_with.filter(
|
||||||
|
s => s.status === 'invite_sent'
|
||||||
|
)
|
||||||
|
},
|
||||||
hasChartActive() {
|
hasChartActive() {
|
||||||
return (
|
return (
|
||||||
this.chartConfig.showBalance ||
|
this.chartConfig.showBalance ||
|
||||||
|
|
@ -675,6 +700,69 @@ window.WalletPageLogic = {
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async updateSharePermissions(permission) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
'/api/v1/wallet/share',
|
||||||
|
this.g.wallet.adminkey,
|
||||||
|
permission
|
||||||
|
)
|
||||||
|
Object.assign(permission, data)
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'Wallet permission updated.',
|
||||||
|
type: 'positive'
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async inviteUserToWallet() {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
'/api/v1/wallet/share/invite',
|
||||||
|
this.g.wallet.adminkey,
|
||||||
|
{
|
||||||
|
...this.walletShareInvite,
|
||||||
|
status: 'invite_sent',
|
||||||
|
wallet_id: this.g.wallet.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.g.wallet.extra.shared_with.push(data)
|
||||||
|
this.walletShareInvite = {username: '', permissions: []}
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'User invited to wallet.',
|
||||||
|
type: 'positive'
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteSharePermission(permission) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to remove this share permission?')
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
`/api/v1/wallet/share/${permission.request_id}`,
|
||||||
|
this.g.wallet.adminkey
|
||||||
|
)
|
||||||
|
this.g.wallet.extra.shared_with =
|
||||||
|
this.g.wallet.extra.shared_with.filter(
|
||||||
|
value => value.wallet_id !== permission.wallet_id
|
||||||
|
)
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'Wallet permission deleted.',
|
||||||
|
type: 'positive'
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
deleteWallet() {
|
deleteWallet() {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this wallet?')
|
.confirmDialog('Are you sure you want to delete this wallet?')
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ window.windowMixin = {
|
||||||
toggleSubs: true,
|
toggleSubs: true,
|
||||||
mobileSimple: true,
|
mobileSimple: true,
|
||||||
walletFlip: true,
|
walletFlip: true,
|
||||||
showAddWalletDialog: {show: false},
|
addWalletDialog: {show: false, walletType: 'lightning'},
|
||||||
|
walletTypes: [{label: 'Lightning Wallet', value: 'lightning'}],
|
||||||
isUserAuthorized: false,
|
isUserAuthorized: false,
|
||||||
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
|
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
|
||||||
allowedThemes: WINDOW_SETTINGS['LNBITS_THEME_OPTIONS'],
|
allowedThemes: WINDOW_SETTINGS['LNBITS_THEME_OPTIONS'],
|
||||||
|
|
@ -46,23 +47,14 @@ window.windowMixin = {
|
||||||
path: '/wallets'
|
path: '/wallets'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submitAddWallet() {
|
handleWalletAction(payload) {
|
||||||
if (
|
if (payload.action === 'create-wallet') {
|
||||||
this.showAddWalletDialog.name &&
|
this.showAddNewWalletDialog()
|
||||||
this.showAddWalletDialog.name.length > 0
|
|
||||||
) {
|
|
||||||
LNbits.api.createWallet(
|
|
||||||
this.g.user.wallets[0],
|
|
||||||
this.showAddWalletDialog.name
|
|
||||||
)
|
|
||||||
this.showAddWalletDialog = {show: false}
|
|
||||||
} else {
|
|
||||||
this.$q.notify({
|
|
||||||
message: 'Please enter a name for the wallet',
|
|
||||||
color: 'negative'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showAddNewWalletDialog() {
|
||||||
|
this.addWalletDialog = {show: true, walletType: 'lightning'}
|
||||||
|
},
|
||||||
simpleMobile() {
|
simpleMobile() {
|
||||||
this.$q.localStorage.set('lnbits.mobileSimple', !this.mobileSimple)
|
this.$q.localStorage.set('lnbits.mobileSimple', !this.mobileSimple)
|
||||||
this.refreshRoute()
|
this.refreshRoute()
|
||||||
|
|
@ -326,6 +318,12 @@ window.windowMixin = {
|
||||||
if (window.user) {
|
if (window.user) {
|
||||||
this.g.user = Vue.reactive(window.LNbits.map.user(window.user))
|
this.g.user = Vue.reactive(window.LNbits.map.user(window.user))
|
||||||
}
|
}
|
||||||
|
if (this.g.user?.extra?.wallet_invite_requests?.length) {
|
||||||
|
this.walletTypes.push({
|
||||||
|
label: `Lightning Wallet (Share Invite: ${this.g.user.extra.wallet_invite_requests.length})`,
|
||||||
|
value: 'lightning-shared'
|
||||||
|
})
|
||||||
|
}
|
||||||
if (window.wallet) {
|
if (window.wallet) {
|
||||||
this.g.wallet = Vue.reactive(window.LNbits.map.wallet(window.wallet))
|
this.g.wallet = Vue.reactive(window.LNbits.map.wallet(window.wallet))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
"js/components/admin/lnbits-admin-site-customisation.js",
|
"js/components/admin/lnbits-admin-site-customisation.js",
|
||||||
"js/components/admin/lnbits-admin-library.js",
|
"js/components/admin/lnbits-admin-library.js",
|
||||||
"js/components/admin/lnbits-admin-audit.js",
|
"js/components/admin/lnbits-admin-audit.js",
|
||||||
|
"js/components/lnbits-new-user-wallet.js",
|
||||||
"js/components/lnbits-qrcode.js",
|
"js/components/lnbits-qrcode.js",
|
||||||
"js/components/lnbits-qrcode-lnurl.js",
|
"js/components/lnbits-qrcode-lnurl.js",
|
||||||
"js/components/extension-settings.js",
|
"js/components/extension-settings.js",
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,7 @@
|
||||||
<lnbits-wallet-list
|
<lnbits-wallet-list
|
||||||
v-if="!walletFlip"
|
v-if="!walletFlip"
|
||||||
:balance="balance"
|
:balance="balance"
|
||||||
|
@wallet-action="handleWalletAction"
|
||||||
></lnbits-wallet-list>
|
></lnbits-wallet-list>
|
||||||
<lnbits-manage
|
<lnbits-manage
|
||||||
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
||||||
|
|
@ -295,57 +296,31 @@
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="row no-wrap q-pr-md">
|
<div class="row no-wrap q-pr-md">
|
||||||
<q-card class="wallet-list-card">
|
<q-card
|
||||||
|
@click="showAddNewWalletDialog()"
|
||||||
|
class="wallet-list-card cursor-pointer"
|
||||||
|
>
|
||||||
<q-card-section
|
<q-card-section
|
||||||
class="flex flex-center column full-height text-center"
|
class="flex flex-center column full-height text-center"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<q-btn
|
<q-btn round color="primary" icon="add">
|
||||||
round
|
|
||||||
color="primary"
|
|
||||||
icon="add"
|
|
||||||
@click="showAddWalletDialog.show = true"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('add_new_wallet')"></span
|
><span v-text="$t('add_new_wallet')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-dialog
|
|
||||||
v-model="showAddWalletDialog.show"
|
|
||||||
persistent
|
|
||||||
@hide="showAddWalletDialog = {show: false}"
|
|
||||||
>
|
|
||||||
<q-card style="min-width: 350px">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">
|
|
||||||
<span v-text="$t('wallet_name')"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
<div>
|
||||||
|
<q-badge
|
||||||
<q-card-section class="q-pt-none">
|
@click="addWalletDialog.walletType='lightning-shared '"
|
||||||
<q-input
|
|
||||||
dense
|
dense
|
||||||
v-model="showAddWalletDialog.name"
|
outline
|
||||||
autofocus
|
class="q-mt-sm"
|
||||||
@keyup.enter="submitAddWallet()"
|
>
|
||||||
></q-input>
|
<span
|
||||||
</q-card-section>
|
v-text="'New wallet invite (' + g.user.walletInvitesCount + ')'"
|
||||||
|
></span>
|
||||||
<q-card-actions align="right" class="text-primary">
|
</q-badge>
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:label="$t('cancel')"
|
|
||||||
v-close-popup
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:label="$t('add_wallet')"
|
|
||||||
v-close-popup
|
|
||||||
@click="submitAddWallet()"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -421,6 +396,15 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
|
<q-dialog
|
||||||
|
v-model="addWalletDialog.show"
|
||||||
|
position="top"
|
||||||
|
@hide="addWalletDialog = {show: false}"
|
||||||
|
>
|
||||||
|
<lnbits-new-user-wallet
|
||||||
|
:form-data="formData"
|
||||||
|
></lnbits-new-user-wallet>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<router-view v-if="isVueRoute"></router-view>
|
<router-view v-if="isVueRoute"></router-view>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ include('components/admin/audit.vue') %} {%
|
||||||
include('components/admin/extensions.vue') %} {%
|
include('components/admin/extensions.vue') %} {%
|
||||||
include('components/admin/library.vue') %} {%
|
include('components/admin/library.vue') %} {%
|
||||||
include('components/admin/notifications.vue') %} {%
|
include('components/admin/notifications.vue') %} {%
|
||||||
include('components/admin/server.vue') %}
|
include('components/admin/server.vue') %} {%
|
||||||
|
include('components/new_user_wallet.vue') %}
|
||||||
|
|
||||||
<template id="lnbits-wallet-list">
|
<template id="lnbits-wallet-list">
|
||||||
<q-list
|
<q-list
|
||||||
|
|
@ -56,7 +57,12 @@ include('components/admin/server.vue') %}
|
||||||
<strong v-text="formatBalance(walletRec.sat)"></strong>
|
<strong v-text="formatBalance(walletRec.sat)"></strong>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side v-show="g.wallet && g.wallet.id === walletRec.id">
|
<q-item-section
|
||||||
|
v-if="walletRec.walletType == 'lightning-shared'"
|
||||||
|
side
|
||||||
|
top
|
||||||
|
>
|
||||||
|
<q-icon name="group" :color="walletRec.extra.color" size="xs"></q-icon>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
|
|
@ -75,13 +81,9 @@ include('components/admin/server.vue') %}
|
||||||
></q-item-label>
|
></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item clickable @click="showForm = !showForm">
|
<q-item clickable @click="createWallet()">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon name="add" color="grey-5" size="md"></q-icon>
|
||||||
:name="showForm ? 'remove' : 'add'"
|
|
||||||
color="grey-5"
|
|
||||||
size="md"
|
|
||||||
></q-icon>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
|
|
@ -89,25 +91,13 @@ include('components/admin/server.vue') %}
|
||||||
class="text-caption"
|
class="text-caption"
|
||||||
v-text="$t('add_new_wallet')"
|
v-text="$t('add_new_wallet')"
|
||||||
></q-item-label>
|
></q-item-label>
|
||||||
|
<q-item-section v-if="g.user.walletInvitesCount" side>
|
||||||
|
<q-badge>
|
||||||
|
<span
|
||||||
|
v-text="'Wallet Invite (' + g.user.walletInvitesCount + ')'"
|
||||||
|
></span>
|
||||||
|
</q-badge>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
|
||||||
<q-item v-if="showForm">
|
|
||||||
<q-item-section>
|
|
||||||
<q-form @submit="createWallet">
|
|
||||||
<q-input filled dense v-model="walletName" label="Name wallet *">
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="send"
|
|
||||||
size="sm"
|
|
||||||
@click="createWallet"
|
|
||||||
:disable="walletName === ''"
|
|
||||||
></q-btn>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</q-form>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
|
||||||
98
lnbits/templates/components/new_user_wallet.vue
Normal file
98
lnbits/templates/components/new_user_wallet.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<template id="lnbits-new-user-wallet">
|
||||||
|
<q-card class="q-pa-lg q-pt-md lnbits__dialog-card">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">
|
||||||
|
<span v-text="$t('add_new_wallet')"></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="g.user.walletInvitesCount">
|
||||||
|
<q-badge
|
||||||
|
@click="newWallet.walletType = 'lightning-shared'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-text="
|
||||||
|
'You have ' +
|
||||||
|
g.user.walletInvitesCount +
|
||||||
|
' wallet invitation' +
|
||||||
|
(g.user.walletInvitesCount > 1 ? 's' : '')
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
</q-badge>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="q-pt-none">
|
||||||
|
<q-select
|
||||||
|
v-if="walletTypes.length > 1"
|
||||||
|
:options="walletTypes"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
:label="$t('wallet_type')"
|
||||||
|
v-model="newWallet.walletType"
|
||||||
|
dense
|
||||||
|
></q-select>
|
||||||
|
<q-input
|
||||||
|
v-if="newWallet.walletType == 'lightning'"
|
||||||
|
dense
|
||||||
|
v-model="newWallet.name"
|
||||||
|
:label="$t('wallet_name')"
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="submitAddWallet()"
|
||||||
|
class="q-mt-md"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-if="newWallet.walletType == 'lightning-shared'"
|
||||||
|
v-model="newWallet.sharedWalletId"
|
||||||
|
:label="$t('shared_wallet_id')"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
dense
|
||||||
|
:options="
|
||||||
|
g.user.extra.wallet_invite_requests.map(i => ({
|
||||||
|
label: i.to_wallet_name + ' (' + i.from_user_name + ')',
|
||||||
|
value: i.to_wallet_id
|
||||||
|
}))
|
||||||
|
"
|
||||||
|
class="q-mt-md"
|
||||||
|
></q-select>
|
||||||
|
<div v-if="newWallet.walletType == 'lightning-shared'" class="q-mt-md">
|
||||||
|
<span v-text="$t('shared_wallet_desc')" class="q-mt-lg"></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions class="text-primary">
|
||||||
|
<div class="row full-width">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:label="$t('cancel')"
|
||||||
|
class="float-left"
|
||||||
|
v-close-popup
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<q-btn
|
||||||
|
v-if="newWallet.walletType == 'lightning-shared'"
|
||||||
|
:disabled="!newWallet.sharedWalletId"
|
||||||
|
flat
|
||||||
|
:label="$t('reject_wallet')"
|
||||||
|
v-close-popup
|
||||||
|
color="negative"
|
||||||
|
@click="submitRejectWalletInvitation()"
|
||||||
|
></q-btn>
|
||||||
|
<span v-else></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:label="$t('add_wallet')"
|
||||||
|
v-close-popup
|
||||||
|
@click="submitAddWallet()"
|
||||||
|
class="float-right"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="row items-center justify-between q-gutter-xs">
|
<div class="row items-center justify-between q-gutter-xs">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showAddWalletDialog.show = true"
|
@click="showNewWalletDialog()"
|
||||||
:label="$t('add_wallet')"
|
:label="$t('add_wallet')"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
|
|
@ -125,35 +125,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="showAddWalletDialog.show"
|
v-model="addWalletDialog.show"
|
||||||
persistent
|
persistent
|
||||||
@hide="showAddWalletDialog = {show: false}"
|
@hide="addWalletDialog = {show: false}"
|
||||||
>
|
>
|
||||||
<q-card style="min-width: 350px">
|
<lnbits-new-user-wallet></lnbits-new-user-wallet>
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">
|
|
||||||
<span v-text="$t('wallet_name')"></span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section class="q-pt-none">
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
v-model="showAddWalletDialog.name"
|
|
||||||
autofocus
|
|
||||||
@keyup.enter="submitAddWallet()"
|
|
||||||
></q-input>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right" class="text-primary">
|
|
||||||
<q-btn flat :label="$t('cancel')" v-close-popup></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:label="$t('add_wallet')"
|
|
||||||
v-close-popup
|
|
||||||
@click="submitAddWallet()"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@
|
||||||
"js/components/admin/lnbits-admin-site-customisation.js",
|
"js/components/admin/lnbits-admin-site-customisation.js",
|
||||||
"js/components/admin/lnbits-admin-library.js",
|
"js/components/admin/lnbits-admin-library.js",
|
||||||
"js/components/admin/lnbits-admin-audit.js",
|
"js/components/admin/lnbits-admin-audit.js",
|
||||||
|
"js/components/lnbits-new-user-wallet.js",
|
||||||
"js/components/lnbits-qrcode.js",
|
"js/components/lnbits-qrcode.js",
|
||||||
"js/components/lnbits-qrcode-lnurl.js",
|
"js/components/lnbits-qrcode-lnurl.js",
|
||||||
"js/components/extension-settings.js",
|
"js/components/extension-settings.js",
|
||||||
|
|
|
||||||
|
|
@ -106,15 +106,7 @@ async def user_alan():
|
||||||
if account:
|
if account:
|
||||||
await delete_account(account.id)
|
await delete_account(account.id)
|
||||||
|
|
||||||
account = Account(
|
yield await new_user("alan")
|
||||||
id=uuid4().hex,
|
|
||||||
email="alan@lnbits.com",
|
|
||||||
username="alan",
|
|
||||||
)
|
|
||||||
account.hash_password("secret1234")
|
|
||||||
user = await create_user_account(account)
|
|
||||||
|
|
||||||
yield user
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
|
@ -299,6 +291,20 @@ async def fake_payments(client, inkey_fresh_headers_to):
|
||||||
return fake_data, params
|
return fake_data, params
|
||||||
|
|
||||||
|
|
||||||
|
async def new_user(username: str | None = None) -> User:
|
||||||
|
id_ = uuid4().hex
|
||||||
|
username = username or f"u_{id_[:16]}"
|
||||||
|
account = Account(
|
||||||
|
id=id_,
|
||||||
|
email=f"{username}@lnbits.com",
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
|
account.hash_password("secret1234")
|
||||||
|
user = await create_user_account(account)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
def _settings_cleanup(settings: Settings):
|
def _settings_cleanup(settings: Settings):
|
||||||
settings.lnbits_allow_new_accounts = True
|
settings.lnbits_allow_new_accounts = True
|
||||||
settings.lnbits_allowed_users = []
|
settings.lnbits_allowed_users = []
|
||||||
|
|
|
||||||
826
tests/unit/test_services_shared_wallet.py
Normal file
826
tests/unit/test_services_shared_wallet.py
Normal file
|
|
@ -0,0 +1,826 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lnbits.core.crud.payments import get_payments
|
||||||
|
from lnbits.core.crud.users import delete_account, get_account
|
||||||
|
from lnbits.core.crud.wallets import (
|
||||||
|
create_wallet,
|
||||||
|
delete_wallet,
|
||||||
|
get_wallet,
|
||||||
|
get_wallets,
|
||||||
|
update_wallet,
|
||||||
|
)
|
||||||
|
from lnbits.core.models.users import User
|
||||||
|
from lnbits.core.models.wallets import (
|
||||||
|
Wallet,
|
||||||
|
WalletPermission,
|
||||||
|
WalletSharePermission,
|
||||||
|
WalletShareStatus,
|
||||||
|
WalletType,
|
||||||
|
)
|
||||||
|
from lnbits.core.services.payments import (
|
||||||
|
create_invoice,
|
||||||
|
pay_invoice,
|
||||||
|
update_wallet_balance,
|
||||||
|
)
|
||||||
|
from lnbits.core.services.wallets import (
|
||||||
|
create_lightning_shared_wallet,
|
||||||
|
delete_wallet_share,
|
||||||
|
invite_to_wallet,
|
||||||
|
reject_wallet_invitation,
|
||||||
|
update_wallet_share_permissions,
|
||||||
|
)
|
||||||
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
|
from tests.conftest import new_user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_ok():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
wallet_share = await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert wallet_share.status == WalletShareStatus.INVITE_SENT
|
||||||
|
|
||||||
|
source_wallet = await get_wallet(source_wallet.id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
share = source_wallet.extra.shared_with[0]
|
||||||
|
assert share is not None
|
||||||
|
assert share.request_id is not None
|
||||||
|
assert share.username == invited_user.username
|
||||||
|
assert share.permissions == [WalletPermission.VIEW_PAYMENTS]
|
||||||
|
assert share.status == WalletShareStatus.INVITE_SENT
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
invite_request = invited_user.extra.find_wallet_invite_request(share.request_id)
|
||||||
|
assert invite_request is not None
|
||||||
|
assert invite_request.request_id == share.request_id
|
||||||
|
assert invite_request.from_user_name == owner_user.username or owner_user.email
|
||||||
|
assert invite_request.to_wallet_id == source_wallet.id
|
||||||
|
assert invite_request.to_wallet_name == source_wallet.name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_twice():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="User already invited to this wallet."):
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_two_invites_to_wallet_ok():
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
owner_user_one = await new_user()
|
||||||
|
source_wallet_one = await create_wallet(
|
||||||
|
user_id=owner_user_one.id, wallet_name="source_wallet_one"
|
||||||
|
)
|
||||||
|
|
||||||
|
wallet_share_one = await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet_one,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert wallet_share_one.request_id is not None
|
||||||
|
|
||||||
|
owner_user_two = await new_user()
|
||||||
|
source_wallet_two = await create_wallet(
|
||||||
|
user_id=owner_user_two.id, wallet_name="source_wallet_two"
|
||||||
|
)
|
||||||
|
|
||||||
|
wallet_share_two = await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet_two,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[
|
||||||
|
WalletPermission.VIEW_PAYMENTS,
|
||||||
|
WalletPermission.RECEIVE_PAYMENTS,
|
||||||
|
],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert wallet_share_two.request_id is not None
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
assert len(invited_user.extra.wallet_invite_requests) == 2
|
||||||
|
invite_request_one = invited_user.extra.find_wallet_invite_request(
|
||||||
|
wallet_share_one.request_id
|
||||||
|
)
|
||||||
|
assert invite_request_one is not None
|
||||||
|
assert (
|
||||||
|
invite_request_one.from_user_name == owner_user_one.username
|
||||||
|
or owner_user_one.email
|
||||||
|
)
|
||||||
|
assert invite_request_one.to_wallet_id == source_wallet_one.id
|
||||||
|
assert invite_request_one.to_wallet_name == source_wallet_one.name
|
||||||
|
invite_request_two = invited_user.extra.find_wallet_invite_request(
|
||||||
|
wallet_share_two.request_id
|
||||||
|
)
|
||||||
|
assert invite_request_two is not None
|
||||||
|
assert (
|
||||||
|
invite_request_two.from_user_name == owner_user_two.username
|
||||||
|
or owner_user_two.email
|
||||||
|
)
|
||||||
|
assert invite_request_two.to_wallet_id == source_wallet_two.id
|
||||||
|
assert invite_request_two.to_wallet_name == source_wallet_two.name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_many_invites_and_one_cancel():
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
count = 10
|
||||||
|
source_wallets = await _create_invitations_for_user(invited_user, count)
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
assert len(invited_user.extra.wallet_invite_requests) == count
|
||||||
|
|
||||||
|
mid_index = count // 2
|
||||||
|
mid_wallet = source_wallets[mid_index]
|
||||||
|
share = mid_wallet.extra.shared_with[0]
|
||||||
|
assert share is not None
|
||||||
|
assert share.request_id is not None
|
||||||
|
assert invited_user.extra.find_wallet_invite_request(share.request_id) is not None
|
||||||
|
await delete_wallet_share(mid_wallet, share.request_id)
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
assert len(invited_user.extra.wallet_invite_requests) == count - 1
|
||||||
|
assert invited_user.extra.find_wallet_invite_request(share.request_id) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_many_invites_and_one_reject():
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
count = 10
|
||||||
|
source_wallets = await _create_invitations_for_user(invited_user, count)
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
|
||||||
|
mid_wallet = source_wallets[count // 2]
|
||||||
|
share = mid_wallet.extra.shared_with[0]
|
||||||
|
assert share is not None
|
||||||
|
assert share.request_id is not None
|
||||||
|
assert invited_user.extra.find_wallet_invite_request(share.request_id) is not None
|
||||||
|
await reject_wallet_invitation(invited_user.id, share.request_id)
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
assert len(invited_user.extra.wallet_invite_requests) == count - 1
|
||||||
|
assert invited_user.extra.find_wallet_invite_request(share.request_id) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_many_invites_and_one_accept():
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
count = 10
|
||||||
|
source_wallets = await _create_invitations_for_user(invited_user, count)
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
|
||||||
|
mid_wallet = source_wallets[count // 2]
|
||||||
|
share = mid_wallet.extra.shared_with[0]
|
||||||
|
assert share is not None
|
||||||
|
await create_lightning_shared_wallet(invited_user.id, mid_wallet.id)
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
|
||||||
|
assert invited_user is not None
|
||||||
|
invited_user_wallets = await get_wallets(invited_user.id)
|
||||||
|
assert len(invited_user_wallets) == 2
|
||||||
|
shared_wallet = next(
|
||||||
|
(w for w in invited_user_wallets if w.shared_wallet_id == mid_wallet.id), None
|
||||||
|
)
|
||||||
|
assert shared_wallet is not None
|
||||||
|
assert share.request_id is not None
|
||||||
|
assert shared_wallet.is_lightning_shared_wallet
|
||||||
|
assert shared_wallet.shared_wallet_id == mid_wallet.id
|
||||||
|
assert len(invited_user.extra.wallet_invite_requests) == count - 1
|
||||||
|
assert invited_user.extra.find_wallet_invite_request(share.request_id) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_non_lightning_wallet():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id,
|
||||||
|
wallet_name="source_wallet",
|
||||||
|
wallet_type=WalletType.LIGHTNING_SHARED,
|
||||||
|
shared_wallet_id="some_shared_wallet_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
source_wallet.shared_wallet_id = "some_shared_wallet_id"
|
||||||
|
await update_wallet(source_wallet)
|
||||||
|
with pytest.raises(ValueError, match="Only lightning wallets can be shared."):
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_wallet_owner_not_found():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
# Remove wallet owner by setting an invalid user id
|
||||||
|
source_wallet.user = "invalid_user_id"
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Cannot find wallet owner."):
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_empty_permissions_ok():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
# Test with empty permissions list
|
||||||
|
wallet_share = await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert wallet_share.permissions == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_username_is_email():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.email is not None
|
||||||
|
# Use email instead of username
|
||||||
|
wallet_share = await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.email,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert wallet_share.username == invited_user.email
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_sql_injection_username():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
# Try a SQL injection-like username
|
||||||
|
with pytest.raises(ValueError, match="Invited user not found."):
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username="' OR 1=1; --",
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_long_username():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
long_username = "a" * 256
|
||||||
|
with pytest.raises(ValueError, match="Invited user not found."):
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=long_username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_special_characters_username():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
special_username = "!@#$%^&*()_+-=[]{}|;':,.<>/?"
|
||||||
|
with pytest.raises(ValueError, match="Invited user not found."):
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=special_username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_invite_to_wallet_no_username():
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
special_username = ""
|
||||||
|
with pytest.raises(ValueError, match="Username or email missing."):
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=special_username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_reject_wallet_invitation_user_not_found():
|
||||||
|
with pytest.raises(ValueError, match="Invited user not found."):
|
||||||
|
await reject_wallet_invitation("non_existent_user_id", "some_request_id")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_reject_wallet_invitation_not_found():
|
||||||
|
invited_user = await new_user()
|
||||||
|
with pytest.raises(ValueError, match="Invitation not found."):
|
||||||
|
await reject_wallet_invitation(invited_user.id, "non_existent_request_id")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_delete_wallet_share_bad_wallet_type():
|
||||||
|
shared_wallet = await _create_shared_wallet_for_user(await new_user())
|
||||||
|
with pytest.raises(ValueError, match="Source wallet is not a lightning wallet."):
|
||||||
|
await delete_wallet_share(shared_wallet, "some_request_id")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_delete_wallet_share_not_found(to_wallet: Wallet):
|
||||||
|
with pytest.raises(ValueError, match="Wallet share not found."):
|
||||||
|
await delete_wallet_share(to_wallet, "non_existent_request_id")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_delete_wallet_share_invited_user_not_found():
|
||||||
|
invited_user = await new_user()
|
||||||
|
shared_wallet = await _create_shared_wallet_for_user(invited_user)
|
||||||
|
assert shared_wallet.shared_wallet_id is not None
|
||||||
|
source_wallet = await get_wallet(shared_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
request_id = source_wallet.extra.shared_with[0].request_id
|
||||||
|
assert request_id is not None
|
||||||
|
await delete_account(invited_user.id)
|
||||||
|
resp = await delete_wallet_share(source_wallet, request_id)
|
||||||
|
|
||||||
|
assert resp.success
|
||||||
|
assert resp.message == "Permission removed. Invited user not found."
|
||||||
|
|
||||||
|
source_wallet = await get_wallet(shared_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
assert source_wallet.extra.find_share_by_id(request_id) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_delete_wallet_share_target_wallet_not_found():
|
||||||
|
invited_user = await new_user()
|
||||||
|
shared_wallet = await _create_shared_wallet_for_user(invited_user)
|
||||||
|
assert shared_wallet.shared_wallet_id is not None
|
||||||
|
source_wallet = await get_wallet(shared_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
request_id = source_wallet.extra.shared_with[0].request_id
|
||||||
|
assert request_id is not None
|
||||||
|
await delete_wallet(user_id=invited_user.id, wallet_id=shared_wallet.id)
|
||||||
|
resp = await delete_wallet_share(source_wallet, request_id)
|
||||||
|
|
||||||
|
assert resp.success
|
||||||
|
assert resp.message == "Permission removed. Target wallet not found."
|
||||||
|
|
||||||
|
source_wallet = await get_wallet(shared_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
assert source_wallet.extra.find_share_by_id(request_id) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_delete_wallet_share_ok():
|
||||||
|
invited_user = await new_user()
|
||||||
|
shared_wallet = await _create_shared_wallet_for_user(invited_user)
|
||||||
|
assert shared_wallet.shared_wallet_id is not None
|
||||||
|
source_wallet = await get_wallet(shared_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
request_id = source_wallet.extra.shared_with[0].request_id
|
||||||
|
assert request_id is not None
|
||||||
|
resp = await delete_wallet_share(source_wallet, request_id)
|
||||||
|
|
||||||
|
assert resp.success
|
||||||
|
assert resp.message == "Permission removed."
|
||||||
|
|
||||||
|
source_wallet = await get_wallet(shared_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
assert source_wallet.extra.find_share_by_id(request_id) is None
|
||||||
|
assert len(source_wallet.extra.shared_with) == 0
|
||||||
|
|
||||||
|
shared_wallet = await get_wallet(shared_wallet.id)
|
||||||
|
assert shared_wallet is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_lightning_shared_wallet_ok():
|
||||||
|
invited_user = await new_user()
|
||||||
|
assert invited_user.username is not None
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.RECEIVE_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
assert len(invited_user.extra.wallet_invite_requests) == 1
|
||||||
|
|
||||||
|
mirror_wallet = await create_lightning_shared_wallet(
|
||||||
|
user_id=invited_user.id, source_wallet_id=source_wallet.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.is_lightning_shared_wallet
|
||||||
|
assert mirror_wallet.shared_wallet_id == source_wallet.id
|
||||||
|
assert mirror_wallet.can_receive_payments is True
|
||||||
|
assert mirror_wallet.can_view_payments is False
|
||||||
|
assert mirror_wallet.can_send_payments is False
|
||||||
|
|
||||||
|
source_wallet = await get_wallet(source_wallet.id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
share = source_wallet.extra.find_share_for_wallet(mirror_wallet.id)
|
||||||
|
assert share is not None
|
||||||
|
assert share.status == WalletShareStatus.APPROVED
|
||||||
|
assert share.permissions == [WalletPermission.RECEIVE_PAYMENTS]
|
||||||
|
|
||||||
|
invited_user = await get_account(invited_user.id)
|
||||||
|
assert invited_user is not None
|
||||||
|
assert len(invited_user.extra.wallet_invite_requests) == 0
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="This wallet is already shared with you."):
|
||||||
|
mirror_wallet = await create_lightning_shared_wallet(
|
||||||
|
user_id=invited_user.id, source_wallet_id=source_wallet.id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_shared_wallet_view_permissions(from_wallet: Wallet):
|
||||||
|
invited_user = await new_user()
|
||||||
|
mirror_wallet: Wallet | None = await _create_shared_wallet_for_user(invited_user)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.shared_wallet_id is not None
|
||||||
|
|
||||||
|
assert mirror_wallet.can_view_payments is True
|
||||||
|
assert mirror_wallet.can_send_payments is False
|
||||||
|
assert mirror_wallet.can_receive_payments is False
|
||||||
|
|
||||||
|
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||||
|
assert len(shared_wallet_payments) == 0
|
||||||
|
|
||||||
|
payment_count = 11
|
||||||
|
wallet_balance = 0
|
||||||
|
|
||||||
|
for i in range(payment_count):
|
||||||
|
payment = await create_invoice(
|
||||||
|
wallet_id=mirror_wallet.shared_wallet_id,
|
||||||
|
amount=1000 + i * 100,
|
||||||
|
memo=f"Test invoice {i}",
|
||||||
|
)
|
||||||
|
await pay_invoice(wallet_id=from_wallet.id, payment_request=payment.bolt11)
|
||||||
|
wallet_balance += payment.sat
|
||||||
|
|
||||||
|
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||||
|
assert len(shared_wallet_payments) == payment_count
|
||||||
|
mirror_wallet = await get_wallet(mirror_wallet.id)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.shared_wallet_id is not None
|
||||||
|
assert mirror_wallet.balance == wallet_balance
|
||||||
|
|
||||||
|
source_wallet = await get_wallet(mirror_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
assert source_wallet.balance == wallet_balance
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
InvoiceError, match="Wallet does not have permission to create invoices."
|
||||||
|
):
|
||||||
|
await create_invoice(
|
||||||
|
wallet_id=mirror_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice with no permissions",
|
||||||
|
)
|
||||||
|
|
||||||
|
payment = await create_invoice(
|
||||||
|
wallet_id=from_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice for payment",
|
||||||
|
)
|
||||||
|
with pytest.raises(
|
||||||
|
PaymentError, match="Wallet does not have permission to pay invoices."
|
||||||
|
):
|
||||||
|
await pay_invoice(wallet_id=mirror_wallet.id, payment_request=payment.bolt11)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_shared_wallet_no_permissions(from_wallet: Wallet):
|
||||||
|
invited_user = await new_user()
|
||||||
|
mirror_wallet: Wallet | None = await _create_shared_wallet_for_user(invited_user)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.shared_wallet_id is not None
|
||||||
|
source_wallet = await get_wallet(mirror_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_for_wallet(mirror_wallet.id)
|
||||||
|
assert share is not None
|
||||||
|
share.permissions = []
|
||||||
|
await update_wallet_share_permissions(source_wallet, share)
|
||||||
|
|
||||||
|
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||||
|
assert len(shared_wallet_payments) == 0
|
||||||
|
mirror_wallet = await get_wallet(mirror_wallet.id)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.balance == 0
|
||||||
|
|
||||||
|
payment = await create_invoice(
|
||||||
|
wallet_id=from_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
InvoiceError, match="Wallet does not have permission to create invoices."
|
||||||
|
):
|
||||||
|
await create_invoice(
|
||||||
|
wallet_id=mirror_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice with no permissions",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
PaymentError, match="Wallet does not have permission to pay invoices."
|
||||||
|
):
|
||||||
|
await pay_invoice(wallet_id=mirror_wallet.id, payment_request=payment.bolt11)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_shared_wallet_receive_permission(from_wallet: Wallet):
|
||||||
|
invited_user = await new_user()
|
||||||
|
mirror_wallet: Wallet | None = await _create_shared_wallet_for_user(invited_user)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.shared_wallet_id is not None
|
||||||
|
source_wallet = await get_wallet(mirror_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_for_wallet(mirror_wallet.id)
|
||||||
|
assert share is not None
|
||||||
|
share.permissions = [WalletPermission.RECEIVE_PAYMENTS]
|
||||||
|
await update_wallet_share_permissions(source_wallet, share)
|
||||||
|
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||||
|
# cannot view payments
|
||||||
|
assert len(shared_wallet_payments) == 0
|
||||||
|
mirror_wallet = await get_wallet(mirror_wallet.id)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.balance == 0
|
||||||
|
# ok to create invoice
|
||||||
|
await create_invoice(
|
||||||
|
wallet_id=mirror_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice with no permissions",
|
||||||
|
)
|
||||||
|
|
||||||
|
payment = await create_invoice(
|
||||||
|
wallet_id=from_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice",
|
||||||
|
)
|
||||||
|
# but not to pay
|
||||||
|
await update_wallet_balance(mirror_wallet, 100000)
|
||||||
|
with pytest.raises(
|
||||||
|
PaymentError, match="Wallet does not have permission to pay invoices."
|
||||||
|
):
|
||||||
|
await pay_invoice(wallet_id=mirror_wallet.id, payment_request=payment.bolt11)
|
||||||
|
|
||||||
|
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||||
|
assert len(shared_wallet_payments) == 0
|
||||||
|
mirror_wallet = await get_wallet(mirror_wallet.id)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.balance == 100000
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_for_wallet(mirror_wallet.id)
|
||||||
|
assert share is not None
|
||||||
|
share.permissions.append(WalletPermission.VIEW_PAYMENTS)
|
||||||
|
await update_wallet_share_permissions(source_wallet, share)
|
||||||
|
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||||
|
assert len(shared_wallet_payments) == 2
|
||||||
|
|
||||||
|
# check that paying is still not allowed after adding view permission
|
||||||
|
with pytest.raises(
|
||||||
|
PaymentError, match="Wallet does not have permission to pay invoices."
|
||||||
|
):
|
||||||
|
await pay_invoice(wallet_id=mirror_wallet.id, payment_request=payment.bolt11)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_shared_wallet_send_permission(from_wallet: Wallet):
|
||||||
|
invited_user = await new_user()
|
||||||
|
mirror_wallet: Wallet | None = await _create_shared_wallet_for_user(invited_user)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.shared_wallet_id is not None
|
||||||
|
source_wallet = await get_wallet(mirror_wallet.shared_wallet_id)
|
||||||
|
assert source_wallet is not None
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_for_wallet(mirror_wallet.id)
|
||||||
|
assert share is not None
|
||||||
|
share.permissions = [WalletPermission.SEND_PAYMENTS]
|
||||||
|
await update_wallet_share_permissions(source_wallet, share)
|
||||||
|
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||||
|
assert len(shared_wallet_payments) == 0
|
||||||
|
mirror_wallet = await get_wallet(mirror_wallet.id)
|
||||||
|
assert mirror_wallet is not None
|
||||||
|
assert mirror_wallet.balance == 0
|
||||||
|
# cannot create invoice
|
||||||
|
with pytest.raises(
|
||||||
|
InvoiceError, match="Wallet does not have permission to create invoices."
|
||||||
|
):
|
||||||
|
await create_invoice(
|
||||||
|
wallet_id=mirror_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice with no permissions",
|
||||||
|
)
|
||||||
|
|
||||||
|
# but can pay invoice
|
||||||
|
payment = await create_invoice(
|
||||||
|
wallet_id=from_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice",
|
||||||
|
)
|
||||||
|
await update_wallet_balance(mirror_wallet, 100000)
|
||||||
|
await pay_invoice(wallet_id=mirror_wallet.id, payment_request=payment.bolt11)
|
||||||
|
|
||||||
|
share = source_wallet.extra.find_share_for_wallet(mirror_wallet.id)
|
||||||
|
assert share is not None
|
||||||
|
share.permissions.append(WalletPermission.VIEW_PAYMENTS)
|
||||||
|
await update_wallet_share_permissions(source_wallet, share)
|
||||||
|
shared_wallet_payments = await get_payments(wallet_id=mirror_wallet.id)
|
||||||
|
assert len(shared_wallet_payments) == 2
|
||||||
|
|
||||||
|
# check that creating invoice is still not allowed after adding view permission
|
||||||
|
with pytest.raises(
|
||||||
|
InvoiceError, match="Wallet does not have permission to create invoices."
|
||||||
|
):
|
||||||
|
await create_invoice(
|
||||||
|
wallet_id=mirror_wallet.id,
|
||||||
|
amount=1000,
|
||||||
|
memo="Test invoice with no permissions",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_lightning_shared_wallet_missing_source():
|
||||||
|
invited_user = await new_user()
|
||||||
|
with pytest.raises(ValueError, match="Shared wallet does not exist."):
|
||||||
|
await create_lightning_shared_wallet(
|
||||||
|
invited_user.id, "non_existent_source_wallet_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_lightning_shared_wallet_bad_type():
|
||||||
|
invited_user = await new_user()
|
||||||
|
shared_wallet = await _create_shared_wallet_for_user(invited_user)
|
||||||
|
with pytest.raises(ValueError, match="Shared wallet is not a lightning wallet."):
|
||||||
|
await create_lightning_shared_wallet(invited_user.id, shared_wallet.id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_lightning_shared_wallet_self_mirror(to_wallet: Wallet):
|
||||||
|
with pytest.raises(ValueError, match="Cannot mirror your own wallet."):
|
||||||
|
await create_lightning_shared_wallet(to_wallet.user, to_wallet.id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_lightning_shared_wallet_missing_invitation(to_wallet: Wallet):
|
||||||
|
with pytest.raises(ValueError, match="Cannot find invited user."):
|
||||||
|
await create_lightning_shared_wallet("non_existing_user", to_wallet.id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_lightning_shared_wallet_missing_user(to_wallet: Wallet):
|
||||||
|
invited_user = await new_user()
|
||||||
|
with pytest.raises(ValueError, match="No invitation found for this invited user."):
|
||||||
|
await create_lightning_shared_wallet(invited_user.id, to_wallet.id)
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_invitations_for_user(invited_user, count) -> list[Wallet]:
|
||||||
|
source_wallets = []
|
||||||
|
for i in range(count):
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name=f"source_wallet_{i}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
source_wallets.append(source_wallet)
|
||||||
|
return source_wallets
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_shared_wallet_for_user(invited_user: User) -> Wallet:
|
||||||
|
assert invited_user.username is not None
|
||||||
|
owner_user = await new_user()
|
||||||
|
source_wallet = await create_wallet(
|
||||||
|
user_id=owner_user.id, wallet_name="source_wallet"
|
||||||
|
)
|
||||||
|
|
||||||
|
await invite_to_wallet(
|
||||||
|
source_wallet=source_wallet,
|
||||||
|
data=WalletSharePermission(
|
||||||
|
username=invited_user.username,
|
||||||
|
permissions=[WalletPermission.VIEW_PAYMENTS],
|
||||||
|
status=WalletShareStatus.INVITE_SENT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
shared_wallet = await create_lightning_shared_wallet(
|
||||||
|
user_id=invited_user.id, source_wallet_id=source_wallet.id
|
||||||
|
)
|
||||||
|
return shared_wallet
|
||||||
Loading…
Add table
Add a link
Reference in a new issue