Compare commits

...

250 Commits

Author SHA1 Message Date
Gigi
7a3dd421fb chore: bump version to 0.1.7 2025-10-05 12:55:10 +01:00
Gigi
4d95657bca refactor: keep Bookmarks.tsx under 210 lines by extracting logic
Extracted large functions into separate modules to follow DRY principles
and keep files manageable:

- Created useArticleLoader.ts hook (92 lines)
  - Handles article loading from naddr
  - Fetches article content and highlights
  - Sets up article coordinate for refresh

- Created contentLoader.ts utility (44 lines)
  - Handles both Nostr articles and web URLs
  - Unified content loading logic
  - Reusable across components

Result: Bookmarks.tsx reduced from 282 to 208 lines 

All files now under 210 line limit while maintaining functionality.
2025-10-05 12:47:32 +01:00
Gigi
6f28c3906c fix: show highlights for nostr articles by skipping URL filter
The HighlightsPanel was filtering out ALL highlights that didn't have
a urlReference. But Nostr article highlights reference the article via
the 'a' tag (article coordinate), not a URL.

Since we already fetch highlights specifically for the current article
using fetchHighlightsForArticle(), we don't need to filter them again.

Solution:
- Skip URL filtering when selectedUrl starts with 'nostr:'
- Keep URL filtering for web articles (backwards compatible)
- Highlights are already pre-filtered by the fetch query

This fixes the issue where 101 highlights existed for the default
article but weren't being displayed in the UI.
2025-10-05 12:37:28 +01:00
Gigi
fafe378585 fix: remove ImportMeta interface redeclaration
- ImportMeta is already defined as built-in global by vite/client
- Keep only ImportMetaEnv extension for custom env variables
- Fixes eslint no-redeclare error
2025-10-05 12:23:50 +01:00
Gigi
70b85b0cf0 fix: refresh button now works without login for article highlights
- Track current article coordinate and event ID in state
- Update handleFetchHighlights to refresh article highlights if viewing article
- Fall back to fetching user's highlights only if logged in and not viewing article
- Refresh button now works for anonymous article viewing
- No longer requires activeAccount to refresh highlights

Previously the refresh button only worked when logged in because it tried
to fetch highlights BY the user. Now it intelligently fetches highlights
FOR the current article, or falls back to user highlights if logged in.
2025-10-05 12:23:08 +01:00
Gigi
2297d8ae96 fix: query highlights using both a-tag and e-tag
- Highlights on replaceable events include BOTH 'a' and 'e' tags
- Query for highlights using article coordinate (#a tag)
- Also query using event ID (#e tag) for comprehensive results
- Combine and deduplicate results from both queries
- Add detailed logging to help diagnose why highlights aren't found
- Suggest checking highlighter.com if no highlights found

Per NIP-84 and applesauce implementation, highlights on kind:30023
articles include both an addressable reference ('a' tag) and an event
reference ('e' tag).
2025-10-05 09:19:43 +01:00
Gigi
343f176f06 debug: add detailed logging for highlight fetching
- Log article details (event ID, author, kind, d-tag, coordinate)
- Log filter being used for highlight queries
- Log sample highlight tags when found
- This will help debug why highlights aren't showing
2025-10-05 09:18:55 +01:00
Gigi
ee788cffb0 feat: add caching for nostr-native articles
- Add localStorage caching for kind:30023 articles (same as web articles)
- Cache TTL: 7 days
- Cache key prefix: article_cache_
- Add bypassCache parameter to fetchArticleByNaddr()
- Log cache hits and misses for debugging
- Gracefully handle storage errors

Articles are now cached locally after first fetch, making subsequent
loads instant and reducing relay queries.
2025-10-05 09:17:07 +01:00
Gigi
ca46feb80f fix: fetch highlights by article reference instead of author
- Add fetchHighlightsForArticle() to query highlights by article coordinate
- Use #a tag filter to find highlights that reference the article
- Query well-known relays for highlights even without authentication
- Extract article's d-tag and construct coordinate (kind:pubkey:identifier)
- Keep original fetchHighlights() for fetching user's own highlights
- Add detailed logging for debugging highlight fetching

This fixes the issue where no highlights were shown because we were
querying for highlights created BY the article author rather than
highlights created ABOUT the article.
2025-10-05 09:12:01 +01:00
Gigi
82ab07e606 feat: configure default article via environment variable
- Add VITE_DEFAULT_ARTICLE_NADDR env variable support
- Create .env with default article naddr
- Create .env.example for documentation
- Add vite-env.d.ts for TypeScript type support
- Fallback to hardcoded value if env var not set
- Using Vite's built-in env variable support (no dotenv needed)
2025-10-05 09:08:10 +01:00
Gigi
1f5e3f82b0 feat: load default article on startup with collapsed sidebars
- Redirect root path to default article (naddr)
- Start with both sidebars (bookmarks and highlights) collapsed
- Auto-fetch and show highlights for the article author
- No authentication required to view articles
- Highlights panel auto-expands when article loads
- Login page moved to /login route
2025-10-05 09:06:54 +01:00
Gigi
6265af74f2 docs: clarify why we extract image tag directly in BookmarkItem
Add comment explaining that we extract the image tag directly from
bookmark.tags since we don't have the full NostrEvent here. When we
do have full events (like in articleService), we use getArticleImage()
helper from applesauce-core as intended.
2025-10-05 08:24:05 +01:00
Gigi
e8f44986da feat: display article hero images in bookmark views and reader
- Add image prop to ContentPanel to display hero images
- Extract image tag from kind:30023 bookmark tags
- Display article images in Card, Large, and Compact views
- Show hero image at top of article reader view
- Add CSS styling for article-hero-image and reader-hero-image
- Article images clickable to open article in reader
- Per NIP-23: image tag contains header/preview image URL
2025-10-05 08:22:46 +01:00
Gigi
3d304dab15 fix: use bookmark pubkey for article author instead of tag lookup
- Pass pubkey along with bookmark data to handleSelectUrl
- Use bookmark.pubkey directly when constructing naddr
- More reliable article loading with correct author attribution
- Update type signatures across all components
2025-10-05 08:20:38 +01:00
Gigi
0f7a4d7877 feat: enable clicking on kind:30023 articles to open in reader
- Update handleSelectUrl to detect kind:30023 bookmarks
- Construct naddr from article event data (pubkey, d tag)
- Fetch and render articles using article service
- Update all bookmark views (Compact, Card, Large) to handle articles
- Show 'Read Article' button for kind:30023 bookmarks
- Articles load in the existing ContentPanel with full reader features
2025-10-05 08:19:50 +01:00
Gigi
d5e847e515 feat: show article titles for kind:30023 bookmarks
- Update hydrateItems to detect long-form articles (kind:30023)
- Extract and display article title using getArticleTitle helper
- Article titles now appear as bookmark content in lists
- Provides better context for bookmarked articles
2025-10-05 08:17:34 +01:00
Gigi
edd4e20e22 refactor: integrate long-form article rendering into existing reader view
- Create articleService to fetch articles by naddr
- Update Bookmarks component to detect naddr in URL params
- Articles now render in the existing ContentPanel with highlight support
- Remove standalone Article component
- Articles work seamlessly within the existing three-pane layout
- Support for article metadata (title, image, published date, summary)
2025-10-05 08:12:55 +01:00
Gigi
9b0c59b1ae feat: add native support for rendering Nostr long-form articles (NIP-23)
- Install react-router-dom for routing support
- Create Article component to decode naddr and fetch/render articles
- Add /a/:naddr route to App.tsx for article viewing
- Use applesauce relay pool patterns for event fetching
- Render articles with markdown using ReactMarkdown
- Support article metadata (title, image, published date, summary)
2025-10-05 08:08:34 +01:00
Gigi
8faa2e2de0 chore: bump version to 0.1.6 2025-10-05 04:17:44 +01:00
Gigi
07a5826774 refactor: extract components to keep files under 210 lines
- Extract ColorPicker component from Settings
- Extract FontSelector component from Settings
- Move hexToRgb helper to colorHelpers utils
- Export HIGHLIGHT_COLORS constant from colorHelpers
- Settings.tsx now 209 lines (was 242)
- ContentPanel.tsx now 197 lines (was 204)

Keeps code DRY and improves maintainability
2025-10-05 04:17:03 +01:00
Gigi
21d6916ae3 fix: ensure highlight color CSS variable inherits from parent
Remove local --highlight-rgb declarations that were preventing color inheritance in preview
2025-10-05 04:14:11 +01:00
Gigi
482ba9b2df style: make font size and color buttons match icon button size (33px) 2025-10-05 04:13:28 +01:00
Gigi
e4b6d1a122 feat: add configurable highlight colors
- Add highlightColor setting with 6 preset colors (yellow, orange, pink, green, blue, purple)
- Implement color picker UI with square color swatches
- Use CSS variables to dynamically apply highlight colors
- Add hex to RGB conversion for color transparency support
- Update both marker and underline styles to use selected color
2025-10-05 04:12:31 +01:00
Gigi
b59a295ad3 feat: add highlight style setting (marker & underline) 2025-10-05 04:08:58 +01:00
Gigi
e771b9778f chore: bump version to 0.1.5 2025-10-05 04:05:05 +01:00
Gigi
a95c0ed3ff refactor: reduce file sizes to meet 210 line limit
- Extract URL normalization to urlHelpers utility (DRY)
- Condense Settings.tsx from 212 to 190 lines
  - Inline IconButton props on single lines
  - Shorten preview text
- Condense ContentPanel.tsx from 223 to 190 lines
  - Extract filterHighlightsByUrl function
  - Remove unnecessary logic
- All files now under 210 line limit
- All lint and type checks pass
2025-10-05 04:02:49 +01:00
Gigi
4b9a4fb286 refactor: extract settings logic into custom hook
- Create useSettings hook to handle settings loading, saving, and watching
- Move settings-related logic out of Bookmarks component
- Move font loading logic into the hook
- Reduce Bookmarks.tsx from 221 to 167 lines (under 210 limit)
- Keep code DRY by centralizing settings management
- All lint and type checks pass
2025-10-05 04:01:09 +01:00
Gigi
0c9f68c5cd fix: resolve linter errors
- Remove unused FontAwesomeIcon import from Settings component
- Remove invalid eslint-disable comment for non-configured rule
- Add onSave back to useEffect dependencies to satisfy linter
- All lint checks and type checks now pass
2025-10-05 03:58:19 +01:00
Gigi
850a4bede5 fix: prevent settings from saving unnecessarily
- Wrap handleSaveSettings in useCallback to prevent recreation
- Only trigger save effect when localSettings object changes
- Remove onSave from dependency array with eslint-disable comment
- Settings now only save when user actually changes a setting
2025-10-05 03:56:49 +01:00
Gigi
89c00035a8 refactor: remove debounce from settings auto-save
- Settings now save immediately when changed
- Remove setTimeout debounce logic
- Keep code simple and straightforward
2025-10-05 03:55:15 +01:00
Gigi
61307fc22d feat: implement auto-save for settings with toast notifications
- Create Toast component for user feedback
- Add toast notification styles with slide-in animation
- Remove Save Settings button from Settings component
- Implement auto-save with 500ms debounce on setting changes
- Show success/error toast messages when settings are saved
- Settings now save automatically as user makes changes
- Improves UX by eliminating manual save step
2025-10-05 03:53:02 +01:00
Gigi
31610af706 fix: increase bottom padding in settings content area
- Increase bottom padding in .settings-content from 1rem to 2rem
- Ensures Save Settings button is fully visible and not cut off
- Improves scrollable area spacing
2025-10-05 03:49:42 +01:00
Gigi
e9cbe56bc0 refactor: consolidate settings initialization on login
- Merge settings load and subscription setup into single useEffect
- Ensure settings are loaded immediately upon successful login
- Set up watchSettings subscription at the same time as initial load
- Add eventStore as dependency to ensure proper initialization
- Improves timing and prevents race conditions
2025-10-05 03:48:32 +01:00
Gigi
09bbe10aa6 feat: add settings subscription to watch for Nostr updates
- Import and use watchSettings from settingsService
- Subscribe to settings changes in eventStore
- Automatically update app state when settings change from Nostr
- Properly cleanup subscription on component unmount
- Fixes settings resetting on browser refresh
2025-10-05 03:47:32 +01:00
Gigi
188147d057 fix: update originalHtmlRef when content changes
- Remove faulty conditional that prevented HTML ref from updating
- Now properly stores fresh content when switching articles
- Fixes issue where articles weren't switching properly
2025-10-05 03:42:28 +01:00
Gigi
91fe1711cd fix: move readingStats hook before early returns
- Fixes React Hooks order violation
- All hooks must be called unconditionally in the same order
- Moved readingStats useMemo before the conditional returns
- Resolves 'Rendered more hooks than during the previous render' error
2025-10-05 03:38:42 +01:00
Gigi
cc0b27f7cd fix: replace custom reading time with reading-time-estimator package
- Remove custom readingTime.ts implementation
- Install reading-time-estimator package (browser-compatible)
- Update ContentPanel to use named import from reading-time-estimator
- Fixes browser compatibility issues with reading-time package
- All linting and type checks pass
2025-10-05 03:35:22 +01:00
Gigi
0a3bb72d89 fix: prevent save settings button from being cut off
- Add padding-bottom to settings-content for breathing room
- Add bottom padding to settings-footer
- Add flex-shrink: 0 to footer to prevent it from being compressed
- Ensures Save Settings button is always fully visible
2025-10-05 03:29:24 +01:00
Gigi
2719ad3602 feat: add reading time estimate to articles
- Install reading-time package
- Calculate reading time from article content (html or markdown)
- Display reading time with clock icon in article header
- Strip HTML tags for accurate word count
- Style reading-time indicator similar to highlights
- Shows estimated reading time (e.g., '5 min read')
2025-10-05 03:28:36 +01:00
Gigi
02798e99e8 style: add max-width to main pane for better readability
- Set max-width of 900px for the main content pane
- Center content with margin: 0 auto
- Add horizontal padding for breathing room
- Improves reading experience by preventing overly wide lines
2025-10-05 03:26:54 +01:00
Gigi
aba91f7a52 feat: inline font selector with styled options and fix font size 2025-10-05 03:26:18 +01:00
Gigi
dea647f36a fix: font and size settings now apply 2025-10-05 03:23:45 +01:00
Gigi
8be57dea2c style: improve checkbox alignment and styling
- Fix checkbox alignment with flex display override
- Reduce checkbox size from 20px to 18px
- Add accent-color to match theme
- Improve text color and spacing
- All checkboxes now properly aligned
2025-10-05 03:20:09 +01:00
Gigi
14a429ca61 feat: replace font size dropdown with icon buttons
- Replace dropdown with visual 'A' buttons at different sizes
- Buttons show the actual size they represent
- Active state highlights selected size
- Consistent with view mode button pattern
- More intuitive and visual UX
2025-10-05 03:18:43 +01:00
Gigi
caf73b7c9f refactor: simplify setting label to 'Show highlights'
- Change 'Show highlight underlines' to 'Show highlights'
- More concise and clear
2025-10-05 03:17:06 +01:00
Gigi
915829e82c fix: make font size setting work in preview
- Change h3 font-size from 1.5rem to 1.5em to scale with parent
- Remove hardcoded font-size from preview paragraphs
- Now font size changes are properly reflected in the preview
2025-10-05 03:16:42 +01:00
Gigi
dd3203f34f refactor: remove loading state and show bookmarks as they arrive
- Remove loading state variable from Bookmarks component
- Remove 'Loading bookmarks...' screen
- Start with empty list and populate bookmarks in background
- Remove timeout and loading callbacks from fetchBookmarks
- Better UX: show UI immediately, bookmarks appear when ready
- Bookmarks.tsx now at 197 lines
2025-10-05 03:15:57 +01:00
Gigi
183af3a80c feat: add font size setting
- Add fontSize to UserSettings interface
- Add font size dropdown in Settings with options from 12px to 22px
- Apply font size to preview content instantly
- Set --reading-font-size CSS variable in Bookmarks when settings change
- Apply font-size variable to .reader-html and .reader-markdown
- Condense code in Bookmarks.tsx to stay under 210 lines (now at 207)
2025-10-05 03:13:31 +01:00
Gigi
3e441925e5 feat: close settings when opening an article
- Add setShowSettings(false) to handleSelectUrl
- Ensures settings view closes when user selects a bookmark to read
- File remains at 210 lines
2025-10-05 03:10:46 +01:00
Gigi
9a1efd5b18 fix: prevent duplicate highlight application
- Store original HTML in ref to prevent re-highlighting already highlighted content
- Separate highlight application from click handler attachment effects
- Remove onHighlightClick from highlight application dependencies
- Remove verbose console logging for cleaner code
- Highlights now apply correctly without stacking on top of each other
2025-10-05 03:09:59 +01:00
Gigi
af6538d577 refactor: reduce Bookmarks.tsx to exactly 210 lines
- Remove extra blank line between imports and type definition
- File now at exactly 210 lines
2025-10-05 03:07:29 +01:00
Gigi
0a78d19195 refactor: simplify Bookmarks.tsx to meet 210 line limit
- Remove verbose console.log statements
- Simplify handlers and remove unnecessary blank lines
- Condense font loading logic
- Keep code DRY and simple
- File now at exactly 210 lines
2025-10-05 03:06:36 +01:00
Gigi
25a1adaeaa feat: open highlights panel when clicking highlight in article
- Simple inline handler that opens panel if collapsed
- Sets selected highlight ID
- Clean, minimal implementation
2025-10-05 03:04:40 +01:00
Gigi
5c2c8f618c feat: auto-open highlights panel when clicking highlight in article
- Add handleHighlightClick that opens panel if collapsed
- Sets selected highlight ID to scroll to it in the panel
- Add console logging for debugging
- When user clicks highlight in article with panel closed, panel opens automatically
2025-10-05 03:02:56 +01:00
Gigi
726b8a9c10 Revert "feat: auto-open highlights panel when clicking a highlight"
This reverts commit b6edf8de73.
2025-10-05 03:00:03 +01:00
Gigi
b6edf8de73 feat: auto-open highlights panel when clicking a highlight
- Create handleHighlightClick handler that sets highlight ID and opens panel
- Automatically expand highlights sidebar if collapsed when highlight clicked
- Improves UX by ensuring highlights panel is visible when interacting with highlights
- Applied to both ContentPanel and HighlightsPanel click handlers
2025-10-05 02:58:29 +01:00
Gigi
e10ae00a1a refactor: organize settings into logical groups
- Create three sections: Reading & Display, Layout & Navigation, Startup Preferences
- Add section titles with consistent styling
- Group related settings together:
  - Reading & Display: font, highlight underlines, preview
  - Layout & Navigation: view mode, collapse behavior
  - Startup Preferences: initial sidebar states
- Add section title styling with border and uppercase text
- Better visual hierarchy and organization
2025-10-05 02:57:04 +01:00
Gigi
bb351ae35f feat: preview reflects show underlines setting
- Preview highlight dynamically shows/hides based on showUnderlines setting
- Users can see highlight appearance changes instantly
- Conditional className applied to preview highlight
2025-10-05 02:55:53 +01:00
Gigi
ad1e06c867 feat: add highlight example to preview
- Add highlighted text in preview using content-highlight class
- Shows how highlights appear with different fonts
- Helps users see complete reading experience before saving
2025-10-05 02:53:55 +01:00
Gigi
bf8dfe79dd feat: add live preview of reading font in settings
- Add preview section showing Lorem Ipsum passage
- Preview updates instantly when font is changed
- Load font dynamically for preview
- Style preview to match reader appearance
- Helps users see font changes before saving
2025-10-05 02:53:22 +01:00
Gigi
0ccad88dfd feat: add configurable reading font using Bunny Fonts
- Add readingFont setting to UserSettings interface
- Create fontLoader utility to load fonts from Bunny Fonts
- Add font selector dropdown in settings with popular reading fonts
- Use CSS variable --reading-font to apply font to reader content
- Support fonts: Inter, Lora, Merriweather, Open Sans, Roboto, Source Serif 4, Crimson Text, Libre Baskerville, PT Serif
- Fonts loaded from https://fonts.bunny.net/ (GDPR-friendly)
2025-10-05 02:52:21 +01:00
Gigi
20e9ba1675 style: inline default view mode label and buttons
- Put label and icon buttons on same line
- Remove background container from view mode buttons
- Add setting-inline and setting-buttons classes
- Clean, minimal inline layout without background styling
2025-10-05 02:49:55 +01:00
Gigi
85d3508190 fix: remove header bar styling from settings
- Remove background, border, and bar styling from settings header
- Keep simple header with title and close button on same line
- Match padding with content panel for proper alignment
- Clean, minimal header without visual container
2025-10-05 02:48:27 +01:00
Gigi
4cf0138706 style: align settings header with sidebar header bar
- Change settings-header to settings-header-bar class
- Match styling of sidebar-header-bar (background, border, padding)
- Reduce title font size to match sidebar style
- Adjust padding and spacing for consistent visual alignment
- Settings header now appears on same visual line as sidebar buttons
2025-10-05 02:46:59 +01:00
Gigi
d6e093b3bb refactor: use icon buttons for default view mode setting
- Replace dropdown select with icon-based view switcher
- Use same icons as sidebar header (list, grid, image)
- Add left-aligned view-mode-controls styling for settings
- Maintain consistent UI across view mode selectors
2025-10-05 02:44:58 +01:00
Gigi
6489714f33 style: left-align all settings text and controls
- Add text-align: left to settings view, header, and content
- Add justify-content: flex-start to checkbox labels
- Add flex-shrink: 0 to checkboxes to prevent squishing
- Ensure consistent left alignment throughout settings panel
2025-10-05 02:43:34 +01:00
Gigi
e2749b6a3c feat: collapse both sidepanels when opening settings
- Automatically collapse bookmarks sidebar when settings opened
- Automatically collapse highlights panel when settings opened
- Provides full-width settings view for better UX
2025-10-05 02:41:30 +01:00
Gigi
89e089ad25 refactor: render settings in main pane instead of as overlay
- Move Settings component from overlay to main pane
- Update Settings styling for inline display
- Conditionally render Settings or ContentPanel in main pane
- Remove overlay-specific styles and simplify layout
2025-10-05 02:40:38 +01:00
Gigi
7e5196d73d fix: resolve TypeScript and lint errors 2025-10-05 02:38:51 +01:00
Gigi
1b8c276529 chore: upgrade applesauce packages to v4.0.0
- Update all applesauce packages from 3.1.0 to 4.0.0
- Add applesauce-factory dependency
- Version 4.0.0 includes app-data helpers needed for NIP-78
2025-10-05 02:35:28 +01:00
Gigi
1b381a0f8c fix: export app-data helpers from applesauce-core
- Add app-data.js export to helpers index
- Update import path in settingsService to use applesauce-core/helpers
2025-10-05 02:33:46 +01:00
Gigi
35aa9d6cce feat: add collapse-on-article-open setting
- Add collapseOnArticleOpen setting (default: true)
- Position as first setting in settings panel
- Auto-collapse bookmark bar when user opens an article
- User can disable this behavior in settings
2025-10-05 02:31:23 +01:00
Gigi
f8c8ab5402 feat: add settings panel with NIP-78 storage
- Create settings service using Kind 30078 for user preferences
- Add Settings component with UI for configuring app preferences
- Wire settings icon to open settings modal
- Store settings like default view mode, sidebar collapse states, etc.
- Use d tag: com.dergigi.boris.user-settings
2025-10-05 02:30:30 +01:00
Gigi
67f0a0b3b6 feat: add settings icon next to user profile 2025-10-05 02:23:21 +01:00
Gigi
4ab9d238d5 feat: set compact view as default bookmark view 2025-10-05 02:21:20 +01:00
Gigi
e0ddd43fe4 feat: add localStorage caching for fetched articles
- Cache articles in localStorage with 7-day TTL
- Check cache before fetching from jina.ai
- Add optional bypassCache parameter
- Automatically expire and cleanup old cached content
2025-10-05 02:14:53 +01:00
Gigi
5f8d4b2c47 fix: delay pulse animation to let scroll complete first
- Add 500ms delay before starting pulse animation
- Prevents pulse from starting while element is still scrolling
- Creates better visual flow: scroll → pause → pulse
- Makes the highlight easier to track with your eyes

The pulse now starts after the smooth scroll completes,
making it much clearer which highlight you jumped to.
2025-10-05 02:10:17 +01:00
Gigi
a4c15ecc0e feat: add pulsing animation when scrolling to highlight
- Replace brightness change with subtle pulse animation
- Pulse twice over 1.5 seconds with scale and glow effects
- Scale slightly (1.02x) and increase shadow glow
- More elegant visual feedback than color change
- Easier to spot without being jarring

The highlight now pulses twice when clicked from the
sidebar, making it easy to see where you've jumped to.
2025-10-05 02:08:51 +01:00
Gigi
967aac49ef fix: scroll to highlight in article when clicking sidebar item
- Add selectedHighlightId prop to ContentPanel
- Add useEffect to watch for selectedHighlightId changes
- Find and scroll to the corresponding mark element
- Temporarily brighten the highlight for visual feedback
- Pass selectedHighlightId from Bookmarks to ContentPanel

Now clicking a highlight in the sidebar properly scrolls
to and highlights the text in the article view.
2025-10-05 02:07:05 +01:00
Gigi
d9b50f80d0 feat: scroll to highlight in article when clicking highlight item
- Add onHighlightClick callback to HighlightItem
- Make entire highlight item clickable with pointer cursor
- Reuse existing setSelectedHighlightId to trigger scroll
- Clicking a highlight in sidebar scrolls to it in article view
- Works with existing click-to-scroll from article to sidebar

Users can now click highlights in either direction:
- Click highlight in article → scrolls to item in sidebar
- Click highlight in sidebar → scrolls to text in article
2025-10-05 02:03:28 +01:00
Gigi
6ae101c9c6 feat: make bookmark icon glow blue when article is bookmarked
- Bookmark icon glows blue when selectedUrl matches a bookmark
- Use app's primary blue color (#646cff) for consistency
- Check bookmark URLs with flexible matching (exact, includes)
- Pass selectedUrl to BookmarkList component
- Add glow-blue CSS class with drop-shadow effect

The bookmark icon now glows blue when viewing a bookmarked
article, providing visual feedback that it's in your collection.
2025-10-05 02:00:53 +01:00
Gigi
a62e493590 feat: make highlighter icon glow when article has highlights
- Highlighter icon glows yellow when filteredHighlights > 0
- Add pulsing animation for subtle attention-grabbing effect
- Use highlight color (#ffff00) with drop-shadow for glow
- Only applies when highlights panel is collapsed
- Provides visual feedback that highlights are available

The icon now pulses with a yellow glow when the current
article has highlights, making it easy to see at a glance.
2025-10-05 01:58:18 +01:00
Gigi
9f251d43ad feat: add bookmark icon to collapsed bookmarks panel button
- Show both chevron and bookmark icons when collapsed
- Button width adjusts to fit both icons
- Add gap between icons for proper spacing
- Mirrors the highlights panel behavior
- Makes it clear what the panel contains when collapsed

Both sidebars now show their respective icons (bookmark/highlighter)
when collapsed for better visual consistency and clarity.
2025-10-05 01:56:26 +01:00
Gigi
b326a9d5b3 feat: add highlighter icon to collapsed highlights panel button
- Show both highlighter and chevron icons when collapsed
- Button width adjusts to fit both icons
- Add gap between icons for proper spacing
- Makes it clear what the panel contains when collapsed
2025-10-05 01:55:13 +01:00
Gigi
749270b698 fix: chevrons point outward when panels are collapsed
- Left sidebar collapsed: chevron points left (←) outward
- Right sidebar collapsed: chevron points right (→) outward

This makes it more intuitive - the chevrons point away from
the center when collapsed, indicating the direction to expand.
2025-10-05 01:54:15 +01:00
Gigi
b34d8172e0 refactor: unify collapse/expand mechanics for both sidebars
- Left sidebar: chevron points right (→) when collapsed
- Right sidebar: chevron points left (←) when collapsed
- Both use same button size (36x36px)
- Both use same positioning (top-aligned with 0.75rem padding)
- Both use same styling (background, border, hover states)
- Left sidebar aligns to right, right sidebar to left (mirrored)
- Remove rotation transforms for cleaner implementation

The collapse/expand mechanics now feel and look identical
but properly mirrored for left and right panels.
2025-10-05 01:52:16 +01:00
Gigi
2b9b7d0ebf feat: add refresh button to highlights panel
- Add refresh button with rotate icon to highlights header
- Button spins while loading highlights
- Disabled state when already loading
- Positioned before toggle underlines button
- Calls handleFetchHighlights to refetch from relays
- Add CSS styles for refresh button with disabled state

Users can now manually refresh highlights to see newly
created highlights without reloading the page.
2025-10-04 22:27:38 +01:00
Gigi
2ca23d67de fix: improve collapsed highlights panel button placement
- Change chevron to point right (rotation 180) when collapsed
- Position button at top-left instead of center
- Align with header position for consistency
- Adjust padding to match expanded state header

The expand button now appears at the top of the panel
next to where the highlight count would be, making it
more intuitive and consistent with the UI.
2025-10-04 22:24:18 +01:00
Gigi
a941449ba4 style: change highlights to fluorescent marker style
- Replace underline with semi-transparent yellow background
- Add subtle glow effect with box-shadow
- Add slight padding and border-radius for marker look
- Increase opacity on hover for better feedback
- Adjust colors for both light and dark modes
- Change cursor from 'help' to 'pointer' for clarity

Highlights now look like they were marked with a real
fluorescent highlighter marker instead of just underlined.
2025-10-04 22:22:11 +01:00
Gigi
05636046a8 feat: add click-to-scroll for highlights
- Clicking a highlight in the main text scrolls to it in the sidebar
- Selected highlight is visually highlighted with border and shadow
- Add selectedHighlightId state management in Bookmarks component
- Add click handlers to mark elements in ContentPanel
- Add isSelected prop to HighlightItem with scroll-into-view
- Add CSS styles for selected highlight state
- Set cursor to pointer on clickable highlights

Users can now click on highlighted text to jump to the corresponding
highlight in the right sidebar for easy navigation.
2025-10-04 22:21:43 +01:00
Gigi
1bcaa1998d chore: bump version to 0.1.4 2025-10-04 22:14:00 +01:00
Gigi
e98dc1c5da fix: resolve all linting and type errors
- Remove unused applyHighlightsToText import from ContentPanel
- Replace while(true) with proper condition in findHighlightMatches
- Remove unused match parameter from replaceTextWithMark function

All ESLint and TypeScript checks now pass with no errors.
2025-10-04 22:13:31 +01:00
Gigi
aa8d3c285d fix: apply highlights to markdown content as well as HTML
- Update useEffect to check for both html and markdown content
- Add contentRef to markdown div for DOM manipulation
- Add markdown to useEffect dependencies
- Improve logging to show which content type is available

This fixes the issue where highlights weren't appearing because
the reader service was returning markdown instead of HTML.
2025-10-04 22:09:38 +01:00
Gigi
9ac8e8f69c fix: use requestAnimationFrame for highlight DOM manipulation
- Replace setTimeout with requestAnimationFrame for proper DOM timing
- Ensures contentRef is available before applying highlights
- Reorganize useEffect logic for clearer flow
- Add more specific logging for debugging

This fixes the issue where highlights weren't appearing because
the effect ran before React finished rendering the HTML content.
2025-10-04 22:00:19 +01:00
Gigi
842bfa5491 feat: add toggle button to show/hide highlight underlines
- Add eye/eye-slash toggle button in highlights panel header
- Button only appears when there are highlights to show
- Clicking toggles underlines on/off in the main content panel
- When hidden, removes existing <mark> elements from DOM
- Add showUnderlines state management through Bookmarks component
- Style toggle button consistently with collapse button
- Add highlights-actions container for button group

Users can now toggle highlight visibility without losing the highlight list.
2025-10-04 21:54:18 +01:00
Gigi
e2e5d59197 debug: add detailed logging to highlight application useEffect
- Log when useEffect is triggered
- Log contentRef status, relevant highlights count, and html presence
- Log specific reason when skipping highlight application
- This will help identify why highlights aren't being applied to DOM
2025-10-04 21:52:22 +01:00
Gigi
0255ff5d03 feat: filter highlights panel to show only current article
- Add selectedUrl prop to HighlightsPanel
- Filter highlights by URL using same normalization logic as ContentPanel
- Update count badge to show filtered count
- Improve empty state message based on context
- Now shows "No highlights for this article" instead of all highlights

This makes the highlights panel contextual to the current article being viewed.
2025-10-04 21:50:20 +01:00
Gigi
930cd272cb fix: apply highlights after DOM renders to fix timing issue
- Use useEffect to apply highlights after HTML is rendered
- Add 100ms delay to ensure DOM is fully parsed
- Use ref to directly manipulate rendered content
- Remove pre-rendering highlight application from useMemo
- Add detailed logging for debugging

This fixes the issue where highlights weren't appearing because they
were being applied to the HTML string before the DOM was ready.
2025-10-04 21:44:08 +01:00
Gigi
2dea3c2a5c style: change highlights to yellow underline
- Remove background color, use transparent background
- Change border-bottom from blue to gold/yellow (#ffd700)
- Add subtle yellow background on hover
- Adjust light mode colors for better contrast
2025-10-04 20:45:54 +01:00
Gigi
38b80bc85b refactor: DRY up highlightMatching to stay under 210 lines
- Extract helper functions: normalizeWhitespace, createMarkElement, replaceTextWithMark
- Consolidate duplicate exact/normalized matching logic into tryMarkInTextNodes
- Reduce from 242 lines to 209 lines
- Maintain all functionality while improving code reusability
2025-10-04 20:45:06 +01:00
Gigi
c0de624fe6 refactor: use applesauce helpers for highlight parsing
- Replace manual tag parsing with applesauce-core helper functions
- Use getHighlightText, getHighlightContext, getHighlightComment, etc.
- Add support for highlight comments in UI
- Extract author from attributions using proper helper
- Handle both event and address pointers correctly
- Add styling for highlight comments

This follows applesauce best practices and makes the code more robust.
2025-10-04 20:41:26 +01:00
Gigi
1d7ab59272 feat: deduplicate highlight events by ID
- Add dedupeHighlights function to remove duplicate events from multiple relays
- Log both raw and deduplicated event counts for debugging
- Follows same pattern as bookmark deduplication
2025-10-04 20:39:25 +01:00
Gigi
0803417755 feat: improve highlight URL and text matching
- Use proper URL parsing to normalize URLs (remove www, query params, fragments)
- Add detailed logging for URL comparison to debug matching issues
- Implement two-pass text matching: exact match first, then normalized whitespace
- Handle whitespace variations in highlighted text more flexibly
- Add context to debug logs showing surrounding text

This should make highlights appear more reliably even with URL variations
and whitespace differences between the highlight and the actual content.
2025-10-04 20:32:55 +01:00
Gigi
a602f163fb fix: improve HTML highlight matching with DOM manipulation
- Replace simple string replacement with proper DOM tree walking
- Find text nodes and split them to insert mark elements
- Add extensive debugging to track highlight matching
- Handle text that spans across HTML elements correctly

This should fix the issue where highlights weren't showing up in
article content due to HTML tags breaking up the text.
2025-10-04 20:14:25 +01:00
Gigi
4aa496ec3f fix: improve highlights panel collapse behavior
- Flip chevron icon direction (left when collapsed, right when expanded)
- Match bookmarks sidebar styling for collapsed state
- Remove background/border when collapsed for cleaner look
- Ensure toggle button stays at top of panel
- Add proper hover states for collapsed button

The highlights panel now behaves consistently with the bookmarks sidebar,
with the chevron pointing in the correct direction and proper visual feedback.
2025-10-04 19:59:03 +01:00
Gigi
296600bb0d feat: add inline highlight annotations in content panel
- Create highlightMatching utility to find and apply highlights to text/HTML
- Update ContentPanel to accept highlights and match them to current URL
- Add visual highlighting with yellow background and blue underline
- Show highlight count indicator when content has highlights
- Add hover effects and tooltips showing highlight date
- Support both HTML and markdown content highlighting

Highlighted text now appears underlined in the main content panel when
viewing URLs that have associated NIP-84 highlights.
2025-10-04 19:58:10 +01:00
Gigi
7390104414 feat: add NIP-84 highlights panel with three-pane layout
- Add HighlightsPanel component with collapsible functionality
- Add HighlightItem component to display individual highlights
- Create highlightService to fetch kind 9802 events
- Add Highlight type definitions for NIP-84
- Update Bookmarks to support three-pane layout (bookmarks, content, highlights)
- Add comprehensive CSS styling for highlights panel
- Update README with highlights feature documentation

The highlights panel mirrors the bookmark sidebar on the right side, showing
all NIP-84 highlights with context, source links, and timestamps. Both panels
are independently collapsible for flexible viewing.
2025-10-04 19:47:45 +01:00
Gigi
f4fbc34bc1 fix: update remaining Markr references to Boris
- Update Login component welcome message
- Update package-lock.json name references
2025-10-03 15:00:13 +02:00
Gigi
e83976e5e0 feat: rename app from Markr to Boris
- Update package.json name field
- Update README.md title and references
- Update HTML page title
2025-10-03 14:58:28 +02:00
Gigi
0cf7f93482 refactor: split BookmarkItem into separate view components
- Extract CompactView, LargeView, and CardView into separate files
- Keep all files under 210 lines (BookmarkItem: 307→105 lines)
- Improve code organization and maintainability
- Add shared type definitions for view components
- Keep DRY with shared props object
2025-10-03 10:29:17 +02:00
Gigi
796380ea0d fix: move useEffect hook to top level to comply with Rules of Hooks
- Move OG image fetching useEffect to component top level
- Make hook logic conditional instead of hook call itself
- Prevents 'Rendered more hooks than during previous render' error
- Remove duplicate firstUrlClassification declaration
2025-10-03 10:26:40 +02:00
Gigi
3d6403f139 feat: fetch article hero images using free CORS proxy
- Add Open Graph image extraction from article HTML
- Use allorigins.win as free CORS proxy (no auth required)
- Implement HTML parsing to extract og:image meta tags
- Add in-memory caching to avoid repeated fetches
- Async loading with React useEffect for non-YouTube URLs
- 5 second timeout for fetch requests
- Graceful fallback to icon placeholder on errors
2025-10-03 10:24:34 +02:00
Gigi
57c5be9907 feat: add image preview for large view cards
- Extract YouTube video thumbnails from URLs
- Display thumbnail images as background in large preview cards
- Add gradient overlay for better text contrast
- Fallback to icon placeholder for non-YouTube URLs
- Handle multiple YouTube URL formats (watch, youtu.be, shorts)
- Gracefully handle missing images with icon fallback
2025-10-03 10:16:22 +02:00
Gigi
bd3193957c feat: implement large preview view mode
- Add large preview layout with image placeholder area
- Display truncated content (3 lines max) below preview
- Footer with author, timestamp, and action button
- Clickable preview area opens URL in reader
- Clean, minimalistic design with larger spacing
- All views now fully functional: compact, cards, and large
2025-10-03 10:10:17 +02:00
Gigi
64efb103a4 feat: make card view timestamp clickable to open event
- Timestamp in card view now links to event in search portal
- Add hover effect showing link is clickable
- Remove unused getKindIcon import
- All linter and type checks pass
2025-10-03 10:08:56 +02:00
Gigi
4afd9ed6d1 feat: enhance card view design with modern styling
- Add gradient backgrounds to cards and buttons
- Improve visual hierarchy with borders and dividers
- Enhance hover effects with better shadows and transitions
- Increase padding and spacing for better readability
- Add subtle gradients to bookmark type badges
- Improve kind icon styling with hover effects
- Better typography with increased line height and font sizes
2025-10-03 09:54:34 +02:00
Gigi
7e9cdfb0e1 chore: bump version to 0.1.3 2025-10-03 09:52:03 +02:00
Gigi
bdfb7ca9a6 feat: make entire compact list row clickable to open reader
- Add onClick handler to compact-row div
- Show pointer cursor on rows with URLs
- Add stopPropagation to action button to prevent double-trigger
- Include accessibility attributes (role, tabIndex)
2025-10-03 09:51:34 +02:00
Gigi
288b96d614 refactor: make compact list view even more compact
- Move all elements to a single horizontal line
- Reduce text preview from 100 to 60 characters
- Decrease padding and font sizes
- Fix row height to 28px for consistent spacing
- Improve text truncation with ellipsis
2025-10-03 09:49:07 +02:00
Gigi
99c6a4c23b feat: add view mode switching for bookmarks with compact list view
- Add ViewMode type with options: compact, cards, large
- Add view mode toggle buttons in SidebarHeader
- Implement compact list view rendering in BookmarkItem
- Add CSS styles for compact view with condensed layout
- Cards view remains the default and current style
2025-10-03 09:44:39 +02:00
Gigi
5727a38a7e chore: bump version to 0.1.2 2025-10-03 09:37:56 +02:00
Gigi
9046150d1f fix(ui): make sidebar and reader scroll independently
- Remove position: sticky from sidebar
- Set fixed height on two-pane container (calc(100vh - 4rem))
- Add overflow-y: auto to both sidebar and main panes
- Each pane now scrolls independently without affecting the other
- Fix issue where bookmark bar was 'stuck' with long articles
2025-10-03 09:31:04 +02:00
Gigi
53b54c77e7 feat(reader): open bookmark URLs in reader instead of new window
- Change URL links to buttons that open in reader
- Style URL buttons to look like links (cursor, hover, no button appearance)
- Rename 'content-panel' to 'reader' throughout codebase
- Update all CSS classes: content-panel → reader, content-title → reader-title, etc.
- Change empty state text from 'preview' to 'read' to match reader terminology
- Keep things simple and focused on in-app reading experience
2025-10-03 09:30:28 +02:00
Gigi
d6756dc5a1 refactor: remove duplicate formatDate function from helpers.ts
- Keep single formatDate implementation in bookmarkUtils.tsx
- Both BookmarkList and BookmarkItem already import from bookmarkUtils
- Maintain DRY principle by eliminating duplication
2025-10-03 09:27:56 +02:00
Gigi
5ea81bda8e fix(deps): replace relative-time with date-fns for timestamp formatting
- Replace relative-time package (which uses Temporal API) with date-fns
- Update formatDate to use formatDistanceToNow from date-fns
- Remove relative-time type declarations
- Apply fix to both helpers.ts and bookmarkUtils.tsx
- Fix runtime error: relative-time expects Temporal objects, not Date objects
- date-fns provides better compatibility with current JavaScript standards
2025-10-03 09:25:05 +02:00
Gigi
6ad273b5f9 fix(deps): correct relative-time package usage
- Change from calling relativeTime as function to instantiating RelativeTime class
- Use relativeTime.from(date) method instead of relativeTime(date)
- Update TypeScript type definitions to reflect class-based API
- Fix runtime error: 'Cannot call a class as a function'
- Apply fix to both bookmarkUtils.tsx and helpers.ts
2025-10-03 09:22:19 +02:00
Gigi
32bbda0364 docs(readme): update features, project structure, and add TODO section
- Update features list to reflect current functionality
- Add comprehensive project structure documentation
- Add TODO section with prioritized next steps
- Include high priority, medium priority, and nice-to-have items
- Add contributing guidelines
- Keep technical details about private bookmarks implementation
2025-10-03 02:08:38 +02:00
Gigi
857337c748 fix(ui): swap chevron directions for collapse/expand button
- When sidebar is expanded: show chevron RIGHT (to collapse/hide)
- When sidebar is collapsed: show chevron LEFT (to expand/show)
- Update SidebarHeader to use faChevronRight
- Update BookmarkList collapsed state to use faChevronLeft
2025-10-03 02:06:40 +02:00
Gigi
c21c29d5ee fix(ui): ensure all icon buttons remain perfectly square
- Add padding: 0 and box-sizing: border-box to .icon-button
- Add box-sizing: border-box to .profile-avatar
- Update .sidebar-header-bar .toggle-sidebar-btn to use fixed width/height instead of min values
- Add explicit styling for .bookmarks-container.collapsed .toggle-sidebar-btn
- Ensure borders don't add to total dimensions (33px x 33px including borders)
2025-10-03 02:05:19 +02:00
Gigi
0d956ed692 feat(ui): display timestamps as relative time
- Install relative-time package from npm
- Update formatDate functions to use relative-time instead of toLocaleDateString
- Add TypeScript type definitions for relative-time module
- Show human-friendly relative times (e.g., '2 hours ago', 'yesterday')
- Apply to all timestamp displays (bookmark dates, created dates)
2025-10-03 02:03:31 +02:00
Gigi
8fe01d5337 feat(ui): replace user text with profile image in sidebar header
- Replace 'Logged in as: [user]' text with profile avatar
- Use applesauce ProfileModel to fetch user's profile picture
- Display profile image in 33px square (same size as IconButton)
- Show fallback user icon when no profile image available
- Style avatar with same border radius and styling as IconButton
- Add tooltip showing user display name on hover
2025-10-03 02:02:15 +02:00
Gigi
55c4fe9d4e refactor(ui): move user info and logout to sidebar header bar
- Create new SidebarHeader component as bar-shaped container
- Combine collapse button, user info, and logout button in one bar
- Position header bar at top of bookmark sidebar with matching width
- Remove fixed top-right positioning for user header
- Style as cohesive bar with background, border, and spacing
- Update all prop passing from App through Bookmarks to BookmarkList
- Remove old UserHeader component
2025-10-03 02:00:54 +02:00
Gigi
8014ee4ddd refactor(ui): reduce IconButton size by 25%
- Change default size from 44px to 33px (25% reduction)
- Update min-width and min-height in CSS to match
- Apply size reduction to toggle-sidebar-btn as well for consistency
2025-10-03 01:58:42 +02:00
Gigi
365b84ba9d refactor(ui): remove duplicate bookmark title and heading
- Remove bookmark title (h3) from bookmark items
- Remove duplicate h4 heading from individual-bookmarks section
- Keep only the single 'X bookmarks in this list:' line with event link
2025-10-03 01:57:51 +02:00
Gigi
c419679099 refactor(ui): remove horizontal line below collapse button
- Remove border-bottom from bookmarks-header
- Remove padding-bottom for cleaner appearance
2025-10-03 01:56:44 +02:00
Gigi
e644f07828 refactor(ui): move user info to top-right app header
- Create UserHeader component to display user info and logout button
- Move 'Logged in as: user' from sidebar to app-header in top-right
- Remove user info display from BookmarkList and Bookmarks components
- Simplify bookmarks-header layout (only contains collapse button now)
- Update CSS to display user info and logout button inline with proper spacing
2025-10-03 01:56:16 +02:00
Gigi
448c4dac1c feat(bookmarks): classify URLs by type and adjust action buttons
- Add URL classification system (article, video, youtube, image)
- Classify based on domain (youtube) and file extensions
- Update button text: 'READ NOW' for articles, 'WATCH NOW' for videos, 'VIEW NOW' for images
- Update icons: faBookOpen for articles, faPlay for videos, faEye for images
- Apply classification to both individual URL buttons and main action button
2025-10-03 01:53:49 +02:00
Gigi
85695b5934 feat(ui): improve bookmark list heading with event links
- Replace 'Bookmarks (count)' with 'count bookmarks in this list:'
- Replace 'Individual Bookmarks (count):' with 'count bookmarks in this list:'
- Make 'this list' a clickable link to search.dergigi.com/e/{eventId}
- Add event-link CSS styling with blue color and hover effect
2025-10-03 01:52:07 +02:00
Gigi
ef3ce445f5 refactor(ui): move logout button to top-right of app
- Move logout IconButton from sidebar to App component
- Position logout button fixed at top-right corner
- Remove onLogout prop from Bookmarks and BookmarkList components
- Clean up sidebar header by removing logout button
- Add app-header CSS with fixed positioning and high z-index
2025-10-03 01:51:03 +02:00
Gigi
436bbf2b43 refactor(ui): replace logout button text with icon button
- Replace text logout buttons with IconButton component
- Use faRightFromBracket icon for a cleaner, more minimal interface
- Apply changes to both loading state and normal state
- Maintain consistent styling with ghost variant
2025-10-03 01:48:45 +02:00
Gigi
0c2f528a23 refactor(ui): remove 'Your Bookmarks' heading
- Remove the 'Your Bookmarks (count)' heading from the sidebar
- Keep only the user info and action buttons for a cleaner interface
2025-10-03 01:47:20 +02:00
Gigi
d2cf27db42 refactor(ui): remove header text from app
- Remove 'Markr' title and 'A minimal nostr bookmark client' subtitle
- Clean up the app header for a more minimal interface
2025-10-03 01:46:24 +02:00
Gigi
53a6c86d8a feat(ui): add collapse/expand functionality for bookmarks sidebar
- Add toggle button to collapse/expand the bookmarks sidebar completely
- Sidebar collapses to 60px width showing only expand button
- Main content area expands to fill available space when sidebar collapsed
- Smooth transitions when toggling between states
- Use FontAwesome chevron icons for visual feedback
- Preserve all functionality in both collapsed and expanded states
2025-10-03 01:45:42 +02:00
Gigi
0b058440bc refactor(components): improve type safety and simplify IconButton
- Add proper type guards in ContentWithResolvedProfiles to avoid type assertions
- Remove href/link functionality from IconButton component for simplification
- Replace 'as any' with proper type narrowing using type predicates
2025-10-03 01:43:13 +02:00
Gigi
0964156bcc feat(bookmarks): sort by added_at (recently added first), fallback to event time\n\n- Track synthetic added_at on processed items\n- Keep order aligned with append semantics from Kind 10003 guidance (newest at end)\n- Cite: https://nostrbook.dev/kinds/10003 2025-10-03 01:41:11 +02:00
Gigi
86de9c9f1f chore(release): bump version to 0.1.1 2025-10-03 01:04:20 +02:00
Gigi
974cecb85f style(ui): use full-width slim chevron toggle; keep IconButton square for actions 2025-10-03 01:02:52 +02:00
Gigi
9b245b3d29 style(ui): make kind icon square to match IconButton sizing 2025-10-03 01:01:37 +02:00
Gigi
4fe9fd5470 refactor(ui): use IconButton for kind icon (square, link-capable) 2025-10-03 00:59:45 +02:00
Gigi
18af2d02ea style(ui): remove colored borders and gradients; keep neutral cards 2025-10-03 00:56:58 +02:00
Gigi
a80352d8d3 refactor(ui): use IconButton for all icon-only actions to enforce square sizing 2025-10-03 00:55:51 +02:00
Gigi
6652694304 refactor(ui): extract kind icon mapping to helper and keep BookmarkItem under 210 lines 2025-10-03 00:53:38 +02:00
Gigi
728c269a29 feat(ui): make IconButton square and mobile-tappable (44px min) 2025-10-03 00:50:12 +02:00
Gigi
91c68a9d48 feat(ui): show bookmarked event date top-right; remove event id display 2025-10-03 00:48:48 +02:00
Gigi
f9d381e451 feat(ui): add reusable IconButton component with square styling 2025-10-03 00:47:37 +02:00
Gigi
81a48bd0f6 feat(ui): resolve nprofile/npub mentions to names in content
- Add ResolvedMention component using applesauce ProfileModel
- Update parsed content renderer to use ResolvedMention for mentions
- Mentions now show @name and link to search page
2025-10-03 00:46:11 +02:00
Gigi
386a821c6b feat(ui): make kind icon open event on search.dergigi.com\n\n- Wrap kind icon with link to nevent-encoded event\n- Adds fallback when id is not hex 2025-10-03 00:44:40 +02:00
Gigi
d10e12b8df feat(ui): link author to search.dergigi.com with npub\n\n- Clickable 'by: <author>' opens profile search in new tab\n- Styles for author link 2025-10-03 00:43:19 +02:00
Gigi
c3eb29445e feat(ui): add chevron toggle for URL list (show 3 by default) 2025-10-03 00:40:37 +02:00
Gigi
e0450385ed fix(ui): enforce 210-char truncation for both plain and parsed content\n\n- Show truncated plain text when parsedContent exists and not expanded\n- Render full parsed content only when expanded\n- Keep chevron toggle below content 2025-10-03 00:35:55 +02:00
Gigi
a2620caa29 feat(ui): add 'Read now' button next to each URL in bookmarks\n\n- Display inline book-open icon button per URL\n- Clicking loads readability content in the right panel\n- Added styles for url rows and inline button 2025-10-03 00:32:16 +02:00
Gigi
609e15a738 feat(ui): truncate long bookmark text with expand/collapse chevron\n\n- Show first 210 chars by default\n- Toggle expansion with FontAwesome chevrons\n- Add minimal styles for the toggle 2025-10-03 00:27:31 +02:00
Gigi
fdb8511c87 chore(ui): change 'Author:' label to 'by:' in bookmark cards 2025-10-03 00:26:16 +02:00
Gigi
acce3ad4e2 chore(release): bump version to 0.1.0 2025-10-03 00:23:03 +02:00
Gigi
bdecb1409e refactor(ui): remove copy-to-clipboard buttons from bookmark cards
- Remove copy buttons for event id and author pubkey
- Clean up unused code and imports
2025-10-03 00:22:25 +02:00
Gigi
2ca350ee5f fix(bookmarks): show bookmarked event author instead of list signer\n\n- During hydration, set IndividualBookmark.pubkey to hydrated event.pubkey\n- Ensures author resolution uses the actual author of the bookmarked event 2025-10-03 00:21:24 +02:00
Gigi
20f37b94e1 fix(profile): enable reactive profile fetch via address loader lookup relays and improve fallback display\n\n- Configure createAddressLoader with common profile relays (purplepag.es, primal, nostr.band)\n- Avoid sticky 'Loading profile...' label; fallback to short pubkey until profile loads 2025-10-03 00:16:04 +02:00
Gigi
21890f002d chore(lint): fix hooks rule error by separating content resolver component and helpers
- Move shared helpers into src/utils/helpers.ts
- Add ContentWithResolvedProfiles component file to avoid hooks rule violation
- Use strong IconDefinition type in icon map
- Resolve linter warnings and errors
2025-10-03 00:12:40 +02:00
Gigi
7a5dd2f444 fix(applesauce): attach address/replaceable loaders so ProfileModel resolves reactively
- Use createAddressLoader from applesauce-loaders
- Set eventStore.addressableLoader and replaceableLoader
- Enables reactive profile fetching for logged-in user and mentions
2025-10-03 00:08:10 +02:00
Gigi
5495890204 feat(ui): improve logged-in user profile resolution
- Add debug logging to track profile loading
- Show 'Loading profile...' while profile data is being fetched
- Better handling of profile loading states
- Ensures user sees proper name instead of pubkey when available
2025-10-03 00:04:11 +02:00
Gigi
6d585dcef6 feat(ui): resolve nprofile strings to human-readable names
- Add extractNprofilePubkeys utility to parse nprofile strings from content
- Create ContentWithResolvedProfiles component using applesauce ProfileModel
- Replace nprofile strings with @displayName in bookmark content
- Update BookmarkItem to use resolved content rendering
- Improves readability by showing names instead of long nprofile strings
2025-10-03 00:02:22 +02:00
Gigi
0bae6674ce feat(ui): resolve author names using applesauce ProfileModel
- Import useEventModel and Models from applesauce-react
- Use ProfileModel to fetch author profile data for each bookmark
- Display author name/display_name/nip05 instead of raw pubkey
- Fallback to short pubkey if profile not available
- Improves readability by showing human-readable author names
2025-10-03 00:00:04 +02:00
Gigi
096509baf6 feat(ui): replace kind numbers with FontAwesome icons
- Import all kind-specific icons from FontAwesome
- Add getKindIcon mapping function based on kind-icons.txt
- Replace 'Kind: X' text with visual icon in bookmark-meta
- Add styling for kind-icon with blue accent color
- Fallback to file icon for unmapped kinds
2025-10-02 23:59:11 +02:00
Gigi
4c2626f3c4 feat(ui): add spinner to content loading state
- Replace text with FontAwesome spinner icon
- Add loading-spinner styles for proper alignment
- Improve visual feedback during content fetch
2025-10-02 23:54:19 +02:00
Gigi
70fa3bb6a8 fix(ui): left-align content and constrain images in content panel
- Force left alignment for HTML/Markdown content
- Constrain images to container width and 70vh height
- Improve readability of rendered articles
2025-10-02 23:52:12 +02:00
Gigi
719ddf3f0b feat(readability): render Markdown when proxy provides it
- Detect markdown blocks from r.jina.ai output
- Add react-markdown + remark-gfm for rendering
- Extend ContentPanel to render markdown or HTML
- Add styles for markdown content
2025-10-02 23:46:33 +02:00
Gigi
80408148fb feat: add react-markdown and remark-gfm for markdown rendering 2025-10-02 23:45:09 +02:00
Gigi
4163ffa4ba feat(ui): add READ NOW button to bookmark cards
- Shows when a bookmark has URLs
- Triggers onSelectUrl to load readability content in main panel
- Added styles for prominent call-to-action
2025-10-02 23:41:03 +02:00
Gigi
cf230623a4 chore: remove unused faLock import to satisfy linter 2025-10-02 23:39:04 +02:00
Gigi
9cd4b72f98 chore: commit pending changes 2025-10-02 23:37:13 +02:00
Gigi
eb57330915 feat(ui): add two-pane layout and styles for content panel
- Grid layout with sidebar and main pane
- Styled content panel and readable HTML area
- Sticky sidebar behavior
2025-10-02 23:34:47 +02:00
Gigi
96d93d0e17 feat(layout): add two-pane layout and content fetching pipeline
- Left: bookmark list; Right: content panel
- Selection triggers readability fetch via readerService
- ContentPanel renders title and HTML
- Wires selection from BookmarkItem -> BookmarkList -> Bookmarks
2025-10-02 23:34:21 +02:00
Gigi
1d10c10a44 feat: propagate URL selection through BookmarkList to parent 2025-10-02 23:33:42 +02:00
Gigi
dab35820b7 feat: add onSelectUrl to BookmarkItem and intercept URL clicks
- Adds optional onSelectUrl callback
- Prevents default navigation to open in main content panel
- Keeps middle-click/target blank behavior if no handler
2025-10-02 23:33:05 +02:00
Gigi
9d5e8c194b feat(ui): add ContentPanel component to render readable HTML
- Shows loading/empty states
- Renders title and HTML via dangerouslySetInnerHTML
- Minimal API for integration
2025-10-02 23:32:32 +02:00
Gigi
de32807995 feat(reader): add lightweight readability fetcher via r.jina.ai proxy
- Provide fetchReadableContent(url) returning simplified HTML
- Avoid heavy deps and CORS issues using proxy
- Extract <title> best-effort
2025-10-02 23:32:00 +02:00
Gigi
b671e0e259 feat: update bookmark icons to use fa-bookmark and fa-user-lock
- Replace faGlobe with faBookmark for public bookmarks
- Add faUserLock icon alongside faBookmark for private bookmarks
- Import faBookmark and faUserLock from FontAwesome
- Update CSS to handle icon spacing with flexbox and gap
- Private bookmarks now show both bookmark and user-lock icons
2025-10-02 23:23:57 +02:00
Gigi
e5d6fe99f3 fix: improve word-wrap for long strings and prevent overflow
- Add word-wrap, overflow-wrap, and word-break properties to content areas
- Apply to .parsed-content, .bookmark-content, .individual-bookmark
- Update .nostr-mention and .nostr-link for better long string handling
- Add overflow: hidden to .individual-bookmark container
- Ensures long nprofile strings and URLs break properly within containers
2025-10-02 23:20:09 +02:00
Gigi
9400faa00f feat: display URLs clearly in individual bookmarks
- Extract URLs from bookmark content using extractUrlsFromContent()
- Display extracted URLs in a dedicated section with proper styling
- URLs are clickable and open in new tabs
- Reuse existing CSS styles for consistent appearance
2025-10-02 23:11:22 +02:00
Gigi
5173a37b69 feat: remove 'EVENT' text from bookmark type labels
- Remove bookmark.type display next to lock/globe icons
- Keep only the visual icons for private/public indication
2025-10-02 23:10:42 +02:00
Gigi
50b32a66de feat(bookmarks): extract URLs from content into urlReferences for each item 2025-10-02 22:34:06 +02:00
Gigi
1b41b6e823 fix(timestamps): use seconds for created_at fallbacks; UI date formatting remains ms-converted 2025-10-02 22:30:31 +02:00
Gigi
2696bdb57a fix(bookmarks): dedupe individual bookmarks by id to avoid duplicates in UI 2025-10-02 22:28:47 +02:00
Gigi
e0b042b6c0 docs(readme): document Amethyst-style hidden bookmarks decryption and display flow 2025-10-02 22:05:35 +02:00
Gigi
1226124566 refactor(services): extract helpers and event processing; keep files <210 lines; lint+types clean 2025-10-02 22:03:47 +02:00
Gigi
8967963535 refactor(services): remove usages in bookmarkService, add type guards; lint clean 2025-10-02 21:42:51 +02:00
Gigi
b64fd6cedb chore(release): bump version to 0.0.3 2025-10-02 21:39:14 +02:00
Gigi
c748f173b3 feat(bookmarks): surface manually decrypted hidden tags in UI; convert JSON tags via Helpers.parseBookmarkTags and include in private items immediately 2025-10-02 21:37:37 +02:00
Gigi
51f009a489 feat(bookmarks): try NIP-44 then NIP-04 for manual decryption; cache decrypted hidden tags for display/debug 2025-10-02 21:30:12 +02:00
Gigi
18d936e222 feat: add detailed debugging for decryption process
- Add logging for what content is being sent to browser extension
- Add fallback to try string conversion if object is passed
- Add parsing of extension error responses
- Help identify exact format expected by browser extension
2025-10-02 21:17:27 +02:00
Gigi
ce7fbdbdf3 feat: implement direct decryption for unrecognized event kinds
- Add manual decryption using signer's nip04 capabilities directly
- Parse decrypted content as JSON array of bookmark tags
- Cache decrypted content properly for getHiddenBookmarks to find
- Enable decryption of legacy bookmark events (kind 30001) with encrypted content
2025-10-02 21:08:20 +02:00
Gigi
6e6d43cb25 feat: add manual decryption for unrecognized event kinds
- Add fallback decryption for events with encrypted content but unrecognized kinds
- Temporarily change event kind to 10003 to use applesauce's standard decryption
- Enables decryption of legacy bookmark events (kind 30001) that contain private bookmarks
- Maintains compatibility with standard NIP-51 bookmark events
2025-10-02 20:59:19 +02:00
Gigi
0ed7c1497f feat: sort individual bookmarks by timestamp (newest first)
- Sort allBookmarks array by created_at timestamp in descending order
- Ensures newest bookmarks appear first in the UI
- Maintains chronological order with most recent bookmarks at the top
2025-10-02 20:56:34 +02:00
Gigi
a2c31b32de feat: increase bookmark loading timeout by 2x
- Increase timeout from 10s to 20s for bookmark fetching
- Increase timeout from 10s to 20s for event hydration
- Provide more time for slow relays to respond
2025-10-02 20:54:31 +02:00
Gigi
cdce972e72 feat: enhance bookmark debugging and fetch legacy formats
- Add detailed logging for events with content (potentially encrypted)
- Fetch legacy bookmark format (kind 30001) in addition to NIP-51 standards
- Show content preview for all events to identify encrypted content
- Help identify if private bookmarks are in different event formats
2025-10-02 20:51:20 +02:00
Gigi
00638410c0 feat: add more relays and fix logging for better bookmark debugging
- Add more popular relays for better bookmark discovery
- Fix variable scoping in bookmark event logging
- Enhanced debugging to see dTag and tag content structure
2025-10-02 20:50:07 +02:00
Gigi
2e5d0e3725 feat: add detailed logging to debug bookmark event structure
- Add logging for raw events before deduplication to see all fetched events
- Add detailed tag inspection for bookmark events including dTag and first few tags
- Help identify if private bookmarks are in separate bookmark sets (30003) or main lists (10003)
2025-10-02 20:48:16 +02:00
Gigi
a66c051444 fix: correct bookmark event kinds to match NIP-51 standards
- Fix bookmark list kind from 30001 to 10003 (kinds.BookmarkList)
- Fix bookmark sets kind from 30001 to 30003 (kinds.Bookmarksets)
- Update dedupeNip51Events to handle correct NIP-51 event kinds
- Now fetching the correct bookmark events that support hidden tags
2025-10-02 20:41:53 +02:00
Gigi
eb282fcbb0 fix: fix hidden bookmark detection by using applesauce's built-in logic
- Remove custom isEncryptedContent function that was too restrictive
- Use applesauce's hasHiddenContent() and hasHiddenTags() functions instead
- These properly detect encrypted content regardless of format
- Remove failing relay.snort.social from relay list
- Add detailed logging to show hidden content detection status
2025-10-02 20:38:46 +02:00
Gigi
d54313b015 fix: correct NIP-51 bookmark event kinds and deduplication
- Fix bookmark event kinds: use 30001 (BookmarkList) and 30003 (Bookmarksets) instead of incorrect 10003
- Update dedupeNip51Events to properly handle both bookmark lists and bookmark sets
- Add detailed logging to inspect bookmark event structure and content
- Should now properly detect Amethyst private bookmarks
2025-10-02 20:26:52 +02:00
Gigi
03c6a0c9c7 feat: add wot.dergigi.com relay for improved connectivity
- Add wot.dergigi.com as additional relay option
- Now using 6 relays total for maximum bookmark data availability
2025-10-02 20:09:41 +02:00
Gigi
dc36992199 feat: add relay.dergigi.com to relay list for better connectivity
- Add relay.dergigi.com as additional relay option
- Improve chances of fetching private bookmarks from multiple sources
2025-10-02 20:09:00 +02:00
Gigi
08fc541eaa fix: properly configure browser extension signer for hidden bookmarks
- Fix signer extraction for ExtensionAccount to use nip04/nip44 capabilities
- Add debug logging to understand hidden bookmarks unlocking process
- Ensure ExtensionAccount can be used as HiddenContentSigner for decryption
- Handle both ExtensionAccount and raw signer for maximum compatibility
2025-10-02 19:46:34 +02:00
Gigi
3eca2879ef fix: resolve all linting and type checking issues
- Fix empty catch blocks in BookmarkItem and bookmarkService
- Replace any types with proper NostrEvent interface
- Add proper error handling with console.warn
- Use eslint-disable for unavoidable any types in applesauce integration
2025-10-02 11:26:12 +02:00
Gigi
de428b8719 config: change dev server port from 3000 to 9802 2025-10-02 11:25:10 +02:00
Gigi
ac7f1007a7 feat(bookmarks): fetch all NIP-51 events; dedupe 10003/30001; unlock private via applesauce; hydrate ids; trim logs 2025-10-02 11:22:07 +02:00
Gigi
4db147ddf3 feat(ui): add copy-to-clipboard icons for event id and author pubkey 2025-10-02 11:02:43 +02:00
Gigi
2eda8f3227 chore(debug): log per-event hidden/locked state; log unlock attempts and results 2025-10-02 11:01:23 +02:00
Gigi
64825175a7 fix(bookmarks): unlock based on applesauce hidden-tags state only; keep file compact 2025-10-02 10:59:06 +02:00
Gigi
5ee0f49b69 feat(bookmarks): hydrate event content for pointers; robust unlock (nip04->nip44); aggregate across events 2025-10-02 10:51:33 +02:00
Gigi
7d26372878 feat(ui): add FontAwesome globe/lock icons; render content identically for private/public 2025-10-02 10:44:06 +02:00
Gigi
ab00bd84e6 fix(bookmarks): hide encrypted ciphertext from title/preview; prefer plaintext content 2025-10-02 10:39:57 +02:00
Gigi
ec4473fc51 feat(bookmarks): aggregate list(10003) + set(30001); unlock hidden per-event; merge results 2025-10-02 10:35:07 +02:00
Gigi
0f57338866 fix(bookmarks): avoid unlock with empty ciphertext; require hidden tags + ciphertext 2025-10-02 10:32:02 +02:00
Gigi
a8cdeeaef2 feat(bookmarks): unlock hidden bookmarks via applesauce helpers and signer; reduce logs 2025-10-02 10:30:18 +02:00
Gigi
92d49468cd fix: resolve TypeScript type issues
- Update AccountWithExtension interface to be more flexible
- Add type guard for runtime type checking
- Change fetchBookmarks parameter to accept unknown type with validation
- All linting and type checks now pass
2025-10-02 10:23:01 +02:00
Gigi
a625203fe4 refactor: clean up bookmark service and use proper applesauce approach
- Remove unused imports and variables
- Keep the enhanced debugging for multiple bookmark list events
- Use native applesauce getHiddenBookmarks which should trigger browser extension
- This follows the applesauce models pattern for handling private bookmarks
2025-10-02 10:21:44 +02:00
Gigi
e3efcd4a7c debug: enhance private bookmark detection with detailed logging
- Check multiple bookmark list events for encrypted content
- Add detailed logging for signer availability and type
- Use native applesauce getHiddenBookmarks which should trigger browser extension
- This will help identify why private bookmarks aren't being detected
2025-10-02 10:20:55 +02:00
Gigi
ba76a6a9ef feat: enhance private bookmark detection
- Fetch multiple bookmark list events (limit 10) to find private bookmarks
- Check all events for encrypted content before selecting one
- Add detailed logging to identify which event has encrypted content
- This should help find your private bookmarks if they exist in a different event
2025-10-02 10:19:37 +02:00
Gigi
c5a32b911d debug: add detailed logging for bookmark content
- Check if bookmark list has encrypted content
- Log content preview to understand the structure
- This will help determine why browser extension isn't triggered
2025-10-02 10:15:34 +02:00
Gigi
610de95481 feat: improve private bookmark handling
- Use proper error handling for getHiddenBookmarks
- Add detailed logging to understand what's happening
- The browser extension should be triggered automatically when needed
- This follows the applesauce examples pattern for decryption
2025-10-02 10:14:16 +02:00
Gigi
82c63e5d18 revert: remove manual decryption approach
- Removed manual decryption implementation
- Back to using applesauce helpers directly
- The issue is likely that browser extension needs permission for decryption
- getHiddenBookmarks returns undefined because extension hasn't been triggered yet
- All linting passes
2025-10-02 10:11:13 +02:00
Gigi
b112520056 debug: enhance account signer debugging
- Added detailed logging for account signer capabilities
- Check if account has signer with decrypt method
- This will help identify if the ExtensionAccount has proper NIP-44 decryption capabilities
- Following applesauce examples pattern for signer usage
2025-10-02 10:06:37 +02:00
Gigi
06b15f3fe2 docs: update applesauce cursor rules
- Updated applesauce documentation reference
- Added guidance to use applesauce modules when possible
- Referenced examples directory for proper usage patterns
2025-10-02 10:05:11 +02:00
Gigi
4be8eff80a feat: add debugging for private bookmark decryption
- Added detailed logging to understand why getHiddenBookmarks returns undefined
- Check bookmark list content and encryption format
- Verify account has decryption capabilities
- Pass full account object with extension capabilities to applesauce helpers
- This will help diagnose the NIP-44 vs NIP-04 encryption issue
2025-10-02 10:02:50 +02:00
Gigi
559e7ee944 fix: handle applesauce bookmark structure correctly
- Added processApplesauceBookmarks function to handle applesauce return format
- Fixed processing of {notes: [], articles: [], hashtags: [], urls: []} structure
- Added ApplesauceBookmarks interface for proper typing
- Now correctly processes all bookmark types from applesauce helpers
- Should now show all 13 bookmarks (12 notes + 1 article) instead of just 1
- All linting and type checking passes
2025-10-02 09:46:04 +02:00
Gigi
7fd8e5341e chore: ignore applesauce directory in ESLint configuration
- Added 'applesauce' to ignorePatterns in package.json
- Prevents linting errors from dependency code
- Maintains focus on project-specific code quality
- ESLint now runs cleanly with 0 errors and 0 warnings
2025-10-02 09:44:57 +02:00
Gigi
211a89afbb refactor: eliminate code duplication with DRY principle
- Created processBookmarks helper function to eliminate duplication
- Reduced public/private bookmark processing from 32 lines to 3 lines
- Simplified isPrivate check logic
- Maintained all functionality while improving maintainability
- File reduced from 117 to 105 lines (10% reduction)
- All linting and type checking passes
2025-10-02 09:43:53 +02:00
Gigi
695d3509ac refactor: simplify bookmark service using applesauce helpers
- Removed custom event fetching logic in favor of applesauce helpers
- Use getPublicBookmarks and getHiddenBookmarks directly from applesauce-core
- Simplified code from 152 lines to 105 lines (31% reduction)
- Added proper TypeScript interfaces for type safety
- Enhanced logging to debug bookmark fetching
- All linting and type checking passes
2025-10-02 09:43:17 +02:00
Gigi
7bb037f12a feat: implement getHiddenBookmarks for private bookmarks
- Added getHiddenBookmarks from applesauce-core to fetch private bookmarks
- Created HiddenBookmarkData interface for proper typing
- Handle both array and object return types from getHiddenBookmarks
- Combine public and private bookmarks in the final result
- Maintain type safety with proper TypeScript interfaces
- All linting and type checking passes
2025-10-02 09:39:59 +02:00
Gigi
a7cfc802d1 fix: resolve linting and type checking issues
- Fixed TypeScript error by properly handling undefined activeAccount
- Removed unnecessary 'as any' type assertion
- All linting rules now pass with 0 warnings
- TypeScript compilation passes without errors
2025-10-02 09:38:32 +02:00
Gigi
465278742e refactor: simplify bookmark service and reduce code duplication
- Extracted fetchEvent helper function to eliminate duplication
- Simplified main fetchBookmarks function with cleaner logic
- Used Promise.all for parallel event fetching instead of sequential loops
- Consolidated private bookmark CSS styles and removed duplicates
- Reduced file from 140 to 102 lines while maintaining functionality
- Removed unused imports and simplified error handling
2025-10-02 09:37:41 +02:00
Gigi
79f83b214f feat: add private bookmarks support with NIP-51 and visual indicators
- Updated bookmark types to support private bookmarks with isPrivate and encryptedContent fields
- Enhanced bookmark service to detect encrypted content and mark bookmarks as private
- Added visual indicators for private bookmarks with lock icon and special styling
- Added CSS styles for private bookmarks with red accent border and gradient background
- Updated BookmarkItem component to show private bookmark indicators
- Maintained compatibility with existing public bookmark functionality
2025-10-02 09:36:46 +02:00
Gigi
170feb1bd7 fix: implement proper NIP-44 decryption using applesauce hidden-content helpers
- Replace direct signer method calls with applesauce hidden-content helpers
- Use unlockHiddenContent, getHiddenContent, and isHiddenContentLocked functions
- Add proper error handling for decryption failures
- This follows the correct applesauce pattern for NIP-44 decryption
2025-10-02 09:33:33 +02:00
Gigi
f37deefa36 chore: add applesauce directory to .gitignore
- Exclude applesauce/ directory from version control
- This directory likely contains examples or testing files that shouldn't be tracked
2025-10-02 09:31:40 +02:00
Gigi
ebdfa47bd8 debug: add signer method inspection for NIP-44 decryption
- Add logging to inspect available methods on the applesauce signer
- Try multiple possible method names for NIP-44 decryption
- This will help identify the correct method name for decryption
2025-10-02 09:27:28 +02:00
Gigi
6e57c6227c feat: add private bookmark fetching with NIP-44 decryption
- Update ActiveAccount interface to include signer property
- Modify fetchBookmarks to handle both kind 10003 (public) and kind 30001 (private) bookmark lists
- Implement NIP-44 decryption for private bookmarks using applesauce SimpleSigner
- Support both public tags and encrypted private content in categorized bookmarks
- Maintain backward compatibility with existing public bookmark functionality
2025-10-02 09:25:57 +02:00
Gigi
e0acd2f7e7 feat: change bookmarks display from grid to social feed list layout
- Update .bookmarks-list to use flex column layout with max-width
- Change .bookmarks-grid to flex column for individual bookmarks
- Add social media-like styling with shadows and hover effects
- Improve visual hierarchy and spacing for feed-like appearance
2025-10-02 09:21:42 +02:00
Gigi
2253172e04 refactor: extract components and utilities to keep files under 210 lines
- Extract types to src/types/bookmarks.ts
- Extract utility functions to src/utils/bookmarkUtils.tsx
- Extract BookmarkItem component to src/components/BookmarkItem.tsx
- Extract BookmarkList component to src/components/BookmarkList.tsx
- Extract bookmark fetching logic to src/services/bookmarkService.ts
- Reduce main Bookmarks component from 416 to 100 lines
- Maintain all functionality while improving code organization
- Pass all linting and type checking
2025-10-02 09:19:44 +02:00
Gigi
15d155c565 fix: resolve undefined timeoutId variable in fetchBookmarks function
- Move timeoutId declaration outside try block to make it accessible in both try and catch blocks
- Fixes ESLint no-undef error and TypeScript compilation error
2025-10-02 09:17:05 +02:00
284 changed files with 9607 additions and 8077 deletions

View File

@@ -1,12 +1,10 @@
---
description: applesauce reference documentation and examples
alwaysApply: true
---
If you can use an applesauce-module for something, use applesauce. https://hzrd149.github.io/applesauce/typedoc/modules.html
If you can use an applesauce-module for something, use applesauce.
Code snippets & examples:
- https://hzrd149.github.io/applesauce/snippets/?q=applesauce#nevent1qgszv6q4uryjzr06xfxxew34wwc5hmjfmfpqn229d72gfegsdn2q3fgppemhxue69uhkummn9ekx7mp0qqs8c7umrjum47vjp9jxyyedhyq4v6kvahs6s8tu0r87dvv4cx2ekdq2nepz3
- https://hzrd149.github.io/applesauce/snippets/?q=applesauce#nevent1qgszv6q4uryjzr06xfxxew34wwc5hmjfmfpqn229d72gfegsdn2q3fgpz9mhxue69uhkummnw3ezumrpdejz7qpq860x9snxtqxg2jyn8dpmq8we8j6avnw5dhkpgl2s66fzy3rumatqm36qyh
- https://hzrd149.github.io/applesauce/snippets/?q=applesauce#nevent1qgsrkwjz6dx0pg2q95vd2dkf62kzavwxqxdfz5a72uyyeqt96xfwxfgppemhxue69uhkummn9ekx7mp0qqsgpexqt77wq4hl3j8l4gvza9cq0hedtlcp6veg04ghg5kl322t7tgqk205c
- https://hzrd149.github.io/applesauce/snippets/?q=applesauce#nevent1qgszv6q4uryjzr06xfxxew34wwc5hmjfmfpqn229d72gfegsdn2q3fgpz9mhxue69uhkummnw3ezumrpdejz7qpqqjfzehsxvdq4r2eqc9c5hd80nkznj8sspcs6f77l3498qwz5ne2sst2t48
- https://hzrd149.github.io/applesauce/snippets/?q=applesauce#nevent1qgszv6q4uryjzr06xfxxew34wwc5hmjfmfpqn229d72gfegsdn2q3fgpz9mhxue69uhkummnw3ezumrpdejz7qpqsjgpalr742kqcke5av2ey8pnfz7j837u78l30wuzktw3v8j6vufqufzyte
Documentation: https://hzrd149.github.io/applesauce/typedoc/modules.html
When unsure how to use applesauce correctly, look at the examples in the `applesauce/packages/examples` directory.

View File

@@ -0,0 +1,16 @@
---
description: documentation that's useful when dealing with bookmark events (kind:10003 or kind:30003) or anything related to NIP-51
alwaysApply: false
---
Read the nostrbook to understand how bookmarks work:
- https://nostrbook.dev/kinds/10003
- https://nostrbook.dev/kinds/30003
They are defined in NIP-51:
- https://github.com/nostr-protocol/nips/blob/master/51.md
Also refer to the applesauce bookmark helpers:
- https://github.com/hzrd149/applesauce/blob/17c9dbb0f2c263e2ebd01729ea2fa138eca12bd1/packages/core/src/helpers/bookmarks.ts
Make sure to always use applesauce, and use it properly.

View File

@@ -0,0 +1,6 @@
---
description: when creating or modifying UI elements, especially related to icons and buttons
alwaysApply: false
---
We use FontAwesome. If you can use a fa-icon (instead of text) use a fa-icon. Always strive to keep the UI modern, beautiful, and minimalistic. Shy away from using too many colors, borders, glow, and animations.

View File

@@ -0,0 +1,9 @@
---
description: nostr highlights spec and docs
alwaysApply: false
---
Here's the spec for nostr-native highlights:
- https://github.com/nostr-protocol/nips/blob/master/84.md
- https://nostrbook.dev/kinds/9802

View File

@@ -0,0 +1,11 @@
---
description: anything that has to do with kind:30023 aka nostr blog posts aka nostr-native long-form content
alwaysApply: false
---
Always stick to NIPs. Do everything with applesauce (getArticleTitle, getArticleSummary, getHashtags, getMentions).
- https://github.com/hzrd149/applesauce/blob/17c9dbb0f2c263e2ebd01729ea2fa138eca12bd1/packages/docs/tutorial/02-helpers.md
- https://github.com/nostr-protocol/nips/blob/master/19.md
- https://github.com/nostr-protocol/nips/blob/master/23.md
- https://nostrbook.dev/kinds/30023

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
# Default article to display on app load
# This should be a valid naddr1... string (NIP-19 encoded address pointer to a kind:30023 long-form article)
VITE_DEFAULT_ARTICLE_NADDR=naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew

3
.gitignore vendored
View File

@@ -116,3 +116,6 @@ temp/
.env.development.local
.env.test.local
.env.production.local
# applesauce examples
applesauce/

125
README.md
View File

@@ -1,12 +1,21 @@
# Markr
# Boris
A minimal nostr client for bookmark management, built with [applesauce](https://github.com/hzrd149/applesauce).
## Features
- **Nostr Authentication**: Connect using your nostr account
- **Nostr Authentication**: Connect using your nostr account via browser extension
- **Bookmark Display**: View your nostr bookmarks as per [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md)
- **Minimal UI**: Clean, simple interface focused on bookmark management
- **Content Classification**: Automatically detect and classify URLs (articles, videos, YouTube, images)
- **Reader Mode**: View article content inline with readable formatting
- **Collapsible Sidebar**: Expand/collapse bookmark list for focused reading
- **Profile Integration**: Display user profile images using applesauce ProfileModel
- **Relative Timestamps**: Human-friendly time display (e.g., "2 hours ago")
- **Event Links**: Quick access to view bookmarks on search.dergigi.com
- **Private Bookmarks**: Support for Amethyst-style hidden/encrypted bookmarks
- **Highlights Panel**: View and manage your NIP-84 highlights in a dedicated collapsible panel
- **Three-Pane Layout**: Bookmarks sidebar, content viewer, and highlights panel working together
- **Minimal UI**: Clean, modern interface focused on bookmark management
## Getting Started
@@ -20,7 +29,7 @@ A minimal nostr client for bookmark management, built with [applesauce](https://
1. Clone the repository:
```bash
git clone <your-repo-url>
cd markr
cd boris
```
2. Install dependencies:
@@ -46,8 +55,10 @@ yarn dev
## Usage
1. **Connect**: Click "Connect with Nostr" to authenticate using your nostr account
2. **View Bookmarks**: Once connected, you'll see all your nostr bookmarks
3. **Navigate**: Click on bookmark URLs to open them in a new tab
2. **View Bookmarks**: Once connected, you'll see all your nostr bookmarks in the left sidebar
3. **View Highlights**: Your NIP-84 highlights appear in the right panel
4. **Navigate**: Click on bookmark URLs to view content in the center panel
5. **Collapse Panels**: Use the collapse buttons to hide/show sidebars for focused viewing
## Technical Details
@@ -63,13 +74,66 @@ yarn dev
```
src/
├── components/
│ ├── Login.tsx # Authentication component
── Bookmarks.tsx # Bookmark display component
├── App.tsx # Main application component
├── main.tsx # Application entry point
└── index.css # Global styles
│ ├── Login.tsx # Authentication component
── Bookmarks.tsx # Main bookmarks view with layout
│ ├── BookmarkList.tsx # Bookmark list sidebar
│ ├── BookmarkItem.tsx # Individual bookmark card
│ ├── SidebarHeader.tsx # Header bar with collapse, profile, logout
│ ├── ContentPanel.tsx # Content viewer panel
│ ├── HighlightsPanel.tsx # Highlights sidebar panel (NIP-84)
│ ├── HighlightItem.tsx # Individual highlight display
│ ├── IconButton.tsx # Reusable icon button component
│ ├── ContentWithResolvedProfiles.tsx # Profile mention resolver
│ ├── ResolvedMention.tsx # Nostr mention component
│ └── kindIcon.ts # Kind-specific icon mapping
├── services/
│ ├── bookmarkService.ts # Main bookmark fetching orchestration
│ ├── bookmarkProcessing.ts # Decryption and processing pipeline
│ ├── bookmarkHelpers.ts # Shared types, guards, and utilities
│ ├── bookmarkEvents.ts # Event type handling and deduplication
│ ├── highlightService.ts # Highlight fetching (NIP-84)
│ └── readerService.ts # Content extraction via reader API
├── types/
│ ├── bookmarks.ts # Bookmark type definitions
│ ├── highlights.ts # Highlight type definitions (NIP-84)
│ ├── nostr.d.ts # Nostr type augmentations
│ └── relative-time.d.ts # relative-time package types
├── utils/
│ ├── bookmarkUtils.tsx # Bookmark rendering utilities
│ └── helpers.ts # General helper functions
├── App.tsx # Main application component
├── main.tsx # Application entry point
└── index.css # Global styles
```
### Private (hidden) bookmarks (Amethyst-style)
We support Amethyst-style private (hidden) bookmark lists alongside public ones (NIP51):
- **Detection and unlock**
- Use `Helpers.hasHiddenTags(evt)` and `Helpers.isHiddenTagsLocked(evt)` to detect hidden tags.
- First try `Helpers.unlockHiddenTags(evt, signer)`; if that fails, try with `'nip44'`.
- For events with encrypted `content` that arent recognized as supporting hidden tags (e.g. kind 30001), manually decrypt:
- Prefer `signer.nip44.decrypt(evt.pubkey, evt.content)`, fallback to `signer.nip04.decrypt(evt.pubkey, evt.content)`.
- **Parsing and rendering**
- Decrypted `content` is JSON `string[][]` (tags). Convert with `Helpers.parseBookmarkTags(hiddenTags)`.
- Map to `IndividualBookmark[]` via our `processApplesauceBookmarks(..., isPrivate=true)` and append to the private list so they render immediately alongside public items.
- **Caching for downstream helpers**
- Cache manual results on the event with `BookmarkHiddenSymbol` and also store the decrypted blob under `EncryptedContentSymbol` to aid debugging and hydration.
- **Structure**
- `src/services/bookmarkService.ts`: orchestrates fetching, hydration, and assembling the final bookmark payload.
- `src/services/bookmarkProcessing.ts`: decryption/collection pipeline (unlock, manual decrypt, parse, merge).
- `src/services/bookmarkHelpers.ts`: shared types, guards, mapping, hydration, and symbols.
- `src/services/bookmarkEvents.ts`: event type and deduplication for NIP51 lists/sets.
- **Notes**
- We avoid `any` via narrow type guards for `nip44`/`nip04` decrypt functions.
- Files are kept small and DRY per project rules.
- Built on applesauce helpers (`Helpers.getPublicBookmarks`, `Helpers.getHiddenBookmarks`, etc.). See applesauce docs: https://hzrd149.github.io/applesauce/typedoc/modules.html
### Building for Production
```bash
@@ -80,15 +144,42 @@ pnpm build
yarn build
```
## TODO
### High Priority
- [ ] **Mobile Responsive Design**: Optimize sidebar and content panel for mobile devices
- [ ] **Keyboard Shortcuts**: Add keyboard navigation (collapse sidebar, navigate bookmarks)
- [ ] **Search & Filter**: Add ability to search bookmarks by title, URL, or content
- [ ] **Error Handling**: Improve error states and retry logic for failed fetches
- [ ] **Loading States**: Better skeleton screens and loading indicators
### Medium Priority
- [ ] **Bookmark Creation**: Add ability to create new bookmarks
- [ ] **Bookmark Editing**: Edit existing bookmark metadata and tags
- [ ] **Bookmark Deletion**: Remove bookmarks from lists
- [ ] **Sorting Options**: Sort by date, title, kind, or custom order
- [ ] **Bulk Actions**: Select and perform actions on multiple bookmarks
- [ ] **Video Embeds**: Inline YouTube and video playback for video bookmarks
### Nice to Have
- [ ] **Dark/Light Mode Toggle**: User preference for color scheme
- [ ] **Export Functionality**: Export bookmarks as JSON, CSV, or HTML
- [ ] **Import Bookmarks**: Import from browser bookmarks or other formats
- [ ] **Tags & Categories**: Better organization with custom tags
- [ ] **Bookmark Collections**: Create and manage custom bookmark collections
- [ ] **Offline Support**: Cache bookmarks for offline viewing
- [ ] **Share Bookmarks**: Generate shareable links to bookmark lists
- [ ] **Performance Optimization**: Virtual scrolling for large bookmark lists
- [ ] **Browser Extension**: Quick bookmark saving from any page
## Contributing
This is a minimal MVP. Future enhancements could include:
Contributions are welcome! Please feel free to submit a Pull Request. Make sure to:
- Bookmark creation and editing
- Bookmark organization and tagging
- Search functionality
- Export capabilities
- Mobile-responsive design improvements
- Follow the existing code style
- Keep files under 210 lines
- Use conventional commits
- Run linter and type checks before submitting
## License

1
applesauce Symbolic link
View File

@@ -0,0 +1 @@
../applesauce

6
dist/index.html vendored
View File

@@ -4,9 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Markr - Nostr Bookmarks</title>
<script type="module" crossorigin src="/assets/index-ez6f4baA.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DCTVEVF8.css">
<title>Boris - Nostr Bookmarks</title>
<script type="module" crossorigin src="/assets/index-8PiwZoBK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dljx1pJR.css">
</head>
<body>
<div id="root"></div>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Markr - Nostr Bookmarks</title>
<title>Boris - Nostr Bookmarks</title>
</head>
<body>
<div id="root"></div>

19
kind-icons.txt Normal file
View File

@@ -0,0 +1,19 @@
kind:0 = fa-circle-user
kind:1 = fa-feather
kind:6 = fa-retweet
kind:7 = fa-heart
kind:20 = fa-image
kind:21 = fa-video
kind:22 = fa-video
kind:1063 = fa-file
kind:1337 = fa-laptop-code
kind:1617 = fa-code-pull-request
kind:1621 = fa-bug
kind:1984 = fa-exclamation-triangle
kind:9735 = fa-bolt
kind:9321 = fa-cloud-bolt
kind:9802 = fa-highlighter
kind:30023 = fa-newspaper
kind:10000 = fa-eye-slash
kind:10001 = fa-thumbtack
kind:10003 = fa-bookmark

1291
node_modules/.package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

21
node_modules/@cashu/crypto/LICENSE generated vendored
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 bitcoinjs contributors, gandlaf21
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

12
node_modules/@cashu/crypto/README.md generated vendored
View File

@@ -1,12 +0,0 @@
# Cashu Crypto
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/gandlafbtc/cashu-crypto-ts/node.js.yml)
![GitHub issues](https://img.shields.io/github/issues/gandlafbtc/cashu-crypto-ts)
![GitHub package.json version](https://img.shields.io/github/package-json/v/gandlafbtc/cashu-crypto-ts)
![npm](https://img.shields.io/npm/v/@gandlaf21/cashu-crypto)
![npm type definitions](https://img.shields.io/npm/types/@gandlaf21/cashu-crypto)
![npm bundle size](https://img.shields.io/bundlephobia/min/@gandlaf21/cashu-crypto)
[code coverage](https://gandlafbtc.github.io/cashu-crypto-ts/coverage)
Basic crypto operations for cashu wallets and mints written in TypeScript

View File

@@ -1,5 +0,0 @@
export declare const deriveSecret: (seed: Uint8Array, keysetId: string, counter: number) => Uint8Array;
export declare const deriveBlindingFactor: (seed: Uint8Array, keysetId: string, counter: number) => Uint8Array;
export declare const generateNewMnemonic: () => string;
export declare const deriveSeedFromMnemonic: (mnemonic: string) => Uint8Array;
//# sourceMappingURL=NUT09.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT09.d.ts","sourceRoot":"","sources":["../../src/client/NUT09.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,YAAY,SAAU,UAAU,YAAY,MAAM,WAAW,MAAM,KAAG,UAElF,CAAC;AAEF,eAAO,MAAM,oBAAoB,SAC1B,UAAU,YACN,MAAM,WACP,MAAM,KACb,UAEF,CAAC;AAkBF,eAAO,MAAM,mBAAmB,QAAO,MAGtC,CAAC;AAEF,eAAO,MAAM,sBAAsB,aAAc,MAAM,KAAG,UAGzD,CAAC"}

View File

@@ -1,42 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.deriveSeedFromMnemonic = exports.generateNewMnemonic = exports.deriveBlindingFactor = exports.deriveSecret = void 0;
const bip32_1 = require("@scure/bip32");
const index_js_1 = require("../common/index.js");
const bip39_1 = require("@scure/bip39");
const english_1 = require("@scure/bip39/wordlists/english");
const STANDARD_DERIVATION_PATH = `m/129372'/0'`;
var DerivationType;
(function (DerivationType) {
DerivationType[DerivationType["SECRET"] = 0] = "SECRET";
DerivationType[DerivationType["BLINDING_FACTOR"] = 1] = "BLINDING_FACTOR";
})(DerivationType || (DerivationType = {}));
const deriveSecret = (seed, keysetId, counter) => {
return derive(seed, keysetId, counter, DerivationType.SECRET);
};
exports.deriveSecret = deriveSecret;
const deriveBlindingFactor = (seed, keysetId, counter) => {
return derive(seed, keysetId, counter, DerivationType.BLINDING_FACTOR);
};
exports.deriveBlindingFactor = deriveBlindingFactor;
const derive = (seed, keysetId, counter, secretOrBlinding) => {
const hdkey = bip32_1.HDKey.fromMasterSeed(seed);
const keysetIdInt = (0, index_js_1.getKeysetIdInt)(keysetId);
const derivationPath = `${STANDARD_DERIVATION_PATH}/${keysetIdInt}'/${counter}'/${secretOrBlinding}`;
const derived = hdkey.derive(derivationPath);
if (derived.privateKey === null) {
throw new Error('Could not derive private key');
}
return derived.privateKey;
};
const generateNewMnemonic = () => {
const mnemonic = (0, bip39_1.generateMnemonic)(english_1.wordlist, 128);
return mnemonic;
};
exports.generateNewMnemonic = generateNewMnemonic;
const deriveSeedFromMnemonic = (mnemonic) => {
const seed = (0, bip39_1.mnemonicToSeedSync)(mnemonic);
return seed;
};
exports.deriveSeedFromMnemonic = deriveSeedFromMnemonic;
//# sourceMappingURL=NUT09.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT09.js","sourceRoot":"","sources":["../../src/client/NUT09.ts"],"names":[],"mappings":";;;AAAA,wCAAqC;AACrC,iDAAoD;AACpD,wCAAoE;AACpE,4DAA0D;AAE1D,MAAM,wBAAwB,GAAG,cAAc,CAAC;AAEhD,IAAK,cAGJ;AAHD,WAAK,cAAc;IAClB,uDAAU,CAAA;IACV,yEAAmB,CAAA;AACpB,CAAC,EAHI,cAAc,KAAd,cAAc,QAGlB;AAEM,MAAM,YAAY,GAAG,CAAC,IAAgB,EAAE,QAAgB,EAAE,OAAe,EAAc,EAAE;IAC/F,OAAO,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;AAC/D,CAAC,CAAC;AAFW,QAAA,YAAY,gBAEvB;AAEK,MAAM,oBAAoB,GAAG,CACnC,IAAgB,EAChB,QAAgB,EAChB,OAAe,EACF,EAAE;IACf,OAAO,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;AACxE,CAAC,CAAC;AANW,QAAA,oBAAoB,wBAM/B;AAEF,MAAM,MAAM,GAAG,CACd,IAAgB,EAChB,QAAgB,EAChB,OAAe,EACf,gBAAgC,EACnB,EAAE;IACf,MAAM,KAAK,GAAG,aAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,WAAW,GAAG,IAAA,yBAAc,EAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,cAAc,GAAG,GAAG,wBAAwB,IAAI,WAAW,KAAK,OAAO,KAAK,gBAAgB,EAAE,CAAC;IACrG,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAC7C,IAAI,OAAO,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,OAAO,CAAC,UAAU,CAAC;AAC3B,CAAC,CAAC;AAEK,MAAM,mBAAmB,GAAG,GAAW,EAAE;IAC/C,MAAM,QAAQ,GAAG,IAAA,wBAAgB,EAAC,kBAAQ,EAAE,GAAG,CAAC,CAAC;IACjD,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC;AAHW,QAAA,mBAAmB,uBAG9B;AAEK,MAAM,sBAAsB,GAAG,CAAC,QAAgB,EAAc,EAAE;IACtE,MAAM,IAAI,GAAG,IAAA,0BAAkB,EAAC,QAAQ,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC;AACb,CAAC,CAAC;AAHW,QAAA,sBAAsB,0BAGjC"}

View File

@@ -1,7 +0,0 @@
import { PrivKey } from '@noble/curves/abstract/utils';
import { Proof } from '../common/index.js';
export declare const createP2PKsecret: (pubkey: string) => Uint8Array;
export declare const signP2PKsecret: (secret: Uint8Array, privateKey: PrivKey) => Uint8Array;
export declare const getSignedProofs: (proofs: Array<Proof>, privateKey: string) => Array<Proof>;
export declare const getSignedProof: (proof: Proof, privateKey: PrivKey) => Proof;
//# sourceMappingURL=NUT11.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.d.ts","sourceRoot":"","sources":["../../src/client/NUT11.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA0B,MAAM,8BAA8B,CAAC;AAK/E,OAAO,EAAE,KAAK,EAAU,MAAM,oBAAoB,CAAC;AAEnD,eAAO,MAAM,gBAAgB,WAAY,MAAM,KAAG,UAUjD,CAAC;AAEF,eAAO,MAAM,cAAc,WAAY,UAAU,cAAc,OAAO,eAIrE,CAAC;AAEF,eAAO,MAAM,eAAe,WAAY,MAAM,KAAK,CAAC,cAAc,MAAM,KAAG,MAAM,KAAK,CAYrF,CAAC;AAEF,eAAO,MAAM,cAAc,UAAW,KAAK,cAAc,OAAO,KAAG,KAOlE,CAAC"}

View File

@@ -1,51 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSignedProof = exports.getSignedProofs = exports.signP2PKsecret = exports.createP2PKsecret = void 0;
const utils_1 = require("@noble/curves/abstract/utils");
const sha256_1 = require("@noble/hashes/sha256");
const secp256k1_1 = require("@noble/curves/secp256k1");
const utils_2 = require("@noble/hashes/utils");
const NUT11_js_1 = require("../common/NUT11.js");
const createP2PKsecret = (pubkey) => {
const newSecret = [
'P2PK',
{
nonce: (0, utils_1.bytesToHex)((0, utils_2.randomBytes)(32)),
data: pubkey
}
];
const parsed = JSON.stringify(newSecret);
return new TextEncoder().encode(parsed);
};
exports.createP2PKsecret = createP2PKsecret;
const signP2PKsecret = (secret, privateKey) => {
const msghash = (0, sha256_1.sha256)(new TextDecoder().decode(secret));
const sig = secp256k1_1.schnorr.sign(msghash, privateKey);
return sig;
};
exports.signP2PKsecret = signP2PKsecret;
const getSignedProofs = (proofs, privateKey) => {
return proofs.map((p) => {
try {
const parsed = (0, NUT11_js_1.parseSecret)(p.secret);
if (parsed[0] !== 'P2PK') {
throw new Error('unknown secret type');
}
return (0, exports.getSignedProof)(p, (0, utils_1.hexToBytes)(privateKey));
}
catch (error) {
return p;
}
});
};
exports.getSignedProofs = getSignedProofs;
const getSignedProof = (proof, privateKey) => {
if (!proof.witness) {
proof.witness = {
signatures: [(0, utils_1.bytesToHex)((0, exports.signP2PKsecret)(proof.secret, privateKey))]
};
}
return proof;
};
exports.getSignedProof = getSignedProof;
//# sourceMappingURL=NUT11.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.js","sourceRoot":"","sources":["../../src/client/NUT11.ts"],"names":[],"mappings":";;;AAAA,wDAA+E;AAC/E,iDAA8C;AAC9C,uDAAkD;AAClD,+CAAkD;AAClD,iDAAiD;AAG1C,MAAM,gBAAgB,GAAG,CAAC,MAAc,EAAc,EAAE;IAC9D,MAAM,SAAS,GAAW;QACzB,MAAM;QACN;YACC,KAAK,EAAE,IAAA,kBAAU,EAAC,IAAA,mBAAW,EAAC,EAAE,CAAC,CAAC;YAClC,IAAI,EAAE,MAAM;SACZ;KACD,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzC,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC,CAAC;AAVW,QAAA,gBAAgB,oBAU3B;AAEK,MAAM,cAAc,GAAG,CAAC,MAAkB,EAAE,UAAmB,EAAE,EAAE;IACzE,MAAM,OAAO,GAAG,IAAA,eAAM,EAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACzD,MAAM,GAAG,GAAG,mBAAO,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC9C,OAAO,GAAG,CAAC;AACZ,CAAC,CAAC;AAJW,QAAA,cAAc,kBAIzB;AAEK,MAAM,eAAe,GAAG,CAAC,MAAoB,EAAE,UAAkB,EAAgB,EAAE;IACzF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACvB,IAAI,CAAC;YACJ,MAAM,MAAM,GAAW,IAAA,sBAAW,EAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC7C,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;gBAC1B,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;YACxC,CAAC;YACD,OAAO,IAAA,sBAAc,EAAC,CAAC,EAAE,IAAA,kBAAU,EAAC,UAAU,CAAC,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,CAAC;QACV,CAAC;IACF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC;AAZW,QAAA,eAAe,mBAY1B;AAEK,MAAM,cAAc,GAAG,CAAC,KAAY,EAAE,UAAmB,EAAS,EAAE;IAC1E,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACpB,KAAK,CAAC,OAAO,GAAG;YACf,UAAU,EAAE,CAAC,IAAA,kBAAU,EAAC,IAAA,sBAAc,EAAC,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;SAClE,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC,CAAC;AAPW,QAAA,cAAc,kBAOzB"}

View File

@@ -1,15 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
import type { BlindSignature, Proof, SerializedBlindedMessage, SerializedProof } from '../common/index.js';
export type BlindedMessage = {
B_: ProjPointType<bigint>;
r: bigint;
secret: Uint8Array;
};
export declare function createRandomBlindedMessage(): BlindedMessage;
export declare function blindMessage(secret: Uint8Array, r?: bigint): BlindedMessage;
export declare function unblindSignature(C_: ProjPointType<bigint>, r: bigint, A: ProjPointType<bigint>): ProjPointType<bigint>;
export declare function constructProofFromPromise(promise: BlindSignature, r: bigint, secret: Uint8Array, key: ProjPointType<bigint>): Proof;
export declare const serializeProof: (proof: Proof) => SerializedProof;
export declare const deserializeProof: (proof: SerializedProof) => Proof;
export declare const serializeBlindedMessage: (bm: BlindedMessage, amount: number) => SerializedBlindedMessage;
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAInE,OAAO,KAAK,EACX,cAAc,EACd,KAAK,EACL,wBAAwB,EACxB,eAAe,EACf,MAAM,oBAAoB,CAAC;AAI5B,MAAM,MAAM,cAAc,GAAG;IAC5B,EAAE,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF,wBAAgB,0BAA0B,IAAI,cAAc,CAE3D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,cAAc,CAQ3E;AAED,wBAAgB,gBAAgB,CAC/B,EAAE,EAAE,aAAa,CAAC,MAAM,CAAC,EACzB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GACtB,aAAa,CAAC,MAAM,CAAC,CAGvB;AAED,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,cAAc,EACvB,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,GACxB,KAAK,CAUP;AAED,eAAO,MAAM,cAAc,UAAW,KAAK,KAAG,eAQ7C,CAAC;AAEF,eAAO,MAAM,gBAAgB,UAAW,eAAe,KAAG,KAQzD,CAAC;AACF,eAAO,MAAM,uBAAuB,OAC/B,cAAc,UACV,MAAM,KACZ,wBAKF,CAAC"}

View File

@@ -1,66 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.serializeBlindedMessage = exports.deserializeProof = exports.serializeProof = exports.constructProofFromPromise = exports.unblindSignature = exports.blindMessage = exports.createRandomBlindedMessage = void 0;
const secp256k1_1 = require("@noble/curves/secp256k1");
const utils_1 = require("@noble/hashes/utils");
const utils_js_1 = require("../util/utils.js");
const index_js_1 = require("../common/index.js");
function createRandomBlindedMessage() {
return blindMessage((0, utils_1.randomBytes)(32));
}
exports.createRandomBlindedMessage = createRandomBlindedMessage;
function blindMessage(secret, r) {
const Y = (0, index_js_1.hashToCurve)(secret);
if (!r) {
r = (0, utils_js_1.bytesToNumber)(secp256k1_1.secp256k1.utils.randomPrivateKey());
}
const rG = secp256k1_1.secp256k1.ProjectivePoint.BASE.multiply(r);
const B_ = Y.add(rG);
return { B_, r, secret };
}
exports.blindMessage = blindMessage;
function unblindSignature(C_, r, A) {
const C = C_.subtract(A.multiply(r));
return C;
}
exports.unblindSignature = unblindSignature;
function constructProofFromPromise(promise, r, secret, key) {
const A = key;
const C = unblindSignature(promise.C_, r, A);
const proof = {
id: promise.id,
amount: promise.amount,
secret,
C
};
return proof;
}
exports.constructProofFromPromise = constructProofFromPromise;
const serializeProof = (proof) => {
return {
amount: proof.amount,
C: proof.C.toHex(true),
id: proof.id,
secret: new TextDecoder().decode(proof.secret),
witness: JSON.stringify(proof.witness)
};
};
exports.serializeProof = serializeProof;
const deserializeProof = (proof) => {
return {
amount: proof.amount,
C: (0, index_js_1.pointFromHex)(proof.C),
id: proof.id,
secret: new TextEncoder().encode(proof.secret),
witness: proof.witness ? JSON.parse(proof.witness) : undefined
};
};
exports.deserializeProof = deserializeProof;
const serializeBlindedMessage = (bm, amount) => {
return {
B_: bm.B_.toHex(true),
amount: amount
};
};
exports.serializeBlindedMessage = serializeBlindedMessage;
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":";;;AACA,uDAAoD;AACpD,+CAA0E;AAC1E,+CAAiD;AAOjD,iDAA+D;AAS/D,SAAgB,0BAA0B;IACzC,OAAO,YAAY,CAAC,IAAA,mBAAW,EAAC,EAAE,CAAC,CAAC,CAAC;AACtC,CAAC;AAFD,gEAEC;AAED,SAAgB,YAAY,CAAC,MAAkB,EAAE,CAAU;IAC1D,MAAM,CAAC,GAAG,IAAA,sBAAW,EAAC,MAAM,CAAC,CAAC;IAC9B,IAAI,CAAC,CAAC,EAAE,CAAC;QACR,CAAC,GAAG,IAAA,wBAAa,EAAC,qBAAS,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,EAAE,GAAG,qBAAS,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACrB,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AARD,oCAQC;AAED,SAAgB,gBAAgB,CAC/B,EAAyB,EACzB,CAAS,EACT,CAAwB;IAExB,MAAM,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,OAAO,CAAC,CAAC;AACV,CAAC;AAPD,4CAOC;AAED,SAAgB,yBAAyB,CACxC,OAAuB,EACvB,CAAS,EACT,MAAkB,EAClB,GAA0B;IAE1B,MAAM,CAAC,GAAG,GAAG,CAAC;IACd,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG;QACb,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM;QACN,CAAC;KACD,CAAC;IACF,OAAO,KAAK,CAAC;AACd,CAAC;AAfD,8DAeC;AAEM,MAAM,cAAc,GAAG,CAAC,KAAY,EAAmB,EAAE;IAC/D,OAAO;QACN,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACtB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAC9C,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC;KACtC,CAAC;AACH,CAAC,CAAC;AARW,QAAA,cAAc,kBAQzB;AAEK,MAAM,gBAAgB,GAAG,CAAC,KAAsB,EAAS,EAAE;IACjE,OAAO;QACN,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,CAAC,EAAE,IAAA,uBAAY,EAAC,KAAK,CAAC,CAAC,CAAC;QACxB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAC9C,OAAO,EAAE,KAAK,CAAC,OAAO,CAAA,CAAC,CAAA,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA,CAAC,CAAA,SAAS;KAC1D,CAAC;AACH,CAAC,CAAC;AARW,QAAA,gBAAgB,oBAQ3B;AACK,MAAM,uBAAuB,GAAG,CACtC,EAAkB,EAClB,MAAc,EACa,EAAE;IAC7B,OAAO;QACN,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM;KACd,CAAC;AACH,CAAC,CAAC;AARW,QAAA,uBAAuB,2BAQlC"}

View File

@@ -1,3 +0,0 @@
import { Secret } from "./index.js";
export declare const parseSecret: (secret: string | Uint8Array) => Secret;
//# sourceMappingURL=NUT11.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.d.ts","sourceRoot":"","sources":["../../src/common/NUT11.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,eAAO,MAAM,WAAW,WAAY,MAAM,GAAG,UAAU,WAStD,CAAC"}

View File

@@ -1,16 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseSecret = void 0;
const parseSecret = (secret) => {
try {
if (secret instanceof Uint8Array) {
secret = new TextDecoder().decode(secret);
}
return JSON.parse(secret);
}
catch (e) {
throw new Error("can't parse secret");
}
};
exports.parseSecret = parseSecret;
//# sourceMappingURL=NUT11.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.js","sourceRoot":"","sources":["../../src/common/NUT11.ts"],"names":[],"mappings":";;;AAEO,MAAM,WAAW,GAAG,CAAC,MAA2B,EAAU,EAAE;IAClE,IAAI,CAAC;QACJ,IAAI,MAAM,YAAY,UAAU,EAAE,CAAC;YAClC,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACvC,CAAC;AACF,CAAC,CAAC;AATW,QAAA,WAAW,eAStB"}

View File

@@ -1,65 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
export type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc['length']]>;
export type IntRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;
export type MintKeys = {
[k: string]: Uint8Array;
};
export type SerializedMintKeys = {
[k: string]: string;
};
export type Keyset = {
id: string;
unit: string;
active: boolean;
};
export type BlindSignature = {
C_: ProjPointType<bigint>;
amount: number;
id: string;
};
export type SerializedBlindSignature = {
C_: string;
amount: number;
id: string;
};
export type Proof = {
C: ProjPointType<bigint>;
secret: Uint8Array;
amount: number;
id: string;
witness?: Witness;
};
export type SerializedProof = {
C: string;
secret: string;
amount: number;
id: string;
witness?: string;
};
export type SerializedBlindedMessage = {
B_: string;
amount: number;
witness?: string;
};
export type Secret = [WellKnownSecret, SecretData];
export type WellKnownSecret = 'P2PK';
export type SecretData = {
nonce: string;
data: string;
tags?: Array<Array<string>>;
};
export type Witness = {
signatures: Array<string>;
};
export type Tags = {
[k: string]: string;
};
export type SigFlag = 'SIG_INPUTS' | 'SIG_ALL';
export declare function hashToCurve(secret: Uint8Array): ProjPointType<bigint>;
export declare function pointFromHex(hex: string): ProjPointType<bigint>;
export declare const getKeysetIdInt: (keysetId: string) => bigint;
export declare function createRandomPrivateKey(): Uint8Array;
export declare function serializeMintKeys(mintKeys: MintKeys): SerializedMintKeys;
export declare function deserializeMintKeys(serializedMintKeys: SerializedMintKeys): MintKeys;
export declare function deriveKeysetId(keys: MintKeys): string;
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/common/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAOnE,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,GACzF,GAAG,CAAC,MAAM,CAAC,GACX,SAAS,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEzC,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AAE/F,MAAM,MAAM,QAAQ,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAAE,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG;IAChC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC5B,EAAE,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG;IACnB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;AAEnD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC,MAAM,MAAM,UAAU,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACrB,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAClB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,YAAY,GAAG,SAAS,CAAC;AAI/C,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,CAcrE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,yBAEvC;AAED,eAAO,MAAM,cAAc,aAAc,MAAM,KAAG,MASjD,CAAC;AAEF,wBAAgB,sBAAsB,eAErC;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,kBAAkB,CAMxE;AAED,wBAAgB,mBAAmB,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,QAAQ,CAMpF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAarD"}

View File

@@ -1,85 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.deriveKeysetId = exports.deserializeMintKeys = exports.serializeMintKeys = exports.createRandomPrivateKey = exports.getKeysetIdInt = exports.pointFromHex = exports.hashToCurve = void 0;
const secp256k1_1 = require("@noble/curves/secp256k1");
const sha256_1 = require("@noble/hashes/sha256");
const utils_1 = require("@noble/curves/abstract/utils");
const utils_js_1 = require("../util/utils.js");
const buffer_1 = require("buffer/");
const DOMAIN_SEPARATOR = (0, utils_1.hexToBytes)('536563703235366b315f48617368546f43757276655f43617368755f');
function hashToCurve(secret) {
const msgToHash = (0, sha256_1.sha256)(buffer_1.Buffer.concat([DOMAIN_SEPARATOR, secret]));
const counter = new Uint32Array(1);
const maxIterations = 2 ** 16;
for (let i = 0; i < maxIterations; i++) {
const counterBytes = new Uint8Array(counter.buffer);
const hash = (0, sha256_1.sha256)(buffer_1.Buffer.concat([msgToHash, counterBytes]));
try {
return pointFromHex((0, utils_1.bytesToHex)(buffer_1.Buffer.concat([new Uint8Array([0x02]), hash])));
}
catch (error) {
counter[0]++;
}
}
throw new Error('No valid point found');
}
exports.hashToCurve = hashToCurve;
function pointFromHex(hex) {
return secp256k1_1.secp256k1.ProjectivePoint.fromHex(hex);
}
exports.pointFromHex = pointFromHex;
const getKeysetIdInt = (keysetId) => {
let keysetIdInt;
if (/^[a-fA-F0-9]+$/.test(keysetId)) {
keysetIdInt = (0, utils_js_1.hexToNumber)(keysetId) % BigInt(2 ** 31 - 1);
}
else {
//legacy keyset compatibility
keysetIdInt = (0, utils_js_1.bytesToNumber)((0, utils_js_1.encodeBase64toUint8)(keysetId)) % BigInt(2 ** 31 - 1);
}
return keysetIdInt;
};
exports.getKeysetIdInt = getKeysetIdInt;
function createRandomPrivateKey() {
return secp256k1_1.secp256k1.utils.randomPrivateKey();
}
exports.createRandomPrivateKey = createRandomPrivateKey;
function serializeMintKeys(mintKeys) {
const serializedMintKeys = {};
Object.keys(mintKeys).forEach((p) => {
serializedMintKeys[p] = (0, utils_1.bytesToHex)(mintKeys[p]);
});
return serializedMintKeys;
}
exports.serializeMintKeys = serializeMintKeys;
function deserializeMintKeys(serializedMintKeys) {
const mintKeys = {};
Object.keys(serializedMintKeys).forEach((p) => {
mintKeys[p] = (0, utils_1.hexToBytes)(serializedMintKeys[p]);
});
return mintKeys;
}
exports.deserializeMintKeys = deserializeMintKeys;
function deriveKeysetId(keys) {
const KEYSET_VERSION = '00';
const mapBigInt = (k) => {
return [BigInt(k[0]), k[1]];
};
const pubkeysConcat = Object.entries(serializeMintKeys(keys))
.map(mapBigInt)
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
.map(([, pubKey]) => (0, utils_1.hexToBytes)(pubKey))
.reduce((prev, curr) => mergeUInt8Arrays(prev, curr), new Uint8Array());
const hash = (0, sha256_1.sha256)(pubkeysConcat);
const hashHex = buffer_1.Buffer.from(hash).toString('hex').slice(0, 14);
return '00' + hashHex;
}
exports.deriveKeysetId = deriveKeysetId;
function mergeUInt8Arrays(a1, a2) {
// sum of individual array lengths
const mergedArray = new Uint8Array(a1.length + a2.length);
mergedArray.set(a1);
mergedArray.set(a2, a1.length);
return mergedArray;
}
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/common/index.ts"],"names":[],"mappings":";;;AACA,uDAAoD;AACpD,iDAA8C;AAC9C,wDAAsE;AACtE,+CAAmF;AACnF,oCAAiC;AA0EjC,MAAM,gBAAgB,GAAG,IAAA,kBAAU,EAAC,0DAA0D,CAAC,CAAC;AAEhG,SAAgB,WAAW,CAAC,MAAkB;IAC7C,MAAM,SAAS,GAAG,IAAA,eAAM,EAAC,eAAM,CAAC,MAAM,CAAC,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,YAAY,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,IAAA,eAAM,EAAC,eAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC;YACJ,OAAO,YAAY,CAAC,IAAA,kBAAU,EAAC,eAAM,CAAC,MAAM,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACd,CAAC;IACF,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;AACzC,CAAC;AAdD,kCAcC;AAED,SAAgB,YAAY,CAAC,GAAW;IACvC,OAAO,qBAAS,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAC/C,CAAC;AAFD,oCAEC;AAEM,MAAM,cAAc,GAAG,CAAC,QAAgB,EAAU,EAAE;IAC1D,IAAI,WAAmB,CAAC;IACxB,IAAI,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrC,WAAW,GAAG,IAAA,sBAAW,EAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;SAAM,CAAC;QACP,6BAA6B;QAC7B,WAAW,GAAG,IAAA,wBAAa,EAAC,IAAA,8BAAmB,EAAC,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,WAAW,CAAC;AACpB,CAAC,CAAC;AATW,QAAA,cAAc,kBASzB;AAEF,SAAgB,sBAAsB;IACrC,OAAO,qBAAS,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;AAC3C,CAAC;AAFD,wDAEC;AAED,SAAgB,iBAAiB,CAAC,QAAkB;IACnD,MAAM,kBAAkB,GAAuB,EAAE,CAAC;IAClD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QACnC,kBAAkB,CAAC,CAAC,CAAC,GAAG,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,OAAO,kBAAkB,CAAC;AAC3B,CAAC;AAND,8CAMC;AAED,SAAgB,mBAAmB,CAAC,kBAAsC;IACzE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QAC7C,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAA,kBAAU,EAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC;AACjB,CAAC;AAND,kDAMC;AAED,SAAgB,cAAc,CAAC,IAAc;IAC5C,MAAM,cAAc,GAAG,IAAI,CAAC;IAC5B,MAAM,SAAS,GAAG,CAAC,CAAmB,EAAoB,EAAE;QAC3D,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC;IACF,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;SAC3D,GAAG,CAAC,SAAS,CAAC;SACd,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACxD,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,IAAA,kBAAU,EAAC,MAAM,CAAC,CAAC;SACvC,MAAM,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,UAAU,EAAE,CAAC,CAAC;IACzE,MAAM,IAAI,GAAG,IAAA,eAAM,EAAC,aAAa,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,eAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/D,OAAO,IAAI,GAAG,OAAO,CAAC;AACvB,CAAC;AAbD,wCAaC;AAED,SAAS,gBAAgB,CAAC,EAAc,EAAE,EAAc;IACvD,kCAAkC;IAClC,MAAM,WAAW,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;IAC1D,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpB,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,WAAW,CAAC;AACpB,CAAC"}

View File

@@ -1,4 +0,0 @@
export declare const deriveSecret: (seed: Uint8Array, keysetId: string, counter: number) => Uint8Array;
export declare const deriveBlindingFactor: (seed: Uint8Array, keysetId: string, counter: number) => Uint8Array;
export declare const generateNewMnemonic: () => string;
export declare const deriveSeedFromMnemonic: (mnemonic: string) => Uint8Array;

View File

@@ -1,42 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.deriveSeedFromMnemonic = exports.generateNewMnemonic = exports.deriveBlindingFactor = exports.deriveSecret = void 0;
const bip32_1 = require("@scure/bip32");
const index_js_1 = require("../common/index.js");
const bip39_1 = require("@scure/bip39");
const english_1 = require("@scure/bip39/wordlists/english");
const STANDARD_DERIVATION_PATH = `m/129372'/0'`;
var DerivationType;
(function (DerivationType) {
DerivationType[DerivationType["SECRET"] = 0] = "SECRET";
DerivationType[DerivationType["BLINDING_FACTOR"] = 1] = "BLINDING_FACTOR";
})(DerivationType || (DerivationType = {}));
const deriveSecret = (seed, keysetId, counter) => {
return derive(seed, keysetId, counter, DerivationType.SECRET);
};
exports.deriveSecret = deriveSecret;
const deriveBlindingFactor = (seed, keysetId, counter) => {
return derive(seed, keysetId, counter, DerivationType.BLINDING_FACTOR);
};
exports.deriveBlindingFactor = deriveBlindingFactor;
const derive = (seed, keysetId, counter, secretOrBlinding) => {
const hdkey = bip32_1.HDKey.fromMasterSeed(seed);
const keysetIdInt = (0, index_js_1.getKeysetIdInt)(keysetId);
const derivationPath = `${STANDARD_DERIVATION_PATH}/${keysetIdInt}'/${counter}'/${secretOrBlinding}`;
const derived = hdkey.derive(derivationPath);
if (derived.privateKey === null) {
throw new Error('Could not derive private key');
}
return derived.privateKey;
};
const generateNewMnemonic = () => {
const mnemonic = (0, bip39_1.generateMnemonic)(english_1.wordlist, 128);
return mnemonic;
};
exports.generateNewMnemonic = generateNewMnemonic;
const deriveSeedFromMnemonic = (mnemonic) => {
const seed = (0, bip39_1.mnemonicToSeedSync)(mnemonic);
return seed;
};
exports.deriveSeedFromMnemonic = deriveSeedFromMnemonic;
//# sourceMappingURL=NUT09.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT09.js","sourceRoot":"","sources":["../../../src/client/NUT09.ts"],"names":[],"mappings":";;;AAAA,wCAAqC;AACrC,iDAAoD;AACpD,wCAAoE;AACpE,4DAA0D;AAE1D,MAAM,wBAAwB,GAAG,cAAc,CAAC;AAEhD,IAAK,cAGJ;AAHD,WAAK,cAAc;IAClB,uDAAU,CAAA;IACV,yEAAmB,CAAA;AACpB,CAAC,EAHI,cAAc,KAAd,cAAc,QAGlB;AAEM,MAAM,YAAY,GAAG,CAAC,IAAgB,EAAE,QAAgB,EAAE,OAAe,EAAc,EAAE;IAC/F,OAAO,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;AAC/D,CAAC,CAAC;AAFW,QAAA,YAAY,gBAEvB;AAEK,MAAM,oBAAoB,GAAG,CACnC,IAAgB,EAChB,QAAgB,EAChB,OAAe,EACF,EAAE;IACf,OAAO,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;AACxE,CAAC,CAAC;AANW,QAAA,oBAAoB,wBAM/B;AAEF,MAAM,MAAM,GAAG,CACd,IAAgB,EAChB,QAAgB,EAChB,OAAe,EACf,gBAAgC,EACnB,EAAE;IACf,MAAM,KAAK,GAAG,aAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,WAAW,GAAG,IAAA,yBAAc,EAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,cAAc,GAAG,GAAG,wBAAwB,IAAI,WAAW,KAAK,OAAO,KAAK,gBAAgB,EAAE,CAAC;IACrG,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAC7C,IAAI,OAAO,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,OAAO,CAAC,UAAU,CAAC;AAC3B,CAAC,CAAC;AAEK,MAAM,mBAAmB,GAAG,GAAW,EAAE;IAC/C,MAAM,QAAQ,GAAG,IAAA,wBAAgB,EAAC,kBAAQ,EAAE,GAAG,CAAC,CAAC;IACjD,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC;AAHW,QAAA,mBAAmB,uBAG9B;AAEK,MAAM,sBAAsB,GAAG,CAAC,QAAgB,EAAc,EAAE;IACtE,MAAM,IAAI,GAAG,IAAA,0BAAkB,EAAC,QAAQ,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC;AACb,CAAC,CAAC;AAHW,QAAA,sBAAsB,0BAGjC"}

View File

@@ -1,6 +0,0 @@
import { PrivKey } from '@noble/curves/abstract/utils';
import { Proof } from '../common/index.js';
export declare const createP2PKsecret: (pubkey: string) => Uint8Array;
export declare const signP2PKsecret: (secret: Uint8Array, privateKey: PrivKey) => Uint8Array;
export declare const getSignedProofs: (proofs: Array<Proof>, privateKey: string) => Array<Proof>;
export declare const getSignedProof: (proof: Proof, privateKey: PrivKey) => Proof;

View File

@@ -1,51 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSignedProof = exports.getSignedProofs = exports.signP2PKsecret = exports.createP2PKsecret = void 0;
const utils_1 = require("@noble/curves/abstract/utils");
const sha256_1 = require("@noble/hashes/sha256");
const secp256k1_1 = require("@noble/curves/secp256k1");
const utils_2 = require("@noble/hashes/utils");
const NUT11_js_1 = require("../common/NUT11.js");
const createP2PKsecret = (pubkey) => {
const newSecret = [
'P2PK',
{
nonce: (0, utils_1.bytesToHex)((0, utils_2.randomBytes)(32)),
data: pubkey
}
];
const parsed = JSON.stringify(newSecret);
return new TextEncoder().encode(parsed);
};
exports.createP2PKsecret = createP2PKsecret;
const signP2PKsecret = (secret, privateKey) => {
const msghash = (0, sha256_1.sha256)(new TextDecoder().decode(secret));
const sig = secp256k1_1.schnorr.sign(msghash, privateKey);
return sig;
};
exports.signP2PKsecret = signP2PKsecret;
const getSignedProofs = (proofs, privateKey) => {
return proofs.map((p) => {
try {
const parsed = (0, NUT11_js_1.parseSecret)(p.secret);
if (parsed[0] !== 'P2PK') {
throw new Error('unknown secret type');
}
return (0, exports.getSignedProof)(p, (0, utils_1.hexToBytes)(privateKey));
}
catch (error) {
return p;
}
});
};
exports.getSignedProofs = getSignedProofs;
const getSignedProof = (proof, privateKey) => {
if (!proof.witness) {
proof.witness = {
signatures: [(0, utils_1.bytesToHex)((0, exports.signP2PKsecret)(proof.secret, privateKey))]
};
}
return proof;
};
exports.getSignedProof = getSignedProof;
//# sourceMappingURL=NUT11.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.js","sourceRoot":"","sources":["../../../src/client/NUT11.ts"],"names":[],"mappings":";;;AAAA,wDAA+E;AAC/E,iDAA8C;AAC9C,uDAAkD;AAClD,+CAAkD;AAClD,iDAAiD;AAG1C,MAAM,gBAAgB,GAAG,CAAC,MAAc,EAAc,EAAE;IAC9D,MAAM,SAAS,GAAW;QACzB,MAAM;QACN;YACC,KAAK,EAAE,IAAA,kBAAU,EAAC,IAAA,mBAAW,EAAC,EAAE,CAAC,CAAC;YAClC,IAAI,EAAE,MAAM;SACZ;KACD,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzC,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC,CAAC;AAVW,QAAA,gBAAgB,oBAU3B;AAEK,MAAM,cAAc,GAAG,CAAC,MAAkB,EAAE,UAAmB,EAAE,EAAE;IACzE,MAAM,OAAO,GAAG,IAAA,eAAM,EAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACzD,MAAM,GAAG,GAAG,mBAAO,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC9C,OAAO,GAAG,CAAC;AACZ,CAAC,CAAC;AAJW,QAAA,cAAc,kBAIzB;AAEK,MAAM,eAAe,GAAG,CAAC,MAAoB,EAAE,UAAkB,EAAgB,EAAE;IACzF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACvB,IAAI,CAAC;YACJ,MAAM,MAAM,GAAW,IAAA,sBAAW,EAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC7C,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;gBAC1B,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;YACxC,CAAC;YACD,OAAO,IAAA,sBAAc,EAAC,CAAC,EAAE,IAAA,kBAAU,EAAC,UAAU,CAAC,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,CAAC;QACV,CAAC;IACF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC;AAZW,QAAA,eAAe,mBAY1B;AAEK,MAAM,cAAc,GAAG,CAAC,KAAY,EAAE,UAAmB,EAAS,EAAE;IAC1E,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACpB,KAAK,CAAC,OAAO,GAAG;YACf,UAAU,EAAE,CAAC,IAAA,kBAAU,EAAC,IAAA,sBAAc,EAAC,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;SAClE,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC,CAAC;AAPW,QAAA,cAAc,kBAOzB"}

View File

@@ -1,14 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
import type { BlindSignature, Proof, SerializedBlindedMessage, SerializedProof } from '../common/index.js';
export type BlindedMessage = {
B_: ProjPointType<bigint>;
r: bigint;
secret: Uint8Array;
};
export declare function createRandomBlindedMessage(): BlindedMessage;
export declare function blindMessage(secret: Uint8Array, r?: bigint): BlindedMessage;
export declare function unblindSignature(C_: ProjPointType<bigint>, r: bigint, A: ProjPointType<bigint>): ProjPointType<bigint>;
export declare function constructProofFromPromise(promise: BlindSignature, r: bigint, secret: Uint8Array, key: ProjPointType<bigint>): Proof;
export declare const serializeProof: (proof: Proof) => SerializedProof;
export declare const deserializeProof: (proof: SerializedProof) => Proof;
export declare const serializeBlindedMessage: (bm: BlindedMessage, amount: number) => SerializedBlindedMessage;

View File

@@ -1,66 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.serializeBlindedMessage = exports.deserializeProof = exports.serializeProof = exports.constructProofFromPromise = exports.unblindSignature = exports.blindMessage = exports.createRandomBlindedMessage = void 0;
const secp256k1_1 = require("@noble/curves/secp256k1");
const utils_1 = require("@noble/hashes/utils");
const utils_js_1 = require("../util/utils.js");
const index_js_1 = require("../common/index.js");
function createRandomBlindedMessage() {
return blindMessage((0, utils_1.randomBytes)(32));
}
exports.createRandomBlindedMessage = createRandomBlindedMessage;
function blindMessage(secret, r) {
const Y = (0, index_js_1.hashToCurve)(secret);
if (!r) {
r = (0, utils_js_1.bytesToNumber)(secp256k1_1.secp256k1.utils.randomPrivateKey());
}
const rG = secp256k1_1.secp256k1.ProjectivePoint.BASE.multiply(r);
const B_ = Y.add(rG);
return { B_, r, secret };
}
exports.blindMessage = blindMessage;
function unblindSignature(C_, r, A) {
const C = C_.subtract(A.multiply(r));
return C;
}
exports.unblindSignature = unblindSignature;
function constructProofFromPromise(promise, r, secret, key) {
const A = key;
const C = unblindSignature(promise.C_, r, A);
const proof = {
id: promise.id,
amount: promise.amount,
secret,
C
};
return proof;
}
exports.constructProofFromPromise = constructProofFromPromise;
const serializeProof = (proof) => {
return {
amount: proof.amount,
C: proof.C.toHex(true),
id: proof.id,
secret: new TextDecoder().decode(proof.secret),
witness: JSON.stringify(proof.witness)
};
};
exports.serializeProof = serializeProof;
const deserializeProof = (proof) => {
return {
amount: proof.amount,
C: (0, index_js_1.pointFromHex)(proof.C),
id: proof.id,
secret: new TextEncoder().encode(proof.secret),
witness: proof.witness ? JSON.parse(proof.witness) : undefined
};
};
exports.deserializeProof = deserializeProof;
const serializeBlindedMessage = (bm, amount) => {
return {
B_: bm.B_.toHex(true),
amount: amount
};
};
exports.serializeBlindedMessage = serializeBlindedMessage;
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":";;;AACA,uDAAoD;AACpD,+CAA0E;AAC1E,+CAAiD;AAOjD,iDAA+D;AAS/D,SAAgB,0BAA0B;IACzC,OAAO,YAAY,CAAC,IAAA,mBAAW,EAAC,EAAE,CAAC,CAAC,CAAC;AACtC,CAAC;AAFD,gEAEC;AAED,SAAgB,YAAY,CAAC,MAAkB,EAAE,CAAU;IAC1D,MAAM,CAAC,GAAG,IAAA,sBAAW,EAAC,MAAM,CAAC,CAAC;IAC9B,IAAI,CAAC,CAAC,EAAE,CAAC;QACR,CAAC,GAAG,IAAA,wBAAa,EAAC,qBAAS,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,EAAE,GAAG,qBAAS,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACrB,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AARD,oCAQC;AAED,SAAgB,gBAAgB,CAC/B,EAAyB,EACzB,CAAS,EACT,CAAwB;IAExB,MAAM,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,OAAO,CAAC,CAAC;AACV,CAAC;AAPD,4CAOC;AAED,SAAgB,yBAAyB,CACxC,OAAuB,EACvB,CAAS,EACT,MAAkB,EAClB,GAA0B;IAE1B,MAAM,CAAC,GAAG,GAAG,CAAC;IACd,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG;QACb,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM;QACN,CAAC;KACD,CAAC;IACF,OAAO,KAAK,CAAC;AACd,CAAC;AAfD,8DAeC;AAEM,MAAM,cAAc,GAAG,CAAC,KAAY,EAAmB,EAAE;IAC/D,OAAO;QACN,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACtB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAC9C,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC;KACtC,CAAC;AACH,CAAC,CAAC;AARW,QAAA,cAAc,kBAQzB;AAEK,MAAM,gBAAgB,GAAG,CAAC,KAAsB,EAAS,EAAE;IACjE,OAAO;QACN,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,CAAC,EAAE,IAAA,uBAAY,EAAC,KAAK,CAAC,CAAC,CAAC;QACxB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAC9C,OAAO,EAAE,KAAK,CAAC,OAAO,CAAA,CAAC,CAAA,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA,CAAC,CAAA,SAAS;KAC1D,CAAC;AACH,CAAC,CAAC;AARW,QAAA,gBAAgB,oBAQ3B;AACK,MAAM,uBAAuB,GAAG,CACtC,EAAkB,EAClB,MAAc,EACa,EAAE;IAC7B,OAAO;QACN,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM;KACd,CAAC;AACH,CAAC,CAAC;AARW,QAAA,uBAAuB,2BAQlC"}

View File

@@ -1,2 +0,0 @@
import { Secret } from "./index.js";
export declare const parseSecret: (secret: string | Uint8Array) => Secret;

View File

@@ -1,16 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseSecret = void 0;
const parseSecret = (secret) => {
try {
if (secret instanceof Uint8Array) {
secret = new TextDecoder().decode(secret);
}
return JSON.parse(secret);
}
catch (e) {
throw new Error("can't parse secret");
}
};
exports.parseSecret = parseSecret;
//# sourceMappingURL=NUT11.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.js","sourceRoot":"","sources":["../../../src/common/NUT11.ts"],"names":[],"mappings":";;;AAEO,MAAM,WAAW,GAAG,CAAC,MAA2B,EAAU,EAAE;IAClE,IAAI,CAAC;QACJ,IAAI,MAAM,YAAY,UAAU,EAAE,CAAC;YAClC,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACvC,CAAC;AACF,CAAC,CAAC;AATW,QAAA,WAAW,eAStB"}

View File

@@ -1,64 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
export type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc['length']]>;
export type IntRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;
export type MintKeys = {
[k: string]: Uint8Array;
};
export type SerializedMintKeys = {
[k: string]: string;
};
export type Keyset = {
id: string;
unit: string;
active: boolean;
};
export type BlindSignature = {
C_: ProjPointType<bigint>;
amount: number;
id: string;
};
export type SerializedBlindSignature = {
C_: string;
amount: number;
id: string;
};
export type Proof = {
C: ProjPointType<bigint>;
secret: Uint8Array;
amount: number;
id: string;
witness?: Witness;
};
export type SerializedProof = {
C: string;
secret: string;
amount: number;
id: string;
witness?: string;
};
export type SerializedBlindedMessage = {
B_: string;
amount: number;
witness?: string;
};
export type Secret = [WellKnownSecret, SecretData];
export type WellKnownSecret = 'P2PK';
export type SecretData = {
nonce: string;
data: string;
tags?: Array<Array<string>>;
};
export type Witness = {
signatures: Array<string>;
};
export type Tags = {
[k: string]: string;
};
export type SigFlag = 'SIG_INPUTS' | 'SIG_ALL';
export declare function hashToCurve(secret: Uint8Array): ProjPointType<bigint>;
export declare function pointFromHex(hex: string): ProjPointType<bigint>;
export declare const getKeysetIdInt: (keysetId: string) => bigint;
export declare function createRandomPrivateKey(): Uint8Array;
export declare function serializeMintKeys(mintKeys: MintKeys): SerializedMintKeys;
export declare function deserializeMintKeys(serializedMintKeys: SerializedMintKeys): MintKeys;
export declare function deriveKeysetId(keys: MintKeys): string;

View File

@@ -1,85 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.deriveKeysetId = exports.deserializeMintKeys = exports.serializeMintKeys = exports.createRandomPrivateKey = exports.getKeysetIdInt = exports.pointFromHex = exports.hashToCurve = void 0;
const secp256k1_1 = require("@noble/curves/secp256k1");
const sha256_1 = require("@noble/hashes/sha256");
const utils_1 = require("@noble/curves/abstract/utils");
const utils_js_1 = require("../util/utils.js");
const buffer_1 = require("buffer/");
const DOMAIN_SEPARATOR = (0, utils_1.hexToBytes)('536563703235366b315f48617368546f43757276655f43617368755f');
function hashToCurve(secret) {
const msgToHash = (0, sha256_1.sha256)(buffer_1.Buffer.concat([DOMAIN_SEPARATOR, secret]));
const counter = new Uint32Array(1);
const maxIterations = 2 ** 16;
for (let i = 0; i < maxIterations; i++) {
const counterBytes = new Uint8Array(counter.buffer);
const hash = (0, sha256_1.sha256)(buffer_1.Buffer.concat([msgToHash, counterBytes]));
try {
return pointFromHex((0, utils_1.bytesToHex)(buffer_1.Buffer.concat([new Uint8Array([0x02]), hash])));
}
catch (error) {
counter[0]++;
}
}
throw new Error('No valid point found');
}
exports.hashToCurve = hashToCurve;
function pointFromHex(hex) {
return secp256k1_1.secp256k1.ProjectivePoint.fromHex(hex);
}
exports.pointFromHex = pointFromHex;
const getKeysetIdInt = (keysetId) => {
let keysetIdInt;
if (/^[a-fA-F0-9]+$/.test(keysetId)) {
keysetIdInt = (0, utils_js_1.hexToNumber)(keysetId) % BigInt(2 ** 31 - 1);
}
else {
//legacy keyset compatibility
keysetIdInt = (0, utils_js_1.bytesToNumber)((0, utils_js_1.encodeBase64toUint8)(keysetId)) % BigInt(2 ** 31 - 1);
}
return keysetIdInt;
};
exports.getKeysetIdInt = getKeysetIdInt;
function createRandomPrivateKey() {
return secp256k1_1.secp256k1.utils.randomPrivateKey();
}
exports.createRandomPrivateKey = createRandomPrivateKey;
function serializeMintKeys(mintKeys) {
const serializedMintKeys = {};
Object.keys(mintKeys).forEach((p) => {
serializedMintKeys[p] = (0, utils_1.bytesToHex)(mintKeys[p]);
});
return serializedMintKeys;
}
exports.serializeMintKeys = serializeMintKeys;
function deserializeMintKeys(serializedMintKeys) {
const mintKeys = {};
Object.keys(serializedMintKeys).forEach((p) => {
mintKeys[p] = (0, utils_1.hexToBytes)(serializedMintKeys[p]);
});
return mintKeys;
}
exports.deserializeMintKeys = deserializeMintKeys;
function deriveKeysetId(keys) {
const KEYSET_VERSION = '00';
const mapBigInt = (k) => {
return [BigInt(k[0]), k[1]];
};
const pubkeysConcat = Object.entries(serializeMintKeys(keys))
.map(mapBigInt)
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
.map(([, pubKey]) => (0, utils_1.hexToBytes)(pubKey))
.reduce((prev, curr) => mergeUInt8Arrays(prev, curr), new Uint8Array());
const hash = (0, sha256_1.sha256)(pubkeysConcat);
const hashHex = buffer_1.Buffer.from(hash).toString('hex').slice(0, 14);
return '00' + hashHex;
}
exports.deriveKeysetId = deriveKeysetId;
function mergeUInt8Arrays(a1, a2) {
// sum of individual array lengths
const mergedArray = new Uint8Array(a1.length + a2.length);
mergedArray.set(a1);
mergedArray.set(a2, a1.length);
return mergedArray;
}
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/common/index.ts"],"names":[],"mappings":";;;AACA,uDAAoD;AACpD,iDAA8C;AAC9C,wDAAsE;AACtE,+CAAmF;AACnF,oCAAiC;AA0EjC,MAAM,gBAAgB,GAAG,IAAA,kBAAU,EAAC,0DAA0D,CAAC,CAAC;AAEhG,SAAgB,WAAW,CAAC,MAAkB;IAC7C,MAAM,SAAS,GAAG,IAAA,eAAM,EAAC,eAAM,CAAC,MAAM,CAAC,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,YAAY,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,IAAA,eAAM,EAAC,eAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC;YACJ,OAAO,YAAY,CAAC,IAAA,kBAAU,EAAC,eAAM,CAAC,MAAM,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACd,CAAC;IACF,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;AACzC,CAAC;AAdD,kCAcC;AAED,SAAgB,YAAY,CAAC,GAAW;IACvC,OAAO,qBAAS,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAC/C,CAAC;AAFD,oCAEC;AAEM,MAAM,cAAc,GAAG,CAAC,QAAgB,EAAU,EAAE;IAC1D,IAAI,WAAmB,CAAC;IACxB,IAAI,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrC,WAAW,GAAG,IAAA,sBAAW,EAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;SAAM,CAAC;QACP,6BAA6B;QAC7B,WAAW,GAAG,IAAA,wBAAa,EAAC,IAAA,8BAAmB,EAAC,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,WAAW,CAAC;AACpB,CAAC,CAAC;AATW,QAAA,cAAc,kBASzB;AAEF,SAAgB,sBAAsB;IACrC,OAAO,qBAAS,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;AAC3C,CAAC;AAFD,wDAEC;AAED,SAAgB,iBAAiB,CAAC,QAAkB;IACnD,MAAM,kBAAkB,GAAuB,EAAE,CAAC;IAClD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QACnC,kBAAkB,CAAC,CAAC,CAAC,GAAG,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,OAAO,kBAAkB,CAAC;AAC3B,CAAC;AAND,8CAMC;AAED,SAAgB,mBAAmB,CAAC,kBAAsC;IACzE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QAC7C,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAA,kBAAU,EAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC;AACjB,CAAC;AAND,kDAMC;AAED,SAAgB,cAAc,CAAC,IAAc;IAC5C,MAAM,cAAc,GAAG,IAAI,CAAC;IAC5B,MAAM,SAAS,GAAG,CAAC,CAAmB,EAAoB,EAAE;QAC3D,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC;IACF,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;SAC3D,GAAG,CAAC,SAAS,CAAC;SACd,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACxD,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,IAAA,kBAAU,EAAC,MAAM,CAAC,CAAC;SACvC,MAAM,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,UAAU,EAAE,CAAC,CAAC;IACzE,MAAM,IAAI,GAAG,IAAA,eAAM,EAAC,aAAa,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,eAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/D,OAAO,IAAI,GAAG,OAAO,CAAC;AACvB,CAAC;AAbD,wCAaC;AAED,SAAS,gBAAgB,CAAC,EAAc,EAAE,EAAc;IACvD,kCAAkC;IAClC,MAAM,WAAW,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;IAC1D,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpB,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,WAAW,CAAC;AACpB,CAAC"}

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,4 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
throw new Error('Incorrect usage. Import submodules instead');
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;AAAA,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC"}

View File

@@ -1,2 +0,0 @@
import { Proof } from '../common/index.js';
export declare const verifyP2PKSig: (proof: Proof) => boolean;

View File

@@ -1,37 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyP2PKSig = void 0;
const secp256k1_1 = require("@noble/curves/secp256k1");
const sha256_1 = require("@noble/hashes/sha256");
const NUT11_js_1 = require("../common/NUT11.js");
const verifyP2PKSig = (proof) => {
if (!proof.witness) {
throw new Error('could not verify signature, no witness provided');
}
const parsedSecret = (0, NUT11_js_1.parseSecret)(proof.secret);
// const tags = {} as Tags
// parsedSecret[1].tags.forEach((e: string[]) => {tags[e[0]]=e.shift()})
// if (tags.locktime) {
// const locktime = parseInt(tags.locktime[1])
// let isUnlocked = false
// if (Math.floor(Date.now() / 1000)>=locktime) {
// isUnlocked = true
// }
// }
// if (tags.sigflag as SigFlag) {
// if (tags.sigflag[0]==='SIG_INPUT') {
// }
// else if(tags.sigflag[0]==='SIG_ALL') {
// }
// else {
// throw new Error("Unknown sigflag");
// }
// }
// if (tags.n_sigs) {
// if (tags.pubkeys) {
// }
// }
return secp256k1_1.schnorr.verify(proof.witness.signatures[0], (0, sha256_1.sha256)(new TextDecoder().decode(proof.secret)), parsedSecret[1].data);
};
exports.verifyP2PKSig = verifyP2PKSig;
//# sourceMappingURL=NUT11.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.js","sourceRoot":"","sources":["../../../src/mint/NUT11.ts"],"names":[],"mappings":";;;AAAA,uDAAkD;AAClD,iDAA8C;AAC9C,iDAAiD;AAG1C,MAAM,aAAa,GAAG,CAAC,KAAY,EAAE,EAAE;IAC7C,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,YAAY,GAAG,IAAA,sBAAW,EAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAE/C,0BAA0B;IAC1B,wEAAwE;IACxE,uBAAuB;IACvB,kDAAkD;IAElD,6BAA6B;IAC7B,qDAAqD;IACrD,4BAA4B;IAC5B,QAAQ;IACR,IAAI;IACJ,iCAAiC;IACjC,2CAA2C;IAE3C,QAAQ;IACR,6CAA6C;IAE7C,QAAQ;IACR,aAAa;IACb,8CAA8C;IAC9C,QAAQ;IACR,IAAI;IACJ,qBAAqB;IACrB,0BAA0B;IAE1B,QAAQ;IACR,IAAI;IAEJ,OAAO,mBAAO,CAAC,MAAM,CACpB,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAC3B,IAAA,eAAM,EAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAC9C,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CACpB,CAAC;AACH,CAAC,CAAC;AAtCW,QAAA,aAAa,iBAsCxB"}

View File

@@ -1,14 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
import { BlindSignature, IntRange, Keyset, MintKeys, Proof } from '../common/index.js';
export type KeysetPair = {
keysetId: string;
pubKeys: MintKeys;
privKeys: MintKeys;
};
export type KeysetWithKeys = Keyset & {
pubKeys: MintKeys;
};
export declare function createBlindSignature(B_: ProjPointType<bigint>, privateKey: Uint8Array, amount: number, id: string): BlindSignature;
export declare function getPubKeyFromPrivKey(privKey: Uint8Array): Uint8Array;
export declare function createNewMintKeys(pow2height: IntRange<0, 65>, seed?: Uint8Array): KeysetPair;
export declare function verifyProof(proof: Proof, privKey: Uint8Array): boolean;

View File

@@ -1,53 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyProof = exports.createNewMintKeys = exports.getPubKeyFromPrivKey = exports.createBlindSignature = void 0;
const secp256k1_1 = require("@noble/curves/secp256k1");
const utils_js_1 = require("../util/utils.js");
const index_js_1 = require("../common/index.js");
const bip32_1 = require("@scure/bip32");
const DERIVATION_PATH = "m/0'/0'/0'";
function createBlindSignature(B_, privateKey, amount, id) {
const C_ = B_.multiply((0, utils_js_1.bytesToNumber)(privateKey));
return { C_, amount, id };
}
exports.createBlindSignature = createBlindSignature;
function getPubKeyFromPrivKey(privKey) {
return secp256k1_1.secp256k1.getPublicKey(privKey, true);
}
exports.getPubKeyFromPrivKey = getPubKeyFromPrivKey;
function createNewMintKeys(pow2height, seed) {
let counter = 0n;
const pubKeys = {};
const privKeys = {};
let masterKey;
if (seed) {
masterKey = bip32_1.HDKey.fromMasterSeed(seed);
}
while (counter < pow2height) {
const index = (2n ** counter).toString();
if (masterKey) {
const k = masterKey.derive(`${DERIVATION_PATH}/${counter}`).privateKey;
if (k) {
privKeys[index] = k;
}
else {
throw new Error(`Could not derive Private key from: ${DERIVATION_PATH}/${counter}`);
}
}
else {
privKeys[index] = (0, index_js_1.createRandomPrivateKey)();
}
pubKeys[index] = getPubKeyFromPrivKey(privKeys[index]);
counter++;
}
const keysetId = (0, index_js_1.deriveKeysetId)(pubKeys);
return { pubKeys, privKeys, keysetId };
}
exports.createNewMintKeys = createNewMintKeys;
function verifyProof(proof, privKey) {
const Y = (0, index_js_1.hashToCurve)(proof.secret);
const aY = Y.multiply((0, utils_js_1.bytesToNumber)(privKey));
return aY.equals(proof.C);
}
exports.verifyProof = verifyProof;
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mint/index.ts"],"names":[],"mappings":";;;AACA,uDAAoD;AACpD,+CAAiD;AAEjD,iDAAyF;AACzF,wCAAqC;AAErC,MAAM,eAAe,GAAG,YAAY,CAAC;AAYrC,SAAgB,oBAAoB,CACnC,EAAyB,EACzB,UAAsB,EACtB,MAAc,EACd,EAAU;IAEV,MAAM,EAAE,GAA0B,EAAE,CAAC,QAAQ,CAAC,IAAA,wBAAa,EAAC,UAAU,CAAC,CAAC,CAAC;IACzE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAC3B,CAAC;AARD,oDAQC;AAED,SAAgB,oBAAoB,CAAC,OAAmB;IACvD,OAAO,qBAAS,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC;AAFD,oDAEC;AAED,SAAgB,iBAAiB,CAAC,UAA2B,EAAE,IAAiB;IAC/E,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,SAAS,CAAC;IACd,IAAI,IAAI,EAAE,CAAC;QACV,SAAS,GAAG,aAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAW,CAAC,EAAE,IAAI,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;QACjD,IAAI,SAAS,EAAE,CAAC;YACf,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,eAAe,IAAI,OAAO,EAAE,CAAC,CAAC,UAAU,CAAC;YACvE,IAAI,CAAC,EAAE,CAAC;gBACP,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CAAC,sCAAsC,eAAe,IAAI,OAAO,EAAE,CAAC,CAAC;YACrF,CAAC;QACF,CAAC;aAAM,CAAC;YACP,QAAQ,CAAC,KAAK,CAAC,GAAG,IAAA,iCAAsB,GAAE,CAAC;QAC5C,CAAC;QAED,OAAO,CAAC,KAAK,CAAC,GAAG,oBAAoB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QACvD,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,QAAQ,GAAG,IAAA,yBAAc,EAAC,OAAO,CAAC,CAAC;IACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AACxC,CAAC;AA1BD,8CA0BC;AAED,SAAgB,WAAW,CAAC,KAAY,EAAE,OAAmB;IAC5D,MAAM,CAAC,GAA0B,IAAA,sBAAW,EAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3D,MAAM,EAAE,GAA0B,CAAC,CAAC,QAAQ,CAAC,IAAA,wBAAa,EAAC,OAAO,CAAC,CAAC,CAAC;IACrE,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC;AAJD,kCAIC"}

View File

@@ -1,3 +0,0 @@
export declare function bytesToNumber(bytes: Uint8Array): bigint;
export declare function hexToNumber(hex: string): bigint;
export declare function encodeBase64toUint8(base64String: string): Uint8Array;

View File

@@ -1,18 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodeBase64toUint8 = exports.hexToNumber = exports.bytesToNumber = void 0;
const utils_1 = require("@noble/curves/abstract/utils");
const buffer_1 = require("buffer/");
function bytesToNumber(bytes) {
return hexToNumber((0, utils_1.bytesToHex)(bytes));
}
exports.bytesToNumber = bytesToNumber;
function hexToNumber(hex) {
return BigInt(`0x${hex}`);
}
exports.hexToNumber = hexToNumber;
function encodeBase64toUint8(base64String) {
return buffer_1.Buffer.from(base64String, 'base64');
}
exports.encodeBase64toUint8 = encodeBase64toUint8;
//# sourceMappingURL=utils.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/util/utils.ts"],"names":[],"mappings":";;;AAAA,wDAA0D;AAC1D,oCAAiC;AAEjC,SAAgB,aAAa,CAAC,KAAiB;IAC9C,OAAO,WAAW,CAAC,IAAA,kBAAU,EAAC,KAAK,CAAC,CAAC,CAAC;AACvC,CAAC;AAFD,sCAEC;AAED,SAAgB,WAAW,CAAC,GAAW;IACtC,OAAO,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;AAC3B,CAAC;AAFD,kCAEC;AAED,SAAgB,mBAAmB,CAAC,YAAoB;IACvD,OAAO,eAAM,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;AAC5C,CAAC;AAFD,kDAEC"}

View File

@@ -1 +0,0 @@
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}

View File

@@ -1,3 +0,0 @@
"use strict";
throw new Error('Incorrect usage. Import submodules instead');
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC"}

View File

@@ -1,3 +0,0 @@
import { Proof } from '../common/index.js';
export declare const verifyP2PKSig: (proof: Proof) => boolean;
//# sourceMappingURL=NUT11.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.d.ts","sourceRoot":"","sources":["../../src/mint/NUT11.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,eAAO,MAAM,aAAa,UAAW,KAAK,YAsCzC,CAAC"}

View File

@@ -1,37 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyP2PKSig = void 0;
const secp256k1_1 = require("@noble/curves/secp256k1");
const sha256_1 = require("@noble/hashes/sha256");
const NUT11_js_1 = require("../common/NUT11.js");
const verifyP2PKSig = (proof) => {
if (!proof.witness) {
throw new Error('could not verify signature, no witness provided');
}
const parsedSecret = (0, NUT11_js_1.parseSecret)(proof.secret);
// const tags = {} as Tags
// parsedSecret[1].tags.forEach((e: string[]) => {tags[e[0]]=e.shift()})
// if (tags.locktime) {
// const locktime = parseInt(tags.locktime[1])
// let isUnlocked = false
// if (Math.floor(Date.now() / 1000)>=locktime) {
// isUnlocked = true
// }
// }
// if (tags.sigflag as SigFlag) {
// if (tags.sigflag[0]==='SIG_INPUT') {
// }
// else if(tags.sigflag[0]==='SIG_ALL') {
// }
// else {
// throw new Error("Unknown sigflag");
// }
// }
// if (tags.n_sigs) {
// if (tags.pubkeys) {
// }
// }
return secp256k1_1.schnorr.verify(proof.witness.signatures[0], (0, sha256_1.sha256)(new TextDecoder().decode(proof.secret)), parsedSecret[1].data);
};
exports.verifyP2PKSig = verifyP2PKSig;
//# sourceMappingURL=NUT11.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"NUT11.js","sourceRoot":"","sources":["../../src/mint/NUT11.ts"],"names":[],"mappings":";;;AAAA,uDAAkD;AAClD,iDAA8C;AAC9C,iDAAiD;AAG1C,MAAM,aAAa,GAAG,CAAC,KAAY,EAAE,EAAE;IAC7C,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,YAAY,GAAG,IAAA,sBAAW,EAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAE/C,0BAA0B;IAC1B,wEAAwE;IACxE,uBAAuB;IACvB,kDAAkD;IAElD,6BAA6B;IAC7B,qDAAqD;IACrD,4BAA4B;IAC5B,QAAQ;IACR,IAAI;IACJ,iCAAiC;IACjC,2CAA2C;IAE3C,QAAQ;IACR,6CAA6C;IAE7C,QAAQ;IACR,aAAa;IACb,8CAA8C;IAC9C,QAAQ;IACR,IAAI;IACJ,qBAAqB;IACrB,0BAA0B;IAE1B,QAAQ;IACR,IAAI;IAEJ,OAAO,mBAAO,CAAC,MAAM,CACpB,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAC3B,IAAA,eAAM,EAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAC9C,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CACpB,CAAC;AACH,CAAC,CAAC;AAtCW,QAAA,aAAa,iBAsCxB"}

View File

@@ -1,15 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
import { BlindSignature, IntRange, Keyset, MintKeys, Proof } from '../common/index.js';
export type KeysetPair = {
keysetId: string;
pubKeys: MintKeys;
privKeys: MintKeys;
};
export type KeysetWithKeys = Keyset & {
pubKeys: MintKeys;
};
export declare function createBlindSignature(B_: ProjPointType<bigint>, privateKey: Uint8Array, amount: number, id: string): BlindSignature;
export declare function getPubKeyFromPrivKey(privKey: Uint8Array): Uint8Array;
export declare function createNewMintKeys(pow2height: IntRange<0, 65>, seed?: Uint8Array): KeysetPair;
export declare function verifyProof(proof: Proof, privKey: Uint8Array): boolean;
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mint/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAGnE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAMvF,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,QAAQ,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG;IACrC,OAAO,EAAE,QAAQ,CAAC;CAClB,CAAC;AAEF,wBAAgB,oBAAoB,CACnC,EAAE,EAAE,aAAa,CAAC,MAAM,CAAC,EACzB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,GACR,cAAc,CAGhB;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,UAAU,cAEvD;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,CA0B5F;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAItE"}

View File

@@ -1,53 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyProof = exports.createNewMintKeys = exports.getPubKeyFromPrivKey = exports.createBlindSignature = void 0;
const secp256k1_1 = require("@noble/curves/secp256k1");
const utils_js_1 = require("../util/utils.js");
const index_js_1 = require("../common/index.js");
const bip32_1 = require("@scure/bip32");
const DERIVATION_PATH = "m/0'/0'/0'";
function createBlindSignature(B_, privateKey, amount, id) {
const C_ = B_.multiply((0, utils_js_1.bytesToNumber)(privateKey));
return { C_, amount, id };
}
exports.createBlindSignature = createBlindSignature;
function getPubKeyFromPrivKey(privKey) {
return secp256k1_1.secp256k1.getPublicKey(privKey, true);
}
exports.getPubKeyFromPrivKey = getPubKeyFromPrivKey;
function createNewMintKeys(pow2height, seed) {
let counter = 0n;
const pubKeys = {};
const privKeys = {};
let masterKey;
if (seed) {
masterKey = bip32_1.HDKey.fromMasterSeed(seed);
}
while (counter < pow2height) {
const index = (2n ** counter).toString();
if (masterKey) {
const k = masterKey.derive(`${DERIVATION_PATH}/${counter}`).privateKey;
if (k) {
privKeys[index] = k;
}
else {
throw new Error(`Could not derive Private key from: ${DERIVATION_PATH}/${counter}`);
}
}
else {
privKeys[index] = (0, index_js_1.createRandomPrivateKey)();
}
pubKeys[index] = getPubKeyFromPrivKey(privKeys[index]);
counter++;
}
const keysetId = (0, index_js_1.deriveKeysetId)(pubKeys);
return { pubKeys, privKeys, keysetId };
}
exports.createNewMintKeys = createNewMintKeys;
function verifyProof(proof, privKey) {
const Y = (0, index_js_1.hashToCurve)(proof.secret);
const aY = Y.multiply((0, utils_js_1.bytesToNumber)(privKey));
return aY.equals(proof.C);
}
exports.verifyProof = verifyProof;
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mint/index.ts"],"names":[],"mappings":";;;AACA,uDAAoD;AACpD,+CAAiD;AAEjD,iDAAyF;AACzF,wCAAqC;AAErC,MAAM,eAAe,GAAG,YAAY,CAAC;AAYrC,SAAgB,oBAAoB,CACnC,EAAyB,EACzB,UAAsB,EACtB,MAAc,EACd,EAAU;IAEV,MAAM,EAAE,GAA0B,EAAE,CAAC,QAAQ,CAAC,IAAA,wBAAa,EAAC,UAAU,CAAC,CAAC,CAAC;IACzE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAC3B,CAAC;AARD,oDAQC;AAED,SAAgB,oBAAoB,CAAC,OAAmB;IACvD,OAAO,qBAAS,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC;AAFD,oDAEC;AAED,SAAgB,iBAAiB,CAAC,UAA2B,EAAE,IAAiB;IAC/E,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,SAAS,CAAC;IACd,IAAI,IAAI,EAAE,CAAC;QACV,SAAS,GAAG,aAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAW,CAAC,EAAE,IAAI,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;QACjD,IAAI,SAAS,EAAE,CAAC;YACf,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,eAAe,IAAI,OAAO,EAAE,CAAC,CAAC,UAAU,CAAC;YACvE,IAAI,CAAC,EAAE,CAAC;gBACP,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CAAC,sCAAsC,eAAe,IAAI,OAAO,EAAE,CAAC,CAAC;YACrF,CAAC;QACF,CAAC;aAAM,CAAC;YACP,QAAQ,CAAC,KAAK,CAAC,GAAG,IAAA,iCAAsB,GAAE,CAAC;QAC5C,CAAC;QAED,OAAO,CAAC,KAAK,CAAC,GAAG,oBAAoB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QACvD,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,QAAQ,GAAG,IAAA,yBAAc,EAAC,OAAO,CAAC,CAAC;IACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AACxC,CAAC;AA1BD,8CA0BC;AAED,SAAgB,WAAW,CAAC,KAAY,EAAE,OAAmB;IAC5D,MAAM,CAAC,GAA0B,IAAA,sBAAW,EAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3D,MAAM,EAAE,GAA0B,CAAC,CAAC,QAAQ,CAAC,IAAA,wBAAa,EAAC,OAAO,CAAC,CAAC,CAAC;IACrE,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC;AAJD,kCAIC"}

View File

@@ -1,4 +0,0 @@
export declare function bytesToNumber(bytes: Uint8Array): bigint;
export declare function hexToNumber(hex: string): bigint;
export declare function encodeBase64toUint8(base64String: string): Uint8Array;
//# sourceMappingURL=utils.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/util/utils.ts"],"names":[],"mappings":"AAGA,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEvD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,UAAU,CAEpE"}

View File

@@ -1,18 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodeBase64toUint8 = exports.hexToNumber = exports.bytesToNumber = void 0;
const utils_1 = require("@noble/curves/abstract/utils");
const buffer_1 = require("buffer/");
function bytesToNumber(bytes) {
return hexToNumber((0, utils_1.bytesToHex)(bytes));
}
exports.bytesToNumber = bytesToNumber;
function hexToNumber(hex) {
return BigInt(`0x${hex}`);
}
exports.hexToNumber = hexToNumber;
function encodeBase64toUint8(base64String) {
return buffer_1.Buffer.from(base64String, 'base64');
}
exports.encodeBase64toUint8 = encodeBase64toUint8;
//# sourceMappingURL=utils.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/util/utils.ts"],"names":[],"mappings":";;;AAAA,wDAA0D;AAC1D,oCAAiC;AAEjC,SAAgB,aAAa,CAAC,KAAiB;IAC9C,OAAO,WAAW,CAAC,IAAA,kBAAU,EAAC,KAAK,CAAC,CAAC,CAAC;AACvC,CAAC;AAFD,sCAEC;AAED,SAAgB,WAAW,CAAC,GAAW;IACtC,OAAO,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;AAC3B,CAAC;AAFD,kCAEC;AAED,SAAgB,mBAAmB,CAAC,YAAoB;IACvD,OAAO,eAAM,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;AAC5C,CAAC;AAFD,kDAEC"}

View File

@@ -1,100 +0,0 @@
{
"name": "@cashu/crypto",
"version": "0.2.7",
"description": "Basic cashu crypto functions",
"main": "./modules/index.js",
"files": [
"src",
"modules"
],
"scripts": {
"compile": "rm -rf modules && tsc && tsc -p tsconfig.esm.json",
"test": "jest --coverage --maxWorkers=1",
"dev": "tsc --watch",
"lint": "eslint --ext .js,.ts .",
"format": "prettier --write .",
"typedoc": "typedoc --entryPointStrategy expand ./src"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cashubtc/cashu-crypto-ts.git"
},
"keywords": [
"blindsignature",
"ecash",
"chaumium",
"mint",
"cashu"
],
"author": "gandlaf21",
"license": "MIT",
"dependencies": {
"@noble/curves": "^1.3.0",
"@noble/hashes": "^1.3.3",
"@scure/bip32": "^1.3.3",
"@scure/bip39": "^1.2.2",
"buffer": "^6.0.3"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.1.1",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"ts-jest": "^29.1.2",
"ts-jest-resolver": "^2.0.1",
"ts-node": "^10.9.2",
"typedoc": "^0.25.8",
"typescript": "^5.3.3"
},
"exports": {
"./modules": {
"types": "./modules/index.d.ts",
"import": "./modules/esm/index.js",
"default": "./modules/index.js"
},
"./modules/util": {
"types": "./modules/util/index.d.ts",
"import": "./modules/esm/util/index.js",
"default": "./modules/util/index.js"
},
"./modules/common": {
"types": "./modules/common/index.d.ts",
"import": "./modules/esm/common/index.js",
"default": "./modules/common/index.js"
},
"./modules/mint": {
"types": "./modules/mint/index.d.ts",
"import": "./modules/esm/mint/index.js",
"default": "./modules/mint/index.js"
},
"./modules/client": {
"types": "./modules/client/index.d.ts",
"import": "./modules/esm/client/index.js",
"default": "./modules/client/index.js"
},
"./modules/client/NUT09": {
"types": "./modules/client/NUT09.d.ts",
"import": "./modules/esm/client/NUT09.js",
"default": "./modules/client/NUT09.js"
},
"./modules/common/NUT11": {
"types": "./modules/common/NUT11.d.ts",
"import": "./modules/esm/common/NUT11.js",
"default": "./modules/common/NUT11.js"
},
"./modules/client/NUT11": {
"types": "./modules/client/NUT11.d.ts",
"import": "./modules/esm/client/NUT11.js",
"default": "./modules/client/NUT11.js"
},
"./modules/mint/NUT11": {
"types": "./modules/mint/NUT11.d.ts",
"import": "./modules/esm/mint/NUT11.js",
"default": "./modules/mint/NUT11.js"
}
}
}

View File

@@ -1,49 +0,0 @@
import { HDKey } from '@scure/bip32';
import { getKeysetIdInt } from '../common/index.js';
import { generateMnemonic, mnemonicToSeedSync } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
const STANDARD_DERIVATION_PATH = `m/129372'/0'`;
enum DerivationType {
SECRET = 0,
BLINDING_FACTOR = 1
}
export const deriveSecret = (seed: Uint8Array, keysetId: string, counter: number): Uint8Array => {
return derive(seed, keysetId, counter, DerivationType.SECRET);
};
export const deriveBlindingFactor = (
seed: Uint8Array,
keysetId: string,
counter: number
): Uint8Array => {
return derive(seed, keysetId, counter, DerivationType.BLINDING_FACTOR);
};
const derive = (
seed: Uint8Array,
keysetId: string,
counter: number,
secretOrBlinding: DerivationType
): Uint8Array => {
const hdkey = HDKey.fromMasterSeed(seed);
const keysetIdInt = getKeysetIdInt(keysetId);
const derivationPath = `${STANDARD_DERIVATION_PATH}/${keysetIdInt}'/${counter}'/${secretOrBlinding}`;
const derived = hdkey.derive(derivationPath);
if (derived.privateKey === null) {
throw new Error('Could not derive private key');
}
return derived.privateKey;
};
export const generateNewMnemonic = (): string => {
const mnemonic = generateMnemonic(wordlist, 128);
return mnemonic;
};
export const deriveSeedFromMnemonic = (mnemonic: string): Uint8Array => {
const seed = mnemonicToSeedSync(mnemonic);
return seed;
};

View File

@@ -1,47 +0,0 @@
import { PrivKey, bytesToHex, hexToBytes } from '@noble/curves/abstract/utils';
import { sha256 } from '@noble/hashes/sha256';
import { schnorr } from '@noble/curves/secp256k1';
import { randomBytes } from '@noble/hashes/utils';
import { parseSecret } from '../common/NUT11.js';
import { Proof, Secret } from '../common/index.js';
export const createP2PKsecret = (pubkey: string): Uint8Array => {
const newSecret: Secret = [
'P2PK',
{
nonce: bytesToHex(randomBytes(32)),
data: pubkey
}
];
const parsed = JSON.stringify(newSecret);
return new TextEncoder().encode(parsed);
};
export const signP2PKsecret = (secret: Uint8Array, privateKey: PrivKey) => {
const msghash = sha256(new TextDecoder().decode(secret));
const sig = schnorr.sign(msghash, privateKey);
return sig;
};
export const getSignedProofs = (proofs: Array<Proof>, privateKey: string): Array<Proof> => {
return proofs.map((p) => {
try {
const parsed: Secret = parseSecret(p.secret);
if (parsed[0] !== 'P2PK') {
throw new Error('unknown secret type');
}
return getSignedProof(p, hexToBytes(privateKey));
} catch (error) {
return p;
}
});
};
export const getSignedProof = (proof: Proof, privateKey: PrivKey): Proof => {
if (!proof.witness) {
proof.witness = {
signatures: [bytesToHex(signP2PKsecret(proof.secret, privateKey))]
};
}
return proof;
};

View File

@@ -1,87 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
import { secp256k1 } from '@noble/curves/secp256k1';
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils';
import { bytesToNumber } from '../util/utils.js';
import type {
BlindSignature,
Proof,
SerializedBlindedMessage,
SerializedProof
} from '../common/index.js';
import { hashToCurve, pointFromHex } from '../common/index.js';
import { Witness } from '../common/index';
export type BlindedMessage = {
B_: ProjPointType<bigint>;
r: bigint;
secret: Uint8Array;
};
export function createRandomBlindedMessage(): BlindedMessage {
return blindMessage(randomBytes(32));
}
export function blindMessage(secret: Uint8Array, r?: bigint): BlindedMessage {
const Y = hashToCurve(secret);
if (!r) {
r = bytesToNumber(secp256k1.utils.randomPrivateKey());
}
const rG = secp256k1.ProjectivePoint.BASE.multiply(r);
const B_ = Y.add(rG);
return { B_, r, secret };
}
export function unblindSignature(
C_: ProjPointType<bigint>,
r: bigint,
A: ProjPointType<bigint>
): ProjPointType<bigint> {
const C = C_.subtract(A.multiply(r));
return C;
}
export function constructProofFromPromise(
promise: BlindSignature,
r: bigint,
secret: Uint8Array,
key: ProjPointType<bigint>
): Proof {
const A = key;
const C = unblindSignature(promise.C_, r, A);
const proof = {
id: promise.id,
amount: promise.amount,
secret,
C
};
return proof;
}
export const serializeProof = (proof: Proof): SerializedProof => {
return {
amount: proof.amount,
C: proof.C.toHex(true),
id: proof.id,
secret: new TextDecoder().decode(proof.secret),
witness: JSON.stringify(proof.witness)
};
};
export const deserializeProof = (proof: SerializedProof): Proof => {
return {
amount: proof.amount,
C: pointFromHex(proof.C),
id: proof.id,
secret: new TextEncoder().encode(proof.secret),
witness: proof.witness?JSON.parse(proof.witness):undefined
};
};
export const serializeBlindedMessage = (
bm: BlindedMessage,
amount: number
): SerializedBlindedMessage => {
return {
B_: bm.B_.toHex(true),
amount: amount
};
};

View File

@@ -1,12 +0,0 @@
import { Secret } from "./index.js";
export const parseSecret = (secret: string | Uint8Array): Secret => {
try {
if (secret instanceof Uint8Array) {
secret = new TextDecoder().decode(secret);
}
return JSON.parse(secret);
} catch (e) {
throw new Error("can't parse secret");
}
};

View File

@@ -1,154 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils';
import { bytesToNumber, encodeBase64toUint8, hexToNumber } from '../util/utils.js';
import { Buffer } from 'buffer/';
export type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc['length']]>;
export type IntRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;
export type MintKeys = { [k: string]: Uint8Array };
export type SerializedMintKeys = {
[k: string]: string;
};
export type Keyset = {
id: string;
unit: string;
active: boolean;
};
export type BlindSignature = {
C_: ProjPointType<bigint>;
amount: number;
id: string;
};
export type SerializedBlindSignature = {
C_: string;
amount: number;
id: string;
};
export type Proof = {
C: ProjPointType<bigint>;
secret: Uint8Array;
amount: number;
id: string;
witness?: Witness;
};
export type SerializedProof = {
C: string;
secret: string;
amount: number;
id: string;
witness?: string;
};
export type SerializedBlindedMessage = {
B_: string;
amount: number;
witness?: string;
};
export type Secret = [WellKnownSecret, SecretData];
export type WellKnownSecret = 'P2PK';
export type SecretData = {
nonce: string;
data: string;
tags?: Array<Array<string>>;
};
export type Witness = {
signatures: Array<string>;
};
export type Tags = {
[k: string]: string;
};
export type SigFlag = 'SIG_INPUTS' | 'SIG_ALL';
const DOMAIN_SEPARATOR = hexToBytes('536563703235366b315f48617368546f43757276655f43617368755f');
export function hashToCurve(secret: Uint8Array): ProjPointType<bigint> {
const msgToHash = sha256(Buffer.concat([DOMAIN_SEPARATOR, secret]));
const counter = new Uint32Array(1);
const maxIterations = 2 ** 16;
for (let i = 0; i < maxIterations; i++) {
const counterBytes = new Uint8Array(counter.buffer);
const hash = sha256(Buffer.concat([msgToHash, counterBytes]));
try {
return pointFromHex(bytesToHex(Buffer.concat([new Uint8Array([0x02]), hash])));
} catch (error) {
counter[0]++;
}
}
throw new Error('No valid point found');
}
export function pointFromHex(hex: string) {
return secp256k1.ProjectivePoint.fromHex(hex);
}
export const getKeysetIdInt = (keysetId: string): bigint => {
let keysetIdInt: bigint;
if (/^[a-fA-F0-9]+$/.test(keysetId)) {
keysetIdInt = hexToNumber(keysetId) % BigInt(2 ** 31 - 1);
} else {
//legacy keyset compatibility
keysetIdInt = bytesToNumber(encodeBase64toUint8(keysetId)) % BigInt(2 ** 31 - 1);
}
return keysetIdInt;
};
export function createRandomPrivateKey() {
return secp256k1.utils.randomPrivateKey();
}
export function serializeMintKeys(mintKeys: MintKeys): SerializedMintKeys {
const serializedMintKeys: SerializedMintKeys = {};
Object.keys(mintKeys).forEach((p) => {
serializedMintKeys[p] = bytesToHex(mintKeys[p]);
});
return serializedMintKeys;
}
export function deserializeMintKeys(serializedMintKeys: SerializedMintKeys): MintKeys {
const mintKeys: MintKeys = {};
Object.keys(serializedMintKeys).forEach((p) => {
mintKeys[p] = hexToBytes(serializedMintKeys[p]);
});
return mintKeys;
}
export function deriveKeysetId(keys: MintKeys): string {
const KEYSET_VERSION = '00';
const mapBigInt = (k: [string, string]): [bigint, string] => {
return [BigInt(k[0]), k[1]];
};
const pubkeysConcat = Object.entries(serializeMintKeys(keys))
.map(mapBigInt)
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
.map(([, pubKey]) => hexToBytes(pubKey))
.reduce((prev, curr) => mergeUInt8Arrays(prev, curr), new Uint8Array());
const hash = sha256(pubkeysConcat);
const hashHex = Buffer.from(hash).toString('hex').slice(0, 14);
return '00' + hashHex;
}
function mergeUInt8Arrays(a1: Uint8Array, a2: Uint8Array): Uint8Array {
// sum of individual array lengths
const mergedArray = new Uint8Array(a1.length + a2.length);
mergedArray.set(a1);
mergedArray.set(a2, a1.length);
return mergedArray;
}

View File

@@ -1 +0,0 @@
throw new Error('Incorrect usage. Import submodules instead');

View File

@@ -1,44 +0,0 @@
import { schnorr } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import { parseSecret } from '../common/NUT11.js';
import { Proof } from '../common/index.js';
export const verifyP2PKSig = (proof: Proof) => {
if (!proof.witness) {
throw new Error('could not verify signature, no witness provided');
}
const parsedSecret = parseSecret(proof.secret);
// const tags = {} as Tags
// parsedSecret[1].tags.forEach((e: string[]) => {tags[e[0]]=e.shift()})
// if (tags.locktime) {
// const locktime = parseInt(tags.locktime[1])
// let isUnlocked = false
// if (Math.floor(Date.now() / 1000)>=locktime) {
// isUnlocked = true
// }
// }
// if (tags.sigflag as SigFlag) {
// if (tags.sigflag[0]==='SIG_INPUT') {
// }
// else if(tags.sigflag[0]==='SIG_ALL') {
// }
// else {
// throw new Error("Unknown sigflag");
// }
// }
// if (tags.n_sigs) {
// if (tags.pubkeys) {
// }
// }
return schnorr.verify(
proof.witness.signatures[0],
sha256(new TextDecoder().decode(proof.secret)),
parsedSecret[1].data
);
};

View File

@@ -1,66 +0,0 @@
import { ProjPointType } from '@noble/curves/abstract/weierstrass';
import { secp256k1 } from '@noble/curves/secp256k1';
import { bytesToNumber } from '../util/utils.js';
import { BlindSignature, IntRange, Keyset, MintKeys, Proof } from '../common/index.js';
import { createRandomPrivateKey, deriveKeysetId, hashToCurve } from '../common/index.js';
import { HDKey } from '@scure/bip32';
const DERIVATION_PATH = "m/0'/0'/0'";
export type KeysetPair = {
keysetId: string;
pubKeys: MintKeys;
privKeys: MintKeys;
};
export type KeysetWithKeys = Keyset & {
pubKeys: MintKeys;
};
export function createBlindSignature(
B_: ProjPointType<bigint>,
privateKey: Uint8Array,
amount: number,
id: string
): BlindSignature {
const C_: ProjPointType<bigint> = B_.multiply(bytesToNumber(privateKey));
return { C_, amount, id };
}
export function getPubKeyFromPrivKey(privKey: Uint8Array) {
return secp256k1.getPublicKey(privKey, true);
}
export function createNewMintKeys(pow2height: IntRange<0, 65>, seed?: Uint8Array): KeysetPair {
let counter = 0n;
const pubKeys: MintKeys = {};
const privKeys: MintKeys = {};
let masterKey;
if (seed) {
masterKey = HDKey.fromMasterSeed(seed);
}
while (counter < pow2height) {
const index: string = (2n ** counter).toString();
if (masterKey) {
const k = masterKey.derive(`${DERIVATION_PATH}/${counter}`).privateKey;
if (k) {
privKeys[index] = k;
} else {
throw new Error(`Could not derive Private key from: ${DERIVATION_PATH}/${counter}`);
}
} else {
privKeys[index] = createRandomPrivateKey();
}
pubKeys[index] = getPubKeyFromPrivKey(privKeys[index]);
counter++;
}
const keysetId = deriveKeysetId(pubKeys);
return { pubKeys, privKeys, keysetId };
}
export function verifyProof(proof: Proof, privKey: Uint8Array): boolean {
const Y: ProjPointType<bigint> = hashToCurve(proof.secret);
const aY: ProjPointType<bigint> = Y.multiply(bytesToNumber(privKey));
return aY.equals(proof.C);
}

View File

@@ -1,14 +0,0 @@
import { bytesToHex } from '@noble/curves/abstract/utils';
import { Buffer } from 'buffer/';
export function bytesToNumber(bytes: Uint8Array): bigint {
return hexToNumber(bytesToHex(bytes));
}
export function hexToNumber(hex: string): bigint {
return BigInt(`0x${hex}`);
}
export function encodeBase64toUint8(base64String: string): Uint8Array {
return Buffer.from(base64String, 'base64');
}

View File

@@ -1,6 +1,6 @@
{
"name": "applesauce-accounts",
"version": "3.1.0",
"version": "4.0.0",
"description": "A simple nostr account management system",
"type": "module",
"main": "dist/index.js",
@@ -33,21 +33,23 @@
},
"dependencies": {
"@noble/hashes": "^1.7.1",
"applesauce-signers": "^3.1.0",
"applesauce-core": "^3.1.0",
"applesauce-core": "^4.0.0",
"applesauce-signers": "^4.0.0",
"nanoid": "^5.1.5",
"nostr-tools": "~2.15",
"nostr-tools": "~2.17",
"rxjs": "^7.8.1"
},
"devDependencies": {
"rimraf": "^6.0.1",
"typescript": "^5.8.3",
"vitest": "^3.2.3"
"vitest": "^3.2.4"
},
"funding": {
"type": "lightning",
"url": "lightning:nostrudel@geyser.fund"
},
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc",
"watch:build": "tsc --watch > /dev/null",
"test": "vitest run --passWithNoTests",

View File

@@ -1,3 +1,4 @@
export * from "./app-data.js";
export * from "./blocked-relays.js";
export * from "./blossom.js";
export * from "./bookmarks.js";

View File

@@ -1,3 +1,4 @@
export * from "./app-data.js";
export * from "./blocked-relays.js";
export * from "./blossom.js";
export * from "./bookmarks.js";

View File

@@ -1,6 +1,6 @@
{
"name": "applesauce-actions",
"version": "3.1.0",
"version": "4.0.0",
"description": "A package for performing common nostr actions",
"type": "module",
"main": "dist/index.js",
@@ -32,24 +32,26 @@
}
},
"dependencies": {
"applesauce-core": "^3.1.0",
"applesauce-factory": "^3.1.0",
"nostr-tools": "~2.15",
"applesauce-core": "^4.0.0",
"applesauce-factory": "^4.0.0",
"nostr-tools": "~2.17",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@hirez_io/observer-spy": "^2.2.0",
"@types/debug": "^4.1.12",
"applesauce-signers": "^3.1.0",
"applesauce-signers": "^4.0.0",
"nanoid": "^5.1.5",
"rimraf": "^6.0.1",
"typescript": "^5.8.3",
"vitest": "^3.2.3"
"vitest": "^3.2.4"
},
"funding": {
"type": "lightning",
"url": "lightning:nostrudel@geyser.fund"
},
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc",
"watch:build": "tsc --watch > /dev/null",
"test": "vitest run --passWithNoTests",

View File

@@ -1,84 +0,0 @@
import { Filter, NostrEvent } from "nostr-tools";
import { Subject } from "rxjs";
import { LRU } from "../helpers/lru.js";
import { IEventSet } from "./interface.js";
/**
* A set of nostr events that can be queried and subscribed to
* NOTE: does not handle replaceable events or any deletion logic
*/
export declare class EventSet implements IEventSet {
protected log: import("debug").Debugger;
/** Indexes */
protected kinds: Map<number, Set<import("nostr-tools").Event>>;
protected authors: Map<string, Set<import("nostr-tools").Event>>;
protected tags: LRU<Set<import("nostr-tools").Event>>;
protected created_at: NostrEvent[];
/** LRU cache of last events touched */
events: LRU<import("nostr-tools").Event>;
/** A sorted array of replaceable events by address */
protected replaceable: Map<string, import("nostr-tools").Event[]>;
/** A stream of events inserted into the database */
insert$: Subject<import("nostr-tools").Event>;
/** A stream of events that have been updated */
update$: Subject<import("nostr-tools").Event>;
/** A stream of events removed from the database */
remove$: Subject<import("nostr-tools").Event>;
/** A method thats called before a new event is inserted */
onBeforeInsert?: (event: NostrEvent) => boolean;
/** The number of events in the event set */
get size(): number;
/** Moves an event to the top of the LRU cache */
touch(event: NostrEvent): void;
/** Checks if the database contains an event without touching it */
hasEvent(id: string): boolean;
/** Gets a single event based on id */
getEvent(id: string): NostrEvent | undefined;
/** Checks if the event set has a replaceable event */
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
/** Gets the latest replaceable event */
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
/** Gets the history of a replaceable event */
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
/** Gets all events that match the filters */
getByFilters(filters: Filter | Filter[]): Set<NostrEvent>;
/** Gets a timeline of events that match the filters */
getTimeline(filters: Filter | Filter[]): NostrEvent[];
/** Inserts an event into the database and notifies all subscriptions */
add(event: NostrEvent): NostrEvent | null;
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
update(event: NostrEvent): boolean;
/** Removes an event from the database and notifies all subscriptions */
remove(eventOrId: string | NostrEvent): boolean;
/** A weak map of events that are claimed by other things */
protected claims: WeakMap<import("nostr-tools").Event, any>;
/** Sets the claim on the event and touches it */
claim(event: NostrEvent, claim: any): void;
/** Checks if an event is claimed by anything */
isClaimed(event: NostrEvent): boolean;
/** Removes a claim from an event */
removeClaim(event: NostrEvent, claim: any): void;
/** Removes all claims on an event */
clearClaim(event: NostrEvent): void;
/** Index helper methods */
protected getKindIndex(kind: number): Set<import("nostr-tools").Event>;
protected getAuthorsIndex(author: string): Set<import("nostr-tools").Event>;
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
/** Iterates over all events by author */
iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
/** Iterates over all events by indexable tag and value */
iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>;
/** Iterates over all events by kind */
iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>;
/** Iterates over all events by time */
iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>;
/** Iterates over all events by id */
iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
/** Returns all events that match the filter */
getEventsForFilter(filter: Filter): Set<NostrEvent>;
/** Returns all events that match the filters */
getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
/** Remove the oldest events that are not claimed */
prune(limit?: number): number;
/** Resets the event set */
reset(): void;
}

View File

@@ -1,359 +0,0 @@
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
import { Subject } from "rxjs";
import { getIndexableTags, INDEXABLE_TAGS } from "../helpers/event-tags.js";
import { createReplaceableAddress, isReplaceable } from "../helpers/event.js";
import { LRU } from "../helpers/lru.js";
import { logger } from "../logger.js";
/**
* A set of nostr events that can be queried and subscribed to
* NOTE: does not handle replaceable events or any deletion logic
*/
export class EventSet {
log = logger.extend("EventSet");
/** Indexes */
kinds = new Map();
authors = new Map();
tags = new LRU();
created_at = [];
/** LRU cache of last events touched */
events = new LRU();
/** A sorted array of replaceable events by address */
replaceable = new Map();
/** A stream of events inserted into the database */
insert$ = new Subject();
/** A stream of events that have been updated */
update$ = new Subject();
/** A stream of events removed from the database */
remove$ = new Subject();
/** A method thats called before a new event is inserted */
onBeforeInsert;
/** The number of events in the event set */
get size() {
return this.events.size;
}
/** Moves an event to the top of the LRU cache */
touch(event) {
this.events.set(event.id, event);
}
/** Checks if the database contains an event without touching it */
hasEvent(id) {
return this.events.has(id);
}
/** Gets a single event based on id */
getEvent(id) {
return this.events.get(id);
}
/** Checks if the event set has a replaceable event */
hasReplaceable(kind, pubkey, identifier) {
const events = this.replaceable.get(createReplaceableAddress(kind, pubkey, identifier));
return !!events && events.length > 0;
}
/** Gets the latest replaceable event */
getReplaceable(kind, pubkey, identifier) {
const address = createReplaceableAddress(kind, pubkey, identifier);
const events = this.replaceable.get(address);
return events?.[0];
}
/** Gets the history of a replaceable event */
getReplaceableHistory(kind, pubkey, identifier) {
const address = createReplaceableAddress(kind, pubkey, identifier);
return this.replaceable.get(address);
}
/** Gets all events that match the filters */
getByFilters(filters) {
return this.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
}
/** Gets a timeline of events that match the filters */
getTimeline(filters) {
const timeline = [];
const events = this.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
for (const event of events)
insertEventIntoDescendingList(timeline, event);
return timeline;
}
/** Inserts an event into the database and notifies all subscriptions */
add(event) {
const id = event.id;
const current = this.events.get(id);
if (current)
return current;
// Ignore events if before insert returns false
if (this.onBeforeInsert?.(event) === false)
return null;
this.events.set(id, event);
this.getKindIndex(event.kind).add(event);
this.getAuthorsIndex(event.pubkey).add(event);
// Add the event to the tag indexes if they exist
for (const tag of getIndexableTags(event)) {
if (this.tags.has(tag))
this.getTagIndex(tag).add(event);
}
// Insert into time index
insertEventIntoDescendingList(this.created_at, event);
// Insert into replaceable index
if (isReplaceable(event.kind)) {
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
let array = this.replaceable.get(address);
if (!this.replaceable.has(address)) {
// add an empty array if there is no array
array = [];
this.replaceable.set(address, array);
}
// insert the event into the sorted array
insertEventIntoDescendingList(array, event);
}
// Notify subscribers that the event was inserted
this.insert$.next(event);
return event;
}
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
update(event) {
const inserted = this.add(event);
if (inserted)
this.update$.next(inserted);
return inserted !== null;
}
/** Removes an event from the database and notifies all subscriptions */
remove(eventOrId) {
let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
if (!event)
throw new Error("Missing event");
const id = event.id;
// only remove events that are known
if (!this.events.has(id))
return false;
this.getAuthorsIndex(event.pubkey).delete(event);
this.getKindIndex(event.kind).delete(event);
for (const tag of getIndexableTags(event)) {
if (this.tags.has(tag)) {
this.getTagIndex(tag).delete(event);
}
}
// remove from created_at index
const i = this.created_at.indexOf(event);
this.created_at.splice(i, 1);
this.events.delete(id);
// remove from replaceable index
if (isReplaceable(event.kind)) {
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
const array = this.replaceable.get(address);
if (array && array.includes(event)) {
const idx = array.indexOf(event);
array.splice(idx, 1);
}
}
// remove any claims this event has
this.claims.delete(event);
// notify subscribers this event was removed
this.remove$.next(event);
return true;
}
/** A weak map of events that are claimed by other things */
claims = new WeakMap();
/** Sets the claim on the event and touches it */
claim(event, claim) {
if (!this.claims.has(event)) {
this.claims.set(event, claim);
}
// always touch event
this.touch(event);
}
/** Checks if an event is claimed by anything */
isClaimed(event) {
return this.claims.has(event);
}
/** Removes a claim from an event */
removeClaim(event, claim) {
const current = this.claims.get(event);
if (current === claim)
this.claims.delete(event);
}
/** Removes all claims on an event */
clearClaim(event) {
this.claims.delete(event);
}
/** Index helper methods */
getKindIndex(kind) {
if (!this.kinds.has(kind))
this.kinds.set(kind, new Set());
return this.kinds.get(kind);
}
getAuthorsIndex(author) {
if (!this.authors.has(author))
this.authors.set(author, new Set());
return this.authors.get(author);
}
getTagIndex(tagAndValue) {
if (!this.tags.has(tagAndValue)) {
// build new tag index from existing events
const events = new Set();
const ts = Date.now();
for (const event of this.events.values()) {
if (getIndexableTags(event).has(tagAndValue)) {
events.add(event);
}
}
const took = Date.now() - ts;
if (took > 100)
this.log(`Built index ${tagAndValue} took ${took}ms`);
this.tags.set(tagAndValue, events);
}
return this.tags.get(tagAndValue);
}
/** Iterates over all events by author */
*iterateAuthors(authors) {
for (const author of authors) {
const events = this.authors.get(author);
if (events) {
for (const event of events)
yield event;
}
}
}
/** Iterates over all events by indexable tag and value */
*iterateTag(tag, values) {
for (const value of values) {
const events = this.getTagIndex(tag + ":" + value);
if (events) {
for (const event of events)
yield event;
}
}
}
/** Iterates over all events by kind */
*iterateKinds(kinds) {
for (const kind of kinds) {
const events = this.kinds.get(kind);
if (events) {
for (const event of events)
yield event;
}
}
}
/** Iterates over all events by time */
*iterateTime(since, until) {
let untilIndex = 0;
let sinceIndex = this.created_at.length - 1;
let start = until
? binarySearch(this.created_at, (mid) => {
return mid.created_at - until;
})
: undefined;
if (start)
untilIndex = start[0];
const end = since
? binarySearch(this.created_at, (mid) => {
return mid.created_at - since;
})
: undefined;
if (end)
sinceIndex = end[0];
for (let i = untilIndex; i < sinceIndex; i++) {
yield this.created_at[i];
}
}
/** Iterates over all events by id */
*iterateIds(ids) {
for (const id of ids) {
if (this.events.has(id))
yield this.events.get(id);
}
}
/** Returns all events that match the filter */
getEventsForFilter(filter) {
// search is not supported, return an empty set
if (filter.search)
return new Set();
let first = true;
let events = new Set();
const and = (iterable) => {
const set = iterable instanceof Set ? iterable : new Set(iterable);
if (first) {
events = set;
first = false;
}
else {
for (const event of events) {
if (!set.has(event))
events.delete(event);
}
}
return events;
};
if (filter.ids)
and(this.iterateIds(filter.ids));
let time = null;
// query for time first if since is set
if (filter.since !== undefined) {
time = Array.from(this.iterateTime(filter.since, filter.until));
and(time);
}
for (const t of INDEXABLE_TAGS) {
const key = `#${t}`;
const values = filter[key];
if (values?.length)
and(this.iterateTag(t, values));
}
if (filter.authors)
and(this.iterateAuthors(filter.authors));
if (filter.kinds)
and(this.iterateKinds(filter.kinds));
// query for time last if only until is set
if (filter.since === undefined && filter.until !== undefined) {
time = Array.from(this.iterateTime(filter.since, filter.until));
and(time);
}
// if the filter queried on time and has a limit. truncate the events now
if (filter.limit && time) {
const limited = new Set();
for (const event of time) {
if (limited.size >= filter.limit)
break;
if (events.has(event))
limited.add(event);
}
return limited;
}
return events;
}
/** Returns all events that match the filters */
getEventsForFilters(filters) {
if (filters.length === 0)
throw new Error("No Filters");
let events = new Set();
for (const filter of filters) {
const filtered = this.getEventsForFilter(filter);
for (const event of filtered)
events.add(event);
}
return events;
}
/** Remove the oldest events that are not claimed */
prune(limit = 1000) {
let removed = 0;
let cursor = this.events.first;
while (cursor) {
const event = cursor.value;
if (!this.isClaimed(event)) {
this.remove(event);
removed++;
if (removed >= limit)
break;
}
cursor = cursor.next;
}
return removed;
}
/** Resets the event set */
reset() {
this.events.clear();
this.kinds.clear();
this.authors.clear();
this.tags.clear();
this.created_at = [];
this.replaceable.clear();
this.claims = new WeakMap();
}
}

View File

@@ -1,12 +1,47 @@
import { Filter, NostrEvent } from "nostr-tools";
import { Observable } from "rxjs";
import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
import { Observable, Subject } from "rxjs";
import { AddressPointerWithoutD } from "../helpers/pointers.js";
import { EventSet } from "./event-set.js";
import { IEventStore, ModelConstructor } from "./interface.js";
/** An extended {@link EventSet} that handles replaceable events, delets, and models */
export declare class EventStore implements IEventStore {
database: EventSet;
import { EventMemory } from "./event-memory.js";
import { IEventDatabase, IEventStore } from "./interface.js";
declare const EventStore_base: {
new (...args: any[]): {
[x: string]: any;
models: Map<import("./interface.js").ModelConstructor<any, any[], IEventStore | import("./interface.js").IAsyncEventStore>, Map<string, Observable<any>>>;
modelKeepWarm: number;
model<T extends unknown, Args extends Array<any>>(constructor: import("./interface.js").ModelConstructor<T, Args, IEventStore | import("./interface.js").IAsyncEventStore>, ...args: Args): Observable<T>;
filters(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
event(pointer: string | EventPointer): Observable<NostrEvent | undefined>;
replaceable(pointer: AddressPointer | AddressPointerWithoutD): Observable<NostrEvent | undefined>;
replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
profile(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
contacts(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("nostr-tools/nip19").ProfilePointer[]>;
mutes(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("../helpers/mutes.js").Mutes | undefined>;
mailboxes(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<{
inboxes: string[];
outboxes: string[];
} | undefined>;
blossomServers(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<URL[]>;
reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
thread(root: string | EventPointer | AddressPointer): Observable<import("../models/thread.js").Thread>;
comments(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
events(ids: string[]): Observable<Record<string, NostrEvent | undefined>>;
replaceableSet(pointers: {
kind: number;
pubkey: string;
identifier?: string;
}[]): Observable<Record<string, NostrEvent | undefined>>;
};
} & {
new (): {};
};
/** A wrapper around an event database that handles replaceable events, deletes, and models */
export declare class EventStore extends EventStore_base implements IEventStore {
database: IEventDatabase;
/** Optional memory database for ensuring single event instances */
memory?: EventMemory;
/** Enable this to keep old versions of replaceable events */
keepOldVersions: boolean;
/** Enable this to keep expired events */
@@ -17,11 +52,11 @@ export declare class EventStore implements IEventStore {
*/
verifyEvent?: (event: NostrEvent) => boolean;
/** A stream of new events added to the store */
insert$: Observable<NostrEvent>;
insert$: Subject<import("nostr-tools").Event>;
/** A stream of events that have been updated */
update$: Observable<NostrEvent>;
update$: Subject<import("nostr-tools").Event>;
/** A stream of events that have been removed */
remove$: Observable<NostrEvent>;
remove$: Subject<import("nostr-tools").Event>;
/**
* A method that will be called when an event isn't found in the store
* @experimental
@@ -37,7 +72,9 @@ export declare class EventStore implements IEventStore {
* @experimental
*/
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
constructor();
constructor(database?: IEventDatabase);
/** A method to add all events to memory to ensure there is only ever a single instance of an event */
private mapToMemory;
protected deletedIds: Set<string>;
protected deletedCoords: Map<string, number>;
protected checkDeleted(event: string | NostrEvent): boolean;
@@ -57,12 +94,10 @@ export declare class EventStore implements IEventStore {
* @returns The existing event or the event that was added, if it was ignored returns null
*/
add(event: NostrEvent, fromRelay?: string): NostrEvent | null;
/** Removes an event from the database and updates subscriptions */
/** Removes an event from the store and updates subscriptions */
remove(event: string | NostrEvent): boolean;
/** Add an event to the store and notifies all subscribes it has updated */
update(event: NostrEvent): boolean;
/** Removes any event that is not being used by a subscription */
prune(max?: number): number;
/** Check if the store has an event by id */
hasEvent(id: string): boolean;
/** Get an event by id from the store */
@@ -74,9 +109,11 @@ export declare class EventStore implements IEventStore {
/** Returns all versions of a replaceable event */
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
/** Get all events matching a filter */
getByFilters(filters: Filter | Filter[]): Set<NostrEvent>;
getByFilters(filters: Filter | Filter[]): NostrEvent[];
/** Returns a timeline of events that match filters */
getTimeline(filters: Filter | Filter[]): NostrEvent[];
/** Passthrough method for the database.touch */
touch(event: NostrEvent): void | undefined;
/** Sets the claim on the event and touches it */
claim(event: NostrEvent, claim: any): void;
/** Checks if an event is claimed by anything */
@@ -85,56 +122,13 @@ export declare class EventStore implements IEventStore {
removeClaim(event: NostrEvent, claim: any): void;
/** Removes all claims on an event */
clearClaim(event: NostrEvent): void;
/** A directory of all active models */
protected models: Map<ModelConstructor<any, any[]>, Map<string, Observable<any>>>;
/** How long a model should be kept "warm" while nothing is subscribed to it */
modelKeepWarm: number;
/** Get or create a model on the event store */
model<T extends unknown, Args extends Array<any>>(constructor: ModelConstructor<T, Args>, ...args: Args): Observable<T>;
/**
* Creates an observable that streams all events that match the filter
* @param filters
* @param [onlyNew=false] Only subscribe to new events
*/
filters(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
/** Pass through method for the database.unclaimed */
unclaimed(): Generator<NostrEvent>;
/** Removes any event that is not being used by a subscription */
prune(limit?: number): number;
/** Returns an observable that completes when an event is removed */
removed(id: string): Observable<never>;
/** Creates an observable that emits when event is updated */
updated(event: string | NostrEvent): Observable<NostrEvent>;
/** Creates a {@link EventModel} */
event(pointer: string | EventPointer): Observable<NostrEvent | undefined>;
/** Creates a {@link ReplaceableModel} */
replaceable(pointer: AddressPointer | AddressPointerWithoutD): Observable<NostrEvent | undefined>;
replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
/** Subscribe to an addressable event by pointer */
addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
/** Creates a {@link TimelineModel} */
timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
/** Subscribe to a users profile */
profile(user: string | ProfilePointer): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
/** Subscribe to a users contacts */
contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
/** Subscribe to a users mutes */
mutes(user: string | ProfilePointer): Observable<import("../helpers/mutes.js").Mutes | undefined>;
/** Subscribe to a users NIP-65 mailboxes */
mailboxes(user: string | ProfilePointer): Observable<{
inboxes: string[];
outboxes: string[];
} | undefined>;
/** Subscribe to a users blossom servers */
blossomServers(user: string | ProfilePointer): Observable<URL[]>;
/** Subscribe to an event's reactions */
reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
/** Subscribe to a thread */
thread(root: string | EventPointer | AddressPointer): Observable<import("../models/thread.js").Thread>;
/** Subscribe to a event's comments */
comments(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
/** @deprecated use multiple {@link EventModel} instead */
events(ids: string[]): Observable<Record<string, NostrEvent | undefined>>;
/** @deprecated use multiple {@link ReplaceableModel} instead */
replaceableSet(pointers: {
kind: number;
pubkey: string;
identifier?: string;
}[]): Observable<Record<string, NostrEvent | undefined>>;
}
export {};

View File

@@ -1,26 +1,20 @@
import { kinds } from "nostr-tools";
import { isAddressableKind } from "nostr-tools/kinds";
import { EMPTY, filter, finalize, from, merge, mergeMap, ReplaySubject, share, take, timer } from "rxjs";
import hash_sum from "hash-sum";
import { EMPTY, filter, mergeMap, Subject, take } from "rxjs";
import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
import { createReplaceableAddress, EventStoreSymbol, FromCacheSymbol, isReplaceable } from "../helpers/event.js";
import { getExpirationTimestamp } from "../helpers/expiration.js";
import { matchFilters } from "../helpers/filter.js";
import { parseCoordinate } from "../helpers/pointers.js";
import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
import { unixNow } from "../helpers/time.js";
import { UserBlossomServersModel } from "../models/blossom.js";
import { EventModel, EventsModel, ReplaceableModel, ReplaceableSetModel, TimelineModel } from "../models/common.js";
import { ContactsModel } from "../models/contacts.js";
import { CommentsModel, ThreadModel } from "../models/index.js";
import { MailboxesModel } from "../models/mailboxes.js";
import { MuteModel } from "../models/mutes.js";
import { ProfileModel } from "../models/profile.js";
import { ReactionsModel } from "../models/reactions.js";
import { EventSet } from "./event-set.js";
/** An extended {@link EventSet} that handles replaceable events, delets, and models */
export class EventStore {
import { EventMemory } from "./event-memory.js";
import { EventStoreModelMixin } from "./model-mixin.js";
/** A wrapper around an event database that handles replaceable events, deletes, and models */
export class EventStore extends EventStoreModelMixin(class {
}) {
database;
/** Optional memory database for ensuring single event instances */
memory;
/** Enable this to keep old versions of replaceable events */
keepOldVersions = false;
/** Enable this to keep expired events */
@@ -31,11 +25,11 @@ export class EventStore {
*/
verifyEvent;
/** A stream of new events added to the store */
insert$;
insert$ = new Subject();
/** A stream of events that have been updated */
update$;
update$ = new Subject();
/** A stream of events that have been removed */
remove$;
remove$ = new Subject();
/**
* A method that will be called when an event isn't found in the store
* @experimental
@@ -51,27 +45,31 @@ export class EventStore {
* @experimental
*/
addressableLoader;
constructor() {
this.database = new EventSet();
// verify events before they are added to the database
this.database.onBeforeInsert = (event) => {
// Ignore events that are invalid
if (this.verifyEvent && this.verifyEvent(event) === false)
return false;
else
return true;
};
constructor(database = new EventMemory()) {
super();
if (database) {
this.database = database;
this.memory = new EventMemory();
}
else {
// If no database is provided, its the same as having a memory database
this.database = this.memory = new EventMemory();
}
// when events are added to the database, add the symbol
this.database.insert$.subscribe((event) => {
this.insert$.subscribe((event) => {
Reflect.set(event, EventStoreSymbol, this);
});
// when events are removed from the database, remove the symbol
this.database.remove$.subscribe((event) => {
this.remove$.subscribe((event) => {
Reflect.deleteProperty(event, EventStoreSymbol);
});
this.insert$ = this.database.insert$;
this.update$ = this.database.update$;
this.remove$ = this.database.remove$;
}
mapToMemory(event) {
if (event === undefined)
return undefined;
if (!this.memory)
return event;
return this.memory.add(event);
}
// delete state
deletedIds = new Set();
@@ -137,9 +135,7 @@ export class EventStore {
for (const id of ids) {
this.deletedIds.add(id);
// remove deleted events in the database
const event = this.database.getEvent(id);
if (event)
this.database.remove(event);
this.remove(id);
}
const coords = getDeleteCoordinates(deleteEvent);
for (const coord of coords) {
@@ -152,7 +148,7 @@ export class EventStore {
const events = this.database.getReplaceableHistory(parsed.kind, parsed.pubkey, parsed.identifier) ?? [];
for (const event of events) {
if (event.created_at < deleteEvent.created_at)
this.database.remove(event);
this.remove(event);
}
}
}
@@ -173,6 +169,7 @@ export class EventStore {
* @returns The existing event or the event that was added, if it was ignored returns null
*/
add(event, fromRelay) {
// Handle delete events differently
if (event.kind === kinds.EventDeletion)
this.handleDeleteEvent(event);
// Ignore if the event was deleted
@@ -193,32 +190,38 @@ export class EventStore {
return existing[0];
}
}
else if (this.database.hasEvent(event.id)) {
// Duplicate event, copy symbols and return existing event
const existing = this.database.getEvent(event.id);
if (existing) {
EventStore.mergeDuplicateEvent(event, existing);
return existing;
}
// Verify event before inserting into the database
if (this.verifyEvent && this.verifyEvent(event) === false)
return null;
// Always add event to memory
const existing = this.memory?.add(event);
// If the memory returned a different instance, this is a duplicate event
if (existing && existing !== event) {
// Copy cached symbols and return existing event
EventStore.mergeDuplicateEvent(event, existing);
// attach relay this event was from
if (fromRelay)
addSeenRelay(existing, fromRelay);
return existing;
}
// Insert event into database
const inserted = this.database.add(event);
// If the event was ignored, return null
if (inserted === null)
return null;
const inserted = this.mapToMemory(this.database.add(event));
// Copy cached data if its a duplicate event
if (event !== inserted)
EventStore.mergeDuplicateEvent(event, inserted);
// attach relay this event was from
if (fromRelay)
addSeenRelay(inserted, fromRelay);
// Emit insert$ signal
if (inserted === event)
this.insert$.next(inserted);
// remove all old version of the replaceable event
if (!this.keepOldVersions && isReplaceable(event.kind)) {
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
if (existing) {
if (existing && existing.length > 0) {
const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
for (const old of older)
this.database.remove(old);
this.remove(old);
// return the newest version of the replaceable event
// most of the time this will be === event, but not always
if (existing.length !== older.length)
@@ -230,108 +233,103 @@ export class EventStore {
this.handleExpiringEvent(inserted);
return inserted;
}
/** Removes an event from the database and updates subscriptions */
/** Removes an event from the store and updates subscriptions */
remove(event) {
return this.database.remove(event);
let instance = this.memory?.getEvent(typeof event === "string" ? event : event.id);
// Remove from memory if available
if (this.memory)
this.memory.remove(event);
// Remove the event from the database
const removed = this.database.remove(event);
// If the event was removed, notify the subscriptions
if (removed && instance) {
this.remove$.next(instance);
}
return removed;
}
/** Add an event to the store and notifies all subscribes it has updated */
update(event) {
return this.database.update(event);
}
/** Removes any event that is not being used by a subscription */
prune(max) {
return this.database.prune(max);
// Map the event to the current instance in the database
const e = this.database.add(event);
if (!e)
return false;
// Notify the database that the event has updated
this.database.update?.(event);
this.update$.next(event);
return true;
}
/** Check if the store has an event by id */
hasEvent(id) {
return this.database.hasEvent(id);
// Check if the event exists in memory first, then in the database
return this.memory?.hasEvent(id) || this.database.hasEvent(id);
}
/** Get an event by id from the store */
getEvent(id) {
return this.database.getEvent(id);
// Get the event from memory first, then from the database
return this.memory?.getEvent(id) ?? this.mapToMemory(this.database.getEvent(id));
}
/** Check if the store has a replaceable event */
hasReplaceable(kind, pubkey, d) {
return this.database.hasReplaceable(kind, pubkey, d);
// Check if the event exists in memory first, then in the database
return this.memory?.hasReplaceable(kind, pubkey, d) || this.database.hasReplaceable(kind, pubkey, d);
}
/** Gets the latest version of a replaceable event */
getReplaceable(kind, pubkey, identifier) {
return this.database.getReplaceable(kind, pubkey, identifier);
// Get the event from memory first, then from the database
return (this.memory?.getReplaceable(kind, pubkey, identifier) ??
this.mapToMemory(this.database.getReplaceable(kind, pubkey, identifier)));
}
/** Returns all versions of a replaceable event */
getReplaceableHistory(kind, pubkey, identifier) {
return this.database.getReplaceableHistory(kind, pubkey, identifier);
// Get the events from memory first, then from the database
return (this.memory?.getReplaceableHistory(kind, pubkey, identifier) ??
this.database.getReplaceableHistory(kind, pubkey, identifier)?.map((e) => this.mapToMemory(e) ?? e));
}
/** Get all events matching a filter */
getByFilters(filters) {
return this.database.getByFilters(filters);
// NOTE: no way to read from memory since memory won't have the full set of events
const events = this.database.getByFilters(filters);
// Map events to memory if available for better performance
if (this.memory)
return events.map((e) => this.mapToMemory(e));
else
return events;
}
/** Returns a timeline of events that match filters */
getTimeline(filters) {
return this.database.getTimeline(filters);
const events = this.database.getTimeline(filters);
if (this.memory)
return events.map((e) => this.mapToMemory(e));
else
return events;
}
/** Passthrough method for the database.touch */
touch(event) {
return this.memory?.touch(event);
}
/** Sets the claim on the event and touches it */
claim(event, claim) {
this.database.claim(event, claim);
return this.memory?.claim(event, claim);
}
/** Checks if an event is claimed by anything */
isClaimed(event) {
return this.database.isClaimed(event);
return this.memory?.isClaimed(event) ?? false;
}
/** Removes a claim from an event */
removeClaim(event, claim) {
this.database.removeClaim(event, claim);
return this.memory?.removeClaim(event, claim);
}
/** Removes all claims on an event */
clearClaim(event) {
this.database.clearClaim(event);
return this.memory?.clearClaim(event);
}
/** A directory of all active models */
models = new Map();
/** How long a model should be kept "warm" while nothing is subscribed to it */
modelKeepWarm = 60_000;
/** Get or create a model on the event store */
model(constructor, ...args) {
let models = this.models.get(constructor);
if (!models) {
models = new Map();
this.models.set(constructor, models);
}
const key = constructor.getKey ? constructor.getKey(...args) : hash_sum(args);
let model = models.get(key);
// Create the model if it does not exist
if (!model) {
const cleanup = () => {
// Remove the model from the cache if its the same one
if (models.get(key) === model)
models.delete(key);
};
model = constructor(...args)(this).pipe(
// remove the model when its unsubscribed
finalize(cleanup),
// only subscribe to models once for all subscriptions
share({
connector: () => new ReplaySubject(1),
resetOnComplete: () => timer(this.modelKeepWarm),
resetOnRefCountZero: () => timer(this.modelKeepWarm),
}));
// Add the model to the cache
models.set(key, model);
}
return model;
/** Pass through method for the database.unclaimed */
unclaimed() {
return this.memory?.unclaimed() || (function* () { })();
}
/**
* Creates an observable that streams all events that match the filter
* @param filters
* @param [onlyNew=false] Only subscribe to new events
*/
filters(filters, onlyNew = false) {
filters = Array.isArray(filters) ? filters : [filters];
return merge(
// merge existing events
onlyNew ? EMPTY : from(this.getByFilters(filters)),
// subscribe to future events
this.insert$.pipe(filter((e) => matchFilters(filters, e))));
/** Removes any event that is not being used by a subscription */
prune(limit) {
return this.memory?.prune(limit) ?? 0;
}
/** Returns an observable that completes when an event is removed */
removed(id) {
@@ -348,83 +346,6 @@ export class EventStore {
}
/** Creates an observable that emits when event is updated */
updated(event) {
return this.database.update$.pipe(filter((e) => e.id === event || e === event));
}
// Helper methods for creating models
/** Creates a {@link EventModel} */
event(pointer) {
if (typeof pointer === "string")
pointer = { id: pointer };
return this.model(EventModel, pointer);
}
replaceable(...args) {
let pointer;
// Parse arguments
if (args.length === 1) {
pointer = args[0];
}
else if (args.length === 3 || args.length === 2) {
let [kind, pubkey, identifier] = args;
pointer = { kind, pubkey, identifier };
}
if (!pointer)
throw new Error("Invalid arguments, expected address pointer or kind, pubkey, identifier");
return this.model(ReplaceableModel, pointer);
}
/** Subscribe to an addressable event by pointer */
addressable(pointer) {
return this.model(ReplaceableModel, pointer);
}
/** Creates a {@link TimelineModel} */
timeline(filters, includeOldVersion = false) {
return this.model(TimelineModel, filters, includeOldVersion);
}
/** Subscribe to a users profile */
profile(user) {
return this.model(ProfileModel, user);
}
/** Subscribe to a users contacts */
contacts(user) {
if (typeof user === "string")
user = { pubkey: user };
return this.model(ContactsModel, user);
}
/** Subscribe to a users mutes */
mutes(user) {
if (typeof user === "string")
user = { pubkey: user };
return this.model(MuteModel, user);
}
/** Subscribe to a users NIP-65 mailboxes */
mailboxes(user) {
if (typeof user === "string")
user = { pubkey: user };
return this.model(MailboxesModel, user);
}
/** Subscribe to a users blossom servers */
blossomServers(user) {
if (typeof user === "string")
user = { pubkey: user };
return this.model(UserBlossomServersModel, user);
}
/** Subscribe to an event's reactions */
reactions(event) {
return this.model(ReactionsModel, event);
}
/** Subscribe to a thread */
thread(root) {
return this.model(ThreadModel, root);
}
/** Subscribe to a event's comments */
comments(event) {
return this.model(CommentsModel, event);
}
/** @deprecated use multiple {@link EventModel} instead */
events(ids) {
return this.model(EventsModel, ids);
}
/** @deprecated use multiple {@link ReplaceableModel} instead */
replaceableSet(pointers) {
return this.model(ReplaceableSetModel, pointers);
return this.update$.pipe(filter((e) => e.id === event || e === event));
}
}

View File

@@ -1,3 +1,4 @@
export * from "./async-event-store.js";
export * from "./event-memory.js";
export * from "./event-store.js";
export * from "./event-set.js";
export * from "./interface.js";

View File

@@ -1,3 +1,4 @@
export * from "./async-event-store.js";
export * from "./event-memory.js";
export * from "./event-store.js";
export * from "./event-set.js";
export * from "./interface.js";

View File

@@ -1,28 +1,44 @@
import { Filter, NostrEvent } from "nostr-tools";
import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
import { Observable } from "rxjs";
import { LRU } from "../helpers/lru.js";
import { Mutes } from "../helpers/mutes.js";
import { ProfileContent } from "../helpers/profile.js";
import { Thread } from "../models/thread.js";
import { AddressPointerWithoutD } from "../helpers/pointers.js";
import { ProfileContent } from "../helpers/profile.js";
import { Mutes } from "../helpers/mutes.js";
import { Thread } from "../models/thread.js";
/** The read interface for an event store */
export interface IEventStoreRead {
/** Check if the event store has an event with id */
hasEvent(id: string): boolean;
/** Check if the event store has a replaceable event */
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
/** Get an event by id */
getEvent(id: string): NostrEvent | undefined;
/** Check if the event store has a replaceable event */
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
/** Get a replaceable event */
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
/** Get the history of a replaceable event */
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
/** Get all events that match the filters */
getByFilters(filters: Filter | Filter[]): Set<NostrEvent>;
getByFilters(filters: Filter | Filter[]): NostrEvent[];
/** Get a timeline of events that match the filters */
getTimeline(filters: Filter | Filter[]): NostrEvent[];
}
/** The async read interface for an event store */
export interface IAsyncEventStoreRead {
/** Check if the event store has an event with id */
hasEvent(id: string): Promise<boolean>;
/** Get an event by id */
getEvent(id: string): Promise<NostrEvent | undefined>;
/** Check if the event store has a replaceable event */
hasReplaceable(kind: number, pubkey: string, identifier?: string): Promise<boolean>;
/** Get a replaceable event */
getReplaceable(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent | undefined>;
/** Get the history of a replaceable event */
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent[] | undefined>;
/** Get all events that match the filters */
getByFilters(filters: Filter | Filter[]): Promise<NostrEvent[]>;
/** Get a timeline of events that match the filters */
getTimeline(filters: Filter | Filter[]): Promise<NostrEvent[]>;
}
/** The stream interface for an event store */
export interface IEventStoreStreams {
/** A stream of new events added to the store */
@@ -41,8 +57,19 @@ export interface IEventStoreActions {
/** Notify the store that an event has updated */
update(event: NostrEvent): void;
}
/** The async actions for an event store */
export interface IAsyncEventStoreActions {
/** Add an event to the store */
add(event: NostrEvent): Promise<NostrEvent | null>;
/** Remove an event from the store */
remove(event: string | NostrEvent): Promise<boolean>;
/** Notify the store that an event has updated */
update(event: NostrEvent): Promise<void>;
}
/** The claim interface for an event store */
export interface IEventClaims {
/** Tell the store that this event was used */
touch(event: NostrEvent): void;
/** Sets the claim on the event and touches it */
claim(event: NostrEvent, claim: any): void;
/** Checks if an event is claimed by anything */
@@ -51,59 +78,105 @@ export interface IEventClaims {
removeClaim(event: NostrEvent, claim: any): void;
/** Removes all claims on an event */
clearClaim(event: NostrEvent): void;
/** Returns a generator of unclaimed events in order of least used */
unclaimed(): Generator<NostrEvent>;
}
/** An event store that can be subscribed to */
export interface IEventStoreSubscriptions {
/** Susbscribe to an event by id */
export interface IEventSubscriptions {
/** Subscribe to an event by id */
event(id: string | EventPointer): Observable<NostrEvent | undefined>;
/** Subscribe to a replaceable event by pointer */
replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
/** Subscribe to a replaceable event with legacy arguments */
replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
/** Subscribe to an addressable event by pointer */
addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
/** Subscribe to a batch of events that match the filters */
filter(filters: Filter | Filter[]): Observable<NostrEvent[]>;
filters(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
/** Subscribe to a sorted timeline of events that match the filters */
timeline(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent[]>;
}
/** @deprecated use {@link IEventSubscriptions} instead */
export interface IEventStoreSubscriptions extends IEventSubscriptions {
}
/** Methods for creating common models */
export interface IEventStoreModels {
model<T extends unknown, Args extends Array<any>>(constructor: ModelConstructor<T, Args>, ...args: Args): Observable<T>;
event(id: string): Observable<NostrEvent | undefined>;
replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
export interface IEventModelMixin<TStore extends IEventStore | IAsyncEventStore> {
model<T extends unknown, Args extends Array<any>>(constructor: ModelConstructor<T, Args, TStore>, ...args: Args): Observable<T>;
/** @deprecated use multiple {@link EventModel} instead */
events(ids: string[]): Observable<Record<string, NostrEvent | undefined>>;
/** @deprecated use multiple {@link ReplaceableModel} instead */
replaceableSet(pointers: (AddressPointer | AddressPointerWithoutD)[]): Observable<Record<string, NostrEvent | undefined>>;
}
/** A computed view of an event set or event store */
export type Model<T extends unknown> = (events: IEventStore) => Observable<T>;
/** A constructor for a {@link Model} */
export type ModelConstructor<T extends unknown, Args extends Array<any>> = ((...args: Args) => Model<T>) & {
getKey?: (...args: Args) => string;
};
/** The base interface for a set of events */
export interface IEventSet extends IEventStoreRead, IEventStoreStreams, IEventStoreActions, IEventClaims {
events: LRU<NostrEvent>;
}
export interface IEventStore extends IEventStoreRead, IEventStoreStreams, IEventStoreActions, IEventStoreModels, IEventClaims {
/** Enable this to keep old versions of replaceable events */
keepOldVersions: boolean;
/** Enable this to keep expired events */
keepExpired: boolean;
filters(filters: Filter | Filter[]): Observable<NostrEvent>;
updated(id: string | NostrEvent): Observable<NostrEvent>;
removed(id: string): Observable<never>;
replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
/** Methods for creating helpful models */
export interface IEventHelpfulSubscriptions {
/** Subscribe to a users profile */
profile(user: string | ProfilePointer): Observable<ProfileContent | undefined>;
/** Subscribe to a users contacts */
contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
/** Subscribe to a users mutes */
mutes(user: string | ProfilePointer): Observable<Mutes | undefined>;
/** Subscribe to a users NIP-65 mailboxes */
mailboxes(user: string | ProfilePointer): Observable<{
inboxes: string[];
outboxes: string[];
} | undefined>;
/** Subscribe to a users blossom servers */
blossomServers(user: string | ProfilePointer): Observable<URL[]>;
/** Subscribe to an event's reactions */
reactions(event: NostrEvent): Observable<NostrEvent[]>;
/** Subscribe to a thread */
thread(root: string | EventPointer | AddressPointer): Observable<Thread>;
/** Subscribe to a event's comments */
comments(event: NostrEvent): Observable<NostrEvent[]>;
}
/** @deprecated use {@link IEventModelMixin} instead */
export interface IEventStoreModels extends IEventModelMixin<IEventStore> {
}
/** The interface that is passed to the model for creating subscriptions */
export type ModelEventStore<TStore extends IEventStore | IAsyncEventStore> = IEventStoreStreams & IEventSubscriptions & IEventModelMixin<TStore> & IEventFallbackLoaders & TStore;
/** A computed view of an event set or event store */
export type Model<T extends unknown, TStore extends IEventStore | IAsyncEventStore = IEventStore | IAsyncEventStore> = (events: ModelEventStore<TStore>) => Observable<T>;
/** A constructor for a {@link Model} */
export type ModelConstructor<T extends unknown, Args extends Array<any>, TStore extends IEventStore | IAsyncEventStore = IEventStore> = ((...args: Args) => Model<T, TStore>) & {
getKey?: (...args: Args) => string;
};
/** The base interface for a database of events */
export interface IEventDatabase extends IEventStoreRead {
/** Add an event to the database */
add(event: NostrEvent): NostrEvent;
/** Remove an event from the database */
remove(event: string | NostrEvent): boolean;
/** Notifies the database that an event has updated */
update?: (event: NostrEvent) => void;
}
/** The async base interface for a set of events */
export interface IAsyncEventDatabase extends IAsyncEventStoreRead {
/** Add an event to the database */
add(event: NostrEvent): Promise<NostrEvent>;
/** Remove an event from the database */
remove(event: string | NostrEvent): Promise<boolean>;
/** Notifies the database that an event has updated */
update?: (event: NostrEvent) => void;
}
/** The base interface for the in-memory database of events */
export interface IEventMemory extends IEventStoreRead, IEventClaims {
/** Add an event to the store */
add(event: NostrEvent): NostrEvent;
/** Remove an event from the store */
remove(event: string | NostrEvent): boolean;
}
/** A set of methods that an event store will use to load single events it does not have */
export interface IEventFallbackLoaders {
/** A method that will be called when an event isn't found in the store */
eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
/** A method that will be called when a replaceable event isn't found in the store */
replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
/** A method that will be called when an addressable event isn't found in the store */
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
}
/** The async event store interface */
export interface IAsyncEventStore extends IAsyncEventStoreRead, IEventStoreStreams, IEventSubscriptions, IAsyncEventStoreActions, IEventModelMixin<IAsyncEventStore>, IEventHelpfulSubscriptions, IEventClaims, IEventFallbackLoaders {
}
/** The sync event store interface */
export interface IEventStore extends IEventStoreRead, IEventStoreStreams, IEventSubscriptions, IEventStoreActions, IEventModelMixin<IEventStore>, IEventHelpfulSubscriptions, IEventClaims, IEventFallbackLoaders {
}

View File

@@ -1,7 +1,12 @@
import { NostrEvent } from "nostr-tools";
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
import { HiddenContentSigner } from "./index.js";
export declare const BookmarkPublicSymbol: unique symbol;
export declare const BookmarkHiddenSymbol: unique symbol;
/** Type for unlocked bookmarks events */
export type UnlockedBookmarks = {
[BookmarkHiddenSymbol]: Bookmarks;
};
export type Bookmarks = {
notes: EventPointer[];
articles: AddressPointer[];
@@ -16,5 +21,10 @@ export declare function mergeBookmarks(...bookmarks: (Bookmarks | undefined)[]):
export declare function getBookmarks(bookmark: NostrEvent): Bookmarks;
/** Returns the public bookmarks of the event */
export declare function getPublicBookmarks(bookmark: NostrEvent): Bookmarks;
/** Checks if the hidden bookmarks are unlocked */
export declare function isHiddenBookmarksUnlocked<T extends NostrEvent>(bookmark: T): bookmark is T & UnlockedBookmarks;
/** Returns the bookmarks of the event if its unlocked */
export declare function getHiddenBookmarks(bookmark: NostrEvent): Bookmarks | undefined;
export declare function getHiddenBookmarks<T extends NostrEvent & UnlockedBookmarks>(bookmark: T): Bookmarks;
export declare function getHiddenBookmarks<T extends NostrEvent>(bookmark: T): Bookmarks | undefined;
/** Unlocks the hidden bookmarks on a bookmarks event */
export declare function unlockHiddenBookmarks(bookmark: NostrEvent, signer: HiddenContentSigner): Promise<Bookmarks>;

Some files were not shown because too many files have changed in this diff Show More