mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat(wallet): Bitcoin Connect NWC "Connect wallet" flow [LOCAL — flag ON, do not push as-is]
Phase 0+1 of docs/bitcoin-connect-plan.md (#61). Adds Alby's Bitcoin Connect as a one-tap NWC connect front-end that feeds bitvid's EXISTING wallet pipeline; manual URI entry stays as the backup. - Vendored: pinned @getalby/[email protected].3 bundled by scripts/build-bitcoin-connect.mjs → vendor/bitcoin-connect.bundle.min.js (esbuild, browser ESM). crypto-shim aliases the L402-only `import('crypto')` bitvid never calls. `npm run build:bitcoin-connect` rebuilds it. - Flag: FEATURE_BITCOIN_CONNECT wired like FEATURE_AUDIO_INGEST (instance-config → config → constants). - UI: a "Connect wallet" button in the Wallet Connect pane (hidden unless the flag is on); the manual URI field stays below as the backup. - Controller: ProfileWalletController.handleBitcoinConnect() lazy-imports the bundle (only on click — the ~700KB never touches the main bundle), inits NWC-only with least-privilege requestMethods and persistConnection:false, then on onConnected extracts provider.client.nostrWalletConnectUrl → the SAME validate→save path as the manual field → disconnect() so bitvid solely holds the spending secret. The URI is never logged. NOTE: config/instance-config.js FEATURE_BITCOIN_CONNECT is TRUE for local testing only. Reset to false (shipping default) before pushing/deploying. Local commit; not pushed, not deployed. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
6dd40ce330
commit
69e87c6e15
12 files changed
+1663
-3
No files matched your search
@@ -26,6 +26,7 @@ import {
|
||||
ALLOW_NSFW_CONTENT,
|
||||
FEATURE_NIP71_INGEST,
|
||||
FEATURE_AUDIO_INGEST,
|
||||
FEATURE_BITCOIN_CONNECT,
|
||||
CARD_LIVENESS_POLICY,
|
||||
LIVENESS_PROBE_PREFETCH_MARGIN,
|
||||
DEFAULT_TRUST_SEED_NPUBS,
|
||||
@@ -123,6 +124,7 @@ export const ADMIN_COMMUNITY_BLACKLIST_PREFIX = "community:blacklist";
|
||||
export { ALLOW_NSFW_CONTENT };
|
||||
export { FEATURE_NIP71_INGEST };
|
||||
export { FEATURE_AUDIO_INGEST };
|
||||
export { FEATURE_BITCOIN_CONNECT };
|
||||
export { CARD_LIVENESS_POLICY };
|
||||
export { LIVENESS_PROBE_PREFETCH_MARGIN };
|
||||
export { DEFAULT_TRUST_SEED_NPUBS };
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DEFAULT_PLAYBACK_START_TIMEOUT,
|
||||
FEATURE_NIP71_INGEST as CONFIG_FEATURE_NIP71_INGEST,
|
||||
FEATURE_AUDIO_INGEST as CONFIG_FEATURE_AUDIO_INGEST,
|
||||
FEATURE_BITCOIN_CONNECT as CONFIG_FEATURE_BITCOIN_CONNECT,
|
||||
CARD_LIVENESS_POLICY as CONFIG_CARD_LIVENESS_POLICY,
|
||||
LIVENESS_PROBE_PREFETCH_MARGIN as CONFIG_LIVENESS_PROBE_PREFETCH_MARGIN,
|
||||
IS_DEV_MODE,
|
||||
@@ -199,6 +200,9 @@ const DEFAULT_FLAGS = Object.freeze({
|
||||
// Default OFF ("off = no trace") — only an explicit `true` in the instance
|
||||
// config lights it up.
|
||||
FEATURE_AUDIO_INGEST: CONFIG_FEATURE_AUDIO_INGEST === true,
|
||||
// Bitcoin Connect NWC "Connect wallet" flow (docs/bitcoin-connect-plan.md, #61).
|
||||
// Default OFF; only an explicit `true` in the instance config enables it.
|
||||
FEATURE_BITCOIN_CONNECT: CONFIG_FEATURE_BITCOIN_CONNECT === true,
|
||||
FEATURE_HASHTAG_PREFERENCES: false,
|
||||
FEATURE_TRUST_SEEDS: true, // Rollback: disable to drop baseline trust seeds without code changes.
|
||||
FEATURE_TRUSTED_HIDE_CONTROLS: true,
|
||||
@@ -226,6 +230,7 @@ const runtimeFlags = (() => {
|
||||
FEATURE_NIP71_MIRROR: DEFAULT_FLAGS.FEATURE_NIP71_MIRROR,
|
||||
FEATURE_NIP71_INGEST: DEFAULT_FLAGS.FEATURE_NIP71_INGEST,
|
||||
FEATURE_AUDIO_INGEST: DEFAULT_FLAGS.FEATURE_AUDIO_INGEST,
|
||||
FEATURE_BITCOIN_CONNECT: DEFAULT_FLAGS.FEATURE_BITCOIN_CONNECT,
|
||||
FEATURE_HASHTAG_PREFERENCES: DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES,
|
||||
FEATURE_TRUST_SEEDS: DEFAULT_FLAGS.FEATURE_TRUST_SEEDS,
|
||||
FEATURE_TRUSTED_HIDE_CONTROLS: DEFAULT_FLAGS.FEATURE_TRUSTED_HIDE_CONTROLS,
|
||||
@@ -276,6 +281,11 @@ export let FEATURE_AUDIO_INGEST = coerceBoolean(
|
||||
DEFAULT_FLAGS.FEATURE_AUDIO_INGEST
|
||||
);
|
||||
|
||||
export let FEATURE_BITCOIN_CONNECT = coerceBoolean(
|
||||
runtimeFlags.FEATURE_BITCOIN_CONNECT,
|
||||
DEFAULT_FLAGS.FEATURE_BITCOIN_CONNECT
|
||||
);
|
||||
|
||||
export let FEATURE_HASHTAG_PREFERENCES = coerceBoolean(
|
||||
runtimeFlags.FEATURE_HASHTAG_PREFERENCES,
|
||||
DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
import { FEATURE_BITCOIN_CONNECT } from "../../constants.js";
|
||||
import { showConfirm } from "../confirmDialog.js";
|
||||
import { NWC_URI_SCHEME } from "../profileModalContract.js";
|
||||
|
||||
// Vendored, pinned Bitcoin Connect bundle (scripts/build-bitcoin-connect.mjs).
|
||||
// Lazy-imported only when the user clicks "Connect wallet", so its ~700KB never
|
||||
// weighs on the main bundle. See docs/bitcoin-connect-plan.md.
|
||||
const BITCOIN_CONNECT_BUNDLE_URL =
|
||||
"../../../vendor/bitcoin-connect.bundle.min.js";
|
||||
import { createWalletSyncService } from "../../services/walletSyncService.js";
|
||||
import { runWalletZappabilityCheck } from "./walletZappabilityCheck.js";
|
||||
import {
|
||||
@@ -32,6 +39,11 @@ export class ProfileWalletController {
|
||||
this.walletSyncRestoreBtn = null;
|
||||
this.walletSyncStatus = null;
|
||||
this._walletSyncService = null;
|
||||
|
||||
this.walletBitcoinConnectRow = null;
|
||||
this.walletBitcoinConnectButton = null;
|
||||
this._bitcoinConnectModulePromise = null;
|
||||
this._bitcoinConnectInitialized = false;
|
||||
}
|
||||
|
||||
cacheDomReferences() {
|
||||
@@ -46,9 +58,27 @@ export class ProfileWalletController {
|
||||
this.walletSyncToggle = document.getElementById("walletSyncToggle") || null;
|
||||
this.walletSyncRestoreBtn = document.getElementById("walletSyncRestoreBtn") || null;
|
||||
this.walletSyncStatus = document.getElementById("walletSyncStatus") || null;
|
||||
this.walletBitcoinConnectRow =
|
||||
document.getElementById("profileWalletBitcoinConnectRow") || null;
|
||||
this.walletBitcoinConnectButton =
|
||||
document.getElementById("profileWalletBitcoinConnect") || null;
|
||||
|
||||
// Backwards compatibility alias if needed by tests/external code
|
||||
this.mainController.profileWalletStatusText = this.walletStatusText;
|
||||
|
||||
this.applyBitcoinConnectVisibility();
|
||||
}
|
||||
|
||||
// Reveal the "Connect wallet" button only when the deployment enables the flag.
|
||||
// Off ⇒ the row stays hidden and the vendored bundle is never imported.
|
||||
applyBitcoinConnectVisibility() {
|
||||
if (!(this.walletBitcoinConnectRow instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
this.walletBitcoinConnectRow.classList.toggle(
|
||||
"hidden",
|
||||
FEATURE_BITCOIN_CONNECT !== true,
|
||||
);
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
@@ -90,6 +120,15 @@ export class ProfileWalletController {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
FEATURE_BITCOIN_CONNECT === true &&
|
||||
this.walletBitcoinConnectButton instanceof HTMLElement
|
||||
) {
|
||||
this.walletBitcoinConnectButton.addEventListener("click", () => {
|
||||
void this.handleBitcoinConnect();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletSyncToggle instanceof HTMLElement) {
|
||||
this.walletSyncToggle.addEventListener("change", () => {
|
||||
void this.handleToggleSync();
|
||||
@@ -521,6 +560,104 @@ export class ProfileWalletController {
|
||||
return { valid: true, sanitized: value };
|
||||
}
|
||||
|
||||
// Lazy-load + one-time init of the vendored Bitcoin Connect bundle. Restricts
|
||||
// the modal to NWC and requests only the methods bitvid actually calls. Does
|
||||
// NOT let Bitcoin Connect persist the connection — bitvid owns the secret.
|
||||
async ensureBitcoinConnectModule() {
|
||||
if (this._bitcoinConnectModulePromise) {
|
||||
return this._bitcoinConnectModulePromise;
|
||||
}
|
||||
this._bitcoinConnectModulePromise = (async () => {
|
||||
const bc = await import(BITCOIN_CONNECT_BUNDLE_URL);
|
||||
if (!this._bitcoinConnectInitialized) {
|
||||
bc.init({
|
||||
appName: "bitvid",
|
||||
filters: ["nwc"],
|
||||
persistConnection: false,
|
||||
providerConfig: {
|
||||
nwc: {
|
||||
authorizationUrlOptions: {
|
||||
requestMethods: [
|
||||
"pay_invoice",
|
||||
"get_balance",
|
||||
"make_invoice",
|
||||
"lookup_invoice",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// One persistent handler: extract the NWC URI, hand it to the SAME
|
||||
// validate→save path as the manual field, then disconnect so bitvid is
|
||||
// the only holder of the spending secret. NEVER log the URI.
|
||||
bc.onConnected(async (provider) => {
|
||||
const uri =
|
||||
typeof provider?.client?.nostrWalletConnectUrl === "string"
|
||||
? provider.client.nostrWalletConnectUrl
|
||||
: "";
|
||||
try {
|
||||
if (uri && this.walletUriInput instanceof HTMLElement) {
|
||||
this.setSecretInputValue(this.walletUriInput, uri);
|
||||
await this.handleWalletSave();
|
||||
} else if (!uri) {
|
||||
this.updateWalletStatus(
|
||||
"That wallet didn't expose an NWC connection. Paste one manually below.",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.error("[wallet] Bitcoin Connect save failed", error);
|
||||
} finally {
|
||||
try {
|
||||
bc.disconnect();
|
||||
} catch (disconnectError) {
|
||||
devLogger.warn(
|
||||
"[wallet] Bitcoin Connect disconnect failed",
|
||||
disconnectError,
|
||||
);
|
||||
}
|
||||
try {
|
||||
bc.closeModal?.();
|
||||
} catch (closeError) {
|
||||
// non-fatal
|
||||
}
|
||||
}
|
||||
});
|
||||
this._bitcoinConnectInitialized = true;
|
||||
}
|
||||
return bc;
|
||||
})().catch((error) => {
|
||||
// Reset so a later click can retry a failed load.
|
||||
this._bitcoinConnectModulePromise = null;
|
||||
throw error;
|
||||
});
|
||||
return this._bitcoinConnectModulePromise;
|
||||
}
|
||||
|
||||
async handleBitcoinConnect() {
|
||||
if (FEATURE_BITCOIN_CONNECT !== true || this.isWalletBusy()) {
|
||||
return;
|
||||
}
|
||||
let bc;
|
||||
try {
|
||||
bc = await this.ensureBitcoinConnectModule();
|
||||
} catch (error) {
|
||||
devLogger.error("[wallet] Failed to load Bitcoin Connect", error);
|
||||
this.updateWalletStatus(
|
||||
"Couldn't load the wallet connector. Try again, or paste a connection manually.",
|
||||
"error",
|
||||
);
|
||||
this.mainController.showError?.("Couldn't load the wallet connector.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
bc.launchModal();
|
||||
} catch (error) {
|
||||
devLogger.error("[wallet] Bitcoin Connect launch failed", error);
|
||||
this.updateWalletStatus("Couldn't open the wallet connector.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async handleWalletSave() {
|
||||
const { uri, defaultZap, error } = this.getWalletFormValues();
|
||||
const context = {
|
||||
|
||||
Reference in new issue
Block a user