mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
Determinstic secrets / ecash restore (#131)
* first working version but some sats go missing * back at it * make format * restore to main * move mint database * fix some tests * make format * remove old _construct_outputs we reintroduced in merge with main * add type annotations * add wallet private key to tests * wallet: load proofs * fix tests * _generate_secrets with deterministic generation (temporary) * allow wallet initialization with custom private key * add pk to wallet api test * mint scope=module * remove private_key from test_wallet.py to see if it helps with the github tests * readd private keys to tests * workflow without env * add more private key! * readd env * ledger scope session * add default private key for testing * generate private keys if not available * testing * its working!!! * first iteration of bip32 working * get mint info and add many type annotations * tests * fix tests with bip32 * restore from multiple mints * disable profiler * make format * failed POST /mint do not increment secret counter * store derivation path in each token * fix tests * refactor migrations so private keys can be generated by the wallet with .with_db() classmethod * start fixing tests * all tests passing except those that need to set a specific private key * bip39 mnemonic to seed - with db but restore doesnt work yet with custom seed * mnemonic restore works * enter mnemonic in cli * fix tests to use different mnemonic * properly ask user for seed input * tests: dont ask for inputs * try to fix tests * fix cashu -d * fixing * bump version and add more text to mnemonic enter * add more comments * add many more comments and type annotations in the wallet * dont print generated mnemonic and dont wait for input * fix test * does this fix tests? * sigh.... * make format * do not restore from an initialized wallet * fix mnemonics * fix nitpicks * print wallet name if nonstandard wallet * fix merge error and remove comments * poetry lock and requirements * remove unused code * fix tests * mnemonic.lower() and add keyset id if not present for backwards compat * edit comment
This commit is contained in:
@@ -42,6 +42,8 @@ MINT_DATABASE=data/mint
|
||||
# increment derivation path to rotate to a new keyset
|
||||
MINT_DERIVATION_PATH="0/0/0/0"
|
||||
|
||||
MINT_DATABASE=data/mint
|
||||
|
||||
# Lightning
|
||||
# Supported: LNbitsWallet, FakeWallet
|
||||
MINT_LIGHTNING_BACKEND=LNbitsWallet
|
||||
|
||||
@@ -114,7 +114,7 @@ cashu info
|
||||
|
||||
Returns:
|
||||
```bash
|
||||
Version: 0.12.3
|
||||
Version: 0.13.0
|
||||
Debug: False
|
||||
Cashu dir: /home/user/.cashu
|
||||
Wallet: wallet
|
||||
|
||||
@@ -174,6 +174,7 @@ class Proof(BaseModel):
|
||||
] = "" # unique ID of send attempt, used for grouping pending tokens in the wallet
|
||||
time_created: Union[None, str] = ""
|
||||
time_reserved: Union[None, str] = ""
|
||||
derivation_path: Union[None, str] = "" # derivation path of the proof
|
||||
|
||||
def to_dict(self):
|
||||
# dictionary without the fields that don't need to be send to Carol
|
||||
@@ -349,6 +350,14 @@ class CheckFeesResponse(BaseModel):
|
||||
fee: Union[int, None]
|
||||
|
||||
|
||||
# ------- API: RESTORE -------
|
||||
|
||||
|
||||
class PostRestoreResponse(BaseModel):
|
||||
outputs: List[BlindedMessage] = []
|
||||
promises: List[BlindedSignature] = []
|
||||
|
||||
|
||||
# ------- KEYSETS -------
|
||||
|
||||
|
||||
@@ -571,7 +580,7 @@ class TokenV3(BaseModel):
|
||||
return list(set([p.id for p in self.get_proofs()]))
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, tokenv3_serialized: str):
|
||||
def deserialize(cls, tokenv3_serialized: str) -> "TokenV3":
|
||||
"""
|
||||
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
||||
"""
|
||||
@@ -583,7 +592,7 @@ class TokenV3(BaseModel):
|
||||
token = json.loads(base64.urlsafe_b64decode(token_base64))
|
||||
return cls.parse_obj(token)
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> str:
|
||||
"""
|
||||
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
||||
"""
|
||||
|
||||
@@ -51,13 +51,10 @@ def hash_to_curve(message: bytes) -> PublicKey:
|
||||
|
||||
|
||||
def step1_alice(
|
||||
secret_msg: str, blinding_factor: Optional[bytes] = None
|
||||
secret_msg: str, blinding_factor: Optional[PrivateKey] = None
|
||||
) -> tuple[PublicKey, PrivateKey]:
|
||||
Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8"))
|
||||
if blinding_factor:
|
||||
r = PrivateKey(privkey=blinding_factor, raw=True)
|
||||
else:
|
||||
r = PrivateKey()
|
||||
r = blinding_factor or PrivateKey()
|
||||
B_: PublicKey = Y + r.pubkey # type: ignore
|
||||
return B_, r
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import re
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core.db import COCKROACH, POSTGRES, SQLITE, Database, table_with_schema
|
||||
|
||||
|
||||
@@ -22,6 +24,7 @@ async def migrate_databases(db: Database, migrations_module):
|
||||
if match:
|
||||
version = int(match.group(1))
|
||||
if version > current_versions.get(db_name, 0):
|
||||
logger.debug(f"Migrating {db_name} db: {key}")
|
||||
await migrate(db)
|
||||
|
||||
if db.schema == None:
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field
|
||||
|
||||
env = Env()
|
||||
|
||||
VERSION = "0.12.3"
|
||||
VERSION = "0.13.0"
|
||||
|
||||
|
||||
def find_env_file():
|
||||
@@ -43,8 +43,6 @@ class CashuSettings(BaseSettings):
|
||||
class EnvSettings(CashuSettings):
|
||||
debug: bool = Field(default=False)
|
||||
log_level: str = Field(default="INFO")
|
||||
host: str = Field(default="127.0.0.1")
|
||||
port: int = Field(default=3338)
|
||||
cashu_dir: str = Field(default=os.path.join(str(Path.home()), ".cashu"))
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
def amount_split(amount):
|
||||
def amount_split(amount: int):
|
||||
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
|
||||
bits_amt = bin(amount)[::-1][:-2]
|
||||
rv = []
|
||||
for (pos, bit) in enumerate(bits_amt):
|
||||
for pos, bit in enumerate(bits_amt):
|
||||
if bit == "1":
|
||||
rv.append(2**pos)
|
||||
return rv
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from ..core.base import Invoice, MintKeyset, Proof
|
||||
from ..core.base import BlindedSignature, Invoice, MintKeyset, Proof
|
||||
from ..core.db import Connection, Database, table_with_schema
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ class LedgerCrud:
|
||||
async def store_promise(*args, **kwags):
|
||||
return await store_promise(*args, **kwags) # type: ignore
|
||||
|
||||
async def get_promise(*args, **kwags):
|
||||
return await get_promise(*args, **kwags) # type: ignore
|
||||
|
||||
async def update_lightning_invoice(*args, **kwags):
|
||||
return await update_lightning_invoice(*args, **kwags) # type: ignore
|
||||
|
||||
@@ -51,22 +54,39 @@ async def store_promise(
|
||||
amount: int,
|
||||
B_: str,
|
||||
C_: str,
|
||||
id: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'promises')}
|
||||
(amount, B_b, C_b)
|
||||
VALUES (?, ?, ?)
|
||||
(amount, B_b, C_b, id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
amount,
|
||||
str(B_),
|
||||
str(C_),
|
||||
id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def get_promise(
|
||||
db: Database,
|
||||
B_: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'promises')}
|
||||
WHERE B_b = ?
|
||||
""",
|
||||
(str(B_),),
|
||||
)
|
||||
return BlindedSignature(amount=row[0], C_=row[2], id=row[3]) if row else None
|
||||
|
||||
|
||||
async def get_proofs_used(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
@@ -88,13 +108,14 @@ async def invalidate_proof(
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'proofs_used')}
|
||||
(amount, C, secret)
|
||||
VALUES (?, ?, ?)
|
||||
(amount, C, secret, id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.amount,
|
||||
str(proof.C),
|
||||
str(proof.secret),
|
||||
str(proof.id),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from typing import Dict, List, Literal, Optional, Set, Tuple, Union
|
||||
@@ -178,7 +177,11 @@ class Ledger:
|
||||
C_ = b_dhke.step2_bob(B_, private_key_amount)
|
||||
logger.trace(f"crud: _generate_promise storing promise for {amount}")
|
||||
await self.crud.store_promise(
|
||||
amount=amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db
|
||||
amount=amount,
|
||||
B_=B_.serialize().hex(),
|
||||
C_=C_.serialize().hex(),
|
||||
id=keyset.id,
|
||||
db=self.db,
|
||||
)
|
||||
logger.trace(f"crud: _generate_promise stored promise for {amount}")
|
||||
return BlindedSignature(id=keyset.id, amount=amount, C_=C_.serialize().hex())
|
||||
@@ -804,7 +807,7 @@ class Ledger:
|
||||
"""
|
||||
logger.trace(f"called request_mint")
|
||||
if settings.mint_max_peg_in and amount > settings.mint_max_peg_in:
|
||||
raise Exception(f"Maximum mint amount is {settings.mint_max_peg_in} sats.")
|
||||
raise Exception(f"Maximum mint amount is {settings.mint_max_peg_in} sat.")
|
||||
if settings.mint_peg_out_only:
|
||||
raise Exception("Mint does not allow minting new tokens.")
|
||||
|
||||
@@ -904,7 +907,7 @@ class Ledger:
|
||||
invoice_amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||
if settings.mint_max_peg_out and invoice_amount > settings.mint_max_peg_out:
|
||||
raise Exception(
|
||||
f"Maximum melt amount is {settings.mint_max_peg_out} sats."
|
||||
f"Maximum melt amount is {settings.mint_max_peg_out} sat."
|
||||
)
|
||||
fees_msat = await self.check_fees(invoice)
|
||||
assert total_provided >= invoice_amount + fees_msat / 1000, Exception(
|
||||
@@ -1067,3 +1070,26 @@ class Ledger:
|
||||
|
||||
logger.trace(f"split successful")
|
||||
return prom_fst, prom_snd
|
||||
|
||||
async def restore(
|
||||
self, outputs: List[BlindedMessage]
|
||||
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
|
||||
promises: List[BlindedSignature] = []
|
||||
return_outputs: List[BlindedMessage] = []
|
||||
async with self.db.connect() as conn:
|
||||
for output in outputs:
|
||||
logger.trace(f"looking for promise: {output}")
|
||||
promise = await self.crud.get_promise(
|
||||
B_=output.B_, db=self.db, conn=conn
|
||||
)
|
||||
if promise is not None:
|
||||
# BEGIN backwards compatibility mints pre `m007_proofs_and_promises_store_id`
|
||||
# add keyset id to promise if not present only if the current keyset
|
||||
# is the only one ever used
|
||||
if not promise.id and len(self.keysets.keysets) == 1:
|
||||
promise.id = self.keyset.id
|
||||
# END backwards compatibility
|
||||
promises.append(promise)
|
||||
return_outputs.append(output)
|
||||
logger.trace(f"promise found: {promise}")
|
||||
return return_outputs, promises
|
||||
|
||||
@@ -159,3 +159,20 @@ async def m006_invoices_add_payment_hash(db: Database):
|
||||
await db.execute(
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash"
|
||||
)
|
||||
|
||||
|
||||
async def m007_proofs_and_promises_store_id(db: Database):
|
||||
"""
|
||||
Column that remembers the payment_hash as we're using
|
||||
the column hash as a random identifier now
|
||||
(see https://github.com/cashubtc/nuts/pull/14).
|
||||
"""
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT"
|
||||
)
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT"
|
||||
)
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT"
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..core.base import (
|
||||
PostMeltRequest,
|
||||
PostMintRequest,
|
||||
PostMintResponse,
|
||||
PostRestoreResponse,
|
||||
PostSplitRequest,
|
||||
PostSplitResponse,
|
||||
)
|
||||
@@ -109,7 +110,7 @@ async def request_mint(amount: int = 0) -> Union[GetMintResponse, CashuError]:
|
||||
"""
|
||||
logger.trace(f"> GET /mint: amount={amount}")
|
||||
if amount > 21_000_000 * 100_000_000 or amount <= 0:
|
||||
return CashuError(code=0, error="Amount must be a valid amount of sats.")
|
||||
return CashuError(code=0, error="Amount must be a valid amount of sat.")
|
||||
if settings.mint_peg_out_only:
|
||||
return CashuError(code=0, error="Mint does not allow minting new tokens.")
|
||||
try:
|
||||
@@ -229,3 +230,15 @@ async def split(
|
||||
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
|
||||
logger.trace(f"< POST /split: {resp}")
|
||||
return resp
|
||||
|
||||
|
||||
@router.post(
|
||||
"/restore", name="Restore", summary="Restores a blinded signature from a secret"
|
||||
)
|
||||
async def restore(payload: PostMintRequest) -> Union[CashuError, PostRestoreResponse]:
|
||||
assert payload.outputs, Exception("no outputs provided.")
|
||||
try:
|
||||
outputs, promises = await ledger.restore(payload.outputs)
|
||||
except Exception as exc:
|
||||
return CashuError(code=0, error=str(exc))
|
||||
return PostRestoreResponse(outputs=outputs, promises=promises)
|
||||
|
||||
@@ -65,6 +65,10 @@ class WalletsResponse(BaseModel):
|
||||
wallets: Dict
|
||||
|
||||
|
||||
class RestoreResponse(BaseModel):
|
||||
balance: int
|
||||
|
||||
|
||||
class InfoResponse(BaseModel):
|
||||
version: str
|
||||
wallet: str
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime
|
||||
from itertools import groupby, islice
|
||||
@@ -29,6 +30,7 @@ from .responses import (
|
||||
PayResponse,
|
||||
PendingResponse,
|
||||
ReceiveResponse,
|
||||
RestoreResponse,
|
||||
SendResponse,
|
||||
SwapResponse,
|
||||
WalletsResponse,
|
||||
@@ -40,12 +42,17 @@ router: APIRouter = APIRouter()
|
||||
def create_wallet(
|
||||
url=settings.mint_url, dir=settings.cashu_dir, name=settings.wallet_name
|
||||
):
|
||||
return Wallet(url, os.path.join(dir, name), name=name)
|
||||
return Wallet(
|
||||
url=url,
|
||||
db=os.path.join(dir, name),
|
||||
name=name,
|
||||
)
|
||||
|
||||
|
||||
async def load_mint(wallet: Wallet, mint: Optional[str] = None):
|
||||
if mint:
|
||||
wallet = create_wallet(mint)
|
||||
await init_wallet(wallet)
|
||||
await wallet.load_mint()
|
||||
return wallet
|
||||
|
||||
@@ -390,6 +397,19 @@ async def wallets():
|
||||
return WalletsResponse(wallets=result)
|
||||
|
||||
|
||||
@router.post("/restore", name="Restore wallet", response_model=RestoreResponse)
|
||||
async def restore(
|
||||
to: int = Query(default=..., description="Counter to which restore the wallet"),
|
||||
):
|
||||
if to < 0:
|
||||
raise Exception("Counter must be positive")
|
||||
await wallet.load_mint()
|
||||
await wallet.restore_promises(0, to)
|
||||
await wallet.invalidate(wallet.proofs)
|
||||
wallet.status()
|
||||
return RestoreResponse(balance=wallet.available_balance)
|
||||
|
||||
|
||||
@router.get("/info", name="Information about Cashu wallet", response_model=InfoResponse)
|
||||
async def info():
|
||||
if settings.nostr_private_key:
|
||||
|
||||
@@ -19,7 +19,12 @@ from ...core.helpers import sum_proofs
|
||||
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_seed_and_mnemonic,
|
||||
get_unused_locks,
|
||||
)
|
||||
from ...wallet.wallet import Wallet as Wallet
|
||||
from ..api.api_server import start_api_server
|
||||
from ..cli.cli_helpers import get_mint_wallet, print_mint_balances, verify_mint
|
||||
@@ -41,6 +46,15 @@ def run_api_server(ctx, param, daemon):
|
||||
ctx.exit()
|
||||
|
||||
|
||||
# https://github.com/pallets/click/issues/85#issuecomment-503464628
|
||||
def coro(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
return asyncio.run(f(*args, **kwargs))
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@click.group(cls=NaturalOrderGroup)
|
||||
@click.option(
|
||||
"--host",
|
||||
@@ -64,8 +78,16 @@ def run_api_server(ctx, param, daemon):
|
||||
callback=run_api_server,
|
||||
help="Start server for wallet REST API",
|
||||
)
|
||||
@click.option(
|
||||
"--tests",
|
||||
"-t",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Run in test mode (don't ask for CLI inputs)",
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(ctx: Context, host: str, walletname: str):
|
||||
@coro
|
||||
async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||
if settings.tor and not TorProxy().check_platform():
|
||||
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. You have two options: Either install Tor manually and set TOR=FALSE and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended). Or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly."
|
||||
error_str += "\n\n"
|
||||
@@ -81,31 +103,38 @@ def cli(ctx: Context, host: str, walletname: str):
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["HOST"] = host or settings.mint_url
|
||||
ctx.obj["WALLET_NAME"] = walletname
|
||||
wallet = Wallet(
|
||||
ctx.obj["HOST"], os.path.join(settings.cashu_dir, walletname), name=walletname
|
||||
)
|
||||
ctx.obj["WALLET"] = wallet
|
||||
asyncio.run(init_wallet(ctx.obj["WALLET"], load_proofs=False))
|
||||
settings.wallet_name = walletname
|
||||
|
||||
# MUTLIMINT: Select a wallet
|
||||
db_path = os.path.join(settings.cashu_dir, walletname)
|
||||
# if the command is "restore" we don't want to ask the user for a mnemonic
|
||||
# otherwise it will create a mnemonic and store it in the database
|
||||
if ctx.invoked_subcommand == "restore":
|
||||
wallet = await Wallet.with_db(
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_private_key=True
|
||||
)
|
||||
else:
|
||||
# # we need to run the migrations before we load the wallet for the first time
|
||||
# # otherwise the wallet will not be able to generate a new private key and store it
|
||||
wallet = await Wallet.with_db(
|
||||
ctx.obj["HOST"], db_path, name=walletname, skip_private_key=True
|
||||
)
|
||||
# now with the migrations done, we can load the wallet and generate a new mnemonic if needed
|
||||
wallet = await Wallet.with_db(ctx.obj["HOST"], db_path, name=walletname)
|
||||
|
||||
assert wallet, "Wallet not found."
|
||||
ctx.obj["WALLET"] = wallet
|
||||
# await init_wallet(ctx.obj["WALLET"], load_proofs=False)
|
||||
|
||||
# ------ MUTLIMINT ------- : Select a wallet
|
||||
# only if a command is one of a subset that needs to specify a mint host
|
||||
# if a mint host is already specified as an argument `host`, use it
|
||||
if ctx.invoked_subcommand not in ["send", "invoice", "pay"] or host:
|
||||
return
|
||||
# else: we ask the user to select one
|
||||
ctx.obj["WALLET"] = asyncio.run(
|
||||
get_mint_wallet(ctx)
|
||||
ctx.obj["WALLET"] = await get_mint_wallet(
|
||||
ctx
|
||||
) # select a specific wallet by CLI input
|
||||
asyncio.run(init_wallet(ctx.obj["WALLET"], load_proofs=False))
|
||||
|
||||
|
||||
# https://github.com/pallets/click/issues/85#issuecomment-503464628
|
||||
def coro(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
return asyncio.run(f(*args, **kwargs))
|
||||
|
||||
return wrapper
|
||||
await init_wallet(ctx.obj["WALLET"], load_proofs=False)
|
||||
|
||||
|
||||
@cli.command("pay", help="Pay Lightning invoice.")
|
||||
@@ -227,7 +256,7 @@ async def swap(ctx: Context):
|
||||
if incoming_wallet.url == outgoing_wallet.url:
|
||||
raise Exception("mints for swap have to be different")
|
||||
|
||||
amount = int(input("Enter amount to swap in sats: "))
|
||||
amount = int(input("Enter amount to swap in sat: "))
|
||||
assert amount > 0, "amount is not positive"
|
||||
|
||||
# request invoice from incoming mint
|
||||
@@ -550,6 +579,8 @@ async def locks(ctx):
|
||||
lock_str = f"P2PK:{pubkey}"
|
||||
print("---- Pay to public key (P2PK) lock ----\n")
|
||||
print(f"Lock: {lock_str}")
|
||||
print("")
|
||||
print("To see more information enter: cashu lock")
|
||||
# P2SH locks
|
||||
locks = await get_unused_locks(db=wallet.db)
|
||||
if len(locks):
|
||||
@@ -561,8 +592,7 @@ async def locks(ctx):
|
||||
print(f"Signature: {l.signature}")
|
||||
print("")
|
||||
print(f"--------------------------\n")
|
||||
else:
|
||||
print("No locks found. Create one using: cashu lock")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -616,7 +646,7 @@ async def wallets(ctx):
|
||||
for w in wallets:
|
||||
wallet = Wallet(ctx.obj["HOST"], os.path.join(settings.cashu_dir, w))
|
||||
try:
|
||||
await init_wallet(wallet)
|
||||
await wallet.load_proofs()
|
||||
if wallet.proofs and len(wallet.proofs):
|
||||
active_wallet = False
|
||||
if w == ctx.obj["WALLET_NAME"]:
|
||||
@@ -629,12 +659,12 @@ async def wallets(ctx):
|
||||
|
||||
|
||||
@cli.command("info", help="Information about Cashu wallet.")
|
||||
@click.option(
|
||||
"--mint", "-m", default=False, is_flag=True, help="Fetch mint information."
|
||||
)
|
||||
@click.option("--mint", default=False, is_flag=True, help="Fetch mint information.")
|
||||
@click.option("--mnemonic", default=False, is_flag=True, help="Show your mnemonic.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def info(ctx: Context, mint: bool):
|
||||
async def info(ctx: Context, mint: bool, mnemonic: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
print(f"Version: {settings.version}")
|
||||
print(f"Wallet: {ctx.obj['WALLET_NAME']}")
|
||||
if settings.debug:
|
||||
@@ -657,7 +687,6 @@ async def info(ctx: Context, mint: bool):
|
||||
print(f"HTTP proxy: {settings.http_proxy}")
|
||||
print(f"Mint URL: {ctx.obj['HOST']}")
|
||||
if mint:
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
mint_info: dict = (await wallet._load_mint_info()).dict()
|
||||
print("")
|
||||
print("Mint information:")
|
||||
@@ -677,4 +706,52 @@ async def info(ctx: Context, mint: bool):
|
||||
if mint_info["parameter"]:
|
||||
print(f"Parameter: {mint_info['parameter']}")
|
||||
|
||||
if mnemonic:
|
||||
assert wallet.mnemonic
|
||||
print(f"Mnemonic: {wallet.mnemonic}")
|
||||
return
|
||||
|
||||
|
||||
@cli.command("restore", help="Restore backups.")
|
||||
@click.option(
|
||||
"--batch",
|
||||
"-b",
|
||||
default=25,
|
||||
help="Batch size. Specifies how many proofs are restored in one batch.",
|
||||
type=int,
|
||||
)
|
||||
@click.option(
|
||||
"--to",
|
||||
"-t",
|
||||
default=2,
|
||||
help="Number of empty batches to complete the restore process.",
|
||||
type=int,
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def restore(ctx: Context, to: int, batch: int):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
# check if there is already a mnemonic in the database
|
||||
ret = await get_seed_and_mnemonic(wallet.db)
|
||||
if ret:
|
||||
print(
|
||||
"Wallet already has a mnemonic. You can't restore an already initialized wallet."
|
||||
)
|
||||
print("To restore a wallet, please delete the wallet directory and try again.")
|
||||
print("")
|
||||
print(
|
||||
f"The wallet directory is: {os.path.join(settings.cashu_dir, ctx.obj['WALLET_NAME'])}"
|
||||
)
|
||||
return
|
||||
# ask the user for a mnemonic but allow also no input
|
||||
print("Please enter your mnemonic to restore your balance.")
|
||||
mnemonic = input(
|
||||
"Enter mnemonic: ",
|
||||
)
|
||||
if not mnemonic:
|
||||
print("No mnemonic entered. Exiting.")
|
||||
return
|
||||
|
||||
await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch)
|
||||
await wallet.load_proofs()
|
||||
wallet.status()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from ..core.base import Invoice, KeyBase, P2SHScript, Proof, WalletKeyset
|
||||
from ..core.db import Connection, Database
|
||||
@@ -14,10 +14,17 @@ async def store_proof(
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO proofs
|
||||
(id, amount, C, secret, time_created)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
(id, amount, C, secret, time_created, derivation_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(proof.id, proof.amount, str(proof.C), str(proof.secret), int(time.time())),
|
||||
(
|
||||
proof.id,
|
||||
proof.amount,
|
||||
str(proof.C),
|
||||
str(proof.secret),
|
||||
int(time.time()),
|
||||
proof.derivation_path,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -62,10 +69,17 @@ async def invalidate_proof(
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO proofs_used
|
||||
(amount, C, secret, time_used, id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
(amount, C, secret, time_used, id, derivation_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(proof.amount, str(proof.C), str(proof.secret), int(time.time()), proof.id),
|
||||
(
|
||||
proof.amount,
|
||||
str(proof.C),
|
||||
str(proof.secret),
|
||||
int(time.time()),
|
||||
proof.id,
|
||||
proof.derivation_path,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -329,6 +343,52 @@ async def update_lightning_invoice(
|
||||
)
|
||||
|
||||
|
||||
async def bump_secret_derivation(
|
||||
db: Database,
|
||||
keyset_id: str,
|
||||
by: int = 1,
|
||||
skip: bool = False,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
rows = await (conn or db).fetchone(
|
||||
"SELECT counter from keysets WHERE id = ?", (keyset_id,)
|
||||
)
|
||||
# if no counter for this keyset, create one
|
||||
if not rows:
|
||||
await (conn or db).execute(
|
||||
"UPDATE keysets SET counter = ? WHERE id = ?",
|
||||
(
|
||||
0,
|
||||
keyset_id,
|
||||
),
|
||||
)
|
||||
counter = 0
|
||||
else:
|
||||
counter = int(rows[0])
|
||||
|
||||
if not skip:
|
||||
await (conn or db).execute(
|
||||
f"UPDATE keysets SET counter = counter + {by} WHERE id = ?",
|
||||
(keyset_id,),
|
||||
)
|
||||
return counter
|
||||
|
||||
|
||||
async def set_secret_derivation(
|
||||
db: Database,
|
||||
keyset_id: str,
|
||||
counter: int,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
await (conn or db).execute(
|
||||
"UPDATE keysets SET counter = ? WHERE id = ?",
|
||||
(
|
||||
counter,
|
||||
keyset_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def set_nostr_last_check_timestamp(
|
||||
db: Database,
|
||||
timestamp: int,
|
||||
@@ -351,3 +411,41 @@ async def get_nostr_last_check_timestamp(
|
||||
("dm",),
|
||||
)
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
async def get_seed_and_mnemonic(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
row = await (conn or db).fetchone(
|
||||
f"""
|
||||
SELECT seed, mnemonic from seed
|
||||
""",
|
||||
)
|
||||
return (
|
||||
(
|
||||
row[0],
|
||||
row[1],
|
||||
)
|
||||
if row
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
async def store_seed_and_mnemonic(
|
||||
db: Database,
|
||||
seed: str,
|
||||
mnemonic: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO seed
|
||||
(seed, mnemonic)
|
||||
VALUES (?, ?)
|
||||
""",
|
||||
(
|
||||
seed,
|
||||
mnemonic,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
import click
|
||||
from loguru import logger
|
||||
|
||||
from ..core.base import TokenV1, TokenV2, TokenV3, TokenV3Token
|
||||
from ..core.db import Database
|
||||
from ..core.helpers import sum_proofs
|
||||
from ..core.migrations import migrate_databases
|
||||
from ..core.settings import settings
|
||||
from ..wallet import migrations
|
||||
from ..wallet.crud import get_keyset, get_unused_locks
|
||||
from ..wallet.wallet import Wallet as Wallet
|
||||
from ..wallet.crud import get_keyset
|
||||
from ..wallet.wallet import Wallet
|
||||
|
||||
|
||||
async def migrate_wallet_db(db: Database):
|
||||
await migrate_databases(db, migrations)
|
||||
|
||||
|
||||
async def init_wallet(wallet: Wallet, load_proofs: bool = True):
|
||||
"""Performs migrations and loads proofs from db."""
|
||||
await migrate_databases(wallet.db, migrations)
|
||||
await wallet._migrate_database()
|
||||
await wallet._init_private_key()
|
||||
if load_proofs:
|
||||
await wallet.load_proofs(reload=True)
|
||||
|
||||
@@ -31,7 +35,9 @@ async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
|
||||
assert t.mint, Exception(
|
||||
"redeem_TokenV3_multimint: multimint redeem without URL"
|
||||
)
|
||||
mint_wallet = Wallet(t.mint, os.path.join(settings.cashu_dir, wallet.name))
|
||||
mint_wallet = await Wallet.with_db(
|
||||
t.mint, os.path.join(settings.cashu_dir, wallet.name)
|
||||
)
|
||||
keysets = mint_wallet._get_proofs_keysets(t.proofs)
|
||||
logger.debug(f"Keysets in tokens: {keysets}")
|
||||
# loop over all keysets
|
||||
@@ -130,7 +136,7 @@ async def receive(
|
||||
assert mint_keysets, Exception("we don't know this keyset")
|
||||
assert mint_keysets.mint_url, Exception("we don't know this mint's URL")
|
||||
# now we have the URL
|
||||
mint_wallet = Wallet(
|
||||
mint_wallet = await Wallet.with_db(
|
||||
mint_keysets.mint_url,
|
||||
os.path.join(settings.cashu_dir, wallet.name),
|
||||
)
|
||||
|
||||
@@ -176,3 +176,20 @@ async def m008_keysets_add_public_keys(db: Database):
|
||||
Stores public keys of mint in a new column of table keysets.
|
||||
"""
|
||||
await db.execute("ALTER TABLE keysets ADD COLUMN public_keys TEXT")
|
||||
|
||||
|
||||
async def m009_privatekey_and_determinstic_key_derivation(db: Database):
|
||||
await db.execute("ALTER TABLE keysets ADD COLUMN counter INTEGER DEFAULT 0")
|
||||
await db.execute("ALTER TABLE proofs ADD COLUMN derivation_path TEXT")
|
||||
await db.execute("ALTER TABLE proofs_used ADD COLUMN derivation_path TEXT")
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS seed (
|
||||
seed TEXT NOT NULL,
|
||||
mnemonic TEXT NOT NULL,
|
||||
|
||||
UNIQUE (seed, mnemonic)
|
||||
);
|
||||
"""
|
||||
)
|
||||
# await db.execute("INSERT INTO secret_derivation (counter) VALUES (0)")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
869
poetry.lock
generated
869
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,8 @@ wheel = "^0.38.4"
|
||||
importlib-metadata = "^5.2.0"
|
||||
psycopg2-binary = {version = "^2.9.5", optional = true }
|
||||
httpx = "0.23.0"
|
||||
bip32 = "^3.4"
|
||||
mnemonic = "^0.20"
|
||||
|
||||
[tool.poetry.extras]
|
||||
pgsql = ["psycopg2-binary"]
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
anyio==3.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
attrs==22.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
anyio==3.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
attrs==23.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bip32==3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
certifi==2022.12.7 ; python_version >= "3.7" and python_version < "4"
|
||||
certifi==2023.5.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
charset-normalizer==3.0.1 ; python_version >= "3.7" and python_version < "4"
|
||||
click==8.1.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
charset-normalizer==3.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
click==8.1.5 ; python_version >= "3.7" and python_version < "4.0"
|
||||
coincurve==18.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
ecdsa==0.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
environs==9.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
exceptiongroup==1.1.0 ; python_version >= "3.7" and python_version < "3.11"
|
||||
exceptiongroup==1.1.2 ; python_version >= "3.7" and python_version < "3.11"
|
||||
fastapi==0.83.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
idna==3.4 ; python_version >= "3.7" and python_version < "4"
|
||||
idna==3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
importlib-metadata==5.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
iniconfig==2.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
loguru==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
marshmallow==3.19.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
mnemonic==0.20 ; python_version >= "3.7" and python_version < "4.0"
|
||||
outcome==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
packaging==23.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
packaging==23.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycryptodomex==3.17 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pydantic==1.10.5 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycryptodomex==3.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pydantic==1.10.11 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest-asyncio==0.19.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest==7.2.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-bitcoinlib==0.11.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-dotenv==0.21.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
requests==2.28.2 ; python_version >= "3.7" and python_version < "4"
|
||||
requests==2.31.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
setuptools==65.7.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
@@ -41,11 +42,10 @@ sniffio==1.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
tomli==2.0.1 ; python_version >= "3.7" and python_version < "3.11"
|
||||
typing-extensions==4.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
urllib3==1.26.14 ; python_version >= "3.7" and python_version < "4"
|
||||
typing-extensions==4.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
urllib3==2.0.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
websocket-client==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
websocket-client==1.6.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
wheel==0.38.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
zipp==3.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
|
||||
|
||||
setuptools.setup(
|
||||
name="cashu",
|
||||
version="0.12.3",
|
||||
version="0.13.0",
|
||||
description="Ecash wallet and mint for Bitcoin Lightning",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@@ -37,7 +37,6 @@ class UvicornServer(multiprocessing.Process):
|
||||
def run(self, *args, **kwargs):
|
||||
settings.lightning = False
|
||||
settings.mint_lightning_backend = "FakeWallet"
|
||||
settings.mint_listen_port = 3337
|
||||
settings.mint_database = "data/test_mint"
|
||||
settings.mint_private_key = self.private_key
|
||||
settings.mint_derivation_path = "0/0/0/0"
|
||||
@@ -46,36 +45,24 @@ class UvicornServer(multiprocessing.Process):
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
dirpath = Path("data/test_wallet")
|
||||
dirpath = Path("data/wallet1")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
dirpath = Path("data/wallet2")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
dirpath = Path("data/wallet3")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
self.server.run()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def mint():
|
||||
settings.mint_listen_port = 3337
|
||||
settings.port = 3337
|
||||
settings.mint_url = "http://localhost:3337"
|
||||
settings.port = settings.mint_listen_port
|
||||
settings.nostr_private_key = secrets.token_hex(32) # wrong private key
|
||||
config = uvicorn.Config(
|
||||
"cashu.mint.app:app",
|
||||
port=settings.mint_listen_port,
|
||||
host="127.0.0.1",
|
||||
)
|
||||
|
||||
server = UvicornServer(config=config)
|
||||
server.start()
|
||||
time.sleep(1)
|
||||
yield server
|
||||
server.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def ledger():
|
||||
async def start_mint_init(ledger):
|
||||
async def start_mint_init(ledger: Ledger):
|
||||
await migrate_databases(ledger.db, migrations_mint)
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets()
|
||||
@@ -93,20 +80,18 @@ async def ledger():
|
||||
yield ledger
|
||||
|
||||
|
||||
# @pytest.fixture(autouse=True, scope="session")
|
||||
# def mint_3338():
|
||||
# settings.mint_listen_port = 3338
|
||||
# settings.port = 3338
|
||||
# settings.mint_url = "http://localhost:3338"
|
||||
# settings.port = settings.mint_listen_port
|
||||
# config = uvicorn.Config(
|
||||
# "cashu.mint.app:app",
|
||||
# port=settings.mint_listen_port,
|
||||
# host="127.0.0.1",
|
||||
# )
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def mint():
|
||||
settings.mint_listen_port = 3337
|
||||
settings.mint_url = "http://localhost:3337"
|
||||
config = uvicorn.Config(
|
||||
"cashu.mint.app:app",
|
||||
port=settings.mint_listen_port,
|
||||
host="127.0.0.1",
|
||||
)
|
||||
|
||||
# server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY")
|
||||
# server.start()
|
||||
# time.sleep(1)
|
||||
# yield server
|
||||
# server.stop()
|
||||
server = UvicornServer(config=config)
|
||||
server.start()
|
||||
time.sleep(1)
|
||||
yield server
|
||||
server.stop()
|
||||
|
||||
@@ -13,20 +13,15 @@ from tests.conftest import SERVER_ENDPOINT, mint
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def cli_prefix():
|
||||
yield ["--wallet", "test_wallet", "--host", settings.mint_url]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def wallet():
|
||||
wallet = Wallet(settings.mint_host, "data/test_wallet", "wallet")
|
||||
asyncio.run(migrate_databases(wallet.db, migrations))
|
||||
asyncio.run(wallet.load_proofs())
|
||||
yield wallet
|
||||
yield ["--wallet", "test_cli_wallet", "--host", settings.mint_url, "--tests"]
|
||||
|
||||
|
||||
async def init_wallet():
|
||||
wallet = Wallet(settings.mint_host, "data/test_wallet", "wallet")
|
||||
await migrate_databases(wallet.db, migrations)
|
||||
wallet = await Wallet.with_db(
|
||||
url=settings.mint_host,
|
||||
db="data/test_cli_wallet",
|
||||
name="wallet",
|
||||
)
|
||||
await wallet.load_proofs()
|
||||
return wallet
|
||||
|
||||
@@ -50,7 +45,7 @@ def test_info_with_mint(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "info", "-m"],
|
||||
[*cli_prefix, "info", "--mint"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INFO -M")
|
||||
@@ -59,6 +54,20 @@ def test_info_with_mint(cli_prefix):
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_info_with_mnemonic(cli_prefix):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[*cli_prefix, "info", "--mnemonic"],
|
||||
)
|
||||
assert result.exception is None
|
||||
print("INFO -M")
|
||||
print(result.output)
|
||||
assert "Mnemonic" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
def test_balance(cli_prefix):
|
||||
runner = CliRunner()
|
||||
@@ -113,7 +122,7 @@ def test_wallets(cli_prefix):
|
||||
print("WALLETS")
|
||||
# on github this is empty
|
||||
if len(result.output):
|
||||
assert "test_wallet" in result.output
|
||||
assert "test_cli_wallet" in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
|
||||
@@ -43,9 +43,11 @@ def test_step1():
|
||||
""""""
|
||||
B_, blinding_factor = step1_alice(
|
||||
"test_message",
|
||||
blinding_factor=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
), # 32 bytes
|
||||
blinding_factor=PrivateKey(
|
||||
privkey=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
) # 32 bytes
|
||||
),
|
||||
)
|
||||
|
||||
assert (
|
||||
@@ -60,9 +62,12 @@ def test_step1():
|
||||
def test_step2():
|
||||
B_, _ = step1_alice(
|
||||
"test_message",
|
||||
blinding_factor=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
), # 32 bytes
|
||||
blinding_factor=PrivateKey(
|
||||
privkey=bytes.fromhex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||
),
|
||||
raw=True,
|
||||
),
|
||||
)
|
||||
a = PrivateKey(
|
||||
privkey=bytes.fromhex(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import secrets
|
||||
from typing import List
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
from cashu.core.base import Proof, Secret, SecretKind, Tags
|
||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
||||
@@ -34,10 +36,20 @@ def assert_amt(proofs: List[Proof], expected: int):
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
async def reset_wallet_db(wallet: Wallet):
|
||||
await wallet.db.execute("DELETE FROM proofs")
|
||||
await wallet.db.execute("DELETE FROM proofs_used")
|
||||
await wallet.db.execute("DELETE FROM keysets")
|
||||
await wallet._load_mint()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
wallet1 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="data/wallet1",
|
||||
name="wallet1",
|
||||
)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
yield wallet1
|
||||
@@ -45,14 +57,34 @@ async def wallet1(mint):
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(mint):
|
||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
wallet2 = await Wallet2.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="data/wallet2",
|
||||
name="wallet2",
|
||||
)
|
||||
await wallet2.load_mint()
|
||||
wallet2.status()
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet3(mint):
|
||||
dirpath = Path("data/wallet3")
|
||||
if dirpath.exists() and dirpath.is_dir():
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
wallet3 = await Wallet1.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="data/wallet3",
|
||||
name="wallet3",
|
||||
)
|
||||
await wallet3.db.execute("DELETE FROM proofs")
|
||||
await wallet3.db.execute("DELETE FROM proofs_used")
|
||||
await wallet3.load_mint()
|
||||
wallet3.status()
|
||||
yield wallet3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keys(wallet1: Wallet):
|
||||
assert wallet1.keys.public_keys
|
||||
@@ -277,10 +309,283 @@ async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet):
|
||||
await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_state(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
assert wallet1.balance == 64
|
||||
resp = await wallet1.check_proof_state(wallet1.proofs)
|
||||
assert resp.dict()["spendable"]
|
||||
assert resp.dict()["pending"]
|
||||
|
||||
|
||||
async def test_bump_secret_derivation(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture humble"
|
||||
)
|
||||
secrets1, rs1, derivaion_paths1 = await wallet3.generate_n_secrets(5)
|
||||
secrets2, rs2, derivaion_paths2 = await wallet3.generate_secrets_from_to(0, 4)
|
||||
assert secrets1 == secrets2
|
||||
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
||||
assert derivaion_paths1 == derivaion_paths2
|
||||
assert secrets1 == [
|
||||
"9bfb12704297fe90983907d122838940755fcce370ce51e9e00a4275a347c3fe",
|
||||
"dbc5e05f2b1f24ec0e2ab6e8312d5e13f57ada52594d4caf429a697d9c742490",
|
||||
"06a29fa8081b3a620b50b473fc80cde9a575c3b94358f3513c03007f8b66321e",
|
||||
"652d08c804bd2c5f2c1f3e3d8895860397df394b30473753227d766affd15e89",
|
||||
"654e5997f8a20402f7487296b6f7e463315dd52fc6f6cc5a4e35c7f6ccac77e0",
|
||||
]
|
||||
assert derivaion_paths1 == [
|
||||
"m/129372'/0'/2004500376'/0'",
|
||||
"m/129372'/0'/2004500376'/1'",
|
||||
"m/129372'/0'/2004500376'/2'",
|
||||
"m/129372'/0'/2004500376'/3'",
|
||||
"m/129372'/0'/2004500376'/4'",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bump_secret_derivation_two_steps(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture humble"
|
||||
)
|
||||
secrets1_1, rs1_1, derivaion_paths1 = await wallet3.generate_n_secrets(2)
|
||||
secrets1_2, rs1_2, derivaion_paths2 = await wallet3.generate_n_secrets(3)
|
||||
secrets1 = secrets1_1 + secrets1_2
|
||||
rs1 = rs1_1 + rs1_2
|
||||
secrets2, rs2, derivaion_paths = await wallet3.generate_secrets_from_to(0, 4)
|
||||
assert secrets1 == secrets2
|
||||
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_secrets_from_to(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture humble"
|
||||
)
|
||||
secrets1, rs1, derivaion_paths1 = await wallet3.generate_secrets_from_to(0, 4)
|
||||
assert len(secrets1) == 5
|
||||
secrets2, rs2, derivaion_paths2 = await wallet3.generate_secrets_from_to(2, 4)
|
||||
assert len(secrets2) == 3
|
||||
assert secrets1[2:] == secrets2
|
||||
assert [r.private_key for r in rs1[2:]] == [r.private_key for r in rs2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_mint(wallet3: Wallet):
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.mint(64)
|
||||
assert wallet3.balance == 64
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs()
|
||||
wallet3.proofs = []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises(0, 20)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_with_invalid_mnemonic(wallet3: Wallet):
|
||||
await assert_err(
|
||||
wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture picture"
|
||||
),
|
||||
"Invalid mnemonic",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_split_to_send(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"half depart obvious quality work element tank gorilla view sugar picture humble"
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
await wallet3.mint(64)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||
wallet3.proofs, 32, set_reserved=True
|
||||
)
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs()
|
||||
wallet3.proofs = []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises(0, 100)
|
||||
assert wallet3.balance == 64 * 2
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"hello rug want adapt talent together lunar method bean expose beef position"
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
await wallet3.mint(64)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||
wallet3.proofs, 32, set_reserved=True
|
||||
)
|
||||
|
||||
await wallet2.redeem(spendable_proofs)
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises(0, 100)
|
||||
assert wallet3.balance == 64 + 2 * 32
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
assert wallet3.balance == 32
|
||||
|
||||
|
||||
class ProofBox:
|
||||
proofs: Dict[str, Proof] = {}
|
||||
|
||||
def add(self, proofs: List[Proof]) -> None:
|
||||
for proof in proofs:
|
||||
if proof.secret in self.proofs:
|
||||
if self.proofs[proof.secret].C != proof.C:
|
||||
print("Proofs are not equal")
|
||||
print(self.proofs[proof.secret])
|
||||
print(proof)
|
||||
else:
|
||||
self.proofs[proof.secret] = proof
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet):
|
||||
await wallet3._init_private_key(
|
||||
"lucky broken tell exhibit shuffle tomato ethics virus rabbit spread measure text"
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
await wallet3.mint(64)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||
wallet3.proofs, 32, set_reserved=True
|
||||
)
|
||||
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises(0, 100)
|
||||
assert wallet3.balance == 64 + 2 * 32 + 32
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_send_twice(
|
||||
wallet3: Wallet,
|
||||
):
|
||||
box = ProofBox()
|
||||
wallet3.private_key = PrivateKey()
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
await wallet3.mint(2)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
keep_proofs, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||
wallet3.proofs, 1, set_reserved=True
|
||||
)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.available_balance == 1
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.available_balance == 2
|
||||
assert wallet3.balance == 2
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises(0, 10)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 5
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
# again
|
||||
|
||||
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||
wallet3.proofs, 1, set_reserved=True
|
||||
)
|
||||
box.add(wallet3.proofs)
|
||||
|
||||
assert wallet3.available_balance == 1
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.available_balance == 2
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises(0, 15)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 7
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
assert wallet3.balance == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
|
||||
wallet3: Wallet,
|
||||
):
|
||||
box = ProofBox()
|
||||
await wallet3._init_private_key(
|
||||
"casual demise flight cradle feature hub link slim remember anger front asthma"
|
||||
)
|
||||
await reset_wallet_db(wallet3)
|
||||
|
||||
await wallet3.mint(64)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
keep_proofs, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||
wallet3.proofs, 10, set_reserved=True
|
||||
)
|
||||
box.add(wallet3.proofs)
|
||||
|
||||
assert wallet3.available_balance == 64 - 10
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.available_balance == 64
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises(0, 20)
|
||||
box.add(wallet3.proofs)
|
||||
assert wallet3.balance == 138
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
# again
|
||||
|
||||
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||
wallet3.proofs, 12, set_reserved=True
|
||||
)
|
||||
|
||||
assert wallet3.available_balance == 64 - 12
|
||||
await wallet3.redeem(spendable_proofs)
|
||||
assert wallet3.available_balance == 64
|
||||
|
||||
await reset_wallet_db(wallet3)
|
||||
await wallet3.load_proofs(reload=True)
|
||||
assert wallet3.proofs == []
|
||||
assert wallet3.balance == 0
|
||||
await wallet3.restore_promises(0, 50)
|
||||
assert wallet3.balance == 182
|
||||
await wallet3.invalidate(wallet3.proofs)
|
||||
assert wallet3.balance == 64
|
||||
|
||||
@@ -14,8 +14,11 @@ from tests.conftest import SERVER_ENDPOINT, mint
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet(mint):
|
||||
wallet = Wallet(SERVER_ENDPOINT, "data/test_wallet_api", "wallet_api")
|
||||
await migrate_databases(wallet.db, migrations)
|
||||
wallet = await Wallet.with_db(
|
||||
url=SERVER_ENDPOINT,
|
||||
db="data/test_wallet_api",
|
||||
name="wallet_api",
|
||||
)
|
||||
await wallet.load_mint()
|
||||
wallet.status()
|
||||
yield wallet
|
||||
|
||||
@@ -36,7 +36,7 @@ def assert_amt(proofs: List[Proof], expected: int):
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet_p2pk_1", "wallet1")
|
||||
wallet1 = await Wallet1.with_db(SERVER_ENDPOINT, "data/wallet_p2pk_1", "wallet1")
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
@@ -45,7 +45,7 @@ async def wallet1(mint):
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(mint):
|
||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet_p2pk_2", "wallet2")
|
||||
wallet2 = await Wallet2.with_db(SERVER_ENDPOINT, "data/wallet_p2pk_2", "wallet2")
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet2.load_mint()
|
||||
|
||||
@@ -36,7 +36,7 @@ def assert_amt(proofs: List[Proof], expected: int):
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet1(mint):
|
||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet_p2sh_1", "wallet1")
|
||||
wallet1 = await Wallet1.with_db(SERVER_ENDPOINT, "data/wallet_p2sh_1", "wallet1")
|
||||
await migrate_databases(wallet1.db, migrations)
|
||||
await wallet1.load_mint()
|
||||
wallet1.status()
|
||||
@@ -45,7 +45,7 @@ async def wallet1(mint):
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def wallet2(mint):
|
||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet_p2sh_2", "wallet2")
|
||||
wallet2 = await Wallet2.with_db(SERVER_ENDPOINT, "data/wallet_p2sh_2", "wallet2")
|
||||
await migrate_databases(wallet2.db, migrations)
|
||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||
await wallet2.load_mint()
|
||||
|
||||
Reference in New Issue
Block a user