Reverse conversion: convert any sNight amount the wallet holds - #15
Merged
Conversation
added 3 commits
September 8, 2026 21:46
convertToUnshielded claims its coin as an output addressed to the contract, so the wallet funds it with ordinary shielded coin selection. The new integration test reverses half of a minted coin, then the wallet's own change coin, then the merged value of two separately minted coins - each with a fresh random nonce - and shows the only failure left is insufficient sNight.
The reverse swap no longer requires a coin this browser minted and kept. It builds a fresh-nonce coin for the requested amount, checks that amount against the wallet's sNight balance first, and lets the wallet fund the contract-owned output by its own coin selection. The swap card enables from the wallet total and the balance panel no longer raises an anomaly when the wallet holds less than this browser minted. Docs and the security-suite comments now describe the real requirement: enough sNight, not an exact coin.
The swap card now compares the amount against the wallet's sNight balance and reports the limit in sNight rather than base units. The adapter keeps the authoritative check for a stale balance, but runs it before the step and log callbacks so a locally rejected amount no longer leaves 'approve in wallet' in the activity log, and its message states that its numbers are base units.
Contributor
Author
|
Served-build verification of
Preview deployment of this commit for a Lace test: https://preview-00010.shielded-night.pages.dev (immutable https://2bcc8c70.shielded-night.pages.dev). Note (pre-existing, not introduced here): when the stub's |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
sNight → NIGHT conversion no longer requires "one exact sNight coin minted and retained by this browser". Any amount up to the wallet's sNight balance converts in one wallet approval, including sNight received from another wallet or minted in another browser or origin.
Why the old gate existed, and why it was wrong
Audit 00003 (frontend, finding F1) asserted that
receiveShielded(coin)"requires the wallet to spend the exact existing coin commitment", citing the security suite's "a fabricated nonce cannot balance" vector. That reading is incorrect:receiveadds a validation condition that the coin is present as an output addressed to this contract. The compiled circuit (src/managed/contract/index.js_receiveShielded_0) calls_createZswapOutput_0(coin, contractAddress); compact-runtime appends a contract-owned output to the local zswap state and midnight-js emitsZswapOutput.newContractOwned(coinInfo, …)in the unproven offer. Ledger v8 documentsclaimedShieldedReceivesas "coin commitments (outputs) this contract call requires".shielded-night.security.test.tsis explained by an insufficient sNight balance at that point in the test, not by the nonce.On-chain proof (commit 1)
test/integration/shielded-night.reverse-any-amount.test.ts, run on the repository's local docker stack (midnight-node 1.0.0, indexer-standalone 4.3.3, proof-server 8.1.0) with retries disabled — passed first attempt, 323 s:convertToShieldedWallet.InsufficientFunds: Insufficient funds; balances unchangedThe v2 (Stagenet) contract has the identical construction (
contracts/v2/managed/contract/index.js_receiveShielded_0→_createZswapOutput_0); no 2.x local stack exists in this repo, so that evidence is static.App change (commit 2, plus a follow-up commit for message formatting)
frontend/protocols/shared/adapter-core.ts: the reverse path checks the requested amount againstgetShieldedBalances()first, then builds{ nonce: random 32 bytes, color: wrapper, value: amount }and callsconvertToUnshielded. The browser coin store is not consulted; an uncertain submission surfaces the transaction id with a warning that a blind retry converts more sNight.SwapCard.tsx: enabled from the wallet total, Max = wallet total, "Wallet total: X sNight." replaces the exact-coin text; over-limit amounts are rejected with a formatted message before any wallet interaction.BalancePanel.tsx: the anomaly that fired when the wallet held less than this browser had minted is removed (it would misfire after every reverse).frontend/README.md"Reverse coin limitation" rewritten;TESTING.mdsharp-edge corrected; misleading comments in the security suite fixed (assertions unchanged).node:24.15.0-bookworm, bun 1.4.2, all four installs, typecheck, build, root unit tests) exit 0; chunk hashes identical to the host build.Served-build check (stub wallet, no extension available here)
With a stub connector reporting 5 sNight on Preprod: the reverse form enables with "Wallet total: 5 sNight.", Max fills 5, 7 is rejected locally with the wallet total, and 2 reaches the circuit call (failed only at the stub's proving provider). Real-wallet (Lace) acceptance: preview deployment noted in the PR conversation.
Not a breaking change: contract sources, generated artifacts,
frontend/.envand workflows are untouched; the reverse path strictly widens what is accepted. Merging auto-deploys production throughdeploy.ymlonce push CI is green (CI's integration job runs the new test).