diff --git a/cashu/core/base.py b/cashu/core/base.py
index a78f7ee..00c9a0e 100644
--- a/cashu/core/base.py
+++ b/cashu/core/base.py
@@ -4,10 +4,26 @@ from typing import List
from pydantic import BaseModel
+class P2SHScript(BaseModel):
+ script: str
+ signature: str
+ address: str = None
+
+ @classmethod
+ def from_row(cls, row: Row):
+ return cls(
+ address=row[0],
+ script=row[1],
+ signature=row[2],
+ used=row[3],
+ )
+
+
class Proof(BaseModel):
amount: int
- secret: str
+ secret: str = ""
C: str
+ script: P2SHScript = None
reserved: bool = False # whether this proof is reserved for sending
send_id: str = "" # unique ID of send attempt
time_created: str = ""
@@ -27,12 +43,11 @@ class Proof(BaseModel):
@classmethod
def from_dict(cls, d: dict):
- assert "secret" in d, "no secret in proof"
assert "amount" in d, "no amount in proof"
return cls(
amount=d.get("amount"),
C=d.get("C"),
- secret=d.get("secret"),
+ secret=d.get("secret") or "",
reserved=d.get("reserved") or False,
send_id=d.get("send_id") or "",
time_created=d.get("time_created") or "",
@@ -42,6 +57,9 @@ class Proof(BaseModel):
def to_dict(self):
return dict(amount=self.amount, secret=self.secret, C=self.C)
+ def to_dict_no_secret(self):
+ return dict(amount=self.amount, C=self.C)
+
def __getitem__(self, key):
return self.__getattribute__(key)
diff --git a/cashu/core/db.py b/cashu/core/db.py
index e393aa1..5b05af6 100644
--- a/cashu/core/db.py
+++ b/cashu/core/db.py
@@ -83,45 +83,45 @@ class Database(Compat):
self.name = db_name
self.db_location = db_location
self.db_location_is_url = "://" in self.db_location
-
if self.db_location_is_url:
- database_uri = self.db_location
+ raise Exception("Remote databases not supported. Use SQLite.")
+ # database_uri = self.db_location
- if database_uri.startswith("cockroachdb://"):
- self.type = COCKROACH
- else:
- self.type = POSTGRES
+ # if database_uri.startswith("cockroachdb://"):
+ # self.type = COCKROACH
+ # else:
+ # self.type = POSTGRES
- import psycopg2 # type: ignore
+ # import psycopg2 # type: ignore
- def _parse_timestamp(value, _):
- f = "%Y-%m-%d %H:%M:%S.%f"
- if not "." in value:
- f = "%Y-%m-%d %H:%M:%S"
- return time.mktime(datetime.datetime.strptime(value, f).timetuple())
+ # def _parse_timestamp(value, _):
+ # f = "%Y-%m-%d %H:%M:%S.%f"
+ # if not "." in value:
+ # f = "%Y-%m-%d %H:%M:%S"
+ # return time.mktime(datetime.datetime.strptime(value, f).timetuple())
- psycopg2.extensions.register_type(
- psycopg2.extensions.new_type(
- psycopg2.extensions.DECIMAL.values,
- "DEC2FLOAT",
- lambda value, curs: float(value) if value is not None else None,
- )
- )
- psycopg2.extensions.register_type(
- psycopg2.extensions.new_type(
- (1082, 1083, 1266),
- "DATE2INT",
- lambda value, curs: time.mktime(value.timetuple())
- if value is not None
- else None,
- )
- )
+ # psycopg2.extensions.register_type(
+ # psycopg2.extensions.new_type(
+ # psycopg2.extensions.DECIMAL.values,
+ # "DEC2FLOAT",
+ # lambda value, curs: float(value) if value is not None else None,
+ # )
+ # )
+ # psycopg2.extensions.register_type(
+ # psycopg2.extensions.new_type(
+ # (1082, 1083, 1266),
+ # "DATE2INT",
+ # lambda value, curs: time.mktime(value.timetuple())
+ # if value is not None
+ # else None,
+ # )
+ # )
- psycopg2.extensions.register_type(
- psycopg2.extensions.new_type(
- (1184, 1114), "TIMESTAMP2INT", _parse_timestamp
- )
- )
+ # psycopg2.extensions.register_type(
+ # psycopg2.extensions.new_type(
+ # (1184, 1114), "TIMESTAMP2INT", _parse_timestamp
+ # )
+ # )
else:
if not os.path.exists(self.db_location):
print(f"Creating database directory: {self.db_location}")
diff --git a/cashu/core/helpers.py b/cashu/core/helpers.py
index 0f8c885..680e773 100644
--- a/cashu/core/helpers.py
+++ b/cashu/core/helpers.py
@@ -1,7 +1,8 @@
import asyncio
from functools import partial, wraps
-from cashu.core.settings import LIGHTNING_FEE_PERCENT, LIGHTNING_RESERVE_FEE_MIN
+from cashu.core.settings import (LIGHTNING_FEE_PERCENT,
+ LIGHTNING_RESERVE_FEE_MIN)
def async_wrap(func):
diff --git a/cashu/core/script.py b/cashu/core/script.py
new file mode 100644
index 0000000..ef81a7a
--- /dev/null
+++ b/cashu/core/script.py
@@ -0,0 +1,166 @@
+import base64
+import hashlib
+import random
+
+COIN = 100_000_000
+TXID = "bff785da9f8169f49be92fa95e31f0890c385bfb1bd24d6b94d7900057c617ae"
+SEED = b"__not__used"
+
+from bitcoin.core import CMutableTxIn, CMutableTxOut, COutPoint, CTransaction, lx
+from bitcoin.core.script import *
+from bitcoin.core.scripteval import (
+ SCRIPT_VERIFY_P2SH,
+ EvalScriptError,
+ VerifyScript,
+ VerifyScriptError,
+)
+from bitcoin.wallet import CBitcoinSecret, P2SHBitcoinAddress
+
+
+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_redeemscrip(carol_pubkey):
+ """Create script"""
+ txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG])
+ # 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])
+ 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_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_redeemscrip(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_redeemscrip(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: {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: {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/settings.py b/cashu/core/settings.py
index 36ec220..41003ce 100644
--- a/cashu/core/settings.py
+++ b/cashu/core/settings.py
@@ -1,12 +1,25 @@
+import os
+import sys
from pathlib import Path
-
from environs import Env # type: ignore
+from loguru import logger
env = Env()
-env.read_env()
+
+ENV_FILE = os.path.join(str(Path.home()), ".cashu", ".env")
+if not os.path.isfile(ENV_FILE):
+ ENV_FILE = os.path.join(os.getcwd(), ".env")
+if os.path.isfile(ENV_FILE):
+ env.read_env(ENV_FILE)
+else:
+ ENV_FILE = None
+ env.read_env()
DEBUG = env.bool("DEBUG", default=False)
-CASHU_DIR = env.str("CASHU_DIR", default="~/.cashu")
+if not DEBUG:
+ sys.tracebacklimit = 0
+
+CASHU_DIR = env.str("CASHU_DIR", default=os.path.join(str(Path.home()), ".cashu"))
CASHU_DIR = CASHU_DIR.replace("~", str(Path.home()))
assert len(CASHU_DIR), "CASHU_DIR not defined"
@@ -32,4 +45,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
MAX_ORDER = 64
-VERSION = "0.1.10"
+VERSION = "0.2.0"
diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py
index f174f2d..5a5a6de 100644
--- a/cashu/lightning/lnbits.py
+++ b/cashu/lightning/lnbits.py
@@ -8,13 +8,8 @@ import requests
from cashu.core.settings import LNBITS_ENDPOINT, LNBITS_KEY
-from .base import (
- InvoiceResponse,
- PaymentResponse,
- PaymentStatus,
- StatusResponse,
- Wallet,
-)
+from .base import (InvoiceResponse, PaymentResponse, PaymentStatus,
+ StatusResponse, Wallet)
class LNbitsWallet(Wallet):
diff --git a/cashu/mint/app.py b/cashu/mint/app.py
index 8b722fd..f0a6b73 100644
--- a/cashu/mint/app.py
+++ b/cashu/mint/app.py
@@ -5,7 +5,7 @@ import sys
from fastapi import FastAPI
from loguru import logger
-from cashu.core.settings import VERSION, DEBUG
+from cashu.core.settings import DEBUG, VERSION
from cashu.lightning import WALLET
from cashu.mint.migrations import m001_initial
@@ -40,7 +40,7 @@ def create_app(config_object="core.settings") -> FastAPI:
logger.log(level, record.getMessage())
logger.remove()
- log_level: str = "INFO"
+ log_level: str = "DEBUG" if DEBUG else "INFO"
formatter = Formatter()
logger.add(sys.stderr, level=log_level, format=formatter.format)
diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py
index 18a7195..a608967 100644
--- a/cashu/mint/crud.py
+++ b/cashu/mint/crud.py
@@ -93,7 +93,7 @@ async def get_lightning_invoice(
SELECT * from invoices
WHERE hash = ?
""",
- hash,
+ (hash,),
)
return Invoice.from_row(row)
@@ -106,5 +106,8 @@ async def update_lightning_invoice(
):
await (conn or db).execute(
"UPDATE invoices SET issued = ? WHERE hash = ?",
- (issued, hash),
+ (
+ issued,
+ hash,
+ ),
)
diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py
index 13f6310..522e54d 100644
--- a/cashu/mint/ledger.py
+++ b/cashu/mint/ledger.py
@@ -3,12 +3,15 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c
"""
import hashlib
+from inspect import signature
+from signal import signal
from typing import List, Set
import cashu.core.b_dhke as b_dhke
from cashu.core.base import BlindedMessage, BlindedSignature, Invoice, Proof
from cashu.core.db import Database
from cashu.core.helpers import fee_reserve
+from cashu.core.script import verify_script
from cashu.core.secp import PrivateKey, PublicKey
from cashu.core.settings import LIGHTNING, MAX_ORDER
from cashu.core.split import amount_split
@@ -27,9 +30,9 @@ class Ledger:
def __init__(self, secret_key: str, db: str):
self.proofs_used: Set[str] = set()
- self.master_key: str = secret_key
- self.keys: List[PrivateKey] = self._derive_keys(self.master_key)
- self.pub_keys: List[PublicKey] = self._derive_pubkeys(self.keys)
+ self.master_key = secret_key
+ self.keys = self._derive_keys(self.master_key)
+ self.pub_keys = self._derive_pubkeys(self.keys)
self.db: Database = Database("mint", db)
async def load_used_proofs(self):
@@ -73,6 +76,16 @@ class Ledger:
"""Checks whether the proof was already spent."""
return not proof.secret in self.proofs_used
+ def _verify_secret_or_script(self, proof: Proof):
+ if proof.secret and proof.script:
+ raise Exception("secret and script present at the same time.")
+ return True
+
+ def _verify_secret_criteria(self, proof: Proof):
+ if proof.secret is None or proof.secret == "":
+ raise Exception("no secret in proof.")
+ return True
+
def _verify_proof(self, proof: Proof):
"""Verifies that the proof of promise was issued by this ledger."""
if not self._check_spendable(proof):
@@ -81,6 +94,36 @@ class Ledger:
C = PublicKey(bytes.fromhex(proof.C), raw=True)
return b_dhke.verify(secret_key, C, proof.secret)
+ def _verify_script(self, idx: int, proof: Proof):
+ """
+ Verify bitcoin script in proof.script commited to by
in proof.secret.
+ proof.secret format: P2SH::
+ """
+ # if no script is given
+ if (
+ proof.script is None
+ or proof.script.script is None
+ or proof.script.signature is None
+ ):
+ if len(proof.secret.split("P2SH:")) == 2:
+ # secret indicates a script but no script is present
+ return False
+ else:
+ # secret indicates no script, so treat script as valid
+ return True
+ # execute and verify P2SH
+ txin_p2sh_address, valid = verify_script(
+ proof.script.script, proof.script.signature
+ )
+ if valid:
+ # check if secret commits to script address
+ # format: P2SH::
+ assert len(proof.secret.split(":")) == 3, "secret format wrong."
+ assert proof.secret.split(":")[1] == str(
+ txin_p2sh_address
+ ), f"secret does not contain correct P2SH address: {proof.secret.split(':')[1]}!={txin_p2sh_address}."
+ return valid
+
def _verify_outputs(
self, total: int, amount: int, output_data: List[BlindedMessage]
):
@@ -150,7 +193,7 @@ class Ledger:
"""Checks with the Lightning backend whether an invoice with this payment_hash was paid."""
invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
if invoice.issued:
- raise Exception("tokens already issued for this invoice")
+ raise Exception("tokens already issued for this invoice.")
status = await WALLET.get_invoice_status(payment_hash)
if status.paid:
await update_lightning_invoice(payment_hash, issued=True, db=self.db)
@@ -227,32 +270,43 @@ class Ledger:
return {i: self._check_spendable(p) for i, p in enumerate(proofs)}
async def split(
- self, proofs: List[Proof], amount: int, output_data: List[BlindedMessage]
+ self, proofs: List[Proof], amount: int, outputs: List[BlindedMessage]
):
"""Consumes proofs and prepares new promises based on the amount split."""
- self._verify_split_amount(amount)
- # Verify proofs are valid
- if not all([self._verify_proof(p) for p in proofs]):
- return False
-
total = sum([p.amount for p in proofs])
- if not self._verify_no_duplicates(proofs, output_data):
- raise Exception("duplicate proofs or promises")
+ # verify that amount is kosher
+ self._verify_split_amount(amount)
+ # verify overspending attempt
if amount > total:
- raise Exception("split amount is higher than the total sum")
- if not self._verify_outputs(total, amount, output_data):
- raise Exception("split of promises is not as expected")
+ raise Exception("split amount is higher than the total sum.")
+
+ # Verify scripts
+ if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]):
+ raise Exception("script verification failed.")
+ # Verify secret criteria
+ if not all([self._verify_secret_criteria(p) for p in proofs]):
+ raise Exception("secrets do not match criteria.")
+ # verify that only unique proofs and outputs were used
+ if not self._verify_no_duplicates(proofs, outputs):
+ raise Exception("duplicate proofs or promises.")
+ # verify that outputs have the correct amount
+ if not self._verify_outputs(total, amount, outputs):
+ raise Exception("split of promises is not as expected.")
+ # Verify proofs
+ if not all([self._verify_proof(p) for p in proofs]):
+ raise Exception("could not verify proofs.")
# Mark proofs as used and prepare new promises
await self._invalidate_proofs(proofs)
outs_fst = amount_split(total - amount)
outs_snd = amount_split(amount)
- B_fst = [od.B_ for od in output_data[: len(outs_fst)]]
- B_snd = [od.B_ for od in output_data[len(outs_fst) :]]
+ B_fst = [od.B_ for od in outputs[: len(outs_fst)]]
+ B_snd = [od.B_ for od in outputs[len(outs_fst) :]]
prom_fst, prom_snd = await self._generate_promises(
outs_fst, B_fst
), await self._generate_promises(outs_snd, B_snd)
+ # verify amounts in produced proofs
self._verify_equation_balanced(proofs, prom_fst + prom_snd)
return prom_fst, prom_snd
diff --git a/cashu/mint/router.py b/cashu/mint/router.py
index b7ebf05..be656df 100644
--- a/cashu/mint/router.py
+++ b/cashu/mint/router.py
@@ -3,7 +3,8 @@ from typing import Union
from fastapi import APIRouter
from secp256k1 import PublicKey
-from cashu.core.base import CheckPayload, MeltPayload, MintPayloads, SplitPayload
+from cashu.core.base import (CheckPayload, MeltPayload, MintPayloads,
+ SplitPayload)
from cashu.mint import ledger
router: APIRouter = APIRouter()
@@ -82,7 +83,6 @@ async def split(payload: SplitPayload):
except Exception as exc:
return {"error": str(exc)}
if not split_return:
- """There was a problem with the split"""
- raise Exception("could not split tokens.")
+ return {"error": "there was a problem with the split."}
fst_promises, snd_promises = split_return
return {"fst": fst_promises, "snd": snd_promises}
diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py
index a2c4653..1207091 100644
--- a/cashu/mint/startup.py
+++ b/cashu/mint/startup.py
@@ -2,7 +2,7 @@ import asyncio
from loguru import logger
-from cashu.core.settings import CASHU_DIR
+from cashu.core.settings import CASHU_DIR, LIGHTNING
from cashu.lightning import WALLET
from cashu.mint.migrations import m001_initial
@@ -13,13 +13,14 @@ async def load_ledger():
await asyncio.wait([m001_initial(ledger.db)])
await ledger.load_used_proofs()
- error_message, balance = await WALLET.status()
- if error_message:
- logger.warning(
- f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
- RuntimeWarning,
- )
+ if LIGHTNING:
+ error_message, balance = await WALLET.status()
+ if error_message:
+ logger.warning(
+ f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
+ RuntimeWarning,
+ )
+ logger.info(f"Lightning balance: {balance} sat")
- logger.info(f"Lightning balance: {balance} sat")
logger.info(f"Data dir: {CASHU_DIR}")
logger.info("Mint started.")
diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py
index 09a3f4a..ebc0edf 100755
--- a/cashu/wallet/cli.py
+++ b/cashu/wallet/cli.py
@@ -4,22 +4,24 @@ import asyncio
import base64
import json
import math
+import os
+import sys
from datetime import datetime
from functools import wraps
from itertools import groupby
from operator import itemgetter
import click
-from bech32 import bech32_decode, bech32_encode, convertbits
+from loguru import logger
import cashu.core.bolt11 as bolt11
from cashu.core.base import Proof
from cashu.core.bolt11 import Invoice
from cashu.core.helpers import fee_reserve
from cashu.core.migrations import migrate_databases
-from cashu.core.settings import CASHU_DIR, DEBUG, LIGHTNING, MINT_URL, VERSION
+from cashu.core.settings import CASHU_DIR, DEBUG, LIGHTNING, MINT_URL, VERSION, ENV_FILE
from cashu.wallet import migrations
-from cashu.wallet.crud import get_reserved_proofs
+from cashu.wallet.crud import get_reserved_proofs, get_unused_locks
from cashu.wallet.wallet import Wallet as Wallet
@@ -37,14 +39,19 @@ class NaturalOrderGroup(click.Group):
@click.group(cls=NaturalOrderGroup)
-@click.option("--host", "-h", default=MINT_URL, help="Mint address.")
-@click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet to use.")
+@click.option("--host", "-h", default=MINT_URL, help="Mint URL.")
+@click.option("--wallet", "-w", "walletname", default="wallet", help="Wallet name.")
@click.pass_context
def cli(ctx, host: str, walletname: str):
+ # configure logger
+ logger.remove()
+ logger.add(sys.stderr, level="DEBUG" if DEBUG else "INFO")
ctx.ensure_object(dict)
ctx.obj["HOST"] = host
ctx.obj["WALLET_NAME"] = walletname
- ctx.obj["WALLET"] = Wallet(ctx.obj["HOST"], f"{CASHU_DIR}/{walletname}", walletname)
+ wallet = Wallet(ctx.obj["HOST"], os.path.join(CASHU_DIR, walletname))
+ ctx.obj["WALLET"] = wallet
+ asyncio.run(init_wallet(wallet))
pass
@@ -57,14 +64,14 @@ def coro(f):
return wrapper
-@cli.command("mint", help="Mint tokens.")
+@cli.command("mint", help="Mint.")
@click.argument("amount", type=int)
@click.option("--hash", default="", help="Hash of the paid invoice.", type=str)
@click.pass_context
@coro
async def mint(ctx, amount: int, hash: str):
wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
+ wallet.load_mint()
wallet.status()
if not LIGHTNING:
r = await wallet.mint(amount)
@@ -83,57 +90,79 @@ async def mint(ctx, amount: int, hash: str):
return
-@cli.command("balance", help="See balance.")
+@cli.command("balance", help="Balance.")
@click.pass_context
@coro
async def balance(ctx):
wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
wallet.status()
-@cli.command("send", help="Send tokens.")
+@cli.command("send", help="Send coins.")
@click.argument("amount", type=int)
+@click.option("--lock", "-l", default=None, help="Lock coins (P2SH).", type=str)
@click.pass_context
@coro
-async def send(ctx, amount: int):
+async def send(ctx, amount: int, lock: str):
+ if lock and len(lock) < 22:
+ print("Error: lock has to be at least 22 characters long.")
+ return
+ p2sh = False
+ if lock and len(lock.split("P2SH:")) == 2:
+ p2sh = True
wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
+ wallet.load_mint()
wallet.status()
- _, send_proofs = await wallet.split_to_send(wallet.proofs, amount)
+ _, send_proofs = await wallet.split_to_send(wallet.proofs, amount, lock)
await wallet.set_reserved(send_proofs, reserved=True)
- token = await wallet.serialize_proofs(send_proofs)
- print(token)
+ coin = await wallet.serialize_proofs(
+ send_proofs, hide_secrets=True if lock and not p2sh else False
+ )
+ print(coin)
wallet.status()
-@cli.command("receive", help="Receive tokens.")
-@click.argument("token", type=str)
+@cli.command("receive", help="Receive coins.")
+@click.argument("coin", type=str)
+@click.option("--lock", "-l", default=None, help="Unlock coins.", type=str)
@click.pass_context
@coro
-async def receive(ctx, token: str):
+async def receive(ctx, coin: str, lock: str):
wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
+ wallet.load_mint()
wallet.status()
- proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))]
- _, _ = await wallet.redeem(proofs)
+ if lock:
+ # load the script and signature of this address from the database
+ assert len(lock.split("P2SH:")) == 2, Exception(
+ "lock has wrong format. Expected P2SH:."
+ )
+ address_split = lock.split("P2SH:")[1]
+
+ p2shscripts = await get_unused_locks(address_split, db=wallet.db)
+ assert len(p2shscripts) == 1
+ script = p2shscripts[0].script
+ signature = p2shscripts[0].signature
+ else:
+ script, signature = None, None
+ proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(coin))]
+ _, _ = await wallet.redeem(proofs, snd_script=script, snd_siganture=signature)
wallet.status()
-@cli.command("burn", help="Burn spent tokens.")
-@click.argument("token", required=False, type=str)
-@click.option("--all", "-a", default=False, is_flag=True, help="Burn all spent tokens.")
+@cli.command("burn", help="Burn spent coins.")
+@click.argument("coin", required=False, type=str)
+@click.option("--all", "-a", default=False, is_flag=True, help="Burn all spent coins.")
@click.option(
- "--force", "-f", default=False, is_flag=True, help="Force check on all tokens."
+ "--force", "-f", default=False, is_flag=True, help="Force check on all coins."
)
@click.pass_context
@coro
-async def burn(ctx, token: str, all: bool, force: bool):
+async def burn(ctx, coin: str, all: bool, force: bool):
wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
- if not (all or token or force) or (token and all):
+ wallet.load_mint()
+ if not (all or coin or force) or (coin and all):
print(
- "Error: enter a token or use --all to burn all pending tokens or --force to check all tokens."
+ "Error: enter a coin or use --all to burn all pending coins or --force to check all coins."
)
return
if all:
@@ -145,43 +174,49 @@ async def burn(ctx, token: str, all: bool, force: bool):
else:
# check only the specified ones
proofs = [
- Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))
+ Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(coin))
]
wallet.status()
await wallet.invalidate(proofs)
wallet.status()
-@cli.command("pending", help="Show pending tokens.")
+@cli.command("pending", help="Show pending coins.")
@click.pass_context
@coro
async def pending(ctx):
wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
+ wallet.load_mint()
reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs):
+ print(f"--------------------------\n")
sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id"))
- for key, value in groupby(sorted_proofs, key=itemgetter("send_id")):
+ for i, (key, value) in enumerate(
+ groupby(sorted_proofs, key=itemgetter("send_id"))
+ ):
grouped_proofs = list(value)
- token = await wallet.serialize_proofs(grouped_proofs)
+ coin = await wallet.serialize_proofs(grouped_proofs)
+ coin_hidden_secret = await wallet.serialize_proofs(
+ grouped_proofs, hide_secrets=True
+ )
reserved_date = datetime.utcfromtimestamp(
int(grouped_proofs[0].time_reserved)
).strftime("%Y-%m-%d %H:%M:%S")
print(
- f"Amount: {sum([p['amount'] for p in grouped_proofs])} sat Sent: {reserved_date} ID: {key}\n"
+ f"#{i} Amount: {sum([p['amount'] for p in grouped_proofs])} sat Time: {reserved_date} ID: {key}\n"
)
- print(token)
- print("")
+ print(f"With secret: {coin}\n\nSecretless: {coin_hidden_secret}\n")
+ print(f"--------------------------\n")
wallet.status()
-@cli.command("pay", help="Pay lightning invoice.")
+@cli.command("pay", help="Pay Lightning invoice.")
@click.argument("invoice", type=str)
@click.pass_context
@coro
async def pay(ctx, invoice: str):
wallet: Wallet = ctx.obj["WALLET"]
- await init_wallet(wallet)
+ wallet.load_mint()
wallet.status()
decoded_invoice: Invoice = bolt11.decode(invoice)
amount = math.ceil(
@@ -199,6 +234,47 @@ async def pay(ctx, invoice: str):
wallet.status()
+@cli.command("lock", help="Generate receiving lock.")
+@click.pass_context
+@coro
+async def lock(ctx):
+ wallet: Wallet = ctx.obj["WALLET"]
+ p2shscript = await wallet.create_p2sh_lock()
+ txin_p2sh_address = p2shscript.address
+ print("---- Pay to script hash (P2SH) ----\n")
+ print("Use a lock to receive coins that only you can unlock.")
+ print("")
+ print(f"Public receiving lock: P2SH:{txin_p2sh_address}")
+ print("")
+ print(
+ f"Anyone can send coins to this lock:\n\ncashu send --lock P2SH:{txin_p2sh_address}"
+ )
+ print("")
+ print(
+ f"Only you can receive coins from this lock:\n\ncashu receive --lock P2SH:{txin_p2sh_address}\n"
+ )
+
+
+@cli.command("locks", help="Show unused receiving locks.")
+@click.pass_context
+@coro
+async def locks(ctx):
+ wallet: Wallet = ctx.obj["WALLET"]
+ locks = await get_unused_locks(db=wallet.db)
+ if len(locks):
+ print("")
+ print(f"--------------------------\n")
+ for l in locks:
+ print(f"Address: {l.address}")
+ print(f"Script: {l.script}")
+ print(f"Signature: {l.signature}")
+ print("")
+ print(f"Receive: cashu receive --lock P2SH:{l.address}")
+ print("")
+ print(f"--------------------------\n")
+ return True
+
+
@cli.command("info", help="Information about Cashu wallet.")
@click.pass_context
@coro
@@ -206,5 +282,8 @@ async def info(ctx):
print(f"Version: {VERSION}")
print(f"Debug: {DEBUG}")
print(f"Cashu dir: {CASHU_DIR}")
+ if ENV_FILE:
+ print(f"Settings: {ENV_FILE}")
+ print(f"Wallet: {ctx.obj['WALLET_NAME']}")
print(f"Mint URL: {MINT_URL}")
return
diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py
index c5741b6..bacb968 100644
--- a/cashu/wallet/crud.py
+++ b/cashu/wallet/crud.py
@@ -1,7 +1,7 @@
import time
-from typing import Optional
+from typing import Optional, List, Any
-from cashu.core.base import Proof
+from cashu.core.base import Proof, P2SHScript
from cashu.core.db import Connection, Database
@@ -59,7 +59,7 @@ async def invalidate_proof(
DELETE FROM proofs
WHERE secret = ?
""",
- str(proof["secret"]),
+ (str(proof["secret"]),),
)
await (conn or db).execute(
@@ -80,7 +80,7 @@ async def update_proof_reserved(
conn: Optional[Connection] = None,
):
clauses = []
- values = []
+ values: List[Any] = []
clauses.append("reserved = ?")
values.append(reserved)
@@ -97,3 +97,86 @@ async def update_proof_reserved(
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
(*values, str(proof.secret)),
)
+
+
+async def secret_used(
+ secret: str,
+ db: Database,
+ conn: Optional[Connection] = None,
+):
+
+ rows = await (conn or db).fetchone(
+ """
+ SELECT * from proofs
+ WHERE secret = ?
+ """,
+ (secret,),
+ )
+ return rows is not None
+
+
+async def store_p2sh(
+ p2sh: P2SHScript,
+ db: Database,
+ conn: Optional[Connection] = 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 = None,
+ db: Database = None,
+ conn: Optional[Connection] = None,
+):
+
+ 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(
+ f"""
+ SELECT * from p2sh
+ {where}
+ """,
+ tuple(args),
+ )
+ return [P2SHScript.from_row(r) for r in rows]
+
+
+async def update_p2sh_used(
+ p2sh: P2SHScript,
+ used: bool,
+ db: Database = None,
+ conn: Optional[Connection] = None,
+):
+ clauses = []
+ values = []
+ clauses.append("used = ?")
+ values.append(used)
+
+ await (conn or db).execute(
+ f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?",
+ (*values, str(p2sh.address)),
+ )
diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py
index c8b3ec0..68b4b64 100644
--- a/cashu/wallet/migrations.py
+++ b/cashu/wallet/migrations.py
@@ -79,3 +79,22 @@ async def m003_add_proofs_sendid_and_timestamps(db):
await db.execute("ALTER TABLE proofs ADD COLUMN time_created TIMESTAMP")
await db.execute("ALTER TABLE proofs ADD COLUMN time_reserved TIMESTAMP")
await db.execute("ALTER TABLE proofs_used ADD COLUMN time_used TIMESTAMP")
+
+
+async def m004_p2sh_locks(db: Database):
+ """
+ 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,
+
+ UNIQUE (address, script, signature)
+
+ );
+ """
+ )
diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py
index 27d1ba6..b941869 100644
--- a/cashu/wallet/wallet.py
+++ b/cashu/wallet/wallet.py
@@ -5,6 +5,7 @@ import uuid
from typing import List
import requests
+from loguru import logger
import cashu.core.b_dhke as b_dhke
from cashu.core.base import (
@@ -13,9 +14,16 @@ from cashu.core.base import (
CheckPayload,
MeltPayload,
MintPayloads,
+ P2SHScript,
Proof,
SplitPayload,
)
+from cashu.core.script import (
+ step0_carol_privkey,
+ step0_carol_checksig_redeemscrip,
+ step1_carol_create_p2sh_address,
+ step2_carol_sign_tx,
+)
from cashu.core.db import Database
from cashu.core.secp import PublicKey
from cashu.core.settings import DEBUG
@@ -23,15 +31,16 @@ from cashu.core.split import amount_split
from cashu.wallet.crud import (
get_proofs,
invalidate_proof,
+ secret_used,
store_proof,
update_proof_reserved,
+ store_p2sh,
)
class LedgerAPI:
def __init__(self, url):
self.url = url
- self.keys = self._get_keys(url)
@staticmethod
def _get_keys(url):
@@ -51,79 +60,144 @@ class LedgerAPI:
rv.append(2**pos)
return rv
- def _construct_proofs(self, promises: List[BlindedSignature], secrets: List[str]):
- """Returns proofs of promise from promises."""
+ def _construct_proofs(
+ self, promises: List[BlindedSignature], secrets: List[str], rs: List[str]
+ ):
+ """Returns proofs of promise from promises. Wants secrets and blinding factors rs."""
proofs = []
- for promise, (r, secret) in zip(promises, secrets):
+ for promise, secret, r in zip(promises, secrets, rs):
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
C = b_dhke.step3_alice(C_, r, self.keys[promise.amount])
proof = Proof(amount=promise.amount, C=C.serialize().hex(), secret=secret)
proofs.append(proof)
return proofs
- def _generate_secret(self, randombits=128):
+ @staticmethod
+ def _generate_secret(randombits=128):
"""Returns base64 encoded random string."""
return scrts.token_urlsafe(randombits // 8)
+ def _load_mint(self):
+ assert len(
+ self.url
+ ), "Ledger not initialized correctly: mint URL not specified yet. "
+ self.keys = self._get_keys(self.url)
+ assert len(self.keys) > 0, "did not receive keys from mint."
+
def request_mint(self, amount):
"""Requests a mint from the server and returns Lightning invoice."""
r = requests.get(self.url + "/mint", params={"amount": amount})
return r.json()
- def mint(self, amounts, payment_hash=None):
- """Mints new coins and returns a proof of promise."""
+ @staticmethod
+ def _construct_outputs(amounts: List[int], secrets: List[str]):
+ """Takes a list of amounts and secrets and returns outputs.
+ Outputs are blinded messages `payloads` and blinding factors `rs`"""
+ assert len(amounts) == len(
+ secrets
+ ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}"
payloads: MintPayloads = MintPayloads()
- secrets = []
rs = []
- for amount in amounts:
- secret = self._generate_secret()
- secrets.append(secret)
+ for secret, amount in zip(secrets, amounts):
B_, r = b_dhke.step1_alice(secret)
rs.append(r)
payload: BlindedMessage = BlindedMessage(
amount=amount, B_=B_.serialize().hex()
)
payloads.blinded_messages.append(payload)
- promises_list = requests.post(
+ return payloads, rs
+
+ async def _check_used_secrets(self, secrets):
+ for s in secrets:
+ if await secret_used(s, db=self.db):
+ raise Exception(f"secret already used: {s}")
+
+ def generate_secrets(self, secret, n):
+ """`secret` is the base string that will be tweaked n times"""
+ if len(secret.split("P2SH:")) == 2:
+ return [f"{secret}:{self._generate_secret()}" for i in range(n)]
+ return [f"{i}:{secret}" for i in range(n)]
+
+ async def mint(self, amounts, payment_hash=None):
+ """Mints new coins and returns a proof of promise."""
+ secrets = [self._generate_secret() for s in range(len(amounts))]
+ await self._check_used_secrets(secrets)
+ payloads, rs = self._construct_outputs(amounts, secrets)
+
+ resp = requests.post(
self.url + "/mint",
json=payloads.dict(),
params={"payment_hash": payment_hash},
- ).json()
+ )
+ try:
+ promises_list = resp.json()
+ except:
+ if resp.status_code >= 300:
+ raise Exception(f"Error: {f'mint returned {resp.status_code}'}")
+ else:
+ raise Exception("Unkown mint error.")
if "error" in promises_list:
raise Exception("Error: {}".format(promises_list["error"]))
- promises = [BlindedSignature.from_dict(p) for p in promises_list]
- return self._construct_proofs(promises, [(r, s) for r, s in zip(rs, secrets)])
- def split(self, proofs, amount):
- """Consume proofs and create new promises based on amount split."""
+ promises = [BlindedSignature.from_dict(p) for p in promises_list]
+ return self._construct_proofs(promises, secrets, rs)
+
+ async def split(self, proofs, amount, snd_secret: str = None):
+ """Consume proofs and create new promises based on amount split.
+ If snd_secret is None, random secrets will be generated for the tokens to keep (fst_outputs)
+ and the promises to send (snd_outputs).
+
+ If snd_secret is provided, the wallet will create blinded secrets with those to attach a
+ predefined spending condition to the tokens they want to send."""
+
total = sum([p["amount"] for p in proofs])
fst_amt, snd_amt = total - amount, amount
fst_outputs = amount_split(fst_amt)
snd_outputs = amount_split(snd_amt)
- # TODO: Refactor together with the same procedure in self.mint()
- secrets = []
- payloads: MintPayloads = MintPayloads()
- for output_amt in fst_outputs + snd_outputs:
- secret = self._generate_secret()
- B_, r = b_dhke.step1_alice(secret)
- secrets.append((r, secret))
- payload: BlindedMessage = BlindedMessage(
- amount=output_amt, B_=B_.serialize().hex()
- )
- payloads.blinded_messages.append(payload)
+ amounts = fst_outputs + snd_outputs
+ if snd_secret is None:
+ secrets = [self._generate_secret() for _ in range(len(amounts))]
+ else:
+ snd_secrets = self.generate_secrets(snd_secret, len(snd_outputs))
+ logger.debug(f"Creating proofs with custom secrets: {snd_secrets}")
+ assert len(snd_secrets) == len(
+ snd_outputs
+ ), "number of snd_secrets does not match number of ouptus."
+ # append predefined secrets (to send) to random secrets (to keep)
+ secrets = [
+ self._generate_secret() for s in range(len(fst_outputs))
+ ] + snd_secrets
+
+ assert len(secrets) == len(
+ amounts
+ ), "number of secrets does not match number of outputs"
+ await self._check_used_secrets(secrets)
+ payloads, rs = self._construct_outputs(amounts, secrets)
split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads)
- promises_dict = requests.post(
+ resp = requests.post(
self.url + "/split",
json=split_payload.dict(),
- ).json()
+ )
+
+ try:
+ promises_dict = resp.json()
+ except:
+ if resp.status_code >= 300:
+ raise Exception(f"Error: {f'mint returned {resp.status_code}'}")
+ else:
+ raise Exception("Unkown mint error.")
if "error" in promises_dict:
- raise Exception("Error: {}".format(promises_dict["error"]))
+ raise Exception("Mint Error: {}".format(promises_dict["error"]))
promises_fst = [BlindedSignature.from_dict(p) for p in promises_dict["fst"]]
promises_snd = [BlindedSignature.from_dict(p) for p in promises_dict["snd"]]
- # Obtain proofs from promises
- fst_proofs = self._construct_proofs(promises_fst, secrets[: len(promises_fst)])
- snd_proofs = self._construct_proofs(promises_snd, secrets[len(promises_fst) :])
+ # Construct proofs from promises (i.e., unblind signatures)
+ fst_proofs = self._construct_proofs(
+ promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)]
+ )
+ snd_proofs = self._construct_proofs(
+ promises_snd, secrets[len(promises_fst) :], rs[len(promises_fst) :]
+ )
return fst_proofs, snd_proofs
@@ -154,6 +228,9 @@ class Wallet(LedgerAPI):
self.proofs: List[Proof] = []
self.name = name
+ def load_mint(self):
+ super()._load_mint()
+
async def load_proofs(self):
self.proofs = await get_proofs(db=self.db)
@@ -166,19 +243,34 @@ class Wallet(LedgerAPI):
async def mint(self, amount: int, payment_hash: str = None):
split = amount_split(amount)
- proofs = super().mint(split, payment_hash)
+ proofs = await super().mint(split, payment_hash)
if proofs == []:
raise Exception("received no proofs.")
await self._store_proofs(proofs)
self.proofs += proofs
return proofs
- async def redeem(self, proofs: List[Proof]):
+ async def redeem(
+ self,
+ proofs: List[Proof],
+ snd_script: str = None,
+ snd_siganture: str = None,
+ ):
+ if snd_script and snd_siganture:
+ logger.debug(f"Unlock script: {snd_script}")
+ # attach unlock scripts to proofs
+ for p in proofs:
+ p.script = P2SHScript(script=snd_script, signature=snd_siganture)
return await self.split(proofs, sum(p["amount"] for p in proofs))
- async def split(self, proofs: List[Proof], amount: int):
+ async def split(
+ self,
+ proofs: List[Proof],
+ amount: int,
+ snd_secret: str = None,
+ ):
assert len(proofs) > 0, ValueError("no proofs provided.")
- fst_proofs, snd_proofs = super().split(proofs, amount)
+ fst_proofs, snd_proofs = await super().split(proofs, amount, snd_secret)
if len(fst_proofs) == 0 and len(snd_proofs) == 0:
raise Exception("received no splits.")
used_secrets = [p["secret"] for p in proofs]
@@ -201,18 +293,25 @@ class Wallet(LedgerAPI):
return status["paid"]
@staticmethod
- async def serialize_proofs(proofs: List[Proof]):
- proofs_serialized = [p.to_dict() for p in proofs]
+ async def serialize_proofs(proofs: List[Proof], hide_secrets=False):
+ if hide_secrets:
+ proofs_serialized = [p.to_dict_no_secret() for p in proofs]
+ else:
+ proofs_serialized = [p.to_dict() for p in proofs]
token = base64.urlsafe_b64encode(
json.dumps(proofs_serialized).encode()
).decode()
return token
- async def split_to_send(self, proofs: List[Proof], amount):
+ async def split_to_send(self, proofs: List[Proof], amount, snd_secret: str = None):
"""Like self.split but only considers non-reserved tokens."""
+ if snd_secret:
+ logger.debug(f"Spending conditions: {snd_secret}")
if len([p for p in proofs if not p.reserved]) <= 0:
raise Exception("balance too low.")
- return await self.split([p for p in proofs if not p.reserved], amount)
+ return await self.split(
+ [p for p in proofs if not p.reserved], amount, snd_secret
+ )
async def set_reserved(self, proofs: List[Proof], reserved: bool):
"""Mark a proof as reserved to avoid reuse or delete marking."""
@@ -239,6 +338,21 @@ class Wallet(LedgerAPI):
filter(lambda p: p["secret"] not in invalidate_secrets, self.proofs)
)
+ async def create_p2sh_lock(self):
+ alice_privkey = step0_carol_privkey()
+ txin_redeemScript = step0_carol_checksig_redeemscrip(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)
+ return p2shScript
+
@property
def balance(self):
return sum(p["amount"] for p in self.proofs)
@@ -249,7 +363,7 @@ class Wallet(LedgerAPI):
def status(self):
print(
- f"Balance: {self.balance} sat (Available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)"
+ f"Balance: {self.balance} sat (available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)"
)
def proof_amounts(self):
diff --git a/poetry.lock b/poetry.lock
index 7224f57..038564d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -344,14 +344,6 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
-[[package]]
-name = "psycopg2-binary"
-version = "2.9.3"
-description = "psycopg2 - Python-PostgreSQL Database Adapter"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
[[package]]
name = "py"
version = "1.11.0"
@@ -430,6 +422,14 @@ typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
[package.extras]
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
+[[package]]
+name = "python-bitcoinlib"
+version = "0.11.2"
+description = "The Swiss Army Knife of the Bitcoin protocol."
+category = "main"
+optional = false
+python-versions = "*"
+
[[package]]
name = "python-dotenv"
version = "0.21.0"
@@ -632,7 +632,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
-content-hash = "fbb8f977f71b77cf9b6514134111dc466f7fa1f70f8114e47b858d85c39199e4"
+content-hash = "b4e980ee90226bab07750b1becc8c69df7752f6d168d200a79c782aa1efe61da"
[metadata.files]
anyio = [
@@ -848,64 +848,6 @@ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
-psycopg2-binary = [
- {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"},
- {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"},
- {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"},
- {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"},
- {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"},
- {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
-]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
@@ -964,6 +906,10 @@ pytest-asyncio = [
{file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"},
{file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"},
]
+python-bitcoinlib = [
+ {file = "python-bitcoinlib-0.11.2.tar.gz", hash = "sha256:61ba514e0d232cc84741e49862dcedaf37199b40bba252a17edc654f63d13f39"},
+ {file = "python_bitcoinlib-0.11.2-py3-none-any.whl", hash = "sha256:78bd4ee717fe805cd760dfdd08765e77b7c7dbef4627f8596285e84953756508"},
+]
python-dotenv = [
{file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"},
{file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"},
diff --git a/pyproject.toml b/pyproject.toml
index e16a8d8..c46d31c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "cashu"
-version = "0.1.10"
+version = "0.2.0"
description = "Ecash wallet and mint."
authors = ["calle "]
license = "MIT"
@@ -13,7 +13,6 @@ SQLAlchemy = "1.3.24"
click = "8.0.4"
pydantic = "^1.10.2"
bech32 = "^1.2.0"
-psycopg2-binary = "^2.9.3"
fastapi = "^0.83.0"
environs = "^9.5.0"
uvicorn = "^0.18.3"
@@ -22,6 +21,7 @@ ecdsa = "^0.18.0"
bitstring = "^3.1.9"
secp256k1 = "^0.14.0"
sqlalchemy-aio = "^0.17.0"
+python-bitcoinlib = "^0.11.2"
[tool.poetry.dev-dependencies]
black = {version = "^22.8.0", allow-prereleases = true}
diff --git a/requirements.txt b/requirements.txt
index 192fb45..8fc17f6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,13 +19,13 @@ marshmallow==3.18.0 ; python_version >= "3.7" and python_version < "4.0"
outcome==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
packaging==21.3 ; python_version >= "3.7" and python_version < "4.0"
pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
-psycopg2-binary==2.9.3 ; python_version >= "3.7" and python_version < "4.0"
py==1.11.0 ; python_version >= "3.7" and python_version < "4.0"
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
pydantic==1.10.2 ; python_version >= "3.7" and python_version < "4.0"
pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0"
pytest-asyncio==0.19.0 ; python_version >= "3.7" and python_version < "4.0"
pytest==7.1.3 ; python_version >= "3.7" and python_version < "4.0"
+python-bitcoinlib==0.11.2 ; python_version >= "3.7" and python_version < "4.0"
python-dotenv==0.21.0 ; python_version >= "3.7" and python_version < "4.0"
represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0"
requests==2.27.1 ; python_version >= "3.7" and python_version < "4.0"
diff --git a/setup.py b/setup.py
index b7176cc..07d30e3 100644
--- a/setup.py
+++ b/setup.py
@@ -9,11 +9,11 @@ with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
with open("requirements.txt") as f:
requirements = f.read().splitlines()
-entry_points = {"console_scripts": ["cashu = cahu.wallet.cli:cli"]}
+entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]}
setuptools.setup(
name="cashu",
- version="0.1.10",
+ version="0.2.0",
description="Ecash wallet and mint with Bitcoin Lightning support",
long_description=long_description,
long_description_content_type="text/markdown",
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index a77b141..5800910 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -1,3 +1,6 @@
+import time
+from re import S
+
from cashu.core.helpers import async_unwrap
from cashu.core.migrations import migrate_databases
from cashu.wallet import migrations
@@ -25,10 +28,12 @@ def assert_amt(proofs, expected):
async def run_test():
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
await migrate_databases(wallet1.db, migrations)
+ wallet1.load_mint()
wallet1.status()
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
await migrate_databases(wallet2.db, migrations)
+ wallet2.load_mint()
wallet2.status()
proofs = []
@@ -50,7 +55,7 @@ async def run_test():
# Error: We try to double-spend by providing a valid proof twice
await assert_err(
wallet1.split(wallet1.proofs + proofs, 20),
- f"Error: tokens already spent. Secret: {proofs[0]['secret']}",
+ f"Mint Error: tokens already spent. Secret: {proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64
wallet1.status()
@@ -67,7 +72,7 @@ async def run_test():
# Error: We try to double-spend and it fails
await assert_err(
wallet1.split([proofs[0]], 10),
- f"Error: tokens already spent. Secret: {proofs[0]['secret']}",
+ f"Mint Error: tokens already spent. Secret: {proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64
@@ -96,7 +101,7 @@ async def run_test():
# Error: We try to double-spend and it fails
await assert_err(
wallet1.split(w1_snd_proofs, 5),
- f"Error: tokens already spent. Secret: {w1_snd_proofs[0]['secret']}",
+ f"Mint Error: tokens already spent. Secret: {w1_snd_proofs[0]['secret']}",
)
assert wallet1.balance == 63 + 64 - 20
@@ -105,9 +110,42 @@ async def run_test():
assert wallet1.proof_amounts() == [1, 2, 4, 4, 32, 64]
assert wallet2.proof_amounts() == [4, 16]
+ # manipulate the proof amount
+ # w1_fst_proofs2_manipulated = w1_fst_proofs2.copy()
+ # w1_fst_proofs2_manipulated[0]["amount"] = 123
+ # await assert_err(
+ # wallet1.split(w1_fst_proofs2_manipulated, 20),
+ # "Error: 123",
+ # )
+
+ # try to split an invalid amount
await assert_err(
wallet1.split(w1_snd_proofs, -500),
- "Error: invalid split amount: -500",
+ "Mint Error: invalid split amount: -500",
+ )
+
+ # mint with secrets
+ secret = f"asdasd_{time.time()}"
+ w1_fst_proofs, w1_snd_proofs = await wallet1.split(
+ wallet1.proofs, 65, snd_secret=secret
+ )
+
+ # p2sh test
+ p2shscript = await wallet2.create_p2sh_lock()
+ txin_p2sh_address = p2shscript.address
+ lock = f"P2SH:{txin_p2sh_address}"
+ _, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock)
+ _, _ = await wallet2.redeem(
+ send_proofs, snd_script=p2shscript.script, snd_siganture=p2shscript.signature
+ )
+
+ # strip away the secrets
+ w1_snd_proofs_manipulated = w1_snd_proofs.copy()
+ for p in w1_snd_proofs_manipulated:
+ p.secret = ""
+ await assert_err(
+ wallet2.redeem(w1_snd_proofs_manipulated),
+ "Mint Error: no secret in proof.",
)