Compare commits
No commits in common. "2604cf9d1e3c50d66dde60051d9f5098309fa4e2" and "a3b509cb09304151296e07beb616e339b8edaeda" have entirely different histories.
2604cf9d1e
...
a3b509cb09
5 changed files with 43 additions and 252 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
# Checkpoint — Release packages with profile metadata fix (2026-09-01)
|
# Checkpoint — Keynctr branding and Linux installers (2026-08-31)
|
||||||
|
|
||||||
## Where things are
|
## Where things are
|
||||||
- Project: `/home/avi/Projects/Keynctr`
|
- Project: `/home/avi/Projects/Nostr_Keynctr`
|
||||||
- Git repo: `master` @ `3107508` ("fix: query imported metadata by relay").
|
- Git repo: `master` @ `76deca6` ("feat: add Cosmic theme + motion system").
|
||||||
- Working tree: intended profile metadata fix is committed; unrelated UI/branding changes remain uncommitted and untracked.
|
- Working tree: uncommitted Keynctr branding, packaging metadata, and Cosmic palette/branding updates, plus untracked `.directory`, `.opencode/`, `COSMIC_THEME.md`, and `KeynectrAppIconPossibility02.jpeg`.
|
||||||
|
|
||||||
## What was completed
|
## What was completed
|
||||||
1. **Cosmic branding update.** The Cosmic theme now uses Gold `#F3B407` and Light Blue `#87E6FB`; Cosmic’s dark sidebar presents the logo in white while retaining black-on-white artwork elsewhere. The visible brand is `SOLARPUNK SUMMIT` with `KITCHEN 484`.
|
1. **Cosmic branding update.** The Cosmic theme now uses Gold `#F3B407` and Light Blue `#87E6FB`; Cosmic’s dark sidebar presents the logo in white while retaining black-on-white artwork elsewhere. The visible brand is `SOLARPUNK SUMMIT` with `KITCHEN 484`.
|
||||||
|
|
@ -15,11 +15,7 @@
|
||||||
6. **Keynctr branding.** Replaced the visible `SOLARPUNK SUMMIT` / `KITCHEN 484` labels with `Keynctr` in the window, page title, sidebar, home screen, settings, and tests. Rebuilt artifacts: `frontend/release/Keynctr-0.1.0.AppImage` and `frontend/release/keynectr_0.1.0_amd64.deb`.
|
6. **Keynctr branding.** Replaced the visible `SOLARPUNK SUMMIT` / `KITCHEN 484` labels with `Keynctr` in the window, page title, sidebar, home screen, settings, and tests. Rebuilt artifacts: `frontend/release/Keynctr-0.1.0.AppImage` and `frontend/release/keynectr_0.1.0_amd64.deb`.
|
||||||
|
|
||||||
## Commits added in this session (newest first)
|
## Commits added in this session (newest first)
|
||||||
- `3107508` fix: query imported metadata by relay
|
- No commit added; packaging metadata and checkpoint updates remain uncommitted.
|
||||||
- `da33652` fix: query imported metadata by public key
|
|
||||||
- `bde35bc` fix: import existing profile metadata
|
|
||||||
- `d2d773a` fix: remove Stardust background dots
|
|
||||||
- `7605f51` fix: keep NIP-46 signer subscription open
|
|
||||||
- `76deca6` feat: add Cosmic theme + motion system (palette/branding refinements currently uncommitted)
|
- `76deca6` feat: add Cosmic theme + motion system (palette/branding refinements currently uncommitted)
|
||||||
- `8366af7` feat: add Impeccable themes (light + dark) + unified ProfileEditModal
|
- `8366af7` feat: add Impeccable themes (light + dark) + unified ProfileEditModal
|
||||||
- `832e114` checkpoint: fix delete/undo + themed auto-dismiss bar
|
- `832e114` checkpoint: fix delete/undo + themed auto-dismiss bar
|
||||||
|
|
@ -32,21 +28,11 @@
|
||||||
- Branding verification: `npm test`, `npm run typecheck`, `npm run lint`, `npm run format:check`, `npm run electron:build`, `npm run build`, and `npx electron-builder --linux AppImage deb` passed.
|
- Branding verification: `npm test`, `npm run typecheck`, `npm run lint`, `npm run format:check`, `npm run electron:build`, `npm run build`, and `npx electron-builder --linux AppImage deb` passed.
|
||||||
- Frontend build no longer reports the Cosmic font `@import` ordering warning; standard Vite/ESM and ESLint module warnings remain.
|
- Frontend build no longer reports the Cosmic font `@import` ordering warning; standard Vite/ESM and ESLint module warnings remain.
|
||||||
- Wayland `--ozone-platform` / `has no handler` messages on `electron:build` are harmless.
|
- Wayland `--ozone-platform` / `has no handler` messages on `electron:build` are harmless.
|
||||||
- NIP-46 fix: use a persistent subscription instead of the auto-closing `stream_events` helper, so clients can send requests after EOSE.
|
|
||||||
- Stardust verification: `npm test` (99 passed), `npm run typecheck`, `npm run lint`, `npm run electron:build`, and `npm run build` passed. `npm run format:check` still reports existing issues in three unrelated frontend files.
|
|
||||||
- Existing-account import verification: `cargo test` (115 passed), `cargo clippy --all-targets`, `cargo fmt --check`, `cargo build --release`, `npm test` (99 passed), `npm run typecheck`, `npm run lint`, `npm run electron:build`, and `npm run build` passed. `npm run format:check` reports existing issues in three frontend files.
|
|
||||||
- Imported account naming: the name field is no longer required; kind-0 `display_name`/`name` is used automatically, with a shortened npub fallback. Verification: `cargo test` (116 passed), `cargo clippy --all-targets`, `cargo fmt --check`, `cargo build --release`, `npm test` (99 passed), `npm run typecheck`, `npm run lint`, `npm run electron:build`, and `npm run build` passed. `npm run format:check` still reports existing issues in three frontend files.
|
|
||||||
- Corrected metadata lookup to query with the derived public-key type directly, preventing silent fallback when importing accounts.
|
|
||||||
- Release verification: Rust test suite (116 passed), clippy, fmt, release build, frontend tests (99 passed), typecheck, lint, Electron build, production build, and AppImage/Debian packaging passed. Frontend format check retains three existing warnings.
|
|
||||||
|
|
||||||
## How to resume / reproduce
|
## How to resume / reproduce
|
||||||
GUI (Cosmic): `cargo build --release && cd frontend && npm run build && npm run electron:build && npm start` (or dev: `npm run dev` in one terminal + `NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` in second). Settings → Appearance → `Cosmic — Stardust`. CLI: `cargo run -- settings set theme cosmic`.
|
GUI (Cosmic): `cargo build --release && cd frontend && npm run build && npm run electron:build && npm start` (or dev: `npm run dev` in one terminal + `NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` in second). Settings → Appearance → `Cosmic — Stardust`. CLI: `cargo run -- settings set theme cosmic`.
|
||||||
- Build installers: `cd frontend && npm run build && npm run electron:build && npx electron-builder --linux AppImage deb`. Install the `.deb` with `sudo apt install ./release/keynectr_0.1.0_amd64.deb`, or run the AppImage with `./release/SOLARPUNK\ SUMMIT-0.1.0.AppImage`.
|
- Build installers: `cd frontend && npm run build && npm run electron:build && npx electron-builder --linux AppImage deb`. Install the `.deb` with `sudo apt install ./release/keynectr_0.1.0_amd64.deb`, or run the AppImage with `./release/SOLARPUNK\ SUMMIT-0.1.0.AppImage`.
|
||||||
- Current installers: `sudo apt install ./release/keynectr_0.1.0_amd64.deb`, or `./release/Keynctr-0.1.0.AppImage`.
|
- Current installers: `sudo apt install ./release/keynectr_0.1.0_amd64.deb`, or `./release/Keynctr-0.1.0.AppImage`.
|
||||||
- Signer GUI: unlock vault, open `Signer`, select remote signer in the client, paste its `nostrconnect://` URI, then approve requests. CLI: `cargo run --release -- signer connect <nostrconnect://...>`.
|
|
||||||
- Stardust GUI: select `Settings -> Appearance -> Cosmic - Stardust`, then restart the frontend to load the updated CSS bundle.
|
|
||||||
- Import GUI: restart after rebuilding, open `Profiles -> Add existing account`, enter only the private key, and Keynctr will derive the profile name and metadata from the network.
|
|
||||||
- Release artifacts: `frontend/release/Keynctr-0.1.0.AppImage` and `frontend/release/keynectr_0.1.0_amd64.deb`, verified with `file`.
|
|
||||||
|
|
||||||
## Outstanding / next-step items
|
## Outstanding / next-step items
|
||||||
- Undo restores with empty `secret_key` (stores `ProfileSummary`); needs `StoredProfile` in `undo_history` for full secret recovery.
|
- Undo restores with empty `secret_key` (stores `ProfileSummary`); needs `StoredProfile` in `undo_history` for full secret recovery.
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { useEffect, useRef, useState, type FormEvent } from 'react';
|
|
||||||
import { Button } from '../components/Button';
|
|
||||||
import { ErrorText } from '../components/ErrorText';
|
|
||||||
import { Modal } from '../components/Modal';
|
|
||||||
import { useApp } from '../state/AppProvider';
|
|
||||||
|
|
||||||
export function ImportProfileModal({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
||||||
const { importProfile } = useApp();
|
|
||||||
const [secret, setSecret] = useState('');
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const labelRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setSecret(''); setError(null); setSaving(false);
|
|
||||||
requestAnimationFrame(() => labelRef.current?.focus());
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const submit = async (event: FormEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!secret.trim() || saving) return;
|
|
||||||
setSaving(true); setError(null);
|
|
||||||
try { await importProfile('', secret.trim()); onClose(); }
|
|
||||||
catch (err) { setError(err instanceof Error ? err.message : String(err)); setSaving(false); }
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Modal open={open} title="Add an existing account" onClose={onClose}>
|
|
||||||
<form onSubmit={submit} noValidate>
|
|
||||||
<p className="muted">Import an account using its private key. The key stays in your local vault and is never displayed.</p>
|
|
||||||
<div className="field">
|
|
||||||
<label htmlFor="import-profile-secret">Private key</label>
|
|
||||||
<input id="import-profile-secret" type="password" value={secret} onChange={(e) => setSecret(e.target.value)} placeholder="nsec1... or 64-character hex" autoComplete="off" />
|
|
||||||
{error && <ErrorText>{error}</ErrorText>}
|
|
||||||
</div>
|
|
||||||
<div className="modal-actions">
|
|
||||||
<Button variant="ghost" onClick={onClose} disabled={saving}>Cancel</Button>
|
|
||||||
<Button variant="primary" type="submit" loading={saving} disabled={!secret.trim()}>{saving ? 'Adding…' : 'Add account'}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>;
|
|
||||||
}
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
Light/dark/system themes via `data-theme` on <html>.
|
Light/dark/system themes via `data-theme` on <html>.
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;0,8..60,700;1,8..60,400;1,8..60,500;1,8..60,600;1,8..60,700&display=swap');
|
|
||||||
|
|
||||||
:root,
|
:root,
|
||||||
:root[data-theme='light'] {
|
:root[data-theme='light'] {
|
||||||
--bg: #f6f4f0;
|
--bg: #f6f4f0;
|
||||||
|
|
@ -355,6 +353,8 @@ html[data-theme='impeccable-dark'] .sidebar {
|
||||||
Fonts: Source Serif 4 for body, Cormorant Garamond for headings.
|
Fonts: Source Serif 4 for body, Cormorant Garamond for headings.
|
||||||
Sidebar: Deep (darker background). Workspace: Deep Space (dramatic vignette). */
|
Sidebar: Deep (darker background). Workspace: Deep Space (dramatic vignette). */
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;0,8..60,700;1,8..60,400;1,8..60,500;1,8..60,600;1,8..60,700&display=swap');
|
||||||
|
|
||||||
:root[data-theme='cosmic'] {
|
:root[data-theme='cosmic'] {
|
||||||
/* Stardust dark mode colors */
|
/* Stardust dark mode colors */
|
||||||
--bg: #171109;
|
--bg: #171109;
|
||||||
|
|
@ -365,19 +365,19 @@ html[data-theme='impeccable-dark'] .sidebar {
|
||||||
--border-strong: #3d3220;
|
--border-strong: #3d3220;
|
||||||
--text: #fff5dc;
|
--text: #fff5dc;
|
||||||
--text-muted: #d5bd8d;
|
--text-muted: #d5bd8d;
|
||||||
--primary: #f3b407;
|
--primary: #efbd62;
|
||||||
--primary-hover: #ffc933;
|
--primary-hover: #ffdb91;
|
||||||
--primary-soft: rgba(243, 180, 7, 0.15);
|
--primary-soft: rgba(239, 189, 98, 0.15);
|
||||||
--on-primary: #171109;
|
--on-primary: #171109;
|
||||||
--danger: #f3b407;
|
--danger: #f08e7d;
|
||||||
--danger-soft: rgba(243, 180, 7, 0.15);
|
--danger-soft: rgba(240, 142, 125, 0.15);
|
||||||
--warning: #f3b407;
|
--warning: #f08e7d;
|
||||||
--warning-soft: rgba(243, 180, 7, 0.15);
|
--warning-soft: rgba(240, 142, 125, 0.15);
|
||||||
--success: #87e6fb;
|
--success: #74d7cb;
|
||||||
--success-soft: rgba(135, 230, 251, 0.15);
|
--success-soft: rgba(116, 215, 203, 0.15);
|
||||||
--info: #87e6fb;
|
--info: #74d7cb;
|
||||||
--info-soft: rgba(135, 230, 251, 0.15);
|
--info-soft: rgba(116, 215, 203, 0.15);
|
||||||
--focus: #f3b407;
|
--focus: #efbd62;
|
||||||
--shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
--shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||||
--shadow-modal: 0 12px 40px rgba(0, 0, 0, 0.6);
|
--shadow-modal: 0 12px 40px rgba(0, 0, 0, 0.6);
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
|
|
@ -403,17 +403,17 @@ html[data-theme='cosmic'] h4 {
|
||||||
|
|
||||||
html[data-theme='cosmic'] h1 {
|
html[data-theme='cosmic'] h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: #f3b407;
|
color: #efbd62;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='cosmic'] h2 {
|
html[data-theme='cosmic'] h2 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: #87e6fb;
|
color: #74d7cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='cosmic'] h3 {
|
html[data-theme='cosmic'] h3 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #f3b407;
|
color: #f08e7d;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cosmic sidebar - Deep background (darker) */
|
/* Cosmic sidebar - Deep background (darker) */
|
||||||
|
|
@ -422,13 +422,15 @@ html[data-theme='cosmic'] .sidebar {
|
||||||
border-right-color: var(--border);
|
border-right-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='cosmic'] .sidebar-logo img {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cosmic workspace - Deep Space background with vignette effect */
|
/* Cosmic workspace - Deep Space background with vignette effect */
|
||||||
html[data-theme='cosmic'] .app-shell {
|
html[data-theme='cosmic'] .app-shell {
|
||||||
background:
|
background:
|
||||||
|
radial-gradient(circle at 8% 13%, rgba(255, 245, 220, 0.72) 0 1px, transparent 1.7px),
|
||||||
|
radial-gradient(circle at 27% 7%, rgba(116, 215, 203, 0.82) 0 1px, transparent 1.8px),
|
||||||
|
radial-gradient(circle at 73% 19%, rgba(255, 245, 220, 0.68) 0 1.2px, transparent 1.9px),
|
||||||
|
radial-gradient(circle at 91% 42%, rgba(240, 142, 125, 0.78) 0 1px, transparent 1.8px),
|
||||||
|
radial-gradient(circle at 61% 78%, rgba(255, 245, 220, 0.64) 0 1px, transparent 1.7px),
|
||||||
|
radial-gradient(circle at 18% 84%, rgba(239, 189, 98, 0.82) 0 1.2px, transparent 2px),
|
||||||
radial-gradient(circle at 12% 4%, rgba(239, 189, 98, 0.52), transparent 58rem),
|
radial-gradient(circle at 12% 4%, rgba(239, 189, 98, 0.52), transparent 58rem),
|
||||||
radial-gradient(circle at 88% 12%, rgba(116, 215, 203, 0.42), transparent 46rem),
|
radial-gradient(circle at 88% 12%, rgba(116, 215, 203, 0.42), transparent 46rem),
|
||||||
radial-gradient(circle at 52% 100%, rgba(240, 142, 125, 0.36), transparent 50rem),
|
radial-gradient(circle at 52% 100%, rgba(240, 142, 125, 0.36), transparent 50rem),
|
||||||
|
|
|
||||||
152
src/profiles.rs
152
src/profiles.rs
|
|
@ -1,5 +1,3 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
@ -101,84 +99,6 @@ pub fn create_profile(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an existing profile from a hex or bech32 secret key.
|
|
||||||
pub fn import_profile(
|
|
||||||
vault: &mut Vault,
|
|
||||||
label: String,
|
|
||||||
secret: &str,
|
|
||||||
key: Option<&VaultKey>,
|
|
||||||
settings: &Settings,
|
|
||||||
) -> Result<ProfileSummary, AppError> {
|
|
||||||
if vault.is_encrypted() && key.is_none() {
|
|
||||||
return Err(AppError::vault_locked());
|
|
||||||
}
|
|
||||||
|
|
||||||
let secret_key = SecretKey::from_bech32(secret.trim())
|
|
||||||
.or_else(|_| parse_secret_key(secret.trim()))
|
|
||||||
.map_err(|e| AppError::invalid_secret(format!("Could not import that secret key: {e}")))?;
|
|
||||||
let keys = Keys::new(secret_key);
|
|
||||||
let public_key = keys
|
|
||||||
.public_key()
|
|
||||||
.to_bech32()
|
|
||||||
.map_err(|e| AppError::internal(format!("Could not encode the public key: {e}")))?;
|
|
||||||
if vault
|
|
||||||
.profiles
|
|
||||||
.iter()
|
|
||||||
.any(|profile| profile.public_key == public_key)
|
|
||||||
{
|
|
||||||
return Err(AppError::config("This account is already added."));
|
|
||||||
}
|
|
||||||
|
|
||||||
let secret_hex = Zeroizing::new(hex::encode(keys.secret_key().to_secret_bytes()));
|
|
||||||
let stored_secret = match &vault.crypto {
|
|
||||||
Some(_) => crate::crypto::encrypt_secret(key.expect("guarded above"), &secret_hex)?,
|
|
||||||
None => secret_hex.to_string(),
|
|
||||||
};
|
|
||||||
let created_at = unix_timestamp()?;
|
|
||||||
let metadata = fetch_profile_metadata(&keys.public_key(), &relays::enabled_urls(settings));
|
|
||||||
let label = if label.trim().is_empty() {
|
|
||||||
metadata
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|m| m.display_name.as_deref().or(m.name.as_deref()))
|
|
||||||
.filter(|name| !name.trim().is_empty())
|
|
||||||
.map(str::to_string)
|
|
||||||
.unwrap_or_else(|| shorten_npub(&keys.public_key()))
|
|
||||||
} else {
|
|
||||||
label
|
|
||||||
};
|
|
||||||
let is_active = vault.active_profile.is_none();
|
|
||||||
if is_active {
|
|
||||||
vault.active_profile = Some(public_key.clone());
|
|
||||||
}
|
|
||||||
vault.profiles.push(StoredProfile {
|
|
||||||
label: label.clone(),
|
|
||||||
public_key: public_key.clone(),
|
|
||||||
secret_key: stored_secret,
|
|
||||||
created_at,
|
|
||||||
picture: metadata.as_ref().and_then(|m| m.picture.clone()),
|
|
||||||
nip05: metadata.as_ref().and_then(|m| m.nip05.clone()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let relay_urls = relays::enabled_urls(settings);
|
|
||||||
if !relay_urls.is_empty() {
|
|
||||||
publish_metadata_blocking(
|
|
||||||
&keys,
|
|
||||||
&label,
|
|
||||||
metadata.as_ref().and_then(|m| m.picture.clone()),
|
|
||||||
metadata.as_ref().and_then(|m| m.nip05.clone()),
|
|
||||||
relay_urls,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(ProfileSummary {
|
|
||||||
label,
|
|
||||||
npub: public_key,
|
|
||||||
created_at,
|
|
||||||
is_active,
|
|
||||||
picture: metadata.as_ref().and_then(|m| m.picture.clone()),
|
|
||||||
nip05: metadata.and_then(|m| m.nip05),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The per-relay outcome of publishing a profile metadata event.
|
/// The per-relay outcome of publishing a profile metadata event.
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct MetadataPublishReport {
|
pub struct MetadataPublishReport {
|
||||||
|
|
@ -494,62 +414,6 @@ fn publish_metadata_blocking(
|
||||||
.expect("metadata publish thread panicked")
|
.expect("metadata publish thread panicked")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Best-effort lookup of the account's latest kind-0 metadata. Import must
|
|
||||||
/// still succeed when relays are unavailable, so lookup failures are ignored.
|
|
||||||
fn fetch_profile_metadata(public_key: &PublicKey, relay_urls: &[String]) -> Option<Metadata> {
|
|
||||||
if relay_urls.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let public_key = *public_key;
|
|
||||||
let relay_urls = relay_urls.to_vec();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
tokio::runtime::Runtime::new().ok()?.block_on(async move {
|
|
||||||
let client = Client::builder()
|
|
||||||
.authenticator(SignerAuthenticator::new(Keys::generate()))
|
|
||||||
.build();
|
|
||||||
for url in &relay_urls {
|
|
||||||
let _ = client.add_relay(url.as_str()).await;
|
|
||||||
}
|
|
||||||
client.connect().and_wait(Duration::from_secs(10)).await;
|
|
||||||
let mut best = None;
|
|
||||||
for _url in relay_urls {
|
|
||||||
let events = client
|
|
||||||
.fetch_events(
|
|
||||||
Filter::new()
|
|
||||||
.kind(Kind::Metadata)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(10),
|
|
||||||
)
|
|
||||||
.timeout(Duration::from_secs(5))
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
if let Some(event) = events
|
|
||||||
.and_then(|events| events.into_iter().max_by_key(|event| event.created_at))
|
|
||||||
{
|
|
||||||
if best
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|current: &Event| event.created_at > current.created_at)
|
|
||||||
{
|
|
||||||
best = Some(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.disconnect().await;
|
|
||||||
best.and_then(|event| serde_json::from_str::<Metadata>(&event.content).ok())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.join()
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shorten_npub(public_key: &PublicKey) -> String {
|
|
||||||
public_key
|
|
||||||
.to_bech32()
|
|
||||||
.map(|npub| format!("{}…{}", &npub[..10], &npub[npub.len() - 6..]))
|
|
||||||
.unwrap_or_else(|_| "Imported account".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn publish_metadata_async(
|
async fn publish_metadata_async(
|
||||||
keys: &Keys,
|
keys: &Keys,
|
||||||
label: &str,
|
label: &str,
|
||||||
|
|
@ -816,22 +680,6 @@ mod tests {
|
||||||
assert_eq!(vault.active_profile.as_deref(), Some("npub1alice"));
|
assert_eq!(vault.active_profile.as_deref(), Some("npub1alice"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn import_profile_allows_empty_label() {
|
|
||||||
let mut vault = Vault::empty();
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let summary = import_profile(
|
|
||||||
&mut vault,
|
|
||||||
String::new(),
|
|
||||||
&keys.secret_key().to_bech32().unwrap(),
|
|
||||||
None,
|
|
||||||
&offline_settings(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!summary.label.is_empty());
|
|
||||||
assert_eq!(summary.npub, keys.public_key().to_bech32().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn summaries_never_include_secret_keys() {
|
fn summaries_never_include_secret_keys() {
|
||||||
let vault = populated_vault();
|
let vault = populated_vault();
|
||||||
|
|
|
||||||
|
|
@ -647,13 +647,13 @@ async fn run_sign_task(signer: Signer, app: Arc<tokio::sync::Mutex<App>>, uri: C
|
||||||
client.connect().and_wait(CONNECT_TIMEOUT).await;
|
client.connect().and_wait(CONNECT_TIMEOUT).await;
|
||||||
|
|
||||||
// 4. Subscribe to the client's kind 24133 events so we hear its requests.
|
// 4. Subscribe to the client's kind 24133 events so we hear its requests.
|
||||||
// Do not use `stream_events` here: it is an auto-closing historical-event
|
// `stream_events` opens its internal notification receiver as part of
|
||||||
// helper and ends at EOSE. NIP-46 needs a long-lived subscription because
|
// subscribing, which must happen BEFORE we announce (step 5): the client
|
||||||
// the app sends requests after the initial handshake.
|
// acknowledges within milliseconds and a receiver created afterwards would
|
||||||
|
// miss those early messages (tokio broadcast semantics).
|
||||||
let filter = Filter::new().kind(Kind::NostrConnect).author(uri.peer);
|
let filter = Filter::new().kind(Kind::NostrConnect).author(uri.peer);
|
||||||
let mut notifications = client.notifications();
|
let mut events = match client.stream_events(filter).await {
|
||||||
let subscription = match client.subscribe(filter).await {
|
Ok(events) => events,
|
||||||
Ok(subscription) => subscription,
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
signer.fail(format!("Could not subscribe for messages: {err}"));
|
signer.fail(format!("Could not subscribe for messages: {err}"));
|
||||||
return;
|
return;
|
||||||
|
|
@ -666,22 +666,20 @@ async fn run_sign_task(signer: Signer, app: Arc<tokio::sync::Mutex<App>>, uri: C
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Answer requests until stopped. Notifications are persistent and do
|
// 6. Answer requests until the connection goes away or we are stopped.
|
||||||
// not terminate after EOSE, unlike `stream_events`.
|
|
||||||
loop {
|
loop {
|
||||||
let incoming = match notifications.next().await {
|
let (relay_url, incoming) = match events.next().await {
|
||||||
Some(nostr_sdk::client::ClientNotification::Event {
|
Some(next) => next,
|
||||||
subscription_id,
|
None => {
|
||||||
event,
|
|
||||||
..
|
|
||||||
}) if subscription_id == *subscription.id() => event,
|
|
||||||
Some(nostr_sdk::client::ClientNotification::Shutdown) | None => {
|
|
||||||
signer.fail("The signer connection was closed.");
|
signer.fail("The signer connection was closed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Some(_) => continue,
|
|
||||||
};
|
};
|
||||||
let event = *incoming;
|
let event = match incoming {
|
||||||
|
Ok(event) => event,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let _ = relay_url;
|
||||||
if event.kind != Kind::NostrConnect || event.pubkey != uri.peer {
|
if event.kind != Kind::NostrConnect || event.pubkey != uri.peer {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue