mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-22 11:24:19 +01:00
Wallet: fix secret derivation & new CLI command cashu selfpay (#331)
* refactor wallet restore tests * fix secret derivation * selfpay to refresh tokens
This commit is contained in:
@@ -809,3 +809,30 @@ async def restore(ctx: Context, to: int, batch: int):
|
|||||||
await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch)
|
await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch)
|
||||||
await wallet.load_proofs()
|
await wallet.load_proofs()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("selfpay", help="Refresh tokens.")
|
||||||
|
# @click.option("--all", default=False, is_flag=True, help="Execute on all available mints.")
|
||||||
|
@click.pass_context
|
||||||
|
@coro
|
||||||
|
async def selfpay(ctx: Context, all: bool = False):
|
||||||
|
wallet = await get_mint_wallet(ctx, force_select=True)
|
||||||
|
await wallet.load_mint()
|
||||||
|
|
||||||
|
# get balance on this mint
|
||||||
|
mint_balance_dict = await wallet.balance_per_minturl()
|
||||||
|
mint_balance = mint_balance_dict[wallet.url]["available"]
|
||||||
|
# send balance once to mark as reserved
|
||||||
|
await wallet.split_to_send(wallet.proofs, mint_balance, None, set_reserved=True)
|
||||||
|
# load all reserved proofs (including the one we just sent)
|
||||||
|
reserved_proofs = await get_reserved_proofs(wallet.db)
|
||||||
|
if not len(reserved_proofs):
|
||||||
|
print("No balance on this mint.")
|
||||||
|
return
|
||||||
|
|
||||||
|
token = await wallet.serialize_proofs(reserved_proofs)
|
||||||
|
print(f"Selfpay token for mint {wallet.url}:")
|
||||||
|
print("")
|
||||||
|
print(token)
|
||||||
|
tokenObj = TokenV3.deserialize(token)
|
||||||
|
await receive(wallet, tokenObj)
|
||||||
|
|||||||
@@ -46,12 +46,11 @@ async def get_mint_wallet(ctx: Context, force_select: bool = False):
|
|||||||
mint_url = wallet.url
|
mint_url = wallet.url
|
||||||
|
|
||||||
# load this mint_url into a wallet
|
# load this mint_url into a wallet
|
||||||
mint_wallet = Wallet(
|
mint_wallet = await Wallet.with_db(
|
||||||
mint_url,
|
mint_url,
|
||||||
os.path.join(settings.cashu_dir, ctx.obj["WALLET_NAME"]),
|
os.path.join(settings.cashu_dir, ctx.obj["WALLET_NAME"]),
|
||||||
name=wallet.name,
|
name=wallet.name,
|
||||||
)
|
)
|
||||||
# await mint_wallet.load_mint()
|
|
||||||
await mint_wallet.load_proofs(reload=True)
|
await mint_wallet.load_proofs(reload=True)
|
||||||
|
|
||||||
return mint_wallet
|
return mint_wallet
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class WalletSecrets(SupportsDb, SupportsKeysets):
|
|||||||
await self.generate_determinstic_secret(s) for s in secret_counters
|
await self.generate_determinstic_secret(s) for s in secret_counters
|
||||||
]
|
]
|
||||||
# secrets are supplied as str
|
# secrets are supplied as str
|
||||||
secrets = [hashlib.sha256(s[0]).hexdigest() for s in secrets_rs_derivationpaths]
|
secrets = [s[0].hex() for s in secrets_rs_derivationpaths]
|
||||||
# rs are supplied as PrivateKey
|
# rs are supplied as PrivateKey
|
||||||
rs = [PrivateKey(privkey=s[1], raw=True) for s in secrets_rs_derivationpaths]
|
rs = [PrivateKey(privkey=s[1], raw=True) for s in secrets_rs_derivationpaths]
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ class WalletSecrets(SupportsDb, SupportsKeysets):
|
|||||||
await self.generate_determinstic_secret(s) for s in secret_counters
|
await self.generate_determinstic_secret(s) for s in secret_counters
|
||||||
]
|
]
|
||||||
# secrets are supplied as str
|
# secrets are supplied as str
|
||||||
secrets = [hashlib.sha256(s[0]).hexdigest() for s in secrets_rs_derivationpaths]
|
secrets = [s[0].hex() for s in secrets_rs_derivationpaths]
|
||||||
# rs are supplied as PrivateKey
|
# rs are supplied as PrivateKey
|
||||||
rs = [PrivateKey(privkey=s[1], raw=True) for s in secrets_rs_derivationpaths]
|
rs = [PrivateKey(privkey=s[1], raw=True) for s in secrets_rs_derivationpaths]
|
||||||
derivation_paths = [s[2] for s in secrets_rs_derivationpaths]
|
derivation_paths = [s[2] for s in secrets_rs_derivationpaths]
|
||||||
|
|||||||
@@ -1368,7 +1368,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
|||||||
outputs (List[BlindedMessage]): Outputs for which we request promises
|
outputs (List[BlindedMessage]): Outputs for which we request promises
|
||||||
secrets (List[str]): Secrets generated for the outputs
|
secrets (List[str]): Secrets generated for the outputs
|
||||||
rs (List[PrivateKey]): Random blinding factors generated for the outputs
|
rs (List[PrivateKey]): Random blinding factors generated for the outputs
|
||||||
derivation_paths (List[str]): Derivation paths for the secrets
|
derivation_paths (List[str]): Derivation paths used for the secrets necessary to unblind the promises
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Proof]: List of restored proofs
|
List[Proof]: List of restored proofs
|
||||||
|
|||||||
@@ -284,3 +284,14 @@ def test_pending(cli_prefix):
|
|||||||
assert result.exception is None
|
assert result.exception is None
|
||||||
print(result.output)
|
print(result.output)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_selfpay(cli_prefix):
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[*cli_prefix, "selfpay"],
|
||||||
|
)
|
||||||
|
assert result.exception is None
|
||||||
|
print(result.output)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
|
||||||
from cashu.core.base import Proof
|
from cashu.core.base import Proof
|
||||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
|
||||||
from cashu.core.errors import CashuError, KeysetNotFoundError
|
from cashu.core.errors import CashuError, KeysetNotFoundError
|
||||||
from cashu.core.helpers import sum_proofs
|
from cashu.core.helpers import sum_proofs
|
||||||
from cashu.core.settings import settings
|
from cashu.core.settings import settings
|
||||||
@@ -94,6 +93,7 @@ async def test_get_keys(wallet1: Wallet):
|
|||||||
assert len(wallet1.keys.public_keys) == settings.max_order
|
assert len(wallet1.keys.public_keys) == settings.max_order
|
||||||
keyset = await wallet1._get_keys(wallet1.url)
|
keyset = await wallet1._get_keys(wallet1.url)
|
||||||
assert keyset.id is not None
|
assert keyset.id is not None
|
||||||
|
assert keyset.id == "1cCNIAZ2X/w1"
|
||||||
assert isinstance(keyset.id, str)
|
assert isinstance(keyset.id, str)
|
||||||
assert len(keyset.id) > 0
|
assert len(keyset.id) > 0
|
||||||
|
|
||||||
@@ -312,40 +312,6 @@ async def test_split_invalid_amount(wallet1: Wallet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_create_p2pk_pubkey(wallet1: Wallet):
|
|
||||||
invoice = await wallet1.request_mint(64)
|
|
||||||
await wallet1.mint(64, hash=invoice.hash)
|
|
||||||
pubkey = await wallet1.create_p2pk_pubkey()
|
|
||||||
PublicKey(bytes.fromhex(pubkey), raw=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_p2sh(wallet1: Wallet, wallet2: Wallet):
|
|
||||||
invoice = await wallet1.request_mint(64)
|
|
||||||
await wallet1.mint(64, hash=invoice.hash)
|
|
||||||
_ = await wallet1.create_p2sh_address_and_store() # receiver side
|
|
||||||
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8) # sender side
|
|
||||||
|
|
||||||
frst_proofs, scnd_proofs = await wallet2.redeem(send_proofs) # receiver side
|
|
||||||
assert len(frst_proofs) == 0
|
|
||||||
assert len(scnd_proofs) == 1
|
|
||||||
assert sum_proofs(scnd_proofs) == 8
|
|
||||||
assert wallet2.balance == 8
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet):
|
|
||||||
invoice = await wallet1.request_mint(64)
|
|
||||||
await wallet1.mint(64, hash=invoice.hash)
|
|
||||||
wallet1_address = await wallet1.create_p2sh_address_and_store() # receiver side
|
|
||||||
secret_lock = await wallet1.create_p2sh_lock(wallet1_address) # sender side
|
|
||||||
_, send_proofs = await wallet1.split_to_send(
|
|
||||||
wallet1.proofs, 8, secret_lock
|
|
||||||
) # sender side
|
|
||||||
await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_token_state(wallet1: Wallet):
|
async def test_token_state(wallet1: Wallet):
|
||||||
invoice = await wallet1.request_mint(64)
|
invoice = await wallet1.request_mint(64)
|
||||||
@@ -354,275 +320,3 @@ async def test_token_state(wallet1: Wallet):
|
|||||||
resp = await wallet1.check_proof_state(wallet1.proofs)
|
resp = await wallet1.check_proof_state(wallet1.proofs)
|
||||||
assert resp.dict()["spendable"]
|
assert resp.dict()["spendable"]
|
||||||
assert resp.dict()["pending"]
|
assert resp.dict()["pending"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
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, derivation_paths1 = await wallet3.generate_n_secrets(5)
|
|
||||||
secrets2, rs2, derivation_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 derivation_paths1 == derivation_paths2
|
|
||||||
assert secrets1 == [
|
|
||||||
"9bfb12704297fe90983907d122838940755fcce370ce51e9e00a4275a347c3fe",
|
|
||||||
"dbc5e05f2b1f24ec0e2ab6e8312d5e13f57ada52594d4caf429a697d9c742490",
|
|
||||||
"06a29fa8081b3a620b50b473fc80cde9a575c3b94358f3513c03007f8b66321e",
|
|
||||||
"652d08c804bd2c5f2c1f3e3d8895860397df394b30473753227d766affd15e89",
|
|
||||||
"654e5997f8a20402f7487296b6f7e463315dd52fc6f6cc5a4e35c7f6ccac77e0",
|
|
||||||
]
|
|
||||||
assert derivation_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, derivation_paths1 = await wallet3.generate_n_secrets(2)
|
|
||||||
secrets1_2, rs1_2, derivation_paths2 = await wallet3.generate_n_secrets(3)
|
|
||||||
secrets1 = secrets1_1 + secrets1_2
|
|
||||||
rs1 = rs1_1 + rs1_2
|
|
||||||
secrets2, rs2, derivation_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, derivation_paths1 = await wallet3.generate_secrets_from_to(0, 4)
|
|
||||||
assert len(secrets1) == 5
|
|
||||||
secrets2, rs2, derivation_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)
|
|
||||||
invoice = await wallet3.request_mint(64)
|
|
||||||
await wallet3.mint(64, hash=invoice.hash)
|
|
||||||
assert wallet3.balance == 64
|
|
||||||
await reset_wallet_db(wallet3)
|
|
||||||
await wallet3.load_proofs()
|
|
||||||
wallet3.proofs = []
|
|
||||||
assert wallet3.balance == 0
|
|
||||||
await wallet3.restore_promises_from_to(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)
|
|
||||||
|
|
||||||
invoice = await wallet3.request_mint(64)
|
|
||||||
await wallet3.mint(64, hash=invoice.hash)
|
|
||||||
assert wallet3.balance == 64
|
|
||||||
|
|
||||||
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
|
||||||
|
|
||||||
await reset_wallet_db(wallet3)
|
|
||||||
await wallet3.load_proofs()
|
|
||||||
wallet3.proofs = []
|
|
||||||
assert wallet3.balance == 0
|
|
||||||
await wallet3.restore_promises_from_to(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)
|
|
||||||
invoice = await wallet3.request_mint(64)
|
|
||||||
await wallet3.mint(64, hash=invoice.hash)
|
|
||||||
assert wallet3.balance == 64
|
|
||||||
|
|
||||||
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
|
||||||
|
|
||||||
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_from_to(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)
|
|
||||||
|
|
||||||
invoice = await wallet3.request_mint(64)
|
|
||||||
await wallet3.mint(64, hash=invoice.hash)
|
|
||||||
assert wallet3.balance == 64
|
|
||||||
|
|
||||||
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
|
||||||
|
|
||||||
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_from_to(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)
|
|
||||||
|
|
||||||
invoice = await wallet3.request_mint(2)
|
|
||||||
await wallet3.mint(2, hash=invoice.hash)
|
|
||||||
box.add(wallet3.proofs)
|
|
||||||
assert wallet3.balance == 2
|
|
||||||
|
|
||||||
keep_proofs, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 1, set_reserved=True) # type: ignore
|
|
||||||
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_from_to(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(wallet3.proofs, 1, set_reserved=True) # type: ignore
|
|
||||||
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_from_to(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)
|
|
||||||
|
|
||||||
invoice = await wallet3.request_mint(64)
|
|
||||||
await wallet3.mint(64, hash=invoice.hash)
|
|
||||||
box.add(wallet3.proofs)
|
|
||||||
assert wallet3.balance == 64
|
|
||||||
|
|
||||||
keep_proofs, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 10, set_reserved=True) # type: ignore
|
|
||||||
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_from_to(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(wallet3.proofs, 12, set_reserved=True) # type: ignore
|
|
||||||
|
|
||||||
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_from_to(0, 50)
|
|
||||||
assert wallet3.balance == 182
|
|
||||||
await wallet3.invalidate(wallet3.proofs)
|
|
||||||
assert wallet3.balance == 64
|
|
||||||
|
|||||||
359
tests/test_wallet_restore.py
Normal file
359
tests/test_wallet_restore.py
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
from cashu.core.base import Proof
|
||||||
|
from cashu.core.crypto.secp import PrivateKey
|
||||||
|
from cashu.core.errors import CashuError
|
||||||
|
from cashu.wallet.wallet import Wallet
|
||||||
|
from cashu.wallet.wallet import Wallet as Wallet1
|
||||||
|
from cashu.wallet.wallet import Wallet as Wallet2
|
||||||
|
from tests.conftest import SERVER_ENDPOINT
|
||||||
|
|
||||||
|
|
||||||
|
async def assert_err(f, msg: Union[str, CashuError]):
|
||||||
|
"""Compute f() and expect an error message 'msg'."""
|
||||||
|
try:
|
||||||
|
await f
|
||||||
|
except Exception as exc:
|
||||||
|
error_message: str = str(exc.args[0])
|
||||||
|
if isinstance(msg, CashuError):
|
||||||
|
if msg.detail not in error_message:
|
||||||
|
raise Exception(
|
||||||
|
f"CashuError. Expected error: {msg.detail}, got: {error_message}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if msg not in error_message:
|
||||||
|
raise Exception(f"Expected error: {msg}, got: {error_message}")
|
||||||
|
return
|
||||||
|
raise Exception(f"Expected error: {msg}, got no error")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_amt(proofs: List[Proof], expected: int):
|
||||||
|
"""Assert amounts the proofs contain."""
|
||||||
|
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 = await Wallet1.with_db(
|
||||||
|
url=SERVER_ENDPOINT,
|
||||||
|
db="test_data/wallet1",
|
||||||
|
name="wallet1",
|
||||||
|
)
|
||||||
|
await wallet1.load_mint()
|
||||||
|
wallet1.status()
|
||||||
|
yield wallet1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def wallet2(mint):
|
||||||
|
wallet2 = await Wallet2.with_db(
|
||||||
|
url=SERVER_ENDPOINT,
|
||||||
|
db="test_data/wallet2",
|
||||||
|
name="wallet2",
|
||||||
|
)
|
||||||
|
await wallet2.load_mint()
|
||||||
|
wallet2.status()
|
||||||
|
yield wallet2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def wallet3(mint):
|
||||||
|
dirpath = Path("test_data/wallet3")
|
||||||
|
if dirpath.exists() and dirpath.is_dir():
|
||||||
|
shutil.rmtree(dirpath)
|
||||||
|
|
||||||
|
wallet3 = await Wallet1.with_db(
|
||||||
|
url=SERVER_ENDPOINT,
|
||||||
|
db="test_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_bump_secret_derivation(wallet3: Wallet):
|
||||||
|
await wallet3._init_private_key(
|
||||||
|
"half depart obvious quality work element tank gorilla view sugar picture"
|
||||||
|
" humble"
|
||||||
|
)
|
||||||
|
secrets1, rs1, derivation_paths1 = await wallet3.generate_n_secrets(5)
|
||||||
|
secrets2, rs2, derivation_paths2 = await wallet3.generate_secrets_from_to(0, 4)
|
||||||
|
assert wallet3.keyset_id == "1cCNIAZ2X/w1"
|
||||||
|
assert secrets1 == secrets2
|
||||||
|
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
||||||
|
assert derivation_paths1 == derivation_paths2
|
||||||
|
assert secrets1 == [
|
||||||
|
"9d32fc57e6fa2942d05ee475d28ba6a56839b8cb8a3f174b05ed0ed9d3a420f6",
|
||||||
|
"1c0f2c32e7438e7cc992612049e9dfcdbffd454ea460901f24cc429921437802",
|
||||||
|
"327c606b761af03cbe26fa13c4b34a6183b868c52cda059fe57fdddcb4e1e1e7",
|
||||||
|
"53476919560398b56c0fdc5dd92cf8628b1e06de6f2652b0f7d6e8ac319de3b7",
|
||||||
|
"b2f5d632229378a716be6752fc79ac8c2b43323b820859a7956f2dfe5432b7b4",
|
||||||
|
]
|
||||||
|
assert derivation_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, derivation_paths1 = await wallet3.generate_n_secrets(2)
|
||||||
|
secrets1_2, rs1_2, derivation_paths2 = await wallet3.generate_n_secrets(3)
|
||||||
|
secrets1 = secrets1_1 + secrets1_2
|
||||||
|
rs1 = rs1_1 + rs1_2
|
||||||
|
secrets2, rs2, derivation_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, derivation_paths1 = await wallet3.generate_secrets_from_to(0, 4)
|
||||||
|
assert len(secrets1) == 5
|
||||||
|
secrets2, rs2, derivation_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)
|
||||||
|
invoice = await wallet3.request_mint(64)
|
||||||
|
await wallet3.mint(64, hash=invoice.hash)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs()
|
||||||
|
wallet3.proofs = []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises_from_to(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)
|
||||||
|
|
||||||
|
invoice = await wallet3.request_mint(64)
|
||||||
|
await wallet3.mint(64, hash=invoice.hash)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
||||||
|
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs()
|
||||||
|
wallet3.proofs = []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises_from_to(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)
|
||||||
|
invoice = await wallet3.request_mint(64)
|
||||||
|
await wallet3.mint(64, hash=invoice.hash)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
||||||
|
|
||||||
|
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_from_to(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)
|
||||||
|
|
||||||
|
invoice = await wallet3.request_mint(64)
|
||||||
|
await wallet3.mint(64, hash=invoice.hash)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
_, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 32, set_reserved=True) # type: ignore
|
||||||
|
|
||||||
|
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_from_to(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)
|
||||||
|
|
||||||
|
invoice = await wallet3.request_mint(2)
|
||||||
|
await wallet3.mint(2, hash=invoice.hash)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 2
|
||||||
|
|
||||||
|
keep_proofs, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 1, set_reserved=True) # type: ignore
|
||||||
|
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_from_to(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(wallet3.proofs, 1, set_reserved=True) # type: ignore
|
||||||
|
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_from_to(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)
|
||||||
|
|
||||||
|
invoice = await wallet3.request_mint(64)
|
||||||
|
await wallet3.mint(64, hash=invoice.hash)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
keep_proofs, spendable_proofs = await wallet3.split_to_send(wallet3.proofs, 10, set_reserved=True) # type: ignore
|
||||||
|
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_from_to(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(wallet3.proofs, 12, set_reserved=True) # type: ignore
|
||||||
|
|
||||||
|
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_from_to(0, 50)
|
||||||
|
assert wallet3.balance == 182
|
||||||
|
await wallet3.invalidate(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 64
|
||||||
Reference in New Issue
Block a user