mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
Determinstic secrets / ecash restore (#131)
* first working version but some sats go missing * back at it * make format * restore to main * move mint database * fix some tests * make format * remove old _construct_outputs we reintroduced in merge with main * add type annotations * add wallet private key to tests * wallet: load proofs * fix tests * _generate_secrets with deterministic generation (temporary) * allow wallet initialization with custom private key * add pk to wallet api test * mint scope=module * remove private_key from test_wallet.py to see if it helps with the github tests * readd private keys to tests * workflow without env * add more private key! * readd env * ledger scope session * add default private key for testing * generate private keys if not available * testing * its working!!! * first iteration of bip32 working * get mint info and add many type annotations * tests * fix tests with bip32 * restore from multiple mints * disable profiler * make format * failed POST /mint do not increment secret counter * store derivation path in each token * fix tests * refactor migrations so private keys can be generated by the wallet with .with_db() classmethod * start fixing tests * all tests passing except those that need to set a specific private key * bip39 mnemonic to seed - with db but restore doesnt work yet with custom seed * mnemonic restore works * enter mnemonic in cli * fix tests to use different mnemonic * properly ask user for seed input * tests: dont ask for inputs * try to fix tests * fix cashu -d * fixing * bump version and add more text to mnemonic enter * add more comments * add many more comments and type annotations in the wallet * dont print generated mnemonic and dont wait for input * fix test * does this fix tests? * sigh.... * make format * do not restore from an initialized wallet * fix mnemonics * fix nitpicks * print wallet name if nonstandard wallet * fix merge error and remove comments * poetry lock and requirements * remove unused code * fix tests * mnemonic.lower() and add keyset id if not present for backwards compat * edit comment
This commit is contained in:
@@ -42,6 +42,8 @@ MINT_DATABASE=data/mint
|
|||||||
# increment derivation path to rotate to a new keyset
|
# increment derivation path to rotate to a new keyset
|
||||||
MINT_DERIVATION_PATH="0/0/0/0"
|
MINT_DERIVATION_PATH="0/0/0/0"
|
||||||
|
|
||||||
|
MINT_DATABASE=data/mint
|
||||||
|
|
||||||
# Lightning
|
# Lightning
|
||||||
# Supported: LNbitsWallet, FakeWallet
|
# Supported: LNbitsWallet, FakeWallet
|
||||||
MINT_LIGHTNING_BACKEND=LNbitsWallet
|
MINT_LIGHTNING_BACKEND=LNbitsWallet
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ cashu info
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
```bash
|
```bash
|
||||||
Version: 0.12.3
|
Version: 0.13.0
|
||||||
Debug: False
|
Debug: False
|
||||||
Cashu dir: /home/user/.cashu
|
Cashu dir: /home/user/.cashu
|
||||||
Wallet: wallet
|
Wallet: wallet
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ class Proof(BaseModel):
|
|||||||
] = "" # unique ID of send attempt, used for grouping pending tokens in the wallet
|
] = "" # unique ID of send attempt, used for grouping pending tokens in the wallet
|
||||||
time_created: Union[None, str] = ""
|
time_created: Union[None, str] = ""
|
||||||
time_reserved: Union[None, str] = ""
|
time_reserved: Union[None, str] = ""
|
||||||
|
derivation_path: Union[None, str] = "" # derivation path of the proof
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
# dictionary without the fields that don't need to be send to Carol
|
# dictionary without the fields that don't need to be send to Carol
|
||||||
@@ -349,6 +350,14 @@ class CheckFeesResponse(BaseModel):
|
|||||||
fee: Union[int, None]
|
fee: Union[int, None]
|
||||||
|
|
||||||
|
|
||||||
|
# ------- API: RESTORE -------
|
||||||
|
|
||||||
|
|
||||||
|
class PostRestoreResponse(BaseModel):
|
||||||
|
outputs: List[BlindedMessage] = []
|
||||||
|
promises: List[BlindedSignature] = []
|
||||||
|
|
||||||
|
|
||||||
# ------- KEYSETS -------
|
# ------- KEYSETS -------
|
||||||
|
|
||||||
|
|
||||||
@@ -571,7 +580,7 @@ class TokenV3(BaseModel):
|
|||||||
return list(set([p.id for p in self.get_proofs()]))
|
return list(set([p.id for p in self.get_proofs()]))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def deserialize(cls, tokenv3_serialized: str):
|
def deserialize(cls, tokenv3_serialized: str) -> "TokenV3":
|
||||||
"""
|
"""
|
||||||
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
||||||
"""
|
"""
|
||||||
@@ -583,7 +592,7 @@ class TokenV3(BaseModel):
|
|||||||
token = json.loads(base64.urlsafe_b64decode(token_base64))
|
token = json.loads(base64.urlsafe_b64decode(token_base64))
|
||||||
return cls.parse_obj(token)
|
return cls.parse_obj(token)
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self) -> str:
|
||||||
"""
|
"""
|
||||||
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -51,13 +51,10 @@ def hash_to_curve(message: bytes) -> PublicKey:
|
|||||||
|
|
||||||
|
|
||||||
def step1_alice(
|
def step1_alice(
|
||||||
secret_msg: str, blinding_factor: Optional[bytes] = None
|
secret_msg: str, blinding_factor: Optional[PrivateKey] = None
|
||||||
) -> tuple[PublicKey, PrivateKey]:
|
) -> tuple[PublicKey, PrivateKey]:
|
||||||
Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8"))
|
Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8"))
|
||||||
if blinding_factor:
|
r = blinding_factor or PrivateKey()
|
||||||
r = PrivateKey(privkey=blinding_factor, raw=True)
|
|
||||||
else:
|
|
||||||
r = PrivateKey()
|
|
||||||
B_: PublicKey = Y + r.pubkey # type: ignore
|
B_: PublicKey = Y + r.pubkey # type: ignore
|
||||||
return B_, r
|
return B_, r
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from ..core.db import COCKROACH, POSTGRES, SQLITE, Database, table_with_schema
|
from ..core.db import COCKROACH, POSTGRES, SQLITE, Database, table_with_schema
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ async def migrate_databases(db: Database, migrations_module):
|
|||||||
if match:
|
if match:
|
||||||
version = int(match.group(1))
|
version = int(match.group(1))
|
||||||
if version > current_versions.get(db_name, 0):
|
if version > current_versions.get(db_name, 0):
|
||||||
|
logger.debug(f"Migrating {db_name} db: {key}")
|
||||||
await migrate(db)
|
await migrate(db)
|
||||||
|
|
||||||
if db.schema == None:
|
if db.schema == None:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field
|
|||||||
|
|
||||||
env = Env()
|
env = Env()
|
||||||
|
|
||||||
VERSION = "0.12.3"
|
VERSION = "0.13.0"
|
||||||
|
|
||||||
|
|
||||||
def find_env_file():
|
def find_env_file():
|
||||||
@@ -43,8 +43,6 @@ class CashuSettings(BaseSettings):
|
|||||||
class EnvSettings(CashuSettings):
|
class EnvSettings(CashuSettings):
|
||||||
debug: bool = Field(default=False)
|
debug: bool = Field(default=False)
|
||||||
log_level: str = Field(default="INFO")
|
log_level: str = Field(default="INFO")
|
||||||
host: str = Field(default="127.0.0.1")
|
|
||||||
port: int = Field(default=3338)
|
|
||||||
cashu_dir: str = Field(default=os.path.join(str(Path.home()), ".cashu"))
|
cashu_dir: str = Field(default=os.path.join(str(Path.home()), ".cashu"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
def amount_split(amount):
|
def amount_split(amount: int):
|
||||||
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
|
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
|
||||||
bits_amt = bin(amount)[::-1][:-2]
|
bits_amt = bin(amount)[::-1][:-2]
|
||||||
rv = []
|
rv = []
|
||||||
for (pos, bit) in enumerate(bits_amt):
|
for pos, bit in enumerate(bits_amt):
|
||||||
if bit == "1":
|
if bit == "1":
|
||||||
rv.append(2**pos)
|
rv.append(2**pos)
|
||||||
return rv
|
return rv
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from ..core.base import Invoice, MintKeyset, Proof
|
from ..core.base import BlindedSignature, Invoice, MintKeyset, Proof
|
||||||
from ..core.db import Connection, Database, table_with_schema
|
from ..core.db import Connection, Database, table_with_schema
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +42,9 @@ class LedgerCrud:
|
|||||||
async def store_promise(*args, **kwags):
|
async def store_promise(*args, **kwags):
|
||||||
return await store_promise(*args, **kwags) # type: ignore
|
return await store_promise(*args, **kwags) # type: ignore
|
||||||
|
|
||||||
|
async def get_promise(*args, **kwags):
|
||||||
|
return await get_promise(*args, **kwags) # type: ignore
|
||||||
|
|
||||||
async def update_lightning_invoice(*args, **kwags):
|
async def update_lightning_invoice(*args, **kwags):
|
||||||
return await update_lightning_invoice(*args, **kwags) # type: ignore
|
return await update_lightning_invoice(*args, **kwags) # type: ignore
|
||||||
|
|
||||||
@@ -51,22 +54,39 @@ async def store_promise(
|
|||||||
amount: int,
|
amount: int,
|
||||||
B_: str,
|
B_: str,
|
||||||
C_: str,
|
C_: str,
|
||||||
|
id: str,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
):
|
):
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO {table_with_schema(db, 'promises')}
|
INSERT INTO {table_with_schema(db, 'promises')}
|
||||||
(amount, B_b, C_b)
|
(amount, B_b, C_b, id)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
amount,
|
amount,
|
||||||
str(B_),
|
str(B_),
|
||||||
str(C_),
|
str(C_),
|
||||||
|
id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_promise(
|
||||||
|
db: Database,
|
||||||
|
B_: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
row = await (conn or db).fetchone(
|
||||||
|
f"""
|
||||||
|
SELECT * from {table_with_schema(db, 'promises')}
|
||||||
|
WHERE B_b = ?
|
||||||
|
""",
|
||||||
|
(str(B_),),
|
||||||
|
)
|
||||||
|
return BlindedSignature(amount=row[0], C_=row[2], id=row[3]) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_proofs_used(
|
async def get_proofs_used(
|
||||||
db: Database,
|
db: Database,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
@@ -88,13 +108,14 @@ async def invalidate_proof(
|
|||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO {table_with_schema(db, 'proofs_used')}
|
INSERT INTO {table_with_schema(db, 'proofs_used')}
|
||||||
(amount, C, secret)
|
(amount, C, secret, id)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
proof.amount,
|
proof.amount,
|
||||||
str(proof.C),
|
str(proof.C),
|
||||||
str(proof.secret),
|
str(proof.secret),
|
||||||
|
str(proof.id),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
from typing import Dict, List, Literal, Optional, Set, Tuple, Union
|
from typing import Dict, List, Literal, Optional, Set, Tuple, Union
|
||||||
@@ -178,7 +177,11 @@ class Ledger:
|
|||||||
C_ = b_dhke.step2_bob(B_, private_key_amount)
|
C_ = b_dhke.step2_bob(B_, private_key_amount)
|
||||||
logger.trace(f"crud: _generate_promise storing promise for {amount}")
|
logger.trace(f"crud: _generate_promise storing promise for {amount}")
|
||||||
await self.crud.store_promise(
|
await self.crud.store_promise(
|
||||||
amount=amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db
|
amount=amount,
|
||||||
|
B_=B_.serialize().hex(),
|
||||||
|
C_=C_.serialize().hex(),
|
||||||
|
id=keyset.id,
|
||||||
|
db=self.db,
|
||||||
)
|
)
|
||||||
logger.trace(f"crud: _generate_promise stored promise for {amount}")
|
logger.trace(f"crud: _generate_promise stored promise for {amount}")
|
||||||
return BlindedSignature(id=keyset.id, amount=amount, C_=C_.serialize().hex())
|
return BlindedSignature(id=keyset.id, amount=amount, C_=C_.serialize().hex())
|
||||||
@@ -804,7 +807,7 @@ class Ledger:
|
|||||||
"""
|
"""
|
||||||
logger.trace(f"called request_mint")
|
logger.trace(f"called request_mint")
|
||||||
if settings.mint_max_peg_in and amount > settings.mint_max_peg_in:
|
if settings.mint_max_peg_in and amount > settings.mint_max_peg_in:
|
||||||
raise Exception(f"Maximum mint amount is {settings.mint_max_peg_in} sats.")
|
raise Exception(f"Maximum mint amount is {settings.mint_max_peg_in} sat.")
|
||||||
if settings.mint_peg_out_only:
|
if settings.mint_peg_out_only:
|
||||||
raise Exception("Mint does not allow minting new tokens.")
|
raise Exception("Mint does not allow minting new tokens.")
|
||||||
|
|
||||||
@@ -904,7 +907,7 @@ class Ledger:
|
|||||||
invoice_amount = math.ceil(invoice_obj.amount_msat / 1000)
|
invoice_amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||||
if settings.mint_max_peg_out and invoice_amount > settings.mint_max_peg_out:
|
if settings.mint_max_peg_out and invoice_amount > settings.mint_max_peg_out:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Maximum melt amount is {settings.mint_max_peg_out} sats."
|
f"Maximum melt amount is {settings.mint_max_peg_out} sat."
|
||||||
)
|
)
|
||||||
fees_msat = await self.check_fees(invoice)
|
fees_msat = await self.check_fees(invoice)
|
||||||
assert total_provided >= invoice_amount + fees_msat / 1000, Exception(
|
assert total_provided >= invoice_amount + fees_msat / 1000, Exception(
|
||||||
@@ -1067,3 +1070,26 @@ class Ledger:
|
|||||||
|
|
||||||
logger.trace(f"split successful")
|
logger.trace(f"split successful")
|
||||||
return prom_fst, prom_snd
|
return prom_fst, prom_snd
|
||||||
|
|
||||||
|
async def restore(
|
||||||
|
self, outputs: List[BlindedMessage]
|
||||||
|
) -> Tuple[List[BlindedMessage], List[BlindedSignature]]:
|
||||||
|
promises: List[BlindedSignature] = []
|
||||||
|
return_outputs: List[BlindedMessage] = []
|
||||||
|
async with self.db.connect() as conn:
|
||||||
|
for output in outputs:
|
||||||
|
logger.trace(f"looking for promise: {output}")
|
||||||
|
promise = await self.crud.get_promise(
|
||||||
|
B_=output.B_, db=self.db, conn=conn
|
||||||
|
)
|
||||||
|
if promise is not None:
|
||||||
|
# BEGIN backwards compatibility mints pre `m007_proofs_and_promises_store_id`
|
||||||
|
# add keyset id to promise if not present only if the current keyset
|
||||||
|
# is the only one ever used
|
||||||
|
if not promise.id and len(self.keysets.keysets) == 1:
|
||||||
|
promise.id = self.keyset.id
|
||||||
|
# END backwards compatibility
|
||||||
|
promises.append(promise)
|
||||||
|
return_outputs.append(output)
|
||||||
|
logger.trace(f"promise found: {promise}")
|
||||||
|
return return_outputs, promises
|
||||||
|
|||||||
@@ -159,3 +159,20 @@ async def m006_invoices_add_payment_hash(db: Database):
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash"
|
f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m007_proofs_and_promises_store_id(db: Database):
|
||||||
|
"""
|
||||||
|
Column that remembers the payment_hash as we're using
|
||||||
|
the column hash as a random identifier now
|
||||||
|
(see https://github.com/cashubtc/nuts/pull/14).
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'proofs_used')} ADD COLUMN id TEXT"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'proofs_pending')} ADD COLUMN id TEXT"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
f"ALTER TABLE {table_with_schema(db, 'promises')} ADD COLUMN id TEXT"
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from ..core.base import (
|
|||||||
PostMeltRequest,
|
PostMeltRequest,
|
||||||
PostMintRequest,
|
PostMintRequest,
|
||||||
PostMintResponse,
|
PostMintResponse,
|
||||||
|
PostRestoreResponse,
|
||||||
PostSplitRequest,
|
PostSplitRequest,
|
||||||
PostSplitResponse,
|
PostSplitResponse,
|
||||||
)
|
)
|
||||||
@@ -109,7 +110,7 @@ async def request_mint(amount: int = 0) -> Union[GetMintResponse, CashuError]:
|
|||||||
"""
|
"""
|
||||||
logger.trace(f"> GET /mint: amount={amount}")
|
logger.trace(f"> GET /mint: amount={amount}")
|
||||||
if amount > 21_000_000 * 100_000_000 or amount <= 0:
|
if amount > 21_000_000 * 100_000_000 or amount <= 0:
|
||||||
return CashuError(code=0, error="Amount must be a valid amount of sats.")
|
return CashuError(code=0, error="Amount must be a valid amount of sat.")
|
||||||
if settings.mint_peg_out_only:
|
if settings.mint_peg_out_only:
|
||||||
return CashuError(code=0, error="Mint does not allow minting new tokens.")
|
return CashuError(code=0, error="Mint does not allow minting new tokens.")
|
||||||
try:
|
try:
|
||||||
@@ -229,3 +230,15 @@ async def split(
|
|||||||
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
|
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
|
||||||
logger.trace(f"< POST /split: {resp}")
|
logger.trace(f"< POST /split: {resp}")
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/restore", name="Restore", summary="Restores a blinded signature from a secret"
|
||||||
|
)
|
||||||
|
async def restore(payload: PostMintRequest) -> Union[CashuError, PostRestoreResponse]:
|
||||||
|
assert payload.outputs, Exception("no outputs provided.")
|
||||||
|
try:
|
||||||
|
outputs, promises = await ledger.restore(payload.outputs)
|
||||||
|
except Exception as exc:
|
||||||
|
return CashuError(code=0, error=str(exc))
|
||||||
|
return PostRestoreResponse(outputs=outputs, promises=promises)
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ class WalletsResponse(BaseModel):
|
|||||||
wallets: Dict
|
wallets: Dict
|
||||||
|
|
||||||
|
|
||||||
|
class RestoreResponse(BaseModel):
|
||||||
|
balance: int
|
||||||
|
|
||||||
|
|
||||||
class InfoResponse(BaseModel):
|
class InfoResponse(BaseModel):
|
||||||
version: str
|
version: str
|
||||||
wallet: str
|
wallet: str
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import groupby, islice
|
from itertools import groupby, islice
|
||||||
@@ -29,6 +30,7 @@ from .responses import (
|
|||||||
PayResponse,
|
PayResponse,
|
||||||
PendingResponse,
|
PendingResponse,
|
||||||
ReceiveResponse,
|
ReceiveResponse,
|
||||||
|
RestoreResponse,
|
||||||
SendResponse,
|
SendResponse,
|
||||||
SwapResponse,
|
SwapResponse,
|
||||||
WalletsResponse,
|
WalletsResponse,
|
||||||
@@ -40,12 +42,17 @@ router: APIRouter = APIRouter()
|
|||||||
def create_wallet(
|
def create_wallet(
|
||||||
url=settings.mint_url, dir=settings.cashu_dir, name=settings.wallet_name
|
url=settings.mint_url, dir=settings.cashu_dir, name=settings.wallet_name
|
||||||
):
|
):
|
||||||
return Wallet(url, os.path.join(dir, name), name=name)
|
return Wallet(
|
||||||
|
url=url,
|
||||||
|
db=os.path.join(dir, name),
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def load_mint(wallet: Wallet, mint: Optional[str] = None):
|
async def load_mint(wallet: Wallet, mint: Optional[str] = None):
|
||||||
if mint:
|
if mint:
|
||||||
wallet = create_wallet(mint)
|
wallet = create_wallet(mint)
|
||||||
|
await init_wallet(wallet)
|
||||||
await wallet.load_mint()
|
await wallet.load_mint()
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
@@ -390,6 +397,19 @@ async def wallets():
|
|||||||
return WalletsResponse(wallets=result)
|
return WalletsResponse(wallets=result)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/restore", name="Restore wallet", response_model=RestoreResponse)
|
||||||
|
async def restore(
|
||||||
|
to: int = Query(default=..., description="Counter to which restore the wallet"),
|
||||||
|
):
|
||||||
|
if to < 0:
|
||||||
|
raise Exception("Counter must be positive")
|
||||||
|
await wallet.load_mint()
|
||||||
|
await wallet.restore_promises(0, to)
|
||||||
|
await wallet.invalidate(wallet.proofs)
|
||||||
|
wallet.status()
|
||||||
|
return RestoreResponse(balance=wallet.available_balance)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/info", name="Information about Cashu wallet", response_model=InfoResponse)
|
@router.get("/info", name="Information about Cashu wallet", response_model=InfoResponse)
|
||||||
async def info():
|
async def info():
|
||||||
if settings.nostr_private_key:
|
if settings.nostr_private_key:
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ from ...core.helpers import sum_proofs
|
|||||||
from ...core.settings import settings
|
from ...core.settings import settings
|
||||||
from ...nostr.nostr.client.client import NostrClient
|
from ...nostr.nostr.client.client import NostrClient
|
||||||
from ...tor.tor import TorProxy
|
from ...tor.tor import TorProxy
|
||||||
from ...wallet.crud import get_lightning_invoices, get_reserved_proofs, get_unused_locks
|
from ...wallet.crud import (
|
||||||
|
get_lightning_invoices,
|
||||||
|
get_reserved_proofs,
|
||||||
|
get_seed_and_mnemonic,
|
||||||
|
get_unused_locks,
|
||||||
|
)
|
||||||
from ...wallet.wallet import Wallet as Wallet
|
from ...wallet.wallet import Wallet as Wallet
|
||||||
from ..api.api_server import start_api_server
|
from ..api.api_server import start_api_server
|
||||||
from ..cli.cli_helpers import get_mint_wallet, print_mint_balances, verify_mint
|
from ..cli.cli_helpers import get_mint_wallet, print_mint_balances, verify_mint
|
||||||
@@ -41,6 +46,15 @@ def run_api_server(ctx, param, daemon):
|
|||||||
ctx.exit()
|
ctx.exit()
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/pallets/click/issues/85#issuecomment-503464628
|
||||||
|
def coro(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return asyncio.run(f(*args, **kwargs))
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@click.group(cls=NaturalOrderGroup)
|
@click.group(cls=NaturalOrderGroup)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--host",
|
"--host",
|
||||||
@@ -64,8 +78,16 @@ def run_api_server(ctx, param, daemon):
|
|||||||
callback=run_api_server,
|
callback=run_api_server,
|
||||||
help="Start server for wallet REST API",
|
help="Start server for wallet REST API",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--tests",
|
||||||
|
"-t",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Run in test mode (don't ask for CLI inputs)",
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx: Context, host: str, walletname: str):
|
@coro
|
||||||
|
async def cli(ctx: Context, host: str, walletname: str, tests: bool):
|
||||||
if settings.tor and not TorProxy().check_platform():
|
if settings.tor and not TorProxy().check_platform():
|
||||||
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. You have two options: Either install Tor manually and set TOR=FALSE and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended). Or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly."
|
error_str = "Your settings say TOR=true but the built-in Tor bundle is not supported on your system. You have two options: Either install Tor manually and set TOR=FALSE and SOCKS_HOST=localhost and SOCKS_PORT=9050 in your Cashu config (recommended). Or turn off Tor by setting TOR=false (not recommended). Cashu will not work until you edit your config file accordingly."
|
||||||
error_str += "\n\n"
|
error_str += "\n\n"
|
||||||
@@ -81,31 +103,38 @@ def cli(ctx: Context, host: str, walletname: str):
|
|||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
ctx.obj["HOST"] = host or settings.mint_url
|
ctx.obj["HOST"] = host or settings.mint_url
|
||||||
ctx.obj["WALLET_NAME"] = walletname
|
ctx.obj["WALLET_NAME"] = walletname
|
||||||
wallet = Wallet(
|
settings.wallet_name = walletname
|
||||||
ctx.obj["HOST"], os.path.join(settings.cashu_dir, walletname), name=walletname
|
|
||||||
)
|
|
||||||
ctx.obj["WALLET"] = wallet
|
|
||||||
asyncio.run(init_wallet(ctx.obj["WALLET"], load_proofs=False))
|
|
||||||
|
|
||||||
# MUTLIMINT: Select a wallet
|
db_path = os.path.join(settings.cashu_dir, walletname)
|
||||||
|
# if the command is "restore" we don't want to ask the user for a mnemonic
|
||||||
|
# otherwise it will create a mnemonic and store it in the database
|
||||||
|
if ctx.invoked_subcommand == "restore":
|
||||||
|
wallet = await Wallet.with_db(
|
||||||
|
ctx.obj["HOST"], db_path, name=walletname, skip_private_key=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# # we need to run the migrations before we load the wallet for the first time
|
||||||
|
# # otherwise the wallet will not be able to generate a new private key and store it
|
||||||
|
wallet = await Wallet.with_db(
|
||||||
|
ctx.obj["HOST"], db_path, name=walletname, skip_private_key=True
|
||||||
|
)
|
||||||
|
# now with the migrations done, we can load the wallet and generate a new mnemonic if needed
|
||||||
|
wallet = await Wallet.with_db(ctx.obj["HOST"], db_path, name=walletname)
|
||||||
|
|
||||||
|
assert wallet, "Wallet not found."
|
||||||
|
ctx.obj["WALLET"] = wallet
|
||||||
|
# await init_wallet(ctx.obj["WALLET"], load_proofs=False)
|
||||||
|
|
||||||
|
# ------ MUTLIMINT ------- : Select a wallet
|
||||||
# only if a command is one of a subset that needs to specify a mint host
|
# only if a command is one of a subset that needs to specify a mint host
|
||||||
# if a mint host is already specified as an argument `host`, use it
|
# if a mint host is already specified as an argument `host`, use it
|
||||||
if ctx.invoked_subcommand not in ["send", "invoice", "pay"] or host:
|
if ctx.invoked_subcommand not in ["send", "invoice", "pay"] or host:
|
||||||
return
|
return
|
||||||
# else: we ask the user to select one
|
# else: we ask the user to select one
|
||||||
ctx.obj["WALLET"] = asyncio.run(
|
ctx.obj["WALLET"] = await get_mint_wallet(
|
||||||
get_mint_wallet(ctx)
|
ctx
|
||||||
) # select a specific wallet by CLI input
|
) # select a specific wallet by CLI input
|
||||||
asyncio.run(init_wallet(ctx.obj["WALLET"], load_proofs=False))
|
await init_wallet(ctx.obj["WALLET"], load_proofs=False)
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/pallets/click/issues/85#issuecomment-503464628
|
|
||||||
def coro(f):
|
|
||||||
@wraps(f)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return asyncio.run(f(*args, **kwargs))
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("pay", help="Pay Lightning invoice.")
|
@cli.command("pay", help="Pay Lightning invoice.")
|
||||||
@@ -227,7 +256,7 @@ async def swap(ctx: Context):
|
|||||||
if incoming_wallet.url == outgoing_wallet.url:
|
if incoming_wallet.url == outgoing_wallet.url:
|
||||||
raise Exception("mints for swap have to be different")
|
raise Exception("mints for swap have to be different")
|
||||||
|
|
||||||
amount = int(input("Enter amount to swap in sats: "))
|
amount = int(input("Enter amount to swap in sat: "))
|
||||||
assert amount > 0, "amount is not positive"
|
assert amount > 0, "amount is not positive"
|
||||||
|
|
||||||
# request invoice from incoming mint
|
# request invoice from incoming mint
|
||||||
@@ -550,6 +579,8 @@ async def locks(ctx):
|
|||||||
lock_str = f"P2PK:{pubkey}"
|
lock_str = f"P2PK:{pubkey}"
|
||||||
print("---- Pay to public key (P2PK) lock ----\n")
|
print("---- Pay to public key (P2PK) lock ----\n")
|
||||||
print(f"Lock: {lock_str}")
|
print(f"Lock: {lock_str}")
|
||||||
|
print("")
|
||||||
|
print("To see more information enter: cashu lock")
|
||||||
# P2SH locks
|
# P2SH locks
|
||||||
locks = await get_unused_locks(db=wallet.db)
|
locks = await get_unused_locks(db=wallet.db)
|
||||||
if len(locks):
|
if len(locks):
|
||||||
@@ -561,8 +592,7 @@ async def locks(ctx):
|
|||||||
print(f"Signature: {l.signature}")
|
print(f"Signature: {l.signature}")
|
||||||
print("")
|
print("")
|
||||||
print(f"--------------------------\n")
|
print(f"--------------------------\n")
|
||||||
else:
|
|
||||||
print("No locks found. Create one using: cashu lock")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -616,7 +646,7 @@ async def wallets(ctx):
|
|||||||
for w in wallets:
|
for w in wallets:
|
||||||
wallet = Wallet(ctx.obj["HOST"], os.path.join(settings.cashu_dir, w))
|
wallet = Wallet(ctx.obj["HOST"], os.path.join(settings.cashu_dir, w))
|
||||||
try:
|
try:
|
||||||
await init_wallet(wallet)
|
await wallet.load_proofs()
|
||||||
if wallet.proofs and len(wallet.proofs):
|
if wallet.proofs and len(wallet.proofs):
|
||||||
active_wallet = False
|
active_wallet = False
|
||||||
if w == ctx.obj["WALLET_NAME"]:
|
if w == ctx.obj["WALLET_NAME"]:
|
||||||
@@ -629,12 +659,12 @@ async def wallets(ctx):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command("info", help="Information about Cashu wallet.")
|
@cli.command("info", help="Information about Cashu wallet.")
|
||||||
@click.option(
|
@click.option("--mint", default=False, is_flag=True, help="Fetch mint information.")
|
||||||
"--mint", "-m", default=False, is_flag=True, help="Fetch mint information."
|
@click.option("--mnemonic", default=False, is_flag=True, help="Show your mnemonic.")
|
||||||
)
|
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@coro
|
@coro
|
||||||
async def info(ctx: Context, mint: bool):
|
async def info(ctx: Context, mint: bool, mnemonic: bool):
|
||||||
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
print(f"Version: {settings.version}")
|
print(f"Version: {settings.version}")
|
||||||
print(f"Wallet: {ctx.obj['WALLET_NAME']}")
|
print(f"Wallet: {ctx.obj['WALLET_NAME']}")
|
||||||
if settings.debug:
|
if settings.debug:
|
||||||
@@ -657,7 +687,6 @@ async def info(ctx: Context, mint: bool):
|
|||||||
print(f"HTTP proxy: {settings.http_proxy}")
|
print(f"HTTP proxy: {settings.http_proxy}")
|
||||||
print(f"Mint URL: {ctx.obj['HOST']}")
|
print(f"Mint URL: {ctx.obj['HOST']}")
|
||||||
if mint:
|
if mint:
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
|
||||||
mint_info: dict = (await wallet._load_mint_info()).dict()
|
mint_info: dict = (await wallet._load_mint_info()).dict()
|
||||||
print("")
|
print("")
|
||||||
print("Mint information:")
|
print("Mint information:")
|
||||||
@@ -677,4 +706,52 @@ async def info(ctx: Context, mint: bool):
|
|||||||
if mint_info["parameter"]:
|
if mint_info["parameter"]:
|
||||||
print(f"Parameter: {mint_info['parameter']}")
|
print(f"Parameter: {mint_info['parameter']}")
|
||||||
|
|
||||||
|
if mnemonic:
|
||||||
|
assert wallet.mnemonic
|
||||||
|
print(f"Mnemonic: {wallet.mnemonic}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("restore", help="Restore backups.")
|
||||||
|
@click.option(
|
||||||
|
"--batch",
|
||||||
|
"-b",
|
||||||
|
default=25,
|
||||||
|
help="Batch size. Specifies how many proofs are restored in one batch.",
|
||||||
|
type=int,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--to",
|
||||||
|
"-t",
|
||||||
|
default=2,
|
||||||
|
help="Number of empty batches to complete the restore process.",
|
||||||
|
type=int,
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
@coro
|
||||||
|
async def restore(ctx: Context, to: int, batch: int):
|
||||||
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
|
# check if there is already a mnemonic in the database
|
||||||
|
ret = await get_seed_and_mnemonic(wallet.db)
|
||||||
|
if ret:
|
||||||
|
print(
|
||||||
|
"Wallet already has a mnemonic. You can't restore an already initialized wallet."
|
||||||
|
)
|
||||||
|
print("To restore a wallet, please delete the wallet directory and try again.")
|
||||||
|
print("")
|
||||||
|
print(
|
||||||
|
f"The wallet directory is: {os.path.join(settings.cashu_dir, ctx.obj['WALLET_NAME'])}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# ask the user for a mnemonic but allow also no input
|
||||||
|
print("Please enter your mnemonic to restore your balance.")
|
||||||
|
mnemonic = input(
|
||||||
|
"Enter mnemonic: ",
|
||||||
|
)
|
||||||
|
if not mnemonic:
|
||||||
|
print("No mnemonic entered. Exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch)
|
||||||
|
await wallet.load_proofs()
|
||||||
|
wallet.status()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
from ..core.base import Invoice, KeyBase, P2SHScript, Proof, WalletKeyset
|
from ..core.base import Invoice, KeyBase, P2SHScript, Proof, WalletKeyset
|
||||||
from ..core.db import Connection, Database
|
from ..core.db import Connection, Database
|
||||||
@@ -14,10 +14,17 @@ async def store_proof(
|
|||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO proofs
|
INSERT INTO proofs
|
||||||
(id, amount, C, secret, time_created)
|
(id, amount, C, secret, time_created, derivation_path)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(proof.id, proof.amount, str(proof.C), str(proof.secret), int(time.time())),
|
(
|
||||||
|
proof.id,
|
||||||
|
proof.amount,
|
||||||
|
str(proof.C),
|
||||||
|
str(proof.secret),
|
||||||
|
int(time.time()),
|
||||||
|
proof.derivation_path,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -62,10 +69,17 @@ async def invalidate_proof(
|
|||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO proofs_used
|
INSERT INTO proofs_used
|
||||||
(amount, C, secret, time_used, id)
|
(amount, C, secret, time_used, id, derivation_path)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(proof.amount, str(proof.C), str(proof.secret), int(time.time()), proof.id),
|
(
|
||||||
|
proof.amount,
|
||||||
|
str(proof.C),
|
||||||
|
str(proof.secret),
|
||||||
|
int(time.time()),
|
||||||
|
proof.id,
|
||||||
|
proof.derivation_path,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -329,6 +343,52 @@ async def update_lightning_invoice(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def bump_secret_derivation(
|
||||||
|
db: Database,
|
||||||
|
keyset_id: str,
|
||||||
|
by: int = 1,
|
||||||
|
skip: bool = False,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
rows = await (conn or db).fetchone(
|
||||||
|
"SELECT counter from keysets WHERE id = ?", (keyset_id,)
|
||||||
|
)
|
||||||
|
# if no counter for this keyset, create one
|
||||||
|
if not rows:
|
||||||
|
await (conn or db).execute(
|
||||||
|
"UPDATE keysets SET counter = ? WHERE id = ?",
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
keyset_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
counter = 0
|
||||||
|
else:
|
||||||
|
counter = int(rows[0])
|
||||||
|
|
||||||
|
if not skip:
|
||||||
|
await (conn or db).execute(
|
||||||
|
f"UPDATE keysets SET counter = counter + {by} WHERE id = ?",
|
||||||
|
(keyset_id,),
|
||||||
|
)
|
||||||
|
return counter
|
||||||
|
|
||||||
|
|
||||||
|
async def set_secret_derivation(
|
||||||
|
db: Database,
|
||||||
|
keyset_id: str,
|
||||||
|
counter: int,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
await (conn or db).execute(
|
||||||
|
"UPDATE keysets SET counter = ? WHERE id = ?",
|
||||||
|
(
|
||||||
|
counter,
|
||||||
|
keyset_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def set_nostr_last_check_timestamp(
|
async def set_nostr_last_check_timestamp(
|
||||||
db: Database,
|
db: Database,
|
||||||
timestamp: int,
|
timestamp: int,
|
||||||
@@ -351,3 +411,41 @@ async def get_nostr_last_check_timestamp(
|
|||||||
("dm",),
|
("dm",),
|
||||||
)
|
)
|
||||||
return row[0] if row else None
|
return row[0] if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_seed_and_mnemonic(
|
||||||
|
db: Database,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> Optional[Tuple[str, str]]:
|
||||||
|
row = await (conn or db).fetchone(
|
||||||
|
f"""
|
||||||
|
SELECT seed, mnemonic from seed
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
row[0],
|
||||||
|
row[1],
|
||||||
|
)
|
||||||
|
if row
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def store_seed_and_mnemonic(
|
||||||
|
db: Database,
|
||||||
|
seed: str,
|
||||||
|
mnemonic: str,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
await (conn or db).execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO seed
|
||||||
|
(seed, mnemonic)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
seed,
|
||||||
|
mnemonic,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
import click
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from ..core.base import TokenV1, TokenV2, TokenV3, TokenV3Token
|
from ..core.base import TokenV1, TokenV2, TokenV3, TokenV3Token
|
||||||
|
from ..core.db import Database
|
||||||
from ..core.helpers import sum_proofs
|
from ..core.helpers import sum_proofs
|
||||||
from ..core.migrations import migrate_databases
|
from ..core.migrations import migrate_databases
|
||||||
from ..core.settings import settings
|
from ..core.settings import settings
|
||||||
from ..wallet import migrations
|
from ..wallet import migrations
|
||||||
from ..wallet.crud import get_keyset, get_unused_locks
|
from ..wallet.crud import get_keyset
|
||||||
from ..wallet.wallet import Wallet as Wallet
|
from ..wallet.wallet import Wallet
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate_wallet_db(db: Database):
|
||||||
|
await migrate_databases(db, migrations)
|
||||||
|
|
||||||
|
|
||||||
async def init_wallet(wallet: Wallet, load_proofs: bool = True):
|
async def init_wallet(wallet: Wallet, load_proofs: bool = True):
|
||||||
"""Performs migrations and loads proofs from db."""
|
"""Performs migrations and loads proofs from db."""
|
||||||
await migrate_databases(wallet.db, migrations)
|
await wallet._migrate_database()
|
||||||
|
await wallet._init_private_key()
|
||||||
if load_proofs:
|
if load_proofs:
|
||||||
await wallet.load_proofs(reload=True)
|
await wallet.load_proofs(reload=True)
|
||||||
|
|
||||||
@@ -31,7 +35,9 @@ async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
|
|||||||
assert t.mint, Exception(
|
assert t.mint, Exception(
|
||||||
"redeem_TokenV3_multimint: multimint redeem without URL"
|
"redeem_TokenV3_multimint: multimint redeem without URL"
|
||||||
)
|
)
|
||||||
mint_wallet = Wallet(t.mint, os.path.join(settings.cashu_dir, wallet.name))
|
mint_wallet = await Wallet.with_db(
|
||||||
|
t.mint, os.path.join(settings.cashu_dir, wallet.name)
|
||||||
|
)
|
||||||
keysets = mint_wallet._get_proofs_keysets(t.proofs)
|
keysets = mint_wallet._get_proofs_keysets(t.proofs)
|
||||||
logger.debug(f"Keysets in tokens: {keysets}")
|
logger.debug(f"Keysets in tokens: {keysets}")
|
||||||
# loop over all keysets
|
# loop over all keysets
|
||||||
@@ -130,7 +136,7 @@ async def receive(
|
|||||||
assert mint_keysets, Exception("we don't know this keyset")
|
assert mint_keysets, Exception("we don't know this keyset")
|
||||||
assert mint_keysets.mint_url, Exception("we don't know this mint's URL")
|
assert mint_keysets.mint_url, Exception("we don't know this mint's URL")
|
||||||
# now we have the URL
|
# now we have the URL
|
||||||
mint_wallet = Wallet(
|
mint_wallet = await Wallet.with_db(
|
||||||
mint_keysets.mint_url,
|
mint_keysets.mint_url,
|
||||||
os.path.join(settings.cashu_dir, wallet.name),
|
os.path.join(settings.cashu_dir, wallet.name),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -176,3 +176,20 @@ async def m008_keysets_add_public_keys(db: Database):
|
|||||||
Stores public keys of mint in a new column of table keysets.
|
Stores public keys of mint in a new column of table keysets.
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE keysets ADD COLUMN public_keys TEXT")
|
await db.execute("ALTER TABLE keysets ADD COLUMN public_keys TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m009_privatekey_and_determinstic_key_derivation(db: Database):
|
||||||
|
await db.execute("ALTER TABLE keysets ADD COLUMN counter INTEGER DEFAULT 0")
|
||||||
|
await db.execute("ALTER TABLE proofs ADD COLUMN derivation_path TEXT")
|
||||||
|
await db.execute("ALTER TABLE proofs_used ADD COLUMN derivation_path TEXT")
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS seed (
|
||||||
|
seed TEXT NOT NULL,
|
||||||
|
mnemonic TEXT NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (seed, mnemonic)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
# await db.execute("INSERT INTO secret_derivation (counter) VALUES (0)")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
869
poetry.lock
generated
869
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,8 @@ wheel = "^0.38.4"
|
|||||||
importlib-metadata = "^5.2.0"
|
importlib-metadata = "^5.2.0"
|
||||||
psycopg2-binary = {version = "^2.9.5", optional = true }
|
psycopg2-binary = {version = "^2.9.5", optional = true }
|
||||||
httpx = "0.23.0"
|
httpx = "0.23.0"
|
||||||
|
bip32 = "^3.4"
|
||||||
|
mnemonic = "^0.20"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
pgsql = ["psycopg2-binary"]
|
pgsql = ["psycopg2-binary"]
|
||||||
|
|||||||
@@ -1,38 +1,39 @@
|
|||||||
anyio==3.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
anyio==3.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
attrs==22.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
attrs==23.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
bip32==3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
certifi==2022.12.7 ; python_version >= "3.7" and python_version < "4"
|
certifi==2023.5.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
|
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
charset-normalizer==3.0.1 ; python_version >= "3.7" and python_version < "4"
|
charset-normalizer==3.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
click==8.1.3 ; python_version >= "3.7" and python_version < "4.0"
|
click==8.1.5 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
coincurve==18.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||||
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
ecdsa==0.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
ecdsa==0.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
environs==9.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
environs==9.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
exceptiongroup==1.1.0 ; python_version >= "3.7" and python_version < "3.11"
|
exceptiongroup==1.1.2 ; python_version >= "3.7" and python_version < "3.11"
|
||||||
fastapi==0.83.0 ; python_version >= "3.7" and python_version < "4.0"
|
fastapi==0.83.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
|
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
idna==3.4 ; python_version >= "3.7" and python_version < "4"
|
idna==3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
importlib-metadata==5.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
importlib-metadata==5.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
iniconfig==2.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
|
||||||
loguru==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
|
loguru==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
marshmallow==3.19.0 ; python_version >= "3.7" and python_version < "4.0"
|
marshmallow==3.19.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
mnemonic==0.20 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
outcome==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
outcome==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
packaging==23.0 ; python_version >= "3.7" and python_version < "4.0"
|
packaging==23.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
|
||||||
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
|
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pycryptodomex==3.17 ; python_version >= "3.7" and python_version < "4.0"
|
pycryptodomex==3.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pydantic==1.10.5 ; python_version >= "3.7" and python_version < "4.0"
|
pydantic==1.10.11 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
pysocks==1.7.1 ; 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.2.2 ; python_version >= "3.7" and python_version < "4.0"
|
|
||||||
python-bitcoinlib==0.11.2 ; 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.1 ; python_version >= "3.7" and python_version < "4.0"
|
python-dotenv==0.21.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
represent==1.6.0.post0 ; 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.28.2 ; python_version >= "3.7" and python_version < "4"
|
requests==2.31.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
setuptools==65.7.0 ; python_version >= "3.7" and python_version < "4.0"
|
setuptools==65.7.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
@@ -41,11 +42,10 @@ sniffio==1.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
|||||||
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0"
|
sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
tomli==2.0.1 ; python_version >= "3.7" and python_version < "3.11"
|
typing-extensions==4.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
typing-extensions==4.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
urllib3==2.0.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
urllib3==1.26.14 ; python_version >= "3.7" and python_version < "4"
|
|
||||||
uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0"
|
uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
websocket-client==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
websocket-client==1.6.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
wheel==0.38.4 ; python_version >= "3.7" and python_version < "4.0"
|
wheel==0.38.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||||
zipp==3.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
zipp==3.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
|
|||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="cashu",
|
name="cashu",
|
||||||
version="0.12.3",
|
version="0.13.0",
|
||||||
description="Ecash wallet and mint for Bitcoin Lightning",
|
description="Ecash wallet and mint for Bitcoin Lightning",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class UvicornServer(multiprocessing.Process):
|
|||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
settings.lightning = False
|
settings.lightning = False
|
||||||
settings.mint_lightning_backend = "FakeWallet"
|
settings.mint_lightning_backend = "FakeWallet"
|
||||||
settings.mint_listen_port = 3337
|
|
||||||
settings.mint_database = "data/test_mint"
|
settings.mint_database = "data/test_mint"
|
||||||
settings.mint_private_key = self.private_key
|
settings.mint_private_key = self.private_key
|
||||||
settings.mint_derivation_path = "0/0/0/0"
|
settings.mint_derivation_path = "0/0/0/0"
|
||||||
@@ -46,36 +45,24 @@ class UvicornServer(multiprocessing.Process):
|
|||||||
if dirpath.exists() and dirpath.is_dir():
|
if dirpath.exists() and dirpath.is_dir():
|
||||||
shutil.rmtree(dirpath)
|
shutil.rmtree(dirpath)
|
||||||
|
|
||||||
dirpath = Path("data/test_wallet")
|
dirpath = Path("data/wallet1")
|
||||||
|
if dirpath.exists() and dirpath.is_dir():
|
||||||
|
shutil.rmtree(dirpath)
|
||||||
|
|
||||||
|
dirpath = Path("data/wallet2")
|
||||||
|
if dirpath.exists() and dirpath.is_dir():
|
||||||
|
shutil.rmtree(dirpath)
|
||||||
|
|
||||||
|
dirpath = Path("data/wallet3")
|
||||||
if dirpath.exists() and dirpath.is_dir():
|
if dirpath.exists() and dirpath.is_dir():
|
||||||
shutil.rmtree(dirpath)
|
shutil.rmtree(dirpath)
|
||||||
|
|
||||||
self.server.run()
|
self.server.run()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True, scope="session")
|
|
||||||
def mint():
|
|
||||||
settings.mint_listen_port = 3337
|
|
||||||
settings.port = 3337
|
|
||||||
settings.mint_url = "http://localhost:3337"
|
|
||||||
settings.port = settings.mint_listen_port
|
|
||||||
settings.nostr_private_key = secrets.token_hex(32) # wrong private key
|
|
||||||
config = uvicorn.Config(
|
|
||||||
"cashu.mint.app:app",
|
|
||||||
port=settings.mint_listen_port,
|
|
||||||
host="127.0.0.1",
|
|
||||||
)
|
|
||||||
|
|
||||||
server = UvicornServer(config=config)
|
|
||||||
server.start()
|
|
||||||
time.sleep(1)
|
|
||||||
yield server
|
|
||||||
server.stop()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
async def ledger():
|
async def ledger():
|
||||||
async def start_mint_init(ledger):
|
async def start_mint_init(ledger: Ledger):
|
||||||
await migrate_databases(ledger.db, migrations_mint)
|
await migrate_databases(ledger.db, migrations_mint)
|
||||||
await ledger.load_used_proofs()
|
await ledger.load_used_proofs()
|
||||||
await ledger.init_keysets()
|
await ledger.init_keysets()
|
||||||
@@ -93,20 +80,18 @@ async def ledger():
|
|||||||
yield ledger
|
yield ledger
|
||||||
|
|
||||||
|
|
||||||
# @pytest.fixture(autouse=True, scope="session")
|
@pytest.fixture(autouse=True, scope="session")
|
||||||
# def mint_3338():
|
def mint():
|
||||||
# settings.mint_listen_port = 3338
|
settings.mint_listen_port = 3337
|
||||||
# settings.port = 3338
|
settings.mint_url = "http://localhost:3337"
|
||||||
# settings.mint_url = "http://localhost:3338"
|
config = uvicorn.Config(
|
||||||
# settings.port = settings.mint_listen_port
|
"cashu.mint.app:app",
|
||||||
# config = uvicorn.Config(
|
port=settings.mint_listen_port,
|
||||||
# "cashu.mint.app:app",
|
host="127.0.0.1",
|
||||||
# port=settings.mint_listen_port,
|
)
|
||||||
# host="127.0.0.1",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY")
|
server = UvicornServer(config=config)
|
||||||
# server.start()
|
server.start()
|
||||||
# time.sleep(1)
|
time.sleep(1)
|
||||||
# yield server
|
yield server
|
||||||
# server.stop()
|
server.stop()
|
||||||
|
|||||||
@@ -13,20 +13,15 @@ from tests.conftest import SERVER_ENDPOINT, mint
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True, scope="session")
|
@pytest.fixture(autouse=True, scope="session")
|
||||||
def cli_prefix():
|
def cli_prefix():
|
||||||
yield ["--wallet", "test_wallet", "--host", settings.mint_url]
|
yield ["--wallet", "test_cli_wallet", "--host", settings.mint_url, "--tests"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def wallet():
|
|
||||||
wallet = Wallet(settings.mint_host, "data/test_wallet", "wallet")
|
|
||||||
asyncio.run(migrate_databases(wallet.db, migrations))
|
|
||||||
asyncio.run(wallet.load_proofs())
|
|
||||||
yield wallet
|
|
||||||
|
|
||||||
|
|
||||||
async def init_wallet():
|
async def init_wallet():
|
||||||
wallet = Wallet(settings.mint_host, "data/test_wallet", "wallet")
|
wallet = await Wallet.with_db(
|
||||||
await migrate_databases(wallet.db, migrations)
|
url=settings.mint_host,
|
||||||
|
db="data/test_cli_wallet",
|
||||||
|
name="wallet",
|
||||||
|
)
|
||||||
await wallet.load_proofs()
|
await wallet.load_proofs()
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
@@ -50,7 +45,7 @@ def test_info_with_mint(cli_prefix):
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
cli,
|
cli,
|
||||||
[*cli_prefix, "info", "-m"],
|
[*cli_prefix, "info", "--mint"],
|
||||||
)
|
)
|
||||||
assert result.exception is None
|
assert result.exception is None
|
||||||
print("INFO -M")
|
print("INFO -M")
|
||||||
@@ -59,6 +54,20 @@ def test_info_with_mint(cli_prefix):
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
def test_info_with_mnemonic(cli_prefix):
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[*cli_prefix, "info", "--mnemonic"],
|
||||||
|
)
|
||||||
|
assert result.exception is None
|
||||||
|
print("INFO -M")
|
||||||
|
print(result.output)
|
||||||
|
assert "Mnemonic" in result.output
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
def test_balance(cli_prefix):
|
def test_balance(cli_prefix):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
@@ -113,7 +122,7 @@ def test_wallets(cli_prefix):
|
|||||||
print("WALLETS")
|
print("WALLETS")
|
||||||
# on github this is empty
|
# on github this is empty
|
||||||
if len(result.output):
|
if len(result.output):
|
||||||
assert "test_wallet" in result.output
|
assert "test_cli_wallet" in result.output
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,11 @@ def test_step1():
|
|||||||
""""""
|
""""""
|
||||||
B_, blinding_factor = step1_alice(
|
B_, blinding_factor = step1_alice(
|
||||||
"test_message",
|
"test_message",
|
||||||
blinding_factor=bytes.fromhex(
|
blinding_factor=PrivateKey(
|
||||||
|
privkey=bytes.fromhex(
|
||||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
), # 32 bytes
|
) # 32 bytes
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
@@ -60,9 +62,12 @@ def test_step1():
|
|||||||
def test_step2():
|
def test_step2():
|
||||||
B_, _ = step1_alice(
|
B_, _ = step1_alice(
|
||||||
"test_message",
|
"test_message",
|
||||||
blinding_factor=bytes.fromhex(
|
blinding_factor=PrivateKey(
|
||||||
|
privkey=bytes.fromhex(
|
||||||
"0000000000000000000000000000000000000000000000000000000000000001"
|
"0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
), # 32 bytes
|
),
|
||||||
|
raw=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
a = PrivateKey(
|
a = PrivateKey(
|
||||||
privkey=bytes.fromhex(
|
privkey=bytes.fromhex(
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import copy
|
import shutil
|
||||||
import secrets
|
import time
|
||||||
from typing import List
|
from pathlib import Path
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
from cashu.core.base import Proof, Secret, SecretKind, Tags
|
from cashu.core.base import Proof, Secret, SecretKind, Tags
|
||||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
||||||
@@ -34,10 +36,20 @@ def assert_amt(proofs: List[Proof], expected: int):
|
|||||||
assert [p.amount for p in proofs] == expected
|
assert [p.amount for p in proofs] == expected
|
||||||
|
|
||||||
|
|
||||||
|
async def reset_wallet_db(wallet: Wallet):
|
||||||
|
await wallet.db.execute("DELETE FROM proofs")
|
||||||
|
await wallet.db.execute("DELETE FROM proofs_used")
|
||||||
|
await wallet.db.execute("DELETE FROM keysets")
|
||||||
|
await wallet._load_mint()
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
async def wallet1(mint):
|
async def wallet1(mint):
|
||||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet1", "wallet1")
|
wallet1 = await Wallet1.with_db(
|
||||||
await migrate_databases(wallet1.db, migrations)
|
url=SERVER_ENDPOINT,
|
||||||
|
db="data/wallet1",
|
||||||
|
name="wallet1",
|
||||||
|
)
|
||||||
await wallet1.load_mint()
|
await wallet1.load_mint()
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
yield wallet1
|
yield wallet1
|
||||||
@@ -45,14 +57,34 @@ async def wallet1(mint):
|
|||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
async def wallet2(mint):
|
async def wallet2(mint):
|
||||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet2", "wallet2")
|
wallet2 = await Wallet2.with_db(
|
||||||
await migrate_databases(wallet2.db, migrations)
|
url=SERVER_ENDPOINT,
|
||||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
db="data/wallet2",
|
||||||
|
name="wallet2",
|
||||||
|
)
|
||||||
await wallet2.load_mint()
|
await wallet2.load_mint()
|
||||||
wallet2.status()
|
wallet2.status()
|
||||||
yield wallet2
|
yield wallet2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def wallet3(mint):
|
||||||
|
dirpath = Path("data/wallet3")
|
||||||
|
if dirpath.exists() and dirpath.is_dir():
|
||||||
|
shutil.rmtree(dirpath)
|
||||||
|
|
||||||
|
wallet3 = await Wallet1.with_db(
|
||||||
|
url=SERVER_ENDPOINT,
|
||||||
|
db="data/wallet3",
|
||||||
|
name="wallet3",
|
||||||
|
)
|
||||||
|
await wallet3.db.execute("DELETE FROM proofs")
|
||||||
|
await wallet3.db.execute("DELETE FROM proofs_used")
|
||||||
|
await wallet3.load_mint()
|
||||||
|
wallet3.status()
|
||||||
|
yield wallet3
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_keys(wallet1: Wallet):
|
async def test_get_keys(wallet1: Wallet):
|
||||||
assert wallet1.keys.public_keys
|
assert wallet1.keys.public_keys
|
||||||
@@ -277,10 +309,283 @@ async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet):
|
|||||||
await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver
|
await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_token_state(wallet1: Wallet):
|
async def test_token_state(wallet1: Wallet):
|
||||||
await wallet1.mint(64)
|
await wallet1.mint(64)
|
||||||
assert wallet1.balance == 64
|
assert wallet1.balance == 64
|
||||||
resp = await wallet1.check_proof_state(wallet1.proofs)
|
resp = await wallet1.check_proof_state(wallet1.proofs)
|
||||||
assert resp.dict()["spendable"]
|
assert resp.dict()["spendable"]
|
||||||
assert resp.dict()["pending"]
|
assert resp.dict()["pending"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_bump_secret_derivation(wallet3: Wallet):
|
||||||
|
await wallet3._init_private_key(
|
||||||
|
"half depart obvious quality work element tank gorilla view sugar picture humble"
|
||||||
|
)
|
||||||
|
secrets1, rs1, derivaion_paths1 = await wallet3.generate_n_secrets(5)
|
||||||
|
secrets2, rs2, derivaion_paths2 = await wallet3.generate_secrets_from_to(0, 4)
|
||||||
|
assert secrets1 == secrets2
|
||||||
|
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
||||||
|
assert derivaion_paths1 == derivaion_paths2
|
||||||
|
assert secrets1 == [
|
||||||
|
"9bfb12704297fe90983907d122838940755fcce370ce51e9e00a4275a347c3fe",
|
||||||
|
"dbc5e05f2b1f24ec0e2ab6e8312d5e13f57ada52594d4caf429a697d9c742490",
|
||||||
|
"06a29fa8081b3a620b50b473fc80cde9a575c3b94358f3513c03007f8b66321e",
|
||||||
|
"652d08c804bd2c5f2c1f3e3d8895860397df394b30473753227d766affd15e89",
|
||||||
|
"654e5997f8a20402f7487296b6f7e463315dd52fc6f6cc5a4e35c7f6ccac77e0",
|
||||||
|
]
|
||||||
|
assert derivaion_paths1 == [
|
||||||
|
"m/129372'/0'/2004500376'/0'",
|
||||||
|
"m/129372'/0'/2004500376'/1'",
|
||||||
|
"m/129372'/0'/2004500376'/2'",
|
||||||
|
"m/129372'/0'/2004500376'/3'",
|
||||||
|
"m/129372'/0'/2004500376'/4'",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bump_secret_derivation_two_steps(wallet3: Wallet):
|
||||||
|
await wallet3._init_private_key(
|
||||||
|
"half depart obvious quality work element tank gorilla view sugar picture humble"
|
||||||
|
)
|
||||||
|
secrets1_1, rs1_1, derivaion_paths1 = await wallet3.generate_n_secrets(2)
|
||||||
|
secrets1_2, rs1_2, derivaion_paths2 = await wallet3.generate_n_secrets(3)
|
||||||
|
secrets1 = secrets1_1 + secrets1_2
|
||||||
|
rs1 = rs1_1 + rs1_2
|
||||||
|
secrets2, rs2, derivaion_paths = await wallet3.generate_secrets_from_to(0, 4)
|
||||||
|
assert secrets1 == secrets2
|
||||||
|
assert [r.private_key for r in rs1] == [r.private_key for r in rs2]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generate_secrets_from_to(wallet3: Wallet):
|
||||||
|
await wallet3._init_private_key(
|
||||||
|
"half depart obvious quality work element tank gorilla view sugar picture humble"
|
||||||
|
)
|
||||||
|
secrets1, rs1, derivaion_paths1 = await wallet3.generate_secrets_from_to(0, 4)
|
||||||
|
assert len(secrets1) == 5
|
||||||
|
secrets2, rs2, derivaion_paths2 = await wallet3.generate_secrets_from_to(2, 4)
|
||||||
|
assert len(secrets2) == 3
|
||||||
|
assert secrets1[2:] == secrets2
|
||||||
|
assert [r.private_key for r in rs1[2:]] == [r.private_key for r in rs2]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restore_wallet_after_mint(wallet3: Wallet):
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.mint(64)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs()
|
||||||
|
wallet3.proofs = []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises(0, 20)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restore_wallet_with_invalid_mnemonic(wallet3: Wallet):
|
||||||
|
await assert_err(
|
||||||
|
wallet3._init_private_key(
|
||||||
|
"half depart obvious quality work element tank gorilla view sugar picture picture"
|
||||||
|
),
|
||||||
|
"Invalid mnemonic",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restore_wallet_after_split_to_send(wallet3: Wallet):
|
||||||
|
await wallet3._init_private_key(
|
||||||
|
"half depart obvious quality work element tank gorilla view sugar picture humble"
|
||||||
|
)
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
|
||||||
|
await wallet3.mint(64)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||||
|
wallet3.proofs, 32, set_reserved=True
|
||||||
|
)
|
||||||
|
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs()
|
||||||
|
wallet3.proofs = []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises(0, 100)
|
||||||
|
assert wallet3.balance == 64 * 2
|
||||||
|
await wallet3.invalidate(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restore_wallet_after_send_and_receive(wallet3: Wallet, wallet2: Wallet):
|
||||||
|
await wallet3._init_private_key(
|
||||||
|
"hello rug want adapt talent together lunar method bean expose beef position"
|
||||||
|
)
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
|
||||||
|
await wallet3.mint(64)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||||
|
wallet3.proofs, 32, set_reserved=True
|
||||||
|
)
|
||||||
|
|
||||||
|
await wallet2.redeem(spendable_proofs)
|
||||||
|
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs(reload=True)
|
||||||
|
assert wallet3.proofs == []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises(0, 100)
|
||||||
|
assert wallet3.balance == 64 + 2 * 32
|
||||||
|
await wallet3.invalidate(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 32
|
||||||
|
|
||||||
|
|
||||||
|
class ProofBox:
|
||||||
|
proofs: Dict[str, Proof] = {}
|
||||||
|
|
||||||
|
def add(self, proofs: List[Proof]) -> None:
|
||||||
|
for proof in proofs:
|
||||||
|
if proof.secret in self.proofs:
|
||||||
|
if self.proofs[proof.secret].C != proof.C:
|
||||||
|
print("Proofs are not equal")
|
||||||
|
print(self.proofs[proof.secret])
|
||||||
|
print(proof)
|
||||||
|
else:
|
||||||
|
self.proofs[proof.secret] = proof
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restore_wallet_after_send_and_self_receive(wallet3: Wallet):
|
||||||
|
await wallet3._init_private_key(
|
||||||
|
"lucky broken tell exhibit shuffle tomato ethics virus rabbit spread measure text"
|
||||||
|
)
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
|
||||||
|
await wallet3.mint(64)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||||
|
wallet3.proofs, 32, set_reserved=True
|
||||||
|
)
|
||||||
|
|
||||||
|
await wallet3.redeem(spendable_proofs)
|
||||||
|
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs(reload=True)
|
||||||
|
assert wallet3.proofs == []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises(0, 100)
|
||||||
|
assert wallet3.balance == 64 + 2 * 32 + 32
|
||||||
|
await wallet3.invalidate(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restore_wallet_after_send_twice(
|
||||||
|
wallet3: Wallet,
|
||||||
|
):
|
||||||
|
box = ProofBox()
|
||||||
|
wallet3.private_key = PrivateKey()
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
|
||||||
|
await wallet3.mint(2)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 2
|
||||||
|
|
||||||
|
keep_proofs, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||||
|
wallet3.proofs, 1, set_reserved=True
|
||||||
|
)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.available_balance == 1
|
||||||
|
await wallet3.redeem(spendable_proofs)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.available_balance == 2
|
||||||
|
assert wallet3.balance == 2
|
||||||
|
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs(reload=True)
|
||||||
|
assert wallet3.proofs == []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises(0, 10)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 5
|
||||||
|
await wallet3.invalidate(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 2
|
||||||
|
|
||||||
|
# again
|
||||||
|
|
||||||
|
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||||
|
wallet3.proofs, 1, set_reserved=True
|
||||||
|
)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
|
||||||
|
assert wallet3.available_balance == 1
|
||||||
|
await wallet3.redeem(spendable_proofs)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.available_balance == 2
|
||||||
|
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs(reload=True)
|
||||||
|
assert wallet3.proofs == []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises(0, 15)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 7
|
||||||
|
await wallet3.invalidate(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
|
||||||
|
wallet3: Wallet,
|
||||||
|
):
|
||||||
|
box = ProofBox()
|
||||||
|
await wallet3._init_private_key(
|
||||||
|
"casual demise flight cradle feature hub link slim remember anger front asthma"
|
||||||
|
)
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
|
||||||
|
await wallet3.mint(64)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
keep_proofs, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||||
|
wallet3.proofs, 10, set_reserved=True
|
||||||
|
)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
|
||||||
|
assert wallet3.available_balance == 64 - 10
|
||||||
|
await wallet3.redeem(spendable_proofs)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.available_balance == 64
|
||||||
|
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs(reload=True)
|
||||||
|
assert wallet3.proofs == []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises(0, 20)
|
||||||
|
box.add(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 138
|
||||||
|
await wallet3.invalidate(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|
||||||
|
# again
|
||||||
|
|
||||||
|
_, spendable_proofs = await wallet3.split_to_send( # type: ignore
|
||||||
|
wallet3.proofs, 12, set_reserved=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert wallet3.available_balance == 64 - 12
|
||||||
|
await wallet3.redeem(spendable_proofs)
|
||||||
|
assert wallet3.available_balance == 64
|
||||||
|
|
||||||
|
await reset_wallet_db(wallet3)
|
||||||
|
await wallet3.load_proofs(reload=True)
|
||||||
|
assert wallet3.proofs == []
|
||||||
|
assert wallet3.balance == 0
|
||||||
|
await wallet3.restore_promises(0, 50)
|
||||||
|
assert wallet3.balance == 182
|
||||||
|
await wallet3.invalidate(wallet3.proofs)
|
||||||
|
assert wallet3.balance == 64
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ from tests.conftest import SERVER_ENDPOINT, mint
|
|||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
async def wallet(mint):
|
async def wallet(mint):
|
||||||
wallet = Wallet(SERVER_ENDPOINT, "data/test_wallet_api", "wallet_api")
|
wallet = await Wallet.with_db(
|
||||||
await migrate_databases(wallet.db, migrations)
|
url=SERVER_ENDPOINT,
|
||||||
|
db="data/test_wallet_api",
|
||||||
|
name="wallet_api",
|
||||||
|
)
|
||||||
await wallet.load_mint()
|
await wallet.load_mint()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
yield wallet
|
yield wallet
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def assert_amt(proofs: List[Proof], expected: int):
|
|||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
async def wallet1(mint):
|
async def wallet1(mint):
|
||||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet_p2pk_1", "wallet1")
|
wallet1 = await Wallet1.with_db(SERVER_ENDPOINT, "data/wallet_p2pk_1", "wallet1")
|
||||||
await migrate_databases(wallet1.db, migrations)
|
await migrate_databases(wallet1.db, migrations)
|
||||||
await wallet1.load_mint()
|
await wallet1.load_mint()
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
@@ -45,7 +45,7 @@ async def wallet1(mint):
|
|||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
async def wallet2(mint):
|
async def wallet2(mint):
|
||||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet_p2pk_2", "wallet2")
|
wallet2 = await Wallet2.with_db(SERVER_ENDPOINT, "data/wallet_p2pk_2", "wallet2")
|
||||||
await migrate_databases(wallet2.db, migrations)
|
await migrate_databases(wallet2.db, migrations)
|
||||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||||
await wallet2.load_mint()
|
await wallet2.load_mint()
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def assert_amt(proofs: List[Proof], expected: int):
|
|||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
async def wallet1(mint):
|
async def wallet1(mint):
|
||||||
wallet1 = Wallet1(SERVER_ENDPOINT, "data/wallet_p2sh_1", "wallet1")
|
wallet1 = await Wallet1.with_db(SERVER_ENDPOINT, "data/wallet_p2sh_1", "wallet1")
|
||||||
await migrate_databases(wallet1.db, migrations)
|
await migrate_databases(wallet1.db, migrations)
|
||||||
await wallet1.load_mint()
|
await wallet1.load_mint()
|
||||||
wallet1.status()
|
wallet1.status()
|
||||||
@@ -45,7 +45,7 @@ async def wallet1(mint):
|
|||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
async def wallet2(mint):
|
async def wallet2(mint):
|
||||||
wallet2 = Wallet2(SERVER_ENDPOINT, "data/wallet_p2sh_2", "wallet2")
|
wallet2 = await Wallet2.with_db(SERVER_ENDPOINT, "data/wallet_p2sh_2", "wallet2")
|
||||||
await migrate_databases(wallet2.db, migrations)
|
await migrate_databases(wallet2.db, migrations)
|
||||||
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
|
||||||
await wallet2.load_mint()
|
await wallet2.load_mint()
|
||||||
|
|||||||
Reference in New Issue
Block a user