Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wallet: add CLI flag --force-swap flag and force swapping all inactive keysets #580

Merged
merged 5 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,31 +222,31 @@ class PostMeltRequest_deprecated(BaseModel):
# ------- API: SPLIT -------


class PostSplitRequest(BaseModel):
class PostSwapRequest(BaseModel):
inputs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
outputs: List[BlindedMessage] = Field(
..., max_items=settings.mint_max_request_length
)


class PostSplitResponse(BaseModel):
class PostSwapResponse(BaseModel):
signatures: List[BlindedSignature]


# deprecated since 0.13.0
class PostSplitRequest_Deprecated(BaseModel):
class PostSwapRequest_Deprecated(BaseModel):
proofs: List[Proof] = Field(..., max_items=settings.mint_max_request_length)
amount: Optional[int] = None
outputs: List[BlindedMessage_Deprecated] = Field(
..., max_items=settings.mint_max_request_length
)


class PostSplitResponse_Deprecated(BaseModel):
class PostSwapResponse_Deprecated(BaseModel):
promises: List[BlindedSignature] = []


class PostSplitResponse_Very_Deprecated(BaseModel):
class PostSwapResponse_Very_Deprecated(BaseModel):
fst: List[BlindedSignature] = []
snd: List[BlindedSignature] = []
deprecated: str = "The amount field is deprecated since 0.13.0"
Expand Down
14 changes: 7 additions & 7 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,28 +961,28 @@ async def melt(

return PostMeltQuoteResponse.from_melt_quote(melt_quote)

async def split(
async def swap(
self,
*,
proofs: List[Proof],
outputs: List[BlindedMessage],
keyset: Optional[MintKeyset] = None,
):
"""Consumes proofs and prepares new promises based on the amount split. Used for splitting tokens
"""Consumes proofs and prepares new promises based on the amount swap. Used for swapping tokens
Before sending or for redeeming tokens for new ones that have been received by another wallet.

Args:
proofs (List[Proof]): Proofs to be invalidated for the split.
proofs (List[Proof]): Proofs to be invalidated for the swap.
outputs (List[BlindedMessage]): New outputs that should be signed in return.
keyset (Optional[MintKeyset], optional): Keyset to use. Uses default keyset if not given. Defaults to None.

Raises:
Exception: Validation of proofs or outputs failed

Returns:
Tuple[List[BlindSignature],List[BlindSignature]]: Promises on both sides of the split.
List[BlindedSignature]: New promises (signatures) for the outputs.
"""
logger.trace("split called")
logger.trace("swap called")
# verify spending inputs, outputs, and spending conditions
await self.verify_inputs_and_outputs(proofs=proofs, outputs=outputs)
await self.db_write._set_proofs_pending(proofs)
Expand All @@ -991,13 +991,13 @@ async def split(
await self._invalidate_proofs(proofs=proofs, conn=conn)
promises = await self._generate_promises(outputs, keyset, conn)
except Exception as e:
logger.trace(f"split failed: {e}")
logger.trace(f"swap failed: {e}")
raise e
finally:
# delete proofs from pending list
await self.db_write._unset_proofs_pending(proofs)

logger.trace("split successful")
logger.trace("swap successful")
return promises

async def restore(
Expand Down
20 changes: 10 additions & 10 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
PostMintResponse,
PostRestoreRequest,
PostRestoreResponse,
PostSplitRequest,
PostSplitResponse,
PostSwapRequest,
PostSwapResponse,
)
from ..core.settings import settings
from ..mint.startup import ledger
Expand Down Expand Up @@ -312,28 +312,28 @@ async def melt(request: Request, payload: PostMeltRequest) -> PostMeltQuoteRespo
"/v1/swap",
name="Swap tokens",
summary="Swap inputs for outputs of the same value",
response_model=PostSplitResponse,
response_model=PostSwapResponse,
response_description=(
"An array of blinded signatures that can be used to create proofs."
),
)
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def swap(
request: Request,
payload: PostSplitRequest,
) -> PostSplitResponse:
payload: PostSwapRequest,
) -> PostSwapResponse:
"""
Requests a set of Proofs to be split into two a new set of BlindedSignatures.
Requests a set of Proofs to be swapped for another set of BlindSignatures.

This endpoint is used by Alice to split a set of proofs before making a payment to Carol.
It is then used by Carol (by setting split=total) to redeem the tokens.
This endpoint can be used by Alice to swap a set of proofs before making a payment to Carol.
It can then used by Carol to redeem the tokens for new proofs.
"""
logger.trace(f"> POST /v1/swap: {payload}")
assert payload.outputs, Exception("no outputs provided.")

signatures = await ledger.split(proofs=payload.inputs, outputs=payload.outputs)
signatures = await ledger.swap(proofs=payload.inputs, outputs=payload.outputs)

return PostSplitResponse(signatures=signatures)
return PostSwapResponse(signatures=signatures)


@router.post(
Expand Down
18 changes: 9 additions & 9 deletions cashu/mint/router_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
PostMintResponse_deprecated,
PostRestoreRequest_Deprecated,
PostRestoreResponse,
PostSplitRequest_Deprecated,
PostSplitResponse_Deprecated,
PostSplitResponse_Very_Deprecated,
PostSwapRequest_Deprecated,
PostSwapResponse_Deprecated,
PostSwapResponse_Very_Deprecated,
)
from ..core.settings import settings
from .limit import limiter
Expand Down Expand Up @@ -270,7 +270,7 @@ async def check_fees(
name="Split",
summary="Split proofs at a specified amount",
# response_model=Union[
# PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated
# PostSwapResponse_Very_Deprecated, PostSwapResponse_Deprecated
# ],
response_description=(
"A list of blinded signatures that can be used to create proofs."
Expand All @@ -280,8 +280,8 @@ async def check_fees(
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def split_deprecated(
request: Request,
payload: PostSplitRequest_Deprecated,
# ) -> Union[PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated]:
payload: PostSwapRequest_Deprecated,
# ) -> Union[PostSwapResponse_Very_Deprecated, PostSwapResponse_Deprecated]:
):
"""
Requests a set of Proofs to be split into two a new set of BlindedSignatures.
Expand All @@ -297,7 +297,7 @@ async def split_deprecated(
for o in payload.outputs
]
# END BACKWARDS COMPATIBILITY < 0.14
promises = await ledger.split(proofs=payload.proofs, outputs=outputs)
promises = await ledger.swap(proofs=payload.proofs, outputs=outputs)

if payload.amount:
# BEGIN backwards compatibility < 0.13
Expand All @@ -319,10 +319,10 @@ async def split_deprecated(
f" {sum([p.amount for p in frst_promises])} sat and send:"
f" {len(scnd_promises)}: {sum([p.amount for p in scnd_promises])} sat"
)
return PostSplitResponse_Very_Deprecated(fst=frst_promises, snd=scnd_promises)
return PostSwapResponse_Very_Deprecated(fst=frst_promises, snd=scnd_promises)
# END backwards compatibility < 0.13
else:
return PostSplitResponse_Deprecated(promises=promises)
return PostSwapResponse_Deprecated(promises=promises)


@router_deprecated.post(
Expand Down
4 changes: 2 additions & 2 deletions cashu/wallet/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ async def swap(
if outgoing_wallet.available_balance < total_amount:
raise Exception("balance too low")

_, send_proofs = await outgoing_wallet.split_to_send(
_, send_proofs = await outgoing_wallet.swap_to_send(
outgoing_wallet.proofs, total_amount, set_reserved=True
)
await outgoing_wallet.melt(
Expand Down Expand Up @@ -433,7 +433,7 @@ async def restore(
if to < 0:
raise Exception("Counter must be positive")
await wallet.load_mint()
await wallet.restore_promises_from_to(0, to)
await wallet.restore_promises_from_to(wallet.keyset_id, 0, to)
await wallet.invalidate(wallet.proofs, check_spendable=True)
return RestoreResponse(balance=wallet.available_balance)

Expand Down
10 changes: 10 additions & 0 deletions cashu/wallet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,14 @@ async def balance(ctx: Context, verbose):
help="Include fees for receiving token.",
type=bool,
)
@click.option(
"--force-swap",
"-s",
default=False,
is_flag=True,
help="Force swap token.",
type=bool,
)
@click.pass_context
@coro
async def send_command(
Expand All @@ -562,6 +570,7 @@ async def send_command(
yes: bool,
offline: bool,
include_fees: bool,
force_swap: bool,
):
wallet: Wallet = ctx.obj["WALLET"]
amount = int(amount * 100) if wallet.unit in [Unit.usd, Unit.eur] else int(amount)
Expand All @@ -575,6 +584,7 @@ async def send_command(
include_dleq=dleq,
include_fees=include_fees,
memo=memo,
force_swap=force_swap,
)
else:
await send_nostr(wallet, amount=amount, pubkey=nostr, verbose=verbose, yes=yes)
Expand Down
6 changes: 3 additions & 3 deletions cashu/wallet/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ async def send(
include_dleq: bool = False,
include_fees: bool = False,
memo: Optional[str] = None,
force_swap: bool = False,
):
"""
Prints token to send to stdout.
Expand Down Expand Up @@ -144,13 +145,12 @@ async def send(
await wallet.load_proofs()

await wallet.load_mint()
if secret_lock:
_, send_proofs = await wallet.split_to_send(
if secret_lock or force_swap:
_, send_proofs = await wallet.swap_to_send(
wallet.proofs,
amount,
set_reserved=False, # we set reserved later
secret_lock=secret_lock,
include_fees=include_fees,
)
else:
send_proofs, fees = await wallet.select_to_send(
Expand Down
2 changes: 1 addition & 1 deletion cashu/wallet/lightning/lightning.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async def pay_invoice(self, pr: str) -> PaymentResponse:
if self.available_balance < total_amount:
print("Error: Balance too low.")
return PaymentResponse(ok=False)
_, send_proofs = await self.split_to_send(self.proofs, total_amount)
_, send_proofs = await self.swap_to_send(self.proofs, total_amount)
try:
resp = await self.melt(send_proofs, pr, quote.fee_reserve, quote.quote)
if resp.change:
Expand Down
2 changes: 1 addition & 1 deletion cashu/wallet/nostr.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async def send_nostr(
pubkey = await nip5_to_pubkey(wallet, pubkey)
await wallet.load_mint()
await wallet.load_proofs()
_, send_proofs = await wallet.split_to_send(
_, send_proofs = await wallet.swap_to_send(
wallet.proofs, amount, set_reserved=True, include_fees=False
)
token = await wallet.serialize_proofs(send_proofs, include_dleq=include_dleq)
Expand Down
1 change: 1 addition & 0 deletions cashu/wallet/proofs.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ async def serialize_proofs(
try:
_ = [bytes.fromhex(p.id) for p in proofs]
except ValueError:
logger.debug("Proof with base64 keyset, using legacy token serialization")
legacy = True

if legacy:
Expand Down
15 changes: 9 additions & 6 deletions cashu/wallet/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,27 +105,28 @@ async def _generate_random_secret(self) -> str:
return hashlib.sha256(os.urandom(32)).hexdigest()

async def generate_determinstic_secret(
self, counter: int
self, counter: int, keyset_id: Optional[str] = None
) -> Tuple[bytes, bytes, str]:
"""
Determinstically generates two secrets (one as the secret message,
one as the blinding factor).
"""
assert self.bip32, "BIP32 not initialized yet."
keyset_id = keyset_id or self.keyset_id
# integer keyset id modulo max number of bip32 child keys
try:
keyest_id_int = int.from_bytes(bytes.fromhex(self.keyset_id), "big") % (
keyest_id_int = int.from_bytes(bytes.fromhex(keyset_id), "big") % (
2**31 - 1
)
except ValueError:
# BEGIN: BACKWARDS COMPATIBILITY < 0.15.0 keyset id is not hex
# calculate an integer keyset id from the base64 encoded keyset id
keyest_id_int = int.from_bytes(base64.b64decode(self.keyset_id), "big") % (
keyest_id_int = int.from_bytes(base64.b64decode(keyset_id), "big") % (
2**31 - 1
)
# END: BACKWARDS COMPATIBILITY < 0.15.0 keyset id is not hex

logger.trace(f"keyset id: {self.keyset_id} becomes {keyest_id_int}")
logger.trace(f"keyset id: {keyset_id} becomes {keyest_id_int}")
token_derivation_path = f"m/129372'/0'/{keyest_id_int}'/{counter}'"
# for secret
secret_derivation_path = f"{token_derivation_path}/0"
Expand Down Expand Up @@ -177,13 +178,14 @@ async def generate_n_secrets(
return secrets, rs, derivation_paths

async def generate_secrets_from_to(
self, from_counter: int, to_counter: int
self, from_counter: int, to_counter: int, keyset_id: Optional[str] = None
) -> Tuple[List[str], List[PrivateKey], List[str]]:
"""Generates secrets and blinding factors from `from_counter` to `to_counter`

Args:
from_counter (int): Start counter
to_counter (int): End counter
keyset_id (Optional[str], optional): Keyset id. Defaults to None.

Returns:
Tuple[List[str], List[PrivateKey], List[str]]: Secrets, blinding factors, derivation paths
Expand All @@ -196,7 +198,8 @@ async def generate_secrets_from_to(
), "from_counter must be smaller than to_counter"
secret_counters = [c for c in range(from_counter, to_counter + 1)]
secrets_rs_derivationpaths = [
await self.generate_determinstic_secret(s) for s in secret_counters
await self.generate_determinstic_secret(s, keyset_id)
for s in secret_counters
]
# secrets are supplied as str
secrets = [s[0].hex() for s in secrets_rs_derivationpaths]
Expand Down
Loading
Loading