mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-24 03:54:21 +01:00
@@ -79,12 +79,8 @@ class Proof(BaseModel):
|
||||
secret: str = "" # secret or message to be blinded and signed
|
||||
C: str = "" # signature on secret, unblinded by wallet
|
||||
dleq: Union[DLEQWallet, None] = None # DLEQ proof
|
||||
|
||||
witness: Union[None, str] = "" # witness for spending condition
|
||||
# p2pksigs: Union[List[str], None] = [] # P2PK signature
|
||||
# p2shscript: Union[P2SHWitness, 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
|
||||
reserved: Union[None, bool] = False
|
||||
# unique ID of send attempt, used for grouping pending tokens in the wallet
|
||||
|
||||
@@ -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.")
|
||||
@@ -8,7 +8,6 @@ from .crypto.secp import PrivateKey
|
||||
|
||||
|
||||
class SecretKind:
|
||||
P2SH = "P2SH"
|
||||
P2PK = "P2PK"
|
||||
HTLC = "HTLC"
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ from ..core.p2pk import (
|
||||
SigFlags,
|
||||
verify_p2pk_signature,
|
||||
)
|
||||
from ..core.script import verify_bitcoin_script
|
||||
from ..core.secret import Secret, SecretKind
|
||||
|
||||
|
||||
@@ -23,11 +22,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}")
|
||||
@@ -35,35 +33,6 @@ class LedgerSpendingConditions:
|
||||
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:
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...core.base import Invoice, P2SHWitness
|
||||
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[P2SHWitness]
|
||||
locks: List[str]
|
||||
|
||||
|
||||
class InvoicesResponse(BaseModel):
|
||||
|
||||
@@ -13,7 +13,7 @@ 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
|
||||
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(
|
||||
|
||||
@@ -23,7 +23,6 @@ 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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import time
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from ..core.base import Invoice, P2SHWitness, 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: P2SHWitness,
|
||||
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[P2SHWitness]:
|
||||
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 [P2SHWitness(**r) for r in rows]
|
||||
|
||||
|
||||
async def update_p2sh_used(
|
||||
p2sh: P2SHWitness,
|
||||
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 = "",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import base64
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -8,7 +7,6 @@ from ..core import bolt11 as bolt11
|
||||
from ..core.base import (
|
||||
BlindedMessage,
|
||||
P2PKWitness,
|
||||
P2SHWitness,
|
||||
Proof,
|
||||
)
|
||||
from ..core.crypto.secp import PrivateKey
|
||||
@@ -18,41 +16,14 @@ from ..core.p2pk import (
|
||||
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 = P2SHWitness(
|
||||
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 (
|
||||
@@ -89,23 +60,6 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
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
|
||||
@@ -187,24 +141,6 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
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.witness = P2SHWitness(script=p2sh_script, signature=p2sh_signature).json()
|
||||
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}")
|
||||
@@ -221,14 +157,12 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
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:
|
||||
@@ -248,15 +182,8 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -707,7 +707,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
|
||||
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
|
||||
|
||||
96
poetry.lock
generated
96
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user