Compare commits

...

96 Commits

Author SHA1 Message Date
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
258 changed files with 5704 additions and 7923 deletions

View File

@@ -3,4 +3,4 @@ description: when creating or modifying UI elements, especially related to icons
alwaysApply: false
---
We use FontAwesome. If you can use a fa-icon (instead of text) use a fa-icon.
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

@@ -1,4 +1,4 @@
# Markr
# Boris
A minimal nostr client for bookmark management, built with [applesauce](https://github.com/hzrd149/applesauce).
@@ -13,6 +13,8 @@ A minimal nostr client for bookmark management, built with [applesauce](https://
- **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
@@ -27,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:
@@ -53,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
@@ -76,6 +80,8 @@ src/
│ ├── 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
@@ -85,9 +91,11 @@ src/
│ ├── 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/

4
dist/index.html vendored
View File

@@ -5,8 +5,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">
<script type="module" crossorigin src="/assets/index-sYF0VIKc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BNyWhz1u.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>

533
node_modules/.package-lock.json generated vendored
View File

@@ -1,6 +1,6 @@
{
"name": "markr",
"version": "0.1.1",
"name": "boris",
"version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -320,32 +320,6 @@
"node": ">=22.4.0"
}
},
"node_modules/@cashu/crypto": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.2.7.tgz",
"integrity": "sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==",
"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"
}
},
"node_modules/@cashu/crypto/node_modules/@scure/bip39": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
"integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -1914,16 +1888,16 @@
}
},
"node_modules/applesauce-accounts": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-accounts/-/applesauce-accounts-3.1.0.tgz",
"integrity": "sha512-F0xFi4CU5Ak917VcUap+6YIxyvbjP2BJejWBjVurFMY6tCK3kWWPQnoPc+GjU6LkMsL8BfQcOQ6wKIX4MvTB7Q==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/applesauce-accounts/-/applesauce-accounts-4.0.0.tgz",
"integrity": "sha512-JHv63cRSSWXqbId6anNfOlPCE+sr3zJeHT6Jt9p+3X+hZkHrlNy5sLBK+fBHvKc5w/eQFsmnIWYfuLxPdqZcrA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.7.1",
"applesauce-core": "^3.1.0",
"applesauce-signers": "^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"
},
"funding": {
@@ -1932,14 +1906,14 @@
}
},
"node_modules/applesauce-actions": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-actions/-/applesauce-actions-3.1.0.tgz",
"integrity": "sha512-wNZQr1qAmlQCjOQQXK0sHyNtcEbYbcUmA97bYg4KctSwlEQAI14fUTltYHdLZ3Zw4xJ9F0Xq4O2YCUJA4UOYlA==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/applesauce-actions/-/applesauce-actions-4.0.0.tgz",
"integrity": "sha512-oYAjrazKGDINeVwypNDnV9eNSv7ZDTjNeV3azo5jeUU1haEQ0t+zwVWzGxk9/VutT1yWQHFsCZBInYZIegfLhQ==",
"license": "MIT",
"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"
},
"funding": {
@@ -1970,90 +1944,7 @@
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/applesauce-content/node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-content/node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-content/node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-content/node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-content/node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-content/node_modules/@scure/bip32/node_modules/@noble/hashes": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
"integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-content/node_modules/@scure/bip32/node_modules/@scure/base": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-content/node_modules/applesauce-core": {
"node_modules/applesauce-core": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/applesauce-core/-/applesauce-core-4.0.0.tgz",
"integrity": "sha512-cXg9lDU0PKpAeVw7FGN3jzKB/k9kX5YOI7uwzrZhibYB5PfpHAYRiVexxBuFdT2RNaDgQogXl75gV2hag5uLuw==",
@@ -2074,82 +1965,14 @@
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/applesauce-content/node_modules/nostr-tools": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.17.0.tgz",
"integrity": "sha512-lrvHM7cSaGhz7F0YuBvgHMoU2s8/KuThihDoOYk8w5gpVHTy0DeUCAgCN8uLGeuSl5MAWekJr9Dkfo5HClqO9w==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1",
"nostr-wasm": "0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/applesauce-content/node_modules/nostr-tools/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-content/node_modules/nostr-tools/node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/applesauce-core": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-core/-/applesauce-core-3.1.0.tgz",
"integrity": "sha512-rIvtAYm8jJiLkv251yT12olmlmlkeT5x9kptWlAz0wMiAhymGG/RoWtMN80mbOAebjwcLCRLRfrAO6YYal1XpQ==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.7.1",
"@scure/base": "^1.2.4",
"debug": "^4.4.0",
"fast-deep-equal": "^3.1.3",
"hash-sum": "^2.0.0",
"light-bolt11-decoder": "^3.2.0",
"nanoid": "^5.0.9",
"nostr-tools": "~2.15",
"rxjs": "^7.8.1"
},
"funding": {
"type": "lightning",
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/applesauce-factory": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-factory/-/applesauce-factory-3.1.0.tgz",
"integrity": "sha512-K9onWy8yvWnQp2c+a227IFHv65ToDH4B4yoLEOuLs2xOp7BwtZCoIdVZbWVk93X1KvA7pBTtXCjDbwTcH7LCjQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/applesauce-factory/-/applesauce-factory-4.0.0.tgz",
"integrity": "sha512-Sqsg+bC7CkRXMxXLkO6YGoKxy/Aqtia9YenasS5qjPOQFmyFMwKRxaHCu6vX6KdpNSABusw0b9Tnn4gTh6CxLw==",
"license": "MIT",
"dependencies": {
"applesauce-content": "^3.1.0",
"applesauce-core": "^3.1.0",
"applesauce-content": "^4.0.0",
"applesauce-core": "^4.0.0",
"nanoid": "^5.0.9",
"nostr-tools": "^2.13"
},
@@ -2158,51 +1981,15 @@
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/applesauce-factory/node_modules/@cashu/cashu-ts": {
"version": "2.0.0-rc1",
"resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-2.0.0-rc1.tgz",
"integrity": "sha512-39459l7x/fUMEgOsCdGLLl6rMekO4nbv+wEuavmyElh8hgN8t66wcb29AJvdFTb6K3lPACKF2rs/jAlPYrN7Ng==",
"license": "MIT",
"dependencies": {
"@cashu/crypto": "^0.2.7",
"@noble/curves": "^1.3.0",
"@noble/hashes": "^1.3.3",
"@scure/bip32": "^1.3.3",
"buffer": "^6.0.3"
}
},
"node_modules/applesauce-factory/node_modules/applesauce-content": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-content/-/applesauce-content-3.1.0.tgz",
"integrity": "sha512-dxXmEzMz5KQIdaKOVJg2ufphVPoECWa6l7NIQo5mXQGrjv3VrT5QY5x0MVWJWWcC4fRBwE8xIhJyfhIeosymMQ==",
"license": "MIT",
"dependencies": {
"@cashu/cashu-ts": "2.0.0-rc1",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/unist": "^3.0.3",
"applesauce-core": "^3.1.0",
"mdast-util-find-and-replace": "^3.0.2",
"nostr-tools": "~2.15",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"unified": "^11.0.5",
"unist-util-visit-parents": "^6.0.1"
},
"funding": {
"type": "lightning",
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/applesauce-loaders": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-loaders/-/applesauce-loaders-3.1.0.tgz",
"integrity": "sha512-9IY5RYqoXcIgAAdJNuMjMBr+CI85z1yj708C92UiP9YMQ4mrIIEvZbMmWLApBzRn5XsmmEa00a/iNlXpkRg9Sw==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/applesauce-loaders/-/applesauce-loaders-4.0.0.tgz",
"integrity": "sha512-yNlpzCeUlkpq1jehyHPre0C3ey9anMDTpPysqdV9Rca+U2QnAWduzQ+Eo8FVS1X9jZRD6s/gkLSnCnW1+kX7uQ==",
"license": "MIT",
"dependencies": {
"applesauce-core": "^3.1.0",
"applesauce-core": "^4.0.0",
"nanoid": "^5.0.9",
"nostr-tools": "~2.15",
"nostr-tools": "~2.17",
"rxjs": "^7.8.1"
},
"funding": {
@@ -2211,18 +1998,18 @@
}
},
"node_modules/applesauce-react": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-react/-/applesauce-react-3.1.0.tgz",
"integrity": "sha512-9zKbHOkXTGBLRe2uZvwAyPLTVPfelQQ7BvVTYHb2hg1JbhphMQqDSas3kaHBltt9ZCVh0xdv8JupmKWTUeCzag==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/applesauce-react/-/applesauce-react-4.0.0.tgz",
"integrity": "sha512-eVDUf3GL1j4bsL1Y8GsC/2sywajLu1oJioCNajUsm68hf5+zIR0rLHWaA4y0o5Rcctf/O4UbYkFztj1XHcuHgg==",
"license": "MIT",
"dependencies": {
"applesauce-accounts": "^3.1.0",
"applesauce-actions": "^3.1.0",
"applesauce-content": "^3.1.0",
"applesauce-core": "^3.1.0",
"applesauce-factory": "^3.1.0",
"applesauce-accounts": "^4.0.0",
"applesauce-actions": "^4.0.0",
"applesauce-content": "^4.0.0",
"applesauce-core": "^4.0.0",
"applesauce-factory": "^4.0.0",
"hash-sum": "^2.0.0",
"nostr-tools": "~2.15",
"nostr-tools": "~2.17",
"observable-hooks": "^4.2.4",
"react": "^18.3.1",
"rxjs": "^7.8.1"
@@ -2232,52 +2019,16 @@
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/applesauce-react/node_modules/@cashu/cashu-ts": {
"version": "2.0.0-rc1",
"resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-2.0.0-rc1.tgz",
"integrity": "sha512-39459l7x/fUMEgOsCdGLLl6rMekO4nbv+wEuavmyElh8hgN8t66wcb29AJvdFTb6K3lPACKF2rs/jAlPYrN7Ng==",
"license": "MIT",
"dependencies": {
"@cashu/crypto": "^0.2.7",
"@noble/curves": "^1.3.0",
"@noble/hashes": "^1.3.3",
"@scure/bip32": "^1.3.3",
"buffer": "^6.0.3"
}
},
"node_modules/applesauce-react/node_modules/applesauce-content": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-content/-/applesauce-content-3.1.0.tgz",
"integrity": "sha512-dxXmEzMz5KQIdaKOVJg2ufphVPoECWa6l7NIQo5mXQGrjv3VrT5QY5x0MVWJWWcC4fRBwE8xIhJyfhIeosymMQ==",
"license": "MIT",
"dependencies": {
"@cashu/cashu-ts": "2.0.0-rc1",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/unist": "^3.0.3",
"applesauce-core": "^3.1.0",
"mdast-util-find-and-replace": "^3.0.2",
"nostr-tools": "~2.15",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"unified": "^11.0.5",
"unist-util-visit-parents": "^6.0.1"
},
"funding": {
"type": "lightning",
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/applesauce-relay": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-relay/-/applesauce-relay-3.1.0.tgz",
"integrity": "sha512-YseV51O3pc9IIX9MoP4XrVmkUq6a0u8h7n/B4zg0Y3bCQEs415LbM3UwIwZzF1DAEnphOu2xXgkH/9QCg5HvFg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/applesauce-relay/-/applesauce-relay-4.0.0.tgz",
"integrity": "sha512-qoWjh9dABdL7AuSe4cmKiyZhNvrVZBRXA1GQgWiKSynm+rNYP+6Rc4SDT3vndNMqx9jRKbL4jwvKeU5vkVpFRA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.7.1",
"applesauce-core": "^3.1.0",
"applesauce-core": "^4.0.0",
"nanoid": "^5.0.9",
"nostr-tools": "~2.15",
"nostr-tools": "~2.17",
"rxjs": "^7.8.1"
},
"funding": {
@@ -2286,18 +2037,18 @@
}
},
"node_modules/applesauce-signers": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/applesauce-signers/-/applesauce-signers-3.1.0.tgz",
"integrity": "sha512-7loxZ3hKSAhDRDs9rtVP/MPBZRmSbutE7g8/ALnXe5ihytbKSdF+0L5NBd0fxpSLMvmd8wEFOha6LsT03I5RCQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/applesauce-signers/-/applesauce-signers-4.0.0.tgz",
"integrity": "sha512-AHrPtH1Oy0l1OS7jwd/DfseUpOhTE8JhZIUIElcQgAlqE7Cgg5FO+LF7dWRPUNb3zn8P+mzEQN2wJhby84SNcA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.7.1",
"@noble/secp256k1": "^1.7.1",
"@scure/base": "^1.2.4",
"applesauce-core": "^3.1.0",
"applesauce-core": "^4.0.0",
"debug": "^4.4.0",
"nanoid": "^5.0.9",
"nostr-tools": "~2.15",
"nostr-tools": "~2.17",
"rxjs": "^7.8.2"
},
"funding": {
@@ -2339,26 +2090,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.10",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz",
@@ -2426,30 +2157,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2660,6 +2367,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -2708,6 +2424,61 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.228",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz",
@@ -2715,6 +2486,18 @@
"dev": true,
"license": "ISC"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -2768,7 +2551,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -3317,25 +3099,24 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-3-Clause"
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
@@ -3498,6 +3279,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4605,9 +4395,9 @@
"license": "MIT"
},
"node_modules/nostr-tools": {
"version": "2.15.2",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.15.2.tgz",
"integrity": "sha512-utmqVVS4HMDiwhIgI6Cr+KqA4aUhF3Sb755iO/qCiqxc5H9JW/9Z3N1RO/jKWpjP6q/Vx0lru7IYuiPvk+2/ng==",
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.17.0.tgz",
"integrity": "sha512-lrvHM7cSaGhz7F0YuBvgHMoU2s8/KuThihDoOYk8w5gpVHTy0DeUCAgCN8uLGeuSl5MAWekJr9Dkfo5HClqO9w==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
@@ -4816,6 +4606,12 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4860,7 +4656,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -4880,7 +4675,6 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -4909,7 +4703,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
@@ -5037,6 +4830,15 @@
"node": ">=0.10.0"
}
},
"node_modules/reading-time-estimator": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/reading-time-estimator/-/reading-time-estimator-1.14.0.tgz",
"integrity": "sha512-EWLj21Ou07uUvZWE0suAGPvEebhp91ZABl8jhTzZXY/ziBOPXfQ4tZ1eHiUV7moQ1NJ1KJj9krWuFlnoMx0upA==",
"license": "MIT",
"dependencies": {
"sanitize-html": "^2.15.0"
}
},
"node_modules/remark": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz",
@@ -5232,6 +5034,20 @@
"tslib": "^2.1.0"
}
},
"node_modules/sanitize-html": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
"integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"htmlparser2": "^8.0.0",
"is-plain-object": "^5.0.0",
"parse-srcset": "^1.0.2",
"postcss": "^8.3.11"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -5291,7 +5107,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"

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>;

View File

@@ -1,6 +1,6 @@
import { kinds } from "nostr-tools";
import { getOrComputeCachedValue } from "./cache.js";
import { getHiddenTags, isHiddenTagsLocked } from "./index.js";
import { getHiddenTags, isHiddenTagsUnlocked, notifyEventUpdate, unlockHiddenTags, } from "./index.js";
import { getAddressPointerFromATag, getCoordinateFromAddressPointer, getEventPointerFromETag, mergeAddressPointers, mergeEventPointers, } from "./pointers.js";
export const BookmarkPublicSymbol = Symbol.for("bookmark-public");
export const BookmarkHiddenSymbol = Symbol.for("bookmark-hidden");
@@ -63,9 +63,34 @@ export function getBookmarks(bookmark) {
export function getPublicBookmarks(bookmark) {
return getOrComputeCachedValue(bookmark, BookmarkPublicSymbol, () => parseBookmarkTags(bookmark.tags));
}
/** Returns the bookmarks of the event if its unlocked */
export function getHiddenBookmarks(bookmark) {
if (isHiddenTagsLocked(bookmark))
return undefined;
return getOrComputeCachedValue(bookmark, BookmarkHiddenSymbol, () => parseBookmarkTags(getHiddenTags(bookmark)));
/** Checks if the hidden bookmarks are unlocked */
export function isHiddenBookmarksUnlocked(bookmark) {
return isHiddenTagsUnlocked(bookmark) && Reflect.has(bookmark, BookmarkHiddenSymbol);
}
export function getHiddenBookmarks(bookmark) {
if (isHiddenBookmarksUnlocked(bookmark))
return bookmark[BookmarkHiddenSymbol];
//get hidden tags
const tags = getHiddenTags(bookmark);
if (!tags)
return undefined;
// parse bookmarks
const bookmarks = parseBookmarkTags(tags);
// set cached value
Reflect.set(bookmark, BookmarkHiddenSymbol, bookmarks);
return bookmarks;
}
/** Unlocks the hidden bookmarks on a bookmarks event */
export async function unlockHiddenBookmarks(bookmark, signer) {
if (isHiddenBookmarksUnlocked(bookmark))
return bookmark[BookmarkHiddenSymbol];
// unlock hidden tags
await unlockHiddenTags(bookmark, signer);
// get hidden bookmarks
const bookmarks = getHiddenBookmarks(bookmark);
if (!bookmarks)
throw new Error("Failed to unlock hidden bookmarks");
// notify event store
notifyEventUpdate(bookmark);
return bookmarks;
}

View File

@@ -1,6 +1,9 @@
import { NostrEvent } from "nostr-tools";
import { ExternalPointer, ExternalIdentifiers } from "./external-id.js";
import { KnownEvent } from "./index.js";
export declare const COMMENT_KIND = 1111;
/** Type for validated comment events */
export type CommentEvent = KnownEvent<typeof COMMENT_KIND>;
export type CommentEventPointer = {
type: "event";
id: string;
@@ -22,30 +25,20 @@ export type CommentExternalPointer<T extends keyof ExternalIdentifiers> = Extern
export type CommentPointer = CommentEventPointer | CommentAddressPointer | CommentExternalPointer<keyof ExternalIdentifiers>;
export declare const CommentRootPointerSymbol: unique symbol;
export declare const CommentReplyPointerSymbol: unique symbol;
/**
* Gets the EventPointer from an array of tags
* @throws
*/
/** Gets the EventPointer from an array of tags */
export declare function getCommentEventPointer(tags: string[][], root?: boolean): CommentEventPointer | null;
/**
* Gets the AddressPointer from an array of tags
* @throws
*/
/** Gets the AddressPointer from an array of tags */
export declare function getCommentAddressPointer(tags: string[][], root?: boolean): CommentAddressPointer | null;
/**
* Gets the ExternalPointer from an array of tags
* @throws
*/
/** Gets the ExternalPointer from an array of tags */
export declare function getCommentExternalPointer(tags: string[][], root?: boolean): CommentExternalPointer<keyof ExternalIdentifiers> | null;
/**
* Returns the root pointer for a comment
* @throws
*/
/** Returns the root pointer for a comment */
export declare function getCommentRootPointer(comment: CommentEvent): CommentPointer;
export declare function getCommentRootPointer(comment: NostrEvent): CommentPointer | null;
/**
* Returns the reply pointer for a comment
* @throws
*/
/** Returns the reply pointer for a comment */
export declare function getCommentReplyPointer(comment: NostrEvent): CommentPointer | null;
/** Checks if a pointer is a {@link CommentEventPointer} */
export declare function isCommentEventPointer(pointer: any): pointer is CommentEventPointer;
/** Checks if a pointer is a {@link CommentAddressPointer} */
export declare function isCommentAddressPointer(pointer: any): pointer is CommentAddressPointer;
/** Checks if a comment event is valid */
export declare function isValidComment(comment: NostrEvent): comment is CommentEvent;

View File

@@ -5,16 +5,14 @@ import { isSafeRelayURL } from "./relays.js";
export const COMMENT_KIND = 1111;
export const CommentRootPointerSymbol = Symbol.for("comment-root-pointer");
export const CommentReplyPointerSymbol = Symbol.for("comment-reply-pointer");
/**
* Gets the EventPointer from an array of tags
* @throws
*/
/** Gets the EventPointer from an array of tags */
export function getCommentEventPointer(tags, root = false) {
const eTag = tags.find((t) => t[0] === (root ? "E" : "e"));
const kind = tags.find((t) => t[0] === (root ? "K" : "k"))?.[1];
if (eTag) {
// Missing kind tag, return null
if (!kind)
throw new Error("Missing kind tag");
return null;
// only the root pubkey can be gotten from the tags, since due to quotes and mentions there will be many "p" tags for replies
const rootPubkey = root ? tags.find((t) => t[0] === "P")?.[1] : undefined;
const pointer = {
@@ -28,17 +26,15 @@ export function getCommentEventPointer(tags, root = false) {
}
return null;
}
/**
* Gets the AddressPointer from an array of tags
* @throws
*/
/** Gets the AddressPointer from an array of tags */
export function getCommentAddressPointer(tags, root = false) {
const aTag = tags.find((t) => t[0] === (root ? "A" : "a"));
const eTag = tags.find((t) => t[0] === (root ? "E" : "e"));
const kind = tags.find((t) => t[0] === (root ? "K" : "k"))?.[1];
if (aTag) {
// Missing kind tag, return null
if (!kind)
throw new Error("Missing kind tag");
return null;
const addressPointer = getAddressPointerFromATag(aTag);
const pointer = {
type: "address",
@@ -52,16 +48,10 @@ export function getCommentAddressPointer(tags, root = false) {
}
return null;
}
/**
* Gets the ExternalPointer from an array of tags
* @throws
*/
/** Gets the ExternalPointer from an array of tags */
export function getCommentExternalPointer(tags, root = false) {
const iTag = tags.find((t) => t[0] === (root ? "I" : "i"));
const kind = tags.find((t) => t[0] === (root ? "K" : "k"))?.[1];
if (iTag) {
if (!kind)
throw new Error("Missing kind tag");
return {
type: "external",
...getExternalPointerFromTag(iTag),
@@ -69,13 +59,9 @@ export function getCommentExternalPointer(tags, root = false) {
}
return null;
}
/**
* Returns the root pointer for a comment
* @throws
*/
export function getCommentRootPointer(comment) {
if (comment.kind !== COMMENT_KIND)
throw new Error("Event is not a comment");
return null;
return getOrComputeCachedValue(comment, CommentRootPointerSymbol, () => {
// check for address pointer first since it can also have E tags
const A = getCommentAddressPointer(comment.tags, true);
@@ -90,13 +76,10 @@ export function getCommentRootPointer(comment) {
return null;
});
}
/**
* Returns the reply pointer for a comment
* @throws
*/
/** Returns the reply pointer for a comment */
export function getCommentReplyPointer(comment) {
if (comment.kind !== COMMENT_KIND)
throw new Error("Event is not a comment");
return null;
return getOrComputeCachedValue(comment, CommentReplyPointerSymbol, () => {
// check for address pointer first since it can also have E tags
const A = getCommentAddressPointer(comment.tags, false);
@@ -111,15 +94,21 @@ export function getCommentReplyPointer(comment) {
return null;
});
}
/** Checks if a pointer is a {@link CommentEventPointer} */
export function isCommentEventPointer(pointer) {
return (Reflect.has(pointer, "id") &&
Reflect.has(pointer, "kind") &&
!Reflect.has(pointer, "identifier") &&
typeof pointer.kind === "number");
}
/** Checks if a pointer is a {@link CommentAddressPointer} */
export function isCommentAddressPointer(pointer) {
return (Reflect.has(pointer, "identifier") &&
Reflect.has(pointer, "pubkey") &&
Reflect.has(pointer, "kind") &&
typeof pointer.kind === "number");
}
/** Checks if a comment event is valid */
export function isValidComment(comment) {
return (comment.kind === COMMENT_KIND && getCommentRootPointer(comment) !== null && getCommentReplyPointer(comment) !== null);
}

View File

@@ -1,14 +1,23 @@
import { NostrEvent } from "nostr-tools";
import { ProfilePointer } from "nostr-tools/nip19";
import { HiddenContentSigner } from "./hidden-content.js";
export declare const ContactsRelaysSymbol: unique symbol;
export declare const PublicContactsSymbol: unique symbol;
export declare const HiddenContactsSymbol: unique symbol;
export declare function getRelaysFromContactsEvent(event: NostrEvent): Map<string, "all" | "inbox" | "outbox"> | null;
/** Type for contact events with unlocked hidden tags */
export type UnlockedContacts = {
[HiddenContactsSymbol]: ProfilePointer[];
};
export declare function getRelaysFromContactsEvent(event: NostrEvent): Map<string, "inbox" | "outbox" | "all"> | null;
/** Merges any number of contact lists into a single list */
export declare function mergeContacts(...pointers: (ProfilePointer | undefined | (ProfilePointer | undefined)[])[]): ProfilePointer[];
/** Returns all public and hidden contacts from a contacts list event */
export declare function getContacts(event: NostrEvent): ProfilePointer[];
/** Returns only the public contacts from a contacts list event */
export declare function getPublicContacts(event: NostrEvent): ProfilePointer[];
/** Checks if the hidden contacts are unlocked */
export declare function isHiddenContactsUnlocked<T extends NostrEvent>(event: T): event is T & UnlockedContacts;
/** Returns only the hidden contacts from a contacts list event */
export declare function getHiddenContacts(event: NostrEvent): ProfilePointer[] | undefined;
/** Unlocks the hidden contacts */
export declare function unlockHiddenContacts(event: NostrEvent, signer: HiddenContentSigner): Promise<ProfilePointer[]>;

View File

@@ -2,7 +2,8 @@ import { getOrComputeCachedValue } from "./cache.js";
import { isSafeRelayURL } from "./relays.js";
import { isPTag, processTags } from "./tags.js";
import { getProfilePointerFromPTag } from "./pointers.js";
import { getHiddenTags, isHiddenTagsLocked } from "./hidden-tags.js";
import { getHiddenTags, isHiddenTagsUnlocked, unlockHiddenTags } from "./hidden-tags.js";
import { notifyEventUpdate } from "./index.js";
export const ContactsRelaysSymbol = Symbol.for("contacts-relays");
export const PublicContactsSymbol = Symbol.for("public-contacts");
export const HiddenContactsSymbol = Symbol.for("hidden-contacts");
@@ -51,9 +52,35 @@ export function getContacts(event) {
export function getPublicContacts(event) {
return getOrComputeCachedValue(event, PublicContactsSymbol, () => processTags(event.tags, (t) => (isPTag(t) ? t : undefined), getProfilePointerFromPTag));
}
/** Checks if the hidden contacts are unlocked */
export function isHiddenContactsUnlocked(event) {
return isHiddenTagsUnlocked(event) && Reflect.has(event, HiddenContactsSymbol);
}
/** Returns only the hidden contacts from a contacts list event */
export function getHiddenContacts(event) {
if (isHiddenTagsLocked(event))
if (isHiddenContactsUnlocked(event))
return event[HiddenContactsSymbol];
// Get hidden tags
const tags = getHiddenTags(event);
if (!tags)
return undefined;
return getOrComputeCachedValue(event, HiddenContactsSymbol, () => processTags(getHiddenTags(event), (t) => (isPTag(t) ? t : undefined), getProfilePointerFromPTag));
// Parse tags
const contacts = processTags(tags, (t) => (isPTag(t) ? t : undefined), getProfilePointerFromPTag);
// Set cache and notify event store
Reflect.set(event, HiddenContactsSymbol, contacts);
return contacts;
}
/** Unlocks the hidden contacts */
export async function unlockHiddenContacts(event, signer) {
if (isHiddenContactsUnlocked(event))
return event[HiddenContactsSymbol];
// Unlock hidden tags
await unlockHiddenTags(event, signer);
// Get hidden contacts
const contacts = getHiddenContacts(event);
if (!contacts)
throw new Error("Failed to unlock hidden contacts");
// Set cache and notify event store
notifyEventUpdate(event);
return contacts;
}

View File

@@ -1,7 +1,7 @@
import { kinds } from "nostr-tools";
import { catchError, combineLatest, distinct, EMPTY, filter, isObservable, map, merge, mergeMap, of, switchMap, } from "rxjs";
import { logger } from "../logger.js";
import { canHaveEncryptedContent, getEncryptedContent, isEncryptedContentLocked, setEncryptedContentCache, } from "./encrypted-content.js";
import { canHaveEncryptedContent, getEncryptedContent, isEncryptedContentUnlocked, setEncryptedContentCache, } from "./encrypted-content.js";
import { notifyEventUpdate } from "./event.js";
import { getGiftWrapSeal, getSealGiftWrap, getSealRumor } from "./gift-wraps.js";
/** A symbol that is used to mark encrypted content as being from a cache */
@@ -32,7 +32,7 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
const restore = eventStore.insert$
.pipe(
// Look for events that support encrypted content and are locked
filter((e) => canHaveEncryptedContent(e.kind) && isEncryptedContentLocked(e)),
filter((e) => canHaveEncryptedContent(e.kind) && isEncryptedContentUnlocked(e) === false),
// Get the encrypted content from storage
mergeMap((event) =>
// Wait for storage to be available
@@ -52,11 +52,11 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
const restoreSeals = eventStore.update$
.pipe(
// Look for gift wraps that are unlocked
filter((e) => e.kind === kinds.GiftWrap && !isEncryptedContentLocked(e)),
filter((e) => e.kind === kinds.GiftWrap && isEncryptedContentUnlocked(e)),
// Get the seal event
map((gift) => getGiftWrapSeal(gift)),
// Look for gift wraps with locked seals
filter((seal) => seal !== undefined && isEncryptedContentLocked(seal)),
filter((seal) => seal !== undefined && isEncryptedContentUnlocked(seal) === false),
// Only attempt to unlock seals once
distinct((seal) => seal.id),
// Get encrypted content from storage
@@ -84,7 +84,7 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
.pipe(
// Look for events that support encrypted content and are unlocked and not from the cache
filter(([event]) => canHaveEncryptedContent(event.kind) &&
!isEncryptedContentLocked(event) &&
isEncryptedContentUnlocked(event) &&
!isEncryptedContentFromCache(event)),
// Only persist the encrypted content once
distinct(([event]) => event.id))
@@ -106,13 +106,13 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
const persistSeals = combineLatest([merge(eventStore.update$, eventStore.insert$), storage$])
.pipe(
// Look for gift wraps that are unlocked
filter(([event]) => event.kind === kinds.GiftWrap && !isEncryptedContentLocked(event)),
filter(([event]) => event.kind === kinds.GiftWrap && isEncryptedContentUnlocked(event)),
// Get the seal event
map(([gift, storage]) => [getGiftWrapSeal(gift), storage]),
// Make sure the seal is defined
filter(([seal]) => seal !== undefined),
// Make sure seal is unlocked and not from cache
filter(([seal]) => !isEncryptedContentLocked(seal) && !isEncryptedContentFromCache(seal)),
filter(([seal]) => isEncryptedContentUnlocked(seal) && !isEncryptedContentFromCache(seal)),
// Only persist the seal once
distinct(([seal]) => seal.id))
.subscribe(async ([seal, storage]) => {

View File

@@ -10,7 +10,12 @@ export interface EncryptedContentSigner {
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
};
}
/** Encryption method types */
export type EncryptionMethod = "nip04" | "nip44";
/** Type for an event who's encrypted content is unlocked */
export type UnlockedEncryptedContent = {
[EncryptedContentSymbol]: string;
};
/** A pair of encryption methods for encrypting and decrypting event content */
export interface EncryptionMethods {
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
@@ -37,14 +42,16 @@ export declare function hasEncryptedContent<T extends {
content: string;
}>(event: T): boolean;
/** Returns the encrypted content for an event if it is unlocked */
export declare function getEncryptedContent<T extends UnlockedEncryptedContent>(event: T): string;
export declare function getEncryptedContent<T extends object>(event: T): string | undefined;
/** Checks if the encrypted content is locked */
export declare function isEncryptedContentLocked<T extends object>(event: T): boolean;
/** Checks if the encrypted content is unlocked and casts it to the {@link UnlockedEncryptedContent} type */
export declare function isEncryptedContentUnlocked<T extends object>(event: T): event is T & UnlockedEncryptedContent;
/**
* Unlocks the encrypted content in an event and caches it
* @param event The event with content to decrypt
* @param pubkey The other pubkey that encrypted the content
* @param signer A signer to use to decrypt the content
* @throws If the event kind does not support encrypted content
*/
export declare function unlockEncryptedContent<T extends {
kind: number;

View File

@@ -1,5 +1,5 @@
import { kinds } from "nostr-tools";
import { isEvent, notifyEventUpdate } from "./event.js";
import { notifyEventUpdate } from "./event.js";
/** A symbol use to store the encrypted content of an event in memory */
export const EncryptedContentSymbol = Symbol.for("encrypted-content");
/** Various event kinds that can have encrypted content and which encryption method they use */
@@ -39,37 +39,40 @@ export function canHaveEncryptedContent(kind) {
export function hasEncryptedContent(event) {
return event.content.length > 0;
}
/** Returns the encrypted content for an event if it is unlocked */
export function getEncryptedContent(event) {
return Reflect.get(event, EncryptedContentSymbol);
}
/** Checks if the encrypted content is locked */
export function isEncryptedContentLocked(event) {
return Reflect.has(event, EncryptedContentSymbol) === false;
/** Checks if the encrypted content is unlocked and casts it to the {@link UnlockedEncryptedContent} type */
export function isEncryptedContentUnlocked(event) {
return Reflect.has(event, EncryptedContentSymbol) === true;
}
/**
* Unlocks the encrypted content in an event and caches it
* @param event The event with content to decrypt
* @param pubkey The other pubkey that encrypted the content
* @param signer A signer to use to decrypt the content
* @throws If the event kind does not support encrypted content
*/
export async function unlockEncryptedContent(event, pubkey, signer) {
if (!canHaveEncryptedContent(event.kind))
throw new Error("Event kind does not support encrypted content");
// Get the encryption methods from the signer
const encryption = getEncryptedContentEncryptionMethods(event.kind, signer);
const plaintext = await encryption.decrypt(pubkey, event.content);
// Set the cached value and trigger update
setEncryptedContentCache(event, plaintext);
// Return the decrypted content
return plaintext;
}
/** Sets the encrypted content on an event and updates it if its part of an event store */
export function setEncryptedContentCache(event, plaintext) {
Reflect.set(event, EncryptedContentSymbol, plaintext);
// if the event has been added to an event store, notify it
if (isEvent(event))
notifyEventUpdate(event);
notifyEventUpdate(event);
}
/** Removes the encrypted content cache on an event */
export function lockEncryptedContent(event) {
Reflect.deleteProperty(event, EncryptedContentSymbol);
// if the event has been added to an event store, notify it
if (isEvent(event))
notifyEventUpdate(event);
notifyEventUpdate(event);
}

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