Summary
After importing key images from an air-gapped signer (tested with both Cupcake and a third-party signer), Cake Wallet immediately shows "unexpected error: Key image not found" when attempting to create the transaction. Closing the send flow and retrying the send works on the second attempt without needing another sync.
Steps to Reproduce
- Set up a view-only XMR wallet in Cake Wallet paired with an air-gapped signer (Cupcake or other)
- Receive XMR to the wallet, wait for confirmations
- Initiate a Send in Cake Wallet
- Cake shows "Outputs (all)" / "Outputs (partial)" toggle and displays the outputs QR
- Scan the outputs QR with the air-gapped signer
- The signer displays key images QR
- Scan the key images QR with Cake Wallet
- Result: "Transaction Error: unexpected error: Key image not found"
Workaround: Close the error, close the entire send flow, tap Send again. The second attempt skips the outputs sync (key images are now present) and proceeds directly to transaction creation, which succeeds.
Root Cause Analysis
The issue is a stale unspentCoins cache in Cake's send flow:
-
Before the sync flow: unspentCoins is populated via updateUnspent() → CoinsImpl::refresh(). For a view-only wallet, each transfer's m_key_image is a locally-computed placeholder (not the real key image from the signer), with m_key_image_known = false. These placeholder key images are stored as hex strings in unspentCoins.
-
After key image import: importKeyImagesUR calls wallet2::import_key_images which overwrites m_transfers[n].m_key_image with the real key images and sets m_key_image_known = true. The import succeeds.
-
Transaction creation: createTransaction() checks if (unspentCoins.isEmpty) — but unspentCoins is not empty (populated in step 1), so updateUnspent() is not called. The stale placeholder key image hex strings are passed as preferredInputs to the C FFI.
-
In C++ createTransactionMultDest (coin-control patch): For each preferred input, frozen(keyImage) → get_transfer_details(ki) iterates m_transfers looking for a matching td.m_key_image. The stale placeholder key images no longer match any transfer (they were overwritten in step 2), so CHECK_AND_ASSERT_THROW_MES(false, "Key image not found") is thrown.
-
On the second attempt: The failure path clears/invalidates unspentCoins, so updateUnspent() runs fresh with the real key images. The transaction succeeds.
Suggested Fix
After importKeyImagesUR returns successfully, clear or refresh unspentCoins so that subsequent createTransaction calls use the real key images:
// In the UR page handler after importing key images:
final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur);
if (result) {
// Refresh the coin cache with real key images
await monero!.updateUnspent(animatedURmodel.wallet);
Navigator.of(context).pop(true);
}
Alternatively, createTransaction could always call updateUnspent() rather than conditionally checking unspentCoins.isEmpty.
Environment
- Cake Wallet v6.0.2 (latest, iOS)
- Tested with both Cupcake v1.1.0 (macOS) and a third-party air-gapped signer
- Monero mainnet
- Same bug occurs with both "Outputs (all)" and "Outputs (partial)" modes
- Same bug occurs with both "all key images" and "partial key images" from the signer
Summary
After importing key images from an air-gapped signer (tested with both Cupcake and a third-party signer), Cake Wallet immediately shows "unexpected error: Key image not found" when attempting to create the transaction. Closing the send flow and retrying the send works on the second attempt without needing another sync.
Steps to Reproduce
Workaround: Close the error, close the entire send flow, tap Send again. The second attempt skips the outputs sync (key images are now present) and proceeds directly to transaction creation, which succeeds.
Root Cause Analysis
The issue is a stale
unspentCoinscache in Cake's send flow:Before the sync flow:
unspentCoinsis populated viaupdateUnspent()→CoinsImpl::refresh(). For a view-only wallet, each transfer'sm_key_imageis a locally-computed placeholder (not the real key image from the signer), withm_key_image_known = false. These placeholder key images are stored as hex strings inunspentCoins.After key image import:
importKeyImagesURcallswallet2::import_key_imageswhich overwritesm_transfers[n].m_key_imagewith the real key images and setsm_key_image_known = true. The import succeeds.Transaction creation:
createTransaction()checksif (unspentCoins.isEmpty)— butunspentCoinsis not empty (populated in step 1), soupdateUnspent()is not called. The stale placeholder key image hex strings are passed aspreferredInputsto the C FFI.In C++
createTransactionMultDest(coin-control patch): For each preferred input,frozen(keyImage)→get_transfer_details(ki)iteratesm_transferslooking for a matchingtd.m_key_image. The stale placeholder key images no longer match any transfer (they were overwritten in step 2), soCHECK_AND_ASSERT_THROW_MES(false, "Key image not found")is thrown.On the second attempt: The failure path clears/invalidates
unspentCoins, soupdateUnspent()runs fresh with the real key images. The transaction succeeds.Suggested Fix
After
importKeyImagesURreturns successfully, clear or refreshunspentCoinsso that subsequentcreateTransactioncalls use the real key images:Alternatively,
createTransactioncould always callupdateUnspent()rather than conditionally checkingunspentCoins.isEmpty.Environment