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

Allow to show tx history via CLI and API #315

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cashu/wallet/api/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,7 @@ class InfoResponse(BaseModel):
nostr_public_key: Optional[str] = None
nostr_relays: List[str] = []
socks_proxy: Optional[str] = None


class HistoryResponse(BaseModel):
txs: List
19 changes: 18 additions & 1 deletion cashu/wallet/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
from ...core.settings import settings
from ...nostr.nostr.client.client import NostrClient
from ...tor.tor import TorProxy
from ...wallet.crud import get_lightning_invoices, get_reserved_proofs, get_unused_locks
from ...wallet.crud import (
get_lightning_invoices,
get_reserved_proofs,
get_tx_history,
get_unused_locks,
)
from ...wallet.helpers import (
deserialize_token_from_string,
init_wallet,
Expand All @@ -27,6 +32,7 @@
from .responses import (
BalanceResponse,
BurnResponse,
HistoryResponse,
InfoResponse,
InvoiceResponse,
InvoicesResponse,
Expand Down Expand Up @@ -446,3 +452,14 @@ async def info():
nostr_relays=nostr_relays,
socks_proxy=settings.socks_proxy,
)


@router.get("/history", name="Transaction history", response_model=HistoryResponse)
async def history(
number: int = Query(default=None, description="Show only last n transactions"),
):
txs = await get_tx_history(wallet.db)
txs = sorted(txs, key=itemgetter("time"), reverse=True)
if number:
txs = txs[:number]
return HistoryResponse(txs=txs)
38 changes: 38 additions & 0 deletions cashu/wallet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
get_lightning_invoices,
get_reserved_proofs,
get_seed_and_mnemonic,
get_tx_history,
get_unused_locks,
)
from ...wallet.wallet import Wallet as Wallet
Expand Down Expand Up @@ -790,3 +791,40 @@ async def restore(ctx: Context, to: int, batch: int):
await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch)
await wallet.load_proofs()
wallet.status()


@cli.command("history", help="Show transaction history.")
@click.option(
"--detailed",
"-d",
default=False,
help="Show detailed information on transactions.",
is_flag=True,
)
@click.option(
"--number", "-n", default=None, help="Show only last n transactions.", type=int
)
@click.pass_context
@coro
async def history(ctx: Context, detailed: bool, number: int):
wallet: Wallet = ctx.obj["WALLET"]
txs = await get_tx_history(wallet.db)
txs = sorted(txs, key=itemgetter("time"), reverse=True)
if number:
txs = txs[:number]
print("----------------Transaction history----------------")
print("Tx type Amount Time")
print("---------------------------------------------------")
for row in txs:
tx_type, amount, token, hash, preimage, time = row
output = (
"{:<12}".format(tx_type)
+ "{:>15}".format(amount)
+ "\t"
+ "{}".format(datetime.utcfromtimestamp(int(time)))
)
if detailed:
output += "\n" + "Token: " + token
if tx_type == "lightning":
output += "\n" + "Hash: " + hash
print(output)
32 changes: 31 additions & 1 deletion cashu/wallet/crud.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import time
from typing import Any, List, Optional, Tuple
from typing import Any, List, Optional, Tuple, Union

from ..core.base import Invoice, P2SHScript, Proof, WalletKeyset
from ..core.db import Connection, Database
Expand Down Expand Up @@ -444,3 +444,33 @@ async def store_seed_and_mnemonic(
mnemonic,
),
)


async def store_tx(
db: Database,
type: str,
amount: int,
token: str,
hash: str,
preimage: Union[str, None],
time: int,
conn: Optional[Connection] = None,
):
await (conn or db).execute(
"""
INSERT INTO tx_history
(type, amount, token, hash, preimage, time)
VALUES (?, ?, ?, ?, ?, ?)
""",
(type, amount, token, hash, preimage, time),
)


async def get_tx_history(
db: Database,
conn: Optional[Connection] = None,
):
rows = await (conn or db).fetchall("""
SELECT * from tx_history
""")
return rows
17 changes: 14 additions & 3 deletions cashu/wallet/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import json
import os
import time

from loguru import logger

Expand All @@ -10,7 +11,7 @@
from ..core.migrations import migrate_databases
from ..core.settings import settings
from ..wallet import migrations
from ..wallet.crud import get_keyset
from ..wallet.crud import get_keyset, store_tx
from ..wallet.wallet import Wallet


Expand Down Expand Up @@ -40,6 +41,7 @@ async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
Helper function to iterate thruogh a token with multiple mints and redeem them from
these mints one keyset at a time.
"""
amount = 0
for t in token.token:
assert t.mint, Exception(
"redeem_TokenV3_multimint: multimint redeem without URL"
Expand All @@ -56,6 +58,8 @@ async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
redeem_proofs = [p for p in t.proofs if p.id == keyset]
_, _ = await mint_wallet.redeem(redeem_proofs)
print(f"Received {sum_proofs(redeem_proofs)} sats")
amount += sum_proofs(redeem_proofs)
return amount


def serialize_TokenV2_to_TokenV3(tokenv2: TokenV2):
Expand Down Expand Up @@ -130,7 +134,7 @@ async def receive(

if includes_mint_info:
# redeem tokens with new wallet instances
await redeem_TokenV3_multimint(
amount = await redeem_TokenV3_multimint(
wallet,
tokenObj,
)
Expand All @@ -151,7 +155,12 @@ async def receive(
)
await mint_wallet.load_mint(keyset_in_token)
_, _ = await mint_wallet.redeem(proofs)
print(f"Received {sum_proofs(proofs)} sats")
amount = sum_proofs(proofs)
print(f"Received {amount} sats")

await store_tx(
wallet.db, "ecash", amount, tokenObj.serialize(), "NA", "NA", int(time.time())
)

# reload main wallet so the balance updates
await wallet.load_proofs(reload=True)
Expand Down Expand Up @@ -215,6 +224,8 @@ async def send(
)
print(token)

await store_tx(wallet.db, "ecash", -amount, token, "NA", "NA", int(time.time()))

if legacy:
print("")
print("Old token format:")
Expand Down
17 changes: 17 additions & 0 deletions cashu/wallet/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,20 @@ async def m009_privatekey_and_determinstic_key_derivation(db: Database):
);
""")
# await db.execute("INSERT INTO secret_derivation (counter) VALUES (0)")


async def m010_tx_history(db: Database):
"""
Stores Lightning invoices.
"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS tx_history (
type TEXT NOT NULL,
amount INTEGER NOT NULL,
token TEXT NOT NULL,
hash TEXT NOT NULL,
preimage TEXT NOT NULL,
time TIMESTAMP DEFAULT {db.timestamp_now}

);
""")
21 changes: 19 additions & 2 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
store_p2sh,
store_proof,
store_seed_and_mnemonic,
store_tx,
update_lightning_invoice,
update_proof_reserved,
)
Expand Down Expand Up @@ -919,8 +920,13 @@ async def mint(
raise Exception("received no proofs.")
await self._store_proofs(proofs)
if hash:
time_paid = int(time.time())
await update_lightning_invoice(
db=self.db, hash=hash, paid=True, time_paid=int(time.time())
db=self.db, hash=hash, paid=True, time_paid=time_paid
)
token = await self.serialize_proofs(proofs)
await store_tx(
self.db, "lightning", amount, token, hash, "Unknown", time_paid
)
self.proofs += proofs
return proofs
Expand Down Expand Up @@ -1156,17 +1162,28 @@ async def pay_lightning(
if status.paid:
# the payment was successful
await self.invalidate(proofs)
time_paid = int(time.time())
invoice_obj = Invoice(
amount=-sum_proofs(proofs),
pr=invoice,
preimage=status.preimage,
paid=True,
time_paid=time.time(),
time_paid=time_paid,
hash="",
)
# we have a unique constraint on the hash, so we generate a random one if it doesn't exist
invoice_obj.hash = invoice_obj.hash or await self._generate_secret()
await store_lightning_invoice(db=self.db, invoice=invoice_obj)
token = await self.serialize_proofs(proofs)
await store_tx(
self.db,
"lightning",
-sum_proofs(proofs),
token,
invoice_obj.hash,
status.preimage,
time_paid,
)

# handle change and produce proofs
if status.change:
Expand Down
7 changes: 7 additions & 0 deletions tests/test_wallet_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ async def test_info():
assert response.json()["version"]


@pytest.mark.asyncio
async def test_history():
with TestClient(app) as client:
response = client.get("/history")
assert response.status_code == 200


@pytest.mark.asyncio
async def test_flow(wallet: Wallet):
with TestClient(app) as client:
Expand Down
Loading