mirror of
https://github.com/aljazceru/awesome-nostr.git
synced 2026-10-05 19:28:28 +00:00
@@ -4,23 +4,25 @@ nostr.net services [start.nostr.net](https://start.nostr.net) || [relay.nostr.ne
|
||||
**R**elays" and is an open protocol for censorship-resistant global networks.
|
||||
|
||||
## Most popular
|
||||
|
||||
### Apps
|
||||
- [Zapstore](https://zapstore.dev/) - A truly open app store [⚡ zap](lightning:zapstore@rizful.com)
|
||||
- [Hivetalk](https://honey.hivetalk.org/) - Nostr + Lightning Video Conferencing [⚡ zap](lightning:bitkarrot@primal.net)
|
||||
|
||||
### Mobile clients
|
||||
- [Damus](https://damus.io/) - The OG nostr client for iOS
|
||||
- [Amethyst](https://www.amethyst.social/) - Android-only app
|
||||
- [Primal](https://primal.net/downloads) - iOS and Android apps
|
||||
- [YakiHonne](https://yakihonne.com/yakihonne-mobile-app) - iOS and Android app
|
||||
- [Ditto](https://ditto.pub) - Android app
|
||||
- [Damus](https://damus.io/) - iOS, Android and Desktop client [⚡ zap](lightning:damus@sendsats.lol)
|
||||
- [Wisp](https://wisp.mobile/) - Android-only app [⚡ zap](lightning:utxo@rizful.com)
|
||||
- [Amethyst](https://www.amethyst.social/) - Android-only app [⚡ zap](lightning:vitor@vitorpamplona.com)
|
||||
- [Primal](https://primal.net/downloads) - iOS and Android apps [⚡ zap](lightning:miljan@primal.net)
|
||||
- [YakiHonne](https://yakihonne.com/yakihonne-mobile-app) - iOS and Android app [⚡ zap](lightning:yakihonne@getalby.com)
|
||||
|
||||
### Web clients
|
||||
- [snort.social](https://snort.social/)
|
||||
- [primal.net](https://primal.net/)
|
||||
- [coracle.social](https://coracle.social/)
|
||||
- [YakiHonne](https://yakihonne.com)
|
||||
- [Ditto](https://ditto.pub)
|
||||
|
||||
### Other
|
||||
- [Zapstore](https://zapstore.dev/) - Web of trust based app store
|
||||
- [Hivetalk](https://honey.hivetalk.org/) - Nostr + Lightning Video Conferencing
|
||||
- [primal.net](https://primal.net/) [⚡ zap](lightning:miljan@primal.net)
|
||||
- [nostria](https://nostria.app/) [⚡ zap](lightning:sondreb@rizful.com)
|
||||
- [coracle.social](https://coracle.social/) [⚡ zap](lightning:hodlbod@getalby.com)
|
||||
- [YakiHonne](https://yakihonne.com) [⚡ zap](lightning:yakihonne@getalby.com)
|
||||
- [Ditto](https://ditto.pub) [⚡ zap](lightning:alex@alexgleason.me)
|
||||
- [Jumble@Imwald](https://jumble.imwald.eu/) [⚡ zap](lightning:stellainforest@cake.cash)
|
||||
|
||||
## Protocol
|
||||
|
||||
@@ -30,7 +32,7 @@ nostr.net services [start.nostr.net](https://start.nostr.net) || [relay.nostr.ne
|
||||
- [Nostr: Solucionando la censura de una vez por todas](https://estudiobitcoin.com/nostr-solucionando-la-censura-de-una-vez-por-todas/)
|
||||
- [nostr.how](https://nostr.how) - A comprehensive non-technical beginners guide to Nostr. Available in several languages.
|
||||
- [nostr.com](https://nostr.com) - nostr getting started guide
|
||||
- [nostrich.love](https://nostrich.love/) - a normie-friendly introduction and quick start guide.
|
||||
- [nostrich.love](https://nostrich.love/) - a normie-friendly introduction and quick start guide. [⚡ zap](lightning:nostrich@wallet.yakihonne.com)
|
||||
- [NostrHub](https://nostrhub.io) - Your HUB for all things Nostr. Discover NIPs and publish your own Custom NIPs, Community forum, Nostr app directory, Nostr app GitHub repositories, DVMs, and more.
|
||||
- [nostr](https://github.com/nostr-protocol/nostr) - Overview and FAQ
|
||||
- [data-vending-machines](https://github.com/nostr-protocol/data-vending-machines) - DVM documentation and kind registry
|
||||
@@ -38,7 +40,7 @@ nostr.net services [start.nostr.net](https://start.nostr.net) || [relay.nostr.ne
|
||||
- [tinkering with nostr without any client](https://medium.com/@p2w34/tinkering-with-the-nostr-protocol-will-it-take-twitter-over-74c4bf0fea66)
|
||||
- [UseNostr](https://usenostr.org) - A small guide for anyone who wants to learn more about how nostr works and what it can do.
|
||||
- [Building Nostr](https://building-nostr.coracle.social/) - A guide for developers building on Nostr (PDF/EPUB available).
|
||||
- [persian nostr book](https://github.com/kehiy/persian-nostr-book) - a book written in the Persian language about nostr, containing nips translation.
|
||||
- [persian nostr book](https://github.com/kehiy/persian-nostr-book) - a book written in the Persian language about nostr, containing nips translation. [⚡ zap](lightning:kehiy@walletofsatoshi.com)
|
||||
- [NosTracker](https://marcodpt.github.io/nostracker/) [](https://github.com/marcodpt/nostracker) - Information about NIP support for clients, relays, tools and libraries. Like a caniuse but for NIPs.
|
||||
- [Grow Nostr Initiative](https://grownostr.org) - Empowering the future of decentralized social communication through community initiatives, education, and outreach.
|
||||
- [Marmot Protocol](https://github.com/marmot-protocol/marmot) - A messaging protocol specification for efficient end-to-end encrypted group messaging using Nostr's decentralized identity & relay network combined with the MLS Protocol.
|
||||
@@ -68,7 +70,7 @@ of the public instances.
|
||||
- [210Maxi](https://github.com/dezh-tech/ddsr/tree/main/210maxi) - A nostr relay that only accepts 210 character events (NIP-B1).
|
||||
- [Denostr](https://github.com/guakamoli/denostr) - Deno based, cloud native nostr implemention.
|
||||
- [Ephemerelay](https://gitlab.com/soapbox-pub/ephemerelay) - An in-memory Nostr relay that doesn't store data.
|
||||
- [Fenrir-s](https://github.com/rushmi0/Fenrir-s) - a Nostr Relay written in Kotlin with PostgreSQL backend and GraalVM support.
|
||||
- [Fenrir-s](https://github.com/rushmi0/Fenrir-s) - a Nostr Relay written in Kotlin with PostgreSQL backend and GraalVM support. [⚡ zap](lightning:parkinghot99@walletofsatoshi.com)
|
||||
- [gnost-relay](https://github.com/barkyq/gnost-relay)  - nostr relay written in go backed by PostgreSQL database.
|
||||
- [grain](https://github.com/0ceanslim/grain) - A highly configurable, multipurpose relay written in Go, Storing Events in a MongoDB.
|
||||
- [HAVEN](https://github.com/bitvora/haven) - Four relays in one + Blossom Media Server for a modern, sovereign and personal Nostr experience.
|
||||
@@ -92,7 +94,7 @@ of the public instances.
|
||||
- [Nostr client and relay](https://github.com/pedro-vicente/nostr_client_relay) - C++ engine that allows building Nostr applications for command line, desktop or web.
|
||||
- [nostrcheck-server](https://github.com/quentintaranpino/nostrcheck-server) - A server with relay, File Hosting, Nostr Address, Lightning Redirects, NWC and WoT.
|
||||
- [nostr_relay](https://code.pobblelabs.org/fossil/nostr_relay/) - a nostr relay written in Python, backed by SQLite
|
||||
- [nostr-filter-relay](https://github.com/atrifat/nostr-filter-relay) - A nostr relay docker image package which filter content based on content type (SFW/NSFW), user type, language, hate speech (toxic comment), sentiment, topic, and various rules.
|
||||
- [nostr-filter-relay](https://github.com/atrifat/nostr-filter-relay) - A nostr relay docker image package which filter content based on content type (SFW/NSFW), user type, language, hate speech (toxic comment), sentiment, topic, and various rules. [⚡ zap](lightning:rifat@getalby.com)
|
||||
- [nostr-relay-nestjs](https://github.com/CodyTseng/nostr-relay-nestjs) - A Nostr relay with a clear architecture and high test coverage (PostgreSQL, MeiliSearch, NestJS)
|
||||
- [nostr-relay](https://github.com/mattn/nostr-relay) - Nostr relay in Go based on relayer. Backend by sqlite3/PostgreSQL/mysql.
|
||||
- [nostr-rs-relay](https://sr.ht/~gheartsfield/nostr-rs-relay/) - a minimalistic relay written in Rust that saves data on SQLite
|
||||
@@ -156,7 +158,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [Habla.news](https://github.com/verbiricha/habla.news) - Habla allows you to read, write, curate and monetize long-form content over Nostr, a censorship-resistant protocol for social media that uses long-form Nostr content.
|
||||
- [Highlighter](https://highlighter.com) - Discover and share curated insights by people you trust.
|
||||
- [Breefly](https://breefly.social) - A low-stimulus environment where you can read articles published on nostr.
|
||||
- [Decent Newsroom](https://decentnewsroom.com/) - Explore, publish and create long form articles and magazines on nostr.
|
||||
- [Decent Newsroom](https://decentnewsroom.com/) - Explore, publish and create long form articles and magazines on nostr. [⚡ zap](lightning:decentnewsroom@rizful.com)
|
||||
- [readwithboris.com](https://www.readwithboris.com/) - Long form reading and highlighting app.
|
||||
- [blogo](https://github.com/pluja/blogo) - A simple, fast and clean blog platform powered by Nostr.
|
||||
- [blogstack.io](https://blogstack.io) - Long-form publishing platform on Nostr.
|
||||
@@ -276,7 +278,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [Jester instance](https://jesterui.github.io/)
|
||||
- [JiYou](https://github.com/TimA314/JiYou) - A nostr PWA client with a highly customizable UI
|
||||
- [JiYou.social](https://JiYou.social) - live instance
|
||||
- [Jumble](https://github.com/CodyTseng/jumble) - A web client focused on browsing relay feeds
|
||||
- [Jumble](https://github.com/CodyTseng/jumble) - A web client focused on browsing relay feeds [⚡ zap](lightning:codytseng@getalby.com)
|
||||
- [jumble.social](https://jumble.social/) - live instance
|
||||
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub) - A nostr daemon for Lightning nodes
|
||||
- [Listr](https://listr.lol/) - Curate, organize and discover everything Nostr has to offer. Listr is the best way to create, manage, and browse Nostr lists.
|
||||
@@ -304,7 +306,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [ni.py](https://github.com/0n4t3/nipy) - CLI post only client for Nostr, Activity Pub, and the AT Protocol written in Python.
|
||||
- [Noflux](https://github.com/fiatjaf/noflux) - minimalist and opinionated feed reader
|
||||
- [Nootti](https://nootti.com) - Cross-posting iOS & iPad client app for Bluesky, Mastodon and Nostr
|
||||
- [Nos](https://github.com/planetary-social/nos)  - A UX-focused iOS / MacOS native app focused on a polished new user experience. [planetary.social](https://planetary.social)
|
||||
- [Nos](https://github.com/planetary-social/nos)  - A UX-focused iOS / MacOS native app focused on a polished new user experience. [planetary.social](https://planetary.social) [⚡ zap](lightning:strongsnail1@primal.net)
|
||||
- [nos.social](https://nos.social)
|
||||
- [nosbin](https://nosbin.com/) - pastebin over nostr
|
||||
- [noscl](https://github.com/fiatjaf/noscl) - a basic command-line client written in Go
|
||||
@@ -337,18 +339,18 @@ Websites with lists of relays and their performance/health:
|
||||
- [Nostrord](https://github.com/nostrord/nostrord) - A cross-platform NIP-29 group chat client built with Kotlin Multiplatform & Compose Multiplatform.
|
||||
- [Nostros](https://github.com/KoalaSat/nostros) - A nostr mobile client for Android
|
||||
- [Nostrtium](https://github.com/pjv/nostrtium) - Post to Nostr directly from within WordPress
|
||||
- [noStrudel](https://github.com/hzrd149/nostrudel) - hzrd149's personal nostr client.
|
||||
- [noStrudel](https://github.com/hzrd149/nostrudel) - hzrd149's personal nostr client. [⚡ zap](lightning:nostrudel@npub.cash)
|
||||
- [nostrudel.ninja](https://nostrudel.ninja/) - live instance
|
||||
- [nostrweb](https://git.qcode.ch/nostr/nostrweb) - another Nostr web client in vanilla JS
|
||||
- [nostr.ch](https://nostr.ch/) - live instance
|
||||
- [nostter](https://github.com/SnowCait/nostter) - Twitter-like web client
|
||||
- [nostui](https://github.com/akiomik/nostui) - A TUI client for Nostr
|
||||
- [Nostur](https://nostur.com) - Nostr client app for iOS and MacOS. ([Apple Store link](https://apps.apple.com/us/app/nostur/id1672780508))
|
||||
- [Nostur](https://nostur.com) - Nostr client app for iOS and MacOS. ([Apple Store link](https://apps.apple.com/us/app/nostur/id1672780508)) [⚡ zap](lightning:weathereddarkness25@getalby.com)
|
||||
- [Notedeck](https://github.com/damus-io/notedeck) - Rust based desktop nostr browser by Damus
|
||||
- [notestack.com](https://notestack.com) - Blogging site for nostr, supports markdown
|
||||
- [NOW - Nostr Outbox for Wordpress](https://github.com/Mnpezz/nostr-outbox-for-wordpress/tree/main) - Send WordPress and WooCommerce notifications via Nostr instead of email. Includes Lightning payments at checkout, Nostr login, NIP-05 verification, and encrypted direct messaging.
|
||||
- [npub.world](https://npub.world/) - A nostr profile search engine
|
||||
- [Nuxstr](https://github.com/sebastix/nuxstr) - Starter template for building a Nostr client with the Nuxt Vue framework and Nostr Dev Kit (NDK).
|
||||
- [Nuxstr](https://github.com/sebastix/nuxstr) - Starter template for building a Nostr client with the Nuxt Vue framework and Nostr Dev Kit (NDK). [⚡ zap](lightning:sebastian@lnd.sebastix.com)
|
||||
- [nuxstr.nostrver.se](https://nuxstr.nostrver.se) - demo
|
||||
- [n_cord](https://github.com/0n4t3/n_cord) - A Discord inspired chat style client standard Nostr notes, built in static HTML & JavaScript.
|
||||
- [0n4t3.github.io/n_cord/](https://0n4t3.github.io/n_cord/) - live instance
|
||||
@@ -402,7 +404,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [u-blog.vercel.app](https://u-blog.vercel.app)
|
||||
- [Vector](https://github.com/VectorPrivacy/Vector) - A privacy-focused encrypted messenger built on Nostr using NIP-17, with zero metadata leakage. Available for Windows, macOS, Linux, and Android.
|
||||
- [vectorapp.io](https://vectorapp.io)
|
||||
- [Vega](https://github.com/hoornet/vega) - A client for Linux, Windows and macOS (Tauri + React), with long-form writing and reading (NIP-23), V4V podcast streaming, WoT filtering and an optional built-in relay.
|
||||
- [Vega](https://github.com/hoornet/vega) - A client for Linux, Windows and macOS (Tauri + React), with long-form writing and reading (NIP-23), V4V podcast streaming, WoT filtering and an optional built-in relay. [⚡ zap](lightning:jure@getalby.com)
|
||||
- [veganostr.com](https://veganostr.com/)
|
||||
- [Votestr](https://votestr.com/) - Poll web app with nostr authentication and blind signature unlinkability
|
||||
- [Voyage](https://github.com/dluvian/voyage) - Lightweight Android client with a Reddit-like UI
|
||||
@@ -418,7 +420,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [Yana](https://github.com/frnandu/yana) - Yana is a nostr client focused on performance in slower devices and modularity of features.
|
||||
- [yana.do](https://yana.do)
|
||||
- [ygege](https://github.com/UwUDev/ygege) - Support Tor High-performance torrents indexer for services using the U2P system written in Rust.
|
||||
- [Zap Cooking](https://zap.cooking/) - Nostr's only recipe-sharing client. A place where food culture can live openly and grow naturally.
|
||||
- [Zap Cooking](https://zap.cooking/) - Nostr's only recipe-sharing client. A place where food culture can live openly and grow naturally. [⚡ zap](lightning:zapcooking@sats.zap.cooking)
|
||||
- [zapddit](https://zapddit.com)  - Reddit-style client for nostr where you can follow topics (similar to subreddits), mute certain topics (like #bitcoin), upzap and downzap notes using your hard-earned sats.
|
||||
- [Zaplytics](https://github.com/derekross/zaplytics) - Analytics for your Nostr Zaps, helping creators dive into their Zap earnings. [Zaplytics.app](https://zaplytics.app/)
|
||||
- [Zappix](https://github.com/derekross/zappix) - A nostr image sharing application for browsing, sharing, and zapping visual content. [Zappix.app](https://zappix.app/)
|
||||
@@ -465,7 +467,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [Shopstr](https://github.com/shopstr-eng/shopstr) - The Lightning and Cashu-native self-sovereign marketplace built on top of NIP-99 includes fixed-price items
|
||||
- [Kartapio](https://github.com/vstabile/kartapio) - A Nostr-based marketplace.
|
||||
- [Milk Market](https://milk.market) - A Nostr-native marketplace.
|
||||
- [Mostro Mobile](https://github.com/MostroP2P/mobile) - Mobile client for the Mostro P2P exchange platform.
|
||||
- [Mostro Mobile](https://github.com/MostroP2P/mobile) - Mobile client for the Mostro P2P exchange platform. [⚡ zap](lightning:mostro_p2p@sats.mobi)
|
||||
|
||||
### Web Bookmarking ([NIP-B0](https://github.com/nostr-protocol/nips/blob/master/B0.md))
|
||||
- [Community Curated Nostr Stuff (CCNS)](https://github.com/Sebastix/CCNS)![stars] - CCNS is a Nostr-focused community platform centered around link aggregation and discussion topics.
|
||||
@@ -501,7 +503,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [nip07-awaiter](https://github.com/penpenpng/nip07-awaiter) - Minimal utility to access NIP-07 interface safely.
|
||||
- [nmostr](https://github.com/Gruruya/nmostr) - a Nim library for Nostr
|
||||
- [NNostr.Client](https://github.com/Kukks/NNostr) - a C# Nostr library for use by clients
|
||||
- [noscrypt](https://github.com/vnuge/noscrypt) - A C89 library for nostr specific cryptography operations.
|
||||
- [noscrypt](https://github.com/vnuge/noscrypt) - A C89 library for nostr specific cryptography operations. [⚡ zap](lightning:chiptuner@coinos.io)
|
||||
- [nostr (zig-nostr)](https://github.com/zig-nostr/nostr) - A fast Nostr protocol library for Zig. Native libsecp256k1 signing and a memory-mapped LMDB store give zero-copy, local-first reads with sub-millisecond feed queries. Covers keys and events, encrypted messaging and remote signing (NIP-44/46/49), and a relay transport.
|
||||
- [nostr_rust](https://github.com/0xtlt/nostr_rust) - Functional Rust implementation of the nostr protocol
|
||||
- [Nostrbook](https://nostrbook.dev/) - A Comprehensive Registry of Nostr Documentation. Structured, detailed knowledge about Nostr for both humans and AI.
|
||||
@@ -513,7 +515,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [nostr-double-ratchet](https://github.com/mmalmi/nostr-double-ratchet) - Signal-style secure private messaging channels
|
||||
- [nostr-fetch](https://github.com/jiftechnify/nostr-fetch) - A utility library that allows JS/TS apps to effortlessly fetch past events from Nostr relays.
|
||||
- [nostr-geotags](https://github.com/sandwichfarm/nostr-geotags) - NPM module that generates geotags for nostr events given data points such as GPS/DD, an ISO-3166-1 countryCode and/or an ISO-3166-2 regionName, as well as many passthroughts. Supports ISO-3166-1/2/3
|
||||
- [nostr-hooks](https://github.com/ostyjs/nostr-hooks) - React hooks for developing Nostr clients. It's simple and intelligent.
|
||||
- [nostr-hooks](https://github.com/ostyjs/nostr-hooks) - React hooks for developing Nostr clients. It's simple and intelligent. [⚡ zap](lightning:sepehr@getalby.com)
|
||||
- [nostr-java](https://github.com/tcheeric/nostr-java) - A nostr client library, written in java, for generating, signing and publishing events.
|
||||
- [nostr-js](https://github.com/jb55/nostr-js) - a javascript implementation of the nostr protocol
|
||||
- [nostr_relay_management](https://github.com/anasfik/nostr_relay_management) - Dart/Flutter support for NIP-86 (Relay Management) with better developer experience.
|
||||
@@ -598,7 +600,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [bray](https://github.com/forgesworn/bray) - Trust-aware Nostr MCP server for AI agents. 234 tools across identity, social, trust, dispatch, relay, marketplace, privacy, and encrypted access. Works with Claude, ChatGPT, Gemini, Cursor, or any MCP client.
|
||||
- [Bookmarkstr](https://bookmarkstr.store/) - A browser extension for reading and managing nostr bookmarks
|
||||
- [BlazeJump.NostrConnect](https://github.com/drmikesamy/BlazeJump.NostrConnect) - an Android based remote NostrConnect signer and web project using QR codes and Nip44 encryption by default
|
||||
- [cafe-society.news](https://github.com/colealbon/cafe-society) - Self-sovereign machine learning training tool for nostr global feed. Trained models can be monetized/distributed over nostr private messages. Your pre-paid model subscribers register via lightning payments.
|
||||
- [cafe-society.news](https://github.com/colealbon/cafe-society) - Self-sovereign machine learning training tool for nostr global feed. Trained models can be monetized/distributed over nostr private messages. Your pre-paid model subscribers register via lightning payments. [⚡ zap](lightning:cafe@getalby.com)
|
||||
- [CSV Importer](https://csv-importer.coracle.social/) - Publish Nostr events in bulk from CSV files.
|
||||
- [Chief](https://github.com/0xtrr/chief) - A Strfry write policy plugin that provides blacklists for public keys, event kinds and words/sentences.
|
||||
- [Cloud Seeder](https://github.com/ipv6rslimited/cloudseeder) - A 1-click deployment and management tool for nostr-rs-relay and other appliances.
|
||||
@@ -661,7 +663,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [nostr-cln-events](http://git.jb55.com/nostr-cln-events) - A CLN plugin to push clightning node events to nostr
|
||||
- [nostr-commander](https://github.com/8go/nostr-commander-rs) - simple but convenient CLI-based Nostr app for following users, sending DMs, etc.
|
||||
- [nostr-components](https://github.com/saiy2k/nostr-components) - Nostr Components makes it easy to embed Nostr profiles, posts, follow buttons, Live chat box, comment section, DM buttons in any website
|
||||
- [NostrComments](https://github.com/briskness-byte/NostrComments) - Browser extension adding a comment thread to any web page, stored on relays rather than on the site being discussed, so the page owner cannot remove it. Chrome, Firefox and userscript.
|
||||
- [NostrComments](https://github.com/briskness-byte/NostrComments) - Browser extension adding a comment thread to any web page, stored on relays rather than on the site being discussed, so the page owner cannot remove it. Chrome, Firefox and userscript. [⚡ zap](lightning:slurpnc@coinos.io)
|
||||
- [nostr-crdt](https://github.com/YousefED/nostr-crdt)  - Use Nostr for collaborative, decentralized, local-first applications with nostr-CRDT Yjs provider.
|
||||
- [nostr-delete](https://github.com/blakejakopovic/nostr_delete) - generate delete events requesting relays drop and delete content you've published. Blasts out delete requests to many relays.
|
||||
- [nostr-delete web app](https://nostr-delete.vercel.app/)
|
||||
@@ -685,7 +687,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [nostr.json generator](https://github.com/SnowCait/nostr-json-generator) - Generate NIP-05 nostr.json from NIP-65 or NIP-07
|
||||
- [NostrAirTracker](https://github.com/gourcetools/NostrAirTracker) - Elonjet on Nostr
|
||||
- [nostraap](https://github.com/ccs-mz/nostraap) - NostraApp is a decentralized messaging application inspired by WhatsApp, built on the Nostr protocol, with native support for end-to-end encryption, distributed relay networks, and Bitcoin Lightning payments (Zaps)
|
||||
- [nostress](https://github.com/4383/nostress) - ⚡ Modern Python CLI for Nostr protocol interactions - secure key management, event creation, and relay operations
|
||||
- [nostress](https://github.com/4383/nostress) - ⚡ Modern Python CLI for Nostr protocol interactions - secure key management, event creation, and relay operations [⚡ zap](lightning:hberaud@nostrcheck.me)
|
||||
- [nostrame](https://github.com/Anderson-Juhasc/nostrame) - Nostr Signer and Account Management Extension
|
||||
- [nostrandom.netlify.app](https://nostrandom.netlify.app/) - generate publish-able Nostr event with random keys
|
||||
- [nostrefresh](https://github.com/melvincarvalho/nostrefresh) - simple refresh function for nostr web pages
|
||||
@@ -708,7 +710,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [Note Nuke](https://www.mutable.top/note-nuke) - A standalone feature of Mutable that lets users delete an event with maximum relay coverage.
|
||||
- [novia](https://github.com/teamnovia/novia) - Nostr video archive
|
||||
- [nsecbunkerd](https://github.com/kind-0/nsecbunkerd) - Keep your nostr keys in a single place and provide fine-grained access to your team members.
|
||||
- [Obsidian Nostr Writer](https://github.com/jamesmagoo/nostr-writer#readme) - A free Obsidian plugin that allows writers to seamlessly publish to Nostr
|
||||
- [Obsidian Nostr Writer](https://github.com/jamesmagoo/nostr-writer#readme) - A free Obsidian plugin that allows writers to seamlessly publish to Nostr [⚡ zap](lightning:magoo@getalby.com)
|
||||
- [osint-user-discovery](https://github.com/Inforensics/osint-user-discovery) - OSINT Discovery is a set of Python scripts designed to search for users or URLs across different social media platforms(nostr, mastodon) and caching services.
|
||||
- [Plebs vs. Zombies](https://www.plebsvszombies.cc) - A Nostr utility for managing dormant follows and cleaning up your follow list.
|
||||
- [powrelay.xyz](https://powrelay.xyz) - A proof of work-based nostr explorer.
|
||||
@@ -727,7 +729,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [Spamster](https://github.com/gourcetools/spamster) - Spamster is for testing relays and spam filters.
|
||||
- [strfry policies](https://gitlab.com/soapbox-pub/strfry-policies) - A collection of moderation & antispam policies for the strfry relay developed in TypeScript/Deno.
|
||||
- [TaskQ5](https://github.com/duozhutuan/Taskq5) - TaskQ5 is a task distribution platform built on nostr where you can post tasks if you need help
|
||||
- [toll-booth](https://github.com/forgesworn/toll-booth) - HTTP 402 payment middleware with NWC backend and Cashu support. Companion tools: toll-booth-announce (kind 31402 service discovery), toll-booth-dvm (NIP-90 DVM bridge), 402-mcp (AI agent L402 client).
|
||||
- [toll-booth](https://github.com/forgesworn/toll-booth) - HTTP 402 payment middleware with NWC backend and Cashu support. Companion tools: toll-booth-announce (kind 31402 service discovery), toll-booth-dvm (NIP-90 DVM bridge), 402-mcp (AI agent L402 client). [⚡ zap](lightning:profusemeat89@walletofsatoshi.com)
|
||||
- [tostr](https://github.com/slaninas/tostr) - a twitter to nostr bot
|
||||
- [Undelete my Nostr](https://yonle.github.io/undelete-my-nostr) - Simple tool for restoring deleted nostr account.
|
||||
- [y-ndk](https://github.com/colealbon/y-ndk) - yjs provider over nostr relays
|
||||
@@ -788,7 +790,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [nanostr](https://github.com/xbol0/nanostr) - A NIP-05 name server written in Deno.
|
||||
- [Nostr-Check.com](https://nostr-check.com) - A free NIP-05 ID registration service.
|
||||
- [NostrAddress.com](https://nostraddress.com) - Free and Paid Nostr Address (NIP-05) identifier service with premium relay and vanity domains.
|
||||
- [nostrcheck.me](https://nostrcheck.me/) - A free NIP-05 ID (nostr address) registration service, lightning redirection and media uploads.
|
||||
- [nostrcheck.me](https://nostrcheck.me/) - A free NIP-05 ID (nostr address) registration service, lightning redirection and media uploads. [⚡ zap](lightning:public@nostrcheck.me)
|
||||
- [Nostrich House](https://nostrich.house) - Paid NIP-05 service with nostr bot interface. Buy your nostr address with DM to nostrich@nostrich.house for 1 sat/hour, anonymous, immediate.
|
||||
- [nostrich.love](https://uselessshit.co/nostr/nip-05/) - A Nostr Address registration service.
|
||||
- [nostrplebs](https://nostrplebs.com) - The oldest and original Nostr address registration and identity management service.
|
||||
@@ -796,7 +798,7 @@ Websites with lists of relays and their performance/health:
|
||||
- [pleroma2nip05](https://code.taurix.net/guy/pleroma2nip05) - A Python based service to link pleroma ID's to nostr keys.
|
||||
- [siamstr.com](https://siamstr.com) - A free NIP-05 ID registration service, lightning redirection.
|
||||
- [younostr.com](https://younostr.com) - A NIP-05 ID registration service (in portuguese).
|
||||
- [hunos.hu](https://hunos.hu) - Free NIP-05 identity for the Hungarian community.
|
||||
- [hunos.hu](https://hunos.hu) - Free NIP-05 identity for the Hungarian community. [⚡ zap](lightning:hunosrelay@minibits.cash)
|
||||
- [zap.club](https://zap.club) - NIP-05 ID service for @zap.club handles.
|
||||
- [zaps.lol](https://zaps.lol)- A free and open source NIP-05 ID registration service. [Run your own](https://github.com/jigglycrumb/nostr-address-provider).
|
||||
|
||||
@@ -928,7 +930,7 @@ Endpoints (services or apps that expose a LN wallet via NWC)
|
||||
- [Wallet of Satoshi](https://www.walletofsatoshi.com/) - Custodial lightning wallet
|
||||
- [zap_server](https://github.com/UTXOnly/zap_server) - An LNURL server to recieve zaps to tor hosted node and generate kind 9735 zap receipt events
|
||||
- [Zebedee app](https://zebedee.io/app) - Zebedee's wallet/lightning app
|
||||
- [ZeusLN](https://github.com/ZeusLN/zeus) - A mobile Bitcoin/Lightning app for LND, Core Lightning, and Eclair node operators
|
||||
- [ZeusLN](https://github.com/ZeusLN/zeus) - A mobile Bitcoin/Lightning app for LND, Core Lightning, and Eclair node operators [⚡ zap](lightning:tips@pay.zeusln.app)
|
||||
- [CDK](https://cashudevkit.org) - Cashu Development Kit for building ecash wallets and mints.
|
||||
- [eNuts](https://enuts.cash) - Cross-platform Cashu ecash wallet with Nostr integration.
|
||||
- [Minibits](https://github.com/minibits-cash/minibits_wallet) - Mobile Cashu ecash wallet with Lightning and Nostr.
|
||||
@@ -961,10 +963,10 @@ Endpoints (services or apps that expose a LN wallet via NWC)
|
||||
## NIP-96 File Storage Servers
|
||||
- [mockingyou.com](https://mockingyou.com)
|
||||
- [nostpic](https://nostpic.com)
|
||||
- [nostr.build](https://nostr.build)
|
||||
- [nostr.build](https://nostr.build) [⚡ zap](lightning:nostrbob@primal.net)
|
||||
- [nostrcheck.me](https://nostrcheck.me)
|
||||
- [nostrage](https://nostrage.com)
|
||||
- [NostrMedia.com](https://nostrmedia.com)
|
||||
- [NostrMedia.com](https://nostrmedia.com) [⚡ zap](lightning:b0cc79@wallet.yakihonne.com)
|
||||
- [sovbit](https://files.sovbit.host)
|
||||
- [sove](https://sove.rent)
|
||||
- [void.cat](https://void.cat)
|
||||
@@ -1085,7 +1087,7 @@ Outside of nostr itself, you find the community on:
|
||||
- [Tracker for Undocumented Nostr Event Kinds](https://undocumented.nostrkinds.info/) - monitors undocumented event kinds in the nostr network
|
||||
- [vanilla-js-nostr](https://github.com/supertestnet/vanilla-js-nostr) - a demo of posting and viewing a feed in nostr using vanilla javascript
|
||||
- [wellorder nostr datasets](https://wiki.wellorder.net/wiki/nostr-datasets/) - Public standardized nostr datasets for benchmarking, data science, or other analysis.
|
||||
- [Zaplife](https://zaplife.lol) - Real-time feed for nostr zaps. The best tool to shut up the "lightning doesn't work" people.
|
||||
- [Zaplife](https://zaplife.lol) - Real-time feed for nostr zaps. The best tool to shut up the "lightning doesn't work" people. [⚡ zap](lightning:pablof7z@primal.net)
|
||||
- [The Attention Protocol](https://joinnextblock.com/billboard/book) - a love story published on nostr
|
||||
- [AndOtherStuff](https://andotherstuff.org/) - a collective that creates and stewards freedom tech, open tools, protocols, and applications.
|
||||
|
||||
@@ -1109,7 +1111,7 @@ Outside of nostr itself, you find the community on:
|
||||
- [nostore](https://github.com/ursuscamp/nostore) - Nostr Signer Extension for iOS/macOS Safari
|
||||
- [nostrpy](https://github.com/monty888/nostrpy) - relay, client, and other tooling in python (No longer being developed.)
|
||||
- [Nozzle](https://github.com/dluvian/Nozzle) - A lightweight Android client
|
||||
- [Plebstr](https://plebstr.com) - Nostr client Reimagined, the most beautiful Twitter-like nostr client for iOS & Android.
|
||||
- [Plebstr](https://plebstr.com) - Nostr client Reimagined, the most beautiful Twitter-like nostr client for iOS & Android. [⚡ zap](lightning:passivebit94@walletofsatoshi.com)
|
||||
- [rsslay](https://github.com/piraces/rsslay) - fork of the rsslay by @fiatjaf. a bridge that puts RSS feeds into Nostr optimized, more funcionalities and UI improvements. Live at [rsslay.nostr.moe](https://rsslay.nostr.moe/)
|
||||
- [Nsec App](https://nsec.app) - Browser-based NIP-46 bunker that lets you sign events without exposing your nsec.
|
||||
- [nostrapp.link](https://nostrapp.link/) - Nostr App Manager
|
||||
@@ -1120,6 +1122,14 @@ Outside of nostr itself, you find the community on:
|
||||
|
||||
If you'd like to add something to this list, please submit a [Pull Request on GitHub](https://github.com/aljazceru/awesome-nostr/).
|
||||
|
||||
Project maintainers can make their entry zappable by appending a lightning chip (and optionally a donate link) to their entry — visitors can then zap it directly from [nostr.net/grant](https://nostr.net/grant/):
|
||||
|
||||
```
|
||||
- [Your Project](https://example.com) - What it does [⚡ zap](lightning:you@wallet.com) [donate](https://geyser.fund/project/xyz)
|
||||
```
|
||||
|
||||
Only submit an address you control or that is officially published by the project.
|
||||
|
||||
This directory is maintained by [aljaz](https://disobey.dev/contact/). Your contributions help keep this information up-to-date and valuable.
|
||||
|
||||
## Contributors
|
||||
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
---
|
||||
permalink: /grant/
|
||||
---
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>grantless · zap the projects building nostr | nostr.net</title>
|
||||
<meta name="description" content="Zap-funded, grantless support for nostr projects. Connect your wallet, pick projects, zap them all — with NIP-57 zap receipts.">
|
||||
<link rel="icon" type="image/png" href="Logo.png">
|
||||
<link rel="apple-touch-icon" href="Logo.png">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
|
||||
|
||||
:root {
|
||||
--background: #fdf8f3;
|
||||
--card: #ffffff;
|
||||
--text: #2d1b3d;
|
||||
--text-muted: #8b7e97;
|
||||
--border: #ecdfe8;
|
||||
--zap: #f7931a;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #191020;
|
||||
--card: #241b2e;
|
||||
--text: #ede8f5;
|
||||
--text-muted: #8b7e97;
|
||||
--border: #342a40;
|
||||
}
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.brand img { width: 28px; height: 28px; border-radius: 6px; }
|
||||
|
||||
.back {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.back:hover { color: var(--zap); }
|
||||
|
||||
main {
|
||||
max-width: 720px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 1.5rem 4rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: -0.03em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
h1 .fa-bolt { color: var(--zap); }
|
||||
|
||||
.tagline {
|
||||
color: var(--text-muted);
|
||||
margin: 0.75rem 0 2rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
background: rgba(247, 147, 26, 0.08);
|
||||
border: 1px dashed rgba(247, 147, 26, 0.45);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
font-size: 0.92rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 2rem 0 1rem;
|
||||
}
|
||||
|
||||
.zap-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.zap-row:hover {
|
||||
border-color: var(--zap);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(247, 147, 26, 0.15);
|
||||
}
|
||||
|
||||
.zap-row strong { font-size: 0.98rem; }
|
||||
|
||||
.zap-row .what {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
|
||||
.zap-row code {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
background: rgba(247, 147, 26, 0.06);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.zap-row .fa-bolt { color: var(--zap); margin-left: auto; }
|
||||
|
||||
pre {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
font-size: 0.82rem;
|
||||
overflow-x: auto;
|
||||
margin: 0.5rem 0 1rem;
|
||||
}
|
||||
|
||||
a.pr-link { color: var(--zap); text-decoration: none; font-weight: 600; }
|
||||
a.pr-link:hover { text-decoration: underline; }
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a class="brand" href="/">
|
||||
<img src="Logo.png" alt="nostr.net logo">
|
||||
<span>.NET</span>
|
||||
</a>
|
||||
<a class="back" href="/"><i class="fas fa-arrow-left" aria-hidden="true"></i> back to the list</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<h1><i class="fas fa-bolt" aria-hidden="true"></i> grantless</h1>
|
||||
<p class="tagline">Zap-funded nostr. No gatekeepers, no grant committees — the list is the funding platform.</p>
|
||||
|
||||
<div class="status">
|
||||
<i class="fas fa-hammer" aria-hidden="true"></i>
|
||||
The batch-zap UI is being built: connect your wallet via NWC (Bitcoin Connect), tick any number of
|
||||
projects, and zap them all in one go — with full NIP-57 zap receipts. It lands on this page soon.
|
||||
</div>
|
||||
|
||||
<h2>Meanwhile, zap directly</h2>
|
||||
|
||||
<a class="zap-row" href="lightning:aljaz@minibits.cash">
|
||||
<strong>nostr.net</strong>
|
||||
<span class="what">curator & host — relay and list maintenance</span>
|
||||
<code>aljaz@minibits.cash</code>
|
||||
<i class="fas fa-bolt" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
<a class="zap-row" href="lightning:bitkarrot@primal.net">
|
||||
<strong>Hivetalk</strong>
|
||||
<span class="what">Nostr + Lightning Video Conferencing</span>
|
||||
<code>bitkarrot@primal.net</code>
|
||||
<i class="fas fa-bolt" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
<h2>Project maintainer?</h2>
|
||||
<p>Add a zap chip to your entry on the list via pull request:</p>
|
||||
<pre>- [Your Project](https://example.com) - What it does [⚡ zap](lightning:you@wallet.com)</pre>
|
||||
<p><a class="pr-link" href="https://github.com/aljazceru/awesome-nostr/">Submit a PR on GitHub →</a></p>
|
||||
</main>
|
||||
|
||||
<footer>© 2025 nostr.net</footer>
|
||||
</body>
|
||||
</html>
|
||||
+15
@@ -61,6 +61,12 @@
|
||||
<option value="dracula">Dracula</option>
|
||||
</select>
|
||||
-->
|
||||
<bc-button id="bcConnect"></bc-button>
|
||||
<a id="fundButton" class="fund-toggle" href="lightning:aljaz@minibits.cash"
|
||||
title="Fund nostr.net ⚡ aljaz@minibits.cash"
|
||||
aria-label="Fund nostr.net with a Lightning zap">
|
||||
<i class="fas fa-bolt"></i>
|
||||
</a>
|
||||
<button id="darkModeToggle" class="theme-toggle" aria-label="Toggle dark mode">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
@@ -89,6 +95,11 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content" role="main">
|
||||
<a class="fund-banner" href="#zappable-projects">
|
||||
<i class="fas fa-bolt" aria-hidden="true"></i>
|
||||
<span class="fund-banner-text"><strong>grantless</strong> — zap the projects building nostr. Every project with a ⚡ chip is zappable, right here on the list.</span>
|
||||
<span class="fund-banner-cta">Zappable projects <i class="fas fa-arrow-right" aria-hidden="true"></i></span>
|
||||
</a>
|
||||
<header class="content-header">
|
||||
<div class="view-controls">
|
||||
<!-- Dark mode toggle has been moved to top nav -->
|
||||
@@ -103,8 +114,12 @@
|
||||
<p>© 2025 nostr.net All rights reserved.</p>
|
||||
</footer>
|
||||
<!-- Add marked.js before your script -->
|
||||
<script type="module">
|
||||
import 'https://esm.sh/@getalby/bitcoin-connect@3.12.3';
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="script.js"></script>
|
||||
<script src="zap.js"></script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-E6V2R7B0KQ"></script>
|
||||
|
||||
@@ -334,6 +334,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// grantless: banner jumps to the Zappable projects section
|
||||
const fundBanner = document.querySelector('.fund-banner');
|
||||
if (fundBanner) {
|
||||
fundBanner.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const link = document.querySelector(`.nav-links a[data-section="${ZAPPABLE_SECTION}"]`);
|
||||
if (link) link.click();
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure we're working with valid elements
|
||||
if (!sidebar || !menuToggle) {
|
||||
console.error('Required elements not found');
|
||||
@@ -537,34 +547,20 @@ function searchResourceList(ulElement, container, searchTerm, sectionName) {
|
||||
Array.from(ulElement.children).forEach(li => {
|
||||
// Search in the main item if it has a link
|
||||
if (li.querySelector(':scope > a')) {
|
||||
const resourceName = li.querySelector(':scope > a')?.textContent || '';
|
||||
const resourceLink = li.querySelector(':scope > a')?.href || '';
|
||||
const description = li.textContent
|
||||
.replace(resourceName, '') // Remove the resource name
|
||||
.replace(/^\s*-\s*/, '') // Remove leading dash
|
||||
.replace(/\s*\[!\[.*?\]\(.*?\)\]\(.*?\)\s*/, '') // Remove GitHub stars badge if present
|
||||
.trim();
|
||||
|
||||
const searchableText = [resourceName, description, resourceLink]
|
||||
const resource = parseListItem(li);
|
||||
const searchableText = [resource.name, resource.description, resource.link]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
|
||||
if (searchableText.includes(searchTerm)) {
|
||||
const card = createResourceCard({
|
||||
name: resourceName,
|
||||
link: resourceLink,
|
||||
description: description,
|
||||
stars: li.querySelector(':scope > img[alt="stars"]')
|
||||
? parseInt(li.querySelector(':scope > img[alt="stars"]').src.match(/stars\/(\d+)/)?.[1]) || 0
|
||||
: 0
|
||||
});
|
||||
|
||||
const card = createResourceCard(resource);
|
||||
|
||||
// Add section label to card
|
||||
const sectionLabel = document.createElement('div');
|
||||
sectionLabel.className = 'category-label';
|
||||
sectionLabel.textContent = sectionName;
|
||||
card.insertBefore(sectionLabel, card.firstChild);
|
||||
|
||||
|
||||
container.appendChild(card);
|
||||
}
|
||||
}
|
||||
@@ -762,6 +758,86 @@ function parseResourceLine(line) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== grantless: zap/donate chip support =====
|
||||
// Reads a rendered markdown <li> and extracts the resource link plus the
|
||||
// optional chips from the README convention:
|
||||
// - [Name](https://...) - description [⚡ zap](lightning:addr@host) [donate](https://...)
|
||||
const LIGHTNING_SCHEME_RE = /^lightning:/i;
|
||||
|
||||
function parseListItem(li) {
|
||||
let name = '';
|
||||
let link = '';
|
||||
let lnAddress = null;
|
||||
let donateUrl = null;
|
||||
|
||||
Array.from(li.querySelectorAll('a')).forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const label = a.textContent.trim().toLowerCase();
|
||||
if (LIGHTNING_SCHEME_RE.test(href)) {
|
||||
if (lnAddress === null) lnAddress = href.replace(LIGHTNING_SCHEME_RE, '');
|
||||
} else if (label === 'donate' && /^https?:/i.test(href)) {
|
||||
if (donateUrl === null) donateUrl = href;
|
||||
} else if (!name) {
|
||||
name = a.textContent.trim();
|
||||
link = href;
|
||||
}
|
||||
});
|
||||
|
||||
// Description = li text minus the name, chips, stars badge and nested lists
|
||||
const clone = li.cloneNode(true);
|
||||
clone.querySelectorAll('ul').forEach(u => u.remove());
|
||||
clone.querySelectorAll('img').forEach(i => i.remove());
|
||||
clone.querySelectorAll('a').forEach(a => {
|
||||
const href = a.getAttribute('href') || '';
|
||||
const label = a.textContent.trim().toLowerCase();
|
||||
if (LIGHTNING_SCHEME_RE.test(href) || label === 'donate') a.remove();
|
||||
});
|
||||
let description = clone.textContent.replace(/\s+/g, ' ').trim();
|
||||
if (name && description.toLowerCase().startsWith(name.toLowerCase())) {
|
||||
description = description.slice(name.length).trim();
|
||||
}
|
||||
description = description.replace(/^[-–—:]\s*/, '').trim();
|
||||
|
||||
const starsImg = li.querySelector('img[alt="stars"]');
|
||||
return {
|
||||
name,
|
||||
link,
|
||||
description,
|
||||
stars: starsImg ? parseInt(starsImg.src.match(/stars\/(\d+)/)?.[1]) || 0 : 0,
|
||||
lnAddress,
|
||||
donateUrl
|
||||
};
|
||||
}
|
||||
|
||||
// ===== grantless: aggregated "Zappable projects" section =====
|
||||
const CURATOR_LN_ADDRESS = 'aljaz@minibits.cash';
|
||||
const ZAPPABLE_SECTION = 'Zappable projects';
|
||||
|
||||
// Builds a synthetic resource list containing nostr.net (curator, pinned)
|
||||
// plus every entry from any section that carries a lightning: chip.
|
||||
function buildZappableSection(sections) {
|
||||
const ul = document.createElement('ul');
|
||||
|
||||
// nostr.net itself — curator & host, always first
|
||||
const curator = document.createElement('li');
|
||||
curator.innerHTML = `<a href="https://nostr.net">nostr.net</a> - Curator & host — this list and relay.nostr.net <a href="lightning:${CURATOR_LN_ADDRESS}">⚡ zap</a>`;
|
||||
ul.appendChild(curator);
|
||||
|
||||
// collect zappable entries from all sections, nested lists included
|
||||
const collect = (list) => {
|
||||
Array.from(list.children).forEach(li => {
|
||||
if (parseListItem(li).lnAddress) ul.appendChild(li.cloneNode(true));
|
||||
const nested = li.querySelector(':scope > ul');
|
||||
if (nested) collect(nested);
|
||||
});
|
||||
};
|
||||
Object.values(sections).forEach(content =>
|
||||
content.forEach(item => { if (item.type === 'resources') collect(item.element); })
|
||||
);
|
||||
|
||||
return [{ type: 'resources', element: ul }];
|
||||
}
|
||||
|
||||
// Modified createResourceCard function to display markdown links in a cleaner format
|
||||
function createResourceCard(resource) {
|
||||
const card = document.createElement('div');
|
||||
@@ -845,6 +921,19 @@ function createResourceCard(resource) {
|
||||
${resource.description}
|
||||
</div>
|
||||
` : ''}
|
||||
${(resource.lnAddress || resource.donateUrl) ? `
|
||||
<div class="resource-chips">
|
||||
${resource.lnAddress ? `
|
||||
<a class="zap-chip" href="lightning:${resource.lnAddress}"
|
||||
title="Zap ${resource.name} · ${resource.lnAddress}"
|
||||
data-ln-address="${resource.lnAddress}">
|
||||
<i class="fas fa-bolt" aria-hidden="true"></i> Zap
|
||||
</a>` : ''}
|
||||
${resource.donateUrl ? `
|
||||
<a class="donate-chip" href="${resource.donateUrl}" target="_blank" rel="noopener">
|
||||
<i class="fas fa-heart" aria-hidden="true"></i> Donate
|
||||
</a>` : ''}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1042,16 +1131,25 @@ async function parseAndDisplayContent() {
|
||||
sections[currentSection] = currentSectionContent;
|
||||
}
|
||||
|
||||
// grantless: insert the aggregated zappable section after the first section
|
||||
const withZappable = {};
|
||||
const originalEntries = Object.entries(sections);
|
||||
if (originalEntries.length) {
|
||||
withZappable[originalEntries[0][0]] = originalEntries[0][1];
|
||||
}
|
||||
withZappable[ZAPPABLE_SECTION] = buildZappableSection(sections);
|
||||
originalEntries.slice(1).forEach(([name, content]) => { withZappable[name] = content; });
|
||||
|
||||
// Store sections globally
|
||||
window.parsedResources = sections;
|
||||
window.parsedResources = withZappable;
|
||||
|
||||
// Generate navigation
|
||||
const sectionNames = Object.keys(sections);
|
||||
const sectionNames = Object.keys(withZappable);
|
||||
generateNavigation(sectionNames);
|
||||
|
||||
// Display initial section
|
||||
if (sectionNames.length > 0) {
|
||||
displaySection(sectionNames[0], sections);
|
||||
displaySection(sectionNames[0], withZappable);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -1080,14 +1178,7 @@ function displaySection(sectionName, sections) {
|
||||
} else if (item.type === 'resources') {
|
||||
// Resource list
|
||||
Array.from(item.element.children).forEach(li => {
|
||||
const card = createResourceCard({
|
||||
name: li.querySelector('a')?.textContent || '',
|
||||
link: li.querySelector('a')?.href || '',
|
||||
description: li.textContent.split('- ')[1]?.trim() || '',
|
||||
stars: li.querySelector('img[alt="stars"]')
|
||||
? parseInt(li.querySelector('img[alt="stars"]').src.match(/stars\/(\d+)/)?.[1]) || 0
|
||||
: 0
|
||||
});
|
||||
const card = createResourceCard(parseListItem(li));
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
+346
@@ -941,3 +941,349 @@ button:focus-visible {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Grantless: zap & donate chips ===== */
|
||||
.resource-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.zap-chip,
|
||||
.donate-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border: 1px solid rgba(247, 147, 26, 0.35);
|
||||
transition: all var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zap-chip {
|
||||
color: #f7931a;
|
||||
background: rgba(247, 147, 26, 0.1);
|
||||
}
|
||||
|
||||
.zap-chip:hover {
|
||||
background: #f7931a;
|
||||
color: #fff;
|
||||
border-color: #f7931a;
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.donate-chip {
|
||||
color: var(--link-color);
|
||||
background: var(--input-background);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.donate-chip:hover {
|
||||
border-color: var(--link-color);
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Nav "Fund nostr.net" zap button */
|
||||
.fund-toggle {
|
||||
background: rgba(247, 147, 26, 0.12);
|
||||
border: 1px solid rgba(247, 147, 26, 0.4);
|
||||
color: #f7931a;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.fund-toggle:hover {
|
||||
background: #f7931a;
|
||||
border-color: #f7931a;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 16px rgba(247, 147, 26, 0.35);
|
||||
}
|
||||
|
||||
/* Homepage banner linking to the Grant page */
|
||||
.fund-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
background: linear-gradient(135deg, rgba(247, 147, 26, 0.08), rgba(247, 147, 26, 0.02));
|
||||
border: 1px dashed rgba(247, 147, 26, 0.45);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 1.25rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.4;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.fund-banner:hover {
|
||||
border-style: solid;
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.fund-banner > .fa-bolt {
|
||||
color: #f7931a;
|
||||
font-size: 1.05rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fund-banner-cta {
|
||||
margin-left: auto;
|
||||
color: #f7931a;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.fund-banner-cta {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== In-page zapping (Bitcoin Connect) ===== */
|
||||
.theme-controls bc-button {
|
||||
margin-right: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* zap chip interaction states */
|
||||
.zap-chip { position: relative; }
|
||||
.zap-chip[data-state="zapping"] {
|
||||
opacity: 0.75;
|
||||
pointer-events: none;
|
||||
}
|
||||
.zap-chip[data-state="zapping"] .fa-bolt::before {
|
||||
content: "\f1ce"; /* fa-spinner */
|
||||
display: inline-block;
|
||||
animation: zap-spin 0.9s linear infinite;
|
||||
}
|
||||
.zap-chip[data-state="zapped"] {
|
||||
background: #2e7d32;
|
||||
border-color: #2e7d32;
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
.zap-chip[data-state="zapped"] .fa-bolt::before { content: "\f00c"; /* fa-check */ }
|
||||
.zap-chip[data-state="error"] {
|
||||
background: #c62828;
|
||||
border-color: #c62828;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@keyframes zap-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* hint that in-page zapping is available once a wallet is connected */
|
||||
body.webln-ready .zap-chip {
|
||||
box-shadow: 0 0 0 1px rgba(247, 147, 26, 0.35);
|
||||
}
|
||||
|
||||
/* toasts */
|
||||
.zap-toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 3000;
|
||||
max-width: min(92vw, 480px);
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
|
||||
animation: zap-toast-in 0.25s ease;
|
||||
background: var(--card-background, #fff);
|
||||
color: var(--text-color, #2d1b3d);
|
||||
border: 1px solid var(--border-color, #ecdfe8);
|
||||
}
|
||||
.zap-toast a { color: #f7931a; }
|
||||
.zap-toast-success { border-color: #2e7d32; }
|
||||
.zap-toast-success::before { content: "✓ "; color: #2e7d32; font-weight: 700; }
|
||||
.zap-toast-error { border-color: #c62828; }
|
||||
.zap-toast-error::before { content: "✕ "; color: #c62828; font-weight: 700; }
|
||||
|
||||
@keyframes zap-toast-in {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
/* ===== Zap fallback dialog (QR + lightning: link, no wallet needed) ===== */
|
||||
.zap-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2500;
|
||||
background: rgba(20, 12, 28, 0.55);
|
||||
backdrop-filter: blur(3px);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
animation: zap-toast-in 0.2s ease;
|
||||
}
|
||||
|
||||
.zap-modal {
|
||||
position: relative;
|
||||
background: var(--card-background, #fff);
|
||||
color: var(--text-color, #2d1b3d);
|
||||
border: 1px solid var(--border-color, #ecdfe8);
|
||||
border-radius: var(--radius-lg, 20px);
|
||||
box-shadow: var(--shadow-lg, 0 8px 30px rgba(0,0,0,0.3));
|
||||
padding: 22px 18px 14px;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.zap-modal-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted, #8b7e97);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.zap-modal-close:hover { color: var(--text-color, #2d1b3d); background: var(--hover-color, #f6f0f8); }
|
||||
|
||||
.zap-modal-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.zap-modal-title .fa-bolt { color: #f7931a; margin-right: 4px; }
|
||||
|
||||
.zap-qr-box {
|
||||
display: inline-block;
|
||||
background: #ffffff;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color, #ecdfe8);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.zap-qr svg {
|
||||
display: block;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.zap-modal-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted, #8b7e97);
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
/* single compact line: address + copy + open-wallet */
|
||||
.zap-modal-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.zap-modal-addr {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
background: var(--input-background, rgba(45,27,61,0.04));
|
||||
border: 1px solid var(--border-color, #ecdfe8);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.zap-modal-copy {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: var(--input-background, rgba(45,27,61,0.04));
|
||||
color: var(--text-muted, #8b7e97);
|
||||
border: 1px solid var(--border-color, #ecdfe8);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all var(--transition, 0.25s);
|
||||
}
|
||||
.zap-modal-copy:hover {
|
||||
color: var(--primary-color, #e05297);
|
||||
border-color: var(--primary-color, #e05297);
|
||||
}
|
||||
.zap-modal-copy.copied {
|
||||
color: #2e7d32;
|
||||
border-color: #2e7d32;
|
||||
}
|
||||
|
||||
.zap-modal-btn-primary {
|
||||
background: #f7931a;
|
||||
color: #fff;
|
||||
border-color: #f7931a;
|
||||
}
|
||||
.zap-modal-btn-primary:hover { background: #e5840d; color: #fff; text-decoration: none; }
|
||||
|
||||
.zap-modal-connect {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-top: 1px dashed var(--border-color, #ecdfe8);
|
||||
padding: 10px 0 2px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--link-color, #7c5cbf);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.zap-modal-connect:hover { text-decoration: underline; }
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.zap-qr svg { width: 160px; height: 160px; }
|
||||
}
|
||||
|
||||
/* shared compact button base for the zap dialog */
|
||||
.zap-modal-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all var(--transition, 0.25s);
|
||||
border: 1px solid transparent;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
// ===== grantless: in-page zapping via Bitcoin Connect (WebLN / NWC) =====
|
||||
// Loaded after script.js. Intercepts .zap-chip and #fundButton clicks:
|
||||
// - wallet connected → LNURL-pay + NIP-57 zap receipt, paid in-page
|
||||
// - no wallet → opens Bitcoin Connect modal, zaps pending target after connect
|
||||
// Falls back to plain lightning: links if JS/zap flow fails (links are never broken).
|
||||
|
||||
(() => {
|
||||
const ZAP_SATS = 1000; // default zap amount for chips
|
||||
const ZAP_RELAYS = ['wss://relay.nostr.net', 'wss://relay.damus.io', 'wss://nos.lol'];
|
||||
const NOBLE_URL = 'https://esm.sh/@noble/curves@1.4.0/secp256k1'; // lazy: only for anon zap receipts
|
||||
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
||||
|
||||
let pendingZap = null; // { lnAddress, name, chip }
|
||||
let pollTimer = null;
|
||||
|
||||
// ---------- small utils ----------
|
||||
const hex = (bytes) => [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
const unhex = (h) => new Uint8Array(h.match(/../g).map(x => parseInt(x, 16)));
|
||||
|
||||
async function sha256Hex(str) {
|
||||
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
|
||||
return hex(new Uint8Array(buf));
|
||||
}
|
||||
|
||||
// bech32 encode (for the optional lnurl tag in zap requests)
|
||||
function bech32Polymod(values) {
|
||||
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
||||
let chk = 1;
|
||||
for (const v of values) {
|
||||
const top = chk >> 25;
|
||||
chk = (chk & 0x1ffffff) << 5 ^ v;
|
||||
for (let i = 0; i < 5; i++) if ((top >> i) & 1) chk ^= GEN[i];
|
||||
}
|
||||
return chk;
|
||||
}
|
||||
function lnurlEncode(url) {
|
||||
const hrp = 'lnurl';
|
||||
const data = new TextEncoder().encode(url);
|
||||
const acc = { acc: 0, bits: 0, ret: [] };
|
||||
for (const b of data) {
|
||||
acc.acc = (acc.acc << 8) | b; acc.bits += 8;
|
||||
while (acc.bits >= 5) { acc.bits -= 5; acc.ret.push((acc.acc >> acc.bits) & 31); }
|
||||
}
|
||||
if (acc.bits) acc.ret.push((acc.acc << (5 - acc.bits)) & 31);
|
||||
const values = [...hrp].map(c => c.charCodeAt(0) >> 5)
|
||||
.concat([0], [...hrp].map(c => c.charCodeAt(0) & 31), acc.ret);
|
||||
const pm = bech32Polymod([...values, 0, 0, 0, 0, 0, 0]) ^ 1;
|
||||
const cs = [...Array(6)].map((_, i) => (pm >> 5 * (5 - i)) & 31);
|
||||
return hrp + '1' + [...acc.ret, ...cs].map(d => CHARSET[d]).join('');
|
||||
}
|
||||
|
||||
// ---------- toast ----------
|
||||
function toast(msg, type = 'info', ms = 4200) {
|
||||
document.querySelectorAll('.zap-toast').forEach(t => t.remove());
|
||||
const el = document.createElement('div');
|
||||
el.className = `zap-toast zap-toast-${type}`;
|
||||
el.setAttribute('role', 'status');
|
||||
el.innerHTML = msg;
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => el.remove(), ms);
|
||||
}
|
||||
|
||||
// ---------- LNURL ----------
|
||||
function lnurlpUrl(address) {
|
||||
const [user, host] = address.split('@');
|
||||
return `https://${host}/.well-known/lnurlp/${encodeURIComponent(user)}`;
|
||||
}
|
||||
|
||||
async function fetchLnurlParams(address) {
|
||||
const r = await fetch(lnurlpUrl(address));
|
||||
if (!r.ok) throw new Error(`lnurlp ${r.status}`);
|
||||
const j = await r.json();
|
||||
if (j.status === 'ERROR') throw new Error(j.reason || 'lnurlp error');
|
||||
if (j.tag !== 'payRequest') throw new Error('not a pay request');
|
||||
return j;
|
||||
}
|
||||
|
||||
async function fetchInvoice(params, msat, zapEvent) {
|
||||
const cb = new URL(params.callback);
|
||||
cb.searchParams.set('amount', String(msat));
|
||||
if (zapEvent) cb.searchParams.set('nostr', JSON.stringify(zapEvent));
|
||||
const r = await fetch(cb);
|
||||
const j = await r.json();
|
||||
if (j.status === 'ERROR') throw new Error(j.reason || 'invoice error');
|
||||
if (!j.pr) throw new Error('no invoice');
|
||||
return j;
|
||||
}
|
||||
|
||||
// ---------- NIP-57 zap request ----------
|
||||
async function buildZapRequest(params, msat, address) {
|
||||
if (!params.allowsNostr || !params.nostrPubkey) return { event: null, anon: false };
|
||||
|
||||
const tags = [
|
||||
['p', params.nostrPubkey],
|
||||
['relays', ...ZAP_RELAYS],
|
||||
['amount', String(msat)],
|
||||
['lnurl', lnurlEncode(lnurlpUrl(address))]
|
||||
];
|
||||
|
||||
// Prefer the user's real identity via NIP-07…
|
||||
if (window.nostr?.signEvent && window.nostr?.getPublicKey) {
|
||||
try {
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
const ev = await window.nostr.signEvent({
|
||||
kind: 9734, pubkey, created_at: Math.floor(Date.now() / 1000),
|
||||
tags, content: ''
|
||||
});
|
||||
return { event: { ...ev, pubkey: ev.pubkey || pubkey }, anon: false };
|
||||
} catch (e) {
|
||||
console.warn('NIP-07 signing failed, falling back to anon zap', e);
|
||||
}
|
||||
}
|
||||
|
||||
// …otherwise an ephemeral key → still a valid (anonymous) zap receipt
|
||||
try {
|
||||
const { schnorr } = await import(NOBLE_URL);
|
||||
const priv = hex(crypto.getRandomValues(new Uint8Array(32)));
|
||||
const pubkey = hex(schnorr.getPublicKey(priv));
|
||||
const created_at = Math.floor(Date.now() / 1000);
|
||||
const event = { pubkey, created_at, kind: 9734, tags: [...tags, ['anon', '']], content: '' };
|
||||
const id = await sha256Hex(JSON.stringify([0, pubkey, created_at, 9734, event.tags, '']));
|
||||
event.id = id;
|
||||
event.sig = hex(schnorr.sign(unhex(id), priv));
|
||||
return { event, anon: true };
|
||||
} catch (e) {
|
||||
console.warn('anon signing unavailable, paying without receipt', e);
|
||||
return { event: null, anon: false };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- payment ----------
|
||||
const weblnReady = () => !!(window.webln && typeof window.webln.sendPayment === 'function');
|
||||
|
||||
async function payInvoice(pr) {
|
||||
if (!weblnReady()) throw new Error('wallet not connected');
|
||||
if (window.webln.enable) { try { await window.webln.enable(); } catch (e) { /* already enabled */ } }
|
||||
return window.webln.sendPayment(pr);
|
||||
}
|
||||
|
||||
// ---------- main zap flow ----------
|
||||
async function zap(lnAddress, name, chip) {
|
||||
if (chip?.dataset.zapping === '1') return;
|
||||
if (chip) { chip.dataset.zapping = '1'; chip.dataset.state = 'zapping'; }
|
||||
|
||||
try {
|
||||
toast(`⚡ Zapping <strong>${escapeHtml(name)}</strong>…`, 'info', 9000);
|
||||
const params = await fetchLnurlParams(lnAddress);
|
||||
|
||||
let msat = ZAP_SATS * 1000;
|
||||
const min = params.minSendable || 1000, max = params.maxSendable || Infinity;
|
||||
const clamped = Math.min(Math.max(msat, min), max);
|
||||
if (clamped !== msat) { msat = clamped; toast(`Amount adjusted to ${msat / 1000} sats (wallet limits)`, 'info'); }
|
||||
|
||||
const { event, anon } = await buildZapRequest(params, msat, lnAddress);
|
||||
const inv = await fetchInvoice(params, msat, event);
|
||||
await payInvoice(inv.pr);
|
||||
|
||||
if (chip) { chip.dataset.state = 'zapped'; delete chip.dataset.zapping; }
|
||||
const sats = msat / 1000;
|
||||
toast(
|
||||
event
|
||||
? `⚡ Zapped <strong>${escapeHtml(name)}</strong> ${sats} sats${anon ? ' (anon zap receipt)' : ' · zap receipt published'}`
|
||||
: `⚡ Sent <strong>${escapeHtml(name)}</strong> ${sats} sats (no zap receipt)`,
|
||||
'success', 6000
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('zap failed:', err);
|
||||
if (chip) { chip.dataset.state = 'error'; delete chip.dataset.zapping; setTimeout(() => { if (chip.dataset.state === 'error') delete chip.dataset.state; }, 2500); }
|
||||
toast(`Couldn't zap ${escapeHtml(name)}: ${escapeHtml(err.message || 'failed')}`, 'error', 6000);
|
||||
}
|
||||
}
|
||||
|
||||
const escapeHtml = (s) => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
|
||||
// ---------- connect-modal path ----------
|
||||
function openConnectModal() {
|
||||
const btn = document.querySelector('bc-button');
|
||||
if (!btn) return false;
|
||||
const inner = btn.shadowRoot?.querySelector('bci-button');
|
||||
if (inner) inner.click(); else btn.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
function watchForConnection() {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = setInterval(() => {
|
||||
if (weblnReady()) {
|
||||
clearInterval(pollTimer);
|
||||
toast('Wallet connected ⚡', 'success', 2500);
|
||||
if (pendingZap) {
|
||||
const p = pendingZap; pendingZap = null;
|
||||
setTimeout(() => zap(p.lnAddress, p.name, p.chip), 300);
|
||||
}
|
||||
}
|
||||
}, 600);
|
||||
setTimeout(() => clearInterval(pollTimer), 180000); // stop watching after 3 min
|
||||
}
|
||||
|
||||
// ---------- QR fallback dialog (no wallet connection required) ----------
|
||||
let qrLibPromise = null;
|
||||
async function loadQr() {
|
||||
if (!qrLibPromise) qrLibPromise = import('https://esm.sh/qrcode-generator@1.4.4').then(m => m.default || m);
|
||||
return qrLibPromise;
|
||||
}
|
||||
|
||||
const closeZapModal = () => document.querySelector('.zap-modal-backdrop')?.remove();
|
||||
|
||||
async function showZapModal({ lnAddress, name, chip }) {
|
||||
closeZapModal();
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'zap-modal-backdrop';
|
||||
backdrop.innerHTML = `
|
||||
<div class="zap-modal" role="dialog" aria-modal="true" aria-label="Zap ${escapeHtml(name)}">
|
||||
<button class="zap-modal-close" aria-label="Close">✕</button>
|
||||
<h3 class="zap-modal-title"><i class="fas fa-bolt" aria-hidden="true"></i> Zap ${escapeHtml(name)}</h3>
|
||||
<div class="zap-qr-box"><div class="zap-qr" aria-hidden="true"></div></div>
|
||||
<p class="zap-modal-hint">Scan with any Lightning wallet — no connection needed.</p>
|
||||
<div class="zap-modal-line">
|
||||
<code class="zap-modal-addr" title="${escapeHtml(lnAddress)}">${escapeHtml(lnAddress)}</code>
|
||||
<button class="zap-modal-copy" type="button" title="Copy lightning address" aria-label="Copy lightning address">
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
</button>
|
||||
<a class="zap-modal-btn zap-modal-btn-primary" href="lightning:${escapeHtml(lnAddress)}" title="Open in your Lightning wallet">
|
||||
<i class="fas fa-wallet" aria-hidden="true"></i> Open wallet
|
||||
</a>
|
||||
</div>
|
||||
<button class="zap-modal-connect" type="button">
|
||||
or connect a wallet once and zap right here →
|
||||
</button>
|
||||
</div>`;
|
||||
document.body.appendChild(backdrop);
|
||||
|
||||
// QR: LNURL-pay encoded as lightning: URI (static, zero network)
|
||||
try {
|
||||
const q = await loadQr();
|
||||
const qr = q(0, 'M');
|
||||
qr.addData('lightning:' + lnurlEncode(lnurlpUrl(lnAddress)).toUpperCase(), 'Byte');
|
||||
qr.make();
|
||||
backdrop.querySelector('.zap-qr').innerHTML = qr.createSvgTag({ cellSize: 4, margin: 0 });
|
||||
} catch (e) {
|
||||
console.warn('QR generation failed', e);
|
||||
backdrop.querySelector('.zap-qr-box')?.remove();
|
||||
}
|
||||
|
||||
// wiring
|
||||
backdrop.querySelector('.zap-modal-close').addEventListener('click', closeZapModal);
|
||||
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) closeZapModal(); });
|
||||
|
||||
const copyBtn = backdrop.querySelector('.zap-modal-copy');
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(lnAddress);
|
||||
} catch (e) {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = lnAddress; document.body.appendChild(ta);
|
||||
ta.select(); document.execCommand('copy'); ta.remove();
|
||||
}
|
||||
copyBtn.innerHTML = '<i class="fas fa-check" aria-hidden="true"></i>';
|
||||
copyBtn.classList.add('copied');
|
||||
setTimeout(() => {
|
||||
copyBtn.innerHTML = '<i class="fas fa-copy" aria-hidden="true"></i>';
|
||||
copyBtn.classList.remove('copied');
|
||||
}, 1600);
|
||||
});
|
||||
|
||||
backdrop.querySelector('.zap-modal-connect').addEventListener('click', () => {
|
||||
closeZapModal();
|
||||
pendingZap = { lnAddress, name, chip };
|
||||
if (openConnectModal()) {
|
||||
toast('Connect your wallet once — then zap any project without leaving the page.', 'info', 5000);
|
||||
watchForConnection();
|
||||
} else {
|
||||
toast('No WebLN wallet found. Install <a href="https://getalby.com" target="_blank" rel="noopener">Alby</a>, or zap via the QR above.', 'info', 6000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeZapModal(); });
|
||||
|
||||
// ---------- click delegation ----------
|
||||
document.addEventListener('click', (e) => {
|
||||
const chip = e.target.closest('a.zap-chip, #fundButton');
|
||||
if (!chip) return;
|
||||
|
||||
const href = chip.getAttribute('href') || '';
|
||||
if (!href.startsWith('lightning:')) return;
|
||||
|
||||
e.preventDefault();
|
||||
const lnAddress = chip.dataset.lnAddress || href.replace(/^lightning:/i, '');
|
||||
const name = chip.dataset.name || chip.closest('.resource-card')?.querySelector('.resource-title a')?.textContent.trim() || 'nostr.net';
|
||||
|
||||
if (weblnReady()) {
|
||||
zap(lnAddress, name, chip);
|
||||
} else {
|
||||
showZapModal({ lnAddress, name, chip });
|
||||
}
|
||||
});
|
||||
|
||||
// extension-provided WebLN (e.g. Alby without BC): chips become "ready" style
|
||||
const markReady = () => { if (weblnReady()) document.body.classList.add('webln-ready'); };
|
||||
markReady();
|
||||
setTimeout(markReady, 1500);
|
||||
window.addEventListener('bc:onpaid', markReady);
|
||||
})();
|
||||
Reference in New Issue
Block a user