├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── enhancement_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_STYLE.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── clippy.toml ├── contrib ├── README.md ├── fund │ ├── rust-nostr-donations-bitcoin-address.txt │ ├── rust-nostr-donations-bitcoin-address.txt.asc │ ├── rust-nostr-donations-liquid-address.txt │ └── rust-nostr-donations-liquid-address.txt.asc ├── release │ ├── ANNOUNCEMENT_TEMPLATE.txt │ └── RELEASE_STEPS.md ├── scripts │ ├── check-crates.sh │ ├── check-deny.sh │ ├── check-docs.sh │ ├── check-fmt.sh │ ├── check.sh │ ├── contributors.py │ ├── precommit.sh │ └── release.sh └── verify-commits │ └── trusted-keys ├── crates ├── nostr-blossom │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ ├── delete.rs │ │ ├── download.rs │ │ ├── integration_test.rs │ │ ├── list.rs │ │ └── upload.rs │ └── src │ │ ├── bud01.rs │ │ ├── bud02.rs │ │ ├── client.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ └── prelude.rs ├── nostr-cli │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── cli │ │ ├── io.rs │ │ ├── mod.rs │ │ └── parser.rs │ │ ├── main.rs │ │ └── util.rs ├── nostr-connect │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ ├── handle-auth-url.rs │ │ └── nostr-connect-signer.rs │ └── src │ │ ├── client.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── prelude.rs │ │ └── signer.rs ├── nostr-database │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── fbs │ │ └── event.fbs │ ├── justfile │ └── src │ │ ├── collections │ │ ├── events.rs │ │ ├── mod.rs │ │ └── tree.rs │ │ ├── error.rs │ │ ├── ext.rs │ │ ├── flatbuffers │ │ ├── event_generated.rs │ │ └── mod.rs │ │ ├── helper.rs │ │ ├── lib.rs │ │ ├── memory.rs │ │ ├── prelude.rs │ │ └── profile.rs ├── nostr-indexeddb │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── error.rs │ │ └── lib.rs ├── nostr-keyring │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ ├── async.rs │ │ └── blocking.rs │ └── src │ │ ├── lib.rs │ │ └── prelude.rs ├── nostr-lmdb │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── store │ │ ├── error.rs │ │ ├── ingester.rs │ │ ├── lmdb │ │ ├── index.rs │ │ └── mod.rs │ │ ├── mod.rs │ │ └── types │ │ ├── filter.rs │ │ └── mod.rs ├── nostr-mls-memory-storage │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── groups.rs │ │ ├── lib.rs │ │ ├── messages.rs │ │ └── welcomes.rs ├── nostr-mls-sqlite-storage │ ├── Cargo.toml │ ├── README.md │ ├── migrations │ │ └── V100__initial.sql │ └── src │ │ ├── db.rs │ │ ├── error.rs │ │ ├── groups.rs │ │ ├── lib.rs │ │ ├── messages.rs │ │ ├── migrations.rs │ │ └── welcomes.rs ├── nostr-mls-storage │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── groups │ │ ├── error.rs │ │ ├── mod.rs │ │ └── types.rs │ │ ├── lib.rs │ │ ├── messages │ │ ├── error.rs │ │ ├── mod.rs │ │ └── types.rs │ │ └── welcomes │ │ ├── error.rs │ │ ├── mod.rs │ │ └── types.rs ├── nostr-mls │ ├── Cargo.toml │ ├── README.md │ ├── docs │ │ ├── plan.md │ │ └── resources.md │ ├── examples │ │ ├── mls_memory.rs │ │ └── mls_sqlite.rs │ └── src │ │ ├── constant.rs │ │ ├── error.rs │ │ ├── extension.rs │ │ ├── groups.rs │ │ ├── key_packages.rs │ │ ├── lib.rs │ │ ├── messages.rs │ │ ├── prelude.rs │ │ └── welcomes.rs ├── nostr-ndb │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── nostr-relay-builder │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ ├── hyper.rs │ │ ├── local-with-hs.rs │ │ ├── mock.rs │ │ └── policy.rs │ └── src │ │ ├── builder.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── local │ │ ├── inner.rs │ │ ├── mod.rs │ │ ├── session.rs │ │ └── util.rs │ │ ├── mock.rs │ │ └── prelude.rs ├── nostr-relay-pool │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ └── pool.rs │ └── src │ │ ├── lib.rs │ │ ├── monitor.rs │ │ ├── policy.rs │ │ ├── pool │ │ ├── builder.rs │ │ ├── constants.rs │ │ ├── error.rs │ │ ├── inner.rs │ │ ├── mod.rs │ │ ├── options.rs │ │ └── output.rs │ │ ├── prelude.rs │ │ ├── relay │ │ ├── constants.rs │ │ ├── error.rs │ │ ├── flags.rs │ │ ├── inner.rs │ │ ├── limits.rs │ │ ├── mod.rs │ │ ├── options.rs │ │ ├── ping.rs │ │ ├── stats.rs │ │ └── status.rs │ │ ├── shared.rs │ │ ├── stream.rs │ │ └── transport │ │ ├── error.rs │ │ ├── mod.rs │ │ └── websocket.rs ├── nostr-sdk │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ ├── aggregated-query.rs │ │ ├── blacklist.rs │ │ ├── bot.rs │ │ ├── client.rs │ │ ├── code_snippet.rs │ │ ├── comment.rs │ │ ├── fetch-events.rs │ │ ├── gossip.rs │ │ ├── limits.rs │ │ ├── lmdb.rs │ │ ├── monitor.rs │ │ ├── nostr-connect.rs │ │ ├── nostrdb.rs │ │ ├── status.rs │ │ ├── stream-events.rs │ │ ├── subscriptions.rs │ │ ├── switch-account.rs │ │ ├── tor.rs │ │ └── whitelist.rs │ └── src │ │ ├── client │ │ ├── builder.rs │ │ ├── error.rs │ │ ├── mod.rs │ │ └── options.rs │ │ ├── gossip │ │ ├── constant.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ └── prelude.rs ├── nostr │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ ├── embedded │ │ │ ├── .gitignore │ │ │ ├── Cargo.toml │ │ │ ├── README.md │ │ │ ├── justfile │ │ │ ├── memory.x │ │ │ ├── rust-toolchain.toml │ │ │ └── src │ │ │ │ └── main.rs │ │ ├── keys.rs │ │ ├── nip05.rs │ │ ├── nip06.rs │ │ ├── nip09.rs │ │ ├── nip11.rs │ │ ├── nip13.rs │ │ ├── nip15.rs │ │ ├── nip19.rs │ │ ├── nip57.rs │ │ ├── nip96.rs │ │ ├── nip98.rs │ │ └── parser.rs │ └── src │ │ ├── event │ │ ├── borrow.rs │ │ ├── builder.rs │ │ ├── error.rs │ │ ├── id.rs │ │ ├── kind.rs │ │ ├── mod.rs │ │ ├── tag │ │ │ ├── cow.rs │ │ │ ├── error.rs │ │ │ ├── kind.rs │ │ │ ├── list.rs │ │ │ ├── mod.rs │ │ │ └── standard.rs │ │ └── unsigned.rs │ │ ├── filter.rs │ │ ├── key │ │ ├── mod.rs │ │ ├── public_key.rs │ │ ├── secret_key.rs │ │ └── vanity.rs │ │ ├── lib.rs │ │ ├── message │ │ ├── client.rs │ │ ├── mod.rs │ │ └── relay │ │ │ └── mod.rs │ │ ├── nips │ │ ├── mod.rs │ │ ├── nip01.rs │ │ ├── nip02.rs │ │ ├── nip04.rs │ │ ├── nip05.rs │ │ ├── nip06 │ │ │ ├── bip32.rs │ │ │ └── mod.rs │ │ ├── nip07.rs │ │ ├── nip09.rs │ │ ├── nip10.rs │ │ ├── nip11.rs │ │ ├── nip13.rs │ │ ├── nip15.rs │ │ ├── nip17.rs │ │ ├── nip19.rs │ │ ├── nip21.rs │ │ ├── nip22.rs │ │ ├── nip26.rs │ │ ├── nip34.rs │ │ ├── nip35.rs │ │ ├── nip38.rs │ │ ├── nip39.rs │ │ ├── nip42.rs │ │ ├── nip44 │ │ │ ├── mod.rs │ │ │ ├── nip44.vectors.json │ │ │ └── v2.rs │ │ ├── nip46.rs │ │ ├── nip47.rs │ │ ├── nip48.rs │ │ ├── nip49.rs │ │ ├── nip51.rs │ │ ├── nip53.rs │ │ ├── nip56.rs │ │ ├── nip57.rs │ │ ├── nip58.rs │ │ ├── nip59.rs │ │ ├── nip62.rs │ │ ├── nip65.rs │ │ ├── nip73.rs │ │ ├── nip88.rs │ │ ├── nip90.rs │ │ ├── nip94.rs │ │ ├── nip96.rs │ │ ├── nip98.rs │ │ └── nipc0.rs │ │ ├── parser.rs │ │ ├── prelude.rs │ │ ├── signer.rs │ │ ├── types │ │ ├── image.rs │ │ ├── mod.rs │ │ ├── time │ │ │ ├── mod.rs │ │ │ └── supplier.rs │ │ └── url.rs │ │ └── util │ │ ├── hex.rs │ │ ├── hkdf.rs │ │ └── mod.rs └── nwc │ ├── Cargo.toml │ ├── README.md │ ├── examples │ └── nwc.rs │ └── src │ ├── error.rs │ ├── lib.rs │ ├── options.rs │ └── prelude.rs ├── deny.toml ├── justfile ├── maintainers.yaml ├── rust-toolchain.toml └── rustfmt.toml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: [ 2 | 'https://rust-nostr.org/donate', 3 | ] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | 12 | **To Reproduce** 13 | 14 | 15 | **Expected behavior** 16 | 17 | 18 | **Build environment** 19 | * Library: 20 | * Version/tag/commit: 21 | * OS+version: 22 | 23 | **Additional context** 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement request 3 | about: Request a new feature or change to an existing feature 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the enhancement** 10 | 11 | 12 | **Use case** 13 | 14 | 15 | **Additional context** 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | ### Notes to the reviewers 6 | 7 | 9 | 10 | ### Checklist 11 | 12 | * [ ] I followed the [contribution guidelines](https://github.com/rust-nostr/nostr/blob/master/CONTRIBUTING.md) 13 | * [ ] I ran `just precommit` or `just check` before committing 14 | * [ ] I updated the [CHANGELOG](https://github.com/rust-nostr/nostr/blob/master/CHANGELOG.md) (if applicable) 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | fmt: 14 | name: Format 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Check 20 | run: bash contrib/scripts/check-fmt.sh check 21 | 22 | check-crates: 23 | name: Check crates 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Check 29 | run: bash contrib/scripts/check-crates.sh "" ci 30 | 31 | check-crates-msrv: 32 | name: Check crates (MSRV) 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | - name: Check 38 | run: bash contrib/scripts/check-crates.sh msrv ci 39 | 40 | check-docs: 41 | name: Check docs 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | - name: Check 47 | run: bash contrib/scripts/check-docs.sh 48 | 49 | build-no-std: 50 | name: Build no_std 51 | runs-on: ubuntu-latest 52 | defaults: 53 | run: 54 | working-directory: ./crates/nostr/examples/embedded 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v4 58 | - name: Set default toolchain 59 | run: rustup default nightly 60 | - name: Set profile 61 | run: rustup set profile minimal 62 | - name: Install just 63 | run: cargo install just 64 | - name: Init 65 | run: sudo apt update && just init 66 | - name: Build 67 | run: just build 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | db/ 3 | .DS_Store 4 | *.db 5 | *.db-shm 6 | *.db-wal 7 | *.mdb 8 | many-events.json 9 | many-events.json.zst 10 | .idea/ 11 | .vscode/ 12 | env/ 13 | .repomix-*.txt 14 | vibe-tools.config.json 15 | .cursor/rules/ 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*"] 3 | default-members = ["crates/*"] 4 | resolver = "2" 5 | 6 | [workspace.package] 7 | authors = ["Yuki Kishimoto ", "Rust Nostr Developers"] 8 | homepage = "https://github.com/rust-nostr/nostr" 9 | repository = "https://github.com/rust-nostr/nostr.git" 10 | license = "MIT" 11 | rust-version = "1.70.0" 12 | 13 | [workspace.dependencies] 14 | async-utility = "0.3" 15 | async-wsocket = "0.13" 16 | atomic-destructor = { version = "0.3", default-features = false } 17 | base64 = { version = "0.22", default-features = false } 18 | clap = "=4.4.18" 19 | js-sys = "0.3" 20 | lru = { version = "0.14", default-features = false } 21 | negentropy = { version = "0.5", default-features = false } 22 | nostr = { version = "0.42", path = "./crates/nostr", default-features = false } 23 | nostr-connect = { version = "0.42", path = "./crates/nostr-connect", default-features = false } 24 | nostr-database = { version = "0.42", path = "./crates/nostr-database", default-features = false } 25 | nostr-indexeddb = { version = "0.42", path = "./crates/nostr-indexeddb", default-features = false } 26 | nostr-lmdb = { version = "0.42", path = "./crates/nostr-lmdb", default-features = false } 27 | nostr-mls-memory-storage = { version = "0.42", path = "./crates/nostr-mls-memory-storage", default-features = false } 28 | nostr-mls-sqlite-storage = { version = "0.42", path = "./crates/nostr-mls-sqlite-storage", default-features = false } 29 | nostr-mls-storage = { version = "0.42", path = "./crates/nostr-mls-storage", default-features = false } 30 | nostr-mls = { version = "0.42", path = "./crates/nostr-mls", default-features = false } 31 | nostr-ndb = { version = "0.42", path = "./crates/nostr-ndb", default-features = false } 32 | nostr-relay-builder = { version = "0.42", path = "./crates/nostr-relay-builder", default-features = false } 33 | nostr-relay-pool = { version = "0.42", path = "./crates/nostr-relay-pool", default-features = false } 34 | nostr-sdk = { version = "0.42", path = "./crates/nostr-sdk", default-features = false } 35 | reqwest = { version = "0.12", default-features = false } 36 | serde = { version = "1.0", default-features = false } 37 | serde_json = { version = "1.0", default-features = false } 38 | tempfile = "3.19" 39 | tokio = { version = ">=1.37", default-features = false } 40 | tracing = { version = "0.1", default-features = false } 41 | tracing-subscriber = "0.3" 42 | wasm-bindgen = { version = "0.2", default-features = false } 43 | wasm-bindgen-futures = "0.4" 44 | web-sys = { version = "0.3", default-features = false } 45 | 46 | [profile.release] 47 | lto = true 48 | codegen-units = 1 49 | panic = "abort" 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 Yuki Kishimoto 4 | Copyright (c) 2023-2025 Rust Nostr Developers 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | To report security issues send an email to **yukikishimoto@protonmail.com** 4 | 5 | The following keys may be used to communicate sensitive information to developers: 6 | 7 | | Name | Fingerprint | 8 | | ------------------- | -------------------------------------------------------------------------------------------------------------------------- | 9 | | Yuki Kishimoto | 86F3 105A DFA8 AB58 7268 DCD7 8D3D CD04 2496 19D1 | 10 | 11 | You can import a key by running the following command with that individual’s fingerprint: 12 | 13 | ``` 14 | gpg --keyserver hkps://keys.openpgp.org --recv-keys "" 15 | ``` 16 | 17 | Ensure that you put quotes around fingerprints containing spaces. 18 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | too-many-arguments-threshold = 13 -------------------------------------------------------------------------------- /contrib/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-nostr/nostr/3461c04ece122bc0d936358ed4f06d2931d2530c/contrib/README.md -------------------------------------------------------------------------------- /contrib/fund/rust-nostr-donations-bitcoin-address.txt: -------------------------------------------------------------------------------- 1 | The Rust Nostr Project has one official bitcoin (BTC) donation address: 2 | 3 | bc1quk478kpm45744q5pt3p9j42fnv72ykytmt3z0j 4 | 5 | Last updated: 2024-01-17 -------------------------------------------------------------------------------- /contrib/fund/rust-nostr-donations-bitcoin-address.txt.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNATURE----- 2 | 3 | iQIzBAABCgAdFiEEhvMQWt+oq1hyaNzXjT3NBCSWGdEFAmWoE0kACgkQjT3NBCSW 4 | GdGTyQ/8CFJoIw674WySiI9n0kTvmrycsMu5xxm+AsAOP2DFXPKscdC3zRAW/scS 5 | 0qi6QsKbdXvf76NBsOx4VY6gK0Xkwp6M5ZwJLyS50EABECc47utronRzp8gGSWgz 6 | PYMB5s1WSsvph0Y9CXQN5UOVjPnpiMlvy49pBQvw1rexi/pAHPB+C73fGTuDbiMG 7 | PKk4+7q2RWNqgOzBGKUIqRLqmj8Vvx5GOk43ZQ9KoVqE/j1fQonwQ0tGmRkHhMEi 8 | 3sYQHlj3qgqD/wOa7FgOZJY+gwQErn3FXngINVhueSeVnnurBHZPLexhjdIUVvvT 9 | 0bavxChDJ0Q7+B4tBBbClS8A0kEHEciRWZOXY/edegcFP3rtTn0gQn2gyCEsNr3h 10 | gXHy5RoqkekrdtkCYxN0sJj3mLrPIhJz6ZjoEcczEWNaPNEMcSULG6iHES64U8JJ 11 | 4jZyRmJM3DyLilORiMCbPj+ADtV1p+L7B4a8yatLXX+DIQkyMdFVJ5oCM6Zo9joI 12 | vFt5YTr1igqPto6y0lRFYgATSzlOCnmy2/oTNes2RwnHwyGigjtjhbbsN7umzNpq 13 | 3PfmWl3KffQCc3/fnISrO2I31QObeG81VAn0T4BgDX5AcSRp0EJqyeOOgy4p/LEK 14 | FOotFLWX79XAYWRRgKAZRyDmBfiMrlfanhPTAE45hBC1yneqfPw= 15 | =foWS 16 | -----END PGP SIGNATURE----- 17 | -------------------------------------------------------------------------------- /contrib/fund/rust-nostr-donations-liquid-address.txt: -------------------------------------------------------------------------------- 1 | The Rust Nostr Project has one official bitcoin liquid (L-BTC) donation address: 2 | 3 | lq1qqdwn93gehkq4mtz2amsagawgfd6y9ksrkekal5u8tmle07f9fa0kgcnfez4lguhekeeyhy78nfqy8tyqvxayywgpwvm73t6av 4 | 5 | Last updated: 2024-11-15 6 | -------------------------------------------------------------------------------- /contrib/fund/rust-nostr-donations-liquid-address.txt.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNATURE----- 2 | 3 | iQIzBAABCgAdFiEEhvMQWt+oq1hyaNzXjT3NBCSWGdEFAmc3wz0ACgkQjT3NBCSW 4 | GdGNYRAAubXoaUcPN8TlXo/M8ctN0Lex+7tB/rPH/2Om3AgBfFl03rDM6zSaVTiI 5 | Ntbts82cascPzVCIwwwFXW2jyzeuK3Zpv42qoxMoNijlTkP6MjIn9pMO++0LEZZq 6 | vWrp381qd6PtzEWmEbbqa2XksR9t/VJYLSvX1APBUEqzmoDfpt+NyghZy/NtQuxM 7 | nmUuD/BkFoGTmJApRiCEvbMeKENltYGioG6eTqedUqgPIiltYz4F1G1zXTH6490C 8 | oz0z2p+/YqIlSeLSgLYgrXMbX2D29BpmKJ+12MCPDutuUvLH2nWN/42px5kIyKYP 9 | qMxOv3Vf7UhG5PkUOyJTYPzp2CTb78DjP3JyPOmP5cpvJsvvavApUp5uPrKxiVQJ 10 | voZBzC2/jcCSy64QZ5C7fkz9rBMvUJhf61L/9FlUAkV38ZzPIlJjhjLrx+9ZcWVp 11 | uT2z11FgcAY1YXYnXdq+SnDLsA5idUCo0SQBBHoCZDIFvdI6P70zs4KzdA+hG6Da 12 | Lqo9jwKtL5Z5X++9PGE8AjyGh6sqYyuh7sObBeJnIWRqERekvDu4HqZmUOf4CkLw 13 | ef65CuXhu2aCoD369rg2TRi4jVVLqVRs9muwGIJeL6iu0Hfw52ONB4y52tE+HK/m 14 | Rv75bEX3uiC5brogj7wW1vybr+wbwWt3rnFvv2pIrIDYShe85LU= 15 | =WOxw 16 | -----END PGP SIGNATURE----- 17 | -------------------------------------------------------------------------------- /contrib/release/ANNOUNCEMENT_TEMPLATE.txt: -------------------------------------------------------------------------------- 1 | ## rust-nostr v is out! 🦀 2 | 3 | ### Summary 4 | 5 | 6 | 7 | Full changelog: https://rust-nostr.org/changelog 8 | 9 | ### Contributors 10 | 11 | Thanks to all contributors! 12 | 13 | 14 | 15 | ### Links 16 | 17 | https://rust-nostr.org 18 | https://rust-nostr.org/donate 19 | 20 | #rustnostr #nostr #rustlang #programming #rust #python #csharp #dotnet #javascript #kotlin #swift #flutter 21 | -------------------------------------------------------------------------------- /contrib/release/RELEASE_STEPS.md: -------------------------------------------------------------------------------- 1 | # Release Checks 2 | 3 | * Run `just check` to verify that everything compile 4 | 5 | * Try to compile `kotlin` bindings (`nostr-sdk-ffi`) since compilation could fail during gradlew due to enumerations names. 6 | 7 | * Bump versions 8 | * Rust in various `Cargo.toml` 9 | * Android in `lib/build.gradle.kts` 10 | * JVM in `lib/build.gradle.kts` 11 | * Python in `setup.py` 12 | * Flutter in `pubspec.yaml` (other repository) 13 | * JavaScript in `package.json` 14 | * Swift Package DOESN'T require version update 15 | 16 | * Commit and push (**without tag**): `Bump to vX.X.X` 17 | 18 | * Release crates and bindings 19 | * Publish crates with `just release` or `bash ./contrib/scripts/release.sh` 20 | * Publish `Kotlin` (android + JVM) bindings 21 | * Publish `Python` bindings 22 | * Publish `JavaScript` bindings 23 | * Publish `Swift` bindings 24 | * Publish `Flutter` bindings (other repository) 25 | 26 | * Bump versions in `book` (**without commit**, commit in next step) 27 | * Update examples 28 | * Search in the code for `UNCOMMENT_ON_RELEASE` string and uncomment the code (examples added in book before release) 29 | * Rust book tests: `just check-book` 30 | 31 | * Update `CHANGELOG.md` 32 | 33 | * Commit and push (**WITH tag**) 34 | * `Release vX.X.X` 35 | -------------------------------------------------------------------------------- /contrib/scripts/check-deny.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Install cargo-deny 6 | cargo deny --version || cargo install cargo-deny 7 | 8 | # Check 9 | cargo deny check bans --show-stats 10 | cargo deny check advisories 11 | cargo deny check sources 12 | -------------------------------------------------------------------------------- /contrib/scripts/check-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | buildargs=( 6 | "-p nostr" 7 | "-p nostr-database" 8 | "-p nostr-relay-pool" 9 | "-p nostr-connect" 10 | "-p nwc" 11 | "-p nostr-sdk" 12 | ) 13 | 14 | for arg in "${buildargs[@]}"; do 15 | echo "Checking '$arg' docs" 16 | cargo doc --no-deps $arg --all-features 17 | echo 18 | done 19 | -------------------------------------------------------------------------------- /contrib/scripts/check-fmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | version="nightly-2025-03-12" 6 | flags="" 7 | 8 | # Check if "check" is passed as an argument 9 | if [[ "$#" -gt 0 && "$1" == "check" ]]; then 10 | flags="--check" 11 | fi 12 | 13 | # Install toolchain 14 | cargo +$version --version || rustup install $version 15 | 16 | # Install rustfmt 17 | cargo +$version fmt --version || rustup component add rustfmt --toolchain $version 18 | 19 | # Check workspace crates 20 | cargo +$version fmt --all -- --config format_code_in_doc_comments=true $flags 21 | -------------------------------------------------------------------------------- /contrib/scripts/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -exuo pipefail 4 | 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | 7 | "${DIR}/check-fmt.sh" check # Check if Rust code is formatted 8 | "${DIR}/check-crates.sh" # Check all crates 9 | "${DIR}/check-crates.sh" msrv "" # Check all crates MSRV 10 | "${DIR}/check-docs.sh" # Check Rust docs 11 | -------------------------------------------------------------------------------- /contrib/scripts/precommit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -exuo pipefail 4 | 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | 7 | "${DIR}/check-fmt.sh" # Format the code 8 | "${DIR}/check-crates.sh" # Check all crates 9 | "${DIR}/check-docs.sh" # Check Rust docs 10 | -------------------------------------------------------------------------------- /contrib/scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | args=( 6 | "-p nostr" 7 | "-p nostr-database" 8 | "-p nostr-lmdb" 9 | "-p nostr-mls-storage" 10 | "-p nostr-mls-memory-storage" 11 | "-p nostr-mls-sqlite-storage" 12 | "-p nostr-mls" 13 | "-p nostr-ndb" 14 | "-p nostr-indexeddb" 15 | "-p nostr-keyring" 16 | "-p nostr-relay-builder" 17 | "-p nostr-relay-pool" 18 | "-p nwc" 19 | "-p nostr-connect" 20 | "-p nostr-sdk" 21 | "-p nostr-cli" 22 | ) 23 | 24 | for arg in "${args[@]}"; 25 | do 26 | echo "Publishing '$arg'" 27 | cargo publish $arg 28 | echo 29 | done 30 | -------------------------------------------------------------------------------- /contrib/verify-commits/trusted-keys: -------------------------------------------------------------------------------- 1 | 86F3105ADFA8AB587268DCD78D3DCD04249619D1 -------------------------------------------------------------------------------- /crates/nostr-blossom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-blossom" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "A library for interacting with the Blossom protocol" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version.workspace = true 12 | keywords = ["nostr", "blossom"] 13 | 14 | [dependencies] 15 | base64.workspace = true 16 | nostr = { workspace = true, features = ["std"] } 17 | reqwest = { workspace = true, default-features = false, features = ["json", "rustls-tls"] } 18 | serde = { workspace = true, features = ["derive"] } 19 | 20 | [dev-dependencies] 21 | clap = { workspace = true, features = ["derive"] } 22 | tokio = { workspace = true, features = ["full"] } 23 | -------------------------------------------------------------------------------- /crates/nostr-blossom/README.md: -------------------------------------------------------------------------------- 1 | # Blossom 2 | 3 | A library for interacting with the [Blossom protocol](https://github.com/hzrd149/blossom). 4 | 5 | ## State 6 | 7 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 8 | 9 | ## Implemented BUDs 10 | 11 | - **Basic data structures:** [BUD-01](https://github.com/hzrd149/blossom/blob/master/buds/01.md), [BUD-02](https://github.com/hzrd149/blossom/blob/master/buds/02.md) 12 | - **Client:** [BUD-01](https://github.com/hzrd149/blossom/blob/master/buds/01.md), [BUD-02](https://github.com/hzrd149/blossom/blob/master/buds/02.md) 13 | - **Server:** Not implemented 14 | 15 | ## Donations 16 | 17 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 18 | 19 | ## License 20 | 21 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 22 | -------------------------------------------------------------------------------- /crates/nostr-blossom/examples/delete.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use nostr::hashes::sha256::Hash as Sha256Hash; 3 | use nostr::prelude::*; 4 | use nostr_blossom::prelude::*; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(author, version, about = "Delete a blob from a Blossom server", long_about = None)] 8 | struct Args { 9 | /// The server URL to connect to 10 | #[arg(long)] 11 | server: Url, 12 | 13 | /// The SHA256 hash of the blob to delete (in hex) 14 | #[arg(long)] 15 | sha256: Sha256Hash, 16 | 17 | /// Optional private key for signing the deletion 18 | #[arg(long, value_name = "PRIVATE_KEY")] 19 | private_key: SecretKey, 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<()> { 24 | let args = Args::parse(); 25 | 26 | let client = BlossomClient::new(args.server); 27 | 28 | // Create signer keys using the given private key 29 | let keys = Keys::new(args.private_key); 30 | 31 | println!("Attempting to delete blob with SHA256: {}", args.sha256); 32 | 33 | match client.delete_blob(args.sha256, None, &keys).await { 34 | Ok(()) => println!("Blob deleted successfully."), 35 | Err(e) => eprintln!("{e}"), 36 | } 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /crates/nostr-blossom/examples/download.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use clap::Parser; 4 | use nostr::hashes::sha256::Hash as Sha256Hash; 5 | use nostr::prelude::*; 6 | use nostr_blossom::prelude::*; 7 | 8 | #[derive(Parser, Debug)] 9 | #[command(author, version, about = "Download a blob from a Blossom server", long_about = None)] 10 | struct Args { 11 | /// The server URL to connect to 12 | #[arg(long)] 13 | server: Url, 14 | 15 | /// SHA256 hash of the blob to download 16 | #[arg(long)] 17 | sha256: Sha256Hash, 18 | 19 | /// Private key to use for authorization 20 | #[arg(long)] 21 | private_key: SecretKey, 22 | } 23 | 24 | #[tokio::main] 25 | async fn main() -> Result<()> { 26 | let args = Args::parse(); 27 | 28 | // Initialize the client. 29 | let client = BlossomClient::new(args.server); 30 | 31 | // Parse the private key. 32 | let keypair = Keys::new(args.private_key); 33 | 34 | // Download the blob with optional authorization. 35 | match client 36 | .get_blob(args.sha256, None, None, Some(&keypair)) 37 | .await 38 | { 39 | Ok(blob) => { 40 | println!("Successfully downloaded blob with {} bytes", blob.len()); 41 | let file_name = format!("{}", args.sha256); 42 | fs::write(&file_name, &blob)?; 43 | println!("Blob saved as {}", file_name); 44 | } 45 | Err(e) => { 46 | eprintln!("{e}"); 47 | } 48 | } 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /crates/nostr-blossom/examples/list.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use nostr::prelude::*; 3 | use nostr_blossom::prelude::*; 4 | 5 | #[derive(Parser, Debug)] 6 | #[command(author, version, about = "List blob on a Blossom server", long_about = None)] 7 | struct Args { 8 | /// The server URL to connect to 9 | #[arg(long)] 10 | server: Url, 11 | 12 | /// The public key to list blobs for 13 | #[arg(long)] 14 | pubkey: PublicKey, 15 | 16 | /// Optional private key for authorization (in hex) 17 | #[arg(long)] 18 | private_key: Option, 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<()> { 23 | let args = Args::parse(); 24 | let client = BlossomClient::new(args.server); 25 | 26 | // Check if a private key was provided and branch accordingly 27 | if let Some(private_key) = args.private_key { 28 | // Attempt to create the secret key, propagating error if parsing fails 29 | let keys = Keys::new(private_key); 30 | 31 | let descriptors = client 32 | .list_blobs(&args.pubkey, None, None, None, Some(&keys)) 33 | .await?; 34 | 35 | println!("Successfully listed blobs (with auth):"); 36 | for descriptor in descriptors { 37 | println!("{:?}", descriptor); 38 | } 39 | } else { 40 | let descriptors = client 41 | .list_blobs(&args.pubkey, None, None, None, None::<&Keys>) 42 | .await?; 43 | 44 | println!("Successfully listed blobs (without auth):"); 45 | for descriptor in descriptors { 46 | println!("{:?}", descriptor); 47 | } 48 | } 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /crates/nostr-blossom/examples/upload.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::PathBuf; 3 | 4 | use clap::Parser; 5 | use nostr::prelude::*; 6 | use nostr_blossom::prelude::*; 7 | 8 | #[derive(Parser, Debug)] 9 | #[command(author, version, about = "Upload a blob to a Blossom server", long_about = None)] 10 | struct Args { 11 | /// The server URL to connect to 12 | #[arg(long)] 13 | server: Url, 14 | 15 | /// Path to the file to upload 16 | #[arg(long)] 17 | file: PathBuf, 18 | 19 | /// Optional content type (e.g., "application/octet-stream") 20 | #[arg(long)] 21 | content_type: Option, 22 | 23 | /// Optional private key for signing the upload 24 | #[arg(long)] 25 | private_key: Option, 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() -> Result<()> { 30 | let args = Args::parse(); 31 | 32 | let client = BlossomClient::new(args.server); 33 | 34 | // Read file data from the specified file path. 35 | let data = fs::read(&args.file)?; 36 | 37 | // Use the provided content type or default to "application/octet-stream" 38 | let content_type = args 39 | .content_type 40 | .clone() 41 | .or_else(|| Some("application/octet-stream".to_string())); 42 | 43 | // Create signer keys. 44 | // If a private key is provided, try to use it; otherwise generate a new key. 45 | let keys = match args.private_key { 46 | Some(private_key) => Keys::new(private_key), 47 | None => Keys::generate(), 48 | }; 49 | 50 | match client 51 | .upload_blob(data, content_type, None, Some(&keys)) 52 | .await 53 | { 54 | Ok(descriptor) => { 55 | println!("Successfully uploaded blob: {:?}", descriptor); 56 | } 57 | Err(e) => { 58 | eprintln!("{e}"); 59 | } 60 | } 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /crates/nostr-blossom/src/bud02.rs: -------------------------------------------------------------------------------- 1 | //! Implements data structures specific to BUD-02 2 | 3 | use nostr::hashes::sha256::Hash as Sha256Hash; 4 | use nostr::{Timestamp, Url}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// A descriptor for the blob 8 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 9 | pub struct BlobDescriptor { 10 | /// The URL at which the blob/file can be accessed 11 | pub url: Url, 12 | /// The SHA256 hash of the contents in the blob 13 | pub sha256: Sha256Hash, 14 | /// The size of the blob/file, in bytes 15 | pub size: u32, 16 | #[serde(rename = "type")] 17 | /// Mime type of the blob/file 18 | pub mime_type: Option, 19 | /// The date at which the blob was uploaded, as a UNIX timestamp (in seconds) 20 | pub uploaded: Timestamp, 21 | } 22 | -------------------------------------------------------------------------------- /crates/nostr-blossom/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! This crate implements the Blossom protocol for decentralized content storage and retrieval. 6 | //! 7 | //! The Blossom protocol defines a standard for storing and retrieving blobs (binary large objects) 8 | //! in a decentralized manner, using the Nostr protocol for authorization and discovery. 9 | //! 10 | //! 11 | 12 | #![forbid(unsafe_code)] 13 | #![warn(missing_docs)] 14 | #![warn(rustdoc::bare_urls)] 15 | #![warn(clippy::large_futures)] 16 | 17 | pub mod bud01; 18 | pub mod bud02; 19 | pub mod client; 20 | pub mod error; 21 | pub mod prelude; 22 | -------------------------------------------------------------------------------- /crates/nostr-blossom/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unused_imports)] 8 | #![allow(unknown_lints)] 9 | #![allow(ambiguous_glob_reexports)] 10 | #![doc(hidden)] 11 | 12 | pub use crate::bud01::*; 13 | pub use crate::bud02::*; 14 | pub use crate::client::*; 15 | pub use crate::error::*; 16 | -------------------------------------------------------------------------------- /crates/nostr-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-cli" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Nostr CLI" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | keywords = ["nostr", "cli"] 12 | 13 | [dependencies] 14 | clap = { workspace = true, features = ["derive"] } 15 | dialoguer = "0.11" 16 | dirs = "5.0" 17 | indicatif = "0.17" 18 | nostr-connect.workspace = true 19 | nostr-relay-builder.workspace = true 20 | nostr-sdk = { workspace = true, features = ["all-nips", "lmdb", "tor"] } 21 | once_cell = { version = "1.21", default-features = false } 22 | prettytable-rs = "0.10" 23 | regex = "1.11.1" 24 | rustyline = { version = "16.0", default-features = false, features = ["with-file-history"] } 25 | tokio = { workspace = true, features = ["full"] } 26 | -------------------------------------------------------------------------------- /crates/nostr-cli/README.md: -------------------------------------------------------------------------------- 1 | # Nostr CLI 2 | 3 | ## Install 4 | 5 | ```bash 6 | cargo install nostr-cli 7 | ``` 8 | 9 | ## Usage 10 | 11 | Check all commands with: 12 | 13 | ```bash 14 | nostr-cli --help 15 | ``` 16 | 17 | ## State 18 | 19 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 20 | 21 | ## Donations 22 | 23 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 24 | 25 | ## License 26 | 27 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 28 | -------------------------------------------------------------------------------- /crates/nostr-cli/src/cli/io.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use dialoguer::{Confirm, Input, Password}; 6 | use nostr_sdk::{Keys, Result}; 7 | 8 | pub fn get_optional_input(prompt: S) -> Result> 9 | where 10 | S: Into, 11 | { 12 | let input: String = Input::new() 13 | .with_prompt(prompt) 14 | .allow_empty(true) 15 | .default(String::new()) 16 | .interact_text()?; 17 | 18 | if input.is_empty() { 19 | Ok(None) 20 | } else { 21 | Ok(Some(input)) 22 | } 23 | } 24 | 25 | pub fn get_keys(prompt: S) -> Result 26 | where 27 | S: Into, 28 | { 29 | let secret_key = Password::new().with_prompt(prompt).interact()?; 30 | Ok(Keys::parse(&secret_key)?) 31 | } 32 | 33 | /* pub fn get_password_with_confirmation() -> Result { 34 | Ok(Password::new() 35 | .with_prompt("New password") 36 | .with_confirmation("Confirm password", "Passwords mismatching") 37 | .interact()?) 38 | } */ 39 | 40 | pub fn ask(prompt: S) -> Result 41 | where 42 | S: Into, 43 | { 44 | if Confirm::new() 45 | .with_prompt(prompt) 46 | .default(false) 47 | .interact()? 48 | { 49 | Ok(true) 50 | } else { 51 | Ok(false) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/nostr-cli/src/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_sdk::prelude::*; 6 | use prettytable::{row, Table}; 7 | 8 | pub fn print_events(events: I, json: bool) 9 | where 10 | I: IntoIterator, 11 | { 12 | if json { 13 | for (index, event) in events.into_iter().enumerate() { 14 | println!("{}. {}", index + 1, event.as_pretty_json()); 15 | } 16 | } else { 17 | let mut table: Table = Table::new(); 18 | 19 | table.set_titles(row!["#", "ID", "Author", "Kind", "Created At"]); 20 | 21 | for (index, event) in events.into_iter().enumerate() { 22 | table.add_row(row![ 23 | index + 1, 24 | event.id, 25 | event.pubkey, 26 | event.kind, 27 | event.created_at.to_human_datetime() 28 | ]); 29 | } 30 | 31 | table.printstd(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/nostr-connect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-connect" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Nostr Connect (NIP46)" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version.workspace = true 12 | keywords = ["nostr", "signer", "nip46", "nostr-connect"] 13 | 14 | [features] 15 | default = [] 16 | tor = ["nostr-relay-pool/tor"] 17 | 18 | [dependencies] 19 | async-utility.workspace = true 20 | nostr = { workspace = true, features = ["std", "nip04", "nip44", "nip46"] } 21 | nostr-relay-pool.workspace = true 22 | tokio = { workspace = true, features = ["sync"] } 23 | tracing.workspace = true 24 | 25 | [dev-dependencies] 26 | dialoguer = "0.11" 27 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 28 | webbrowser = "1.0" 29 | 30 | [[example]] 31 | name = "handle-auth-url" 32 | 33 | [[example]] 34 | name = "nostr-connect-signer" 35 | -------------------------------------------------------------------------------- /crates/nostr-connect/README.md: -------------------------------------------------------------------------------- 1 | # Nostr Connect (NIP46) 2 | 3 | ## Description 4 | 5 | This NIP introduces remote signing for Nostr, allowing for secure and 6 | decentralized signing of events using a hardware device or remote signer. The 7 | protocol enables 2-way communication between the remote signer and a Nostr 8 | client, providing a method for private key exposure minimization and improved 9 | security. 10 | 11 | ## State 12 | 13 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 14 | 15 | ## Donations 16 | 17 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 18 | 19 | ## License 20 | 21 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 22 | -------------------------------------------------------------------------------- /crates/nostr-connect/examples/handle-auth-url.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_connect::prelude::*; 8 | 9 | #[derive(Debug, Clone)] 10 | struct MyAuthUrlHandler; 11 | 12 | impl AuthUrlHandler for MyAuthUrlHandler { 13 | fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { 14 | Box::pin(async move { 15 | println!("Opening auth url: {auth_url}"); 16 | webbrowser::open(auth_url.as_str())?; 17 | Ok(()) 18 | }) 19 | } 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<()> { 24 | tracing_subscriber::fmt::init(); 25 | 26 | let uri = NostrConnectURI::parse("bunker://79dff8f82963424e0bb02708a22e44b4980893e3a4be0fa3cb60a43b946764e3?relay=wss://relay.nsec.app")?; 27 | let app_keys = Keys::parse("nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99")?; 28 | let timeout = Duration::from_secs(120); 29 | 30 | let mut connect = NostrConnect::new(uri, app_keys, timeout, None)?; 31 | 32 | // Set auth_url handler 33 | connect.auth_url_handler(MyAuthUrlHandler); 34 | 35 | let receiver = 36 | PublicKey::parse("npub1acg6thl5psv62405rljzkj8spesceyfz2c32udakc2ak0dmvfeyse9p35c")?; 37 | let content = connect.nip44_encrypt(&receiver, "Hi").await?; 38 | println!("Content: {content}"); 39 | 40 | let event = EventBuilder::text_note("Testing rust-nostr") 41 | .sign(&connect) 42 | .await?; 43 | println!("Event: {}", event.as_json()); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /crates/nostr-connect/examples/nostr-connect-signer.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use dialoguer::Confirm; 6 | use nostr_connect::prelude::*; 7 | 8 | const SIGNER_SECRET_KEY: &str = "nsec12kcgs78l06p30jz7z7h3n2x2cy99nw2z6zspjdp7qc206887mwvs95lnkx"; 9 | const USER_SECRET_KEY: &str = "nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85"; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | tracing_subscriber::fmt::init(); 14 | 15 | let keys = NostrConnectKeys { 16 | signer: Keys::parse(SIGNER_SECRET_KEY)?, 17 | user: Keys::parse(USER_SECRET_KEY)?, 18 | }; 19 | 20 | // Compose signer 21 | let signer = NostrConnectRemoteSigner::new(keys, ["wss://relay.nsec.app"], None, None)?; 22 | 23 | // Compose signer from URI 24 | // let uri = NostrConnectURI::parse("nostrconnect://...")?; 25 | // let signer = NostrConnectRemoteSigner::from_uri(uri, keys, None, None)?; 26 | 27 | // Print bunker URI 28 | let uri = signer.bunker_uri(); 29 | println!("\n{uri}\n"); 30 | 31 | // Serve signer 32 | signer.serve(CustomActions).await?; 33 | 34 | Ok(()) 35 | } 36 | 37 | struct CustomActions; 38 | 39 | impl NostrConnectSignerActions for CustomActions { 40 | fn approve(&self, public_key: &PublicKey, req: &NostrConnectRequest) -> bool { 41 | println!("Public key: {public_key}"); 42 | println!("{req:#?}\n"); 43 | Confirm::new() 44 | .with_prompt("Approve request?") 45 | .default(false) 46 | .interact() 47 | .unwrap_or_default() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/nostr-connect/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Nostr Connect (NIP46) 6 | //! 7 | //! 8 | 9 | #![forbid(unsafe_code)] 10 | #![warn(missing_docs)] 11 | #![warn(rustdoc::bare_urls)] 12 | #![warn(clippy::large_futures)] 13 | 14 | pub mod client; 15 | pub mod error; 16 | pub mod prelude; 17 | pub mod signer; 18 | -------------------------------------------------------------------------------- /crates/nostr-connect/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | #![doc(hidden)] 10 | 11 | pub use nostr::prelude::*; 12 | 13 | pub use crate::client::*; 14 | pub use crate::error::*; 15 | pub use crate::signer::*; 16 | -------------------------------------------------------------------------------- /crates/nostr-database/.gitignore: -------------------------------------------------------------------------------- 1 | *.svg 2 | tracing.folded 3 | perf.data 4 | perf.data.old 5 | many-events.json* -------------------------------------------------------------------------------- /crates/nostr-database/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-database" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Events database abstraction and in-memory implementation" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version.workspace = true 12 | keywords = ["nostr", "database"] 13 | 14 | [features] 15 | default = [] 16 | flatbuf = ["dep:flatbuffers"] 17 | 18 | [dependencies] 19 | flatbuffers = { version = "23.5", optional = true } 20 | lru.workspace = true 21 | nostr = { workspace = true, features = ["std"] } 22 | tokio = { workspace = true, features = ["sync"] } 23 | 24 | [dev-dependencies] 25 | tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } 26 | -------------------------------------------------------------------------------- /crates/nostr-database/README.md: -------------------------------------------------------------------------------- 1 | # Nostr (Events) Database 2 | 3 | Events database abstraction and in-memory implementation for nostr apps. 4 | 5 | ## Nostr Database Trait 6 | 7 | This library contains the `NostrDatabase` and `NostrDatabaseExt` traits. You can use the [default backends](#default-backends) or implement your one (like PostgreSQL, ...). 8 | 9 | ## Default backends 10 | 11 | * Memory (RAM, both native and web), available in this library 12 | * LMDB (native), available at [`nostr-lmdb`](https://crates.io/crates/nostr-lmdb) 13 | * [nostrdb](https://github.com/damus-io/nostrdb) (native), available at [`nostr-ndb`](https://crates.io/crates/nostr-ndb) 14 | * IndexedDB (web), available at [`nostr-indexeddb`](https://crates.io/crates/nostr-indexeddb) 15 | 16 | ## Crate Feature Flags 17 | 18 | The following crate feature flags are available: 19 | 20 | | Feature | Default | Description | 21 | |-----------|:-------:|--------------------------------------------------------| 22 | | `flatbuf` | No | Enable `flatbuffers` de/serialization for nostr events | 23 | 24 | ## State 25 | 26 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 27 | 28 | ## Donations 29 | 30 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 31 | 32 | ## License 33 | 34 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 35 | -------------------------------------------------------------------------------- /crates/nostr-database/fbs/event.fbs: -------------------------------------------------------------------------------- 1 | namespace EventFbs; 2 | 3 | struct Fixed32Bytes { 4 | val: [ubyte:32]; 5 | } 6 | 7 | struct Fixed64Bytes { 8 | val: [ubyte:64]; 9 | } 10 | 11 | table StringVector { 12 | data: [string]; 13 | } 14 | 15 | table Event { 16 | id: Fixed32Bytes; 17 | pubkey: Fixed32Bytes; 18 | created_at: ulong; 19 | kind: ulong; 20 | tags: [StringVector]; 21 | content: string; 22 | sig: Fixed64Bytes; 23 | } 24 | 25 | root_type Event; -------------------------------------------------------------------------------- /crates/nostr-database/justfile: -------------------------------------------------------------------------------- 1 | flatbuf: 2 | flatc --rust -o ./src/flatbuffers ./fbs/event.fbs 3 | flatc --rust -o ./src/flatbuffers ./fbs/event_seen_by.fbs -------------------------------------------------------------------------------- /crates/nostr-database/src/collections/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | pub mod events; 6 | pub mod tree; 7 | -------------------------------------------------------------------------------- /crates/nostr-database/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Nostr Database Error 6 | 7 | use std::fmt; 8 | 9 | /// Database Error 10 | #[derive(Debug)] 11 | pub enum DatabaseError { 12 | /// An error happened in the underlying database backend. 13 | Backend(Box), 14 | /// Not supported 15 | NotSupported, 16 | } 17 | 18 | impl std::error::Error for DatabaseError {} 19 | 20 | impl fmt::Display for DatabaseError { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | match self { 23 | Self::Backend(e) => write!(f, "{e}"), 24 | Self::NotSupported => write!(f, "not supported"), 25 | } 26 | } 27 | } 28 | 29 | impl DatabaseError { 30 | /// Create a new backend error 31 | /// 32 | /// Shorthand for `Error::Backend(Box::new(error))`. 33 | #[inline] 34 | pub fn backend(error: E) -> Self 35 | where 36 | E: std::error::Error + Send + Sync + 'static, 37 | { 38 | Self::Backend(Box::new(error)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/nostr-database/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | #![doc(hidden)] 10 | 11 | pub use nostr::prelude::*; 12 | 13 | pub use crate::ext::*; 14 | pub use crate::memory::{self, *}; 15 | pub use crate::*; 16 | -------------------------------------------------------------------------------- /crates/nostr-database/src/profile.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Profile 6 | 7 | use core::cmp::Ordering; 8 | use core::hash::{Hash, Hasher}; 9 | 10 | use nostr::{Metadata, PublicKey}; 11 | 12 | /// Profile 13 | #[derive(Debug, Clone)] 14 | pub struct Profile { 15 | public_key: PublicKey, 16 | metadata: Metadata, 17 | } 18 | 19 | impl PartialEq for Profile { 20 | fn eq(&self, other: &Self) -> bool { 21 | self.public_key == other.public_key 22 | } 23 | } 24 | 25 | impl Eq for Profile {} 26 | 27 | impl PartialOrd for Profile { 28 | fn partial_cmp(&self, other: &Self) -> Option { 29 | Some(self.cmp(other)) 30 | } 31 | } 32 | 33 | impl Ord for Profile { 34 | fn cmp(&self, other: &Self) -> Ordering { 35 | self.name().cmp(&other.name()) 36 | } 37 | } 38 | 39 | impl Hash for Profile { 40 | fn hash(&self, state: &mut H) { 41 | self.public_key.hash(state) 42 | } 43 | } 44 | 45 | impl From for Profile { 46 | fn from(public_key: PublicKey) -> Self { 47 | Self::new(public_key, Metadata::default()) 48 | } 49 | } 50 | 51 | impl Profile { 52 | /// Compose new profile 53 | pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { 54 | Self { 55 | public_key, 56 | metadata, 57 | } 58 | } 59 | 60 | /// Get profile public key 61 | pub fn public_key(&self) -> PublicKey { 62 | self.public_key 63 | } 64 | 65 | /// Get profile metadata 66 | pub fn metadata(&self) -> Metadata { 67 | self.metadata.clone() 68 | } 69 | 70 | /// Get profile name 71 | /// 72 | /// Steps (go to next step if field is `None` or `empty`): 73 | /// * Check `display_name` field 74 | /// * Check `name` field 75 | /// * Return cutted public key (ex. `00000000:00000002`) 76 | pub fn name(&self) -> String { 77 | if let Some(display_name) = &self.metadata.display_name { 78 | if !display_name.is_empty() { 79 | return display_name.clone(); 80 | } 81 | } 82 | 83 | if let Some(name) = &self.metadata.name { 84 | if !name.is_empty() { 85 | return name.clone(); 86 | } 87 | } 88 | 89 | cut_public_key(self.public_key) 90 | } 91 | } 92 | 93 | /// Get the first and last 8 chars of a [`PublicKey`] 94 | /// 95 | /// Ex. `00000000:00000002` 96 | pub fn cut_public_key(pk: PublicKey) -> String { 97 | let pk = pk.to_string(); 98 | format!("{}:{}", &pk[0..8], &pk[pk.len() - 8..]) 99 | } 100 | -------------------------------------------------------------------------------- /crates/nostr-indexeddb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-indexeddb" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Web's IndexedDB Storage backend for Nostr apps" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version.workspace = true 12 | keywords = ["nostr", "database", "indexeddb"] 13 | 14 | [dependencies] 15 | indexed_db_futures = "0.5" 16 | nostr = { workspace = true, features = ["std"] } 17 | nostr-database = { workspace = true, features = ["flatbuf"] } 18 | wasm-bindgen.workspace = true 19 | -------------------------------------------------------------------------------- /crates/nostr-indexeddb/README.md: -------------------------------------------------------------------------------- 1 | # Nostr IndexedDB 2 | 3 | This crate implements a storage backend on IndexedDB for web environments. 4 | 5 | ## State 6 | 7 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 8 | 9 | ## Donations 10 | 11 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 12 | 13 | ## License 14 | 15 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details -------------------------------------------------------------------------------- /crates/nostr-indexeddb/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::fmt; 6 | 7 | use nostr_database::DatabaseError; 8 | 9 | /// IndexedDB error 10 | #[derive(Debug)] 11 | pub enum IndexedDBError { 12 | /// DOM error 13 | DomException { 14 | /// DomException code 15 | code: u16, 16 | /// Specific name of the DomException 17 | name: String, 18 | /// Message given to the DomException 19 | message: String, 20 | }, 21 | /// Mutex poisoned 22 | MutexPoisoned, 23 | } 24 | 25 | impl std::error::Error for IndexedDBError {} 26 | 27 | impl fmt::Display for IndexedDBError { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | Self::DomException { 31 | name, 32 | code, 33 | message, 34 | } => write!(f, "DomException {name} ({code}): {message}"), 35 | Self::MutexPoisoned => write!(f, "mutex poisoned"), 36 | } 37 | } 38 | } 39 | 40 | impl From for IndexedDBError { 41 | fn from(frm: indexed_db_futures::web_sys::DomException) -> Self { 42 | Self::DomException { 43 | name: frm.name(), 44 | message: frm.message(), 45 | code: frm.code(), 46 | } 47 | } 48 | } 49 | 50 | impl From for DatabaseError { 51 | fn from(e: IndexedDBError) -> Self { 52 | Self::backend(e) 53 | } 54 | } 55 | 56 | pub(crate) fn into_err(e: indexed_db_futures::web_sys::DomException) -> DatabaseError { 57 | let indexed_err: IndexedDBError = e.into(); 58 | DatabaseError::backend(indexed_err) 59 | } 60 | -------------------------------------------------------------------------------- /crates/nostr-keyring/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-keyring" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Keyring for nostr" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version = "1.75.0" 12 | keywords = ["nostr", "keyring"] 13 | 14 | [features] 15 | default = [] 16 | # Enables async APIs 17 | async = ["dep:async-utility"] 18 | 19 | [dependencies] 20 | async-utility = { workspace = true, optional = true } 21 | keyring = { version = "3.6", features = ["apple-native", "linux-native", "windows-native"] } # MSRV: 1.75.0 22 | nostr = { workspace = true, features = ["std"] } 23 | 24 | [dev-dependencies] 25 | tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } 26 | 27 | [[example]] 28 | name = "async" 29 | required-features = ["async"] 30 | 31 | [[example]] 32 | name = "blocking" 33 | -------------------------------------------------------------------------------- /crates/nostr-keyring/README.md: -------------------------------------------------------------------------------- 1 | # Nostr Keyring 2 | 3 | ## Crate Feature Flags 4 | 5 | The following crate feature flags are available: 6 | 7 | | Feature | Default | Description | 8 | |---------|:-------:|-------------------------------------------| 9 | | `async` | No | Enable async APIs | 10 | 11 | ## State 12 | 13 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 14 | 15 | ## Donations 16 | 17 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 18 | 19 | ## License 20 | 21 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 22 | -------------------------------------------------------------------------------- /crates/nostr-keyring/examples/async.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_keyring::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | let keys = Keys::parse("nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99")?; 10 | 11 | let keyring = NostrKeyring::new("rust-nostr-test"); 12 | 13 | keyring.set_async("test", &keys).await?; 14 | 15 | let found_keys = keyring.get_async("test").await?; 16 | 17 | assert_eq!(keys, found_keys); 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /crates/nostr-keyring/examples/blocking.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_keyring::prelude::*; 6 | 7 | fn main() -> Result<()> { 8 | let keys = Keys::parse("nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99")?; 9 | 10 | let keyring = NostrKeyring::new("rust-nostr-test"); 11 | 12 | keyring.set("test", &keys)?; 13 | 14 | let found_keys = keyring.get("test")?; 15 | 16 | assert_eq!(keys, found_keys); 17 | 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /crates/nostr-keyring/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | #![doc(hidden)] 10 | 11 | pub use nostr::prelude::*; 12 | 13 | pub use crate::*; 14 | -------------------------------------------------------------------------------- /crates/nostr-lmdb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-lmdb" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "LMDB storage backend for nostr apps" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version = "1.72.0" 12 | keywords = ["nostr", "database", "lmdb"] 13 | 14 | [dependencies] 15 | async-utility.workspace = true 16 | nostr = { workspace = true, features = ["std"] } 17 | nostr-database = { workspace = true, features = ["flatbuf"] } 18 | tokio = { workspace = true, features = ["sync"] } 19 | tracing.workspace = true 20 | 21 | [target.'cfg(not(all(target_os = "macos", target_os = "ios")))'.dependencies] 22 | heed = { version = "0.20", default-features = false, features = ["read-txn-no-tls"] } 23 | 24 | # POSIX semaphores are required for Apple's App Sandbox on iOS & macOS 25 | [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] 26 | heed = { version = "0.20", default-features = false, features = ["read-txn-no-tls", "posix-sem"] } 27 | 28 | [dev-dependencies] 29 | tempfile.workspace = true 30 | tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } 31 | -------------------------------------------------------------------------------- /crates/nostr-lmdb/README.md: -------------------------------------------------------------------------------- 1 | # Nostr LMDB 2 | 3 | LMDB storage backend for nostr apps 4 | 5 | ## State 6 | 7 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 8 | 9 | ## Donations 10 | 11 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 12 | 13 | ## License 14 | 15 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 16 | -------------------------------------------------------------------------------- /crates/nostr-lmdb/src/store/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Michael Dilger 2 | // Copyright (c) 2022-2023 Yuki Kishimoto 3 | // Copyright (c) 2023-2025 Rust Nostr Developers 4 | // Distributed under the MIT software license 5 | 6 | use std::{fmt, io}; 7 | 8 | use async_utility::tokio::task::JoinError; 9 | use nostr::{key, secp256k1}; 10 | use nostr_database::flatbuffers; 11 | use tokio::sync::oneshot; 12 | 13 | #[derive(Debug)] 14 | pub enum Error { 15 | /// An upstream I/O error 16 | Io(io::Error), 17 | /// An error from LMDB 18 | Heed(heed::Error), 19 | /// Flatbuffers error 20 | FlatBuffers(flatbuffers::Error), 21 | Thread(JoinError), 22 | Key(key::Error), 23 | Secp256k1(secp256k1::Error), 24 | OneshotRecv(oneshot::error::RecvError), 25 | /// MPSC send error 26 | MpscSend, 27 | /// The event kind is wrong 28 | WrongEventKind, 29 | /// Not found 30 | NotFound, 31 | } 32 | 33 | impl std::error::Error for Error {} 34 | 35 | impl fmt::Display for Error { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | match self { 38 | Self::Io(e) => write!(f, "{e}"), 39 | Self::Heed(e) => write!(f, "{e}"), 40 | Self::FlatBuffers(e) => write!(f, "{e}"), 41 | Self::Thread(e) => write!(f, "{e}"), 42 | Self::Key(e) => write!(f, "{e}"), 43 | Self::Secp256k1(e) => write!(f, "{e}"), 44 | Self::OneshotRecv(e) => write!(f, "{e}"), 45 | Self::MpscSend => write!(f, "mpsc channel send error"), 46 | Self::NotFound => write!(f, "Not found"), 47 | Self::WrongEventKind => write!(f, "Wrong event kind"), 48 | } 49 | } 50 | } 51 | 52 | impl From for Error { 53 | fn from(e: io::Error) -> Self { 54 | Self::Io(e) 55 | } 56 | } 57 | 58 | impl From for Error { 59 | fn from(e: heed::Error) -> Self { 60 | Self::Heed(e) 61 | } 62 | } 63 | 64 | impl From for Error { 65 | fn from(e: flatbuffers::Error) -> Self { 66 | Self::FlatBuffers(e) 67 | } 68 | } 69 | 70 | impl From for Error { 71 | fn from(e: JoinError) -> Self { 72 | Self::Thread(e) 73 | } 74 | } 75 | 76 | impl From for Error { 77 | fn from(e: key::Error) -> Self { 78 | Self::Key(e) 79 | } 80 | } 81 | 82 | impl From for Error { 83 | fn from(e: secp256k1::Error) -> Self { 84 | Self::Secp256k1(e) 85 | } 86 | } 87 | 88 | impl From for Error { 89 | fn from(e: oneshot::error::RecvError) -> Self { 90 | Self::OneshotRecv(e) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/nostr-lmdb/src/store/types/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | mod filter; 6 | 7 | pub use self::filter::DatabaseFilter; 8 | -------------------------------------------------------------------------------- /crates/nostr-mls-memory-storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-mls-memory-storage" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "In-memory database for nostr-mls that implements the NostrMlsStorageProvider Trait" 6 | authors = ["Jeff Gardner ", "Yuki Kishimoto ", "Rust Nostr Developers"] 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version = "1.74.0" 12 | keywords = ["nostr", "mls", "openmls", "memory"] 13 | 14 | [dependencies] 15 | lru.workspace = true 16 | nostr = { workspace = true, features = ["std"] } 17 | nostr-mls-storage.workspace = true 18 | openmls = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 19 | openmls_memory_storage = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 20 | parking_lot = "0.12" 21 | -------------------------------------------------------------------------------- /crates/nostr-mls-memory-storage/README.md: -------------------------------------------------------------------------------- 1 | # Nostr MLS Memory Storage 2 | 3 | Memory-based storage implementation for [Nostr MLS](../nostr-mls). This crate provides a storage backend that implements the `NostrMlsStorageProvider` trait from the [nostr-mls-storage](../nostr-mls-storage) crate. 4 | 5 | ## Features 6 | 7 | - Uses an LRU (Least Recently Used) caching mechanism to store data in memory 8 | - Provides both read and write operations that are thread-safe through `parking_lot::RwLock` 9 | - Configurable cache size (default: 1000 items) 10 | - Non-persistent storage that is cleared when the application terminates 11 | 12 | ## Performance 13 | 14 | This implementation uses `parking_lot::RwLock` instead of the standard library's `std::sync::RwLock` for improved performance. The `parking_lot` implementation offers several advantages: 15 | 16 | - Smaller memory footprint 17 | - Faster lock acquisition and release 18 | - No poisoning on panic 19 | - More efficient read-heavy workloads, which is ideal for this caching implementation 20 | - Consistent behavior across different platforms 21 | 22 | ## Example Usage 23 | 24 | ```rust,ignore 25 | use nostr_mls_memory_storage::NostrMlsMemoryStorage; 26 | use nostr_mls_storage::NostrMlsStorageProvider; 27 | 28 | // Create a new memory storage instance 29 | let storage = NostrMlsMemoryStorage::default(); 30 | 31 | // Or create with a custom cache size 32 | let custom_storage = NostrMlsMemoryStorage::with_cache_size(100); 33 | ``` 34 | 35 | For more advanced usage examples, see the tests in the source code. 36 | 37 | ## State 38 | 39 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 40 | 41 | ## Donations 42 | 43 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 44 | 45 | ## License 46 | 47 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 48 | -------------------------------------------------------------------------------- /crates/nostr-mls-memory-storage/src/messages.rs: -------------------------------------------------------------------------------- 1 | //! Memory-based storage implementation of the NostrMlsStorageProvider trait for Nostr MLS messages 2 | 3 | use nostr::EventId; 4 | use nostr_mls_storage::messages::error::MessageError; 5 | use nostr_mls_storage::messages::types::*; 6 | use nostr_mls_storage::messages::MessageStorage; 7 | 8 | use crate::NostrMlsMemoryStorage; 9 | 10 | impl MessageStorage for NostrMlsMemoryStorage { 11 | fn save_message(&self, message: Message) -> Result<(), MessageError> { 12 | // Save in the messages cache 13 | let mut cache = self.messages_cache.write(); 14 | cache.put(message.id, message.clone()); 15 | 16 | // Save in the messages_by_group cache 17 | let mut group_cache = self.messages_by_group_cache.write(); 18 | match group_cache.get_mut(&message.mls_group_id) { 19 | Some(group_messages) => { 20 | // TODO: time complexity here is O(n). We probably want to use another data struct here. 21 | 22 | // Find and update existing message or add new one 23 | match group_messages.iter().position(|m| m.id == message.id) { 24 | Some(idx) => { 25 | group_messages[idx] = message; 26 | } 27 | None => { 28 | group_messages.push(message); 29 | } 30 | } 31 | } 32 | // Not found, insert new 33 | None => { 34 | group_cache.put(message.mls_group_id.clone(), vec![message]); 35 | } 36 | } 37 | 38 | Ok(()) 39 | } 40 | 41 | fn find_message_by_event_id( 42 | &self, 43 | event_id: &EventId, 44 | ) -> Result, MessageError> { 45 | let cache = self.messages_cache.read(); 46 | Ok(cache.peek(event_id).cloned()) 47 | } 48 | 49 | fn find_processed_message_by_event_id( 50 | &self, 51 | event_id: &EventId, 52 | ) -> Result, MessageError> { 53 | let cache = self.processed_messages_cache.read(); 54 | Ok(cache.peek(event_id).cloned()) 55 | } 56 | 57 | fn save_processed_message( 58 | &self, 59 | processed_message: ProcessedMessage, 60 | ) -> Result<(), MessageError> { 61 | let mut cache = self.processed_messages_cache.write(); 62 | cache.put(processed_message.wrapper_event_id, processed_message); 63 | 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/nostr-mls-memory-storage/src/welcomes.rs: -------------------------------------------------------------------------------- 1 | //! Memory-based storage implementation of the NostrMlsStorageProvider trait for Nostr MLS welcomes 2 | 3 | use nostr::EventId; 4 | use nostr_mls_storage::welcomes::error::WelcomeError; 5 | use nostr_mls_storage::welcomes::types::*; 6 | use nostr_mls_storage::welcomes::WelcomeStorage; 7 | 8 | use crate::NostrMlsMemoryStorage; 9 | 10 | impl WelcomeStorage for NostrMlsMemoryStorage { 11 | fn save_welcome(&self, welcome: Welcome) -> Result<(), WelcomeError> { 12 | let mut cache = self.welcomes_cache.write(); 13 | cache.put(welcome.id, welcome); 14 | 15 | Ok(()) 16 | } 17 | 18 | fn pending_welcomes(&self) -> Result, WelcomeError> { 19 | let cache = self.welcomes_cache.read(); 20 | let welcomes: Vec = cache 21 | .iter() 22 | .map(|(_, v)| v.clone()) 23 | .filter(|welcome| welcome.state == WelcomeState::Pending) 24 | .collect(); 25 | 26 | Ok(welcomes) 27 | } 28 | 29 | fn find_welcome_by_event_id( 30 | &self, 31 | event_id: &EventId, 32 | ) -> Result, WelcomeError> { 33 | let cache = self.welcomes_cache.read(); 34 | Ok(cache.peek(event_id).cloned()) 35 | } 36 | 37 | fn save_processed_welcome( 38 | &self, 39 | processed_welcome: ProcessedWelcome, 40 | ) -> Result<(), WelcomeError> { 41 | let mut cache = self.processed_welcomes_cache.write(); 42 | cache.put(processed_welcome.wrapper_event_id, processed_welcome); 43 | 44 | Ok(()) 45 | } 46 | 47 | fn find_processed_welcome_by_event_id( 48 | &self, 49 | event_id: &EventId, 50 | ) -> Result, WelcomeError> { 51 | let cache = self.processed_welcomes_cache.read(); 52 | Ok(cache.peek(event_id).cloned()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/nostr-mls-sqlite-storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-mls-sqlite-storage" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Sqlite database for nostr-mls that implements the NostrMlsStorageProvider Trait" 6 | authors = ["Jeff Gardner ", "Yuki Kishimoto ", "Rust Nostr Developers"] 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version = "1.74.0" 12 | keywords = ["nostr", "mls", "openmls", "sqlite"] 13 | 14 | [dependencies] 15 | nostr = { workspace = true, features = ["std"] } 16 | nostr-mls-storage.workspace = true 17 | openmls = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 18 | openmls_sqlite_storage = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 19 | refinery = { version = "0.8", features = ["rusqlite"] } # MSRV is 1.75.0 20 | rusqlite = { version = "0.32", features = ["bundled"] } 21 | serde = { workspace = true, features = ["derive"] } 22 | serde_json.workspace = true 23 | tracing = { workspace = true, features = ["std"] } 24 | 25 | [dev-dependencies] 26 | tempfile.workspace = true 27 | -------------------------------------------------------------------------------- /crates/nostr-mls-sqlite-storage/README.md: -------------------------------------------------------------------------------- 1 | # Nostr MLS Sqlite Storage 2 | 3 | Sqlite MLS storage backend for nostr apps 4 | 5 | ## State 6 | 7 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 8 | 9 | ## Donations 10 | 11 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 12 | 13 | ## License 14 | 15 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 16 | -------------------------------------------------------------------------------- /crates/nostr-mls-sqlite-storage/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the SQLite storage implementation. 2 | 3 | use std::fmt; 4 | 5 | /// Error type for SQLite storage operations. 6 | #[derive(Debug)] 7 | pub enum Error { 8 | /// SQLite database error 9 | Database(String), 10 | /// Error from rusqlite 11 | Rusqlite(rusqlite::Error), 12 | /// Error during database migration 13 | Refinery(refinery::Error), 14 | /// Error from OpenMLS 15 | OpenMls(String), 16 | } 17 | 18 | impl std::error::Error for Error {} 19 | 20 | impl fmt::Display for Error { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | match self { 23 | Self::Database(msg) => write!(f, "Database error: {}", msg), 24 | Self::Rusqlite(err) => write!(f, "SQLite error: {}", err), 25 | Self::Refinery(msg) => write!(f, "Migration error: {}", msg), 26 | Self::OpenMls(msg) => write!(f, "OpenMLS error: {}", msg), 27 | } 28 | } 29 | } 30 | 31 | impl From for Error { 32 | fn from(e: rusqlite::Error) -> Self { 33 | Self::Rusqlite(e) 34 | } 35 | } 36 | 37 | impl From for Error { 38 | fn from(e: refinery::Error) -> Self { 39 | Self::Refinery(e) 40 | } 41 | } 42 | 43 | impl From for Error { 44 | fn from(e: std::io::Error) -> Self { 45 | Self::Database(format!("IO error: {}", e)) 46 | } 47 | } 48 | 49 | impl From for rusqlite::Error { 50 | fn from(err: Error) -> Self { 51 | rusqlite::Error::FromSqlConversionFailure( 52 | 0, 53 | rusqlite::types::Type::Text, 54 | Box::new(std::io::Error::new( 55 | std::io::ErrorKind::InvalidData, 56 | err.to_string(), 57 | )), 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/nostr-mls-sqlite-storage/src/migrations.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::Connection; 2 | 3 | use crate::error::Error; 4 | 5 | // Embed the SQL migrations 6 | refinery::embed_migrations!("migrations"); 7 | 8 | /// Run database migrations to set up or upgrade the database schema. 9 | /// We use a custom migration table name to avoid conflicts with migrations from the OpenMls SqliteStorage crate. 10 | /// 11 | /// # Arguments 12 | /// 13 | /// * `conn` - The SQLite database connection. 14 | /// 15 | /// # Returns 16 | /// 17 | /// Result indicating success or failure of the migration process. 18 | pub fn run_migrations(conn: &mut Connection) -> Result<(), Error> { 19 | // Run the migrations 20 | let report = migrations::runner() 21 | .set_migration_table_name("_refinery_schema_history_nostr_mls") 22 | .run(conn)?; 23 | 24 | // Log the results 25 | for migration in report.applied_migrations() { 26 | tracing::info!( 27 | "Applied migration: {} (version: {})", 28 | migration.name(), 29 | migration.version() 30 | ); 31 | } 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-mls-storage" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Storage abstraction for nostr-mls that wraps OpenMLS storage backends" 6 | authors = ["Jeff Gardner ", "Yuki Kishimoto ", "Rust Nostr Developers"] 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version = "1.74.0" 12 | keywords = ["nostr", "mls", "openmls"] 13 | 14 | [dependencies] 15 | nostr = { workspace = true, features = ["std"] } 16 | openmls = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 17 | openmls_traits = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 18 | serde = { workspace = true, features = ["derive"] } 19 | serde_json = { workspace = true, features = ["std"] } 20 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/README.md: -------------------------------------------------------------------------------- 1 | # Nostr MLS Storage 2 | 3 | This crate provides an abstraction for the storage layer that MLS requires. 4 | 5 | ## NostrMlsStorageProvider trait 6 | 7 | THis library contains the `NostrMlsStorageProvider` trait. 8 | 9 | ## State 10 | 11 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 12 | 13 | ## Donations 14 | 15 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 16 | 17 | ## License 18 | 19 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 20 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/src/groups/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the groups module 2 | 3 | use std::fmt; 4 | 5 | /// Invalid group state 6 | #[derive(Debug, PartialEq, Eq)] 7 | pub enum InvalidGroupState { 8 | /// Group has no admins 9 | NoAdmins, 10 | /// Group has no relays 11 | NoRelays, 12 | } 13 | 14 | impl fmt::Display for InvalidGroupState { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | match self { 17 | Self::NoAdmins => write!(f, "group has no admins"), 18 | Self::NoRelays => write!(f, "group has no relays"), 19 | } 20 | } 21 | } 22 | 23 | /// Error types for the groups module 24 | #[derive(Debug)] 25 | pub enum GroupError { 26 | /// Invalid parameters 27 | InvalidParameters(String), 28 | /// Database error 29 | DatabaseError(String), 30 | /// Invalid state 31 | InvalidState(InvalidGroupState), 32 | } 33 | 34 | impl std::error::Error for GroupError {} 35 | 36 | impl fmt::Display for GroupError { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | match self { 39 | Self::InvalidParameters(message) => write!(f, "Invalid parameters: {}", message), 40 | Self::DatabaseError(message) => write!(f, "Database error: {}", message), 41 | Self::InvalidState(state) => write!(f, "Invalid state: {state}"), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/src/groups/mod.rs: -------------------------------------------------------------------------------- 1 | //! Groups module 2 | //! 3 | //! This module is responsible for storing and retrieving groups 4 | //! It also handles the parsing of group content 5 | //! 6 | //! The groups are stored in the database and can be retrieved by MLS group ID or Nostr group ID 7 | //! 8 | //! Here we also define the storage traits that are used to store and retrieve groups 9 | 10 | use std::collections::BTreeSet; 11 | 12 | use nostr::PublicKey; 13 | use openmls::group::GroupId; 14 | 15 | pub mod error; 16 | pub mod types; 17 | 18 | use self::error::GroupError; 19 | use self::types::*; 20 | use crate::messages::types::Message; 21 | 22 | /// Storage traits for the groups module 23 | pub trait GroupStorage { 24 | /// Get all groups 25 | fn all_groups(&self) -> Result, GroupError>; 26 | 27 | /// Find a group by MLS group ID 28 | fn find_group_by_mls_group_id(&self, group_id: &GroupId) -> Result, GroupError>; 29 | 30 | /// Find a group by Nostr group ID 31 | fn find_group_by_nostr_group_id( 32 | &self, 33 | nostr_group_id: &[u8; 32], 34 | ) -> Result, GroupError>; 35 | 36 | /// Save a group 37 | fn save_group(&self, group: Group) -> Result<(), GroupError>; 38 | 39 | /// Get all messages for a group 40 | fn messages(&self, group_id: &GroupId) -> Result, GroupError>; 41 | 42 | /// Get all admins for a group 43 | fn admins(&self, group_id: &GroupId) -> Result, GroupError>; 44 | 45 | /// Get all relays for a group 46 | fn group_relays(&self, group_id: &GroupId) -> Result, GroupError>; 47 | 48 | /// Save a group relay 49 | fn save_group_relay(&self, group_relay: GroupRelay) -> Result<(), GroupError>; 50 | 51 | /// Get an exporter secret for a group and epoch 52 | fn get_group_exporter_secret( 53 | &self, 54 | group_id: &GroupId, 55 | epoch: u64, 56 | ) -> Result, GroupError>; 57 | 58 | /// Save an exporter secret for a group and epoch 59 | fn save_group_exporter_secret( 60 | &self, 61 | group_exporter_secret: GroupExporterSecret, 62 | ) -> Result<(), GroupError>; 63 | } 64 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Nostr MLS storage - A set of storage provider traits and types for implementing MLS storage 2 | //! It is designed to be used in conjunction with the `openmls` crate. 3 | 4 | #![forbid(unsafe_code)] 5 | #![warn(missing_docs)] 6 | #![warn(rustdoc::bare_urls)] 7 | 8 | use openmls_traits::storage::StorageProvider; 9 | 10 | pub mod groups; 11 | pub mod messages; 12 | pub mod welcomes; 13 | 14 | use self::groups::GroupStorage; 15 | use self::messages::MessageStorage; 16 | use self::welcomes::WelcomeStorage; 17 | 18 | const CURRENT_VERSION: u16 = 1; 19 | 20 | /// Backend 21 | #[derive(Debug, Clone, PartialEq, Eq)] 22 | pub enum Backend { 23 | /// Memory 24 | Memory, 25 | /// SQLite 26 | SQLite, 27 | } 28 | 29 | impl Backend { 30 | /// Check if it's a persistent backend 31 | /// 32 | /// All values different from [`Backend::Memory`] are considered persistent 33 | pub fn is_persistent(&self) -> bool { 34 | !matches!(self, Self::Memory) 35 | } 36 | } 37 | 38 | /// Storage provider for the Nostr MLS storage 39 | pub trait NostrMlsStorageProvider: GroupStorage + MessageStorage + WelcomeStorage { 40 | /// The OpenMLS storage provider 41 | type OpenMlsStorageProvider: StorageProvider; 42 | 43 | /// Returns the backend type. 44 | /// 45 | /// # Returns 46 | /// 47 | /// [`Backend::Memory`] indicating this is a memory-based storage implementation. 48 | fn backend(&self) -> Backend; 49 | 50 | /// Get a reference to the openmls storage provider. 51 | /// 52 | /// This method provides access to the underlying OpenMLS storage provider. 53 | /// This is primarily useful for internal operations and testing. 54 | /// 55 | /// # Returns 56 | /// 57 | /// A reference to the openmls storage implementation. 58 | fn openmls_storage(&self) -> &Self::OpenMlsStorageProvider; 59 | 60 | /// Get a mutable reference to the openmls storage provider. 61 | /// 62 | /// This method provides mutable access to the underlying OpenMLS storage provider. 63 | /// This is primarily useful for internal operations and testing. 64 | /// 65 | /// # Returns 66 | /// 67 | /// A mutable reference to the openmls storage implementation. 68 | fn openmls_storage_mut(&mut self) -> &mut Self::OpenMlsStorageProvider; 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use super::*; 74 | 75 | #[test] 76 | fn test_backend_is_persistent() { 77 | assert!(!Backend::Memory.is_persistent()); 78 | assert!(Backend::SQLite.is_persistent()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/src/messages/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the messages module 2 | 3 | use std::fmt; 4 | 5 | /// Error types for the messages module 6 | #[derive(Debug)] 7 | pub enum MessageError { 8 | /// Invalid parameters 9 | InvalidParameters(String), 10 | /// Database error 11 | DatabaseError(String), 12 | } 13 | 14 | impl std::error::Error for MessageError {} 15 | 16 | impl fmt::Display for MessageError { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | match self { 19 | Self::InvalidParameters(message) => write!(f, "Invalid parameters: {}", message), 20 | Self::DatabaseError(message) => write!(f, "Database error: {}", message), 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/src/messages/mod.rs: -------------------------------------------------------------------------------- 1 | //! Messages module 2 | //! 3 | //! This module is responsible for storing and retrieving messages 4 | //! 5 | //! The messages are stored in the database and can be retrieved by event ID 6 | //! 7 | //! Here we also define the storage traits that are used to store and retrieve messages 8 | 9 | use nostr::EventId; 10 | 11 | pub mod error; 12 | pub mod types; 13 | 14 | use self::error::MessageError; 15 | use self::types::*; 16 | 17 | /// Storage traits for the messages module 18 | pub trait MessageStorage { 19 | /// Save a message 20 | fn save_message(&self, message: Message) -> Result<(), MessageError>; 21 | 22 | /// Find a message by event ID 23 | fn find_message_by_event_id(&self, event_id: &EventId) 24 | -> Result, MessageError>; 25 | 26 | /// Save a processed message 27 | fn save_processed_message( 28 | &self, 29 | processed_message: ProcessedMessage, 30 | ) -> Result<(), MessageError>; 31 | 32 | /// Find a processed message by event ID 33 | fn find_processed_message_by_event_id( 34 | &self, 35 | event_id: &EventId, 36 | ) -> Result, MessageError>; 37 | } 38 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/src/welcomes/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the welcomes module 2 | 3 | use std::fmt; 4 | 5 | /// Error types for the welcomes module 6 | #[derive(Debug)] 7 | pub enum WelcomeError { 8 | /// Invalid parameters 9 | InvalidParameters(String), 10 | /// Database error 11 | DatabaseError(String), 12 | } 13 | 14 | impl std::error::Error for WelcomeError {} 15 | 16 | impl fmt::Display for WelcomeError { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | match self { 19 | Self::InvalidParameters(message) => write!(f, "Invalid parameters: {}", message), 20 | Self::DatabaseError(message) => write!(f, "Database error: {}", message), 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/nostr-mls-storage/src/welcomes/mod.rs: -------------------------------------------------------------------------------- 1 | //! Welcomes module 2 | //! 3 | //! This module is responsible for storing and retrieving welcomes 4 | //! It also handles the parsing of welcome content 5 | //! 6 | //! The welcomes are stored in the database and can be retrieved by event ID 7 | //! 8 | //! Here we also define the storage traits that are used to store and retrieve welcomes 9 | 10 | use nostr::EventId; 11 | 12 | pub mod error; 13 | pub mod types; 14 | 15 | use self::error::WelcomeError; 16 | use self::types::*; 17 | 18 | /// Storage traits for the welcomes module 19 | pub trait WelcomeStorage { 20 | /// Save a welcome 21 | fn save_welcome(&self, welcome: Welcome) -> Result<(), WelcomeError>; 22 | 23 | /// Find a welcome by event ID 24 | fn find_welcome_by_event_id(&self, event_id: &EventId) 25 | -> Result, WelcomeError>; 26 | 27 | /// Get all pending welcomes 28 | fn pending_welcomes(&self) -> Result, WelcomeError>; 29 | 30 | /// Save a processed welcome 31 | fn save_processed_welcome( 32 | &self, 33 | processed_welcome: ProcessedWelcome, 34 | ) -> Result<(), WelcomeError>; 35 | 36 | /// Find a processed welcome by event ID 37 | fn find_processed_welcome_by_event_id( 38 | &self, 39 | event_id: &EventId, 40 | ) -> Result, WelcomeError>; 41 | } 42 | -------------------------------------------------------------------------------- /crates/nostr-mls/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-mls" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "A simplified interface to build secure messaging apps on nostr with MLS." 6 | authors = ["Jeff Gardner ", "Yuki Kishimoto ", "Rust Nostr Developers"] 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version = "1.74.0" 12 | keywords = ["nostr", "mls", "openmls"] 13 | 14 | [dependencies] 15 | nostr = { workspace = true, features = ["std", "nip44"] } 16 | nostr-mls-storage.workspace = true 17 | openmls = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 18 | openmls_basic_credential = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 19 | openmls_rust_crypto = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 20 | openmls_traits = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false } 21 | tls_codec = "0.4" 22 | tracing = { workspace = true, features = ["std"] } 23 | 24 | [dev-dependencies] 25 | nostr-mls-memory-storage.workspace = true 26 | nostr-mls-sqlite-storage.workspace = true 27 | tempfile.workspace = true 28 | tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } 29 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 30 | 31 | [[example]] 32 | name = "mls_memory" 33 | path = "examples/mls_memory.rs" 34 | 35 | [[example]] 36 | name = "mls_sqlite" 37 | path = "examples/mls_sqlite.rs" 38 | -------------------------------------------------------------------------------- /crates/nostr-mls/README.md: -------------------------------------------------------------------------------- 1 | # Nostr Messaging Layer Security (MLS) 2 | 3 | ## Description 4 | 5 | A simplified interface to build secure messaging apps on nostr with MLS ([RFC 9420](https://datatracker.ietf.org/doc/html/rfc9420)), 6 | according to [NIP-EE](https://github.com/nostr-protocol/nips/pull/1427). 7 | 8 | ## State 9 | 10 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 11 | 12 | ## Donations 13 | 14 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 15 | 16 | ## License 17 | 18 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 19 | -------------------------------------------------------------------------------- /crates/nostr-mls/docs/resources.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | This is a collection of useful links to documentation that can be helpful for both human and AI agents. 4 | 5 | [NIP-EE](https://github.com/nostr-protocol/nips/blob/001c516f7294308143515a494a35213fc45978df/EE.md): This is the Nostr NIP that describes how to implement MLS based secure messaging using the Nostr protocol. 6 | [Plan](./plan.md): This is a doc that describes some of our principles when writing this library and both the current and set of interfaces we want this library to have for consumers. Some of the current interfaces would ideally be removed. 7 | [OpenMLS](https://github.com/openmls/openmls): This is the MLS implementation that we're using to work with MLS primitives 8 | 9 | -------------------------------------------------------------------------------- /crates/nostr-mls/src/constant.rs: -------------------------------------------------------------------------------- 1 | //! Nostr MLS constants 2 | 3 | use openmls::extensions::ExtensionType; 4 | use openmls_traits::types::Ciphersuite; 5 | 6 | /// Nostr Group Data extension type 7 | pub const NOSTR_GROUP_DATA_EXTENSION_TYPE: u16 = 0xF2EE; // Be FREE 8 | 9 | /// Default ciphersuite for Nostr Groups. 10 | /// This is also the only required ciphersuite for Nostr Groups. 11 | pub const DEFAULT_CIPHERSUITE: Ciphersuite = 12 | Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519; 13 | 14 | /// Required extensions for Nostr Groups. 15 | pub const REQUIRED_EXTENSIONS: [ExtensionType; 4] = [ 16 | ExtensionType::RequiredCapabilities, 17 | ExtensionType::LastResort, 18 | ExtensionType::RatchetTree, 19 | ExtensionType::Unknown(NOSTR_GROUP_DATA_EXTENSION_TYPE), 20 | ]; 21 | 22 | // /// GREASE values for MLS. 23 | // TODO: Remove this once we've added GREASE support. 24 | // const GREASE: [u16; 15] = [ 25 | // 0x0A0A, 0x1A1A, 0x2A2A, 0x3A3A, 0x4A4A, 0x5A5A, 0x6A6A, 0x7A7A, 0x8A8A, 0x9A9A, 0xAAAA, 26 | // 0xBABA, 0xCACA, 0xDADA, 0xEAEA, 27 | // ]; 28 | -------------------------------------------------------------------------------- /crates/nostr-mls/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | #![doc(hidden)] 10 | 11 | pub use nostr::prelude::*; 12 | pub use nostr_mls_storage::groups::{types as group_types, GroupStorage}; 13 | pub use nostr_mls_storage::messages::{types as message_types, MessageStorage}; 14 | pub use nostr_mls_storage::welcomes::{types as welcome_types, WelcomeStorage}; 15 | pub use nostr_mls_storage::{Backend, NostrMlsStorageProvider}; 16 | pub use openmls::prelude::*; 17 | 18 | pub use crate::extension::*; 19 | pub use crate::groups::*; 20 | pub use crate::welcomes::*; 21 | pub use crate::*; 22 | -------------------------------------------------------------------------------- /crates/nostr-ndb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-ndb" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "ndb (nostrdb) storage backend for Nostr apps" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version.workspace = true 12 | keywords = ["nostr", "database", "ndb", "nostrdb"] 13 | 14 | [dependencies] 15 | nostr = { workspace = true, features = ["std"] } 16 | nostr-database.workspace = true 17 | nostrdb = "0.6" 18 | -------------------------------------------------------------------------------- /crates/nostr-ndb/README.md: -------------------------------------------------------------------------------- 1 | # nostr-ndb 2 | 3 | This crate implements [nostrdb](https://github.com/damus-io/nostrdb) storage backend. 4 | 5 | ## State 6 | 7 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 8 | 9 | ## Donations 10 | 11 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 12 | 13 | ## License 14 | 15 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 16 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-relay-builder" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Build your own custom nostr relay!" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version.workspace = true 12 | keywords = ["nostr", "relay", "builder"] 13 | 14 | [features] 15 | default = [] 16 | tor = ["async-wsocket/tor-launch-service"] 17 | 18 | [dependencies] 19 | async-utility.workspace = true 20 | async-wsocket.workspace = true 21 | atomic-destructor.workspace = true 22 | negentropy = { workspace = true, features = ["std"] } 23 | nostr = { workspace = true, default-features = false, features = ["std"] } 24 | nostr-database.workspace = true 25 | tokio = { workspace = true, features = ["macros", "net", "sync"] } 26 | tracing.workspace = true 27 | 28 | [dev-dependencies] 29 | base64 = { workspace = true, features = ["std"] } 30 | hyper = { version = "1.6", features = ["server", "http1"] } 31 | hyper-util = { version = "0.1", features = ["tokio"] } 32 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 33 | 34 | [[example]] 35 | name = "hyper" 36 | 37 | [[example]] 38 | name = "local-with-hs" 39 | required-features = ["tor"] 40 | 41 | [[example]] 42 | name = "mock" 43 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/README.md: -------------------------------------------------------------------------------- 1 | # Nostr Relay Builder 2 | 3 | ## Description 4 | 5 | Build your own custom nostr relay! 6 | 7 | This library contains all the stuff to easily build a nostr relay. 8 | It also contains a ready-to-use `MockRelay` for unit tests. 9 | 10 | ## State 11 | 12 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 13 | 14 | ## Donations 15 | 16 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 17 | 18 | ## License 19 | 20 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details 21 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/examples/local-with-hs.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_relay_builder::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let tor = RelayBuilderHiddenService::new("rust-nostr-local-hs-test"); 14 | let builder = RelayBuilder::default().tor(tor); 15 | 16 | let relay = LocalRelay::run(builder).await?; 17 | 18 | println!("Url: {}", relay.url()); 19 | println!("Hidden service: {:?}", relay.hidden_service()); 20 | 21 | // Keep up the program 22 | loop { 23 | tokio::time::sleep(Duration::from_secs(60)).await; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/examples/mock.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_relay_builder::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let relay = MockRelay::run().await?; 14 | 15 | println!("Url: {}", relay.url()); 16 | 17 | // Keep up the program 18 | loop { 19 | tokio::time::sleep(Duration::from_secs(60)).await; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/examples/policy.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::collections::HashSet; 6 | use std::net::SocketAddr; 7 | use std::time::Duration; 8 | 9 | use nostr_relay_builder::prelude::*; 10 | 11 | /// Accept only certain event kinds 12 | #[derive(Debug)] 13 | struct AcceptKinds { 14 | pub kinds: HashSet, 15 | } 16 | 17 | impl WritePolicy for AcceptKinds { 18 | fn admit_event<'a>( 19 | &'a self, 20 | event: &'a Event, 21 | _addr: &'a SocketAddr, 22 | ) -> BoxedFuture<'a, PolicyResult> { 23 | Box::pin(async move { 24 | if self.kinds.contains(&event.kind) { 25 | PolicyResult::Accept 26 | } else { 27 | PolicyResult::Reject("kind not accepted".to_string()) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | /// Reject requests if there are more than [limit] authors in the filter 34 | #[derive(Debug)] 35 | struct RejectAuthorLimit { 36 | pub limit: usize, 37 | } 38 | 39 | impl QueryPolicy for RejectAuthorLimit { 40 | fn admit_query<'a>( 41 | &'a self, 42 | query: &'a Filter, 43 | _addr: &'a SocketAddr, 44 | ) -> BoxedFuture<'a, PolicyResult> { 45 | Box::pin(async move { 46 | if query.authors.as_ref().map(|a| a.len()).unwrap_or(0) > self.limit { 47 | PolicyResult::Reject("query too expensive".to_string()) 48 | } else { 49 | PolicyResult::Accept 50 | } 51 | }) 52 | } 53 | } 54 | 55 | #[tokio::main] 56 | async fn main() -> Result<()> { 57 | tracing_subscriber::fmt::init(); 58 | 59 | let accept_profile_data = AcceptKinds { 60 | kinds: HashSet::from([Kind::Metadata, Kind::RelayList, Kind::ContactList]), 61 | }; 62 | 63 | let low_author_limit = RejectAuthorLimit { limit: 2 }; 64 | 65 | let builder = RelayBuilder::default() 66 | .write_policy(accept_profile_data) 67 | .query_policy(low_author_limit); 68 | 69 | let relay = LocalRelay::run(builder).await?; 70 | 71 | println!("Url: {}", relay.url()); 72 | 73 | // Keep up the program 74 | loop { 75 | tokio::time::sleep(Duration::from_secs(60)).await; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Relay builder error 6 | 7 | use std::{fmt, io}; 8 | 9 | #[cfg(feature = "tor")] 10 | use async_wsocket::native::tor; 11 | 12 | /// Relay builder error 13 | #[derive(Debug)] 14 | pub enum Error { 15 | /// I/O error 16 | IO(io::Error), 17 | /// Tor error 18 | #[cfg(feature = "tor")] 19 | Tor(tor::Error), 20 | } 21 | 22 | impl std::error::Error for Error {} 23 | 24 | impl fmt::Display for Error { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | match self { 27 | Self::IO(e) => write!(f, "{e}"), 28 | #[cfg(feature = "tor")] 29 | Self::Tor(e) => write!(f, "{e}"), 30 | } 31 | } 32 | } 33 | 34 | impl From for Error { 35 | fn from(e: io::Error) -> Self { 36 | Self::IO(e) 37 | } 38 | } 39 | 40 | #[cfg(feature = "tor")] 41 | impl From for Error { 42 | fn from(e: tor::Error) -> Self { 43 | Self::Tor(e) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Nostr Relay Builder and Mock Relay for tests 6 | 7 | #![forbid(unsafe_code)] 8 | #![warn(missing_docs)] 9 | #![warn(rustdoc::bare_urls)] 10 | #![warn(clippy::large_futures)] 11 | 12 | pub mod builder; 13 | pub mod error; 14 | pub mod local; 15 | pub mod mock; 16 | pub mod prelude; 17 | 18 | pub use self::builder::RelayBuilder; 19 | pub use self::error::Error; 20 | pub use self::local::LocalRelay; 21 | pub use self::mock::MockRelay; 22 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/src/local/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! A local nostr relay 6 | 7 | use std::net::SocketAddr; 8 | 9 | use atomic_destructor::AtomicDestructor; 10 | use nostr_database::prelude::*; 11 | use tokio::io::{AsyncRead, AsyncWrite}; 12 | 13 | mod inner; 14 | mod session; 15 | mod util; 16 | 17 | use self::inner::InnerLocalRelay; 18 | use crate::builder::RelayBuilder; 19 | use crate::error::Error; 20 | 21 | /// A local nostr relay 22 | /// 23 | /// This is automatically shutdown when all instances/clones are dropped! 24 | #[derive(Debug, Clone)] 25 | pub struct LocalRelay { 26 | inner: AtomicDestructor, 27 | } 28 | 29 | impl LocalRelay { 30 | /// Create a new local relay without listening 31 | #[inline] 32 | pub async fn new(builder: RelayBuilder) -> Result { 33 | Ok(Self { 34 | inner: AtomicDestructor::new(InnerLocalRelay::new(builder).await?), 35 | }) 36 | } 37 | 38 | /// Run local relay from [`RelayBuilder`] 39 | #[inline] 40 | pub async fn run(builder: RelayBuilder) -> Result { 41 | Ok(Self { 42 | inner: AtomicDestructor::new(InnerLocalRelay::run(builder).await?), 43 | }) 44 | } 45 | 46 | /// Get url 47 | #[inline] 48 | pub fn url(&self) -> String { 49 | self.inner.url() 50 | } 51 | 52 | /// Get hidden service address if available 53 | #[inline] 54 | #[cfg(feature = "tor")] 55 | pub fn hidden_service(&self) -> Option<&str> { 56 | self.inner.hidden_service() 57 | } 58 | 59 | /// Send event to subscribers 60 | /// 61 | /// Return `true` if the event is successfully sent. 62 | /// 63 | /// This method doesn't save the event into the database! 64 | /// It's intended to be used ONLY when the database is shared with other apps (i.e. with the nostr-sdk `Client`). 65 | pub fn notify_event(&self, event: Event) -> bool { 66 | self.inner.notify_event(event) 67 | } 68 | 69 | /// Shutdown relay 70 | #[inline] 71 | pub fn shutdown(&self) { 72 | self.inner.shutdown(); 73 | } 74 | 75 | /// Pass an already upgraded stream 76 | pub async fn take_connection(&self, stream: S, addr: SocketAddr) -> Result<()> 77 | where 78 | S: AsyncRead + AsyncWrite + Unpin, 79 | { 80 | self.inner.handle_upgraded_connection(stream, addr).await 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/src/local/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; 6 | 7 | use nostr::secp256k1::rand::rngs::OsRng; 8 | use nostr::secp256k1::rand::Rng; 9 | use tokio::net::TcpListener; 10 | 11 | pub async fn find_available_port() -> u16 { 12 | let mut rng: OsRng = OsRng; 13 | loop { 14 | let port: u16 = rng.gen_range(1024..=u16::MAX); 15 | if port_is_available(port).await { 16 | return port; 17 | } 18 | } 19 | } 20 | 21 | #[inline] 22 | pub async fn port_is_available(port: u16) -> bool { 23 | TcpListener::bind(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port))) 24 | .await 25 | .is_ok() 26 | } 27 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/src/mock.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! A mock relay for (unit) tests. 6 | 7 | use std::ops::Deref; 8 | 9 | use nostr::prelude::*; 10 | use nostr_database::prelude::*; 11 | 12 | use crate::builder::{RelayBuilder, RelayTestOptions}; 13 | use crate::error::Error; 14 | use crate::local::LocalRelay; 15 | 16 | /// A mock relay for (unit) tests. 17 | /// 18 | /// Check [`LocalRelay`] for more details. 19 | #[derive(Debug, Clone)] 20 | pub struct MockRelay { 21 | local: LocalRelay, 22 | } 23 | 24 | impl Deref for MockRelay { 25 | type Target = LocalRelay; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.local 29 | } 30 | } 31 | 32 | impl MockRelay { 33 | /// Run mock relay 34 | #[inline] 35 | pub async fn run() -> Result { 36 | let builder = RelayBuilder::default(); 37 | Ok(Self { 38 | local: LocalRelay::run(builder).await?, 39 | }) 40 | } 41 | 42 | /// Run unresponsive relay 43 | #[inline] 44 | pub async fn run_with_opts(opts: RelayTestOptions) -> Result { 45 | let builder = RelayBuilder::default().test(opts); 46 | Ok(Self { 47 | local: LocalRelay::run(builder).await?, 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/nostr-relay-builder/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | #![doc(hidden)] 10 | 11 | // External crates 12 | pub use nostr::prelude::*; 13 | pub use nostr_database::prelude::*; 14 | 15 | pub use crate::builder::{self, *}; 16 | pub use crate::local::{self, *}; 17 | // Internal modules 18 | pub use crate::mock::{self, *}; 19 | pub use crate::*; 20 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nostr-relay-pool" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Nostr Relay Pool" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version.workspace = true 12 | keywords = ["nostr", "relay", "pool"] 13 | 14 | [features] 15 | default = [] 16 | tor = ["async-wsocket/tor"] 17 | nip11 = ["nostr/nip11"] 18 | 19 | [dependencies] 20 | async-utility.workspace = true 21 | async-wsocket = { workspace = true, features = ["socks"] } 22 | atomic-destructor.workspace = true 23 | lru.workspace = true 24 | negentropy = { workspace = true, features = ["std"] } 25 | nostr = { workspace = true, features = ["std"] } 26 | nostr-database.workspace = true 27 | tokio = { workspace = true, features = ["macros", "sync"] } 28 | tracing.workspace = true 29 | 30 | [dev-dependencies] 31 | nostr-relay-builder.workspace = true 32 | tokio = { workspace = true, features = ["rt-multi-thread"] } 33 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 34 | 35 | [[example]] 36 | name = "pool" 37 | 38 | [lints.rust] 39 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bench)'] } 40 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/README.md: -------------------------------------------------------------------------------- 1 | # Nostr Relay Pool 2 | 3 | ## Crate Feature Flags 4 | 5 | The following crate feature flags are available: 6 | 7 | | Feature | Default | Description | 8 | |---------|:-------:|-------------------------------------------| 9 | | `tor` | No | Enable support for embedded tor client | 10 | | `nip11` | No | Enable NIP-11: Relay Information Document | 11 | 12 | ## State 13 | 14 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 15 | 16 | ## Donations 17 | 18 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 19 | 20 | ## License 21 | 22 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details -------------------------------------------------------------------------------- /crates/nostr-relay-pool/examples/pool.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_relay_pool::prelude::*; 8 | use tracing_subscriber::fmt::format::FmtSpan; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | tracing_subscriber::fmt::fmt() 13 | .with_span_events(FmtSpan::CLOSE) 14 | .with_env_filter("info,nostr_relay_pool::relay::internal=trace") 15 | .init(); 16 | 17 | { 18 | let pool = RelayPool::default(); 19 | 20 | pool.add_relay("wss://relay.damus.io", RelayOptions::default()) 21 | .await?; 22 | pool.connect().await; 23 | 24 | let event = Event::from_json(r#"{"content":"","created_at":1698412975,"id":"f55c30722f056e330d8a7a6a9ba1522f7522c0f1ced1c93d78ea833c78a3d6ec","kind":3,"pubkey":"f831caf722214748c72db4829986bd0cbb2bb8b3aeade1c959624a52a9629046","sig":"5092a9ffaecdae7d7794706f085ff5852befdf79df424cc3419bb797bf515ae05d4f19404cb8324b8b4380a4bd497763ac7b0f3b1b63ef4d3baa17e5f5901808","tags":[["p","4ddeb9109a8cd29ba279a637f5ec344f2479ee07df1f4043f3fe26d8948cfef9","",""],["p","bb6fd06e156929649a73e6b278af5e648214a69d88943702f1fb627c02179b95","",""],["p","b8b8210f33888fdbf5cedee9edf13c3e9638612698fe6408aff8609059053420","",""],["p","9dcee4fabcd690dc1da9abdba94afebf82e1e7614f4ea92d61d52ef9cd74e083","",""],["p","3eea9e831fefdaa8df35187a204d82edb589a36b170955ac5ca6b88340befaa0","",""],["p","885238ab4568f271b572bf48b9d6f99fa07644731f288259bd395998ee24754e","",""],["p","568a25c71fba591e39bebe309794d5c15d27dbfa7114cacb9f3586ea1314d126","",""]]}"#).unwrap(); 25 | pool.send_event(&event).await?; 26 | } 27 | // Pool dropped 28 | 29 | tokio::time::sleep(Duration::from_secs(10)).await; 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Nostr Relay Pool 6 | 7 | #![forbid(unsafe_code)] 8 | #![warn(missing_docs)] 9 | #![warn(rustdoc::bare_urls)] 10 | #![warn(clippy::large_futures)] 11 | #![allow(unknown_lints)] // TODO: remove when MSRV >= 1.72.0, required for `clippy::arc_with_non_send_sync` 12 | #![allow(clippy::arc_with_non_send_sync)] 13 | #![allow(clippy::mutable_key_type)] // TODO: remove when possible. Needed to suppress false positive for `BTreeSet` 14 | #![cfg_attr(bench, feature(test))] 15 | 16 | #[cfg(bench)] 17 | extern crate test; 18 | 19 | pub use async_wsocket::ConnectionMode; 20 | 21 | pub mod monitor; 22 | pub mod policy; 23 | pub mod pool; 24 | pub mod prelude; 25 | pub mod relay; 26 | #[doc(hidden)] 27 | mod shared; 28 | pub mod stream; 29 | pub mod transport; 30 | 31 | pub use self::pool::options::RelayPoolOptions; 32 | pub use self::pool::{Output, RelayPool, RelayPoolNotification}; 33 | pub use self::relay::flags::{AtomicRelayServiceFlags, RelayServiceFlags}; 34 | pub use self::relay::limits::RelayLimits; 35 | pub use self::relay::options::{ 36 | RelayOptions, SubscribeAutoCloseOptions, SubscribeOptions, SyncDirection, SyncOptions, 37 | }; 38 | pub use self::relay::stats::RelayConnectionStats; 39 | pub use self::relay::{Reconciliation, Relay, RelayNotification, RelayStatus}; 40 | 41 | // Not public API. 42 | #[doc(hidden)] 43 | pub mod __private { 44 | #[doc(hidden)] 45 | pub use super::shared::{SharedState, SharedStateError}; 46 | } 47 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/monitor.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Monitor 6 | 7 | use nostr::RelayUrl; 8 | use tokio::sync::broadcast::{self, Receiver, Sender}; 9 | 10 | use crate::relay::RelayStatus; 11 | 12 | /// Relay monitor notification 13 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 14 | pub enum MonitorNotification { 15 | /// Relay status changed 16 | StatusChanged { 17 | /// Relay URL 18 | relay_url: RelayUrl, 19 | /// Status 20 | status: RelayStatus, 21 | }, 22 | } 23 | 24 | /// Relay monitor 25 | #[derive(Debug, Clone)] 26 | pub struct Monitor { 27 | channel: Sender, 28 | } 29 | 30 | impl Monitor { 31 | /// Create a new monitor with the given channel size 32 | /// 33 | /// For more details, check [`broadcast::channel`]. 34 | pub fn new(channel_size: usize) -> Self { 35 | let (tx, ..) = broadcast::channel(channel_size); 36 | 37 | Self { channel: tx } 38 | } 39 | 40 | /// Subscribe to monitor notifications 41 | /// 42 | ///
When you call this method, you subscribe to the notifications channel from that precise moment. Anything received by relay/s before that moment is not included in the channel!
43 | #[inline] 44 | pub fn subscribe(&self) -> Receiver { 45 | self.channel.subscribe() 46 | } 47 | 48 | #[inline] 49 | fn notify(&self, notification: MonitorNotification) { 50 | let _ = self.channel.send(notification); 51 | } 52 | 53 | #[inline] 54 | pub(crate) fn notify_status_change(&self, relay_url: RelayUrl, status: RelayStatus) { 55 | self.notify(MonitorNotification::StatusChanged { relay_url, status }); 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | 63 | #[test] 64 | #[should_panic] 65 | fn test_monitor_capacity_is_zero() { 66 | Monitor::new(0); 67 | } 68 | 69 | #[test] 70 | #[should_panic] 71 | fn test_monitor_capacity_overflows() { 72 | let _ = Monitor::new(usize::MAX / 2); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/pool/builder.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Relay Pool builder 6 | 7 | use std::sync::Arc; 8 | 9 | use nostr::NostrSigner; 10 | use nostr_database::{MemoryDatabase, NostrDatabase}; 11 | 12 | use super::options::RelayPoolOptions; 13 | use super::RelayPool; 14 | use crate::monitor::Monitor; 15 | use crate::policy::AdmitPolicy; 16 | use crate::transport::websocket::{DefaultWebsocketTransport, WebSocketTransport}; 17 | 18 | /// Relay Pool builder 19 | #[derive(Debug, Clone)] 20 | pub struct RelayPoolBuilder { 21 | /// WebSocket transport 22 | pub websocket_transport: Arc, 23 | /// Admission policy 24 | pub admit_policy: Option>, 25 | /// Relay monitor 26 | pub monitor: Option, 27 | /// Relay pool options 28 | pub opts: RelayPoolOptions, 29 | // Private stuff 30 | #[doc(hidden)] 31 | pub __database: Arc, 32 | #[doc(hidden)] 33 | pub __signer: Option>, 34 | } 35 | 36 | impl Default for RelayPoolBuilder { 37 | fn default() -> Self { 38 | Self { 39 | websocket_transport: Arc::new(DefaultWebsocketTransport), 40 | admit_policy: None, 41 | monitor: None, 42 | opts: RelayPoolOptions::default(), 43 | __database: Arc::new(MemoryDatabase::default()), 44 | __signer: None, 45 | } 46 | } 47 | } 48 | 49 | impl RelayPoolBuilder { 50 | /// New default builder 51 | #[inline] 52 | pub fn new() -> Self { 53 | Self::default() 54 | } 55 | 56 | /// Set a WebSocket transport 57 | #[inline] 58 | pub fn websocket_transport(mut self, transport: T) -> Self 59 | where 60 | T: WebSocketTransport + 'static, 61 | { 62 | self.websocket_transport = Arc::new(transport); 63 | self 64 | } 65 | 66 | /// Admission policy 67 | #[inline] 68 | pub fn admit_policy(mut self, policy: T) -> Self 69 | where 70 | T: AdmitPolicy + 'static, 71 | { 72 | self.admit_policy = Some(Arc::new(policy)); 73 | self 74 | } 75 | 76 | /// Set monitor 77 | #[inline] 78 | pub fn monitor(mut self, monitor: Monitor) -> Self { 79 | self.monitor = Some(monitor); 80 | self 81 | } 82 | 83 | /// Set options 84 | #[inline] 85 | pub fn opts(mut self, opts: RelayPoolOptions) -> Self { 86 | self.opts = opts; 87 | self 88 | } 89 | 90 | /// Build relay pool 91 | #[inline] 92 | pub fn build(self) -> RelayPool { 93 | RelayPool::from_builder(self) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/pool/constants.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Constants 6 | 7 | /// Relay Pool default notification channel size 8 | pub const DEFAULT_NOTIFICATION_CHANNEL_SIZE: usize = 4096; 9 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/pool/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::convert::Infallible; 6 | use std::fmt; 7 | 8 | use nostr::types::url; 9 | use nostr_database::DatabaseError; 10 | 11 | use crate::__private::SharedStateError; 12 | use crate::relay; 13 | 14 | /// Relay Pool error 15 | #[derive(Debug)] 16 | pub enum Error { 17 | /// Shared state error 18 | SharedState(SharedStateError), 19 | /// Url parse error 20 | RelayUrl(url::Error), 21 | /// Relay error 22 | Relay(relay::Error), 23 | /// Database error 24 | Database(DatabaseError), 25 | /// Notification Handler error 26 | Handler(String), 27 | /// Too many relays 28 | TooManyRelays { 29 | /// Max numer allowed 30 | limit: usize, 31 | }, 32 | /// No relays 33 | NoRelays, 34 | /// No relays specified 35 | NoRelaysSpecified, 36 | /// Negentropy reconciliation failed 37 | NegentropyReconciliationFailed, 38 | /// Relay not found 39 | RelayNotFound, 40 | /// Relay Pool is shutdown 41 | Shutdown, 42 | } 43 | 44 | impl std::error::Error for Error {} 45 | 46 | impl fmt::Display for Error { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | match self { 49 | Self::SharedState(e) => write!(f, "{e}"), 50 | Self::RelayUrl(e) => write!(f, "{e}"), 51 | Self::Relay(e) => write!(f, "{e}"), 52 | Self::Database(e) => write!(f, "{e}"), 53 | Self::Handler(e) => write!(f, "{e}"), 54 | Self::TooManyRelays { limit } => write!(f, "too many relays (limit: {limit})"), 55 | Self::NoRelays => write!(f, "no relays"), 56 | Self::NoRelaysSpecified => write!(f, "no relays specified"), 57 | Self::NegentropyReconciliationFailed => write!(f, "negentropy reconciliation failed"), 58 | Self::RelayNotFound => write!(f, "relay not found"), 59 | Self::Shutdown => write!(f, "relay pool is shutdown"), 60 | } 61 | } 62 | } 63 | 64 | impl From for Error { 65 | fn from(e: SharedStateError) -> Self { 66 | Self::SharedState(e) 67 | } 68 | } 69 | 70 | impl From for Error { 71 | fn from(e: url::Error) -> Self { 72 | Self::RelayUrl(e) 73 | } 74 | } 75 | 76 | impl From for Error { 77 | fn from(e: relay::Error) -> Self { 78 | Self::Relay(e) 79 | } 80 | } 81 | 82 | impl From for Error { 83 | fn from(e: DatabaseError) -> Self { 84 | Self::Database(e) 85 | } 86 | } 87 | 88 | impl From for Error { 89 | fn from(_: Infallible) -> Self { 90 | unreachable!() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/pool/options.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Pool options 6 | 7 | use super::constants::DEFAULT_NOTIFICATION_CHANNEL_SIZE; 8 | 9 | /// Relay Pool Options 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 11 | pub struct RelayPoolOptions { 12 | pub(super) max_relays: Option, 13 | pub(super) nip42_auto_authentication: bool, 14 | pub(super) notification_channel_size: usize, 15 | } 16 | 17 | impl Default for RelayPoolOptions { 18 | fn default() -> Self { 19 | Self { 20 | max_relays: None, 21 | nip42_auto_authentication: true, 22 | notification_channel_size: DEFAULT_NOTIFICATION_CHANNEL_SIZE, 23 | } 24 | } 25 | } 26 | 27 | impl RelayPoolOptions { 28 | /// New default options 29 | #[inline] 30 | pub fn new() -> Self { 31 | Self::default() 32 | } 33 | 34 | /// Max relays (default: None) 35 | #[inline] 36 | pub fn max_relays(mut self, num: Option) -> Self { 37 | self.max_relays = num; 38 | self 39 | } 40 | 41 | /// Auto authenticate to relays (default: true) 42 | /// 43 | /// 44 | #[inline] 45 | pub fn automatic_authentication(mut self, enabled: bool) -> Self { 46 | self.nip42_auto_authentication = enabled; 47 | self 48 | } 49 | 50 | /// Notification channel size (default: [`DEFAULT_NOTIFICATION_CHANNEL_SIZE`]) 51 | #[inline] 52 | pub fn notification_channel_size(mut self, size: usize) -> Self { 53 | self.notification_channel_size = size; 54 | self 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/pool/output.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::collections::{HashMap, HashSet}; 6 | use std::fmt::Debug; 7 | use std::ops::{Deref, DerefMut}; 8 | 9 | use nostr::{EventId, RelayUrl, SubscriptionId}; 10 | 11 | /// Output 12 | /// 13 | /// Send or negentropy reconciliation output 14 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 15 | pub struct Output 16 | where 17 | T: Debug, 18 | { 19 | /// Value 20 | pub val: T, 21 | /// Set of relays that success 22 | pub success: HashSet, 23 | /// Map of relays that failed, with related errors. 24 | pub failed: HashMap, 25 | } 26 | 27 | impl Deref for Output 28 | where 29 | T: Debug, 30 | { 31 | type Target = T; 32 | 33 | fn deref(&self) -> &Self::Target { 34 | &self.val 35 | } 36 | } 37 | 38 | impl DerefMut for Output 39 | where 40 | T: Debug, 41 | { 42 | fn deref_mut(&mut self) -> &mut Self::Target { 43 | &mut self.val 44 | } 45 | } 46 | 47 | impl Output { 48 | /// Get event ID 49 | #[inline] 50 | pub fn id(&self) -> &EventId { 51 | self.deref() 52 | } 53 | } 54 | 55 | impl Output { 56 | /// Get subscription ID 57 | #[inline] 58 | pub fn id(&self) -> &SubscriptionId { 59 | self.deref() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | #![doc(hidden)] 10 | 11 | // External crates 12 | pub use async_utility::futures_util::StreamExt; // Needed for `RelayPool::stream_events_of` 13 | pub use nostr::prelude::*; 14 | pub use nostr_database::*; 15 | 16 | // Internal modules 17 | pub use crate::monitor::{self, *}; 18 | pub use crate::policy::*; 19 | pub use crate::pool::builder::*; 20 | pub use crate::pool::constants::*; 21 | pub use crate::pool::options::*; 22 | pub use crate::pool::{self, *}; 23 | pub use crate::relay::{self, *}; 24 | pub use crate::stream::*; 25 | pub use crate::*; 26 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/relay/constants.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Relay constants 6 | 7 | use core::ops::RangeInclusive; 8 | use core::time::Duration; 9 | 10 | pub(super) const WAIT_FOR_OK_TIMEOUT: Duration = Duration::from_secs(10); 11 | pub(super) const WAIT_FOR_AUTHENTICATION_TIMEOUT: Duration = Duration::from_secs(7); 12 | pub(super) const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60); 13 | 14 | /// Relay default notification channel size 15 | pub const DEFAULT_NOTIFICATION_CHANNEL_SIZE: usize = 2048; 16 | 17 | /// Max relay size 18 | pub const MAX_MESSAGE_SIZE: u32 = 5 * 1024 * 1024; // 5 MB 19 | /// Max event size 20 | pub const MAX_EVENT_SIZE: u32 = 70 * 1024; // 70 kB 21 | /// Max event size for contact list kind 22 | pub const MAX_CONTACT_LIST_EVENT_SIZE: u32 = 840 * 1024; // 840 kB 23 | 24 | pub(super) const DEFAULT_RETRY_INTERVAL: Duration = Duration::from_secs(10); 25 | // Not increase the max retry interval too much. 26 | // Keep it small, avoid huge waits before reconnection if internet was gone for much time and then come back. 27 | pub(super) const MAX_RETRY_INTERVAL: Duration = Duration::from_secs(60); 28 | pub(super) const JITTER_RANGE: RangeInclusive = -3..=3; 29 | 30 | pub(super) const NEGENTROPY_FRAME_SIZE_LIMIT: u64 = 60_000; // Default frame limit is 128k. Halve that (hex encoding) and subtract a bit (JSON msg overhead) 31 | pub(super) const NEGENTROPY_HIGH_WATER_UP: usize = 100; 32 | pub(super) const NEGENTROPY_LOW_WATER_UP: usize = 50; 33 | pub(super) const NEGENTROPY_BATCH_SIZE_DOWN: usize = 100; 34 | 35 | pub(super) const MIN_ATTEMPTS: usize = 1; 36 | pub(super) const MIN_SUCCESS_RATE: f64 = 0.90; 37 | 38 | pub(super) const PING_INTERVAL: Duration = Duration::from_secs(55); // Used also for latency calculation 39 | 40 | pub(super) const WEBSOCKET_TX_TIMEOUT: Duration = Duration::from_secs(10); 41 | 42 | #[cfg(not(target_arch = "wasm32"))] 43 | pub(crate) const LATENCY_MIN_READS: u64 = 3; 44 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/relay/ping.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | #[cfg(not(target_arch = "wasm32"))] 6 | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 7 | #[cfg(not(target_arch = "wasm32"))] 8 | use std::time::Instant; 9 | 10 | #[cfg(not(target_arch = "wasm32"))] 11 | use tokio::sync::RwLock; 12 | 13 | #[derive(Debug)] 14 | #[cfg(not(target_arch = "wasm32"))] 15 | pub(super) struct PingTracker { 16 | sent_at: RwLock, 17 | last_nonce: AtomicU64, 18 | replied: AtomicBool, 19 | } 20 | 21 | #[derive(Debug, Default)] 22 | #[cfg(target_arch = "wasm32")] 23 | pub(super) struct PingTracker {} 24 | 25 | #[cfg(not(target_arch = "wasm32"))] 26 | impl Default for PingTracker { 27 | fn default() -> Self { 28 | Self { 29 | sent_at: RwLock::new(Instant::now()), 30 | last_nonce: AtomicU64::new(0), 31 | replied: AtomicBool::new(false), 32 | } 33 | } 34 | } 35 | 36 | #[cfg(not(target_arch = "wasm32"))] 37 | impl PingTracker { 38 | /// Get sent at 39 | #[inline] 40 | pub async fn sent_at(&self) -> Instant { 41 | *self.sent_at.read().await 42 | } 43 | 44 | /// Last nonce 45 | #[inline] 46 | pub fn last_nonce(&self) -> u64 { 47 | self.last_nonce.load(Ordering::SeqCst) 48 | } 49 | 50 | /// Replied 51 | #[inline] 52 | pub fn replied(&self) -> bool { 53 | self.replied.load(Ordering::SeqCst) 54 | } 55 | 56 | pub(super) async fn just_sent(&self) { 57 | let mut sent_at = self.sent_at.write().await; 58 | *sent_at = Instant::now(); 59 | } 60 | 61 | #[inline] 62 | pub(super) fn set_last_nonce(&self, nonce: u64) { 63 | self.last_nonce.store(nonce, Ordering::SeqCst) 64 | } 65 | 66 | #[inline] 67 | pub(super) fn set_replied(&self, replied: bool) { 68 | self.replied.store(replied, Ordering::SeqCst); 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | 76 | #[tokio::test] 77 | async fn test_ping_tracker_sent() { 78 | let tracker = PingTracker::default(); 79 | let before = tracker.sent_at().await; 80 | tracker.just_sent().await; 81 | let after = tracker.sent_at().await; 82 | assert!(after >= before); 83 | } 84 | 85 | #[test] 86 | fn test_ping_tracker_last_nonce() { 87 | let tracker = PingTracker::default(); 88 | tracker.set_last_nonce(42); 89 | assert_eq!(tracker.last_nonce(), 42); 90 | } 91 | 92 | #[test] 93 | fn test_ping_tracker_replied() { 94 | let tracker = PingTracker::default(); 95 | tracker.set_replied(true); 96 | assert!(tracker.replied()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/stream.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Streams 6 | 7 | use std::pin::Pin; 8 | use std::task::{Context, Poll}; 9 | 10 | use async_utility::futures_util::Stream; 11 | use tokio::sync::mpsc::Receiver; 12 | 13 | /// A wrapper around [`Receiver`] that implements [`Stream`]. 14 | #[derive(Debug)] 15 | pub struct ReceiverStream { 16 | inner: Receiver, 17 | } 18 | 19 | impl ReceiverStream { 20 | /// Create a new `ReceiverStream`. 21 | #[inline] 22 | pub fn new(recv: Receiver) -> Self { 23 | Self { inner: recv } 24 | } 25 | 26 | /// Get back the inner [`Receiver`]. 27 | #[inline] 28 | pub fn into_inner(self) -> Receiver { 29 | self.inner 30 | } 31 | 32 | /// Closes the receiving half of a channel without dropping it. 33 | /// 34 | /// Check [`Receiver::close`] to learn more. 35 | #[inline] 36 | pub fn close(&mut self) { 37 | self.inner.close(); 38 | } 39 | } 40 | 41 | impl Stream for ReceiverStream { 42 | type Item = T; 43 | 44 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 45 | self.inner.poll_recv(cx) 46 | } 47 | } 48 | 49 | impl AsRef> for ReceiverStream { 50 | fn as_ref(&self) -> &Receiver { 51 | &self.inner 52 | } 53 | } 54 | 55 | impl AsMut> for ReceiverStream { 56 | fn as_mut(&mut self) -> &mut Receiver { 57 | &mut self.inner 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/transport/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Transport Error 6 | 7 | use core::fmt; 8 | 9 | /// Transport Error 10 | #[derive(Debug)] 11 | pub enum TransportError { 12 | /// An error happened in the underlying backend. 13 | Backend(Box), 14 | } 15 | 16 | impl std::error::Error for TransportError {} 17 | 18 | impl fmt::Display for TransportError { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | match self { 21 | Self::Backend(e) => write!(f, "{e}"), 22 | } 23 | } 24 | } 25 | 26 | impl TransportError { 27 | /// Create a new backend error 28 | /// 29 | /// Shorthand for `Error::Backend(Box::new(error))`. 30 | #[inline] 31 | pub fn backend(error: E) -> Self 32 | where 33 | E: std::error::Error + Send + Sync + 'static, 34 | { 35 | Self::Backend(Box::new(error)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/nostr-relay-pool/src/transport/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Nostr transports 6 | 7 | pub mod error; 8 | pub mod websocket; 9 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/aggregated-query.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_sdk::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let database = NostrLMDB::open("./db/nostr-lmdb")?; 14 | let client: Client = Client::builder().database(database).build(); 15 | client.add_relay("wss://relay.damus.io").await?; 16 | 17 | client.connect().await; 18 | 19 | let public_key = 20 | PublicKey::from_bech32("npub1080l37pfvdpyuzasyuy2ytjykjvq3ylr5jlqlg7tvzjrh9r8vn3sf5yaph")?; 21 | 22 | // ################ Aggregated query with same filter ################ 23 | let filter = Filter::new() 24 | .author(public_key) 25 | .kind(Kind::TextNote) 26 | .limit(50); 27 | let stored_events = client.database().query(filter.clone()).await?; 28 | let fetched_events = client.fetch_events(filter, Duration::from_secs(10)).await?; 29 | let events = stored_events.merge(fetched_events); 30 | 31 | for event in events.into_iter() { 32 | println!("{}", event.as_json()); 33 | } 34 | 35 | // ################ Aggregated query with different filters ################ 36 | 37 | // Query events from database 38 | let filter = Filter::new().author(public_key).kind(Kind::TextNote); 39 | let stored_events = client.database().query(filter).await?; 40 | 41 | // Query events from relays 42 | let filter = Filter::new().author(public_key).kind(Kind::Metadata); 43 | let fetched_events = client.fetch_events(filter, Duration::from_secs(10)).await?; 44 | 45 | // Add temp relay and fetch other events 46 | client.add_relay("wss://nostr.oxtr.dev").await?; 47 | client.connect_relay("wss://nostr.oxtr.dev").await?; 48 | let filter = Filter::new().kind(Kind::ContactList).limit(100); 49 | let fetched_events_from = client 50 | .fetch_events_from(["wss://nostr.oxtr.dev"], filter, Duration::from_secs(10)) 51 | .await?; 52 | client.force_remove_relay("wss://nostr.oxtr.dev").await?; 53 | 54 | // Aggregate results (can be done many times) 55 | let events = stored_events 56 | .merge(fetched_events) 57 | .merge(fetched_events_from); 58 | 59 | for event in events.into_iter() { 60 | println!("{}", event.as_json()); 61 | } 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/blacklist.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::collections::HashSet; 6 | use std::time::Duration; 7 | 8 | use nostr_sdk::prelude::*; 9 | 10 | #[derive(Debug, Default)] 11 | struct Filtering { 12 | muted_public_keys: HashSet, 13 | } 14 | 15 | impl AdmitPolicy for Filtering { 16 | fn admit_event<'a>( 17 | &'a self, 18 | _relay_url: &'a RelayUrl, 19 | _subscription_id: &'a SubscriptionId, 20 | event: &'a Event, 21 | ) -> BoxedFuture<'a, Result> { 22 | Box::pin(async move { 23 | if self.muted_public_keys.contains(&event.pubkey) { 24 | return Ok(AdmitStatus::rejected("Muted")); 25 | } 26 | 27 | Ok(AdmitStatus::success()) 28 | }) 29 | } 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() -> Result<()> { 34 | tracing_subscriber::fmt::init(); 35 | 36 | let mut filtering = Filtering::default(); 37 | 38 | // Mute public key 39 | let muted_public_key = 40 | PublicKey::from_bech32("npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft")?; 41 | filtering.muted_public_keys.insert(muted_public_key); 42 | 43 | // Init client 44 | let client = Client::builder().admit_policy(filtering).build(); 45 | client.add_relay("wss://relay.damus.io").await?; 46 | client.connect().await; 47 | 48 | // Get events from all connected relays 49 | let public_key = 50 | PublicKey::from_bech32("npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s")?; 51 | let filter = Filter::new() 52 | .authors([muted_public_key, public_key]) 53 | .kind(Kind::Metadata); 54 | let events = client.fetch_events(filter, Duration::from_secs(10)).await?; 55 | println!("Received {} events.", events.len()); 56 | 57 | assert_eq!(events.len(), 1); 58 | assert_eq!(events.first_owned().unwrap().pubkey, public_key); 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/client.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_sdk::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | tracing_subscriber::fmt::init(); 10 | 11 | let keys = Keys::parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; 12 | let client = Client::new(keys); 13 | 14 | client.add_relay("wss://relay.damus.io").await?; 15 | client.add_relay("wss://nostr.wine").await?; 16 | client.add_relay("wss://relay.rip").await?; 17 | 18 | client.connect().await; 19 | 20 | // Publish a text note 21 | let builder = EventBuilder::text_note("Hello world"); 22 | let output = client.send_event_builder(builder).await?; 23 | println!("Event ID: {}", output.id().to_bech32()?); 24 | println!("Sent to: {:?}", output.success); 25 | println!("Not sent to: {:?}", output.failed); 26 | 27 | // Create a text note POW event to relays 28 | let builder = EventBuilder::text_note("POW text note from rust-nostr").pow(20); 29 | client.send_event_builder(builder).await?; 30 | 31 | // Send a text note POW event to specific relays 32 | let builder = EventBuilder::text_note("POW text note from rust-nostr 16").pow(16); 33 | client 34 | .send_event_builder_to(["wss://relay.damus.io", "wss://relay.rip"], builder) 35 | .await?; 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/code_snippet.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_sdk::prelude::*; 6 | 7 | const EXAMPLE_SNIPPET: &str = include_str!("code_snippet.rs"); 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let keys = Keys::generate(); 14 | let client = Client::new(keys); 15 | 16 | client.add_relay("wss://relay.damus.io").await?; 17 | client.add_relay("wss://nos.lol").await?; 18 | client.add_relay("wss://nostr.mom").await?; 19 | 20 | client.connect().await; 21 | 22 | // Build a code snippet for this example :) 23 | let snippet: CodeSnippet = CodeSnippet::new(EXAMPLE_SNIPPET) 24 | .name("code_snippts.rs") 25 | .description("Snippet that snippet itself") 26 | .language("rust") 27 | .extension("rs") 28 | .license("MIT"); 29 | 30 | let builder = EventBuilder::code_snippet(snippet); 31 | 32 | let event = client.send_event_builder(builder).await?; 33 | let nevent = Nip19Event::new(*event.id()).relays(vec![ 34 | RelayUrl::parse("wss://nos.lol")?, 35 | RelayUrl::parse("wss://nostr.mom")?, 36 | ]); 37 | 38 | tracing::info!("Done, check the event `{}`", nevent.to_bech32()?); 39 | 40 | client.shutdown().await; 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/comment.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_sdk::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let keys = Keys::generate(); 14 | let client = Client::builder().signer(keys).build(); 15 | 16 | client.add_relay("wss://relay.damus.io/").await?; 17 | client.add_relay("wss://relay.primal.net/").await?; 18 | 19 | client.connect().await; 20 | 21 | let event_id = 22 | EventId::from_bech32("note1hrrgx2309my3wgeecx2tt6fl2nl8hcwl0myr3xvkcqpnq24pxg2q06armr")?; 23 | let events = client 24 | .fetch_events(Filter::new().id(event_id), Duration::from_secs(10)) 25 | .await?; 26 | 27 | let comment_to = events.first().unwrap(); 28 | let builder = EventBuilder::comment("This is a reply", comment_to, None, None); 29 | 30 | let output = client.send_event_builder(builder).await?; 31 | println!("Output: {:?}", output); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/fetch-events.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_sdk::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let public_key = 14 | PublicKey::from_bech32("npub1080l37pfvdpyuzasyuy2ytjykjvq3ylr5jlqlg7tvzjrh9r8vn3sf5yaph")?; 15 | 16 | let client = Client::default(); 17 | client.add_relay("wss://relay.damus.io").await?; 18 | client.add_relay("wss://relay.rip").await?; 19 | 20 | client.connect().await; 21 | 22 | let filter = Filter::new().author(public_key).kind(Kind::Metadata); 23 | let events = client.fetch_events(filter, Duration::from_secs(10)).await?; 24 | println!("{events:#?}"); 25 | 26 | let filter = Filter::new() 27 | .author(public_key) 28 | .kind(Kind::TextNote) 29 | .limit(3); 30 | let events = client 31 | .fetch_events_from( 32 | ["wss://relay.damus.io", "wss://relay.rip"], 33 | filter, 34 | Duration::from_secs(10), 35 | ) 36 | .await?; 37 | println!("{events:#?}"); 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/gossip.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_sdk::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let keys = Keys::parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; 14 | let opts = Options::new().gossip(true); 15 | let client = Client::builder().signer(keys).opts(opts).build(); 16 | 17 | client.add_discovery_relay("wss://relay.damus.io").await?; 18 | client.add_discovery_relay("wss://purplepag.es").await?; 19 | //client.add_discovery_relay("ws://oxtrdevav64z64yb7x6rjg4ntzqjhedm5b5zjqulugknhzr46ny2qbad.onion").await?; 20 | 21 | // client.add_relay("wss://relay.snort.social").await?; 22 | // client.add_relay("wss://relay.damus.io").await?; 23 | 24 | client.connect().await; 25 | 26 | // Publish a text note 27 | let pubkey = 28 | PublicKey::parse("npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet")?; 29 | 30 | let builder = EventBuilder::text_note( 31 | "Hello world nostr:npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet", 32 | ) 33 | .tag(Tag::public_key(pubkey)); 34 | let output = client.send_event_builder(builder).await?; 35 | println!("Event ID: {}", output.to_bech32()?); 36 | 37 | println!("Sent to:"); 38 | for url in output.success.into_iter() { 39 | println!("- {url}"); 40 | } 41 | 42 | println!("Not sent to:"); 43 | for (url, reason) in output.failed.into_iter() { 44 | println!("- {url}: {reason:?}"); 45 | } 46 | 47 | // Get events 48 | let filter = Filter::new().author(pubkey).kind(Kind::TextNote).limit(3); 49 | let events = client.fetch_events(filter, Duration::from_secs(10)).await?; 50 | 51 | for event in events.into_iter() { 52 | println!("{}", event.as_json()); 53 | } 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/limits.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_sdk::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | tracing_subscriber::fmt::init(); 10 | 11 | // Customize relay limits 12 | let mut limits = RelayLimits::default(); 13 | limits.messages.max_size = Some(10_000); 14 | limits.events.max_size = Some(3_000); 15 | 16 | // OR, disable all limits 17 | let limits = RelayLimits::disable(); 18 | 19 | // Compose options and limits 20 | let opts = Options::new().relay_limits(limits); 21 | let client = Client::builder().opts(opts).build(); 22 | 23 | // Add relays and connect 24 | client.add_relay("wss://relay.damus.io").await?; 25 | client.add_relay("wss://nostr.oxtr.dev").await?; 26 | client.connect().await; 27 | 28 | // ... 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/lmdb.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_sdk::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | tracing_subscriber::fmt::init(); 10 | 11 | let keys = Keys::parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; 12 | 13 | let database = NostrLMDB::open("./db/nostr-lmdb")?; 14 | let client: Client = ClientBuilder::default() 15 | .signer(keys.clone()) 16 | .database(database) 17 | .build(); 18 | 19 | client.add_relay("wss://relay.damus.io").await?; 20 | client.add_relay("wss://nostr.wine").await?; 21 | client.add_relay("wss://nostr.oxtr.dev").await?; 22 | 23 | client.connect().await; 24 | 25 | // Publish a text note 26 | let builder = EventBuilder::text_note("Hello world"); 27 | client.send_event_builder(builder).await?; 28 | 29 | // Negentropy sync 30 | let filter = Filter::new().author(keys.public_key()); 31 | let (tx, mut rx) = SyncProgress::channel(); 32 | let opts = SyncOptions::default().progress(tx); 33 | 34 | tokio::spawn(async move { 35 | while rx.changed().await.is_ok() { 36 | let progress = *rx.borrow_and_update(); 37 | if progress.total > 0 { 38 | println!("{:.2}%", progress.percentage() * 100.0); 39 | } 40 | } 41 | }); 42 | let output = client.sync(filter, &opts).await?; 43 | 44 | println!("Local: {}", output.local.len()); 45 | println!("Remote: {}", output.remote.len()); 46 | println!("Sent: {}", output.sent.len()); 47 | println!("Received: {}", output.received.len()); 48 | println!("Failures:"); 49 | for (url, map) in output.send_failures.iter() { 50 | println!("* '{url}':"); 51 | for (id, e) in map.iter() { 52 | println!(" - {id}: {e}"); 53 | } 54 | } 55 | 56 | // Query events from database 57 | let filter = Filter::new().author(keys.public_key()).limit(10); 58 | let events = client.database().query(filter).await?; 59 | println!("Events: {events:?}"); 60 | 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/monitor.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_sdk::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | tracing_subscriber::fmt::init(); 10 | 11 | let monitor = Monitor::new(4096); 12 | let client = Client::builder().monitor(monitor).build(); 13 | 14 | // Subscribe to monitor notifications 15 | let mut notifications = client.monitor().unwrap().subscribe(); 16 | 17 | client.add_relay("wss://relay.damus.io").await?; 18 | client.add_relay("wss://nostr.wine").await?; 19 | client.add_relay("wss://relay.rip").await?; 20 | 21 | client.connect().await; 22 | 23 | while let Ok(notification) = notifications.recv().await { 24 | match notification { 25 | MonitorNotification::StatusChanged { relay_url, status } => { 26 | println!("Relay status changed for {relay_url}: {status}") 27 | } 28 | } 29 | } 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/nostr-connect.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_connect::prelude::*; 8 | use nostr_sdk::prelude::*; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | tracing_subscriber::fmt::init(); 13 | 14 | let app_keys = Keys::parse("nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99")?; 15 | 16 | // Compose signer from bunker URI 17 | let uri = NostrConnectURI::parse("bunker://79dff8f82963424e0bb02708a22e44b4980893e3a4be0fa3cb60a43b946764e3?relay=wss://relay.nsec.app")?; 18 | let signer = NostrConnect::new(uri, app_keys, Duration::from_secs(120), None)?; 19 | 20 | // Compose signer 21 | // let uri = NostrConnectURI::client( 22 | // app_keys.public_key(), 23 | // [Url::parse("wss://relay.nsec.app")?], 24 | // "Test app", 25 | // ); 26 | // println!("\n{uri}\n"); 27 | // let signer = NostrConnect::new(uri, app_keys, Duration::from_secs(120), None)?; 28 | 29 | // Get bunker URI for future connections 30 | let bunker_uri: NostrConnectURI = signer.bunker_uri().await?; 31 | println!("\nBunker URI: {bunker_uri}\n"); 32 | 33 | // Compose client 34 | let client = Client::new(signer); 35 | client.add_relay("wss://relay.damus.io").await?; 36 | client.connect().await; 37 | 38 | // Publish events 39 | let builder = EventBuilder::text_note("Testing rust-nostr NIP46 signer [bunker]"); 40 | let output = client.send_event_builder(builder).await?; 41 | println!("Published text note: {}\n", output.id()); 42 | 43 | let receiver = 44 | PublicKey::from_bech32("npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet")?; 45 | let output = client 46 | .send_private_msg(receiver, "Hello from rust-nostr", []) 47 | .await?; 48 | println!("Sent DM: {}", output.id()); 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/nostrdb.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_sdk::prelude::*; 6 | 7 | const BECH32_SK: &str = "nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85"; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let keys = Keys::parse(BECH32_SK)?; 14 | 15 | let database = NdbDatabase::open("./db/ndb")?; 16 | let client: Client = Client::builder() 17 | .signer(keys.clone()) 18 | .database(database) 19 | .build(); 20 | 21 | client.add_relay("wss://relay.damus.io").await?; 22 | client.add_relay("wss://atl.purplerelay.com").await?; 23 | client.connect().await; 24 | 25 | // Publish a text note 26 | let builder = EventBuilder::text_note("Hello world"); 27 | client.send_event_builder(builder).await?; 28 | 29 | // Negentropy reconcile 30 | let filter = Filter::new().author(keys.public_key()); 31 | client.sync(filter, &SyncOptions::default()).await?; 32 | 33 | // Query events from database 34 | let filter = Filter::new().author(keys.public_key()).limit(10); 35 | let events = client.database().query(filter).await?; 36 | println!("Events: {events:?}"); 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/status.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_sdk::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let keys = Keys::parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; 14 | let client = Client::new(keys); 15 | 16 | client.add_relay("wss://relay.damus.io").await?; 17 | client.add_relay("wss://nostr.wine").await?; 18 | client.add_relay("wss://relay.rip").await?; 19 | 20 | client.connect().await; 21 | 22 | // Send a General statuses event to relays 23 | let general = LiveStatus::new(StatusType::General); 24 | let builder = EventBuilder::live_status(general, "Building rust-nostr"); 25 | client.send_event_builder(builder).await?; 26 | 27 | // Send a Music statuses event to relays 28 | let music = LiveStatus { 29 | status_type: StatusType::Music, 30 | expiration: Some(Timestamp::now() + Duration::from_secs(60 * 60 * 24)), 31 | reference: Some("spotify:search:Intergalatic%20-%20Beastie%20Boys".into()), 32 | }; 33 | let builder = EventBuilder::live_status(music, "Intergalatic - Beastie Boys"); 34 | client.send_event_builder(builder).await?; 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/stream-events.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_sdk::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let client = Client::default(); 14 | client.add_relay("wss://relay.damus.io").await?; 15 | client.add_relay("wss://nos.lol").await?; 16 | 17 | client.connect().await; 18 | 19 | // Stream events from all connected relays 20 | let filter = Filter::new().kind(Kind::TextNote).limit(100); 21 | let mut stream = client 22 | .stream_events(filter, Duration::from_secs(15)) 23 | .await?; 24 | 25 | while let Some(event) = stream.next().await { 26 | println!("{}", event.as_json()); 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/switch-account.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | use nostr_sdk::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | // Account 1 14 | let keys1 = Keys::parse("nsec12kcgs78l06p30jz7z7h3n2x2cy99nw2z6zspjdp7qc206887mwvs95lnkx")?; 15 | let client = Client::new(keys1.clone()); 16 | 17 | client.add_relay("wss://relay.damus.io").await?; 18 | client.connect().await; 19 | 20 | // Subscribe 21 | let filter = Filter::new() 22 | .author(keys1.public_key) 23 | .kind(Kind::TextNote) 24 | .limit(10); 25 | client.subscribe(filter, None).await?; 26 | 27 | // Wait a little 28 | tokio::time::sleep(Duration::from_secs(20)).await; 29 | 30 | println!("Switching account..."); 31 | 32 | // Reset client to change account 33 | client.reset().await; 34 | 35 | // Account 2 36 | let keys2 = Keys::parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; 37 | client.set_signer(keys2.clone()).await; 38 | 39 | client.add_relay("wss://nostr.oxtr.dev").await?; 40 | client.connect().await; 41 | 42 | println!("Account switched"); 43 | 44 | // Subscribe 45 | let filter = Filter::new() 46 | .author(keys2.public_key) 47 | .kind(Kind::TextNote) 48 | .limit(5); 49 | client.subscribe(filter, None).await?; 50 | 51 | client 52 | .handle_notifications(|notification| async move { 53 | println!("{notification:?}"); 54 | Ok(false) 55 | }) 56 | .await?; 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/tor.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr_sdk::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | tracing_subscriber::fmt::init(); 10 | 11 | // Parse keys 12 | let keys = Keys::parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; 13 | 14 | // Configure client to use embedded tor for `.onion` relays 15 | let connection: Connection = Connection::new() 16 | .embedded_tor() 17 | .target(ConnectionTarget::Onion); 18 | let opts = Options::new().connection(connection); 19 | let client = Client::builder().signer(keys.clone()).opts(opts).build(); 20 | 21 | // Add relays 22 | client.add_relay("wss://relay.damus.io").await?; 23 | client 24 | .add_relay("ws://oxtrdevav64z64yb7x6rjg4ntzqjhedm5b5zjqulugknhzr46ny2qbad.onion") 25 | .await?; 26 | client 27 | .add_relay("ws://2jsnlhfnelig5acq6iacydmzdbdmg7xwunm4xl6qwbvzacw4lwrjmlyd.onion") 28 | .await?; 29 | 30 | client.connect().await; 31 | 32 | let filter: Filter = Filter::new().pubkey(keys.public_key()).limit(0); 33 | client.subscribe(filter, None).await?; 34 | 35 | // Handle subscription notifications with `handle_notifications` method 36 | client 37 | .handle_notifications(|notification| async { 38 | if let RelayPoolNotification::Event { event, .. } = notification { 39 | if event.kind == Kind::GiftWrap { 40 | let UnwrappedGift { rumor, .. } = client.unwrap_gift_wrap(&event).await?; 41 | println!("Rumor: {}", rumor.as_json()); 42 | } else { 43 | println!("{:?}", event); 44 | } 45 | } 46 | Ok(false) // Set to true to exit from the loop 47 | }) 48 | .await?; 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /crates/nostr-sdk/examples/whitelist.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::collections::HashSet; 6 | use std::time::Duration; 7 | 8 | use nostr_sdk::prelude::*; 9 | 10 | #[derive(Debug, Default)] 11 | struct WoT { 12 | allowed_public_keys: HashSet, 13 | } 14 | 15 | impl AdmitPolicy for WoT { 16 | fn admit_event<'a>( 17 | &'a self, 18 | _relay_url: &'a RelayUrl, 19 | _subscription_id: &'a SubscriptionId, 20 | event: &'a Event, 21 | ) -> BoxedFuture<'a, Result> { 22 | Box::pin(async move { 23 | if self.allowed_public_keys.contains(&event.pubkey) { 24 | return Ok(AdmitStatus::success()); 25 | } 26 | 27 | Ok(AdmitStatus::rejected("Not in whitelist")) 28 | }) 29 | } 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() -> Result<()> { 34 | tracing_subscriber::fmt::init(); 35 | 36 | let mut wot = WoT::default(); 37 | 38 | // Allow public key 39 | let allowed_public_key = 40 | PublicKey::from_bech32("npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet")?; 41 | wot.allowed_public_keys.insert(allowed_public_key); 42 | 43 | // Init client 44 | let client = Client::builder().admit_policy(wot).build(); 45 | client.add_relay("wss://relay.damus.io").await?; 46 | client.connect().await; 47 | 48 | let not_in_whitelist_public_key = 49 | PublicKey::from_bech32("npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s")?; 50 | 51 | // Get events from all connected relays 52 | let filter = Filter::new() 53 | .authors([allowed_public_key, not_in_whitelist_public_key]) 54 | .kind(Kind::Metadata); 55 | let events = client.fetch_events(filter, Duration::from_secs(10)).await?; 56 | println!("Received {} events.", events.len()); 57 | 58 | assert_eq!(events.len(), 1); 59 | assert_eq!(events.first().unwrap().pubkey, allowed_public_key); 60 | 61 | for event in events.into_iter() { 62 | println!("{}", event.as_json()); 63 | } 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /crates/nostr-sdk/src/gossip/constant.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::time::Duration; 6 | 7 | /// Max number of relays allowed in NIP17/NIP65 lists 8 | pub const MAX_RELAYS_LIST: usize = 5; 9 | pub const PUBKEY_METADATA_OUTDATED_AFTER: Duration = Duration::from_secs(60 * 60); // 60 min 10 | pub const CHECK_OUTDATED_INTERVAL: Duration = Duration::from_secs(60 * 5); // 5 min 11 | -------------------------------------------------------------------------------- /crates/nostr-sdk/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! High level Nostr client library. 6 | 7 | #![forbid(unsafe_code)] 8 | #![warn(missing_docs)] 9 | #![warn(rustdoc::bare_urls)] 10 | #![warn(clippy::large_futures)] 11 | #![allow(unknown_lints)] // TODO: remove when MSRV >= 1.72.0, required for `clippy::arc_with_non_send_sync` 12 | #![allow(clippy::arc_with_non_send_sync)] 13 | #![allow(clippy::mutable_key_type)] // TODO: remove when possible. Needed to suppress false positive for `BTreeSet` 14 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 15 | #![cfg_attr(feature = "all-nips", doc = include_str!("../README.md"))] 16 | 17 | #[doc(hidden)] 18 | pub use async_utility; 19 | #[doc(hidden)] 20 | pub use nostr::{self, *}; 21 | #[doc(hidden)] 22 | #[cfg(all(target_arch = "wasm32", feature = "indexeddb"))] 23 | pub use nostr_indexeddb::WebDatabase; 24 | #[doc(hidden)] 25 | #[cfg(feature = "lmdb")] 26 | pub use nostr_lmdb::NostrLMDB; 27 | #[doc(hidden)] 28 | #[cfg(feature = "ndb")] 29 | pub use nostr_ndb::{self as ndb, NdbDatabase}; 30 | #[doc(hidden)] 31 | pub use nostr_relay_pool::{ 32 | self as pool, AtomicRelayServiceFlags, Relay, RelayConnectionStats, RelayOptions, RelayPool, 33 | RelayPoolNotification, RelayPoolOptions, RelayServiceFlags, RelayStatus, 34 | SubscribeAutoCloseOptions, SubscribeOptions, SyncDirection, SyncOptions, 35 | }; 36 | 37 | pub mod client; 38 | mod gossip; 39 | pub mod prelude; 40 | 41 | pub use self::client::{Client, ClientBuilder, Options}; 42 | -------------------------------------------------------------------------------- /crates/nostr-sdk/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | #![doc(hidden)] 10 | 11 | // External crates 12 | pub use nostr::prelude::*; 13 | pub use nostr_database::prelude::*; 14 | pub use nostr_relay_pool::prelude::*; 15 | 16 | // Internal modules 17 | pub use crate::client::*; 18 | pub use crate::*; 19 | -------------------------------------------------------------------------------- /crates/nostr/examples/embedded/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock -------------------------------------------------------------------------------- /crates/nostr/examples/embedded/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "embedded" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # Prevent this from interfering with workspaces 7 | [workspace] 8 | members = ["."] 9 | 10 | [[bin]] 11 | name = "embedded" 12 | test = false 13 | bench = false 14 | 15 | [dependencies] 16 | alloc-cortex-m = "0.4.1" 17 | cortex-m = "0.6.0" 18 | cortex-m-rt = "0.6.10" 19 | cortex-m-semihosting = "0.3.3" 20 | nostr = { path = "../../../nostr", default-features = false, features = ["alloc", "nip06"] } 21 | 22 | [profile.release] 23 | opt-level = "z" 24 | codegen-units = 1 # better optimizations 25 | lto = true # better optimizations 26 | debug = true # symbols are nice and they don't increase the size on Flash 27 | -------------------------------------------------------------------------------- /crates/nostr/examples/embedded/README.md: -------------------------------------------------------------------------------- 1 | # Embedded 2 | 3 | ## Running 4 | 5 | To run the embedded test, first prepare your environment: 6 | 7 | ```shell 8 | make init 9 | ``` 10 | 11 | Then: 12 | 13 | ```shell 14 | make run 15 | ``` 16 | 17 | Output should be something like: 18 | 19 | ```text 20 | heap size 262144 21 | 22 | Restored keys from bech32: 23 | - Secret Key (hex): 9571a568a42b9e05646a349c783159b906b498119390df9a5a02667155128028 24 | - Public Key (hex): aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4 25 | - Secret Key (bech32): nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99 26 | - Public Key (bech32): npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy 27 | 28 | Restore keys from mnemonic: 29 | - Secret Key (hex): 06992419a8fe821dd8de03d4c300614e8feefb5ea936b76f89976dcace8aebee 30 | - Public Key (hex): 648777b13344158549551f215ab2885d71af8861456eebea7102b1c729fc2de2 31 | - Secret Key (bech32): nsec1q6vjgxdgl6ppmkx7q02vxqrpf687a7674ymtwmufjaku4n52a0hq9glmaf 32 | - Public Key (bech32): npub1vjrh0vfngs2c2j24rus44v5gt4c6lzrpg4hwh6n3q2cuw20u9h3q4qf4pg 33 | 34 | Random keys (using FakeRng): 35 | - Secret Key (hex): 3939393939393939393939393939393939393939393939393939393939393939 36 | - Public Key (hex): 1ff10be221c7b140505038042f5cc86530e9851a0e6c70ee16c18268768c2e02 37 | - Secret Key (bech32): nsec18yunjwfe8yunjwfe8yunjwfe8yunjwfe8yunjwfe8yunjwfe8yusu2d2eh 38 | - Public Key (bech32): npub1rlcshc3pc7c5q5zs8qzz7hxgv5cwnpg6pek8pmskcxpxsa5v9cpqqk7k0t 39 | ``` 40 | 41 | Note that this heap size is required because of the amount of stack used by libsecp256k1 when initializing a context. 42 | -------------------------------------------------------------------------------- /crates/nostr/examples/embedded/justfile: -------------------------------------------------------------------------------- 1 | RUSTFLAGS := "-C link-arg=-Tlink.x" 2 | CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER := "qemu-system-arm -cpu cortex-m3 -machine mps2-an385 -nographic -semihosting-config enable=on,target=native -kernel" 3 | 4 | default: build 5 | 6 | init: 7 | sudo apt install -y gcc-arm-none-eabi qemu-system-arm gdb-multiarch 8 | 9 | build: 10 | RUSTFLAGS="{{RUSTFLAGS}}" CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER="{{CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER}}" cargo build --release --target thumbv7m-none-eabi 11 | 12 | run: 13 | RUSTFLAGS="{{RUSTFLAGS}}" CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER="{{CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER}}" cargo run --release --target thumbv7m-none-eabi -------------------------------------------------------------------------------- /crates/nostr/examples/embedded/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | FLASH : ORIGIN = 0x00000000, LENGTH = 512K 4 | RAM : ORIGIN = 0x20000000, LENGTH = 512K 5 | } 6 | -------------------------------------------------------------------------------- /crates/nostr/examples/embedded/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | profile = "minimal" 4 | targets = ["thumbv7m-none-eabi"] -------------------------------------------------------------------------------- /crates/nostr/examples/keys.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | fn main() -> Result<()> { 8 | // Random keys 9 | let keys = Keys::generate(); 10 | let public_key = keys.public_key(); 11 | let secret_key = keys.secret_key(); 12 | 13 | println!("Public key: {}", public_key); 14 | println!("Public key bech32: {}", public_key.to_bech32()?); 15 | println!("Secret key: {}", secret_key.to_secret_hex()); 16 | println!("Secret key bech32: {}", secret_key.to_bech32()?); 17 | 18 | // Bech32 keys 19 | let keys = Keys::parse("nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99")?; 20 | println!("Public key: {}", keys.public_key()); 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip05.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | let public_key = 10 | PublicKey::parse("b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a")?; 11 | 12 | if nip05::verify(&public_key, "0xtr@oxtr.dev", None).await? { 13 | println!("NIP05 verified"); 14 | } else { 15 | println!("NIP05 NOT verified"); 16 | } 17 | 18 | let profile: Nip05Profile = nip05::profile("_@fiatjaf.com", None).await?; 19 | println!("Public key: {}", profile.public_key); 20 | println!("Relays: {:?}", profile.relays); 21 | println!("Relays (NIP46): {:?}", profile.nip46); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip06.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::nips::nip06::FromMnemonic; 6 | use nostr::nips::nip19::ToBech32; 7 | use nostr::{Keys, Result}; 8 | 9 | const MNEMONIC_PHRASE: &str = "equal dragon fabric refuse stable cherry smoke allow alley easy never medal attend together lumber movie what sad siege weather matrix buffalo state shoot"; 10 | 11 | fn main() -> Result<()> { 12 | let keys = Keys::from_mnemonic(MNEMONIC_PHRASE, Some("mypassphrase"))?; 13 | println!("{}", keys.secret_key().to_bech32()?); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip09.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | fn main() -> Result<()> { 8 | let keys = Keys::parse("nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85")?; 9 | 10 | let event_id = 11 | EventId::from_hex("7469af3be8c8e06e1b50ef1caceba30392ddc0b6614507398b7d7daa4c218e96")?; 12 | 13 | let request = EventDeletionRequest::new() 14 | .id(event_id) 15 | .reason("these posts were published by accident"); 16 | 17 | let event: Event = EventBuilder::delete(request).sign_with_keys(&keys)?; 18 | println!("{}", event.as_json()); 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip11.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | let relay_url = Url::parse("wss://relay.damus.io")?; 10 | 11 | let info = RelayInformationDocument::get(relay_url, Nip11GetOptions::default()).await?; 12 | 13 | println!("{:#?}", info); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip13.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | fn main() -> Result<()> { 8 | let keys = Keys::parse("6b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e")?; 9 | 10 | let difficulty = 20; // leading zero bits 11 | let msg_content = "This is a Nostr message with embedded proof-of-work"; 12 | 13 | let event: Event = EventBuilder::text_note(msg_content) 14 | .pow(difficulty) 15 | .sign_with_keys(&keys)?; 16 | 17 | println!("{}", event.as_json()); 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip15.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 ProTom 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | fn main() -> Result<()> { 8 | let keys = Keys::parse("6b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e")?; 9 | 10 | let shipping = ShippingMethod::new("123", 5.50).name("DHL"); 11 | 12 | let stall = StallData::new("123", "my test stall", "USD") 13 | .description("this is a test stall") 14 | .shipping(vec![shipping.clone()]); 15 | 16 | let stall_event = EventBuilder::stall_data(stall).sign_with_keys(&keys)?; 17 | println!("{}", stall_event.as_json()); 18 | 19 | let product = ProductData::new("1", "123", "my test product", "USD") 20 | .description("this is a test product") 21 | .price(5.50) 22 | .shipping(vec![shipping.get_shipping_cost()]) 23 | .images(vec!["https://example.com/image.png".into()]) 24 | .categories(vec!["test".into()]); 25 | 26 | let product_event = EventBuilder::product_data(product).sign_with_keys(&keys)?; 27 | println!("{}", product_event.as_json()); 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip19.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | fn main() -> Result<()> { 8 | let pubkey = 9 | PublicKey::from_hex("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")?; 10 | let profile = Nip19Profile::new( 11 | pubkey, 12 | [ 13 | RelayUrl::parse("wss://r.x.com").unwrap(), 14 | RelayUrl::parse("wss://djbas.sadkb.com").unwrap(), 15 | ], 16 | ); 17 | println!("{}", profile.to_bech32()?); 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip57.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | fn main() -> Result<()> { 8 | let keys = Keys::parse("6b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e")?; 9 | 10 | let public_key = 11 | PublicKey::from_bech32("npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy")?; 12 | let relays = [RelayUrl::parse("wss://relay.damus.io").unwrap()]; 13 | let data = ZapRequestData::new(public_key, relays).message("Zap!"); 14 | 15 | let public_zap: Event = EventBuilder::public_zap_request(data.clone()).sign_with_keys(&keys)?; 16 | println!("Public zap request: {}", public_zap.as_json()); 17 | 18 | let anon_zap: Event = nip57::anonymous_zap_request(data.clone())?; 19 | println!("Anonymous zap request: {}", anon_zap.as_json()); 20 | 21 | let private_zap: Event = nip57::private_zap_request(data, &keys)?; 22 | println!("Private zap request: {}", private_zap.as_json()); 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip96.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | const FILE: &[u8] = &[ 8 | 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 9 | 0, 0, 31, 21, 196, 137, 0, 0, 0, 1, 115, 82, 71, 66, 0, 174, 206, 28, 233, 0, 0, 0, 4, 103, 65, 10 | 77, 65, 0, 0, 177, 143, 11, 252, 97, 5, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 28, 35, 0, 0, 28, 11 | 35, 1, 199, 111, 168, 100, 0, 0, 0, 12, 73, 68, 65, 84, 8, 29, 99, 248, 255, 255, 63, 0, 5, 12 | 254, 2, 254, 135, 150, 28, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, 13 | ]; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | let keys = Keys::parse("nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99")?; 18 | 19 | let server_url: Url = Url::parse("https://NostrMedia.com")?; 20 | 21 | let config: ServerConfig = nip96::get_server_config(server_url, None).await?; 22 | 23 | let file_data: Vec = FILE.to_vec(); 24 | 25 | // Upload 26 | let url: Url = nip96::upload_data(&keys, &config, file_data, None, None).await?; 27 | println!("File uploaded: {url}"); 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /crates/nostr/examples/nip98.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | let keys = Keys::parse("nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99")?; 10 | 11 | let server_url: Url = Url::parse("https://example.com")?; 12 | let method = HttpMethod::GET; 13 | 14 | let auth = HttpData::new(server_url, method) 15 | .to_authorization(&keys) 16 | .await?; 17 | 18 | println!("{auth}"); 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /crates/nostr/examples/parser.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use nostr::prelude::*; 6 | 7 | fn main() -> Result<()> { 8 | let parser = NostrParser::new(); 9 | 10 | let text: &str = "I have never been very active in discussions but working on rust-nostr (at the time called nostr-rs-sdk) since September 2022 🦀 \n\nIf I remember correctly there were also nostr:nprofile1qqsqfyvdlsmvj0nakmxq6c8n0c2j9uwrddjd8a95ynzn9479jhlth3gpvemhxue69uhkv6tvw3jhytnwdaehgu3wwa5kuef0dec82c33w94xwcmdd3cxketedsux6ertwecrgues0pk8xdrew33h27pkd4unvvpkw3nkv7pe0p68gat58ycrw6ps0fenwdnvva48w0mzwfhkzerrv9ehg0t5wf6k2qgnwaehxw309ac82unsd3jhqct89ejhxtcpz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsh8njvk and nostr:nprofile1qqswuyd9ml6qcxd92h6pleptfrcqucvvjy39vg4wx7mv9wm8kakyujgpypmhxue69uhkx6r0wf6hxtndd94k2erfd3nk2u3wvdhk6w35xs6z7qgwwaehxw309ahx7uewd3hkctcpypmhxue69uhkummnw3ezuetfde6kuer6wasku7nfvuh8xurpvdjj7a0nq40"; 11 | 12 | for token in parser.parse(text) { 13 | println!("{token:?}"); 14 | } 15 | 16 | for token in parser.parse("Check this: https://example.com/foo/bar.html") { 17 | println!("{token:?}"); 18 | } 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /crates/nostr/src/event/borrow.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Borrowed Event 6 | 7 | use alloc::vec::Vec; 8 | use core::cmp::Ordering; 9 | use core::hash::{Hash, Hasher}; 10 | 11 | use secp256k1::schnorr::Signature; 12 | 13 | use super::tag::cow::CowTag; 14 | use crate::{Event, EventId, Kind, PublicKey, Tags, Timestamp}; 15 | 16 | /// Borrowed event 17 | #[derive(Debug, Clone)] 18 | pub struct EventBorrow<'a> { 19 | /// Event ID 20 | pub id: &'a [u8; 32], 21 | /// Author 22 | pub pubkey: &'a [u8; 32], 23 | /// UNIX timestamp (seconds) 24 | pub created_at: Timestamp, 25 | /// Kind 26 | pub kind: u16, 27 | /// Tag list 28 | pub tags: Vec>, 29 | /// Content 30 | pub content: &'a str, 31 | /// Signature 32 | pub sig: &'a [u8; 64], 33 | } 34 | 35 | impl PartialEq for EventBorrow<'_> { 36 | fn eq(&self, other: &Self) -> bool { 37 | self.id == other.id 38 | } 39 | } 40 | 41 | impl Eq for EventBorrow<'_> {} 42 | 43 | impl PartialOrd for EventBorrow<'_> { 44 | fn partial_cmp(&self, other: &Self) -> Option { 45 | Some(self.cmp(other)) 46 | } 47 | } 48 | 49 | impl Ord for EventBorrow<'_> { 50 | fn cmp(&self, other: &Self) -> Ordering { 51 | if self.created_at != other.created_at { 52 | // Descending order 53 | // Lookup ID: EVENT_ORD_IMPL 54 | self.created_at.cmp(&other.created_at).reverse() 55 | } else { 56 | self.id.cmp(other.id) 57 | } 58 | } 59 | } 60 | 61 | impl Hash for EventBorrow<'_> { 62 | fn hash(&self, state: &mut H) { 63 | self.id.hash(state); 64 | } 65 | } 66 | 67 | impl EventBorrow<'_> { 68 | /// Into owned event 69 | pub fn into_owned(self) -> Event { 70 | Event::new( 71 | EventId::from_byte_array(*self.id), 72 | PublicKey::from_byte_array(*self.pubkey), 73 | self.created_at, 74 | Kind::from_u16(self.kind), 75 | Tags::from_list(self.tags.into_iter().map(|t| t.into_owned()).collect()), 76 | self.content, 77 | // SAFETY: signature panic only if it's not 64 byte long 78 | Signature::from_slice(self.sig.as_slice()).expect("valid signature"), 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/nostr/src/event/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use alloc::string::{String, ToString}; 6 | use core::fmt; 7 | 8 | use crate::signer::SignerError; 9 | use crate::util::hex; 10 | 11 | /// Event error 12 | #[derive(Debug, PartialEq, Eq)] 13 | pub enum Error { 14 | /// Error serializing or deserializing JSON data 15 | Json(String), 16 | /// Signer error 17 | Signer(String), 18 | /// Hex decode error 19 | Hex(hex::Error), 20 | /// Unknown JSON event key 21 | UnknownKey(String), 22 | /// Invalid event ID 23 | InvalidId, 24 | /// Invalid signature 25 | InvalidSignature, 26 | } 27 | 28 | #[cfg(feature = "std")] 29 | impl std::error::Error for Error {} 30 | 31 | impl fmt::Display for Error { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | match self { 34 | Self::Json(e) => write!(f, "{e}"), 35 | Self::Signer(e) => write!(f, "{e}"), 36 | Self::Hex(e) => write!(f, "{e}"), 37 | Self::UnknownKey(key) => write!(f, "Unknown key: {key}"), 38 | Self::InvalidId => write!(f, "Invalid event ID"), 39 | Self::InvalidSignature => write!(f, "Invalid signature"), 40 | } 41 | } 42 | } 43 | 44 | impl From for Error { 45 | fn from(e: serde_json::Error) -> Self { 46 | Self::Json(e.to_string()) 47 | } 48 | } 49 | 50 | impl From for Error { 51 | fn from(e: SignerError) -> Self { 52 | Self::Signer(e.to_string()) 53 | } 54 | } 55 | 56 | impl From for Error { 57 | fn from(e: hex::Error) -> Self { 58 | Self::Hex(e) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/nostr/src/event/tag/cow.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Cow Tag 6 | 7 | use alloc::borrow::Cow; 8 | use alloc::string::String; 9 | use alloc::vec::Vec; 10 | use core::str::FromStr; 11 | 12 | use super::error::Error; 13 | use super::Tag; 14 | use crate::filter::SingleLetterTag; 15 | 16 | /// Cow Tag 17 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 18 | pub struct CowTag<'a> { 19 | buf: Vec>, 20 | } 21 | 22 | impl<'a> CowTag<'a> { 23 | /// Parse tag 24 | /// 25 | /// Return error if the tag is empty! 26 | pub fn parse(tag: Vec>) -> Result { 27 | // Check if it's empty 28 | if tag.is_empty() { 29 | return Err(Error::EmptyTag); 30 | } 31 | 32 | Ok(Self { buf: tag }) 33 | } 34 | 35 | /// Extract tag name and value 36 | pub fn extract(&self) -> Option<(SingleLetterTag, &str)> { 37 | if self.buf.len() >= 2 { 38 | let tag_name: SingleLetterTag = SingleLetterTag::from_str(&self.buf[0]).ok()?; 39 | let tag_value: &str = &self.buf[1]; 40 | Some((tag_name, tag_value)) 41 | } else { 42 | None 43 | } 44 | } 45 | 46 | /// Into owned tag 47 | pub fn into_owned(self) -> Tag { 48 | let buf: Vec = self.buf.into_iter().map(|t| t.into_owned()).collect(); 49 | Tag::new_with_empty_cell(buf) 50 | } 51 | 52 | /// Get inner value 53 | #[inline] 54 | pub fn into_inner(self) -> Vec> { 55 | self.buf 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/nostr/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Rust implementation of the Nostr protocol. 6 | 7 | #![cfg_attr(not(target_arch = "wasm32"), forbid(unsafe_code))] 8 | #![cfg_attr(target_arch = "wasm32", deny(unsafe_code))] 9 | #![cfg_attr(test, allow(missing_docs))] 10 | #![cfg_attr(not(test), warn(missing_docs))] 11 | #![warn(rustdoc::bare_urls)] 12 | #![allow(unknown_lints)] // TODO: remove when MSRV >= 1.72.0, required for `clippy::arc_with_non_send_sync` 13 | #![allow(clippy::arc_with_non_send_sync)] 14 | #![cfg_attr(not(feature = "std"), no_std)] 15 | #![cfg_attr(bench, feature(test))] 16 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 17 | #![cfg_attr(all(feature = "std", feature = "all-nips"), doc = include_str!("../README.md"))] 18 | 19 | #[cfg(not(any(feature = "std", feature = "alloc")))] 20 | compile_error!("at least one of the `std` or `alloc` features must be enabled"); 21 | 22 | #[cfg(bench)] 23 | extern crate test; 24 | 25 | #[cfg(feature = "std")] 26 | #[macro_use] 27 | extern crate std; 28 | #[macro_use] 29 | extern crate alloc; 30 | 31 | #[macro_use] 32 | extern crate serde; 33 | pub extern crate bitcoin_hashes as hashes; 34 | pub extern crate secp256k1; 35 | 36 | #[doc(hidden)] 37 | #[cfg(any(feature = "nip04", feature = "nip44"))] 38 | pub use base64; 39 | #[doc(hidden)] 40 | #[cfg(feature = "nip06")] 41 | pub use bip39; 42 | #[doc(hidden)] 43 | pub use serde_json; 44 | 45 | pub mod event; 46 | pub mod filter; 47 | pub mod key; 48 | pub mod message; 49 | pub mod nips; 50 | pub mod parser; 51 | pub mod prelude; 52 | pub mod signer; 53 | pub mod types; 54 | pub mod util; 55 | 56 | #[doc(hidden)] 57 | pub use self::event::tag::{Tag, TagKind, TagStandard, Tags}; 58 | #[doc(hidden)] 59 | pub use self::event::{Event, EventBuilder, EventId, Kind, UnsignedEvent}; 60 | #[doc(hidden)] 61 | pub use self::filter::{Alphabet, Filter, SingleLetterTag}; 62 | #[doc(hidden)] 63 | pub use self::key::{Keys, PublicKey, SecretKey}; 64 | #[doc(hidden)] 65 | pub use self::message::{ClientMessage, RelayMessage, SubscriptionId}; 66 | #[doc(hidden)] 67 | pub use self::nips::nip01::Metadata; 68 | #[doc(hidden)] 69 | pub use self::nips::nip19::{FromBech32, ToBech32}; 70 | #[doc(hidden)] 71 | pub use self::signer::{NostrSigner, SignerError}; 72 | #[doc(hidden)] 73 | pub use self::types::{ImageDimensions, RelayUrl, Timestamp, TryIntoUrl, Url}; 74 | #[doc(hidden)] 75 | pub use self::util::JsonUtil; 76 | #[doc(hidden)] 77 | #[cfg(feature = "std")] 78 | pub use self::util::SECP256K1; 79 | 80 | /// Result 81 | #[doc(hidden)] 82 | #[cfg(feature = "std")] 83 | pub type Result> = std::result::Result; 84 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIPs 6 | //! 7 | //! See all at 8 | 9 | pub mod nip01; 10 | pub mod nip02; 11 | #[cfg(feature = "nip04")] 12 | pub mod nip04; 13 | #[cfg(all(feature = "std", feature = "nip05"))] 14 | pub mod nip05; 15 | #[cfg(feature = "nip06")] 16 | pub mod nip06; 17 | #[cfg(all(feature = "nip07", target_arch = "wasm32"))] 18 | pub mod nip07; 19 | pub mod nip09; 20 | pub mod nip10; 21 | #[cfg(all(feature = "std", feature = "nip11"))] 22 | pub mod nip11; 23 | pub mod nip13; 24 | pub mod nip15; 25 | pub mod nip17; 26 | pub mod nip19; 27 | pub mod nip21; 28 | pub mod nip22; 29 | pub mod nip26; 30 | pub mod nip34; 31 | pub mod nip35; 32 | pub mod nip38; 33 | pub mod nip39; 34 | pub mod nip42; 35 | #[cfg(feature = "nip44")] 36 | pub mod nip44; 37 | #[cfg(all(feature = "std", feature = "nip46"))] 38 | pub mod nip46; 39 | #[cfg(feature = "nip47")] 40 | pub mod nip47; 41 | pub mod nip48; 42 | #[cfg(feature = "nip49")] 43 | pub mod nip49; 44 | pub mod nip51; 45 | pub mod nip53; 46 | pub mod nip56; 47 | #[cfg(feature = "nip57")] 48 | pub mod nip57; 49 | pub mod nip58; 50 | #[cfg(feature = "nip59")] 51 | pub mod nip59; 52 | pub mod nip62; 53 | pub mod nip65; 54 | pub mod nip73; 55 | pub mod nip88; 56 | pub mod nip90; 57 | pub mod nip94; 58 | #[cfg(all(feature = "std", feature = "nip96"))] 59 | pub mod nip96; 60 | #[cfg(feature = "nip98")] 61 | pub mod nip98; 62 | pub mod nipc0; 63 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip02.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP02: Follow List 6 | //! 7 | //! 8 | 9 | use alloc::string::String; 10 | 11 | use crate::key::PublicKey; 12 | use crate::types::RelayUrl; 13 | 14 | /// Contact 15 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 16 | pub struct Contact { 17 | /// Public key 18 | pub public_key: PublicKey, 19 | /// Relay url 20 | pub relay_url: Option, 21 | /// Alias 22 | pub alias: Option, 23 | } 24 | 25 | impl Contact { 26 | /// Create new contact 27 | #[inline] 28 | pub fn new(public_key: PublicKey) -> Self { 29 | Self { 30 | public_key, 31 | relay_url: None, 32 | alias: None, 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip10.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP10: Conventions for clients' use of `e` and `p` tags in text events 6 | //! 7 | //! 8 | 9 | use core::fmt; 10 | use core::str::FromStr; 11 | 12 | /// NIP10 error 13 | #[derive(Debug, PartialEq, Eq)] 14 | pub enum Error { 15 | /// Invalid marker 16 | InvalidMarker, 17 | } 18 | 19 | #[cfg(feature = "std")] 20 | impl std::error::Error for Error {} 21 | 22 | impl fmt::Display for Error { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | match self { 25 | Self::InvalidMarker => write!(f, "invalid marker"), 26 | } 27 | } 28 | } 29 | 30 | /// Marker 31 | /// 32 | /// 33 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 34 | pub enum Marker { 35 | /// Root 36 | Root, 37 | /// Reply 38 | Reply, 39 | } 40 | 41 | impl fmt::Display for Marker { 42 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 43 | match self { 44 | Self::Root => write!(f, "root"), 45 | Self::Reply => write!(f, "reply"), 46 | } 47 | } 48 | } 49 | 50 | impl FromStr for Marker { 51 | type Err = Error; 52 | 53 | fn from_str(marker: &str) -> Result { 54 | match marker { 55 | "root" => Ok(Self::Root), 56 | "reply" => Ok(Self::Reply), 57 | _ => Err(Error::InvalidMarker), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip17.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP17: Private Direct Message 6 | //! 7 | //! 8 | 9 | use alloc::boxed::Box; 10 | use core::iter; 11 | 12 | use crate::{Event, Kind, RelayUrl, TagStandard}; 13 | 14 | /// Extracts the relay list 15 | pub fn extract_relay_list<'a>(event: &'a Event) -> Box + 'a> { 16 | if event.kind != Kind::InboxRelays { 17 | return Box::new(iter::empty()); 18 | } 19 | 20 | Box::new(event.tags.iter().filter_map(|tag| { 21 | if let Some(TagStandard::Relay(url)) = tag.as_standardized() { 22 | Some(url) 23 | } else { 24 | None 25 | } 26 | })) 27 | } 28 | 29 | /// Extracts the relay list 30 | pub fn extract_owned_relay_list(event: Event) -> Box> { 31 | if event.kind != Kind::InboxRelays { 32 | return Box::new(iter::empty()); 33 | } 34 | 35 | Box::new(event.tags.into_iter().filter_map(|tag| { 36 | if let Some(TagStandard::Relay(url)) = tag.to_standardized() { 37 | Some(url) 38 | } else { 39 | None 40 | } 41 | })) 42 | } 43 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip35.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP35: Torrents 6 | //! 7 | //! This module implements support for sharing BitTorrent metadata and comments through nostr events. 8 | //! 9 | //! 10 | 11 | use alloc::string::{String, ToString}; 12 | use alloc::vec::Vec; 13 | 14 | use hashes::sha1::Hash as Sha1Hash; 15 | 16 | use crate::types::url::Url; 17 | use crate::{EventBuilder, Kind, Tag, TagKind}; 18 | 19 | /// Represents a file within a torrent. 20 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 21 | pub struct TorrentFile { 22 | /// File name/path 23 | pub name: String, 24 | /// File size in bytes 25 | pub size: u64, 26 | } 27 | 28 | /// Torrent metadata 29 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 30 | pub struct Torrent { 31 | /// Torrent title 32 | pub title: String, 33 | /// Long description 34 | pub description: String, 35 | /// BitTorrent info hash 36 | pub info_hash: Sha1Hash, 37 | /// Files included in torrent 38 | pub files: Vec, 39 | /// Tracker URLs 40 | pub trackers: Vec, 41 | /// Categories (e.g. "video,movie,4k") 42 | pub categories: Vec, 43 | /// Additional hashtags 44 | pub hashtags: Vec, 45 | } 46 | 47 | impl Torrent { 48 | /// Converts the torrent metadata into an [`EventBuilder`]. 49 | pub fn to_event_builder(self) -> EventBuilder { 50 | let mut tags: Vec = Vec::with_capacity( 51 | 2 + self.files.len() 52 | + self.trackers.len() 53 | + self.categories.len() 54 | + self.hashtags.len(), 55 | ); 56 | 57 | tags.push(Tag::title(self.title)); 58 | 59 | tags.push(Tag::custom(TagKind::x(), [self.info_hash.to_string()])); 60 | 61 | for file in self.files.into_iter() { 62 | tags.push(Tag::custom( 63 | TagKind::File, 64 | [file.name, file.size.to_string()], 65 | )); 66 | } 67 | 68 | for tracker in self.trackers.into_iter() { 69 | tags.push(Tag::custom(TagKind::Tracker, [tracker.to_string()])); 70 | } 71 | 72 | for cat in self.categories.into_iter() { 73 | tags.push(Tag::custom(TagKind::i(), [format!("tcat:{cat}")])); 74 | } 75 | 76 | for tag in self.hashtags.into_iter() { 77 | tags.push(Tag::hashtag(tag)); 78 | } 79 | 80 | EventBuilder::new(Kind::Torrent, self.description).tags(tags) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip38.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP38: User Statuses 6 | //! 7 | //! 8 | 9 | use alloc::string::{String, ToString}; 10 | use alloc::vec::Vec; 11 | use core::fmt; 12 | 13 | use crate::{Tag, Timestamp}; 14 | 15 | /// NIP38 types 16 | #[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] 17 | pub enum StatusType { 18 | /// General status: "Working", "Hiking", etc. 19 | #[default] 20 | General, 21 | /// Music what you are currently listening to 22 | Music, 23 | /// Custom status: "Playing", "Reading", etc. 24 | Custom(String), 25 | } 26 | 27 | impl fmt::Display for StatusType { 28 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 29 | match self { 30 | Self::General => write!(f, "general"), 31 | Self::Music => write!(f, "music"), 32 | Self::Custom(s) => write!(f, "{s}"), 33 | } 34 | } 35 | } 36 | 37 | /// User status 38 | #[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] 39 | pub struct LiveStatus { 40 | /// Status type, includes: General, Music or Custom 41 | pub status_type: StatusType, 42 | /// Expiration time of the status (Optional) 43 | pub expiration: Option, 44 | /// Reference to the external resource (Optional) 45 | pub reference: Option, 46 | } 47 | 48 | impl LiveStatus { 49 | /// Create a new user status 50 | #[inline] 51 | pub fn new(status_type: StatusType) -> Self { 52 | Self { 53 | status_type, 54 | expiration: None, 55 | reference: None, 56 | } 57 | } 58 | } 59 | 60 | impl From for Vec { 61 | fn from( 62 | LiveStatus { 63 | status_type, 64 | expiration, 65 | reference, 66 | }: LiveStatus, 67 | ) -> Self { 68 | let mut tags = 69 | Vec::with_capacity(1 + expiration.is_some() as usize + reference.is_some() as usize); 70 | 71 | tags.push(Tag::identifier(status_type.to_string())); 72 | 73 | if let Some(expire_at) = expiration { 74 | tags.push(Tag::expiration(expire_at)); 75 | } 76 | 77 | if let Some(content) = reference { 78 | tags.push(Tag::reference(content)); 79 | } 80 | 81 | tags 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip48.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP48: Proxy Tags 6 | //! 7 | //! 8 | 9 | use alloc::string::{String, ToString}; 10 | use core::fmt; 11 | 12 | /// NIP48 Proxy Protocol 13 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 14 | pub enum Protocol { 15 | /// ActivityPub 16 | ActivityPub, 17 | /// AT Protocol 18 | ATProto, 19 | /// Rss 20 | Rss, 21 | /// Web 22 | Web, 23 | /// Custom 24 | Custom(String), 25 | } 26 | 27 | impl fmt::Display for Protocol { 28 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 29 | match self { 30 | Self::ActivityPub => write!(f, "activitypub"), 31 | Self::ATProto => write!(f, "atproto"), 32 | Self::Rss => write!(f, "rss"), 33 | Self::Web => write!(f, "web"), 34 | Self::Custom(m) => write!(f, "{m}"), 35 | } 36 | } 37 | } 38 | 39 | impl From for Protocol 40 | where 41 | S: AsRef, 42 | { 43 | fn from(s: S) -> Self { 44 | match s.as_ref() { 45 | "activitypub" => Self::ActivityPub, 46 | "atproto" => Self::ATProto, 47 | "rss" => Self::Rss, 48 | "web" => Self::Web, 49 | s => Self::Custom(s.to_string()), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip56.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP56: Reporting 6 | //! 7 | //! 8 | 9 | use core::fmt; 10 | use core::str::FromStr; 11 | 12 | /// NIP56 error 13 | #[derive(Debug, PartialEq, Eq)] 14 | pub enum Error { 15 | /// Unknown [`Report`] 16 | UnknownReportType, 17 | } 18 | 19 | #[cfg(feature = "std")] 20 | impl std::error::Error for Error {} 21 | 22 | impl fmt::Display for Error { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | match self { 25 | Self::UnknownReportType => write!(f, "Unknown report type"), 26 | } 27 | } 28 | } 29 | 30 | /// Report 31 | /// 32 | /// 33 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 34 | pub enum Report { 35 | /// Depictions of nudity, porn, etc 36 | Nudity, 37 | /// Virus, trojan horse, worm, robot, spyware, adware, back door, ransomware, rootkit, kidnapper, etc. 38 | Malware, 39 | /// Profanity, hateful speech, etc. 40 | Profanity, 41 | /// Something which may be illegal in some jurisdiction 42 | Illegal, 43 | /// Spam 44 | Spam, 45 | /// Someone pretending to be someone else 46 | Impersonation, 47 | /// Reports that don't fit in the above categories 48 | Other, 49 | } 50 | 51 | impl fmt::Display for Report { 52 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 53 | match self { 54 | Self::Nudity => write!(f, "nudity"), 55 | Self::Malware => write!(f, "malware"), 56 | Self::Profanity => write!(f, "profanity"), 57 | Self::Illegal => write!(f, "illegal"), 58 | Self::Spam => write!(f, "spam"), 59 | Self::Impersonation => write!(f, "impersonation"), 60 | Self::Other => write!(f, "other"), 61 | } 62 | } 63 | } 64 | 65 | impl FromStr for Report { 66 | type Err = Error; 67 | 68 | fn from_str(s: &str) -> Result { 69 | match s { 70 | "nudity" => Ok(Self::Nudity), 71 | "malware" => Ok(Self::Malware), 72 | "profanity" => Ok(Self::Profanity), 73 | "illegal" => Ok(Self::Illegal), 74 | "spam" => Ok(Self::Spam), 75 | "impersonation" => Ok(Self::Impersonation), 76 | "other" => Ok(Self::Other), 77 | _ => Err(Error::UnknownReportType), 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip58.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP58: Badges 6 | //! 7 | //! 8 | 9 | use alloc::vec::Vec; 10 | use core::fmt; 11 | 12 | use crate::types::RelayUrl; 13 | use crate::{Event, Kind, PublicKey, Tag, TagStandard}; 14 | 15 | #[derive(Debug, PartialEq, Eq)] 16 | /// Badge Award error 17 | pub enum Error { 18 | /// Invalid length 19 | InvalidLength, 20 | /// Invalid kind 21 | InvalidKind, 22 | /// Identifier tag not found 23 | IdentifierTagNotFound, 24 | /// Mismatched badge definition or award 25 | MismatchedBadgeDefinitionOrAward, 26 | /// Badge awards lack the awarded public key 27 | BadgeAwardsLackAwardedPublicKey, 28 | } 29 | 30 | #[cfg(feature = "std")] 31 | impl std::error::Error for Error {} 32 | 33 | impl fmt::Display for Error { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | match self { 36 | Self::InvalidLength => write!(f, "invalid length"), 37 | Self::InvalidKind => write!(f, "invalid kind"), 38 | Self::IdentifierTagNotFound => write!(f, "identifier tag not found"), 39 | Self::MismatchedBadgeDefinitionOrAward => write!(f, "mismatched badge definition/award"), 40 | Self::BadgeAwardsLackAwardedPublicKey => write!(f, "badge award events lack the awarded public keybadge award events lack the awarded public key"), 41 | } 42 | } 43 | } 44 | 45 | /// Helper function to filter events for a specific [`Kind`] 46 | #[inline] 47 | pub(crate) fn filter_for_kind(events: Vec, kind_needed: &Kind) -> Vec { 48 | events 49 | .into_iter() 50 | .filter(|e| &e.kind == kind_needed) 51 | .collect() 52 | } 53 | 54 | /// Helper function to extract the awarded public key from an array of PubKey tags 55 | pub(crate) fn extract_awarded_public_key<'a>( 56 | tags: &'a [Tag], 57 | awarded_public_key: &PublicKey, 58 | ) -> Option<(&'a PublicKey, &'a Option)> { 59 | tags.iter().find_map(|t| match t.as_standardized() { 60 | Some(TagStandard::PublicKey { 61 | public_key, 62 | relay_url, 63 | .. 64 | }) if public_key == awarded_public_key => Some((public_key, relay_url)), 65 | _ => None, 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /crates/nostr/src/nips/nip62.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NIP-62: Request to Vanish 6 | //! 7 | //! 8 | 9 | use alloc::vec::Vec; 10 | 11 | use crate::RelayUrl; 12 | 13 | /// Request to Vanish target 14 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 15 | pub enum VanishTarget { 16 | /// Request to vanish from all relays 17 | AllRelays, 18 | /// Request to vanish from a specific list of relays. 19 | Relays(Vec), 20 | } 21 | 22 | impl VanishTarget { 23 | /// Vanish from a single relay 24 | #[inline] 25 | pub fn relay(relay: RelayUrl) -> Self { 26 | Self::Relays(vec![relay]) 27 | } 28 | 29 | /// Vanish from multiple relays 30 | #[inline] 31 | pub fn relays(relays: I) -> Self 32 | where 33 | I: IntoIterator, 34 | { 35 | Self::Relays(relays.into_iter().collect()) 36 | } 37 | 38 | /// Vanish from all relays 39 | pub fn all_relays() -> Self { 40 | Self::AllRelays 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/nostr/src/types/image.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Image 6 | 7 | use core::fmt; 8 | use core::num::ParseIntError; 9 | use core::str::{FromStr, Split}; 10 | 11 | #[derive(Debug, PartialEq, Eq)] 12 | /// Image error 13 | pub enum Error { 14 | /// Impossible to parse integer 15 | ParseIntError(ParseIntError), 16 | /// Invalid Image Dimensions 17 | InvalidImageDimensions, 18 | } 19 | 20 | #[cfg(feature = "std")] 21 | impl std::error::Error for Error {} 22 | 23 | impl fmt::Display for Error { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | match self { 26 | Self::ParseIntError(e) => write!(f, "{e}"), 27 | Self::InvalidImageDimensions => write!(f, "Invalid image dimensions"), 28 | } 29 | } 30 | } 31 | 32 | impl From for Error { 33 | fn from(e: ParseIntError) -> Self { 34 | Self::ParseIntError(e) 35 | } 36 | } 37 | 38 | /// Simple struct to hold `width` x `height`. 39 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 40 | pub struct ImageDimensions { 41 | /// Width 42 | pub width: u64, 43 | /// Height 44 | pub height: u64, 45 | } 46 | 47 | impl ImageDimensions { 48 | /// Net image dimensions 49 | #[inline] 50 | pub fn new(width: u64, height: u64) -> Self { 51 | Self { width, height } 52 | } 53 | } 54 | 55 | impl FromStr for ImageDimensions { 56 | type Err = Error; 57 | 58 | fn from_str(s: &str) -> Result { 59 | let mut spitted: Split = s.split('x'); 60 | if let (Some(width), Some(height)) = (spitted.next(), spitted.next()) { 61 | Ok(Self::new(width.parse()?, height.parse()?)) 62 | } else { 63 | Err(Error::InvalidImageDimensions) 64 | } 65 | } 66 | } 67 | 68 | impl fmt::Display for ImageDimensions { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | write!(f, "{}x{}", self.width, self.height) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/nostr/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Types 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | 10 | pub mod image; 11 | pub mod time; 12 | pub mod url; 13 | 14 | pub use self::image::*; 15 | pub use self::time::*; 16 | pub use self::url::*; 17 | -------------------------------------------------------------------------------- /crates/nostr/src/util/hkdf.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! HKDF Util 6 | 7 | use alloc::vec::Vec; 8 | 9 | use hashes::hmac::{Hmac, HmacEngine}; 10 | use hashes::sha256::Hash as Sha256Hash; 11 | use hashes::{Hash, HashEngine}; 12 | 13 | /// HKDF extract 14 | #[inline] 15 | pub fn extract(salt: &[u8], input_key_material: &[u8]) -> Hmac { 16 | let mut engine: HmacEngine = HmacEngine::new(salt); 17 | engine.input(input_key_material); 18 | Hmac::from_engine(engine) 19 | } 20 | 21 | /// HKDF expand 22 | pub fn expand(prk: &[u8], info: &[u8], output_len: usize) -> Vec { 23 | let mut output: Vec = Vec::with_capacity(output_len); 24 | let mut t: Vec = Vec::with_capacity(32); 25 | 26 | let mut i: u8 = 1u8; 27 | while output.len() < output_len { 28 | let mut engine: HmacEngine = HmacEngine::new(prk); 29 | 30 | if !t.is_empty() { 31 | engine.input(&t); 32 | } 33 | 34 | engine.input(info); 35 | engine.input(&[i]); 36 | 37 | t = Hmac::from_engine(engine).to_byte_array().to_vec(); 38 | output.extend_from_slice(&t); 39 | 40 | i += 1; 41 | } 42 | 43 | output.truncate(output_len); 44 | output 45 | } 46 | -------------------------------------------------------------------------------- /crates/nwc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nwc" 3 | version = "0.42.0" 4 | edition = "2021" 5 | description = "Nostr Wallet Connect" 6 | authors.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | readme = "README.md" 11 | rust-version.workspace = true 12 | keywords = ["nostr", "zapper", "nwc"] 13 | 14 | [features] 15 | default = [] 16 | tor = ["nostr-relay-pool/tor"] 17 | 18 | [dependencies] 19 | async-utility.workspace = true 20 | nostr = { workspace = true, features = ["std", "nip47"] } 21 | nostr-relay-pool.workspace = true 22 | tracing = { workspace = true, features = ["std"] } 23 | 24 | [dev-dependencies] 25 | tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } 26 | tracing-subscriber.workspace = true 27 | -------------------------------------------------------------------------------- /crates/nwc/README.md: -------------------------------------------------------------------------------- 1 | # NWC 2 | 3 | NWC client and zapper backend for Nostr apps 4 | 5 | ## State 6 | 7 | **This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. 8 | 9 | ## Donations 10 | 11 | `rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). 12 | 13 | ## License 14 | 15 | This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details -------------------------------------------------------------------------------- /crates/nwc/examples/nwc.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | use std::str::FromStr; 6 | 7 | use nwc::prelude::*; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | tracing_subscriber::fmt::init(); 12 | 13 | let mut nwc_uri_string = String::new(); 14 | 15 | println!("Please enter a NWC string"); 16 | std::io::stdin() 17 | .read_line(&mut nwc_uri_string) 18 | .expect("Failed to read line"); 19 | 20 | // Parse URI and compose NWC client 21 | let uri: NostrWalletConnectURI = 22 | NostrWalletConnectURI::from_str(&nwc_uri_string).expect("Failed to parse NWC URI"); 23 | let nwc: NWC = NWC::new(uri); 24 | 25 | // Get balance 26 | let balance = nwc.get_balance().await?; 27 | println!("Balance: {balance} msat"); 28 | 29 | let request: PayInvoiceRequest = PayInvoiceRequest::new(""); 30 | let response = nwc.pay_invoice(request).await?; 31 | println!("Response: {response:?}"); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /crates/nwc/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NWC error 6 | 7 | use std::fmt; 8 | 9 | use nostr::nips::nip47; 10 | use nostr_relay_pool::pool; 11 | 12 | /// NWC error 13 | #[derive(Debug)] 14 | pub enum Error { 15 | /// NIP47 error 16 | NIP47(nip47::Error), 17 | /// Relay Pool 18 | Pool(pool::Error), 19 | /// Premature exit 20 | PrematureExit, 21 | /// Request timeout 22 | Timeout, 23 | } 24 | 25 | impl std::error::Error for Error {} 26 | 27 | impl fmt::Display for Error { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | Self::NIP47(e) => write!(f, "{e}"), 31 | Self::Pool(e) => write!(f, "{e}"), 32 | Self::PrematureExit => write!(f, "premature exit"), 33 | Self::Timeout => write!(f, "timeout"), 34 | } 35 | } 36 | } 37 | 38 | impl From for Error { 39 | fn from(e: nip47::Error) -> Self { 40 | Self::NIP47(e) 41 | } 42 | } 43 | 44 | impl From for Error { 45 | fn from(e: pool::Error) -> Self { 46 | Self::Pool(e) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/nwc/src/options.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! NWC Options 6 | 7 | use std::time::Duration; 8 | 9 | use nostr_relay_pool::{ConnectionMode, RelayOptions}; 10 | 11 | /// NWC options 12 | #[derive(Debug, Clone)] 13 | pub struct NostrWalletConnectOptions { 14 | pub(super) relay: RelayOptions, 15 | pub(super) timeout: Duration, 16 | } 17 | 18 | impl Default for NostrWalletConnectOptions { 19 | fn default() -> Self { 20 | Self { 21 | relay: RelayOptions::default(), 22 | timeout: Duration::from_secs(60), 23 | } 24 | } 25 | } 26 | 27 | impl NostrWalletConnectOptions { 28 | /// New default NWC options 29 | #[inline] 30 | pub fn new() -> Self { 31 | Self::default() 32 | } 33 | 34 | /// Set connection mode 35 | pub fn connection_mode(self, mode: ConnectionMode) -> Self { 36 | Self { 37 | relay: self.relay.connection_mode(mode), 38 | ..self 39 | } 40 | } 41 | 42 | /// Set NWC requests timeout (default: 10 secs) 43 | #[inline] 44 | pub fn timeout(mut self, timeout: Duration) -> Self { 45 | self.timeout = timeout; 46 | self 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/nwc/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023 Yuki Kishimoto 2 | // Copyright (c) 2023-2025 Rust Nostr Developers 3 | // Distributed under the MIT software license 4 | 5 | //! Prelude 6 | 7 | #![allow(unknown_lints)] 8 | #![allow(ambiguous_glob_reexports)] 9 | #![doc(hidden)] 10 | 11 | pub use nostr::prelude::*; 12 | pub use nostr_relay_pool::prelude::*; 13 | 14 | pub use crate::*; 15 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env just --justfile 2 | 3 | set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] 4 | 5 | default: 6 | @just --list 7 | 8 | # Build nostr CLI (release) 9 | cli: 10 | cargo build -p nostr-cli --release 11 | 12 | # Execute a partial check (MSRV is not checked) 13 | precommit: 14 | @bash contrib/scripts/precommit.sh 15 | 16 | # Execute a full check 17 | check: 18 | @bash contrib/scripts/check.sh 19 | 20 | # Format the entire Rust code 21 | fmt: 22 | @bash contrib/scripts/check-fmt.sh 23 | 24 | # Check if the Rust code is formatted 25 | check-fmt: 26 | @bash contrib/scripts/check-fmt.sh check 27 | 28 | # Check all the crates 29 | check-crates: 30 | @bash contrib/scripts/check-crates.sh 31 | 32 | # Check MSRV of all the crates 33 | check-crates-msrv: 34 | @bash contrib/scripts/check-crates.sh msrv 35 | 36 | # Check Rust docs 37 | check-docs: 38 | @bash contrib/scripts/check-docs.sh 39 | 40 | # Check cargo-deny 41 | check-deny: 42 | @bash contrib/scripts/check-deny.sh 43 | 44 | # Release rust crates 45 | [confirm] 46 | release: 47 | @bash contrib/scripts/release.sh 48 | 49 | # Run benches (unstable) 50 | bench: 51 | RUSTFLAGS='--cfg=bench' cargo +nightly bench 52 | 53 | # Check cargo duplicate dependencies 54 | dup: 55 | cargo tree -d 56 | 57 | # Remove artifacts that cargo has generated 58 | clean: 59 | cargo clean 60 | 61 | # Get many-events.json to test database performance 62 | many-events: 63 | curl https://cdn.jb55.com/s/many-events.json.zst -o many-events.json.zst 64 | zstd -d many-events.json.zst 65 | 66 | # Count the lines of codes of this project 67 | loc: 68 | @echo "--- Counting lines of .rs files (LOC):" && find crates/ -type f -name "*.rs" -not -path "*/target/*" -exec cat {} \; | wc -l 69 | -------------------------------------------------------------------------------- /maintainers.yaml: -------------------------------------------------------------------------------- 1 | maintainers: 2 | - npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet 3 | relays: 4 | - wss://relay.damus.io 5 | - wss://nos.lol 6 | - wss://nostr.mom 7 | - wss://nostr.oxtr.dev 8 | - wss://relay.nostr.bg 9 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.0" 3 | profile = "minimal" 4 | components = ["clippy", "rust-docs", "rustfmt"] 5 | targets = ["wasm32-unknown-unknown"] 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 4 2 | newline_style = "Auto" 3 | reorder_imports = true 4 | reorder_modules = true 5 | reorder_impl_items = false 6 | indent_style = "Block" 7 | normalize_comments = false 8 | imports_granularity = "Module" 9 | group_imports = "StdExternalCrate" --------------------------------------------------------------------------------