Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Wallet] Define responses for API #233

Merged
merged 2 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 75 additions & 0 deletions cashu/wallet/api/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Dict, List, Union

from pydantic import BaseModel

from ...core.base import Invoice, P2SHScript


class PayResponse(BaseModel):
amount: int
fee: int
amount_with_fee: int
initial_balance: int
balance: int


class InvoiceResponse(BaseModel):
amount: int
invoice: Union[Invoice, None] = None
hash: Union[str, None] = None
initial_balance: int
balance: int


class BalanceResponse(BaseModel):
balance: int
keysets: Union[Dict, None] = None
mints: Union[Dict, None] = None


class SendResponse(BaseModel):
balance: int
token: str
npub: Union[str, None] = None


class ReceiveResponse(BaseModel):
initial_balance: int
balance: int


class BurnResponse(BaseModel):
balance: int


class PendingResponse(BaseModel):
pending_token: Dict


class LockResponse(BaseModel):
P2SH: Union[str, None]


class LocksResponse(BaseModel):
locks: List[P2SHScript]


class InvoicesResponse(BaseModel):
invoices: List[Invoice]


class WalletsResponse(BaseModel):
wallets: Dict


class InfoResponse(BaseModel):
version: str
wallet: str
debug: bool
cashu_dir: str
mint_url: str
settings: Union[str, None]
tor: bool
nostr_public_key: Union[str, None] = None
nostr_relays: List[str] = []
socks_proxy: Union[str, None] = None
183 changes: 95 additions & 88 deletions cashu/wallet/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@
from ...wallet.nostr import receive_nostr, send_nostr
from ...wallet.wallet import Wallet as Wallet
from .api_helpers import verify_mints
from .responses import (
BalanceResponse,
BurnResponse,
InfoResponse,
InvoiceResponse,
InvoicesResponse,
LockResponse,
LocksResponse,
PayResponse,
PendingResponse,
ReceiveResponse,
SendResponse,
WalletsResponse,
)

router: APIRouter = APIRouter()

Expand Down Expand Up @@ -48,7 +62,7 @@ async def start_wallet():
await init_wallet(wallet)


@router.post("/pay", name="Pay lightning invoice")
@router.post("/pay", name="Pay lightning invoice", response_model=PayResponse)
async def pay(
invoice: str = Query(default=..., description="Lightning invoice to pay"),
mint: str = Query(
Expand Down Expand Up @@ -77,16 +91,18 @@ async def pay(
_, send_proofs = await wallet.split_to_send(wallet.proofs, total_amount)
await wallet.pay_lightning(send_proofs, invoice)
await wallet.load_proofs()
return {
"amount": total_amount - fee_reserve_sat,
"fee": fee_reserve_sat,
"amount_with_fee": total_amount,
"initial_balance": initial_balance,
"balance": wallet.available_balance,
}
return PayResponse(
amount=total_amount - fee_reserve_sat,
fee=fee_reserve_sat,
amount_with_fee=total_amount,
initial_balance=initial_balance,
balance=wallet.available_balance,
)


@router.post("/invoice", name="Request lightning invoice")
@router.post(
"/invoice", name="Request lightning invoice", response_model=InvoiceResponse
)
async def invoice(
amount: int = Query(default=..., description="Amount to request in invoice"),
hash: str = Query(default=None, description="Hash of paid invoice"),
Expand All @@ -100,44 +116,45 @@ async def invoice(
initial_balance = wallet.available_balance
if not settings.lightning:
r = await wallet.mint(amount)
return {
"amount": amount,
"balance": wallet.available_balance,
"initial_balance": initial_balance,
}
return InvoiceResponse(
amount=amount,
balance=wallet.available_balance,
initial_balance=initial_balance,
)
elif amount and not hash:
invoice = await wallet.request_mint(amount)
return {
"invoice": invoice,
"balance": wallet.available_balance,
"initial_balance": initial_balance,
}
return InvoiceResponse(
invoice=invoice,
balance=wallet.available_balance,
initial_balance=initial_balance,
)
elif amount and hash:
await wallet.mint(amount, hash)
return {
"amount": amount,
"hash": hash,
"balance": wallet.available_balance,
"initial_balance": initial_balance,
}
return InvoiceResponse(
amount=amount,
hash=hash,
balance=wallet.available_balance,
initial_balance=initial_balance,
)
return


@router.get("/balance", name="Balance", summary="Display balance.")
@router.get(
"/balance",
name="Balance",
summary="Display balance.",
response_model=BalanceResponse,
)
async def balance():
await wallet.load_proofs()
result: dict = {"balance": wallet.available_balance}
keyset_balances = wallet.balance_per_keyset()
if len(keyset_balances) > 0:
result.update({"keysets": keyset_balances})
mint_balances = await wallet.balance_per_minturl()
if len(mint_balances) > 0:
result.update({"mints": mint_balances})

return result
return BalanceResponse(
balance=wallet.available_balance, keysets=keyset_balances, mints=mint_balances
)


@router.post("/send", name="Send tokens")
@router.post("/send", name="Send tokens", response_model=SendResponse)
async def send_command(
amount: int = Query(default=..., description="Amount to send"),
nostr: str = Query(default=None, description="Send to nostr pubkey"),
Expand All @@ -156,27 +173,23 @@ async def send_command(
balance, token = await send(wallet, amount, lock, legacy=False)
except Exception as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
return {"balance": balance, "token": token}
return SendResponse(balance=balance, token=token)
else:
try:
token, pubkey = await send_nostr(wallet, amount, nostr)
except Exception as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
return {
"balance": wallet.available_balance,
"token": token,
"npub": pubkey,
}
return SendResponse(balance=wallet.available_balance, token=token, npub=pubkey)


@router.post("/receive", name="Receive tokens")
@router.post("/receive", name="Receive tokens", response_model=ReceiveResponse)
async def receive_command(
token: str = Query(default=None, description="Token to receive"),
lock: str = Query(default=None, description="Unlock tokens"),
nostr: bool = Query(default=False, description="Receive tokens via nostr"),
all: bool = Query(default=False, description="Receive all pending tokens"),
):
result = {"initial_balance": wallet.available_balance}
initial_balance = wallet.available_balance
if token:
try:
tokenObj: TokenV3 = await deserialize_token_from_string(token)
Expand Down Expand Up @@ -223,11 +236,10 @@ async def receive_command(
detail="enter token or use either flag --nostr or --all.",
)
assert balance
result.update({"balance": balance})
return result
return ReceiveResponse(initial_balance=initial_balance, balance=balance)


@router.post("/burn", name="Burn spent tokens")
@router.post("/burn", name="Burn spent tokens", response_model=BurnResponse)
async def burn(
token: str = Query(default=None, description="Token to burn"),
all: bool = Query(default=False, description="Burn all spent tokens"),
Expand Down Expand Up @@ -268,10 +280,10 @@ async def burn(
await wallet.invalidate(proofs, check_spendable=False)
else:
await wallet.invalidate(proofs)
return {"balance": wallet.available_balance}
return BurnResponse(balance=wallet.available_balance)


@router.get("/pending", name="Show pending tokens")
@router.get("/pending", name="Show pending tokens", response_model=PendingResponse)
async def pending(
number: int = Query(default=None, description="Show only n pending tokens"),
offset: int = Query(
Expand Down Expand Up @@ -312,35 +324,33 @@ async def pending(
}
}
)
return result
return PendingResponse(pending_token=result)


@router.get("/lock", name="Generate receiving lock")
@router.get("/lock", name="Generate receiving lock", response_model=LockResponse)
async def lock():
p2shscript = await wallet.create_p2sh_lock()
txin_p2sh_address = p2shscript.address
return {"P2SH": txin_p2sh_address}
return LockResponse(P2SH=txin_p2sh_address)


@router.get("/locks", name="Show unused receiving locks")
@router.get("/locks", name="Show unused receiving locks", response_model=LocksResponse)
async def locks():
locks = await get_unused_locks(db=wallet.db)
if len(locks):
return {"locks": locks}
else:
return {"locks": []}
return LocksResponse(locks=locks)


@router.get("/invoices", name="List all pending invoices")
@router.get(
"/invoices", name="List all pending invoices", response_model=InvoicesResponse
)
async def invoices():
invoices = await get_lightning_invoices(db=wallet.db)
if len(invoices):
return {"invoices": invoices}
else:
return {"invoices": []}
return InvoicesResponse(invoices=invoices)


@router.get("/wallets", name="List all available wallets")
@router.get(
"/wallets", name="List all available wallets", response_model=WalletsResponse
)
async def wallets():
wallets = [
d for d in listdir(settings.cashu_dir) if isdir(join(settings.cashu_dir, d))
Expand Down Expand Up @@ -371,38 +381,35 @@ async def wallets():
)
except:
pass
return result
return WalletsResponse(wallets=result)


@router.get("/info", name="Information about Cashu wallet")
@router.get("/info", name="Information about Cashu wallet", response_model=InfoResponse)
async def info():
general = {
"version": settings.version,
"wallet": wallet.name,
"debug": settings.debug,
"cashu_dir": settings.cashu_dir,
"mint_url": settings.mint_url,
}
if settings.env_file:
general.update({"settings": settings.env_file})
if settings.tor:
general.update({"tor": settings.tor})
if settings.nostr_private_key:
try:
client = NostrClient(private_key=settings.nostr_private_key, connect=False)
general.update(
{
"nostr": {
"public_key": client.private_key.bech32(),
"relays": settings.nostr_relays,
},
}
)
nostr_public_key = client.private_key.bech32()
nostr_relays = settings.nostr_relays
except:
general.update({"nostr": "Invalid key"})
nostr_public_key = "Invalid key"
nostr_relays = []
else:
nostr_public_key = None
nostr_relays = []
if settings.socks_host:
general.update(
{"socks proxy": settings.socks_host + ":" + str(settings.socks_host)}
)

return general
socks_proxy = settings.socks_host + ":" + str(settings.socks_host)
else:
socks_proxy = None
return InfoResponse(
version=settings.version,
wallet=wallet.name,
debug=settings.debug,
cashu_dir=settings.cashu_dir,
mint_url=settings.mint_url,
settings=settings.env_file,
tor=settings.tor,
nostr_public_key=nostr_public_key,
nostr_relays=nostr_relays,
socks_proxy=socks_proxy,
)
5 changes: 2 additions & 3 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def test_pending():
with TestClient(app) as client:
response = client.get("/pending")
assert response.status_code == 200
assert response.json()["0"]


def test_receive_all(mint):
Expand Down Expand Up @@ -114,7 +113,7 @@ def test_flow(mint):
response = client.post("/send?amount=50")
assert response.json()["balance"] == initial_balance
response = client.get("/pending")
token = response.json()["0"]["token"]
amount = response.json()["0"]["amount"]
token = response.json()["pending_token"]["0"]["token"]
amount = response.json()["pending_token"]["0"]["amount"]
response = client.post(f"/receive?token={token}")
assert response.json()["balance"] == initial_balance + amount