Skip to content
Open
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
38 changes: 37 additions & 1 deletion sixty_nuts/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,43 @@ async def roll_over_proofs(
unspent_proofs: list[Proof],
deleted_event_ids: list[str],
) -> str:
return "" # TODO: Implement roll over logic for wallet cleanup
"""Roll over unspent proofs after a partial spend and return new token id.

This creates a new token event containing only the unspent proofs and marks
the old token event as superseded using the del field. Critical for proper
NIP-60 state transitions and preventing double-spending.

Args:
spent_proofs: List of proofs that were spent (not used in rollover)
unspent_proofs: List of proofs to roll over to new event
deleted_event_ids: List of old event IDs to mark as superseded

Returns:
New token event ID containing the rolled over proofs

Raises:
WalletError: If no unspent proofs provided or event publishing fails
"""
if not unspent_proofs:
raise WalletError("No unspent proofs to roll over")

if not deleted_event_ids:
raise WalletError("No deleted event IDs provided for rollover")

try:
# Use EventManager to publish new token event with rollover
event_id = await self.event_manager.publish_token_event(
unspent_proofs,
deleted_token_ids=deleted_event_ids,
)

if not event_id:
raise WalletError("Failed to publish rollover token event")

return event_id

except Exception as e:
raise WalletError(f"Failed to roll over proofs: {e}") from e

async def _consolidate_proofs(
self, proofs: list[Proof], target_mint: str | None = None
Expand Down
98 changes: 98 additions & 0 deletions tests/unit/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,3 +417,101 @@ def test_raise_if_insufficient_balance_insufficient(self) -> None:

with pytest.raises(WalletError, match="Insufficient balance"):
wallet.raise_if_insufficient_balance(0, 1)


class TestWalletRolloverProofs:
"""Test roll_over_proofs functionality."""

@pytest.mark.asyncio
async def test_roll_over_proofs_success(self) -> None:
"""Test successful proof rollover."""
wallet = Wallet(
nsec=generate_privkey(),
mint_urls=["http://test.mint"],
relay_urls=["ws://test.relay"],
)

spent_proofs: list[Proof] = [
{
"id": "test1",
"amount": 10,
"secret": "secret1",
"C": "C1",
"mint": "http://test.mint",
"unit": "sat",
}
]

unspent_proofs: list[Proof] = [
{
"id": "test2",
"amount": 20,
"secret": "secret2",
"C": "C2",
"mint": "http://test.mint",
"unit": "sat",
}
]

deleted_event_ids = ["event1", "event2"]

with patch.object(wallet.event_manager, 'publish_token_event', new_callable=AsyncMock) as mock_publish:
mock_publish.return_value = "new_event_id"

result = await wallet.roll_over_proofs(
spent_proofs=spent_proofs,
unspent_proofs=unspent_proofs,
deleted_event_ids=deleted_event_ids,
)

assert result == "new_event_id"
mock_publish.assert_called_once_with(
unspent_proofs, deleted_token_ids=deleted_event_ids
)

@pytest.mark.asyncio
async def test_roll_over_proofs_no_unspent(self) -> None:
"""Test rollover with no unspent proofs raises error."""
from sixty_nuts.types import WalletError

wallet = Wallet(
nsec=generate_privkey(),
mint_urls=["http://test.mint"],
relay_urls=["ws://test.relay"],
)

with pytest.raises(WalletError, match="No unspent proofs to roll over"):
await wallet.roll_over_proofs(
spent_proofs=[],
unspent_proofs=[],
deleted_event_ids=["event1"],
)

@pytest.mark.asyncio
async def test_roll_over_proofs_no_deleted_events(self) -> None:
"""Test rollover with no deleted event IDs raises error."""
from sixty_nuts.types import WalletError

wallet = Wallet(
nsec=generate_privkey(),
mint_urls=["http://test.mint"],
relay_urls=["ws://test.relay"],
)

unspent_proofs: list[Proof] = [
{
"id": "test1",
"amount": 10,
"secret": "secret1",
"C": "C1",
"mint": "http://test.mint",
"unit": "sat",
}
]

with pytest.raises(WalletError, match="No deleted event IDs provided for rollover"):
await wallet.roll_over_proofs(
spent_proofs=[],
unspent_proofs=unspent_proofs,
deleted_event_ids=[],
)