Skip to content

Conversation

@dmnyc
Copy link
Contributor

@dmnyc dmnyc commented Jan 6, 2026

Summary

Fixes cross-signer wallet restore compatibility. Backups created with one signer (e.g., Chrome/Alby) were failing to decrypt when restored with a different signer (e.g., iOS Safari with Nostr Build Shack).

Problem

The unified encryption service was trying NDK signer decrypt methods before window.nostr, but some signers behave differently through their NDK interface vs direct window.nostr calls.

Solution

  • Reorder decrypt to try window.nostr first (the original working method)
  • Fall back to NDK signer for NIP-46 remote signers
  • Add automatic fallback between NIP-44 and NIP-04 if primary method fails

Test plan

  • Backup wallet on Chrome with Alby extension
  • Restore wallet on iOS Safari with different NIP-07 signer
  • Verify NIP-46 restore still works as fallback

- Reorder decrypt to try window.nostr first (original working method)
  - Fall back to NDK signer for NIP-46 remote signers
  - Add fallback between NIP-44 and NIP-04 encryption methods
  - Fixes cross-signer restore (Chrome/Alby backup → iOS Safari restore)
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes cross-signer wallet restore compatibility by reordering decryption method priority and adding automatic fallback between encryption methods. Backups created with one NIP-07 signer (e.g., Chrome/Alby) were failing to decrypt when restored with a different signer (e.g., iOS Safari with Nostr Build Shack).

  • Prioritizes window.nostr decrypt over NDK signer methods for better cross-signer compatibility
  • Adds automatic fallback between NIP-44 and NIP-04 encryption methods
  • Implements iterative retry logic with error handling for multiple decryption approaches

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +173 to +174
const result = await signer.nip44Decrypt(sender, ciphertext);
if (result) return result;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return when result is truthy could silently skip errors. If the decrypt function returns an empty string (which is falsy in JavaScript), it will fall through to try other methods even though the decryption technically succeeded. This could lead to unexpected behavior where empty encrypted values are not properly handled. Consider checking for undefined/null explicitly instead of relying on truthiness.

Copilot uses AI. Check for mistakes.
} catch (e) {
console.warn(`[Encryption] ${tryMethod} decryption failed, trying next method...`, e);
lastError = e instanceof Error ? e : new Error(String(e));
// Continue to next method
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The continue statement is unnecessary as it's the last statement in the catch block. The loop will naturally proceed to the next iteration. Removing it would improve code clarity.

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +169
const ndkInstance = get(ndk);
const signer = ndkInstance.signer;
if (signer) {
const sender = new NDKUser({ pubkey: senderPubkey });
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NDKUser and get(ndk) are instantiated on every iteration of the methods loop, even though they're the same for all attempts. This is inefficient and could be moved outside the loop to avoid unnecessary object creation. The ndkInstance and sender should be created once before the loop begins.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +150 to +153
for (const tryMethod of methods) {
try {
// First try window.nostr (NIP-07) - this is the original working method
const nostr = (window as any).nostr;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The window.nostr reference is retrieved on every iteration of the methods loop using (window as any).nostr. This should be retrieved once before the loop to improve performance and code clarity.

Suggested change
for (const tryMethod of methods) {
try {
// First try window.nostr (NIP-07) - this is the original working method
const nostr = (window as any).nostr;
const nostr = (window as any).nostr;
for (const tryMethod of methods) {
try {
// First try window.nostr (NIP-07) - this is the original working method

Copilot uses AI. Check for mistakes.
Comment on lines +155 to +182
console.log('[Encryption] Trying window.nostr.nip44.decrypt...');
const result = await nostr.nip44.decrypt(senderPubkey, ciphertext);
if (result) return result;
}
if (tryMethod === 'nip04' && nostr?.nip04?.decrypt) {
console.log('[Encryption] Trying window.nostr.nip04.decrypt...');
const result = await nostr.nip04.decrypt(senderPubkey, ciphertext);
if (result) return result;
}

// Fallback to NDK signer (for NIP-46 remote signers, etc.)
const ndkInstance = get(ndk);
const signer = ndkInstance.signer;
if (signer) {
const sender = new NDKUser({ pubkey: senderPubkey });

if (tryMethod === 'nip44' && typeof signer.nip44Decrypt === 'function') {
console.log('[Encryption] Trying NDK signer.nip44Decrypt...');
const result = await signer.nip44Decrypt(sender, ciphertext);
if (result) return result;
} else if (tryMethod === 'nip04' && typeof signer.nip04Decrypt === 'function') {
console.log('[Encryption] Trying NDK signer.nip04Decrypt...');
const result = await signer.nip04Decrypt(sender, ciphertext);
if (result) return result;
}
}
} catch (e) {
console.warn(`[Encryption] ${tryMethod} decryption failed, trying next method...`, e);
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console logs should be removed or replaced with a proper logging mechanism before production. Debug logs like these should typically be behind a debug flag or removed entirely, as they can expose sensitive information about encryption attempts and clutter production logs.

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +157
const result = await nostr.nip44.decrypt(senderPubkey, ciphertext);
if (result) return result;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return when result is truthy could silently skip errors. If the decrypt function returns an empty string (which is falsy in JavaScript), it will fall through to try other methods even though the decryption technically succeeded. This could lead to unexpected behavior where empty encrypted values are not properly handled. Consider checking for undefined/null explicitly instead of relying on truthiness.

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +162
const result = await nostr.nip04.decrypt(senderPubkey, ciphertext);
if (result) return result;
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return when result is truthy could silently skip errors. If the decrypt function returns an empty string (which is falsy in JavaScript), it will fall through to try other methods even though the decryption technically succeeded. This could lead to unexpected behavior where empty encrypted values are not properly handled. Consider checking for undefined/null explicitly instead of relying on truthiness.

Copilot uses AI. Check for mistakes.
@spe1020 spe1020 merged commit fa17e36 into zapcooking:main Jan 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants