mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
* 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
220 lines
7.2 KiB
Python
220 lines
7.2 KiB
Python
import base64
|
|
import json
|
|
import os
|
|
|
|
from loguru import logger
|
|
|
|
from ..core.base import TokenV1, TokenV2, TokenV3, TokenV3Token
|
|
from ..core.db import Database
|
|
from ..core.helpers import sum_proofs
|
|
from ..core.migrations import migrate_databases
|
|
from ..core.settings import settings
|
|
from ..wallet import migrations
|
|
from ..wallet.crud import get_keyset
|
|
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):
|
|
"""Performs migrations and loads proofs from db."""
|
|
await wallet._migrate_database()
|
|
await wallet._init_private_key()
|
|
if load_proofs:
|
|
await wallet.load_proofs(reload=True)
|
|
|
|
|
|
async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
|
|
"""
|
|
Helper function to iterate thruogh a token with multiple mints and redeem them from
|
|
these mints one keyset at a time.
|
|
"""
|
|
for t in token.token:
|
|
assert t.mint, Exception(
|
|
"redeem_TokenV3_multimint: multimint redeem without URL"
|
|
)
|
|
mint_wallet = await Wallet.with_db(
|
|
t.mint, os.path.join(settings.cashu_dir, wallet.name)
|
|
)
|
|
keysets = mint_wallet._get_proofs_keysets(t.proofs)
|
|
logger.debug(f"Keysets in tokens: {keysets}")
|
|
# loop over all keysets
|
|
for keyset in set(keysets):
|
|
await mint_wallet.load_mint()
|
|
# redeem proofs of this keyset
|
|
redeem_proofs = [p for p in t.proofs if p.id == keyset]
|
|
_, _ = await mint_wallet.redeem(redeem_proofs)
|
|
print(f"Received {sum_proofs(redeem_proofs)} sats")
|
|
|
|
|
|
def serialize_TokenV2_to_TokenV3(tokenv2: TokenV2):
|
|
"""Helper function to receive legacy TokenV2 tokens.
|
|
Takes a list of proofs and constructs a *serialized* TokenV3 to be received through
|
|
the ordinary path.
|
|
|
|
Returns:
|
|
TokenV3: TokenV3
|
|
"""
|
|
tokenv3 = TokenV3(token=[TokenV3Token(proofs=tokenv2.proofs)])
|
|
if tokenv2.mints:
|
|
tokenv3.token[0].mint = tokenv2.mints[0].url
|
|
token_serialized = tokenv3.serialize()
|
|
return token_serialized
|
|
|
|
|
|
def serialize_TokenV1_to_TokenV3(tokenv1: TokenV1):
|
|
"""Helper function to receive legacy TokenV1 tokens.
|
|
Takes a list of proofs and constructs a *serialized* TokenV3 to be received through
|
|
the ordinary path.
|
|
|
|
Returns:
|
|
TokenV3: TokenV3
|
|
"""
|
|
tokenv3 = TokenV3(token=[TokenV3Token(proofs=tokenv1.__root__)])
|
|
token_serialized = tokenv3.serialize()
|
|
return token_serialized
|
|
|
|
|
|
def deserialize_token_from_string(token: str) -> TokenV3:
|
|
# deserialize token
|
|
|
|
# ----- backwards compatibility -----
|
|
|
|
# V2Tokens (0.7-0.11.0) (eyJwcm9...)
|
|
if token.startswith("eyJwcm9"):
|
|
try:
|
|
tokenv2 = TokenV2.parse_obj(json.loads(base64.urlsafe_b64decode(token)))
|
|
token = serialize_TokenV2_to_TokenV3(tokenv2)
|
|
except:
|
|
pass
|
|
|
|
# V1Tokens (<0.7) (W3siaWQ...)
|
|
if token.startswith("W3siaWQ"):
|
|
try:
|
|
tokenv1 = TokenV1.parse_obj(json.loads(base64.urlsafe_b64decode(token)))
|
|
token = serialize_TokenV1_to_TokenV3(tokenv1)
|
|
except:
|
|
pass
|
|
|
|
# ----- receive token -----
|
|
|
|
# deserialize token
|
|
# dtoken = json.loads(base64.urlsafe_b64decode(token))
|
|
tokenObj = TokenV3.deserialize(token)
|
|
|
|
# tokenObj = TokenV2.parse_obj(dtoken)
|
|
assert len(tokenObj.token), Exception("no proofs in token")
|
|
assert len(tokenObj.token[0].proofs), Exception("no proofs in token")
|
|
return tokenObj
|
|
|
|
|
|
async def receive(
|
|
wallet: Wallet,
|
|
tokenObj: TokenV3,
|
|
):
|
|
logger.debug(f"receive: {tokenObj}")
|
|
proofs = [p for t in tokenObj.token for p in t.proofs]
|
|
|
|
includes_mint_info: bool = any([t.mint for t in tokenObj.token])
|
|
|
|
if includes_mint_info:
|
|
# redeem tokens with new wallet instances
|
|
await redeem_TokenV3_multimint(
|
|
wallet,
|
|
tokenObj,
|
|
)
|
|
else:
|
|
# this is very legacy code, virtually any token should have mint information
|
|
# no mint information present, we extract the proofs and use wallet's default mint
|
|
# first we load the mint URL from the DB
|
|
keyset_in_token = proofs[0].id
|
|
assert keyset_in_token
|
|
# we get the keyset from the db
|
|
mint_keysets = await get_keyset(id=keyset_in_token, db=wallet.db)
|
|
assert mint_keysets, Exception("we don't know this keyset")
|
|
assert mint_keysets.mint_url, Exception("we don't know this mint's URL")
|
|
# now we have the URL
|
|
mint_wallet = await Wallet.with_db(
|
|
mint_keysets.mint_url,
|
|
os.path.join(settings.cashu_dir, wallet.name),
|
|
)
|
|
await mint_wallet.load_mint(keyset_in_token)
|
|
_, _ = await mint_wallet.redeem(proofs)
|
|
print(f"Received {sum_proofs(proofs)} sats")
|
|
|
|
# reload main wallet so the balance updates
|
|
await wallet.load_proofs(reload=True)
|
|
wallet.status()
|
|
return wallet.available_balance
|
|
|
|
|
|
async def send(
|
|
wallet: Wallet, amount: int, lock: str, legacy: bool, split: bool = True
|
|
):
|
|
"""
|
|
Prints token to send to stdout.
|
|
"""
|
|
secret_lock = None
|
|
if lock:
|
|
assert len(lock) > 21, Exception(
|
|
"Error: lock has to be at least 22 characters long."
|
|
)
|
|
if not lock.startswith("P2SH:") and not lock.startswith("P2PK:"):
|
|
raise Exception("Error: lock has to start with P2SH: or P2PK:")
|
|
# we add a time lock to the P2PK lock by appending the current unix time + 14 days
|
|
if lock.startswith("P2PK:") or lock.startswith("P2SH:"):
|
|
logger.debug(f"Locking token to: {lock}")
|
|
logger.debug(
|
|
f"Adding a time lock of {settings.locktime_delta_seconds} seconds."
|
|
)
|
|
if lock.startswith("P2SH:"):
|
|
secret_lock = await wallet.create_p2sh_lock(
|
|
lock.split(":")[1], locktime=settings.locktime_delta_seconds
|
|
)
|
|
elif lock.startswith("P2PK:"):
|
|
secret_lock = await wallet.create_p2pk_lock(
|
|
lock.split(":")[1],
|
|
locktime_seconds=settings.locktime_delta_seconds,
|
|
sig_all=True,
|
|
n_sigs=1,
|
|
)
|
|
|
|
await wallet.load_proofs()
|
|
if split:
|
|
await wallet.load_mint()
|
|
_, send_proofs = await wallet.split_to_send(
|
|
wallet.proofs, amount, secret_lock, set_reserved=True
|
|
)
|
|
else:
|
|
# get a proof with specific amount
|
|
send_proofs = []
|
|
for p in wallet.proofs:
|
|
if not p.reserved and p.amount == amount:
|
|
send_proofs = [p]
|
|
break
|
|
assert send_proofs, Exception(
|
|
f"No proof with this amount found. Available amounts: {set([p.amount for p in wallet.proofs])}"
|
|
)
|
|
await wallet.set_reserved(send_proofs, reserved=True)
|
|
|
|
token = await wallet.serialize_proofs(
|
|
send_proofs,
|
|
include_mints=True,
|
|
)
|
|
print(token)
|
|
|
|
if legacy:
|
|
print("")
|
|
print("Old token format:")
|
|
print("")
|
|
token = await wallet.serialize_proofs(
|
|
send_proofs,
|
|
legacy=True,
|
|
)
|
|
print(token)
|
|
|
|
wallet.status()
|
|
return wallet.available_balance, token
|