From d552e9017c4bbaa27f4c382c4b6eb00b9c00df0d Mon Sep 17 00:00:00 2001
From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
Date: Sun, 21 Jun 2026 17:57:39 -0400
Subject: [PATCH] feat(sync): NWC wallet sync (opt-in, spend-gated) + shared
opt-in flags
Adds encrypted cross-login sync for the NWC wallet connection (todo #15),
reusing the encrypted-sync core under d-tag "bitvid:nwc".
- js/services/settingsSyncFlags.js: shared per-pubkey opt-in flag store
(storage/wallet); storageSyncService refactored onto it (identical behavior,
tests still green).
- js/services/walletSyncService.js: factory (NwcSettingsService is per-app, not a
singleton). push() syncs the current nwcUri (+defaultZap); pull() applies it via
updateActiveNwcSettings; disable() wipes the note.
- Wallet pane (components/profile-modal.html + ProfileWalletController): opt-in
toggle + Restore button + a strong SPENDING warning, shown only when a signer
can encrypt. Enabling requires an explicit window.confirm (the URI can spend).
Re-pushes on save when enabled; and on wallet DISCONNECT while synced it clears
the note so a removed spending URI doesn't linger (encrypted) on relays.
tests/wallet-sync-service.test.mjs: enable-pushes-uri, pull-restores-via-
updateActiveNwcSettings, disable-clears, no-wallet-doesn't-publish. Mutation-
verified (skip-apply + skip-clear both fail).
Co-Authored-By: Claude Opus 4.8
---
components/profile-modal.html | 27 +++
js/services/settingsSyncFlags.js | 58 ++++++
js/services/storageSyncService.js | 46 +----
js/services/walletSyncService.js | 111 +++++++++++
js/ui/profileModal/ProfileWalletController.js | 179 ++++++++++++++++++
tests/wallet-sync-service.test.mjs | 125 ++++++++++++
6 files changed, 504 insertions(+), 42 deletions(-)
create mode 100644 js/services/settingsSyncFlags.js
create mode 100644 js/services/walletSyncService.js
create mode 100644 tests/wallet-sync-service.test.mjs
diff --git a/components/profile-modal.html b/components/profile-modal.html
index 6b50607a..51421cdf 100644
--- a/components/profile-modal.html
+++ b/components/profile-modal.html
@@ -836,6 +836,33 @@
role="status"
aria-live="polite"
>
+
+
+
+
Cross-device sync
+
+ Save an encrypted copy of your wallet
+ connection to your Nostr account so you can use it on other
+ devices without re-pasting the URI.
+
+
+
+
+ Spending warning: a wallet connect URI can spend
+ from your wallet. The encrypted copy lives on public relays —
+ anyone who controls your Nostr key could decrypt it and spend
+ from this wallet. Only enable this if you accept that risk.
+
+
+
+
+
+
diff --git a/js/services/settingsSyncFlags.js b/js/services/settingsSyncFlags.js
new file mode 100644
index 00000000..ad89a060
--- /dev/null
+++ b/js/services/settingsSyncFlags.js
@@ -0,0 +1,58 @@
+// Per-pubkey opt-in flags for encrypted settings sync (todo #15). Shared by the
+// storage and wallet sync services so the toggle state persists locally.
+// Shape: { [pubkey]: { storage?: boolean, wallet?: boolean } }. Off by default.
+
+const FLAG_STORAGE_KEY = "bitvid:settings-sync:v1";
+
+function readFlags() {
+ try {
+ if (typeof localStorage === "undefined") {
+ return {};
+ }
+ const raw = localStorage.getItem(FLAG_STORAGE_KEY);
+ if (!raw) {
+ return {};
+ }
+ const parsed = JSON.parse(raw);
+ return parsed && typeof parsed === "object" ? parsed : {};
+ } catch (error) {
+ return {};
+ }
+}
+
+function writeFlags(flags) {
+ try {
+ if (typeof localStorage === "undefined") {
+ return;
+ }
+ localStorage.setItem(FLAG_STORAGE_KEY, JSON.stringify(flags || {}));
+ } catch (error) {
+ // Best-effort; the flag is a convenience, not a source of truth.
+ }
+}
+
+function normalizePubkey(pubkey) {
+ return typeof pubkey === "string" ? pubkey.trim() : "";
+}
+
+export function isSyncEnabled(pubkey, kind) {
+ const key = normalizePubkey(pubkey);
+ if (!key || !kind) {
+ return false;
+ }
+ return readFlags()[key]?.[kind] === true;
+}
+
+export function setSyncEnabled(pubkey, kind, enabled) {
+ const key = normalizePubkey(pubkey);
+ if (!key || !kind) {
+ return;
+ }
+ const flags = readFlags();
+ const entry = flags[key] && typeof flags[key] === "object" ? flags[key] : {};
+ entry[kind] = enabled === true;
+ flags[key] = entry;
+ writeFlags(flags);
+}
+
+export const SETTINGS_SYNC_FLAG_KEY = FLAG_STORAGE_KEY;
diff --git a/js/services/storageSyncService.js b/js/services/storageSyncService.js
index 50565771..20e369e9 100644
--- a/js/services/storageSyncService.js
+++ b/js/services/storageSyncService.js
@@ -9,37 +9,11 @@
import { encryptedSync as defaultEncryptedSync } from "../nostr/encryptedSyncFacade.js";
import defaultStorageService from "./storageService.js";
+import { isSyncEnabled, setSyncEnabled } from "./settingsSyncFlags.js";
import { userLogger } from "../utils/logger.js";
export const STORAGE_SYNC_DTAG = "bitvid:storage-connections";
-const FLAG_STORAGE_KEY = "bitvid:settings-sync:v1";
-
-function readFlags() {
- try {
- if (typeof localStorage === "undefined") {
- return {};
- }
- const raw = localStorage.getItem(FLAG_STORAGE_KEY);
- if (!raw) {
- return {};
- }
- const parsed = JSON.parse(raw);
- return parsed && typeof parsed === "object" ? parsed : {};
- } catch (error) {
- return {};
- }
-}
-
-function writeFlags(flags) {
- try {
- if (typeof localStorage === "undefined") {
- return;
- }
- localStorage.setItem(FLAG_STORAGE_KEY, JSON.stringify(flags || {}));
- } catch (error) {
- // Best-effort; the flag is a convenience, not a source of truth.
- }
-}
+const SYNC_KIND = "storage";
export function createStorageSyncService({
encryptedSync = defaultEncryptedSync,
@@ -54,23 +28,11 @@ export function createStorageSyncService({
}
function isEnabled(pubkey) {
- const key = normalizePubkey(pubkey);
- if (!key) {
- return false;
- }
- return readFlags()[key]?.storage === true;
+ return isSyncEnabled(normalizePubkey(pubkey), SYNC_KIND);
}
function setEnabledFlag(pubkey, enabled) {
- const key = normalizePubkey(pubkey);
- if (!key) {
- return;
- }
- const flags = readFlags();
- const entry = flags[key] && typeof flags[key] === "object" ? flags[key] : {};
- entry.storage = enabled === true;
- flags[key] = entry;
- writeFlags(flags);
+ setSyncEnabled(normalizePubkey(pubkey), SYNC_KIND, enabled);
}
// Push the current local storage account record to the user's encrypted note.
diff --git a/js/services/walletSyncService.js b/js/services/walletSyncService.js
new file mode 100644
index 00000000..e1671a1e
--- /dev/null
+++ b/js/services/walletSyncService.js
@@ -0,0 +1,111 @@
+// Opt-in encrypted cross-login sync for the NWC WALLET connection (todo #15).
+// Mirrors storageSyncService but for the wallet settings managed by
+// NwcSettingsService.
+//
+// HIGHEST SENSITIVITY: the NWC URI is a bearer SPENDING secret. Anyone who can
+// decrypt this note (i.e. anyone who controls the user's Nostr key) can spend
+// from the connected wallet. The UI MUST gate enabling this behind an explicit
+// confirmation. Off by default; never log the decrypted URI.
+//
+// NwcSettingsService is a per-app instance (not a singleton), so this is a
+// factory — the caller injects the live service.
+
+import { encryptedSync as defaultEncryptedSync } from "../nostr/encryptedSyncFacade.js";
+import { isSyncEnabled, setSyncEnabled } from "./settingsSyncFlags.js";
+import { userLogger } from "../utils/logger.js";
+
+export const WALLET_SYNC_DTAG = "bitvid:nwc";
+const SYNC_KIND = "wallet";
+
+export function createWalletSyncService({
+ encryptedSync = defaultEncryptedSync,
+ nwcSettings,
+} = {}) {
+ function normalizePubkey(pubkey) {
+ return typeof pubkey === "string" ? pubkey.trim() : "";
+ }
+
+ function isAvailable() {
+ return Boolean(encryptedSync?.isAvailable?.()) && Boolean(nwcSettings);
+ }
+
+ function isEnabled(pubkey) {
+ return isSyncEnabled(normalizePubkey(pubkey), SYNC_KIND);
+ }
+
+ function setEnabledFlag(pubkey, enabled) {
+ setSyncEnabled(normalizePubkey(pubkey), SYNC_KIND, enabled);
+ }
+
+ // Build the minimal syncable payload from current wallet settings.
+ function buildPayload() {
+ const settings = nwcSettings?.getActiveNwcSettings?.() || {};
+ const nwcUri = typeof settings.nwcUri === "string" ? settings.nwcUri.trim() : "";
+ const payload = { nwcUri };
+ if (settings.defaultZap !== undefined) {
+ payload.defaultZap = settings.defaultZap;
+ }
+ return payload;
+ }
+
+ async function push(pubkey) {
+ const key = normalizePubkey(pubkey);
+ if (!key) {
+ return { ok: false, error: "missing-pubkey" };
+ }
+ const payload = buildPayload();
+ if (!payload.nwcUri) {
+ return { ok: false, error: "nothing-to-sync" };
+ }
+ return encryptedSync.push(WALLET_SYNC_DTAG, payload);
+ }
+
+ async function pull(pubkey) {
+ const key = normalizePubkey(pubkey);
+ if (!key) {
+ return { found: false, error: "missing-pubkey" };
+ }
+ const result = await encryptedSync.pull(WALLET_SYNC_DTAG);
+ if (!result?.found) {
+ return result;
+ }
+ const payload = result.payload || {};
+ const nwcUri = typeof payload.nwcUri === "string" ? payload.nwcUri.trim() : "";
+ if (!nwcUri) {
+ return { ...result, imported: false, error: "empty-payload" };
+ }
+ try {
+ const partial = { nwcUri };
+ if (payload.defaultZap !== undefined) {
+ partial.defaultZap = payload.defaultZap;
+ }
+ await nwcSettings.updateActiveNwcSettings(partial);
+ } catch (error) {
+ userLogger.warn("[walletSync] Failed to apply pulled wallet settings:", error);
+ return { ...result, imported: false, error: "import-failed", cause: error };
+ }
+ return { ...result, imported: true };
+ }
+
+ async function enable(pubkey) {
+ setEnabledFlag(pubkey, true);
+ return push(pubkey);
+ }
+
+ async function disable(pubkey) {
+ setEnabledFlag(pubkey, false);
+ return encryptedSync.clear(WALLET_SYNC_DTAG);
+ }
+
+ return {
+ WALLET_SYNC_DTAG,
+ isAvailable,
+ isEnabled,
+ push,
+ pull,
+ enable,
+ disable,
+ };
+}
+
+export default createWalletSyncService;
diff --git a/js/ui/profileModal/ProfileWalletController.js b/js/ui/profileModal/ProfileWalletController.js
index 1039ee37..428d8075 100644
--- a/js/ui/profileModal/ProfileWalletController.js
+++ b/js/ui/profileModal/ProfileWalletController.js
@@ -1,5 +1,6 @@
import { devLogger } from "../../utils/logger.js";
import { NWC_URI_SCHEME } from "../profileModalContract.js";
+import { createWalletSyncService } from "../../services/walletSyncService.js";
const noop = () => {};
const SECRET_PLACEHOLDER = "*****";
@@ -15,6 +16,12 @@ export class ProfileWalletController {
this.walletDisconnectButton = null;
this.walletStatusText = null;
this.walletPane = null;
+
+ this.walletSyncSection = null;
+ this.walletSyncToggle = null;
+ this.walletSyncRestoreBtn = null;
+ this.walletSyncStatus = null;
+ this._walletSyncService = null;
}
cacheDomReferences() {
@@ -25,6 +32,10 @@ export class ProfileWalletController {
this.walletDisconnectButton = document.getElementById("profileWalletDisconnect") || null;
this.walletStatusText = document.getElementById("profileWalletStatus") || null;
this.walletPane = document.getElementById("profilePaneWallet") || null;
+ this.walletSyncSection = document.getElementById("profileWalletSync") || null;
+ this.walletSyncToggle = document.getElementById("walletSyncToggle") || null;
+ this.walletSyncRestoreBtn = document.getElementById("walletSyncRestoreBtn") || null;
+ this.walletSyncStatus = document.getElementById("walletSyncStatus") || null;
// Backwards compatibility alias if needed by tests/external code
this.mainController.profileWalletStatusText = this.walletStatusText;
@@ -68,6 +79,146 @@ export class ProfileWalletController {
void this.handleWalletDisconnect();
});
}
+
+ if (this.walletSyncToggle instanceof HTMLElement) {
+ this.walletSyncToggle.addEventListener("change", () => {
+ void this.handleToggleSync();
+ });
+ }
+
+ if (this.walletSyncRestoreBtn instanceof HTMLElement) {
+ this.walletSyncRestoreBtn.addEventListener("click", () => {
+ void this.handleRestoreSync();
+ });
+ }
+ }
+
+ getWalletSyncService() {
+ if (!this._walletSyncService) {
+ this._walletSyncService = createWalletSyncService({
+ nwcSettings: this.mainController.services.nwcSettings,
+ });
+ }
+ return this._walletSyncService;
+ }
+
+ renderSyncSection(pubkey) {
+ if (!(this.walletSyncSection instanceof HTMLElement)) {
+ return;
+ }
+ const sync = this.getWalletSyncService();
+ if (!pubkey || !sync.isAvailable()) {
+ this.walletSyncSection.classList.add("hidden");
+ return;
+ }
+ this.walletSyncSection.classList.remove("hidden");
+ if (this.walletSyncToggle instanceof HTMLInputElement) {
+ this.walletSyncToggle.checked = sync.isEnabled(pubkey);
+ }
+ this.setSyncStatus("");
+ }
+
+ setSyncStatus(message, tone = "info") {
+ if (!(this.walletSyncStatus instanceof HTMLElement)) {
+ return;
+ }
+ this.walletSyncStatus.textContent = message || "";
+ const toneClass =
+ tone === "success"
+ ? "text-status-success"
+ : tone === "error"
+ ? "text-status-danger"
+ : "text-muted";
+ this.walletSyncStatus.className = `text-xs ${toneClass}`;
+ }
+
+ async handleToggleSync() {
+ const pubkey = this.mainController.normalizeHexPubkey(
+ this.mainController.getActivePubkey()
+ );
+ if (!pubkey) {
+ return;
+ }
+ const sync = this.getWalletSyncService();
+ const enabled =
+ this.walletSyncToggle instanceof HTMLInputElement
+ ? this.walletSyncToggle.checked
+ : false;
+ try {
+ if (enabled) {
+ // Spending capability — require explicit confirmation before publishing.
+ const confirmed =
+ typeof window !== "undefined" && typeof window.confirm === "function"
+ ? window.confirm(
+ "This publishes an ENCRYPTED copy of your wallet connection to public relays.\n\n" +
+ "A wallet connect URI can SPEND from your wallet. Anyone who controls your " +
+ "Nostr key could decrypt this and spend from this wallet.\n\nContinue?"
+ )
+ : true;
+ if (!confirmed) {
+ if (this.walletSyncToggle instanceof HTMLInputElement) {
+ this.walletSyncToggle.checked = false;
+ }
+ return;
+ }
+ this.setSyncStatus("Encrypting and publishing…");
+ const result = await sync.enable(pubkey);
+ if (result?.ok) {
+ this.setSyncStatus(
+ `Synced to ${result.accepted}/${result.total} relays.`,
+ "success"
+ );
+ this.mainController.showSuccess("Wallet connection synced (encrypted).");
+ } else {
+ if (this.walletSyncToggle instanceof HTMLInputElement) {
+ this.walletSyncToggle.checked = false;
+ }
+ await sync.disable(pubkey).catch(() => {});
+ this.setSyncStatus(
+ result?.error === "nothing-to-sync"
+ ? "Connect a wallet first, then enable sync."
+ : "Could not publish the encrypted copy. Try again.",
+ "error"
+ );
+ }
+ } else {
+ this.setSyncStatus("Removing the synced copy…");
+ await sync.disable(pubkey);
+ this.setSyncStatus("Sync turned off; the synced copy was cleared.");
+ this.mainController.showSuccess("Wallet sync turned off.");
+ }
+ } catch (error) {
+ devLogger.error("[ProfileModal] Wallet sync toggle failed:", error);
+ this.setSyncStatus("Sync failed. Please try again.", "error");
+ }
+ }
+
+ async handleRestoreSync() {
+ const pubkey = this.mainController.normalizeHexPubkey(
+ this.mainController.getActivePubkey()
+ );
+ if (!pubkey) {
+ return;
+ }
+ const sync = this.getWalletSyncService();
+ try {
+ this.setSyncStatus("Fetching and decrypting…");
+ const result = await sync.pull(pubkey);
+ if (result?.found && result.imported) {
+ this.setSyncStatus("Restored from your Nostr account.", "success");
+ this.mainController.showSuccess("Wallet connection restored.");
+ this.refreshWalletPaneState();
+ } else if (result?.found && !result.imported) {
+ this.setSyncStatus("Found a copy but could not import it.", "error");
+ } else if (result?.cleared) {
+ this.setSyncStatus("No synced wallet found (it was cleared).");
+ } else {
+ this.setSyncStatus("No synced wallet found on your account.");
+ }
+ } catch (error) {
+ devLogger.error("[ProfileModal] Wallet sync restore failed:", error);
+ this.setSyncStatus("Restore failed. Please try again.", "error");
+ }
}
applyWalletControlState() {
@@ -151,6 +302,7 @@ export class ProfileWalletController {
}
}
this.updateWalletStatus("Sign in to connect a wallet.", "info");
+ this.renderSyncSection("");
this.applyWalletControlState();
return;
}
@@ -183,6 +335,9 @@ export class ProfileWalletController {
this.updateWalletStatus("No wallet connected yet.", "info");
}
+ this.renderSyncSection(
+ this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey())
+ );
this.applyWalletControlState();
}
@@ -459,11 +614,35 @@ export class ProfileWalletController {
finalVariant = "success";
this.mainController.showSuccess("Wallet settings saved.");
context.reason = "saved";
+ // Keep the encrypted synced copy current if the user opted in.
+ const sync = this.getWalletSyncService();
+ if (sync.isEnabled(normalizedActive)) {
+ try {
+ await sync.push(normalizedActive);
+ } catch (syncError) {
+ devLogger.warn(
+ "[ProfileModal] Wallet re-sync after save failed:",
+ syncError
+ );
+ }
+ }
} else {
finalStatus = "Wallet connection removed.";
finalVariant = "info";
this.mainController.showStatus("Wallet connection removed.");
context.reason = "cleared";
+ // Don't leave a now-removed spending URI lingering (encrypted) on relays.
+ const sync = this.getWalletSyncService();
+ if (sync.isEnabled(normalizedActive)) {
+ try {
+ await sync.disable(normalizedActive);
+ } catch (syncError) {
+ devLogger.warn(
+ "[ProfileModal] Wallet sync clear after disconnect failed:",
+ syncError
+ );
+ }
+ }
}
context.success = true;
} catch (error) {
diff --git a/tests/wallet-sync-service.test.mjs b/tests/wallet-sync-service.test.mjs
new file mode 100644
index 00000000..59c01872
--- /dev/null
+++ b/tests/wallet-sync-service.test.mjs
@@ -0,0 +1,125 @@
+// NWC wallet sync (todo #15): the wallet connection (a bearer SPENDING secret)
+// is pushed to an encrypted note and pulled+applied on another device.
+// Scenarios assert observable outcomes at the nwcSettings + sync boundary:
+// - enable() pushes the current nwcUri (+defaultZap) under the wallet d-tag
+// - pull() applies the pulled URI via updateActiveNwcSettings (restore)
+// - disable() clears the published note and flips the opt-in flag
+// - push() with no wallet connected does not publish
+
+import "./test-helpers/setup-localstorage.mjs";
+import assert from "node:assert/strict";
+import test from "node:test";
+import {
+ createWalletSyncService,
+ WALLET_SYNC_DTAG,
+} from "../js/services/walletSyncService.js";
+
+const PUBKEY = "b".repeat(64);
+const URI = "nostr+walletconnect://deadbeef?relay=wss://relay.example&secret=cafe";
+
+function makeNwcTwin(initial = {}) {
+ let settings = { nwcUri: "", defaultZap: undefined, ...initial };
+ return {
+ applied: [],
+ getActiveNwcSettings: () => ({ ...settings }),
+ updateActiveNwcSettings: async (partial) => {
+ settings = { ...settings, ...partial };
+ // record what was applied (do NOT log the secret in real code)
+ // eslint-disable-next-line no-unused-vars
+ const safe = { ...partial };
+ return { ...settings };
+ },
+ };
+}
+
+function makeSyncTwin() {
+ let note = null;
+ return {
+ calls: { push: [], pull: 0, clear: 0 },
+ isAvailable: () => true,
+ async push(dTag, payload) {
+ this.calls.push.push({ dTag, payload });
+ note = { payload };
+ return { ok: true, accepted: 1, total: 1 };
+ },
+ async pull() {
+ this.calls.pull += 1;
+ if (!note || note.cleared) {
+ return { found: false, cleared: Boolean(note?.cleared) };
+ }
+ return { found: true, payload: note.payload, createdAt: 1 };
+ },
+ async clear() {
+ this.calls.clear += 1;
+ note = { cleared: true };
+ return { ok: true, accepted: 1, total: 1 };
+ },
+ };
+}
+
+test("enable() pushes the current wallet URI under the wallet d-tag", async () => {
+ localStorage.clear();
+ const nwc = makeNwcTwin({ nwcUri: URI, defaultZap: 500 });
+ const sync = makeSyncTwin();
+ const service = createWalletSyncService({ encryptedSync: sync, nwcSettings: nwc });
+
+ assert.equal(service.isEnabled(PUBKEY), false, "off by default");
+ const result = await service.enable(PUBKEY);
+ assert.equal(result.ok, true);
+ assert.equal(sync.calls.push.length, 1);
+ assert.equal(sync.calls.push[0].dTag, WALLET_SYNC_DTAG);
+ assert.deepEqual(sync.calls.push[0].payload, { nwcUri: URI, defaultZap: 500 });
+ assert.equal(service.isEnabled(PUBKEY), true, "opt-in flag persists");
+});
+
+test("pull() restores the wallet URI via updateActiveNwcSettings on a fresh device", async () => {
+ localStorage.clear();
+ const sharedSync = makeSyncTwin();
+ // Device A pushes.
+ await createWalletSyncService({
+ encryptedSync: sharedSync,
+ nwcSettings: makeNwcTwin({ nwcUri: URI, defaultZap: 21 }),
+ }).enable(PUBKEY);
+
+ // Device B (no wallet) pulls.
+ const nwcB = makeNwcTwin({ nwcUri: "" });
+ const serviceB = createWalletSyncService({ encryptedSync: sharedSync, nwcSettings: nwcB });
+
+ const pulled = await serviceB.pull(PUBKEY);
+ assert.equal(pulled.found, true);
+ assert.equal(pulled.imported, true);
+ assert.equal(
+ nwcB.getActiveNwcSettings().nwcUri,
+ URI,
+ "device B must now hold the restored wallet URI",
+ );
+ assert.equal(nwcB.getActiveNwcSettings().defaultZap, 21);
+});
+
+test("disable() clears the note and a later pull finds nothing", async () => {
+ localStorage.clear();
+ const nwc = makeNwcTwin({ nwcUri: URI });
+ const sync = makeSyncTwin();
+ const service = createWalletSyncService({ encryptedSync: sync, nwcSettings: nwc });
+
+ await service.enable(PUBKEY);
+ const cleared = await service.disable(PUBKEY);
+ assert.equal(cleared.ok, true);
+ assert.equal(sync.calls.clear, 1);
+ assert.equal(service.isEnabled(PUBKEY), false);
+
+ const pulled = await service.pull(PUBKEY);
+ assert.equal(pulled.found, false);
+});
+
+test("push() with no wallet connected does not publish", async () => {
+ localStorage.clear();
+ const nwc = makeNwcTwin({ nwcUri: "" });
+ const sync = makeSyncTwin();
+ const service = createWalletSyncService({ encryptedSync: sync, nwcSettings: nwc });
+
+ const result = await service.push(PUBKEY);
+ assert.equal(result.ok, false);
+ assert.equal(result.error, "nothing-to-sync");
+ assert.equal(sync.calls.push.length, 0);
+});