From c2885247507da1c00d8a58e20e06ac4210e084e5 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:14:00 +0200 Subject: [PATCH] remove p2sh --- cashu/core/base.py | 2 - cashu/core/p2pk.py | 11 --- cashu/core/script.py | 168 ---------------------------------- cashu/core/secret.py | 1 - cashu/mint/conditions.py | 33 +------ cashu/wallet/api/responses.py | 6 +- cashu/wallet/api/router.py | 12 +-- cashu/wallet/cli/cli.py | 38 ++------ cashu/wallet/crud.py | 67 +------------- cashu/wallet/helpers.py | 23 ++--- cashu/wallet/migrations.py | 20 ++-- cashu/wallet/p2pk.py | 79 +--------------- cashu/wallet/wallet.py | 3 +- poetry.lock | 96 +++++++++++++++---- pyproject.toml | 1 - requirements.txt | 1 - tests/test_wallet_p2sh.py | 88 ------------------ 17 files changed, 119 insertions(+), 530 deletions(-) delete mode 100644 cashu/core/script.py delete mode 100644 tests/test_wallet_p2sh.py diff --git a/cashu/core/base.py b/cashu/core/base.py index 5cef1499..b23fad64 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -9,7 +9,6 @@ from .crypto.keys import derive_keys, derive_keyset_id, derive_pubkeys from .crypto.secp import PrivateKey, PublicKey from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12 -from .p2pk import P2SHScript class DLEQ(BaseModel): @@ -47,7 +46,6 @@ class Proof(BaseModel): dleq: Union[DLEQWallet, None] = None # DLEQ proof p2pksigs: Union[List[str], None] = [] # P2PK signature - p2shscript: Union[P2SHScript, None] = None # P2SH spending condition htlcpreimage: Union[str, None] = None # HTLC unlocking preimage htlcsignature: Union[str, None] = None # HTLC unlocking signature # whether this proof is reserved for sending, used for coin management in the wallet diff --git a/cashu/core/p2pk.py b/cashu/core/p2pk.py index 1364649b..181c784e 100644 --- a/cashu/core/p2pk.py +++ b/cashu/core/p2pk.py @@ -3,7 +3,6 @@ from typing import List, Union from loguru import logger -from pydantic import BaseModel from .crypto.secp import PrivateKey, PublicKey from .secret import Secret, SecretKind @@ -64,16 +63,6 @@ def n_sigs(self) -> Union[None, int]: return int(n_sigs) if n_sigs else None -class P2SHScript(BaseModel): - """ - Unlocks P2SH spending condition of a Proof - """ - - script: str - signature: str - address: Union[str, None] = None - - def sign_p2pk_sign(message: bytes, private_key: PrivateKey): # ecdsa version # signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message)) diff --git a/cashu/core/script.py b/cashu/core/script.py deleted file mode 100644 index 3fc682f8..00000000 --- a/cashu/core/script.py +++ /dev/null @@ -1,168 +0,0 @@ -import base64 -import hashlib -import random - -from bitcoin.core import CMutableTxIn, CMutableTxOut, COutPoint, CTransaction, lx -from bitcoin.core.script import OP_CHECKSIG, SIGHASH_ALL, CScript, SignatureHash -from bitcoin.core.scripteval import ( - SCRIPT_VERIFY_P2SH, - EvalScriptError, - VerifyScript, - VerifyScriptError, -) -from bitcoin.wallet import CBitcoinSecret, P2SHBitcoinAddress - -COIN = 100_000_000 -TXID = "bff785da9f8169f49be92fa95e31f0890c385bfb1bd24d6b94d7900057c617ae" -SEED = b"__not__used" - - -def step0_carol_privkey(): - """Private key""" - # h = hashlib.sha256(SEED).digest() - h = hashlib.sha256(str(random.getrandbits(256)).encode()).digest() - seckey = CBitcoinSecret.from_secret_bytes(h) - return seckey - - -def step0_carol_checksig_redeemscript(carol_pubkey): - """Create script""" - txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG]) # type: ignore - # txin_redeemScript = CScript([-123, OP_CHECKLOCKTIMEVERIFY]) - # txin_redeemScript = CScript([3, 3, OP_LESSTHAN, OP_VERIFY]) - return txin_redeemScript - - -def step1_carol_create_p2sh_address(txin_redeemScript): - """Create address (serialized scriptPubKey) to share with Alice""" - txin_p2sh_address = P2SHBitcoinAddress.from_redeemScript(txin_redeemScript) - return txin_p2sh_address - - -def step1_bob_carol_create_tx(txin_p2sh_address): - """Create transaction""" - txid = lx(TXID) - vout = 0 - txin = CMutableTxIn(COutPoint(txid, vout)) - txout = CMutableTxOut( - int(0.0005 * COIN), - P2SHBitcoinAddress(str(txin_p2sh_address)).to_scriptPubKey(), - ) - tx = CTransaction([txin], [txout]) - return tx, txin - - -def step2_carol_sign_tx(txin_redeemScript, privatekey): - """Sign transaction with private key""" - txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) - tx, txin = step1_bob_carol_create_tx(txin_p2sh_address) - sighash = SignatureHash(txin_redeemScript, tx, 0, SIGHASH_ALL) - sig = privatekey.sign(sighash) + bytes([SIGHASH_ALL]) - txin.scriptSig = CScript([sig, txin_redeemScript]) # type: ignore - return txin - - -def step3_bob_verify_script(txin_signature, txin_redeemScript, tx): - txin_scriptPubKey = txin_redeemScript.to_p2sh_scriptPubKey() - try: - VerifyScript( - txin_signature, txin_scriptPubKey, tx, 0, flags=[SCRIPT_VERIFY_P2SH] - ) - return True - except VerifyScriptError as e: - raise Exception("Script verification failed:", e) - except EvalScriptError as e: - print(f"Script: {txin_scriptPubKey.__repr__()}") - raise Exception("Script evaluation failed:", e) - except Exception as e: - raise Exception("Script execution failed:", e) - - -def verify_bitcoin_script(txin_redeemScript_b64, txin_signature_b64): - txin_redeemScript = CScript(base64.urlsafe_b64decode(txin_redeemScript_b64)) - # print("Redeem script:", txin_redeemScript.__repr__()) - # txin_redeemScript = CScript([2, 3, OP_LESSTHAN, OP_VERIFY]) - txin_signature = CScript(value=base64.urlsafe_b64decode(txin_signature_b64)) - - txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) - # print(f"Bob recreates secret: P2SH:{txin_p2sh_address}") - # MINT checks that P2SH:txin_p2sh_address has not been spent yet - # ... - tx, _ = step1_bob_carol_create_tx(txin_p2sh_address) - - # print( - # f"Bob verifies:\nscript: {txin_redeemScript_b64}\nsignature: {txin_signature_b64}\n" - # ) - script_valid = step3_bob_verify_script(txin_signature, txin_redeemScript, tx) - # MINT redeems tokens and stores P2SH:txin_p2sh_address - # ... - # if script_valid: - # print("Successfull.") - # else: - # print("Error.") - return txin_p2sh_address, script_valid - - -# simple test case -if __name__ == "__main__": - # https://github.com/romanz/python-bitcointx/blob/master/examples/spend-p2sh-txout.py - # CAROL shares txin_p2sh_address with ALICE: - - # --------- - # CAROL defines scripthash and ALICE mints them - alice_privkey = step0_carol_privkey() - txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub) - print("Script:", txin_redeemScript.__repr__()) - txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) - print(f"Carol sends Alice secret = P2SH:{txin_p2sh_address}") - print("") - - # --------- - - # ALICE: mint tokens with secret P2SH:txin_p2sh_address - print(f"Alice mints tokens with secret = P2SH:{txin_p2sh_address}") - print("") - # ... - - # --------- - # CAROL redeems with MINT - - # CAROL PRODUCES txin_redeemScript and txin_signature to send to MINT - txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub) - txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig - - txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() - txin_signature_b64 = base64.urlsafe_b64encode(txin_signature).decode() - print( - f"Carol to Bob:\nscript: {txin_redeemScript.__repr__()}\nscript:" - f" {txin_redeemScript_b64}\nsignature: {txin_signature_b64}\n" - ) - print("") - # --------- - # MINT verifies SCRIPT and SIGNATURE and mints tokens - - # MINT receives txin_redeemScript_b64 and txin_signature_b64 fom CAROL: - txin_redeemScript = CScript(base64.urlsafe_b64decode(txin_redeemScript_b64)) - txin_redeemScript_p2sh = txin_p2sh_address.to_redeemScript() - print("Redeem script:", txin_redeemScript.__repr__()) - print("P2SH:", txin_redeemScript_p2sh.__repr__()) - # txin_redeemScript = CScript([2, 3, OP_LESSTHAN, OP_VERIFY]) - txin_signature = CScript(value=base64.urlsafe_b64decode(txin_signature_b64)) - - txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) - print(f"Bob recreates secret: P2SH:{txin_p2sh_address}") - # MINT checks that P2SH:txin_p2sh_address has not been spent yet - # ... - tx, _ = step1_bob_carol_create_tx(txin_p2sh_address) - - print( - f"Bob verifies:\nscript: {txin_redeemScript_b64}\nsignature:" - f" {txin_signature_b64}\n" - ) - script_valid = step3_bob_verify_script(txin_signature, txin_redeemScript, tx) - # MINT redeems tokens and stores P2SH:txin_p2sh_address - # ... - if script_valid: - print("Successfull.") - else: - print("Error.") diff --git a/cashu/core/secret.py b/cashu/core/secret.py index 783cb72d..070b09e0 100644 --- a/cashu/core/secret.py +++ b/cashu/core/secret.py @@ -8,7 +8,6 @@ class SecretKind: - P2SH = "P2SH" P2PK = "P2PK" HTLC = "HTLC" diff --git a/cashu/mint/conditions.py b/cashu/mint/conditions.py index 98843950..f8a13856 100644 --- a/cashu/mint/conditions.py +++ b/cashu/mint/conditions.py @@ -18,7 +18,6 @@ SigFlags, verify_p2pk_signature, ) -from ..core.script import verify_bitcoin_script from ..core.secret import Secret, SecretKind @@ -26,11 +25,10 @@ class LedgerSpendingConditions: def _verify_input_spending_conditions(self, proof: Proof) -> bool: """ Verify spending conditions: - Condition: P2SH - Witnesses proof.p2shscript Condition: P2PK - Witness: proof.p2pksigs Condition: HTLC - Witness: proof.htlcpreimage, proof.htlcsignature """ - # P2SH + try: secret = Secret.deserialize(proof.secret) logger.trace(f"proof.secret: {proof.secret}") @@ -38,35 +36,6 @@ def _verify_input_spending_conditions(self, proof: Proof) -> bool: except Exception: # secret is not a spending condition so we treat is a normal secret return True - if secret.kind == SecretKind.P2SH: - p2pk_secret = P2PKSecret.from_secret(secret) - # check if locktime is in the past - now = time.time() - if p2pk_secret.locktime and p2pk_secret.locktime < now: - logger.trace(f"p2sh locktime ran out ({p2pk_secret.locktime}<{now}).") - return True - logger.trace(f"p2sh locktime still active ({p2pk_secret.locktime}>{now}).") - - if ( - proof.p2shscript is None - or proof.p2shscript.script is None - or proof.p2shscript.signature is None - ): - # no script present although secret indicates one - raise TransactionError("no script in proof.") - - # execute and verify P2SH - txin_p2sh_address, valid = verify_bitcoin_script( - proof.p2shscript.script, proof.p2shscript.signature - ) - if not valid: - raise TransactionError("script invalid.") - # check if secret commits to script address - assert secret.data == str(txin_p2sh_address), ( - f"secret does not contain correct P2SH address: {secret.data} is not" - f" {txin_p2sh_address}." - ) - return True # P2PK if secret.kind == SecretKind.P2PK: diff --git a/cashu/wallet/api/responses.py b/cashu/wallet/api/responses.py index 625a78af..b77d198d 100644 --- a/cashu/wallet/api/responses.py +++ b/cashu/wallet/api/responses.py @@ -2,7 +2,7 @@ from pydantic import BaseModel -from ...core.base import Invoice, P2SHScript +from ...core.base import Invoice class PayResponse(BaseModel): @@ -50,11 +50,11 @@ class PendingResponse(BaseModel): class LockResponse(BaseModel): - P2SH: Optional[str] + P2PK: Optional[str] class LocksResponse(BaseModel): - locks: List[P2SHScript] + locks: List[str] class InvoicesResponse(BaseModel): diff --git a/cashu/wallet/api/router.py b/cashu/wallet/api/router.py index 1f02ef0f..48116abd 100644 --- a/cashu/wallet/api/router.py +++ b/cashu/wallet/api/router.py @@ -13,7 +13,7 @@ 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 from ...wallet.helpers import ( deserialize_token_from_string, init_wallet, @@ -213,7 +213,7 @@ async def balance(): async def send_command( amount: int = Query(default=..., description="Amount to send"), nostr: str = Query(default=None, description="Send to nostr pubkey"), - lock: str = Query(default=None, description="Lock tokens (P2SH)"), + lock: str = Query(default=None, description="Lock tokens (P2PK)"), mint: str = Query( default=None, description="Mint URL to send from (None for default mint)", @@ -354,14 +354,14 @@ async def pending( @router.get("/lock", name="Generate receiving lock", response_model=LockResponse) async def lock(): - address = await wallet.create_p2sh_address_and_store() - return LockResponse(P2SH=address) + pubkey = await wallet.create_p2pk_pubkey() + return LockResponse(P2PK=pubkey) @router.get("/locks", name="Show unused receiving locks", response_model=LocksResponse) async def locks(): - locks = await get_unused_locks(db=wallet.db) - return LocksResponse(locks=locks) + pubkey = await wallet.create_p2pk_pubkey() + return LocksResponse(locks=[pubkey]) @router.get( diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 56e5e01f..f43ba870 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -23,7 +23,6 @@ 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 @@ -351,7 +350,7 @@ async def balance(ctx: Context, verbose): help="Send to nostr pubkey.", type=str, ) -@click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", type=str) +@click.option("--lock", "-l", default=None, help="Lock tokens (P2PK).", type=str) @click.option( "--dleq", "-d", @@ -583,26 +582,14 @@ async def pending(ctx: Context, legacy, number: int, offset: int): @cli.command("lock", help="Generate receiving lock.") -@click.option( - "--p2sh", - "-p", - default=False, - is_flag=True, - help="Create P2SH lock.", - type=bool, -) @click.pass_context @coro -async def lock(ctx, p2sh): +async def lock(ctx): wallet: Wallet = ctx.obj["WALLET"] - if p2sh: - address = await wallet.create_p2sh_address_and_store() - lock_str = f"P2SH:{address}" - print("---- Pay to script hash (P2SH) ----\n") - else: - pubkey = await wallet.create_p2pk_pubkey() - lock_str = f"P2PK:{pubkey}" - print("---- Pay to public key (P2PK) ----\n") + + pubkey = await wallet.create_p2pk_pubkey() + lock_str = f"P2PK:{pubkey}" + print("---- Pay to public key (P2PK) ----\n") print("Use a lock to receive tokens that only you can unlock.") print("") @@ -626,19 +613,6 @@ async def locks(ctx): 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): - print("") - print("---- Pay to script hash (P2SH) locks ----\n") - for lock in locks: - print(f"Lock: P2SH:{lock.address}") - print(f"Script: {lock.script}") - print(f"Signature: {lock.signature}") - print("") - print("--------------------------\n") - return True diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index fcdfab1a..e37a9bc8 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -2,7 +2,7 @@ import time from typing import Any, List, Optional, Tuple -from ..core.base import Invoice, P2SHScript, Proof, WalletKeyset +from ..core.base import Invoice, Proof, WalletKeyset from ..core.db import Connection, Database @@ -122,71 +122,6 @@ async def secret_used( return rows is not None -async def store_p2sh( - p2sh: P2SHScript, - db: Database, - conn: Optional[Connection] = None, -) -> None: - await (conn or db).execute( - """ - INSERT INTO p2sh - (address, script, signature, used) - VALUES (?, ?, ?, ?) - """, - ( - p2sh.address, - p2sh.script, - p2sh.signature, - False, - ), - ) - - -async def get_unused_locks( - address: str = "", - db: Optional[Database] = None, - conn: Optional[Connection] = None, -) -> List[P2SHScript]: - clause: List[str] = [] - args: List[str] = [] - - clause.append("used = 0") - - if address: - clause.append("address = ?") - args.append(address) - - where = "" - if clause: - where = f"WHERE {' AND '.join(clause)}" - - rows = await (conn or db).fetchall( # type: ignore - f""" - SELECT * from p2sh - {where} - """, - tuple(args), - ) - return [P2SHScript(**r) for r in rows] - - -async def update_p2sh_used( - p2sh: P2SHScript, - used: bool, - db: Optional[Database] = None, - conn: Optional[Connection] = None, -) -> None: - clauses = [] - values = [] - clauses.append("used = ?") - values.append(used) - - await (conn or db).execute( # type: ignore - f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?", - (*values, str(p2sh.address)), - ) - - async def store_keyset( keyset: WalletKeyset, mint_url: str = "", diff --git a/cashu/wallet/helpers.py b/cashu/wallet/helpers.py index 418a6b75..c62797a0 100644 --- a/cashu/wallet/helpers.py +++ b/cashu/wallet/helpers.py @@ -176,25 +176,20 @@ async def send( assert len(lock) > 21, Exception( "Error: lock has to be at least 22 characters long." ) - if not lock.startswith("P2SH:") and not lock.startswith("P2PK:"): - raise Exception("Error: lock has to start with P2SH: or P2PK:") + if not lock.startswith("P2PK:"): + raise Exception("Error: lock has to start with P2PK:") # we add a time lock to the P2PK lock by appending the current unix time + 14 days - if lock.startswith("P2PK:") or lock.startswith("P2SH:"): + else: logger.debug(f"Locking token to: {lock}") logger.debug( f"Adding a time lock of {settings.locktime_delta_seconds} seconds." ) - if lock.startswith("P2SH:"): - secret_lock = await wallet.create_p2sh_lock( - lock.split(":")[1], locktime=settings.locktime_delta_seconds - ) - elif lock.startswith("P2PK:"): - secret_lock = await wallet.create_p2pk_lock( - lock.split(":")[1], - locktime_seconds=settings.locktime_delta_seconds, - sig_all=True, - n_sigs=1, - ) + secret_lock = await wallet.create_p2pk_lock( + lock.split(":")[1], + locktime_seconds=settings.locktime_delta_seconds, + sig_all=True, + n_sigs=1, + ) await wallet.load_proofs() if split: diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 94ce47fa..a9e86d33 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -73,19 +73,19 @@ async def m003_add_proofs_sendid_and_timestamps(db: Database): async def m004_p2sh_locks(db: Database): """ - Stores P2SH addresses and unlock scripts. + DEPRECATED: Stores P2SH addresses and unlock scripts. """ - await db.execute(""" - CREATE TABLE IF NOT EXISTS p2sh ( - address TEXT NOT NULL, - script TEXT NOT NULL, - signature TEXT NOT NULL, - used BOOL NOT NULL, + # await db.execute(""" + # CREATE TABLE IF NOT EXISTS p2sh ( + # address TEXT NOT NULL, + # script TEXT NOT NULL, + # signature TEXT NOT NULL, + # used BOOL NOT NULL, - UNIQUE (address, script, signature) + # UNIQUE (address, script, signature) - ); - """) + # ); + # """) async def m005_wallet_keysets(db: Database): diff --git a/cashu/wallet/p2pk.py b/cashu/wallet/p2pk.py index 79d929a6..7d4a77a8 100644 --- a/cashu/wallet/p2pk.py +++ b/cashu/wallet/p2pk.py @@ -1,4 +1,3 @@ -import base64 from datetime import datetime, timedelta from typing import List, Optional @@ -13,45 +12,17 @@ from ..core.db import Database from ..core.p2pk import ( P2PKSecret, - P2SHScript, SigFlags, sign_p2pk_sign, ) -from ..core.script import ( - step0_carol_checksig_redeemscript, - step0_carol_privkey, - step1_carol_create_p2sh_address, - step2_carol_sign_tx, -) from ..core.secret import Secret, SecretKind, Tags -from ..wallet.crud import ( - get_unused_locks, - store_p2sh, -) from .protocols import SupportsDb, SupportsPrivateKey class WalletP2PK(SupportsPrivateKey, SupportsDb): db: Database private_key: Optional[PrivateKey] = None - # ---------- P2SH and P2PK ---------- - - async def create_p2sh_address_and_store(self) -> str: - """Creates a P2SH lock script and stores the script and signature in the database.""" - alice_privkey = step0_carol_privkey() - txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub) - txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) - txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig - txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode() - txin_signature_b64 = base64.urlsafe_b64encode(txin_signature).decode() - p2shScript = P2SHScript( - script=txin_redeemScript_b64, - signature=txin_signature_b64, - address=str(txin_p2sh_address), - ) - await store_p2sh(p2shScript, db=self.db) - assert p2shScript.address - return p2shScript.address + # ---------- P2PK ---------- async def create_p2pk_pubkey(self): assert ( @@ -88,23 +59,6 @@ async def create_p2pk_lock( tags=tags, ) - async def create_p2sh_lock( - self, - address: str, - locktime: Optional[int] = None, - tags: Tags = Tags(), - ) -> Secret: - if locktime: - tags["locktime"] = str( - (datetime.now() + timedelta(seconds=locktime)).timestamp() - ) - - return Secret( - kind=SecretKind.P2SH, - data=address, - tags=tags, - ) - async def sign_p2pk_proofs(self, proofs: List[Proof]) -> List[str]: assert ( self.private_key @@ -186,24 +140,6 @@ async def add_witnesses_to_outputs( outputs = await self.add_p2pk_witnesses_to_outputs(outputs) return outputs - async def add_p2sh_witnesses_to_proofs( - self: SupportsDb, proofs: List[Proof] - ) -> List[Proof]: - # Quirk: we use a single P2SH script and signature pair for all tokens in proofs - address = Secret.deserialize(proofs[0].secret).data - p2shscripts = await get_unused_locks(address, db=self.db) - assert len(p2shscripts) == 1, Exception("lock not found.") - p2sh_script, p2sh_signature = ( - p2shscripts[0].script, - p2shscripts[0].signature, - ) - logger.debug(f"Unlock script: {p2sh_script} signature: {p2sh_signature}") - - # attach unlock scripts to proofs - for p in proofs: - p.p2shscript = P2SHScript(script=p2sh_script, signature=p2sh_signature) - return proofs - async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: p2pk_signatures = await self.sign_p2pk_proofs(proofs) logger.debug(f"Unlock signatures for {len(proofs)} proofs: {p2pk_signatures}") @@ -218,14 +154,12 @@ async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof] return proofs async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: - """Adds witnesses to proofs for P2SH or P2PK redemption. + """Adds witnesses to proofs for P2PK redemption. This method parses the secret of each proof and determines the correct witness type and adds it to the proof if we have it available. Note: In order for this method to work, all proofs must have the same secret type. - This is because we use a single P2SH script and signature pair for all tokens in proofs. - For P2PK, we use an individual signature for each token in proofs. Args: @@ -245,15 +179,8 @@ async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]: # if not, we do not add witnesses (treat as regular token secret) return proofs logger.debug("Spending conditions detected.") - # P2SH scripts - if all([Secret.deserialize(p.secret).kind == SecretKind.P2SH for p in proofs]): - logger.debug("P2SH redemption detected.") - proofs = await self.add_p2sh_witnesses_to_proofs(proofs) - # P2PK signatures - elif all( - [Secret.deserialize(p.secret).kind == SecretKind.P2PK for p in proofs] - ): + if all([Secret.deserialize(p.secret).kind == SecretKind.P2PK for p in proofs]): logger.debug("P2PK redemption detected.") proofs = await self.add_p2pk_witnesses_to_proofs(proofs) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index c761e507..e6538400 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -400,7 +400,6 @@ def _splitrequest_include_fields(proofs: List[Proof]): "amount", "secret", "C", - "p2shscript", "p2pksigs", "htlcpreimage", "htlcsignature", @@ -710,7 +709,7 @@ async def split( if secret_lock is None: secrets, rs, derivation_paths = await self.generate_n_secrets(len(amounts)) else: - # NOTE: we use random blinding factors for P2SH, we won't be able to + # NOTE: we use random blinding factors for locks, we won't be able to # restore these tokens from a backup rs = [] # generate secrets for receiver diff --git a/poetry.lock b/poetry.lock index 33e4df0c..b9472a23 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "anyio" version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -25,6 +26,7 @@ trio = ["trio (<0.22)"] name = "asn1crypto" version = "1.5.1" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" +category = "main" optional = false python-versions = "*" files = [ @@ -36,6 +38,7 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -54,6 +57,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "base58" version = "2.1.1" description = "Base58 and Base58Check implementation." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -68,6 +72,7 @@ tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", " name = "bech32" version = "1.2.0" description = "Reference implementation for Bech32 and segwit addresses." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -79,6 +84,7 @@ files = [ name = "bip32" version = "3.4" description = "Minimalistic implementation of the BIP32 key derivation scheme" +category = "main" optional = false python-versions = "*" files = [ @@ -94,6 +100,7 @@ coincurve = ">=15.0,<19" name = "bitstring" version = "3.1.9" description = "Simple construction, analysis and modification of binary data." +category = "main" optional = false python-versions = "*" files = [ @@ -106,6 +113,7 @@ files = [ name = "black" version = "23.7.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -152,6 +160,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -163,6 +172,7 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" files = [ @@ -239,6 +249,7 @@ pycparser = "*" name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -250,6 +261,7 @@ files = [ name = "charset-normalizer" version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -334,6 +346,7 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -348,6 +361,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "coincurve" version = "18.0.0" description = "Cross-platform Python CFFI bindings for libsecp256k1" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -403,6 +417,7 @@ cffi = ">=1.3.0" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -414,6 +429,7 @@ files = [ name = "coverage" version = "7.3.0" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -481,6 +497,7 @@ toml = ["tomli"] name = "cryptography" version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -526,6 +543,7 @@ test-randomorder = ["pytest-randomly"] name = "distlib" version = "0.3.7" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -537,6 +555,7 @@ files = [ name = "ecdsa" version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -555,6 +574,7 @@ gmpy2 = ["gmpy2"] name = "environs" version = "9.5.0" description = "simplified environment variable parsing" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -576,6 +596,7 @@ tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -590,6 +611,7 @@ test = ["pytest (>=6)"] name = "fastapi" version = "0.101.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -609,6 +631,7 @@ all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)" name = "filelock" version = "3.12.2" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -624,6 +647,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -635,6 +659,7 @@ files = [ name = "httpcore" version = "0.17.3" description = "A minimal low-level HTTP client." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -646,16 +671,17 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = "==1.*" +sniffio = ">=1.0.0,<2.0.0" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" version = "0.24.1" description = "The next generation HTTP client." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -671,14 +697,15 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" version = "2.5.27" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -693,6 +720,7 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -704,6 +732,7 @@ files = [ name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -723,6 +752,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -734,6 +764,7 @@ files = [ name = "loguru" version = "0.7.0" description = "Python logging made (stupidly) simple" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -752,6 +783,7 @@ dev = ["Sphinx (==5.3.0)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegu name = "marshmallow" version = "3.20.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -772,6 +804,7 @@ tests = ["pytest", "pytz", "simplejson"] name = "mnemonic" version = "0.20" description = "Implementation of Bitcoin BIP-0039" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -783,6 +816,7 @@ files = [ name = "mypy" version = "1.5.1" description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -829,6 +863,7 @@ reports = ["lxml"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -840,6 +875,7 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -854,6 +890,7 @@ setuptools = "*" name = "outcome" version = "1.2.0" description = "Capture the outcome of Python function calls." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -868,6 +905,7 @@ attrs = ">=19.2.0" name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -879,6 +917,7 @@ files = [ name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -890,6 +929,7 @@ files = [ name = "platformdirs" version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -905,6 +945,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -920,6 +961,7 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "3.3.3" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -938,6 +980,7 @@ virtualenv = ">=20.10.0" name = "psycopg2-binary" version = "2.9.7" description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1007,6 +1050,7 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1018,6 +1062,7 @@ files = [ name = "pycryptodomex" version = "3.18.0" description = "Cryptographic library for Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1059,6 +1104,7 @@ files = [ name = "pydantic" version = "1.10.12" description = "Data validation and settings management using python type hints" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1111,6 +1157,7 @@ email = ["email-validator (>=1.0.3)"] name = "pysocks" version = "1.7.1" description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1123,6 +1170,7 @@ files = [ name = "pytest" version = "7.4.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1145,6 +1193,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1163,6 +1212,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1177,21 +1227,11 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] -[[package]] -name = "python-bitcoinlib" -version = "0.12.2" -description = "The Swiss Army Knife of the Bitcoin protocol." -optional = false -python-versions = "*" -files = [ - {file = "python-bitcoinlib-0.12.2.tar.gz", hash = "sha256:c65ab61427c77c38d397bfc431f71d86fd355b453a536496ec3fcb41bd10087d"}, - {file = "python_bitcoinlib-0.12.2-py3-none-any.whl", hash = "sha256:2f29a9f475f21c12169b3a6cc8820f34f11362d7ff1200a5703dce3e4e903a44"}, -] - [[package]] name = "python-dotenv" version = "1.0.0" description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1206,6 +1246,7 @@ cli = ["click (>=5.0)"] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1255,6 +1296,7 @@ files = [ name = "represent" version = "1.6.0.post0" description = "Create __repr__ automatically or declaratively." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1272,6 +1314,7 @@ test = ["ipython", "mock", "pytest (>=3.0.5)"] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1293,6 +1336,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "ruff" version = "0.0.284" description = "An extremely fast Python linter, written in Rust." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1319,6 +1363,7 @@ files = [ name = "secp256k1" version = "0.14.0" description = "FFI bindings to libsecp256k1" +category = "main" optional = false python-versions = "*" files = [ @@ -1354,6 +1399,7 @@ cffi = ">=1.3.0" name = "setuptools" version = "68.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1370,6 +1416,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1381,6 +1428,7 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1392,6 +1440,7 @@ files = [ name = "sqlalchemy" version = "1.3.24" description = "Database Abstraction Library" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1447,6 +1496,7 @@ pymysql = ["pymysql", "pymysql (<1)"] name = "sqlalchemy-aio" version = "0.17.0" description = "Async support for SQLAlchemy." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1468,6 +1518,7 @@ trio = ["trio (>=0.15)"] name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1486,6 +1537,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1497,6 +1549,7 @@ files = [ name = "types-requests" version = "2.31.0.2" description = "Typing stubs for requests" +category = "dev" optional = false python-versions = "*" files = [ @@ -1511,6 +1564,7 @@ types-urllib3 = "*" name = "types-urllib3" version = "1.26.25.14" description = "Typing stubs for urllib3" +category = "dev" optional = false python-versions = "*" files = [ @@ -1522,6 +1576,7 @@ files = [ name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1533,6 +1588,7 @@ files = [ name = "urllib3" version = "2.0.4" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1550,6 +1606,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "uvicorn" version = "0.18.3" description = "The lightning-fast ASGI server." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1568,6 +1625,7 @@ standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", name = "virtualenv" version = "20.24.3" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1588,6 +1646,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "websocket-client" version = "1.6.2" description = "WebSocket client for Python with low level API options" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1604,6 +1663,7 @@ test = ["websockets"] name = "wheel" version = "0.41.2" description = "A built-package format for Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1618,6 +1678,7 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1632,6 +1693,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] name = "zipp" version = "3.16.2" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1649,4 +1711,4 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "b801dad77ac4c6b9aca8f25c4fe4d8bab9cb998a2e12d91d9bbf69cb66bb3085" +content-hash = "c8a62986bb458c849aabdd3e2f1e1534e4af6093863177fc27630fc9daa5410a" diff --git a/pyproject.toml b/pyproject.toml index 7a619ba1..f442501c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ ecdsa = "^0.18.0" bitstring = "^3.1.9" secp256k1 = "^0.14.0" sqlalchemy-aio = "^0.17.0" -python-bitcoinlib = "^0.12.2" h11 = "^0.14.0" PySocks = "^1.7.1" cryptography = "^41.0.3" diff --git a/requirements.txt b/requirements.txt index d13ce215..704a7c0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,6 @@ pycparser==2.21 ; python_full_version >= "3.8.1" and python_full_version < "4.0. pycryptodomex==3.18.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" pydantic==1.10.12 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" pysocks==1.7.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" -python-bitcoinlib==0.12.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" python-dotenv==1.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" represent==1.6.0.post0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" requests==2.31.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" diff --git a/tests/test_wallet_p2sh.py b/tests/test_wallet_p2sh.py deleted file mode 100644 index 0ae72185..00000000 --- a/tests/test_wallet_p2sh.py +++ /dev/null @@ -1,88 +0,0 @@ -import secrets -from typing import List - -import pytest -import pytest_asyncio - -from cashu.core.base import Proof -from cashu.core.crypto.secp import PrivateKey, PublicKey -from cashu.core.helpers import sum_proofs -from cashu.core.migrations import migrate_databases -from cashu.wallet import migrations -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): - """Compute f() and expect an error message 'msg'.""" - try: - await f - except Exception as exc: - if str(exc.args[0]) != msg: - raise Exception(f"Expected error: {msg}, got: {exc.args[0]}") - 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 - - -@pytest_asyncio.fixture(scope="function") -async def wallet1(mint): - wallet1 = await Wallet1.with_db( - SERVER_ENDPOINT, "test_data/wallet_p2sh_1", "wallet1" - ) - await migrate_databases(wallet1.db, migrations) - await wallet1.load_mint() - wallet1.status() - yield wallet1 - - -@pytest_asyncio.fixture(scope="function") -async def wallet2(mint): - wallet2 = await Wallet2.with_db( - SERVER_ENDPOINT, "test_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() - wallet2.status() - yield wallet2 - - -@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