mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-04 01:14:21 +01:00
Merge pull request #19 from callebtc/pay_to_scripthash
Pay to scripthash
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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):
|
||||
|
||||
166
cashu/core/script.py
Normal file
166
cashu/core/script.py
Normal file
@@ -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.")
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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 <address> in proof.secret.
|
||||
proof.secret format: P2SH:<address>:<secret>
|
||||
"""
|
||||
# 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:<address>:<secret>
|
||||
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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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>."
|
||||
)
|
||||
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 <amount> --lock P2SH:{txin_p2sh_address}"
|
||||
)
|
||||
print("")
|
||||
print(
|
||||
f"Only you can receive coins from this lock:\n\ncashu receive <coin> --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 <coin> --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
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
80
poetry.lock
generated
80
poetry.lock
generated
@@ -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"},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cashu"
|
||||
version = "0.1.10"
|
||||
version = "0.2.0"
|
||||
description = "Ecash wallet and mint."
|
||||
authors = ["calle <callebtc@protonmail.com>"]
|
||||
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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
4
setup.py
4
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",
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user