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:
callebtc
2023-07-24 13:42:56 +02:00
committed by GitHub
parent 337456333e
commit 0b2468914d
29 changed files with 1881 additions and 650 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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>.
"""

View File

@@ -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

View File

@@ -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:

View File

@@ -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"))

View File

@@ -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

View File

@@ -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),
),
)

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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)

View File

@@ -65,6 +65,10 @@ class WalletsResponse(BaseModel):
wallets: Dict
class RestoreResponse(BaseModel):
balance: int
class InfoResponse(BaseModel):
version: str
wallet: str

View File

@@ -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:

View File

@@ -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()

View File

@@ -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,
),
)

View File

@@ -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),
)

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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",

View File

@@ -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()

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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()