├── .gitignore ├── .pubignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── docs ├── .gitignore ├── .nojekyll ├── README.md ├── babel.config.js ├── deploy.ps1 ├── docs │ ├── contributing.md │ ├── installation.md │ ├── intro.md │ ├── must-know.md │ └── usage │ │ ├── _category_.json │ │ ├── keys-management │ │ ├── _category_.json │ │ ├── encode-decode-key-pair-keys-to-npub-nsec-formats.md │ │ ├── generate-key-pair.md │ │ ├── generate-private-key-directly.md │ │ ├── get-a-key-pair-from-private-key.md │ │ ├── get-a-public-key-from-private-key.md │ │ ├── index.md │ │ └── signing-and-verifying-data.md │ │ └── relays-and-events │ │ ├── _category_.json │ │ ├── connecting-to-relays.md │ │ ├── count_event.md │ │ ├── creating-events.md │ │ ├── listening-to-events.md │ │ └── sending-events.md ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.js │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── markdown-page.md └── static │ ├── .nojekyll │ └── img │ ├── favicon.ico │ ├── logo.png │ ├── undraw_docusaurus_mountain.svg │ ├── undraw_docusaurus_react.svg │ └── undraw_docusaurus_tree.svg ├── example ├── auto_reconnect_after_notice_message_from_a_relay.dart ├── cached_nostr_key_pair.dart ├── check_key_validity.dart ├── connectiong_to_relays.dart ├── count_event_example.dart ├── generate_key_pair.dart ├── generate_nevent_of_nostr_event.dart ├── generate_nprofile_from_pubkey.dart ├── get_npub_and_nsec_and_others_bech32_encoded_keys.dart ├── get_pubkey_from_identifier_nip_05.dart ├── get_pubkey_from_identifier_nip_05__2.dart ├── get_user_metadata.dart ├── listening_to_events.dart ├── main.dart ├── receiving_events_from_reopened_subscriptions_with_same_request.dart ├── relay_document_nip_11.dart ├── search_for_events.dart ├── send_delete_event.dart ├── send_event_asynchronously.dart ├── sending_event_to_relays.dart ├── signing_and_verfiying_messages.dart ├── subscribe_asyncronously_to_events_until_eose.dart └── verify_nip05.dart ├── lib ├── dart_nostr.dart └── nostr │ ├── core │ ├── constants.dart │ ├── exceptions.dart │ ├── extensions.dart │ ├── key_pairs.dart │ └── utils.dart │ ├── dart_nostr.dart │ ├── instance │ ├── bech32 │ │ └── bech32.dart │ ├── keys │ │ └── keys.dart │ ├── registry.dart │ ├── relays │ │ ├── base │ │ │ └── relays.dart │ │ └── relays.dart │ ├── streams.dart │ ├── tlv │ │ ├── base │ │ │ └── base.dart │ │ └── tlv_utils.dart │ ├── utils │ │ └── utils.dart │ └── web_sockets.dart │ ├── model │ ├── base.dart │ ├── count.dart │ ├── debug_options.dart │ ├── ease.dart │ ├── event │ │ └── event.dart │ ├── export.dart │ ├── nostr_event_key.dart │ ├── nostr_events_stream.dart │ ├── notice.dart │ ├── ok.dart │ ├── relay.dart │ ├── relay_informations.dart │ ├── request │ │ ├── close.dart │ │ ├── eose.dart │ │ ├── filter.dart │ │ └── request.dart │ └── tlv.dart │ └── service │ └── services.dart ├── pubspec.yaml ├── test └── nostr │ ├── instance │ └── utils │ │ └── utils_test.dart │ └── model │ └── event │ └── nostr_event_test.dart └── todo.extensionThatDoNotExist /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # dotenv environment variables file 15 | .env* 16 | 17 | # Avoid committing generated Javascript files: 18 | *.dart.js 19 | *.info.json # Produced by the --dump-info flag. 20 | *.js # When generated by dart2js. Don't specify *.js if your 21 | # project includes source files written in JavaScript. 22 | *.js_ 23 | *.js.deps 24 | *.js.map 25 | 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | example/ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Bech", 4 | "damus", 5 | "Informations", 6 | "Nevent", 7 | "Nostr", 8 | "nprofile", 9 | "Npub", 10 | "Nsec", 11 | "pubkey", 12 | "pubspec" 13 | ], 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 9.1.1 4 | 5 | - Provide new nip 05 functions that are nullable based on their success/fasilure instead of having ones that throw exception which may cause unwanted behavior if not been awared of and handled. 6 | 7 | ## 9.1.0 8 | 9 | - Fixed errors caused by strict assumption of content event non-nullability. 10 | - Fixed problem caused by possible received new data type from the relays's `OK` event. 11 | 12 | ## 9.0.0 13 | 14 | - Major structure changes for different services of the package. 15 | 16 | ## 8.2.1 17 | 18 | - Remove the Registering of null/empty onEose callback which lead to a bug in the relays service. 19 | 20 | ## 8.1.1 21 | 22 | - exposed a field in the `NostrFilter` class to allow using more filters. 23 | 24 | ## 8.1.2 25 | 26 | - Made all event fields nullable, exposing the toMap() method publicly for NIP cases where an uncompleted events is required (Gift wraps, Remote signing). 27 | - Minor edits 28 | 29 | ## 8.1.0 30 | 31 | - Making use of the `web_socket_channel` for cross platform compatibility. 32 | 33 | ## 8.0.3 34 | 35 | - Support to sending close commands to specific relays, instead of closing all relays at once. 36 | 37 | ## 8.0.2 38 | 39 | - Fixed issue of async method to get events from relays. 40 | 41 | ## 8.0.1 42 | 43 | - Support for #a tag filter. 44 | 45 | ## 8.0.0 46 | 47 | - Support for identification of relays commands by their name/url, in order to be able to customize behavior based on the relay and the action instead of the action only. 48 | - Minor edits and fixes. 49 | 50 | ## 7.0.1 51 | 52 | - Support for NIP 50 search filter with example. 53 | 54 | ## 7.0.0 55 | 56 | - Breaking changes in most package services. 57 | - Implementations for more asyncronous methods. 58 | - New Mniimal Documentation for the package in readme.md file. 59 | - Minor dev edits, fixes and improvements. 60 | 61 | ## 6.1.0 62 | 63 | - Implmenttaion of free resources method for the relays service of an instance that clears and closes all events registeries and streams. 64 | - Implementation of a new asynchronous methods for sending and receiving events, to ensure actions before and after the event is sent or received. 65 | - More Doc comments for members. 66 | - Minor bug fixes. 67 | 68 | ## 6.0.1 69 | 70 | - Fixed the Stack overflow issue in the event model . 71 | 72 | ## 6.0.0 73 | 74 | - Added ability to create standalone instances of the package services, useful if you want to target Flutter web so you can use only one service for routes and not all of them... 75 | - Break changes in events types, in favor of possible collisions when working with replacable events. 76 | 77 | ## 5.0.1 78 | 79 | - Added documentation config to pubspec.yaml 80 | 81 | ## 5.0.0 82 | 83 | - Fully Breaking changes. 84 | - Adidtion of callbacks triggeres for events, notices... 85 | - Adidtion of more features. 86 | 87 | ## 4.0.0 88 | 89 | - Breaking changes 90 | - Exposed more APIs to the package interface. 91 | - Offered more control over the events sending/receiving. 92 | 93 | ## 3.3.1 94 | 95 | - Bug fixes. 96 | - Added more docs 97 | - More optimizations for the use of the keypair class for quickeer constructions after the first time (caching). 98 | 99 | ## 3.0.0 100 | 101 | - Added new utils methods to the utils service. 102 | - Exposed and modifed some implmentation source service class. 103 | - Minor modifications for better maintainence of code. 104 | - Commented out more APIs of the package. 105 | 106 | ## 2.1.1 107 | 108 | - Changes the dart_bip32_bip44 with bip32_bip44 so it works with dart packages and projects and not Flutter ones sonce it breaks pana scoring system. 109 | 110 | ## 2.1.0 111 | 112 | - Added nprofile & tlv services 113 | 114 | ## 2.0.1 115 | 116 | - Minor changes in the docs. 117 | - Added more docs to memebers that miss it. 118 | 119 | ## 2.0.0 120 | 121 | - Exposed new APIs with new documentation for more developer experience use of this package. 122 | - Addition of utils service. 123 | - Addition of more nostr NIPs in the package. 124 | - Added more examples. 125 | 126 | ## 1.5.1 127 | 128 | - Exported the `NostrEventsStream` model class 129 | 130 | ## 1.5.0 131 | 132 | - Added implementation of bech32 encoder in general. 133 | - Added implementation of npub & nsec encoder. 134 | - Added example for generating npub & nsec keys. 135 | - Added more documentation and documenttaion-example for some memebers that miss it in the keys service. 136 | 137 | ## 1.4.0 138 | 139 | - Added the reconnecting option when a relay sent's a notice message. 140 | 141 | ## 1.3.3 142 | 143 | - refactored the optional memebers to requests in the internal library packages. 144 | - ( experiental ) Implementation of a work around over the relays subscrition limits. 145 | 146 | ## 1.3.2 147 | 148 | - Added a main example. 149 | 150 | ## 1.3.0 151 | 152 | - Add more helper methods. 153 | - Minor fixes. 154 | 155 | ## 1.2.0 156 | 157 | - Added example of litening to events. 158 | - Fixing the subscription id that turns null when not se 159 | 160 | ## 1.1.0 161 | 162 | - Fixed signing and verifying hexadiciaml encoding issue. 163 | - added more example in example/ folder. 164 | 165 | ## 1.0.6 166 | 167 | - Added more helper methods with docs and examples. 168 | 169 | ## 1.0.5 170 | 171 | - Added more docs with examples to more methods. 172 | 173 | ## 1.0.4 174 | 175 | - Highlighted support for more nips in the docs. 176 | 177 | ## 1.0.3 178 | 179 | - Added support for more nips. 180 | - Exposed them in the docs. 181 | 182 | ## 1.0.2 183 | 184 | - Added implementation of nip 11 and its docs 185 | 186 | ## 1.0.1 187 | 188 | - Added docs for nip-05 verification. 189 | 190 | ## 1.0.0 191 | 192 | - Implementation of nip 05 for internet identity verification. 193 | - Adding more docs and examples. 194 | 195 | ## 1.0.2-dev 196 | 197 | - Added more functionalities and parameters to the `relays` service. 198 | 199 | ## 1.0.1-dev 200 | 201 | - organized the main package to services (keys, relays). 202 | - exposed more helper methods. 203 | - added and edited docs 204 | 205 | ## 1.0.0-dev 206 | 207 | - Initial under-development version. 208 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Anas Fikhi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dart Nostr 2 | 3 | This is a Dart/Flutter toolkit for developing [Nostr](https://nostr.com/) client apps faster and easier. 4 | 5 | ## Table of Contents 6 | 7 | - [Supported NIPs](#supported-nips) 8 | - [Installation](#installation) 9 | - [Usage](#usage) 10 | - [Singleton instance vs. multiple instances](#singleton-instance-vs-multiple-instances) 11 | - [Keys](#keys) 12 | - [Generate private and public keys](#generate-private-and-public-keys) 13 | - [Generate a key pair from a private key](#generate-a-key-pair-from-a-private-key) 14 | - [Sign & verify with a private key](#sign--verify-with-a-private-key) 15 | - [More functionalities](#more-functionalities) 16 | - [Events & Relays](#events--relays) 17 | - [Create an event](#create-an-event) 18 | - [Connect to relays](#connect-to-relays) 19 | - [Listen to events](#listen-to-events) 20 | - [As a stream](#as-a-stream) 21 | - [As a future (resolves on EOSE)](#as-a-future-resolves-on-eose) 22 | - [Reconnect & disconnect](#reconnect--disconnect) 23 | - [Send an event](#send-an-event) 24 | - [Send NIP45 COUNT](#send-nip45-count) 25 | - [Relay Metadata NIP11](#relay-metadata-nip11) 26 | - [More functionalities](#more-functionalities-1) 27 | - [More utils](#more-utils) 28 | 29 | ## Supported NIPs 30 | 31 | if you are working on a Nostr client, app... you will be able to apply and use all the following NIPs (Updated 2024-01-18): 32 | 33 | - [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) 34 | - [NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md) 35 | - [NIP-03](https://github.com/nostr-protocol/nips/blob/master/03.md) 36 | - [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) 37 | - [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) 38 | - [NIP-06](https://github.com/nostr-protocol/nips/blob/master/06.md) 39 | - [NIP-08](https://github.com/nostr-protocol/nips/blob/master/08.md) 40 | - [NIP-09](https://github.com/nostr-protocol/nips/blob/master/09.md) 41 | - [NIP-10](https://github.com/nostr-protocol/nips/blob/master/10.md) 42 | - [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md) 43 | - [NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md) 44 | - [NIP-14](https://github.com/nostr-protocol/nips/blob/master/14.md) 45 | - [NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md) 46 | - [NIP-18](https://github.com/nostr-protocol/nips/blob/master/18.md) 47 | - [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) 48 | - [NIP-21](https://github.com/nostr-protocol/nips/blob/master/21.md) 49 | - [NIP-23](https://github.com/nostr-protocol/nips/blob/master/23.md) 50 | - [NIP-24](https://github.com/nostr-protocol/nips/blob/master/24.md) 51 | - [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) 52 | - [NIP-27](https://github.com/nostr-protocol/nips/blob/master/27.md) 53 | - [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md) 54 | - [NIP-30](https://github.com/nostr-protocol/nips/blob/master/30.md) 55 | - [NIP-31](https://github.com/nostr-protocol/nips/blob/master/31.md) 56 | - [NIP-32](https://github.com/nostr-protocol/nips/blob/master/32.md) 57 | - [NIP-36](https://github.com/nostr-protocol/nips/blob/master/36.md) 58 | - [NIP-38](https://github.com/nostr-protocol/nips/blob/master/38.md) 59 | - [NIP-39](https://github.com/nostr-protocol/nips/blob/master/39.md) 60 | - [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md) 61 | - NIP-42 (not yet implemented) 62 | - NIP-44 (not yet implemented) 63 | - [NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md) 64 | - [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) 65 | - [NIP-48](https://github.com/nostr-protocol/nips/blob/master/48.md) 66 | - [NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md) 67 | - [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md) 68 | - [NIP-52](https://github.com/nostr-protocol/nips/blob/master/52.md) 69 | - [NIP-53](https://github.com/nostr-protocol/nips/blob/master/53.md) 70 | - [NIP-56](https://github.com/nostr-protocol/nips/blob/master/56.md) 71 | - [NIP-57: Lightning Zaps](57.md) 72 | - [NIP-58](https://github.com/nostr-protocol/nips/blob/master/58.md) 73 | 74 | - [NIP-72](https://github.com/nostr-protocol/nips/blob/master/72.md) 75 | - [NIP-75](https://github.com/nostr-protocol/nips/blob/master/75.md) 76 | - [NIP-78](https://github.com/nostr-protocol/nips/blob/master/78.md) 77 | - [NIP-84](https://github.com/nostr-protocol/nips/blob/master/84.md) 78 | - [NIP-89](https://github.com/nostr-protocol/nips/blob/master/89.md) 79 | - [NIP-94](https://github.com/nostr-protocol/nips/blob/master/94.md) 80 | - [NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md) 81 | - [NIP-99](https://github.com/nostr-protocol/nips/blob/master/99.md) 82 | 83 | NIPs marked as "not yet implemented" are not supported yet. 84 | 85 | Some existant NIPs are platform specific or can't just be supported directly like [NIP 07](https://github.com/nostr-protocol/nips/blob/master/07.md) which is only web-specific, or [NIP 90](https://github.com/nostr-protocol/nips/blob/master/90.md) which is related to Data Vending machines. 86 | 87 | ## Installation 88 | 89 | Install the package by adding the following to your `pubspec.yaml` file: 90 | 91 | ```yaml 92 | dependencies: 93 | dart_nostr: any 94 | ``` 95 | 96 | Otherwise you can install it from the command line: 97 | 98 | ```bash 99 | # Flutter project 100 | flutter pub add dart_nostr 101 | 102 | # Dart project 103 | dart pub add dart_nostr 104 | ``` 105 | 106 | ## Usage 107 | 108 | ### Singleton instance vs. multiple instances 109 | 110 | The base and only class you need to remember is `Nostr`, all methods and utilities are available through it. 111 | 112 | The `Nostr` class is accessible through two ways, a singleton instance which you can access by calling `Nostr.instance` and a constructor which you can use to create multiple instances of `Nostr`. 113 | 114 | Each instance (including the singleton instance) is independent from each other, so everything you do with one instance will be accessible only through that instance including relays, events, caches, callbacks, etc. 115 | 116 | Use the singleton instance if you want to use the same instance across your app, so as example once you do connect to a set of relays, you can access and use them (send and receive events) from anywhere in your app. 117 | 118 | Use the constructor if you want to create multiple instances of `Nostr`, as example if you want to connect to different relays in different parts of your app, or if you have extensive Nostr relays usage (requests) and you want to separate them into different instances so you avoid relays limits. 119 | 120 | ```dart 121 | /// Singleton instance 122 | final instance = Nostr.instance; 123 | 124 | /// Constructor 125 | final instance = Nostr(); 126 | ``` 127 | 128 | ### Keys 129 | 130 | #### Generate private and public keys 131 | 132 | ```dart 133 | final newKeyPair = instance.keysService.generateKeyPair(); 134 | 135 | print(newKeyPair.public); // Public key 136 | print(newKeyPair.private); // Private key 137 | ``` 138 | 139 | #### Generate a key pair from a private key 140 | 141 | ```dart 142 | final somePrivateKey = "HERE IS MY PRIVATE KEY"; 143 | 144 | final newKeyPair = instance.keysService 145 | .generateKeyPairFromExistingPrivateKey(somePrivateKey); 146 | 147 | print(somePrivateKey == newKeyPair.private); // true 148 | print(newKeyPair.public); // Public key 149 | ``` 150 | 151 | #### Sign & verify with a private key 152 | 153 | ```dart 154 | 155 | /// sign a message with a private key 156 | final signature = instance.keysService.sign( 157 | privateKey: keyPair.private, 158 | message: "hello world", 159 | ); 160 | 161 | print(signature); 162 | 163 | /// verify a message with a public key 164 | final verified = instance.keysService.verify( 165 | publicKey: keyPair.public, 166 | message: "hello world", 167 | signature: signature, 168 | ); 169 | 170 | print(verified); // true 171 | ``` 172 | 173 | Note: `dart_nostr` provides even more easier way to create, sign and verify Nostr events, see the relays and events sections below. 174 | 175 | #### More functionalities 176 | 177 | The package exposes more useful methods, like: 178 | 179 | ```dart 180 | // work with nsec keys 181 | instance.keysService.encodePrivateKeyToNsec(privateKey); 182 | instance.keysService.decodeNsecKeyToPrivateKey(privateKey); 183 | 184 | // work with npub keys 185 | instance.keysService.encodePublicKeyToNpub(privateKey); 186 | instance.keysService.decodeNpubKeyToPublicKey(privateKey); 187 | 188 | // more keys derivations and validations methods 189 | instance.keysService.derivePublicKey(privateKey); 190 | instance.keysService.generatePrivateKey(privateKey); 191 | instance.keysService.isValidPrivateKey(privateKey); 192 | 193 | // general utilities that related to keys 194 | instance.utilsService.decodeBech32(bech32String); 195 | instance.utilsService.encodeBech32(bech32String); 196 | instance.utilsService.pubKeyFromIdentifierNip05(bech32String); 197 | ``` 198 | 199 | ### Events & Relays 200 | 201 | #### Create an event 202 | 203 | Quickest way to create an event is by using the `NostrEvent.fromPartialData` constructor, it does all the heavy lifting for you, like signing the event with the provided private key, generating the event id, etc. 204 | 205 | ```dart 206 | final event = NostrEvent.fromPartialData( 207 | kind: 1, 208 | content: 'example content', 209 | keyPairs: keyPair, // will be used to sign the event 210 | tags: [ 211 | ['t', currentDateInMsAsString], 212 | ], 213 | ); 214 | 215 | print(event.id); // event id 216 | print(event.sig); // event signature 217 | 218 | print(event.serialized()); // event as serialized JSON 219 | ``` 220 | 221 | Note: you can also create an event from scratch with the `NostrEvent` constructor, but you will need to do the heavy lifting yourself, like signing the event, generating the event id, etc. 222 | 223 | #### Connect to relays 224 | for a single `Nostr` instance, you can connect and reconnect to multiple relays once or multiple times, so you will be able to send and receive events later. 225 | 226 | ```dart 227 | try { 228 | 229 | final relays = ['wss://relay.damus.io']; 230 | 231 | await instance.relaysService.init( 232 | relaysUrl: relays, 233 | ); 234 | 235 | print("connected successfully") 236 | } catch (e) { 237 | print(e); 238 | } 239 | ``` 240 | 241 | if anything goes wrong, you will get an exception with the error message. 242 | Note: the `init` method is highly configurable, so you can control the behavior of the connection, like the number of retries, the timeout, wether to throw an exception or not, register callbacks for connections or events... 243 | 244 | #### Listen to events 245 | 246 | ##### As a stream 247 | 248 | ```dart 249 | ```dart 250 | // Creating a request to be sent to the relays. (as example this request will get all events with kind 1 of the provided public key) 251 | final request = NostrRequest( 252 | filters: [ 253 | NostrFilter( 254 | kinds: const [1], 255 | authors: [keyPair.public], 256 | ), 257 | ], 258 | ); 259 | 260 | 261 | // Starting the subscription and listening to events 262 | final nostrStream = Nostr.instance.services.relays.startEventsSubscription( 263 | request: request, 264 | onEose: (ease) => print(ease), 265 | ); 266 | 267 | print(nostrStream.subscriptionId); // The subscription id 268 | 269 | // Listening to events 270 | nostrStream.stream.listen((NostrEvent event) { 271 | print(event.content); 272 | }); 273 | 274 | // close the subscription later 275 | nostrStream.close(); 276 | ``` 277 | 278 | ##### As a future (resolves on EOSE) 279 | 280 | ```dart 281 | // Creating a request to be sent to the relays. (as example this request will get all events with kind 1 of the provided public key) 282 | final request = NostrRequest( 283 | filters: [ 284 | NostrFilter( 285 | kinds: const [1], 286 | authors: [keyPair.public], 287 | ), 288 | ], 289 | ); 290 | 291 | // Call the async method and wait for the result 292 | final events = 293 | await Nostr.instance.services.relays.startEventsSubscriptionAsync( 294 | request: request, 295 | ); 296 | 297 | // print the events 298 | print(events.map((e) => e.content)); 299 | ``` 300 | 301 | Note: `startEventsSubscriptionAsync` will be resolve with an `List` as soon as a relay sends an EOSE command. 302 | 303 | #### Reconnect & disconnect 304 | 305 | ```dart 306 | // reconnect 307 | await Nostr.instance.services.relays.reconnectToRelays( 308 | onRelayListening: onRelayListening, 309 | onRelayConnectionError: onRelayConnectionError, 310 | onRelayConnectionDone: onRelayConnectionDone, 311 | retryOnError: retryOnError, 312 | retryOnClose: retryOnClose, 313 | shouldReconnectToRelayOnNotice: shouldReconnectToRelayOnNotice, 314 | connectionTimeout: connectionTimeout, 315 | ignoreConnectionException: ignoreConnectionException, 316 | lazyListeningToRelays: lazyListeningToRelays, 317 | ); 318 | 319 | // disconnect 320 | await Nostr.instance.services.relays.disconnectFromRelays(); 321 | ``` 322 | 323 | #### Send an event 324 | 325 | ```dart 326 | // sending synchronously 327 | Nostr.instance.services.relays.sendEventToRelays( 328 | event, 329 | onOk: (ok) => print(ok), 330 | ); 331 | 332 | // sending synchronously with a custom timeout 333 | final okCommand = await Nostr.instance.services.relays.sendEventToRelaysAsync( 334 | event, 335 | timeout: const Duration(seconds: 3), 336 | ); 337 | 338 | print(okCommand); 339 | ``` 340 | 341 | Note: `sendEventToRelaysAsync` will be resolve with an `OkCommand` as soon as one relay accepts the event. 342 | 343 | #### Send NIP45 COUNT 344 | 345 | ```dart 346 | // create a count event 347 | final countEvent = NostrCountEvent.fromPartialData( 348 | eventsFilter: NostrFilter( 349 | kinds: const [0], 350 | authors: [keyPair.public], 351 | ), 352 | ); 353 | 354 | // Send the count event synchronously 355 | Nostr.instance.services.relays.sendCountEventToRelays( 356 | countEvent, 357 | onCountResponse: (countRes) { 358 | print('count: $countRes'); 359 | }, 360 | ); 361 | 362 | // Send the count event asynchronously 363 | final countRes = await Nostr.instance.services.relays.sendCountEventToRelaysAsync( 364 | countEvent, 365 | timeout: const Duration(seconds: 3), 366 | ); 367 | 368 | print("found ${countRes.count} events"); 369 | ``` 370 | 371 | #### Relay Metadata NIP11 372 | 373 | ```dart 374 | final relayDoc = await Nostr.instance.services.relays.relayInformationsDocumentNip11( 375 | relayUrl: "wss://relay.damus.io", 376 | ); 377 | 378 | print(relayDoc?.name); 379 | print(relayDoc?.description); 380 | print(relayDoc?.contact); 381 | print(relayDoc?.pubkey); 382 | print(relayDoc?.software); 383 | print(relayDoc?.supportedNips); 384 | print(relayDoc?.version); 385 | ``` 386 | 387 | #### More functionalities 388 | 389 | The package exposes more useful methods, like: 390 | 391 | ```dart 392 | // work with nevent and nevent 393 | final nevent = Nostr.instance.utilsService.encodeNevent( 394 | eventId: event.id, 395 | pubkey: pubkey, 396 | userRelays: [], 397 | ); 398 | 399 | print(nevent); 400 | 401 | final map = Nostr.instance.utilsService.decodeNeventToMap(nevent); 402 | print(map); 403 | 404 | 405 | // work with nprofile 406 | final nprofile = Nostr.instance.utilsService.encodeNProfile( 407 | pubkey: pubkey, 408 | userRelays: [], 409 | ); 410 | 411 | print(nprofile); 412 | 413 | final map = Nostr.instance.utilsService.decodeNprofileToMap(nprofile); 414 | print(map); 415 | 416 | ``` 417 | 418 | ### More utils 419 | 420 | #### Generate random 64 hex 421 | 422 | ```dart 423 | final random = Nostr.instance.utilsService.random64HexChars(); 424 | final randomButBasedOnInput = Nostr.instance.utilsService.consistent64HexChars("input"); 425 | 426 | print(random); 427 | print(randomButBasedOnInput); 428 | ``` 429 | 430 | #### NIP05 related 431 | 432 | ```dart 433 | /// verify a nip05 identifier 434 | final verified = await Nostr.instance.utilsService.verifyNip05( 435 | internetIdentifier: "something@domain.com", 436 | pubKey: pubKey, 437 | ); 438 | 439 | print(verified); // true 440 | 441 | 442 | /// Validate a nip05 identifier format 443 | final isValid = Nostr.instance.utilsService.isValidNip05Identifier("work@gwhyyy.com"); 444 | print(isValid); // true 445 | 446 | /// Get the pubKey from a nip05 identifier 447 | final pubKey = await Nostr.instance.utilsService.pubKeyFromIdentifierNip05( 448 | internetIdentifier: "something@somain.c", 449 | ); 450 | 451 | print(pubKey); 452 | ``` 453 | 454 | ### NIP13 hex difficulty 455 | 456 | ```dart 457 | Nostr.instance.utilsService.countDifficultyOfHex("002f"); 458 | ``` 459 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:very_good_analysis/analysis_options.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasfik/nostr/61e26a82da0bfdc8ee44cce570f93195fc0a6e40/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/deploy.ps1: -------------------------------------------------------------------------------- 1 | $currentPath = (Get-Location).path 2 | $onlyCurrentFolderName = $currentPath.Split("\")[-1] 3 | 4 | function Deploy() { 5 | cmd /C 'set "GIT_USER=anasfik" && npm run deploy' 6 | } 7 | 8 | if ($onlyCurrentFolderName -ne "docs") { 9 | if($onlyCurrentFolderName -eq "nostr") { 10 | 11 | Set-Location docs 12 | Deploy 13 | 14 | } else { 15 | echo "You are not in the right folder" 16 | } 17 | } else { 18 | Deploy 19 | } 20 | 21 | -------------------------------------------------------------------------------- /docs/docs/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Contributing 6 | 7 | If you want to contribute to this project, you can do so by forking its [Github repository](https://github.com/anasfik/nostr), making your changes, and then creating a pull request, I will be grateful for the help, and will be happy to merge your changes into the main project. 8 | 9 | -------------------------------------------------------------------------------- /docs/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | description: Seamlessly integrate dart_nostr into your project with simple terminal commands. Manage dependencies, streamline assets, and elevate your project's capabilities in minutes. 4 | --- 5 | 6 | # Installation 7 | 8 | Wether you already have a Flutter/Dart project, or you will create a new one. You will definitly have a `pubspec.yaml` file, which manage your project dependencies, assets, SDKs.. 9 | 10 | 1. Open your terminal at the path of your project 11 | 12 | 2. Run those command: 13 | 14 | ```dart 15 | flutter pub add dart_nostr 16 | flutter pub get 17 | ``` 18 | 19 | if you are not on a Flutter project, run this instead: 20 | 21 | ```dart 22 | dart pub add dart_nostr 23 | dart pub get 24 | ``` 25 | 26 | 3. This will import the package to your project, check your `pubspec.yaml` file, you should find it under the `dependencies` section. 27 | 28 | 4. After you did installed the package, you can use it by importing it in your `.dart` files. 29 | 30 | ```dart 31 | import 'package:dart_nostr/dart_nostr.dart'; 32 | 33 | // ... 34 | ``` 35 | 36 | Cool, you can pass to next sections in this documentation. 37 | 38 | :::tip 39 | You can install the package from its [remote repository](https://github.com/anasfik/nostr) directly, follow [these instructions](https://dart.dev/tools/pub/dependencies#git-packages) for more detail. 40 | ::: 41 | 42 | ## Whats Next ? 43 | 44 | Now that you have it installed in your project, you can check the next section to learn some stuff that you need to know before starting to use it. 45 | 46 | - [General Overview](./must-know) 47 | -------------------------------------------------------------------------------- /docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: / 4 | description: Jumpstart your journey with the dart_nostr package documentation, your go-to resource for mastering this Dart/Flutter SDK. Created by Mohamed Anas, this package simplifies intricate tasks related to the Nostr protocol. Enjoy developer-friendly APIs, save time, and explore the decentralized world of Nostr. Dive in today! 5 | --- 6 | 7 | 8 | # Get Started 9 | 10 | Hello, welcome to the `dart_nostr` package documentation, it will serve you as a reference to learn & use this package more properly. 11 | 12 | I'am Mohamed Anas, and I'am a 20 yo developer, I made this package because I think it will help people in the future. 13 | 14 | Let's start. 15 | 16 | ## General Overview 17 | 18 | ### What is this ? 19 | 20 | `dart_nostr` is a Dart/Flutter SDK for the [Nostr](https://nostr.com/) protocol, the package is meant as an implementation of the Nostr [documentations & specs](https://github.com/nostr-protocol/nips/blob/master/01.md). 21 | 22 | This packages abstracts the complexity that might face when trying to connect to relays web sockets, send/receive events, listen to events callbacks & much more. And provides a solutions to the problems and difficulties that you will face in the future if you tried implementing things yourself (How I knew That? because I am also a developer who worked with it). 23 | 24 | This package exposes Dart APIs to perform easy-to-use functionality for your future apps, this means for you: 25 | 26 | - More development time saved & gained. 27 | - Easy-to-use methods/functionality. 28 | - Less problems that you will face in the future. 29 | - Developer friendly. 30 | - As a Dart/Flutter developer, you will feel at home. 31 | - Gain the ability to easily extens it's functionality to meet your very special use-case. 32 | 33 | ### Wait, What is Even Nostr ? 34 | 35 | [Nostr](https://nostr.com) is an open protocol that enables global, decentralized, and censorship-resistant social media. 36 | 37 | Well, I could throw an global overview about it here for the next 1200 words. but instead, I recommend you to check first the available official resources of it, these are some of what you need to check: 38 | 39 | - [Nostr Website](https://nostr.com/) 40 | - [Nostr Github Repository](https://github.com/nostr-protocol/nostr) 41 | - [Nostr NIPs Github Repository](https://github.com/nostr-protocol/nips) 42 | 43 | This package allows you to apply almost all Nostr [NIPs](https://github.com/nostr-protocol/nips) that exists for the Nostr protocol, you can create events with your own customization for kinds, tags... and send them to relays, or you can listen & use them. 44 | 45 | ### Why this Documentation ? 46 | 47 | I am writing this documentation website since I saw that self-learning the package from its code and the generated [dart doc](https://dart.dev/tools/dart-doc) might be overwhelming and non-sense. especially if you're new. 48 | 49 | Here, I will treat you like myself seeing this package for the first time and starting with Nostr, you will find all the guides & informations you need from the sidebar on the left. 50 | 51 | ## Issues & Bugs 52 | 53 | If you find any issues or bugs, please report them [here](https://github.com/anasfik/nostr/issues), I will be happy to have a look at them and fix them as soon as possible. 54 | 55 | ## Contributing 56 | 57 | If you want to contribute to this project, you can do so by forking this repository, making your changes, and then creating a pull request, I will be grateful for the help, and will be happy to merge your changes into the main project. 58 | 59 | ## License 60 | 61 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/anasfik/nostr/blob/main/LICENSE) file for details. 62 | 63 |
64 | 65 | ## Whats Next ? 66 | 67 | Check the next section to learn how to install this package in your project. 68 | 69 | - [Installation](./installation) 70 | -------------------------------------------------------------------------------- /docs/docs/must-know.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | description: Master the Nostr protocol with dart_nostr. Effortlessly handle events and operations, except for specific exceptions. Navigate the code structure and access robust functionalities, all guided by a single maintainer. 4 | --- 5 | 6 | # Must Know ? 7 | 8 | This page will help you to get started with `dart_nostr` package, and ensuring that you have the basic knowledge to start using the package. 9 | 10 | ## General knowledge 11 | 12 | `dart_nostr` package and this documentation assumes that you have a basic knowledge about the Nostr protocol, and how it works, if you don't know anything about the Nostr protocol, please read the [Nostr protocol documentation](https://nostr.org/docs/protocol) first, check the [Nostr NIPs](https://github.com/nostr-protocol/nips/) and get as much knowledge as you can about the Nostr protocol, then come back here and continue reading. 13 | 14 | The goal of `dart_nostr` is to achieve almost anything that relate to the Nostr protocol (or at least the hard part), it will provide you with minimal APIs to start building your Nostr application, and so you can focus on your application logic, and not on the Nostr protocol itself. 15 | 16 | ## The package structure 17 | 18 | `dart_nostr` exposes all it's functionality via a `Nostr` class instance, which holds all the future connections, events cache, keys, relays, etc. and so creating a new instance each time will create a whole separated with its own resources. 19 | 20 | However, the packages offers a singleton instance of the `Nostr` class, which you can access it by calling the `Nostr.instance` getter, this will return the singleton instance of the `Nostr` class, which you can use it in your whole application, this is the recommended way to use the package (At least for dart:io platforms, and for medium sized apps), as it will allow you to access the same instance of the `Nostr` class in the whole application, and so you can access the same resources in the whole application, such as the same keys, events, relays, etc: 21 | 22 | ```dart 23 | /// Different instances 24 | final newInstanceOne = Nostr(); 25 | final newInstanceTwo = Nostr(); 26 | 27 | print(newInstanceOne == newInstanceTwo); // false, as they are two different instances 28 | 29 | // Singleton instance 30 | final instance = Nostr.instance; 31 | ``` 32 | 33 | A `Nostr` instance allow developers to access package members via services, which are: 34 | 35 | ```dart 36 | final keysService = instance.keysService; // access to the keys service, which will provide methods to handle user key pairs, private keys, public keys, etc. 37 | 38 | final relaysService = instance.relaysService; // access to the relays service, which will provide methods to interact with your own relays such as sending events, listening to events, etc. 39 | 40 | final utilsService = instance.utilsService; // access the utils service, which provides many handy utils that you will need to use in your app, such as encoding, getting random hex strings to use with requests, etc. 41 | ``` 42 | 43 | Each service has its own methods that you can call and use, for example, the `keysService` has the `generateKeyPair()` method, which will generate a new key pair for your users, and so on. 44 | 45 | The singleton approach allows using the package in the whole application with the same singleton without worrying about relating things around (relays connection with Nostr requests and received events...), however, if you want to use the package in a large application, you may want to create a new instance of the `Nostr` class, and use it in a specific part of your application, this will allow you to separate things around, and so you can use the package in different parts of your application without worrying about relating things around. 46 | 47 | ```dart 48 | // Connect to relays in a specific part of your application. 49 | await instance.relaysService.init( 50 | relayUrls: [ 51 | 'wss://relay1.nostr.org', 52 | 'wss://relay2.nostr.org', 53 | ], 54 | ); 55 | 56 | // Send an event in another part of your application. 57 | await instance.relaysService.sendEventToRelays(/* ... */); 58 | ``` 59 | 60 | :::note 61 | You don't need to worry about the code above, it's just an example to show you how you can use the package in different parts of your application. 62 | ::: 63 | 64 | ## The supported NIPs 65 | 66 | This package allows you to use the Nostr protocol with the following NIPs: 67 | -------------------------------------------------------------------------------- /docs/docs/usage/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Usage", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/usage/keys-management/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Keys Management", 3 | "position": 1 4 | } -------------------------------------------------------------------------------- /docs/docs/usage/keys-management/encode-decode-key-pair-keys-to-npub-nsec-formats.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | description: Learn how to encode & decode a key pair keys to the npub & nsec formats. 4 | --- 5 | 6 | # Create npub & nsec keys 7 | 8 | Nostr is about hex keys, these are what you will use to sign & verify data, or in other, you will create direct events with them. However, Nostr [NIP 19](https://github.com/nostr-protocol/nips/blob/master/19.md) exposes bech32-encoded entities, Please check it first to understand what is npub & nsec keys. 9 | 10 | Let's say we have this key pair: 11 | 12 | ```dart 13 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 14 | 15 | print(keyPair.public); // ... 16 | print(keyPair.private); // ... 17 | ``` 18 | 19 | ## Npub keys (public keys) 20 | 21 | Let's learn how to encode & decode npub keys, in the following sections. 22 | 23 | ### Encode a public key to npub format 24 | 25 | Let's say we want to create the convenable npub key for the public key above, we can use the `encodePublicKeyToNpub()` method of the `Nostr.instance.services.keys`, Example: 26 | 27 | ```dart 28 | final npubKey = Nostr.instance.services.keys.encodePublicKeyToNpub(keyPair.public); 29 | 30 | print(npubKey); // npub... 31 | ``` 32 | 33 | we can now use it, maybe show it to users... 34 | 35 | ### Decodes a npub key to a public key 36 | 37 | Now let's say we want to turn back our npub to our public key, we can use the `decodeNpubKeyToPublicKey()` method of the `Nostr.instance.services.keys`, Example: 38 | 39 | ```dart 40 | final decodedPublicKey = Nostr.instance.services.keys.decodeNpubKeyToPublicKey(npubKey); 41 | 42 | print(decodedPublicKey); // ... 43 | 44 | print(decodedPublicKey == keyPair.public); // true 45 | ``` 46 | 47 | See, we got our public key back. 48 | 49 | ## Nsec keys (private keys) 50 | 51 | Let's learn how to encode & decode nsec keys, in the following sections. 52 | 53 | ### Encode a private key to nsec format 54 | 55 | Let's say we want to create the convenable nsec key for the private key above, we can use the `encodePrivateKeyToNsec()` method of the `Nostr.instance.services.keys`, Example: 56 | 57 | ```dart 58 | 59 | final nsecKey = Nostr.instance.services.keys.encodePrivateKeyToNsec(keyPair.private); 60 | 61 | print(nsecKey); // nsec... 62 | ``` 63 | 64 | We can now use it, maybe show it in the key's owner profile... 65 | 66 | ### Decodes a nsec key to a private key 67 | 68 | Now let's say we want to turn back our nsec to our private key, we can use the `decodeNsecKeyToPrivateKey()` method of the `Nostr.instance.services.keys`, Example: 69 | 70 | ```dart 71 | final decodedPrivateKey = Nostr.instance.services.keys.decodeNsecKeyToPrivateKey(nsecKey); 72 | 73 | print(decodedPrivateKey); // ... 74 | 75 | print(decodedPrivateKey == keyPair.private); // true 76 | ``` 77 | 78 | See, in the same way we got our private key back. 79 | 80 | ## What's next ? 81 | 82 | Learn how to [sign & verify data](./signing-and-verifying-data). 83 | -------------------------------------------------------------------------------- /docs/docs/usage/keys-management/generate-key-pair.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | description: Learn how to generate a new key pair of a private and a public keys for a new user. 4 | --- 5 | 6 | # Generate A New Key Pair 7 | 8 | In order to generate a new key pair of a private and a public keys for a new user as example, you can do it by calling the `generateKeyPair()` method of the `Nostr.instance.services.keys`, This method will return a `NostrKeyPairs` that represents the key pair, like this: 9 | 10 | ```dart 11 | // Generate a new key pair. 12 | NostrKeyPairs keyPair = Nostr.instance.services.keys.generateKeyPair(); 13 | 14 | // Now you can use it as you want. 15 | print(keyPair.private); // ... 16 | print(keyPair.public); // ... 17 | 18 | // A example, creating new events for that user associated with this key pair. 19 | ``` 20 | 21 | You can access and use the private and public keys now in the Nostr operations, such using the public key in events... 22 | 23 | ## What's Next 24 | 25 | Learn how you can create only a private key for a new user, [click here](./generate-private-key-directly), and create its key pair later. 26 | -------------------------------------------------------------------------------- /docs/docs/usage/keys-management/generate-private-key-directly.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | description: Learn how to generate a new private key directly, and use it later to generate a key pair. 4 | --- 5 | 6 | 7 | # Generate A Private Key Directly 8 | 9 | Well, since you know that you can get a whole [key pair](./generate-key-pair) to use with only it's private key, you may want to only generate it. Then creating a key pair from it later, this will be useful if you want to store the private key somewhere and use it later to generate a key pair and so avoiding the instantly derivation of the key pair from the private key. 10 | 11 | For this, you can use the `generatePrivateKey()` method of the `Nostr.instance.services.keys`, this method returns a `String` that represents the private key, Example: 12 | 13 | ```dart 14 | // Generate a new private key. 15 | String privateKey = Nostr.instance.services.keys.generatePrivateKey(); 16 | 17 | // Now you can use it as you want. 18 | print(privateKey); // ... 19 | 20 | // ... 21 | 22 | // later, after one hour as example, you can generate a keypair from it. 23 | NostrKeyPairs keyPair = Nostr.instance.services.keys.generateKeyPairFromExistingPrivateKey(privateKey); 24 | 25 | // Now you can use it as you want. 26 | print(keyPair.private); // ... 27 | print(keyPair.public); // ... 28 | 29 | print(keyPair.private == privateKey) // true 30 | ``` 31 | 32 | ## What's Next 33 | 34 | Learn how you can create a key pair from a key pair, [click here](./get-a-key-pair-from-private-key). 35 | -------------------------------------------------------------------------------- /docs/docs/usage/keys-management/get-a-key-pair-from-private-key.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | description: Learn how to get a key pair from an existent private key. 4 | --- 5 | 6 | 7 | # Get a key pair from an existent private key 8 | 9 | If you have only a [private key](./generate-private-key-directly), and you want to get its associated key pair, you can use the `generateKeyPairFromExistingPrivateKey()` method of the `Nostr.instance.services.keys`, This method will return a `NostrKeyPairs` object that represents that key pair, Example: 10 | 11 | ```dart 12 | // The private key, maybe the one you generated separately. 13 | final privateKey = "THE_PRIVATE_KEY_HEX_STRING"; 14 | 15 | // Generate a new key pair from the private key. 16 | NostrKeyPairs keyPair = Nostr.instance.services.keys.generateKeyPairFromExistingPrivateKey(privateKey); 17 | 18 | // Now you can use it as you want. 19 | print(keyPair.private); // ... 20 | print(keyPair.public); // ... 21 | 22 | print(keyPair.private == privateKey) // true 23 | ``` 24 | 25 | ## What's Next ? 26 | 27 | Learn how you can generate a public key directly from a private one, click [here](./get-a-public-key-from-private-key). 28 | -------------------------------------------------------------------------------- /docs/docs/usage/keys-management/get-a-public-key-from-private-key.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | description: Learn how to get a public key from a private key. 4 | --- 5 | 6 | # Get a public key from a private key 7 | 8 | If you have only a [private key](./generate-private-key-directly), and you want to get its associated public key, you can use the `derivePublicKey()` method of the `Nostr.instance.services.keys`, This method will return a `String` that represents the public key, Example: 9 | 10 | ```dart 11 | // Generate a new private key. 12 | String privateKey = Nostr.instance.services.keys.generatePrivateKey(); 13 | 14 | // Later, after one hour as example, you can get its associated public key. 15 | String publicKey = Nostr.instance.services.keys.derivePublicKey(privateKey); 16 | 17 | // Now you can use both. 18 | print(publicKey); // ... 19 | print(privateKey); // ... 20 | ``` 21 | 22 | ## What's Next ? 23 | 24 | Lear how you can encode & decode a key pair keys to the npub & nsec formats, [click here](./encode-decode-key-pair-keys-to-npub-nsec-formats). 25 | -------------------------------------------------------------------------------- /docs/docs/usage/keys-management/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | # Keys Management 5 | 6 | This section will guide you through the keys management in `dart_nostr`, this includes users key pairs generation, keys derivation, keys encryption, Nostr specific specs for encoding/decoding... etc. 7 | 8 | -------------------------------------------------------------------------------- /docs/docs/usage/keys-management/signing-and-verifying-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | description: Learn how to sign & verify data. 4 | --- 5 | 6 | # Sign and verify a pice of data 7 | 8 | Generating a key pair is not about just generating the private and public keys, it's also about using them to sign and verify data, this is what we will talk about in this section. 9 | 10 | To sign a message as example or any price of data, you will need to use the `sign()` method on the `NostrKeyPair` object, which will return a `String` that represents the signature of the message. Then, if you want to verify that or any other signature, you can use the `verify()` method, which will return a `bool` that indicates if the signature belongs to the user keys or not, Example: 11 | 12 | ## Sign Messages 13 | 14 | You can sign any message you want, using the `sign` method of the `Nostr.instance.services.keys`, like this: 15 | 16 | ```dart 17 | 18 | // The message to sign. 19 | String message = "something, IDK"; 20 | 21 | // The signature of the message. 22 | String signature = Nostr.instance.services.keys.sign( 23 | privateKey: "THE_PRIVATE_KEY_HEX_STRING", 24 | message: message, 25 | ); 26 | 27 | // Use the signature as you want. 28 | print(signature); // ... 29 | 30 | //... 31 | ``` 32 | 33 | This will provide you withe a signature of that data that you can use to verify its ownership later. 34 | 35 | ## Verify Signatures 36 | 37 | You can verify any signature you want, using the `verify` method of the `Nostr.instance.services.keys`, like this: 38 | 39 | ```dart 40 | 41 | // Later, when we get a signature from any source, we can verify it. 42 | bool isSignatureVerified = Nostr.instance.services.keys.verify( 43 | publicKey: "THE_PUBLIC_KEY_HEX_STRING", 44 | message: message, 45 | signature: signature, 46 | ); 47 | 48 | // Use the verification result as you want. 49 | print(isSignatureVerified ); // ... 50 | ``` 51 | 52 | This will provide you with a `bool` that indicates if the signature belongs to the user keys or not. 53 | -------------------------------------------------------------------------------- /docs/docs/usage/relays-and-events/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": 2, 3 | "label": "Relays And Events" 4 | } -------------------------------------------------------------------------------- /docs/docs/usage/relays-and-events/connecting-to-relays.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | 6 | # Managing Relays Connection 7 | 8 | ## Connecting To Relays 9 | 10 | Now you have your relays up and running, and you have your events ready to be sent to them, but how can you send them to your relays? 11 | 12 | Before sending any event to your relays, you will need to initialize/connect to your them at least one time in your Dart/Flutter app before sending any event. 13 | 14 | ```dart 15 | // TODO: add the code here. 16 | ``` 17 | 18 | if you have a Flutter app, I personally recommend you to call this method in the `main()` before the `runApp` is called, so you ensure that the relays are connected before the app starts. 19 | 20 | ```dart 21 | void main() async { 22 | await Nostr.instance.services.relays.init( 23 | relaysUrl: ["wss://eden.nostr.land"], 24 | connectionTimeout: Duration(seconds: 5), 25 | ensureToClearRegistriesBeforeStarting: true, 26 | ignoreConnectionException: true, 27 | lazyListeningToRelays: false, 28 | onRelayConnectionDone: (relayUrl, relayWebSocket) { 29 | print("Connected to relay: $relayUrl"); 30 | }, 31 | onRelayListening: (relayUrl, receivedEvent, relayWebSocket) { 32 | print("Listening to relay: $relayUrl"); 33 | }, 34 | onRelayConnectionError: (relayUrl, error, relayWebSocket) {}, 35 | retryOnClose: true, 36 | retryOnError: true, 37 | shouldReconnectToRelayOnNotice: true, 38 | ); 39 | 40 | // ... 41 | 42 | // if it is a flutter app: 43 | // runApp(MyApp()); 44 | } 45 | ``` 46 | 47 | ## Reconneting to relays 48 | 49 | if you already connected to your relays, and you want to reconnect to them again, you can call the `reconnectToRelays()` method: 50 | 51 | ```dart 52 | await Nostr.instance.services.relays.reconnectToRelays( 53 | connectionTimeout: Duration(seconds: 5), 54 | ignoreConnectionException: true, 55 | lazyListeningToRelays: false, 56 | onRelayConnectionDone: (relayUrl, relayWebSocket) { 57 | print("Connected to relay: $relayUrl"); 58 | }, 59 | onRelayListening: (relayUrl, receivedEvent, relayWebSocket) { 60 | print("Listening to relay: $relayUrl"); 61 | }, 62 | onRelayConnectionError: (relayUrl, error, relayWebSocket) {}, 63 | retryOnClose: true, 64 | retryOnError: true, 65 | shouldReconnectToRelayOnNotice: true, 66 | ); 67 | ``` 68 | 69 | ## Disconnecting from relays 70 | 71 | if you want to disconnect from your relays, you can call the `disconnectFromRelays()` method: 72 | 73 | ```dart 74 | await Nostr.instance.services.relays.disconnectFromRelays( 75 | closeCode: (relayUrl) { 76 | return WebSocketStatus.normalClosure; 77 | }, 78 | closeReason: (relayUrl) { 79 | return "Bye"; 80 | }, 81 | onRelayDisconnect: (relayUrl, relayWebSocket, returnedMessage) { 82 | print("Disconnected from relay: $relayUrl, $returnedMessage"); 83 | }, 84 | ); 85 | 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/docs/usage/relays-and-events/count_event.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Count Event 6 | 7 | You can get the events count for a specific query (filters) which mentioned in [nip45](https://github.com/nostr-protocol/nips/blob/master/45.md) instead of retrieving all their data, this is handy when you want to show numerical count to the user such as follwers, a feed notes number... 8 | 9 | For this, you can use the `sendCountEventToRelays` method, passing a `NostrCountEvent` that represents the count query: 10 | 11 | ```dart 12 | 13 | // Filter to target all notes (kind 1) events with the "nostr" tag. 14 | NostrFilter filter = NostrFilter( 15 | kinds: [1], 16 | t: ["nostr"], 17 | ); 18 | 19 | // create the count event. 20 | final countEvent = NostrCountEvent.fromPartialData( 21 | eventsFilter: filter, 22 | ); 23 | 24 | Nostr.instance.services.relays.sendCountEventToRelays( 25 | countEvent, 26 | onCountResponse: (countRes) { 27 | print("your filter matches ${countRes.count} events"); 28 | }, 29 | ); 30 | ``` 31 | 32 | when the response is got by the relays, the `onCountResponse` callback will be called, you can use it. 33 | 34 | -------------------------------------------------------------------------------- /docs/docs/usage/relays-and-events/creating-events.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Creating Events 6 | 7 | Events are the atomic unit of the Nostr protocol. This is a short overview of various types of events, you can learn more about events from [here.](https://nostr.com/the-protocol/events). 8 | 9 | In the following sections, you will learn how you can create an event using `dart_nostr`, type of implmentation that are offered are the following: 10 | 11 | - Creating a raw event, this means that all fields in the events need te be assigned manually. 12 | - Creating a shortcut event, this means that you only need to set the direct necessary fields and the rest will be handled internally by the package. 13 | - Creating customized events, thsose kind of events can be created using the two previous ways, but some Nostr NIPs, require much additional work to create there events, either some kind of hashing, encryption, setting non-sense fields... which might not be easy for all, so this packages offers some other additional ways to create those events. 14 | 15 | ## Creating a raw event 16 | 17 | You can get the final events that you will send them to your relays by either creating a raw `NostrEvent` object, that allows you to set every field manually such as id, pubkey, sig, content..., you can learn more about a Nostr event from [here.](https://github.com/nostr-protocol/nips/blob/master/01.md), example of creating an event. 18 | 19 | ```dart 20 | // Create a new key pair. 21 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 22 | 23 | // Create a new event. 24 | NostrEvent event = NostrEvent( 25 | pubkey: '', 26 | kind: 0, 27 | content: 'This is a test event content', 28 | createdAt: DateTime.now(), 29 | id: '', // you will need to generate and set the id of the event manually by hashing other event fields, please refer to the official Nostr protocol documentation to learn how to do it yourself. 30 | tags: [], 31 | sig: '', // you will need to generate and set the signature of the event manually by signing the event's id, please refer to the official Nostr protocol documentation to learn how to do it yourself. 32 | ); 33 | 34 | // later, send the event to relays. 35 | // ... 36 | ``` 37 | 38 | ## Creating events with minimum effort 39 | 40 | As it is mentioned, this will require you to set every single value of the event properties manually. Well, we all love easy things right? `dart_nostr` offers the option to handle all this internally and covers you in this part with the `NostrEvent.fromPartialData(...)` factory constructor, which requires you to only set the direct necessary fields and leave the the rest to be handled internally by the package, so you don't need to worry about anything else, this is the newest & fastest way to create an event: 41 | 42 | ```dart 43 | // Create a new key pair. 44 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 45 | 46 | // Create a new event. 47 | final event = NostrEvent.fromPartialData( 48 | kind: 0, 49 | keyPairs: keyPair, 50 | content: 'This is a test event content', 51 | tags: [], 52 | createdAt: DateTime.now(), 53 | ); 54 | 55 | // later, send the event to relays. 56 | // ... 57 | ``` 58 | 59 | The only required fields in the `NostrEvent.fromPartialData` factory constructor here are the `kind`, `keyPairs` and `content` fields. 60 | 61 | **Notes here:** 62 | 63 | - if the `tags` field is `null`, an empty list `[]` will be used in the event. 64 | 65 | - if `createdAt` is ignored, the date which will be used is the instant date when the event is created. In Dart, this means using `DateTime.now()`. 66 | 67 | - The `id`, `sign` and `pubkey` fields is what you don't need to worry about when using this constructor, the package will calculate a encode them for you, and assign them to the event that will be sent to relays. 68 | 69 | **Why `keyPairs` is required ?** 70 | 71 | The `NostrEvent.fromPartialData` requires the `keyPairs` because it needs to get it's private key to sign the event with it, creating the `sig` field of the event. In the other side, the public key will be used directly for the event `pubKey` field. 72 | -------------------------------------------------------------------------------- /docs/docs/usage/relays-and-events/listening-to-events.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Listening & subscribe to events 6 | 7 | After ensuring that your client Dart/Flutter application is connected to a set of your relays, you will need to retrieve events, this is done by sending requests to relays and listen to them. 8 | 9 | In Order to subscribe & listen to a set of target events, you will need to create the request that defines it, and then send it to the relays, and then listen to the stream of events that will be returned. 10 | 11 | Don't worry about sending events, this will be covered in the next documentation section, this is a special implmentation to subscribe to events. 12 | 13 | This is an example of how we can achieve it: 14 | 15 | ```dart 16 | 17 | // Creating a request to retrieve all kind 1 (notes) events that have the "nostr" tag. 18 | final req = NostrRequest( 19 | filters: [ 20 | NostrFilter( 21 | kind: 1, 22 | tags: ["t", "nostr"], 23 | authors: [], 24 | ), 25 | ], 26 | ); 27 | 28 | 29 | // Creating a request to retrieve all kind 1 (notes) events that have the "nostr" tag. 30 | final nostrEventsSubscription = Nostr.instance.services.relays.startEventsSubscription( 31 | request: req, 32 | onEose: (ease) { 33 | print("ease received for subscription id: ${ease.subscriptionId}"); 34 | 35 | // Closing the request as example, see next section. 36 | 37 | }); 38 | 39 | // listening to the stream of the subscription, and print all events in the debug console. 40 | nostrEventsSubscription.stream.listen((event) { 41 | print(event); 42 | }); 43 | 44 | ``` 45 | 46 | in order to trigger an action when the eose command is sent from a relay to our client app, you can pass a callback to the `onEose` parameter. 47 | 48 | ## Closing A subscription 49 | s 50 | in order to close & end aspecific subscription that is created, you can call the `closeEventsSubscription` method 51 | 52 | ```dart 53 | 54 | Nostr.instance.services.relays.closeEventsSubscription( 55 | eose.subscriptionId, 56 | ); 57 | ``` 58 | 59 | This will end & stop events for been received by the relays. -------------------------------------------------------------------------------- /docs/docs/usage/relays-and-events/sending-events.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Sending Events 6 | 7 | When you have an event that is ready to be sent to your relay(s), as explained in [creating events](./sending-events), you can call the `sendEventToRelays()` method to send it to the connected relays. 8 | 9 | ```dart 10 | // Create an example event 11 | final event = NostrEvent.fromPartialData( 12 | kind: 1, 13 | content: 'event content example', 14 | keyPair: userKeyPair, 15 | ); 16 | 17 | // Send the event to the connected relays 18 | // TODO: Add code 19 | ``` 20 | 21 | The event will be sent now to all the connected relays, and if you're already opening a subscription with a request that matches this event, you will receive it in your stream. 22 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 5 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'Dart Nostr', 10 | tagline: 'Dart implmentation for the Nostr protocol', 11 | favicon: 'img/favicon.ico', 12 | 13 | url: 'https://anasfik.github.io', 14 | // Set the // pathname under which your site is served 15 | // For GitHub pages deployment, it is often '//' 16 | baseUrl: '/nostr', 17 | 18 | // GitHub pages deployment config. 19 | // If you aren't using GitHub pages, you don't need these. 20 | organizationName: 'anasfik', // Usually your GitHub org/user name. 21 | projectName: 'nostr', // Usually your repo name. 22 | 23 | onBrokenLinks: 'throw', 24 | onBrokenMarkdownLinks: 'warn', 25 | 26 | // Even if you don't use internalization, you can use this field to set useful 27 | // metadata like html lang. For example, if your site is Chinese, you may want 28 | // to replace "en" with "zh-Hans". 29 | i18n: { 30 | defaultLocale: 'en', 31 | locales: ['en'], 32 | }, 33 | 34 | presets: [ 35 | [ 36 | 'classic', 37 | /** @type {import('@docusaurus/preset-classic').Options} */ 38 | ({ 39 | docs: { 40 | routeBasePath: '/', 41 | sidebarPath: require.resolve('./sidebars.js'), 42 | editUrl: 43 | 'https://github.com/anasfik/nostr/tree/main/docs/', 44 | }, 45 | blog: false, 46 | theme: { 47 | customCss: require.resolve('./src/css/custom.css'), 48 | }, 49 | }), 50 | ], 51 | ], 52 | 53 | themeConfig: 54 | 55 | ({ 56 | // Replace with your project's social card 57 | image: 'img/logo.png', 58 | navbar: { 59 | title: 'Dart Nostr', 60 | logo: { 61 | alt: 'Dart Nostr logo', 62 | src: 'img/logo.png', 63 | }, 64 | items: [ 65 | { 66 | type: 'docSidebar', 67 | sidebarId: 'tutorialSidebar', 68 | position: 'left', 69 | label: 'Documentation', 70 | }, 71 | { 72 | href: 'https://github.com/anasfik/nostr', 73 | label: 'GitHub', 74 | position: 'right', 75 | }, 76 | ], 77 | }, 78 | footer: { 79 | style: 'dark', 80 | links: [ 81 | { 82 | title: 'Docs', 83 | items: [ 84 | { 85 | label: 'Documentation', 86 | to: '/', 87 | }, 88 | ], 89 | }, 90 | { 91 | title: 'Community', 92 | items: [ 93 | { 94 | label: 'Issues', 95 | href: 'https://github.com/anasfik/nostr/issues', 96 | }, 97 | { 98 | label: 'Pub', 99 | href: 'https://pub.dev/packages/dart_nostr', 100 | }, 101 | ], 102 | }, 103 | { 104 | title: 'More', 105 | items: [ 106 | 107 | { 108 | label: 'Author', 109 | href: 'https://github.com/anasfik', 110 | }, 111 | ], 112 | }, 113 | ], 114 | copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`, 115 | }, 116 | prism: { 117 | theme: lightCodeTheme, 118 | darkTheme: darkCodeTheme, 119 | additionalLanguages: ['dart'] 120 | }, 121 | }), 122 | }; 123 | 124 | module.exports = config; 125 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "2.4.1", 18 | "@docusaurus/preset-classic": "2.4.1", 19 | "@mdx-js/react": "^1.6.22", 20 | "clsx": "^1.2.1", 21 | "prism-react-renderer": "^1.3.5", 22 | "react": "^17.0.2", 23 | "react-dom": "^17.0.2" 24 | }, 25 | "devDependencies": { 26 | "@docusaurus/module-type-aliases": "2.4.1" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.5%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "engines": { 41 | "node": ">=16.14" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './styles.module.css'; 4 | 5 | const FeatureList = [ 6 | { 7 | title: 'Easy to Use', 8 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 9 | description: ( 10 | <> 11 | Docusaurus was designed from the ground up to be easily installed and 12 | used to get your website up and running quickly. 13 | 14 | ), 15 | }, 16 | { 17 | title: 'Focus on What Matters', 18 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 19 | description: ( 20 | <> 21 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 22 | ahead and move your docs into the docs directory. 23 | 24 | ), 25 | }, 26 | { 27 | title: 'Powered by React', 28 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 29 | description: ( 30 | <> 31 | Extend or customize your website layout by reusing React. Docusaurus can 32 | be extended while reusing the same header and footer. 33 | 34 | ), 35 | }, 36 | ]; 37 | 38 | function Feature({Svg, title, description}) { 39 | return ( 40 |
41 |
42 | 43 |
44 |
45 |

{title}

46 |

{description}

47 |
48 |
49 | ); 50 | } 51 | 52 | export default function HomepageFeatures() { 53 | return ( 54 |
55 |
56 |
57 | {FeatureList.map((props, idx) => ( 58 | 59 | ))} 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #023e8a; 10 | --ifm-color-primary-dark: #0077b6; 11 | --ifm-color-primary-darker: #0096c7; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #cdb4db; 14 | --ifm-color-primary-lighter: #8338ec; 15 | --ifm-color-primary-lightest: #90e0ef; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #0077b6; 23 | --ifm-color-primary-dark: #023e8a; 24 | --ifm-color-primary-darker: #cdb4db; 25 | --ifm-color-primary-darkest: #03045e; 26 | --ifm-color-primary-light: #090b0b; 27 | --ifm-color-primary-lighter: #48cae4; 28 | --ifm-color-primary-lightest: #90e0ef; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasfik/nostr/61e26a82da0bfdc8ee44cce570f93195fc0a6e40/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasfik/nostr/61e26a82da0bfdc8ee44cce570f93195fc0a6e40/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasfik/nostr/61e26a82da0bfdc8ee44cce570f93195fc0a6e40/docs/static/img/logo.png -------------------------------------------------------------------------------- /docs/static/img/undraw_docusaurus_tree.svg: -------------------------------------------------------------------------------- 1 | 2 | Focus on What Matters 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/auto_reconnect_after_notice_message_from_a_relay.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | // Waiting first for the connection to be established for all relays. 5 | 6 | await Nostr.instance.services.relays.init( 7 | relaysUrl: [ 8 | 'wss://relay.damus.io', 9 | 'wss://eden.nostr.land', 10 | ], 11 | shouldReconnectToRelayOnNotice: true, 12 | ); 13 | 14 | // sending n different requests to the relays. 15 | for (var i = 0; i < 50; i++) { 16 | // Creating the request that we will listen with to events. 17 | 18 | final req = NostrRequest( 19 | filters: [ 20 | NostrFilter( 21 | t: const ['nostr'], 22 | kinds: const [0], 23 | since: DateTime.now().subtract(const Duration(days: 10)), 24 | ), 25 | ], 26 | ); 27 | 28 | print('Starting subscription $i'); 29 | Nostr.instance.services.relays.startEventsSubscription(request: req); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/cached_nostr_key_pair.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() { 4 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 5 | 6 | final existentKeyPair = Nostr.instance.services.keys 7 | .generateKeyPairFromExistingPrivateKey(keyPair.private); 8 | 9 | print(existentKeyPair.private); 10 | } 11 | -------------------------------------------------------------------------------- /example/check_key_validity.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 2 | 3 | void main() { 4 | final nostrKeyPairs = Nostr.instance.services.keys.generateKeyPair(); 5 | 6 | print(nostrKeyPairs.private); 7 | 8 | final firstKey = nostrKeyPairs.private; 9 | const secondKey = ''; 10 | 11 | print( 12 | 'is firstKey a valid key? ${Nostr.instance.services.keys.isValidPrivateKey(firstKey)}', 13 | ); 14 | print( 15 | 'is secondKey a valid key? ${Nostr.instance.services.keys.isValidPrivateKey(secondKey)}', 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /example/connectiong_to_relays.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | Future main() async { 4 | await Nostr.instance.services.relays.init( 5 | relaysUrl: [ 6 | /// your relays ... 7 | ], 8 | onRelayConnectionDone: (relayUrl, relayWebSocket) { 9 | print('Connected to relay: $relayUrl'); 10 | }, 11 | onRelayListening: (relayUrl, receivedEvent, relayWebSocket) { 12 | print('Listening to relay: $relayUrl'); 13 | }, 14 | onRelayConnectionError: (relayUrl, error, relayWebSocket) {}, 15 | retryOnClose: true, 16 | retryOnError: true, 17 | shouldReconnectToRelayOnNotice: true, 18 | ); 19 | 20 | await Future.delayed(const Duration(seconds: 5)); 21 | 22 | await Nostr.instance.services.relays.reconnectToRelays( 23 | connectionTimeout: const Duration(seconds: 5), 24 | ignoreConnectionException: true, 25 | lazyListeningToRelays: false, 26 | onRelayConnectionDone: (relayUrl, relayWebSocket) { 27 | print('Connected to relay: $relayUrl'); 28 | }, 29 | onRelayListening: (relayUrl, receivedEvent, relayWebSocket) { 30 | print('Listening to relay: $relayUrl'); 31 | }, 32 | onRelayConnectionError: (relayUrl, error, relayWebSocket) {}, 33 | retryOnClose: true, 34 | retryOnError: true, 35 | shouldReconnectToRelayOnNotice: true, 36 | ); 37 | 38 | await Future.delayed(const Duration(seconds: 5)); 39 | 40 | await Nostr.instance.services.relays.disconnectFromRelays( 41 | closeCode: (relayUrl) { 42 | return 1000; 43 | }, 44 | closeReason: (relayUrl) { 45 | return 'Bye'; 46 | }, 47 | onRelayDisconnect: (relayUrl, relayWebSocket, returnedMessage) { 48 | print('Disconnected from relay: $relayUrl, $returnedMessage'); 49 | }, 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /example/count_event_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | // init relays 5 | final relays = [ 6 | 'wss://relay.nostr.band', 7 | 'wss://eden.nostr.land', 8 | 'wss://nostr.fmt.wiz.biz', 9 | 'wss://relay.damus.io', 10 | 'wss://nostr-pub.wellorder.net', 11 | 'wss://relay.nostr.info', 12 | 'wss://offchain.pub', 13 | 'wss://nos.lol', 14 | 'wss://brb.io', 15 | 'wss://relay.snort.social', 16 | 'wss://relay.current.fyi', 17 | 'wss://nostr.relayer.se', 18 | ]; 19 | 20 | final relayThatSupportsNip45 = []; 21 | 22 | for (final relay in relays) { 23 | final relayInfo = 24 | await Nostr.instance.services.relays.relayInformationsDocumentNip11( 25 | relayUrl: relay, 26 | throwExceptionIfExists: false, 27 | ); 28 | if (relayInfo?.supportedNips?.contains(45) ?? false) { 29 | relayThatSupportsNip45.add(relay); 30 | break; 31 | } 32 | } 33 | 34 | if (relayThatSupportsNip45.isEmpty) { 35 | throw Exception('no relay supports NIP-45'); 36 | } 37 | 38 | await Nostr.instance.services.relays.init( 39 | relaysUrl: relayThatSupportsNip45, 40 | ); 41 | 42 | // create filter for events to count with. 43 | const filter = NostrFilter( 44 | kinds: [1], 45 | t: ['nostr'], 46 | ); 47 | 48 | // create the count event. 49 | final countEvent = NostrCountEvent.fromPartialData( 50 | eventsFilter: filter, 51 | ); 52 | 53 | Nostr.instance.services.relays.sendCountEventToRelays( 54 | countEvent, 55 | onCountResponse: (relay, countRes) { 56 | print('from relay: $relay'); 57 | 58 | print('your filter matches ${countRes.count} events'); 59 | }, 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /example/generate_key_pair.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | // This method will enable the logs of the library. 5 | Nostr.instance.enableLogs(); 6 | 7 | // generates a key pair. 8 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 9 | 10 | print(keyPair.public); // ... 11 | print(keyPair.private); // ... 12 | 13 | final sameKeyPairGeneratedFromPrivate = Nostr.instance.services.keys 14 | .generateKeyPairFromExistingPrivateKey(keyPair.private); 15 | 16 | print(sameKeyPairGeneratedFromPrivate.public); // ... 17 | print(sameKeyPairGeneratedFromPrivate.private); // ... 18 | 19 | assert(sameKeyPairGeneratedFromPrivate == keyPair); 20 | if (sameKeyPairGeneratedFromPrivate != keyPair) { 21 | throw Exception('Key pair generation has something wrong.'); 22 | } 23 | 24 | final publicKey = Nostr.instance.services.keys 25 | .derivePublicKey(privateKey: sameKeyPairGeneratedFromPrivate.private); 26 | print(publicKey); 27 | 28 | assert(publicKey == sameKeyPairGeneratedFromPrivate.public); 29 | if (publicKey != sameKeyPairGeneratedFromPrivate.public) { 30 | throw Exception('Key pair generation has something wrong.'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/generate_nevent_of_nostr_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 2 | import 'package:dart_nostr/nostr/model/event/event.dart'; 3 | 4 | void main() { 5 | final newKeyPair = Nostr.instance.services.keys.generateKeyPair(); 6 | 7 | print('pubKey: ${newKeyPair.public}'); 8 | 9 | final relays = ['wss://relay.damus.io']; 10 | 11 | final nostrEvent = NostrEvent.fromPartialData( 12 | kind: 1, 13 | content: 'THIS IS EXAMPLE OF NOSTR EVENT CONTENT', 14 | keyPairs: newKeyPair, 15 | ); 16 | 17 | print('event id: ${nostrEvent.id}'); 18 | 19 | if (nostrEvent.id == null) { 20 | throw Exception('event id cannot be null'); 21 | } 22 | 23 | final encodedNEvent = Nostr.instance.services.bech32.encodeNevent( 24 | eventId: nostrEvent.id!, 25 | userRelays: relays, 26 | pubkey: newKeyPair.public, 27 | ); 28 | 29 | print('encodedNEvent: $encodedNEvent'); 30 | 31 | final decodedEvent = 32 | Nostr.instance.services.bech32.decodeNeventToMap(encodedNEvent); 33 | 34 | print('decodedEvent: $decodedEvent'); 35 | } 36 | -------------------------------------------------------------------------------- /example/generate_nprofile_from_pubkey.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 2 | 3 | void main() { 4 | final newKeyPair = Nostr.instance.services.keys.generateKeyPair(); 5 | 6 | final relays = ['wss://relay.damus.io']; 7 | 8 | final nProfile = Nostr.instance.services.bech32.encodeNProfile( 9 | pubkey: newKeyPair.public, 10 | userRelays: relays, 11 | ); 12 | 13 | print('nProfile: $nProfile'); 14 | 15 | final decodedNprofile = 16 | Nostr.instance.services.bech32.decodeNprofileToMap(nProfile); 17 | 18 | print('decodedNprofile: $decodedNprofile'); 19 | } 20 | -------------------------------------------------------------------------------- /example/get_npub_and_nsec_and_others_bech32_encoded_keys.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() { 4 | // we generate a key pair and print the public and private keys 5 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 6 | final publicKey = keyPair.public; 7 | final privateKey = keyPair.private; 8 | print('publicKey: $publicKey'); 9 | print('privateKey: $privateKey'); 10 | 11 | // we encode the public key to an npub key (bech32 encoding) 12 | final npub = Nostr.instance.services.bech32.encodePublicKeyToNpub(publicKey); 13 | print('npub: $npub'); 14 | 15 | // we encode the private key to an nsec key (bech32 encoding) 16 | final nsec = 17 | Nostr.instance.services.bech32.encodePrivateKeyToNsec(privateKey); 18 | print('nsec: $nsec'); 19 | 20 | // we decode the npub key to a public key 21 | final decodedPublicKey = 22 | Nostr.instance.services.bech32.decodeNpubKeyToPublicKey(npub); 23 | print('decodedPublicKey: $decodedPublicKey'); 24 | 25 | // we decode the nsec key to a private key 26 | final decodedPrivateKey = 27 | Nostr.instance.services.bech32.decodeNsecKeyToPrivateKey(nsec); 28 | print('decodedPrivateKey: $decodedPrivateKey'); 29 | 30 | assert(publicKey == decodedPublicKey); 31 | assert(privateKey == decodedPrivateKey); 32 | } 33 | -------------------------------------------------------------------------------- /example/get_pubkey_from_identifier_nip_05.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | Future main() async { 4 | final puKey = await Nostr.instance.services.utils.pubKeyFromIdentifierNip05( 5 | internetIdentifier: 6 | 'aljaz@raw.githubusercontent.com/aljazceru/awesome-nostr/main', 7 | ); 8 | 9 | print(puKey); 10 | } 11 | -------------------------------------------------------------------------------- /example/get_pubkey_from_identifier_nip_05__2.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | try { 5 | final publicKeyFromNip05 = 6 | await Nostr.instance.services.utils.pubKeyFromIdentifierNip05( 7 | internetIdentifier: 'jb55@jb55.com', 8 | ); 9 | 10 | print('publicKeyFromNip05: $publicKeyFromNip05'); // ... 11 | } catch (e) { 12 | print(e); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/get_user_metadata.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | await Nostr.instance.services.relays.init( 5 | relaysUrl: ['wss://relay.damus.io'], 6 | ); 7 | 8 | final request = NostrRequest( 9 | filters: const [ 10 | NostrFilter( 11 | kinds: [0], 12 | limit: 10, 13 | search: 'something Idk', 14 | ), 15 | ], 16 | ); 17 | 18 | final requestStream = Nostr.instance.services.relays.startEventsSubscription( 19 | request: request, 20 | relays: ['wss://relay.nostr.band/all'], 21 | onEose: (relay, ease) { 22 | print('ease received for subscription id: ${ease.subscriptionId}'); 23 | 24 | Nostr.instance.services.relays.closeEventsSubscription( 25 | ease.subscriptionId, 26 | ); 27 | }, 28 | ); 29 | 30 | requestStream.stream.listen(print); 31 | } 32 | -------------------------------------------------------------------------------- /example/listening_to_events.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | // // I did disabled logs here so we can see the output of the example exclusively. 5 | Nostr.instance.enableLogs(); 6 | 7 | // We initialize the Nostr Relays Service with relays. 8 | await Nostr.instance.services.relays.init( 9 | relaysUrl: [ 10 | 'wss://relay.nostr.band/all', 11 | ], 12 | onRelayConnectionError: (relay, error, webSocket) { 13 | print('Relay error: $error'); 14 | }, 15 | onRelayConnectionDone: (relayUrl, webSocket) => 16 | print('Relay done: $relayUrl'), 17 | ); 18 | 19 | final request = NostrRequest( 20 | filters: const [ 21 | NostrFilter( 22 | kinds: [10004], 23 | limit: 100, 24 | ), 25 | ], 26 | ); 27 | 28 | // Now we create the stream of that request. 29 | // ignore: unused_local_variable 30 | final requestStream = Nostr.instance.services.relays.startEventsSubscription( 31 | request: request, 32 | onEose: (relay, ease) { 33 | print('ease received for subscription id: ${ease.subscriptionId}'); 34 | 35 | Nostr.instance.services.relays.closeEventsSubscription( 36 | ease.subscriptionId, 37 | ); 38 | }, 39 | ); 40 | 41 | // We listen to the stream and print the events. 42 | requestStream.stream.listen((event) { 43 | if (event.tags == null) { 44 | print('tags are null'); 45 | 46 | return; 47 | } 48 | 49 | if (event.tags?.isEmpty ?? true) { 50 | print('tags are empty'); 51 | 52 | return; 53 | } 54 | 55 | for (final a in event.tags!) { 56 | if (a.first == 'a') { 57 | print(a); 58 | } 59 | } 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dart_nostr/dart_nostr.dart'; 4 | 5 | Future main() async { 6 | // This method will enable the logs of the library. 7 | Nostr.instance.enableLogs(); 8 | 9 | // generates a key pair. 10 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 11 | 12 | // init relays 13 | await Nostr.instance.services.relays.init( 14 | relaysUrl: ['wss://relay.damus.io'], 15 | ); 16 | 17 | final currentDateInMsAsString = 18 | DateTime.now().millisecondsSinceEpoch.toString(); 19 | 20 | // create an event 21 | final event = NostrEvent.fromPartialData( 22 | kind: 1, 23 | content: 'content', 24 | keyPairs: keyPair, 25 | tags: [ 26 | ['t', currentDateInMsAsString], 27 | ['title', 'ps5'], 28 | ], 29 | ); 30 | 31 | final asMap = event.toMap(); 32 | print(asMap); 33 | 34 | // send the event 35 | Nostr.instance.services.relays.sendEventToRelays(event); 36 | 37 | await Future.delayed(const Duration(seconds: 5)); 38 | 39 | // create a subscription id. 40 | final subscriptionId = Nostr.instance.services.utils.random64HexChars(); 41 | 42 | // creating a request for listening to events. 43 | final request = NostrRequest( 44 | subscriptionId: subscriptionId, 45 | filters: [ 46 | NostrFilter( 47 | kinds: const [1], 48 | t: [currentDateInMsAsString], 49 | authors: [keyPair.public], 50 | ), 51 | ], 52 | ); 53 | 54 | // listen to events 55 | final sub = Nostr.instance.services.relays.startEventsSubscription( 56 | request: request, 57 | onEose: (relay, eose) { 58 | print('eose $eose from $relay'); 59 | }, 60 | ); 61 | 62 | final StreamSubscription subscritpion = sub.stream.listen( 63 | print, 64 | onDone: () { 65 | print('done'); 66 | }, 67 | ); 68 | 69 | await Future.delayed(const Duration(seconds: 5)); 70 | 71 | // cancel the subscription 72 | await subscritpion.cancel().whenComplete(() { 73 | Nostr.instance.services.relays.closeEventsSubscription(subscriptionId); 74 | }); 75 | 76 | await Future.delayed(const Duration(seconds: 5)); 77 | 78 | // create a new event that will not be received by the subscription because it is closed. 79 | final event2 = NostrEvent.fromPartialData( 80 | kind: 1, 81 | content: 'example content', 82 | keyPairs: keyPair, 83 | tags: [ 84 | ['t', currentDateInMsAsString], 85 | ], 86 | ); 87 | 88 | // send the event 2 that will not be received by the subscription because it is closed. 89 | Nostr.instance.services.relays.sendEventToRelays( 90 | event2, 91 | onOk: (relay, ok) { 92 | print('ok $ok from $relay'); 93 | }, 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /example/receiving_events_from_reopened_subscriptions_with_same_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | final relays = [ 5 | 'wss://nos.lol', 6 | ]; 7 | 8 | await Nostr.instance.services.relays.init(relaysUrl: relays); 9 | 10 | final newKeyPair = Nostr.instance.services.keys.generateKeyPair(); 11 | 12 | final event = NostrEvent.fromPartialData( 13 | content: newKeyPair.public, 14 | kind: 1, 15 | keyPairs: newKeyPair, 16 | tags: [ 17 | ['t', newKeyPair.public], 18 | ], 19 | ); 20 | 21 | Nostr.instance.services.relays.sendEventToRelays( 22 | event, 23 | onOk: (relay, ok) { 24 | print('from relay: $relay'); 25 | print('event sent, ${ok.eventId}'); 26 | }, 27 | ); 28 | 29 | await Future.delayed(const Duration(seconds: 5)); 30 | 31 | // ... 32 | 33 | final filter = NostrFilter( 34 | kinds: const [1], 35 | t: [newKeyPair.public], 36 | ); 37 | 38 | final req = NostrRequest(filters: [filter]); 39 | 40 | final sub = Nostr.instance.services.relays.startEventsSubscription( 41 | request: req, 42 | onEose: (relay, eose) { 43 | Nostr.instance.services.relays 44 | .closeEventsSubscription(eose.subscriptionId); 45 | }, 46 | ); 47 | 48 | sub.stream.listen((event) { 49 | print(event.content); 50 | }); 51 | 52 | await Future.delayed(const Duration(seconds: 5)); 53 | 54 | for (var index = 0; index < 50; index++) { 55 | Nostr.instance.services.relays.startEventsSubscription( 56 | request: req, 57 | ); 58 | } 59 | 60 | await Future.delayed(const Duration(seconds: 5)); 61 | 62 | Nostr.instance.services.relays.startEventsSubscription( 63 | request: req, 64 | ); 65 | 66 | await Future.delayed(const Duration(seconds: 5)); 67 | 68 | final anotherEvent = NostrEvent.fromPartialData( 69 | kind: 1, 70 | content: 'another event with different content, but matches same filter', 71 | keyPairs: newKeyPair, 72 | tags: [ 73 | ['t', newKeyPair.public], 74 | ], 75 | ); 76 | 77 | Nostr.instance.services.relays.sendEventToRelays( 78 | anotherEvent, 79 | onOk: (relay, ok) { 80 | print('event sent, ${ok.eventId}'); 81 | }, 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /example/relay_document_nip_11.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | final relayDocument = 5 | await Nostr.instance.services.relays.relayInformationsDocumentNip11( 6 | relayUrl: 'wss://relay.damus.io', 7 | ); 8 | 9 | print(relayDocument?.name); 10 | } 11 | -------------------------------------------------------------------------------- /example/search_for_events.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dart_nostr/dart_nostr.dart'; 4 | 5 | Future main() async { 6 | final instance = Nostr()..disableLogs(); 7 | 8 | // init relays 9 | await instance.services.relays.init( 10 | relaysUrl: [ 11 | 'wss://relay.nostr.band/all', 12 | ], 13 | ); 14 | 15 | final req = NostrRequest( 16 | filters: const [ 17 | NostrFilter( 18 | limit: 500, 19 | kinds: [10004], 20 | ), 21 | NostrFilter( 22 | kinds: [3], 23 | limit: 100, 24 | ), 25 | ], 26 | ); 27 | 28 | final sub = instance.services.relays.startEventsSubscription( 29 | request: req, 30 | ); 31 | 32 | sub.stream.listen((event) { 33 | print(event.tags); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /example/send_delete_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | // Create a new user key pair 5 | final newKeyPair = Nostr.instance.services.keys.generateKeyPair(); 6 | 7 | // set our relays list. 8 | final relays = ['wss://relay.damus.io']; 9 | 10 | // init relays service with our relays list. 11 | await Nostr.instance.services.relays.init(relaysUrl: relays); 12 | 13 | // create a delete event 14 | final deleteEvent = NostrEvent.deleteEvent( 15 | reasonOfDeletion: 16 | 'As example, the user decided to delete his created note events.', 17 | keyPairs: newKeyPair, 18 | eventIdsToBeDeleted: [ 19 | // this is just an example event id. 20 | Nostr.instance.services.utils.random64HexChars(), 21 | ], 22 | ); 23 | 24 | // send the delete event 25 | Nostr.instance.services.relays.sendEventToRelays(deleteEvent); 26 | } 27 | -------------------------------------------------------------------------------- /example/send_event_asynchronously.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dart_nostr/dart_nostr.dart'; 4 | 5 | Future main(List args) async { 6 | await Nostr.instance.services.relays.init( 7 | relaysUrl: [ 8 | 'wss://relay.damus.io', 9 | ], 10 | ); 11 | 12 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 13 | 14 | final event = NostrEvent.fromPartialData( 15 | kind: 1, 16 | content: 'Hello World! from dart_nostr package', 17 | keyPairs: keyPair, 18 | ); 19 | 20 | try { 21 | final okCOmmand = 22 | await Nostr.instance.services.relays.sendEventToRelaysAsync( 23 | event, 24 | timeout: const Duration(seconds: 10), 25 | ); 26 | 27 | print(okCOmmand.isEventAccepted); 28 | } on TimeoutException { 29 | print('Timeout'); 30 | } catch (e) { 31 | print(e); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/sending_event_to_relays.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | // This method will enable the logs of the library. 5 | Nostr.instance.enableLogs(); 6 | final relaysList = [ 7 | 'wss://relay.nostr.band/all', 8 | ]; 9 | 10 | // initialize the relays service. 11 | await Nostr.instance.services.relays.init( 12 | relaysUrl: relaysList, 13 | onRelayConnectionError: (relay, err, websocket) { 14 | print('relay connection error: $err'); 15 | }, 16 | ); 17 | 18 | // generate a key pair. 19 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 20 | 21 | final event = NostrEvent.fromPartialData( 22 | kind: 0, 23 | content: 'test ', 24 | keyPairs: keyPair, 25 | ); 26 | 27 | Nostr.instance.services.relays.sendEventToRelays( 28 | event, 29 | relays: [ 30 | ...relaysList, 31 | 'wss://relay.damus.io', 32 | ], 33 | onOk: (relay, ok) { 34 | print(relay); 35 | print(ok.eventId); 36 | print(ok.isEventAccepted); 37 | print(ok.message); 38 | print('\n'); 39 | }, 40 | ); 41 | 42 | Nostr.instance.services.relays.sendEventToRelays( 43 | event, 44 | relays: [ 45 | ...relaysList, 46 | ], 47 | onOk: (relay, ok) { 48 | print('second only'); 49 | }, 50 | ); 51 | 52 | // ! check logs and run this code. 53 | } 54 | -------------------------------------------------------------------------------- /example/signing_and_verfiying_messages.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() { 4 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 5 | final signature = Nostr.instance.services.keys.sign( 6 | privateKey: keyPair.private, 7 | message: 'message', 8 | ); 9 | 10 | print('signature: $signature'); // ... 11 | final isVerified = Nostr.instance.services.keys.verify( 12 | publicKey: keyPair.public, 13 | message: 'message', 14 | signature: signature, 15 | ); 16 | 17 | print('isVerified: $isVerified'); // true 18 | } 19 | -------------------------------------------------------------------------------- /example/subscribe_asyncronously_to_events_until_eose.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_nostr/dart_nostr.dart'; 4 | 5 | void main(List args) async { 6 | Nostr.instance.disableLogs(); 7 | 8 | await Nostr.instance.services.relays.init( 9 | relaysUrl: [ 10 | 'wss://relay.damus.io', 11 | ], 12 | ); 13 | final keyPair = Nostr.instance.services.keys.generateKeyPair(); 14 | 15 | final event = NostrEvent.fromPartialData( 16 | kind: 0, 17 | content: jsonEncode( 18 | { 19 | 'name': 'Gwhyyy (dart_nostr)', 20 | }, 21 | ), 22 | keyPairs: keyPair, 23 | ); 24 | 25 | try { 26 | final countEvent = NostrCountEvent.fromPartialData( 27 | eventsFilter: NostrFilter( 28 | kinds: const [0], 29 | authors: [keyPair.public], 30 | ), 31 | ); 32 | 33 | final okCommand = 34 | await Nostr.instance.services.relays.sendEventToRelaysAsync( 35 | event, 36 | timeout: const Duration(seconds: 3), 37 | ); 38 | 39 | if (!(okCommand.isEventAccepted ?? true)) { 40 | print('not accepted'); 41 | return; 42 | } 43 | 44 | final request = NostrRequest( 45 | filters: [ 46 | NostrFilter( 47 | limit: 10, 48 | kinds: const [0], 49 | authors: [keyPair.public], 50 | ), 51 | ], 52 | ); 53 | 54 | final events = 55 | await Nostr.instance.services.relays.startEventsSubscriptionAsync( 56 | request: request, 57 | timeout: const Duration(seconds: 10), 58 | ); 59 | 60 | for (final element in events) { 61 | // should our event content here 62 | print('${element.content}\n\n'); 63 | } 64 | 65 | final isFree = await Nostr.instance.services.relays.freeAllResources(); 66 | print('isFree: $isFree'); 67 | } catch (e) { 68 | print(e); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/verify_nip05.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | 3 | void main() async { 4 | const publicKeyToCheckWith = 5 | '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'; 6 | 7 | final isIdentifierVerifiedWithPublixKey = 8 | await Nostr.instance.services.utils.verifyNip05( 9 | internetIdentifier: 'jb55@randomshit.com', 10 | pubKey: publicKeyToCheckWith, 11 | ); 12 | 13 | print( 14 | 'isIdentifierVerifiedWithPublixKey: $isIdentifierVerifiedWithPublixKey', 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/dart_nostr.dart: -------------------------------------------------------------------------------- 1 | export './nostr/core/key_pairs.dart'; 2 | export './nostr/core/utils.dart'; 3 | export './nostr/model/export.dart'; 4 | export 'nostr/dart_nostr.dart'; 5 | -------------------------------------------------------------------------------- /lib/nostr/core/constants.dart: -------------------------------------------------------------------------------- 1 | abstract class NostrConstants { 2 | static const String event = 'EVENT'; 3 | static const String request = 'REQ'; 4 | static const String notice = 'NOTICE'; 5 | static const String count = 'COUNT'; 6 | static const String ok = 'OK'; 7 | static const String close = 'CLOSE'; 8 | static const String eose = 'EOSE'; 9 | 10 | static const String npub = 'npub'; 11 | static const String nsec = 'nsec'; 12 | static const String nProfile = 'nprofile'; 13 | static const String nEvent = 'nevent'; 14 | } 15 | -------------------------------------------------------------------------------- /lib/nostr/core/exceptions.dart: -------------------------------------------------------------------------------- 1 | /// {@template relay_not_found_exception} 2 | /// Thrown when a relay is not found/registered. 3 | /// {@endtemplate} 4 | class RelayNotFoundException implements Exception { 5 | /// {@macro relay_not_found_exception} 6 | RelayNotFoundException(this.relayUrl); 7 | 8 | /// The url of the relay that was not found. 9 | final String relayUrl; 10 | 11 | @override 12 | String toString() { 13 | return 'RelayNotFoundException: Relay with url "$relayUrl" was not found.'; 14 | } 15 | } 16 | 17 | /// {@template nip05_verification_exception} 18 | /// Thrown when there is an error verifying a nip05 identifier. 19 | /// {@endtemplate} 20 | class Nip05VerificationException implements Exception { 21 | /// {@macro nip05_verification_exception} 22 | const Nip05VerificationException({ 23 | this.parent, 24 | }); 25 | 26 | /// Cause of the exception 27 | final Exception? parent; 28 | 29 | @override 30 | String toString() { 31 | return 'Something went wrong while verifying nip05 identifier. ' 32 | 'Underlying issue was: $parent'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/nostr/core/extensions.dart: -------------------------------------------------------------------------------- 1 | extension RelaysListExt on List { 2 | bool containsRelay(String relay) { 3 | return contains(relay); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/nostr/core/key_pairs.dart: -------------------------------------------------------------------------------- 1 | import 'package:bip340/bip340.dart' as bip340; 2 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 3 | import 'package:equatable/equatable.dart'; 4 | 5 | /// {@template nostr_key_pairs} 6 | /// This class is responsible for generating, handling and signing keys. 7 | /// It is used by the [NostrClient] to sign messages. 8 | /// {@endtemplate} 9 | class NostrKeyPairs extends Equatable { 10 | /// {@macro nostr_key_pairs} 11 | factory NostrKeyPairs({ 12 | required String private, 13 | }) { 14 | return NostrKeyPairs._(private: private); 15 | } 16 | 17 | /// {@macro nostr_key_pairs} 18 | NostrKeyPairs._({required this.private}) { 19 | assert( 20 | private.length == 64, 21 | 'Private key should be 64 chars length (32 bytes hex encoded)', 22 | ); 23 | 24 | public = bip340.getPublicKey(private); 25 | } 26 | 27 | /// {@macro nostr_key_pairs} 28 | /// Instantiate a [NostrKeyPairs] from random bytes. 29 | factory NostrKeyPairs.generate() { 30 | return NostrKeyPairs( 31 | private: Nostr.instance.services.utils.random64HexChars(), 32 | ); 33 | } 34 | 35 | /// This is the private generate Key, hex-encoded (64 chars) 36 | final String private; 37 | 38 | /// This is the public generate Key, hex-encoded (64 chars) 39 | late final String public; 40 | 41 | /// This will sign a [message] with the [private] key and return the signature. 42 | String sign(String message) { 43 | final aux = Nostr.instance.services.utils.random64HexChars(); 44 | return bip340.sign(private, message, aux); 45 | } 46 | 47 | /// This will verify a [signature] for a [message] with the [public] key. 48 | static bool verify( 49 | String? pubkey, 50 | String message, 51 | String signature, 52 | ) { 53 | return bip340.verify(pubkey, message, signature); 54 | } 55 | 56 | static bool isValidPrivateKey(String privateKey) { 57 | try { 58 | NostrKeyPairs(private: privateKey); 59 | 60 | return true; 61 | } catch (e) { 62 | return false; 63 | } 64 | } 65 | 66 | @override 67 | List get props => [private, public]; 68 | } 69 | -------------------------------------------------------------------------------- /lib/nostr/core/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer' as dev; 2 | 3 | import 'package:dart_nostr/dart_nostr.dart'; 4 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 5 | import 'package:dart_nostr/nostr/model/debug_options.dart'; 6 | 7 | /// {@template nostr_client_utils} 8 | /// General utils to be used in a whole [Nostr] instance. 9 | /// {@endtemplate} 10 | class NostrLogger { 11 | NostrLogger({ 12 | required this.passedDebugOptions, 13 | }) { 14 | _debugOptions = passedDebugOptions; 15 | } 16 | final NostrDebugOptions passedDebugOptions; 17 | late NostrDebugOptions _debugOptions; 18 | 19 | NostrDebugOptions get debugOptions => _debugOptions; 20 | 21 | /// Disables logs. 22 | void disableLogs() { 23 | _debugOptions = _debugOptions.copyWith(isLogsEnabled: false); 24 | } 25 | 26 | /// Enables logs. 27 | void enableLogs() { 28 | _debugOptions = _debugOptions.copyWith(isLogsEnabled: true); 29 | } 30 | 31 | /// Logs a message, and an optional error. 32 | void log(String message, [Object? error]) { 33 | final isLogsEnabled = _debugOptions.isLogsEnabled; 34 | 35 | if (!isLogsEnabled) { 36 | return; 37 | } 38 | 39 | final tag = _debugOptions.tag; 40 | 41 | var logDisplayName = tag; 42 | 43 | if (error != null) { 44 | logDisplayName = '$tag$error'; 45 | } 46 | 47 | dev.log( 48 | message, 49 | name: logDisplayName, 50 | error: error, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/nostr/dart_nostr.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | import 'package:dart_nostr/nostr/core/utils.dart'; 4 | import 'package:dart_nostr/nostr/model/debug_options.dart'; 5 | import 'package:dart_nostr/nostr/service/services.dart'; 6 | 7 | /// {@template nostr_service} 8 | /// This class is responsible for handling the connection to all relays. 9 | /// {@endtemplate} 10 | class Nostr { 11 | /// {@macro nostr_service} 12 | factory Nostr({ 13 | NostrDebugOptions? debugOptions, 14 | }) { 15 | // utils.log("A Nostr instance created successfully."); 16 | return Nostr._( 17 | debugOptions: debugOptions, 18 | ); 19 | } 20 | 21 | /// {@macro nostr_service} 22 | Nostr._({ 23 | NostrDebugOptions? debugOptions, 24 | }) { 25 | debugOptions ??= NostrDebugOptions.generate(); 26 | 27 | _logger = NostrLogger(passedDebugOptions: debugOptions); 28 | } 29 | 30 | /// Wether this instance resources are disposed or not. 31 | bool _isDisposed = false; 32 | 33 | // 34 | /// {@macro nostr_service} 35 | static final Nostr _instance = Nostr._( 36 | debugOptions: NostrDebugOptions.general(), 37 | ); 38 | 39 | /// {@macro nostr_service} 40 | static Nostr get instance => _instance; 41 | 42 | /// This method will disable the logs of the library. 43 | 44 | void disableLogs() { 45 | _logger.disableLogs(); 46 | } 47 | 48 | /// This method will enable the logs of the library. 49 | 50 | void enableLogs() { 51 | _logger.enableLogs(); 52 | } 53 | 54 | /// {@macro nostr_client_utils} 55 | late final NostrLogger _logger; 56 | 57 | late final services = NostrServices( 58 | logger: _logger, 59 | ); 60 | } 61 | 62 | mixin LifeCycleManager on Nostr { 63 | /// Clears and frees all the resources used by this instance. 64 | 65 | Future dispose() async { 66 | if (_isDisposed) { 67 | _logger.log('This Nostr instance is already disposed.'); 68 | return true; 69 | } 70 | 71 | _isDisposed = true; 72 | 73 | _logger.log('A Nostr instance disposed successfully.'); 74 | 75 | await Future.wait([ 76 | services.keys.freeAllResources(), 77 | services.relays.freeAllResources(), 78 | ]); 79 | 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/nostr/instance/bech32/bech32.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:bech32/bech32.dart'; 5 | import 'package:dart_nostr/nostr/core/constants.dart'; 6 | import 'package:dart_nostr/nostr/core/utils.dart'; 7 | import 'package:dart_nostr/nostr/instance/tlv/tlv_utils.dart'; 8 | import 'package:dart_nostr/nostr/model/tlv.dart'; 9 | import 'package:hex/hex.dart'; 10 | 11 | class NostrBech32 { 12 | NostrBech32({ 13 | required this.logger, 14 | }); 15 | 16 | /// {@macro nostr_utils} 17 | final tlv = NostrTLV(); 18 | 19 | final NostrLogger logger; 20 | 21 | /// Generates a nprofile id from the given [pubkey] and [relays], if no [relays] are given, it will be an empty list. 22 | /// You can decode the generated nprofile id with [decodeNprofileToMap]. 23 | /// 24 | /// 25 | /// Example: 26 | /// 27 | /// ```dart 28 | /// final nProfileId = Nostr.instance.utilsService.encodePubKeyToNProfile( 29 | /// pubkey: "pubkey in hex format", 30 | /// userRelays: ["relay1", "relay2"], 31 | /// ); 32 | /// 33 | /// print(nProfileId); // ... 34 | /// ``` 35 | String encodeNProfile({ 36 | required String pubkey, 37 | List userRelays = const [], 38 | }) { 39 | final map = {'pubkey': pubkey, 'relays': userRelays}; 40 | 41 | return _nProfileMapToBech32(map); 42 | } 43 | 44 | /// Generates a nprofile id from the given [eventId], [pubkey] and [relays]. 45 | /// You can decode the generated nprofile id with [decodeNeventToMap]. 46 | /// 47 | /// Example: 48 | /// ```dart 49 | /// final nEventId = Nostr.instance.utilsService.encodeNevent( 50 | /// eventId: "event id in hex format", 51 | /// pubkey: "pubkey in hex format", 52 | /// userRelays: ["relay1", "relay2"], 53 | /// ); 54 | /// 55 | /// print(nEventId); // ... 56 | /// ``` 57 | String encodeNevent({ 58 | required String eventId, 59 | required String pubkey, 60 | List userRelays = const [], 61 | }) { 62 | final map = { 63 | 'pubkey': pubkey, 64 | 'relays': userRelays, 65 | 'eventId': eventId, 66 | }; 67 | 68 | return _nEventMapToBech32(map); 69 | } 70 | 71 | /// Decodes the given [bech32] nprofile id to a map with pubkey and relays. 72 | /// You can encode a map to a nprofile id with [encodeNProfile]. 73 | /// 74 | /// Example: 75 | /// ```dart 76 | /// final nProfileDecodedMap = Nostr.instance.utilsService.decodeNprofileToMap( 77 | /// "nprofile1:..." 78 | /// ); 79 | /// 80 | /// print(nProfileDecodedMap); // ... 81 | /// ``` 82 | Map decodeNprofileToMap(String bech32) { 83 | final decodedBech32 = decodeBech32(bech32); 84 | 85 | final dataString = decodedBech32[0]; 86 | final data = HEX.decode(dataString); 87 | 88 | final tlvList = tlv.decode(Uint8List.fromList(data)); 89 | final resultMap = _parseNprofileTlvList(tlvList); 90 | 91 | if (resultMap['pubkey'].length != 64) { 92 | throw Exception('Invalid pubkey length'); 93 | } 94 | 95 | return resultMap; 96 | } 97 | 98 | /// Decodes the given [bech32] nprofile id to a map with pubkey and relays. 99 | /// You can encode a map to a nprofile id with [encodeNProfile]. 100 | /// 101 | /// 102 | /// Example: 103 | /// ```dart 104 | /// final nEventDecodedMap = Nostr.instance.utilsService.decodeNeventToMap( 105 | /// "nevent1:..." 106 | /// ); 107 | /// 108 | /// print(nEventDecodedMap); // ... 109 | /// ``` 110 | Map decodeNeventToMap(String bech32) { 111 | final decodedBech32 = decodeBech32(bech32); 112 | 113 | final dataString = decodedBech32[0]; 114 | final data = HEX.decode(dataString); 115 | 116 | final tlvList = tlv.decode(Uint8List.fromList(data)); 117 | final resultMap = _parseNeventTlvList(tlvList); 118 | 119 | if (resultMap['eventId'].length != 64) { 120 | throw Exception('Invalid pubkey length'); 121 | } 122 | 123 | return resultMap; 124 | } 125 | 126 | /// Encodes a Nostr [publicKey] to an npub key (bech32 encoding). 127 | /// 128 | /// ```dart 129 | /// final npubString = Nostr.instance.services.keys.encodePublicKeyToNpub(yourPublicKey); 130 | /// print(npubString); // ... 131 | /// ``` 132 | String encodePublicKeyToNpub(String publicKey) { 133 | return encodeBech32(publicKey, NostrConstants.npub); 134 | } 135 | 136 | /// Encodes a Nostr [privateKey] to an nsec key (bech32 encoding). 137 | /// ```dart 138 | /// final nsecString = Nostr.instance.services.keys.encodePrivateKeyToNsec(yourPrivateKey); 139 | /// print(nsecString); // ... 140 | /// 141 | String encodePrivateKeyToNsec(String privateKey) { 142 | return encodeBech32(privateKey, NostrConstants.nsec); 143 | } 144 | 145 | /// Decodes a Nostr [npubKey] to a public key. 146 | /// 147 | /// ```dart 148 | /// final publicKey = Nostr.instance.services.keys.decodeNpubKeyToPublicKey(yourNpubKey); 149 | /// print(publicKey); // ... 150 | /// ``` 151 | String decodeNpubKeyToPublicKey(String npubKey) { 152 | assert(npubKey.startsWith(NostrConstants.npub)); 153 | 154 | final decodedKeyComponents = decodeBech32(npubKey); 155 | 156 | return decodedKeyComponents.first; 157 | } 158 | 159 | /// Decodes a Nostr [nsecKey] to a private key. 160 | /// 161 | /// ```dart 162 | /// final privateKey = Nostr.instance.services.keys.decodeNsecKeyToPrivateKey(yourNsecKey); 163 | /// print(privateKey); // ... 164 | /// ``` 165 | String decodeNsecKeyToPrivateKey(String nsecKey) { 166 | assert(nsecKey.startsWith(NostrConstants.nsec)); 167 | final decodedKeyComponents = decodeBech32(nsecKey); 168 | 169 | return decodedKeyComponents.first; 170 | } 171 | 172 | /// expects a map with pubkey and relays and [returns] a bech32 encoded nprofile 173 | String _nProfileMapToBech32(Map map) { 174 | final pubkey = map['pubkey'] as String; 175 | 176 | final relays = List.from(map['relays'] as List); 177 | 178 | final tlvList = _generatenProfileTlvList(pubkey, relays); 179 | 180 | final bytes = tlv.encode(tlvList); 181 | 182 | final dataString = HEX.encode(bytes); 183 | 184 | return encodeBech32( 185 | dataString, 186 | NostrConstants.nProfile, 187 | ); 188 | } 189 | 190 | /// Encodes a [hex] string into a bech32 string with a [hrp] human readable part. 191 | /// 192 | /// ```dart 193 | /// final npubString = Nostr.instance.services.keys.encodeBech32(yourHexString, 'npub'); 194 | /// print(npubString); // ... 195 | /// ``` 196 | String encodeBech32(String hex, String hrp) { 197 | final bytes = HEX.decode(hex); 198 | final fiveBitWords = _convertBits(bytes, 8, 5, true); 199 | 200 | return bech32.encode(Bech32(hrp, fiveBitWords), hex.length + hrp.length); 201 | } 202 | 203 | /// Decodes a bech32 string into a [hex] string and a [hrp] human readable part. 204 | /// 205 | /// ```dart 206 | /// final decodedHexString = Nostr.instance.services.keys.decodeBech32(npubString); 207 | /// print(decodedHexString); // ... 208 | /// ``` 209 | List decodeBech32(String bech32String) { 210 | const codec = Bech32Codec(); 211 | final bech32 = codec.decode(bech32String, bech32String.length); 212 | final eightBitWords = _convertBits(bech32.data, 5, 8, false); 213 | return [HEX.encode(eightBitWords), bech32.hrp]; 214 | } 215 | 216 | String _nEventMapToBech32(Map map) { 217 | final eventId = map['eventId'] as String; 218 | final authorPubkey = map['pubkey'] as String?; 219 | final relays = List.from(map['relays'] as List); 220 | 221 | final tlvList = _generatenEventTlvList( 222 | eventId, 223 | authorPubkey, 224 | relays, 225 | ); 226 | 227 | final dataString = HEX.encode(tlv.encode(tlvList)); 228 | 229 | return encodeBech32( 230 | dataString, 231 | NostrConstants.nEvent, 232 | ); 233 | } 234 | 235 | Map _parseNprofileTlvList(List tlvList) { 236 | var pubkey = ''; 237 | final relays = []; 238 | 239 | for (final tlv in tlvList) { 240 | if (tlv.type == 0) { 241 | pubkey = HEX.encode(tlv.value); 242 | } else if (tlv.type == 1) { 243 | relays.add(ascii.decode(tlv.value)); 244 | } 245 | } 246 | return {'pubkey': pubkey, 'relays': relays}; 247 | } 248 | 249 | Map _parseNeventTlvList(List tlvList) { 250 | var pubkey = ''; 251 | final relays = []; 252 | var eventId = ''; 253 | for (final tlv in tlvList) { 254 | if (tlv.type == 0) { 255 | eventId = HEX.encode(tlv.value); 256 | } else if (tlv.type == 1) { 257 | relays.add(ascii.decode(tlv.value)); 258 | } else if (tlv.type == 2) { 259 | pubkey = HEX.encode(tlv.value); 260 | } 261 | } 262 | 263 | return {'eventId': eventId, 'pubkey': pubkey, 'relays': relays}; 264 | } 265 | 266 | /// Generates a list of TLV objects 267 | List _generatenEventTlvList( 268 | String eventId, 269 | String? authorPubkey, 270 | List relays, 271 | ) { 272 | final tlvList = []; 273 | tlvList.add(_generateEventIdTlv(eventId)); 274 | 275 | tlvList.addAll(relays.map(_generateRelayTlv)); 276 | 277 | if (authorPubkey != null) { 278 | tlvList.add(_generateAuthorPubkeyTlv(authorPubkey)); 279 | } 280 | 281 | return tlvList; 282 | } 283 | 284 | /// TLV type 1 285 | /// [relay] must be a string 286 | TLV _generateRelayTlv(String relay) { 287 | final relayBytes = Uint8List.fromList(ascii.encode(relay)); 288 | return TLV(type: 1, length: relayBytes.length, value: relayBytes); 289 | } 290 | 291 | /// TLV type 2 292 | /// [authorPubkey] must be 32 bytes long 293 | TLV _generateAuthorPubkeyTlv(String authorPubkey) { 294 | final authorPubkeyBytes = Uint8List.fromList(HEX.decode(authorPubkey)); 295 | 296 | return TLV(type: 2, length: 32, value: authorPubkeyBytes); 297 | } 298 | 299 | /// TLV type 0 300 | /// [eventId] must be 32 bytes long 301 | TLV _generateEventIdTlv(String eventId) { 302 | final eventIdBytes = Uint8List.fromList(HEX.decode(eventId)); 303 | return TLV(type: 0, length: 32, value: eventIdBytes); 304 | } 305 | 306 | List _generatenProfileTlvList(String pubkey, List relays) { 307 | final pubkeyBytes = _hexDecodeToUint8List(pubkey); 308 | final tlvList = [TLV(type: 0, length: 32, value: pubkeyBytes)]; 309 | 310 | for (final relay in relays) { 311 | final relayBytes = _asciiEncodeToUint8List(relay); 312 | tlvList.add(TLV(type: 1, length: relayBytes.length, value: relayBytes)); 313 | } 314 | 315 | return tlvList; 316 | } 317 | 318 | Uint8List _hexDecodeToUint8List(String hexString) { 319 | return Uint8List.fromList(HEX.decode(hexString)); 320 | } 321 | 322 | Uint8List _asciiEncodeToUint8List(String asciiString) { 323 | return Uint8List.fromList(ascii.encode(asciiString)); 324 | } 325 | 326 | /// Convert bits from one base to another 327 | /// [data] - the data to convert 328 | /// [fromBits] - the number of bits per input value 329 | /// [toBits] - the number of bits per output value 330 | /// [pad] - whether to pad the output if there are not enough bits 331 | /// If pad is true, and there are remaining bits after the conversion, then the remaining bits are left-shifted and added to the result 332 | /// [return] - the converted data 333 | List _convertBits(List data, int fromBits, int toBits, bool pad) { 334 | var acc = 0; 335 | var bits = 0; 336 | final result = []; 337 | 338 | for (final value in data) { 339 | acc = (acc << fromBits) | value; 340 | bits += fromBits; 341 | 342 | while (bits >= toBits) { 343 | bits -= toBits; 344 | result.add((acc >> bits) & ((1 << toBits) - 1)); 345 | } 346 | } 347 | 348 | if (pad) { 349 | if (bits > 0) { 350 | result.add((acc << (toBits - bits)) & ((1 << toBits) - 1)); 351 | } 352 | } else if (bits >= fromBits || (acc & ((1 << bits) - 1)) != 0) { 353 | throw Exception('Invalid padding'); 354 | } 355 | 356 | return result; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /lib/nostr/instance/keys/keys.dart: -------------------------------------------------------------------------------- 1 | import 'package:bip32_bip44/dart_bip32_bip44.dart' as bip32_bip44; 2 | import 'package:bip39/bip39.dart' as bip39; 3 | import 'package:dart_nostr/nostr/core/key_pairs.dart'; 4 | import 'package:dart_nostr/nostr/core/utils.dart'; 5 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 6 | 7 | /// {@template nostr_keys} 8 | /// This class is responsible for generating key pairs and deriving public keys from private keys.. 9 | /// {@endtemplate} 10 | class NostrKeys { 11 | NostrKeys({ 12 | required this.logger, 13 | }); 14 | 15 | /// {@macro nostr_client_utils} 16 | final NostrLogger logger; 17 | 18 | /// A caching system for the key pairs, so we don't have to generate them again. 19 | /// A cache key is the private key, and the value is the [NostrKeyPairs] instance. 20 | static final _keyPairsCache = {}; 21 | 22 | /// Derives a public key from a [privateKey] directly, use this if you want a quick way to get a public key from a private key. 23 | /// 24 | /// 25 | /// ```dart 26 | /// final publicKey = Nostr.instance.services.keys.derivePublicKey(privateKey: yourPrivateKey); 27 | /// print(publicKey); // ... 28 | /// ``` 29 | 30 | String derivePublicKey({required String privateKey}) { 31 | final nostrKeyPairs = _keyPairFrom(privateKey); 32 | 33 | logger.log( 34 | "derived public key from private key, with it's value is: ${nostrKeyPairs.public}", 35 | ); 36 | 37 | return nostrKeyPairs.public; 38 | } 39 | 40 | /// You can use this method to generate a key pair for your end users. 41 | /// 42 | /// 43 | /// ```dart 44 | /// final keyPair = Nostr.instance.services.keys.generateKeyPair(); 45 | /// print(keyPair.public); // ... 46 | /// print(keyPair.private); // ... 47 | /// ``` 48 | 49 | NostrKeyPairs generateKeyPair() { 50 | final nostrKeyPairs = _generateKeyPair(); 51 | 52 | logger.log( 53 | "generated key pairs, with it's public key is: ${nostrKeyPairs.public}", 54 | ); 55 | 56 | return nostrKeyPairs; 57 | } 58 | 59 | /// Generates a key pair from an existing [privateKey], use this if you want to generate a key pair from an existing private key. 60 | /// 61 | /// ```dart 62 | /// final keyPair = Nostr.instance.services.keys.generateKeyPairFromExistingPrivateKey(yourPrivateKey); 63 | /// print(keyPair.public); // ... 64 | /// print(keyPair.private); // ... 65 | /// ``` 66 | 67 | NostrKeyPairs generateKeyPairFromExistingPrivateKey( 68 | String privateKey, 69 | ) { 70 | return _keyPairFrom(privateKey); 71 | } 72 | 73 | /// You can use this method to generate a key pair for your end users. 74 | /// it returns the private key of the generated key pair. 75 | /// 76 | /// ```dart 77 | /// final privateKey = Nostr.instance.services.keys.generatePrivateKey(); 78 | /// print(privateKey); // ... 79 | /// ``` 80 | 81 | String generatePrivateKey() { 82 | return _generateKeyPair().private; 83 | } 84 | 85 | /// You can use this method to sign a [message] with a [privateKey]. 86 | /// 87 | /// ```dart 88 | /// final signature = Nostr.instance.services.keys.sign( 89 | /// privateKey: yourPrivateKey, 90 | /// message: yourMessage, 91 | /// ); 92 | /// 93 | /// print(signature); // ... 94 | /// ``` 95 | 96 | String sign({ 97 | required String privateKey, 98 | required String message, 99 | }) { 100 | final nostrKeyPairs = _keyPairFrom(privateKey); 101 | 102 | final hexEncodedMessage = 103 | Nostr.instance.services.utils.hexEncodeString(message); 104 | 105 | final signature = nostrKeyPairs.sign(hexEncodedMessage); 106 | 107 | logger.log( 108 | "signed message with private key, with it's value is: $signature", 109 | ); 110 | 111 | return signature; 112 | } 113 | 114 | /// You can use this method to verify a [message] with a [publicKey] and it's [signature]. 115 | /// it returns a [bool] that indicates if the [message] is verified or not. 116 | /// 117 | /// ```dart 118 | /// final isVerified = Nostr.instance.services.keys.verify( 119 | /// publicKey: yourPublicKey, 120 | /// message: yourMessage, 121 | /// signature: yourSignature, 122 | /// ); 123 | /// 124 | /// print(isVerified); // ... 125 | /// ``` 126 | bool verify({ 127 | required String publicKey, 128 | required String message, 129 | required String signature, 130 | }) { 131 | final hexEncodedMessage = 132 | Nostr.instance.services.utils.hexEncodeString(message); 133 | final isVerified = 134 | NostrKeyPairs.verify(publicKey, hexEncodedMessage, signature); 135 | 136 | logger.log( 137 | "verified message with public key: $publicKey, with it's value is: $isVerified", 138 | ); 139 | 140 | return isVerified; 141 | } 142 | 143 | /// Weither the [key] is a valid Nostr private key or not. 144 | /// 145 | /// ```dart 146 | /// Nostr.instance.services.keys.isValidPrivateKey('something that is not a key'); // false 147 | /// ``` 148 | 149 | bool isValidPrivateKey(String key) { 150 | return NostrKeyPairs.isValidPrivateKey(key); 151 | } 152 | 153 | /// Wether the given [text] is a valid mnemonic or not. 154 | /// 155 | /// ```dart 156 | /// final isValid = Nostr.instance.services.keys.isMnemonicValid('your mnemonic'); 157 | /// print(isValid); // ... 158 | /// ``` 159 | static bool isMnemonicValid(String text) { 160 | return bip39.validateMnemonic(text); 161 | } 162 | 163 | /// Derives a private key from a [mnemonic] directly, use this if you want a quick way to get a private key from a mnemonic. 164 | /// 165 | /// ```dart 166 | /// final privateKey = Nostr.instance.services.keys.getPrivateKeyFromMnemonic('your mnemonic'); 167 | /// print(privateKey); // ... 168 | /// ``` 169 | static String getPrivateKeyFromMnemonic(String mnemonic) { 170 | final seed = bip39.mnemonicToSeedHex(mnemonic); 171 | final chain = bip32_bip44.Chain.seed(seed); 172 | 173 | final key = 174 | chain.forPath("m/44'/1237'/0'/0") as bip32_bip44.ExtendedPrivateKey; 175 | 176 | final childKey = bip32_bip44.deriveExtendedPrivateChildKey(key, 0); 177 | 178 | var hexChildKey = ''; 179 | 180 | if (childKey.key != null) { 181 | hexChildKey = childKey.key!.toRadixString(16); 182 | } 183 | 184 | return hexChildKey; 185 | } 186 | 187 | /// Clears all the cached key pairs. 188 | Future freeAllResources() async { 189 | _keyPairsCache.clear(); 190 | 191 | return true; 192 | } 193 | 194 | /// Creates a [NostrKeyPairs] from a [privateKey] if it's not already cached, and returns it. 195 | /// if it's already cached, it returns the cached [NostrKeyPairs] instance and saves the regeneration time and resources. 196 | NostrKeyPairs _keyPairFrom(String privateKey) { 197 | if (_keyPairsCache[privateKey] != null) { 198 | return _keyPairsCache[privateKey]!; 199 | } else { 200 | _keyPairsCache[privateKey] = NostrKeyPairs(private: privateKey); 201 | 202 | return _keyPairsCache[privateKey]!; 203 | } 204 | } 205 | 206 | /// Generates a [NostrKeyPairs] and caches it, and returns it. 207 | NostrKeyPairs _generateKeyPair() { 208 | final keyPair = NostrKeyPairs.generate(); 209 | _keyPairsCache[keyPair.private] = keyPair; 210 | 211 | return keyPair; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /lib/nostr/instance/registry.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dart_nostr/nostr/core/exceptions.dart'; 4 | import 'package:dart_nostr/nostr/core/utils.dart'; 5 | import 'package:dart_nostr/nostr/model/count.dart'; 6 | import 'package:dart_nostr/nostr/model/ease.dart'; 7 | import 'package:dart_nostr/nostr/model/event/event.dart'; 8 | import 'package:dart_nostr/nostr/model/ok.dart'; 9 | import 'package:meta/meta.dart'; 10 | import 'package:web_socket_channel/web_socket_channel.dart'; 11 | 12 | typedef SubscriptionCallback 13 | = Map; 14 | 15 | typedef RelayCallbackRegister = Map>; 16 | 17 | /// {@template nostr_registry} 18 | /// This is responsible for registering and retrieving relays [WebSocket]s that are connected to the app. 19 | /// {@endtemplate} 20 | @protected 21 | class NostrRegistry { 22 | /// {@macro nostr_registry} 23 | NostrRegistry({required this.logger}); 24 | 25 | final NostrLogger logger; 26 | 27 | /// This is the registry which will have all relays [WebSocket]s. 28 | final relaysWebSocketsRegistry = {}; 29 | 30 | /// This is the registry which will have all events. 31 | final eventsRegistry = {}; 32 | 33 | /// This is the registry which will have all ok commands callbacks. 34 | final okCommandCallBacks = RelayCallbackRegister(); 35 | 36 | /// This is the registry which will have all eose responses callbacks. 37 | final eoseCommandCallBacks = RelayCallbackRegister(); 38 | 39 | /// This is the registry which will have all count responses callbacks. 40 | final countResponseCallBacks = RelayCallbackRegister(); 41 | 42 | /// Registers a [WebSocket] to the registry with the given [relayUrl]. 43 | /// If a [WebSocket] is already registered with the given [relayUrl], it will be replaced. 44 | WebSocketChannel registerRelayWebSocket({ 45 | required String relayUrl, 46 | required WebSocketChannel webSocket, 47 | }) { 48 | relaysWebSocketsRegistry[relayUrl] = webSocket; 49 | return relaysWebSocketsRegistry[relayUrl]!; 50 | } 51 | 52 | /// Returns the [WebSocket] registered with the given [relayUrl]. 53 | WebSocketChannel? getRelayWebSocket({ 54 | required String relayUrl, 55 | }) { 56 | final targetWebSocket = relaysWebSocketsRegistry[relayUrl]; 57 | 58 | if (targetWebSocket != null) { 59 | final relay = targetWebSocket; 60 | 61 | return relay; 62 | } else { 63 | logger.log( 64 | 'No relay is registered with the given url: $relayUrl, did you forget to register it?', 65 | ); 66 | 67 | throw RelayNotFoundException(relayUrl); 68 | } 69 | } 70 | 71 | /// Returns all [WebSocket]s registered in the registry. 72 | List> allRelaysEntries() { 73 | return relaysWebSocketsRegistry.entries.toList(); 74 | } 75 | 76 | /// Clears all registries. 77 | void clear() { 78 | relaysWebSocketsRegistry.clear(); 79 | eventsRegistry.clear(); 80 | okCommandCallBacks.clear(); 81 | eoseCommandCallBacks.clear(); 82 | countResponseCallBacks.clear(); 83 | } 84 | 85 | /// Wether a [WebSocket] is registered with the given [relayUrl]. 86 | bool isRelayRegistered(String relayUrl) { 87 | return relaysWebSocketsRegistry.containsKey(relayUrl); 88 | } 89 | 90 | /// Wether an event is registered with the given [event]. 91 | bool isEventRegistered(NostrEvent event) { 92 | return eventsRegistry.containsKey(eventUniqueId(event)); 93 | } 94 | 95 | /// Registers an event to the registry with the given [event]. 96 | NostrEvent registerEvent(NostrEvent event) { 97 | eventsRegistry[eventUniqueId(event)] = event; 98 | 99 | return eventsRegistry[eventUniqueId(event)]!; 100 | } 101 | 102 | /// REturns an [event] unique id, See also [NostrEvent.uniqueKey]. 103 | String eventUniqueId(NostrEvent event) { 104 | return event.uniqueKey().toString(); 105 | } 106 | 107 | /// Removes an event from the registry with the given [event]. 108 | bool unregisterRelay(String relay) { 109 | final isUnregistered = relaysWebSocketsRegistry.remove(relay) != null; 110 | 111 | return isUnregistered; 112 | } 113 | 114 | /// Registers an ok command callback to the registry with the given [associatedEventId]. 115 | void registerOkCommandCallBack({ 116 | required String associatedEventId, 117 | required void Function(String relay, NostrEventOkCommand ok) onOk, 118 | required String relay, 119 | }) { 120 | final relayOkRegister = getOrCreateRegister(okCommandCallBacks, relay); 121 | 122 | relayOkRegister[associatedEventId] = onOk; 123 | } 124 | 125 | /// Returns an ok command callback from the registry with the given [associatedEventId]. 126 | void Function( 127 | String relay, 128 | NostrEventOkCommand ok, 129 | )? getOkCommandCallBack({ 130 | required String associatedEventIdWithOkCommand, 131 | required String relay, 132 | }) { 133 | final relayOkRegister = getOrCreateRegister(okCommandCallBacks, relay); 134 | 135 | return relayOkRegister[associatedEventIdWithOkCommand]; 136 | } 137 | 138 | /// Registers an eose command callback to the registry with the given [subscriptionId]. 139 | void registerEoseCommandCallBack({ 140 | required String subscriptionId, 141 | required void Function(String relay, NostrRequestEoseCommand eose) onEose, 142 | required String relay, 143 | }) { 144 | final relayEoseRegister = getOrCreateRegister(eoseCommandCallBacks, relay); 145 | 146 | relayEoseRegister[subscriptionId] = onEose; 147 | } 148 | 149 | /// Returns an eose command callback from the registry with the given [subscriptionId]. 150 | void Function( 151 | String relay, 152 | NostrRequestEoseCommand eose, 153 | )? getEoseCommandCallBack({ 154 | required String subscriptionId, 155 | required String relay, 156 | }) { 157 | final relayEoseRegister = getOrCreateRegister(eoseCommandCallBacks, relay); 158 | 159 | return relayEoseRegister[subscriptionId]; 160 | } 161 | 162 | /// Registers a count response callback to the registry with the given [subscriptionId]. 163 | void registerCountResponseCallBack({ 164 | required String subscriptionId, 165 | required void Function(String relay, NostrCountResponse countResponse) 166 | onCountResponse, 167 | required String relay, 168 | }) { 169 | final relayCountRegister = countResponseCallBacks[subscriptionId]; 170 | 171 | relayCountRegister?[subscriptionId] = onCountResponse; 172 | } 173 | 174 | /// Returns a count response callback from the registry with the given [subscriptionId]. 175 | void Function( 176 | String relay, 177 | NostrCountResponse countResponse, 178 | )? getCountResponseCallBack({ 179 | required String subscriptionId, 180 | required String relay, 181 | }) { 182 | final relayCountRegister = 183 | getOrCreateRegister(countResponseCallBacks, relay); 184 | 185 | return relayCountRegister[subscriptionId]; 186 | } 187 | 188 | /// Clears the events registry. 189 | void clearWebSocketsRegistry() { 190 | relaysWebSocketsRegistry.clear(); 191 | } 192 | 193 | SubscriptionCallback getOrCreateRegister( 194 | RelayCallbackRegister register, 195 | String relay, 196 | ) { 197 | final relayRegister = register[relay]; 198 | 199 | if (relayRegister == null) { 200 | register[relay] = {}; 201 | } 202 | 203 | return register[relay]!; 204 | } 205 | 206 | bool isRelayRegisteredAndConnectedSuccesfully(String relay) { 207 | final relayWebSocket = relaysWebSocketsRegistry[relay]; 208 | 209 | if (relayWebSocket == null) { 210 | return false; 211 | } 212 | 213 | if (relayWebSocket.closeCode == null) { 214 | return true; 215 | } else { 216 | return false; 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /lib/nostr/instance/relays/base/relays.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/nostr/model/count.dart'; 2 | import 'package:dart_nostr/nostr/model/ease.dart'; 3 | import 'package:dart_nostr/nostr/model/event/event.dart'; 4 | import 'package:dart_nostr/nostr/model/nostr_events_stream.dart'; 5 | import 'package:dart_nostr/nostr/model/ok.dart'; 6 | import 'package:dart_nostr/nostr/model/relay_informations.dart'; 7 | import 'package:dart_nostr/nostr/model/request/request.dart'; 8 | import 'package:web_socket_channel/web_socket_channel.dart'; 9 | 10 | abstract class NostrRelaysBase { 11 | // Stream get eventsStream; 12 | // Stream get noticesStream; 13 | Map get relaysWebSocketsRegistry; 14 | Map get eventsRegistry; 15 | 16 | List? relaysList; 17 | 18 | init({ 19 | required List relaysUrl, 20 | void Function( 21 | String relayUrl, 22 | dynamic receivedData, 23 | WebSocketChannel? relayWebSocket, 24 | )? onRelayListening, 25 | void Function( 26 | String relayUrl, 27 | Object? error, 28 | WebSocketChannel? relayWebSocket, 29 | )? onRelayConnectionError, 30 | void Function( 31 | String relayUrl, 32 | WebSocketChannel? relayWebSocket, 33 | )? onRelayConnectionDone, 34 | bool lazyListeningToRelays = false, 35 | bool retryOnError = false, 36 | bool retryOnClose = false, 37 | }); 38 | 39 | @override 40 | Future sendEventToRelaysAsync( 41 | NostrEvent event, { 42 | required Duration timeout, 43 | }); 44 | 45 | void sendEventToRelays( 46 | NostrEvent event, { 47 | void Function(String relay, NostrEventOkCommand ok)? onOk, 48 | }); 49 | 50 | void sendCountEventToRelays( 51 | NostrCountEvent countEvent, { 52 | required void Function(String relay, NostrCountResponse countResponse) 53 | onCountResponse, 54 | }); 55 | 56 | Future sendCountEventToRelaysAsync( 57 | NostrCountEvent countEvent, { 58 | required Duration timeout, 59 | }); 60 | 61 | NostrEventsStream startEventsSubscription({ 62 | required NostrRequest request, 63 | void Function(String relay, NostrRequestEoseCommand ease)? onEose, 64 | bool useConsistentSubscriptionIdBasedOnRequestData = false, 65 | }); 66 | 67 | Future> startEventsSubscriptionAsync({ 68 | required NostrRequest request, 69 | required Duration timeout, 70 | void Function(String relay, NostrRequestEoseCommand ease)? onEose, 71 | bool useConsistentSubscriptionIdBasedOnRequestData = false, 72 | bool shouldThrowErrorOnTimeoutWithoutEose = true, 73 | }); 74 | 75 | void closeEventsSubscription(String subscriptionId, [String? relayUrl]); 76 | 77 | void startListeningToRelay({ 78 | required String relay, 79 | required void Function( 80 | String relayUrl, 81 | dynamic receivedData, 82 | WebSocketChannel? relayWebSocket, 83 | )? onRelayListening, 84 | required void Function( 85 | String relayUrl, 86 | Object? error, 87 | WebSocketChannel? relayWebSocket, 88 | )? onRelayConnectionError, 89 | required void Function(String relayUrl, WebSocketChannel? relayWebSocket)? 90 | onRelayConnectionDone, 91 | required bool retryOnError, 92 | required bool retryOnClose, 93 | required bool shouldReconnectToRelayOnNotice, 94 | required Duration connectionTimeout, 95 | required bool ignoreConnectionException, 96 | required bool lazyListeningToRelays, 97 | }); 98 | 99 | Future relayInformationsDocumentNip11({ 100 | required String relayUrl, 101 | bool throwExceptionIfExists, 102 | }); 103 | Future reconnectToRelays({ 104 | required void Function( 105 | String relayUrl, 106 | dynamic receivedData, 107 | WebSocketChannel? relayWebSocket, 108 | )? onRelayListening, 109 | required void Function( 110 | String relayUrl, 111 | Object? error, 112 | WebSocketChannel? relayWebSocket, 113 | )? onRelayConnectionError, 114 | required void Function(String relayUrl, WebSocketChannel? relayWebSocket)? 115 | onRelayConnectionDone, 116 | required bool retryOnError, 117 | required bool retryOnClose, 118 | required bool shouldReconnectToRelayOnNotice, 119 | required Duration connectionTimeout, 120 | required bool ignoreConnectionException, 121 | required bool lazyListeningToRelays, 122 | bool relayUnregistered = true, 123 | }); 124 | 125 | Future freeAllResources(); 126 | } 127 | -------------------------------------------------------------------------------- /lib/nostr/instance/streams.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dart_nostr/dart_nostr.dart'; 4 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 5 | import 'package:dart_nostr/nostr/model/export.dart'; 6 | 7 | /// {@template nostr_streams_controllers} 8 | /// A service that manages the relays streams messages 9 | /// {@endtemplate} 10 | class NostrStreamsControllers { 11 | /// This is the controller which will receive all events from all relays. 12 | final eventsController = StreamController.broadcast(); 13 | 14 | /// This is the controller which will receive all notices from all relays. 15 | final noticesController = StreamController.broadcast(); 16 | 17 | /// This is the stream which will have all events from all relays, all your sent requests will be included in this stream, and so in order to filter them, you will need to use the [Stream.where] method. 18 | /// ```dart 19 | /// Nostr.instance.relays.stream.where((event) { 20 | /// return event.subscriptionId == "your_subscription_id"; 21 | /// }); 22 | /// ``` 23 | /// 24 | /// You can also use the [Nostr.startEventsSubscription] method to get a stream of events that will be filtered by the [subscriptionId] that you passed to it automatically. 25 | Stream get events => eventsController.stream; 26 | 27 | /// This is the stream which will have all notices from all relays, all of them will be included in this stream, and so in order to filter them, you will need to use the [Stream.where] method. 28 | Stream get notices => noticesController.stream; 29 | 30 | /// Closes all streams. 31 | Future close() async { 32 | await Future.wait([ 33 | eventsController.close(), 34 | noticesController.close(), 35 | ]); 36 | } 37 | 38 | bool get isClosed { 39 | return eventsController.isClosed || noticesController.isClosed; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/nostr/instance/tlv/base/base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:dart_nostr/nostr/model/tlv.dart'; 3 | 4 | abstract class TLVBase { 5 | List decode(Uint8List data); 6 | Uint8List encode(List tlvList); 7 | } 8 | -------------------------------------------------------------------------------- /lib/nostr/instance/tlv/tlv_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:dart_nostr/nostr/instance/tlv/base/base.dart'; 4 | import 'package:dart_nostr/nostr/model/tlv.dart'; 5 | 6 | /// {@template nostr_tlv} 7 | /// This class is responsible for handling the tlv. 8 | /// {@endtemplate} 9 | class NostrTLV implements TLVBase { 10 | /// Decode list bytes to list tlv model 11 | @override 12 | List decode(Uint8List data) { 13 | final tlvList = []; 14 | var offset = 0; 15 | while (offset < data.length) { 16 | final type = data[offset++]; 17 | final length = _decodeLength(data, offset); 18 | offset += _getLengthBytes(length); 19 | final value = data.sublist(offset, offset + length); 20 | offset += length; 21 | tlvList.add(TLV(type: type, length: length, value: value)); 22 | } 23 | return tlvList; 24 | } 25 | 26 | /// Decode length from list bytes 27 | int _decodeLength(Uint8List buffer, int offset) { 28 | var length = buffer[offset] & 255; 29 | if ((length & 128) == 128) { 30 | final numberOfBytes = length & 127; 31 | if (numberOfBytes > 3) { 32 | throw Exception('Invalid length'); 33 | } 34 | length = 0; 35 | for (var i = offset + 1; i < offset + 1 + numberOfBytes; ++i) { 36 | length = length * 256 + (buffer[i] & 255); 37 | } 38 | } 39 | return length; 40 | } 41 | 42 | int _getLengthBytes(int length) { 43 | return (length & 128) == 128 ? 1 + (length & 127) : 1; 44 | } 45 | 46 | /// Encode list tlv to list bytes 47 | @override 48 | Uint8List encode(List tlvList) { 49 | final byteLists = []; 50 | for (final tlv in tlvList) { 51 | final typeBytes = Uint8List.fromList([tlv.type]); 52 | final lengthBytes = _encodeLength(tlv.value.length); 53 | byteLists.addAll([typeBytes, lengthBytes, tlv.value]); 54 | } 55 | return _concatenateUint8List(byteLists); 56 | } 57 | 58 | /// Encode length to list bytes 59 | Uint8List _encodeLength(int length) { 60 | if (length < 128) { 61 | return Uint8List.fromList([length]); 62 | } 63 | final lengthBytesList = [0x82 | (length >> 8), length & 0xFF]; 64 | return Uint8List.fromList(lengthBytesList); 65 | } 66 | 67 | /// concatenate/chain list bytes 68 | Uint8List _concatenateUint8List(List lists) { 69 | final totalLength = 70 | lists.map((list) => list.length).reduce((a, b) => a + b); 71 | final result = Uint8List(totalLength); 72 | var offset = 0; 73 | for (final list in lists) { 74 | result.setRange(offset, offset + list.length, list); 75 | offset += list.length; 76 | } 77 | return result; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/nostr/instance/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | 4 | import 'package:convert/convert.dart'; 5 | import 'package:crypto/crypto.dart'; 6 | import 'package:dart_nostr/nostr/core/exceptions.dart'; 7 | import 'package:dart_nostr/nostr/core/utils.dart'; 8 | 9 | import 'package:http/http.dart' as http; 10 | 11 | /// {@template nostr_utils} 12 | /// This class is responsible for handling some of the helper utils of the library. 13 | /// {@endtemplate} 14 | class NostrUtils { 15 | /// {@macro nostr_utils} 16 | NostrUtils({ 17 | required this.logger, 18 | }); 19 | 20 | /// {@macro nostr_client_utils} 21 | final NostrLogger logger; 22 | 23 | /// Wether the given [identifier] has a valid format. 24 | /// 25 | /// 26 | /// Example: 27 | /// 28 | /// ```dart 29 | /// final isIdentifierValid = Nostr.instance.utilsService.isValidNip05Identifier("example"); 30 | /// print(isIdentifierValid) // false 31 | /// ``` 32 | 33 | bool isValidNip05Identifier(String identifier) { 34 | final emailRegEx = 35 | RegExp(r'^[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9_\-\.]+\.[a-zA-Z]+$'); 36 | 37 | return emailRegEx.hasMatch(identifier); 38 | } 39 | 40 | /// Encodes the given [input] to hex format 41 | /// 42 | /// 43 | /// Example: 44 | /// 45 | /// ```dart 46 | /// final hexDecodedString = Nostr.instance.utilsService.hexEncodeString("example"); 47 | /// print(hexDecodedString); // ... 48 | /// ``` 49 | 50 | String hexEncodeString(String input) { 51 | return hex.encode(utf8.encode(input)); 52 | } 53 | 54 | /// Generates a randwom 64-length hexadecimal string. 55 | /// 56 | /// 57 | /// Example: 58 | /// 59 | /// ```dart 60 | /// final randomGeneratedHex = Nostr.instance.utilsService.random64HexChars(); 61 | /// print(randomGeneratedHex); // ... 62 | /// ``` 63 | 64 | String random64HexChars() { 65 | final random = Random.secure(); 66 | final randomBytes = List.generate(32, (i) => random.nextInt(256)); 67 | 68 | return hex.encode(randomBytes); 69 | } 70 | 71 | /// Generates a random 64 length hexadecimal string that is consistent with the given [input]. 72 | 73 | String consistent64HexChars(String input) { 74 | final randomBytes = utf8.encode(input); 75 | final hashed = sha256.convert(randomBytes); 76 | 77 | return hex.encode(hashed.bytes); 78 | } 79 | 80 | /// This method will verify the [internetIdentifier] with a [pubKey] using the NIP05 implementation, and simply will return a [Future] with a [bool] that indicates if the verification was successful or not. 81 | /// 82 | /// example: 83 | /// ```dart 84 | /// final verified = await Nostr.instance.relays.verifyNip05( 85 | /// internetIdentifier: "localPart@domainPart", 86 | /// pubKey: "pub key in hex format", 87 | /// ); 88 | /// ``` 89 | 90 | Future verifyNip05({ 91 | required String internetIdentifier, 92 | required String pubKey, 93 | }) async { 94 | assert( 95 | pubKey.length == 64 || !pubKey.startsWith('npub'), 96 | 'pub key is invalid, it must be in hex format and not a npub(nip19) key!', 97 | ); 98 | assert( 99 | internetIdentifier.contains('@') && 100 | internetIdentifier.split('@').length == 2, 101 | 'invalid internet identifier', 102 | ); 103 | 104 | try { 105 | final pubKeyFromResponse = await pubKeyFromIdentifierNip05( 106 | internetIdentifier: internetIdentifier, 107 | ); 108 | 109 | return pubKey == pubKeyFromResponse; 110 | } catch (e) { 111 | logger.log( 112 | 'error while verifying nip05 for internet identifier: $internetIdentifier', 113 | e, 114 | ); 115 | rethrow; 116 | } 117 | } 118 | 119 | /// Return the public key found by the NIP05 implementation via the given for the given [internetIdentifier] 120 | /// 121 | /// 122 | /// Example: 123 | /// ```dart 124 | /// final pubKey = await Nostr.instance.relays.pubKeyFromIdentifierNip05( 125 | /// internetIdentifier: "localPart@domainPart", 126 | /// ); 127 | /// 128 | /// print(pubKey); // ... 129 | /// ``` 130 | 131 | Future pubKeyFromIdentifierNip05({ 132 | required String internetIdentifier, 133 | }) async { 134 | try { 135 | final localPart = internetIdentifier.split('@')[0]; 136 | final domainPart = internetIdentifier.split('@')[1]; 137 | 138 | logger.log( 139 | 'Attempt to fetch pubkey for $internetIdentifier from $domainPart', 140 | ); 141 | 142 | final res = await http.get( 143 | Uri.parse('https://$domainPart/.well-known/nostr.json?name=$localPart'), 144 | ); 145 | 146 | final decoded = jsonDecode(res.body) as Map; 147 | if (decoded 148 | case { 149 | 'names': final names as Map, 150 | }) { 151 | logger.log( 152 | 'Pubkey for $localPart is ${names[localPart] ?? 'not found'} ' 153 | 'at $domainPart', 154 | ); 155 | 156 | return names[localPart] as String?; 157 | } 158 | 159 | return null; 160 | } on Exception catch (e) { 161 | logger.log( 162 | 'error while verifying nip05 for internet identifier: ' 163 | '$internetIdentifier', 164 | e, 165 | ); 166 | 167 | throw Nip05VerificationException(parent: e); 168 | } 169 | } 170 | 171 | /// Counts the difficulty of the given [hexString], this wis intebded t be used in the NIP 13 with this package. 172 | /// 173 | /// Example: 174 | /// ```dart 175 | /// final difficulty = Nostr.instance.utilsService.countDifficultyOfHex("002f"); 176 | /// print(difficulty); // 36 177 | /// ``` 178 | /// 179 | 180 | int countDifficultyOfHex(String hexString) { 181 | final idChars = hexString.split(''); 182 | 183 | // encode to bits. 184 | var idCharsBinary = idChars.map((char) { 185 | final charCode = int.parse(char, radix: 16); 186 | final charBinary = charCode.toRadixString(2); 187 | return charBinary; 188 | }).toList(); 189 | 190 | idCharsBinary = idCharsBinary.map((charBinary) { 191 | final charBinaryLength = charBinary.length; 192 | final charBinaryLengthDiff = 4 - charBinaryLength; 193 | final charBinaryPadded = 194 | charBinary.padLeft(charBinaryLength + charBinaryLengthDiff, '0'); 195 | return charBinaryPadded; 196 | }).toList(); 197 | 198 | return idCharsBinary.join().split('1').first.length; 199 | } 200 | 201 | // String _convertBech32toHr(String bech32, {int cutLength = 15}) { 202 | // final int length = bech32.length; 203 | // final String first = bech32.substring(0, cutLength); 204 | // final String last = bech32.substring(length - cutLength, length); 205 | // return "$first:$last"; 206 | // } 207 | 208 | /// [returns] a short version nprofile1:sdf54e:ewfd54 209 | // String _nProfileMapToBech32Hr(Map map) { 210 | // return _convertBech32toHr(_nProfileMapToBech32(map)); 211 | // } 212 | } 213 | -------------------------------------------------------------------------------- /lib/nostr/instance/web_sockets.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/nostr/core/utils.dart'; 2 | import 'package:web_socket_channel/web_socket_channel.dart'; 3 | 4 | /// {@template nostr_web_sockets_service} 5 | /// A service that manages the relays web sockets connections 6 | /// {@endtemplate} 7 | class NostrWebSocketsService { 8 | /// {@macro nostr_web_sockets_service} 9 | NostrWebSocketsService({ 10 | required this.logger, 11 | }); 12 | final NostrLogger logger; 13 | 14 | /// The connection timeout for the web sockets. 15 | Duration _connectionTimeout = const Duration(seconds: 5); 16 | 17 | void set(Duration newDur) { 18 | _connectionTimeout = newDur; 19 | } 20 | 21 | /// Connects to a [relay] web socket, and trigger the [onConnectionSuccess] callback if the connection is successful, or the [onConnectionError] callback if the connection fails. 22 | Future connectRelay({ 23 | required String relay, 24 | bool? shouldIgnoreConnectionException, 25 | void Function(WebSocketChannel webSocket)? onConnectionSuccess, 26 | }) async { 27 | WebSocketChannel? webSocket; 28 | 29 | try { 30 | webSocket = WebSocketChannel.connect( 31 | Uri.parse(relay), 32 | ); 33 | 34 | await webSocket.ready; 35 | 36 | onConnectionSuccess?.call(webSocket); 37 | } catch (e) { 38 | logger.log( 39 | 'error while connecting to the relay with url: $relay', 40 | e, 41 | ); 42 | 43 | if (shouldIgnoreConnectionException ?? true) { 44 | logger.log( 45 | 'The error related to relay: $relay is ignored, because to the ignoreConnectionException parameter is set to true.', 46 | ); 47 | } else { 48 | rethrow; 49 | } 50 | } 51 | } 52 | 53 | /// Changes the protocol of a websocket url to http. 54 | Uri getHttpUrlFromWebSocketUrl(String relayUrl) { 55 | assert( 56 | relayUrl.startsWith('ws://') || relayUrl.startsWith('wss://'), 57 | 'invalid relay url', 58 | ); 59 | 60 | try { 61 | var removeWebsocketSign = relayUrl.replaceFirst('ws://', 'http://'); 62 | removeWebsocketSign = 63 | removeWebsocketSign.replaceFirst('wss://', 'https://'); 64 | return Uri.parse(removeWebsocketSign); 65 | } catch (e) { 66 | logger.log( 67 | 'error while getting http url from websocket url: $relayUrl', 68 | e, 69 | ); 70 | 71 | rethrow; 72 | } 73 | } 74 | 75 | /// Creates a custom http client. 76 | // HttpClient _createCustomHttpClient() { 77 | // final client = HttpClient(); 78 | // client.badCertificateCallback = 79 | // (X509Certificate cert, String host, int port) => true; 80 | // client.connectionTimeout = _connectionTimeout; 81 | 82 | // return client; 83 | // } 84 | } 85 | -------------------------------------------------------------------------------- /lib/nostr/model/base.dart: -------------------------------------------------------------------------------- 1 | // // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | // import 'package:equatable/equatable.dart'; 3 | 4 | // abstract class NostrWebSocketMessage extends Equatable { 5 | // final String object; 6 | 7 | // NostrWebSocketMessage({ 8 | // required this.object, 9 | // }); 10 | // } 11 | 12 | // abstract class ReceivedNostrWebSocketMessage extends NostrWebSocketMessage { 13 | // final DateTime receievedAt; 14 | 15 | // ReceivedNostrWebSocketMessage({ 16 | // required this.receievedAt, 17 | // required super.object, 18 | // }); 19 | // } 20 | 21 | // abstract class SentNostrWebSocketMessage extends NostrWebSocketMessage { 22 | // final DateTime sentAt; 23 | 24 | // SentNostrWebSocketMessage({ 25 | // required this.sentAt, 26 | // required super.object, 27 | // }); 28 | // } 29 | -------------------------------------------------------------------------------- /lib/nostr/model/count.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_nostr/dart_nostr.dart'; 4 | import 'package:dart_nostr/nostr/core/constants.dart'; 5 | import 'package:equatable/equatable.dart'; 6 | 7 | class NostrCountEvent extends Equatable { 8 | const NostrCountEvent({ 9 | required this.eventsFilter, 10 | required this.subscriptionId, 11 | }); 12 | 13 | final NostrFilter eventsFilter; 14 | final String subscriptionId; 15 | 16 | static NostrCountEvent fromPartialData({ 17 | required NostrFilter eventsFilter, 18 | }) { 19 | final createdSubscriptionId = 20 | Nostr.instance.services.utils.consistent64HexChars( 21 | eventsFilter.toMap().toString(), 22 | ); 23 | 24 | return NostrCountEvent( 25 | eventsFilter: eventsFilter, 26 | subscriptionId: createdSubscriptionId, 27 | ); 28 | } 29 | 30 | String serialized() { 31 | return jsonEncode([ 32 | NostrConstants.count, 33 | subscriptionId, 34 | eventsFilter.toMap(), 35 | ]); 36 | } 37 | 38 | @override 39 | List get props => [ 40 | eventsFilter, 41 | subscriptionId, 42 | ]; 43 | } 44 | 45 | class NostrCountResponse extends Equatable { 46 | const NostrCountResponse({ 47 | required this.subscriptionId, 48 | required this.count, 49 | }); 50 | 51 | factory NostrCountResponse.deserialized(String data) { 52 | final decodedData = jsonDecode(data); 53 | assert(decodedData is List); 54 | 55 | final countMap = decodedData[2]; 56 | assert(countMap is Map); 57 | 58 | return NostrCountResponse( 59 | subscriptionId: decodedData[1] as String, 60 | count: int.parse(countMap['count'] as String), 61 | ); 62 | } 63 | final String subscriptionId; 64 | final int count; 65 | @override 66 | List get props => throw UnimplementedError(); 67 | 68 | static bool canBeDeserialized(String data) { 69 | final decodedData = jsonDecode(data); 70 | 71 | assert(decodedData is List); 72 | 73 | if (decodedData[0] != NostrConstants.count) { 74 | return false; 75 | } 76 | 77 | final countMap = decodedData[2]; 78 | if (countMap is Map) { 79 | return countMap 80 | .map((key, value) => MapEntry(key.toUpperCase(), value)) 81 | .containsKey(NostrConstants.count); 82 | } else { 83 | return false; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/nostr/model/debug_options.dart: -------------------------------------------------------------------------------- 1 | class NostrDebugOptions { 2 | NostrDebugOptions({ 3 | required this.tag, 4 | this.isLogsEnabled = true, 5 | }); 6 | 7 | factory NostrDebugOptions.general() { 8 | return NostrDebugOptions( 9 | tag: 'Nostr', 10 | ); 11 | } 12 | factory NostrDebugOptions.generate() { 13 | return NostrDebugOptions( 14 | tag: _incrementallyGenerateTag(), 15 | ); 16 | } 17 | 18 | static var _incrementalNumber = 0; 19 | 20 | bool isLogsEnabled = true; 21 | 22 | final String tag; 23 | 24 | static String _incrementallyGenerateTag() { 25 | _incrementalNumber++; 26 | 27 | return '$_incrementalNumber - Nostr'; 28 | } 29 | 30 | NostrDebugOptions copyWith({ 31 | bool? isLogsEnabled, 32 | }) { 33 | return NostrDebugOptions( 34 | tag: tag, 35 | isLogsEnabled: isLogsEnabled ?? this.isLogsEnabled, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/nostr/model/ease.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:convert'; 3 | 4 | import 'package:dart_nostr/nostr/core/constants.dart'; 5 | import 'package:equatable/equatable.dart'; 6 | 7 | class NostrRequestEoseCommand extends Equatable { 8 | final String subscriptionId; 9 | 10 | const NostrRequestEoseCommand({ 11 | required this.subscriptionId, 12 | }); 13 | 14 | @override 15 | List get props => [ 16 | subscriptionId, 17 | ]; 18 | 19 | static bool canBeDeserialized(String dataFromRelay) { 20 | final decoded = jsonDecode(dataFromRelay) as List; 21 | 22 | return decoded.first == NostrConstants.eose; 23 | } 24 | 25 | static NostrRequestEoseCommand fromRelayMessage(String dataFromRelay) { 26 | assert(canBeDeserialized(dataFromRelay)); 27 | 28 | final decoded = jsonDecode(dataFromRelay) as List; 29 | 30 | return NostrRequestEoseCommand( 31 | subscriptionId: decoded[1] as String, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/nostr/model/event/event.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:convert/convert.dart'; 4 | import 'package:crypto/crypto.dart'; 5 | import 'package:dart_nostr/nostr/core/constants.dart'; 6 | import 'package:dart_nostr/nostr/core/key_pairs.dart'; 7 | import 'package:dart_nostr/nostr/model/nostr_event_key.dart'; 8 | import 'package:equatable/equatable.dart'; 9 | 10 | /// {@template nostr_event} 11 | /// This represents a low level Nostr event that requires setting all fields manually, which requires you to doo all encodings... 12 | /// You can use [NostrEvent.fromPartialData] to create an event with less fields and lower complexity.. 13 | /// {@endtemplate} 14 | class NostrEvent extends Equatable { 15 | const NostrEvent({ 16 | required this.content, 17 | required this.createdAt, 18 | required this.id, 19 | required this.kind, 20 | required this.pubkey, 21 | required this.sig, 22 | required this.tags, 23 | this.subscriptionId, 24 | }); 25 | 26 | /// This represents a nostr event that is received from the relays, 27 | /// it takes directly the relay message which is serialized, and handles all internally 28 | factory NostrEvent.deserialized(String data) { 29 | assert(NostrEvent.canBeDeserialized(data), 'Event is not json-decodable'); 30 | final decoded = jsonDecode(data) as List; 31 | 32 | final event = decoded.last as Map; 33 | return NostrEvent( 34 | id: event['id'] as String, 35 | kind: event['kind'] as int, 36 | content: event['content'] == null ? '' : event['content'] as String, 37 | sig: event['sig'] as String, 38 | pubkey: event['pubkey'] as String, 39 | createdAt: DateTime.fromMillisecondsSinceEpoch( 40 | (event['created_at'] as int) * 1000, 41 | ), 42 | tags: List>.from( 43 | (event['tags'] as List) 44 | .map( 45 | (nestedElem) => (nestedElem as List) 46 | .map( 47 | (nestedElemContent) => nestedElemContent.toString(), 48 | ) 49 | .toList(), 50 | ) 51 | .toList(), 52 | ), 53 | subscriptionId: decoded[1] as String?, 54 | ); 55 | } 56 | 57 | /// The id of the event. 58 | final String? id; 59 | 60 | /// The kind of the event. 61 | final int? kind; 62 | 63 | /// The content of the event. 64 | final String? content; 65 | 66 | /// The signature of the event. 67 | final String? sig; 68 | 69 | /// The public key of the event creator. 70 | final String pubkey; 71 | 72 | /// The creation date of the event. 73 | final DateTime? createdAt; 74 | 75 | /// The tags of the event. 76 | final List>? tags; 77 | 78 | /// The subscription id of the event 79 | /// This is meant for events that are got from the relays, and not for events that are created by you. 80 | final String? subscriptionId; 81 | 82 | /// Wether the given [dataFromRelay] can be deserialized into a [NostrEvent]. 83 | static bool canBeDeserialized(String dataFromRelay) { 84 | final decoded = jsonDecode(dataFromRelay) as List; 85 | 86 | return decoded.first == NostrConstants.event; 87 | } 88 | 89 | /// Creates the [id] of an event, based on Nostr specs. 90 | static String getEventId({ 91 | required int kind, 92 | required String content, 93 | required DateTime createdAt, 94 | required List tags, 95 | required String pubkey, 96 | }) { 97 | final data = [ 98 | 0, 99 | pubkey, 100 | createdAt.millisecondsSinceEpoch ~/ 1000, 101 | kind, 102 | tags, 103 | content, 104 | ]; 105 | 106 | final serializedEvent = jsonEncode(data); 107 | final bytes = utf8.encode(serializedEvent); 108 | final digest = sha256.convert(bytes); 109 | final id = hex.encode(digest.bytes); 110 | 111 | return id; 112 | } 113 | 114 | static NostrEvent fromPartialData({ 115 | required int kind, 116 | required String content, 117 | required NostrKeyPairs keyPairs, 118 | List>? tags, 119 | DateTime? createdAt, 120 | String? ots, 121 | }) { 122 | final pubkey = keyPairs.public; 123 | final tagsToUse = tags ?? []; 124 | final createdAtToUse = createdAt ?? DateTime.now(); 125 | 126 | final id = NostrEvent.getEventId( 127 | kind: kind, 128 | content: content, 129 | createdAt: createdAtToUse, 130 | tags: tagsToUse, 131 | pubkey: pubkey, 132 | ); 133 | 134 | final sig = keyPairs.sign(id); 135 | 136 | return NostrEvent( 137 | id: id, 138 | kind: kind, 139 | content: content, 140 | sig: sig, 141 | pubkey: pubkey, 142 | createdAt: createdAtToUse, 143 | tags: tagsToUse, 144 | ); 145 | } 146 | 147 | /// Creates a new [NostrEvent] with the given [content]. 148 | static NostrEvent deleteEvent({ 149 | required NostrKeyPairs keyPairs, 150 | required List eventIdsToBeDeleted, 151 | String reasonOfDeletion = '', 152 | DateTime? createdAt, 153 | }) { 154 | return fromPartialData( 155 | kind: 5, 156 | content: reasonOfDeletion, 157 | keyPairs: keyPairs, 158 | tags: eventIdsToBeDeleted.map((eventId) => ['e', eventId]).toList(), 159 | createdAt: createdAt, 160 | ); 161 | } 162 | 163 | /// Returns a unique tag for this event that you can use to identify it. 164 | NostrEventKey uniqueKey() { 165 | if (subscriptionId == null) { 166 | throw Exception( 167 | "You can't get a unique key for an event that you created, you can only get a unique key for an event that you got from the relays", 168 | ); 169 | } 170 | 171 | if (id == null) { 172 | throw Exception( 173 | "You can't get a unique key for an event that you created, you can only get a unique key for an event that you got from the relays", 174 | ); 175 | } 176 | 177 | return NostrEventKey( 178 | eventId: id!, 179 | sourceSubscriptionId: subscriptionId!, 180 | originalSourceEvent: this, 181 | ); 182 | } 183 | 184 | /// Returns a serialized [NostrEvent] from this event. 185 | String serialized() { 186 | return jsonEncode([NostrConstants.event, toMap()]); 187 | } 188 | 189 | /// Returns a map representation of this event. 190 | Map toMap() { 191 | return { 192 | if (id != null) 'id': id, 193 | if (kind != null) 'kind': kind, 194 | 'pubkey': pubkey, 195 | if (content != null) 'content': content, 196 | if (sig != null) 'sig': sig, 197 | if (createdAt != null) 198 | 'created_at': createdAt!.millisecondsSinceEpoch ~/ 1000, 199 | if (tags != null) 200 | 'tags': tags!.map((tag) => tag.map((e) => e).toList()).toList(), 201 | }; 202 | } 203 | 204 | bool isVerified() { 205 | if (id == null || sig == null) { 206 | return false; 207 | } 208 | 209 | return NostrKeyPairs.verify( 210 | pubkey, 211 | id!, 212 | sig!, 213 | ); 214 | } 215 | 216 | @override 217 | List get props => [ 218 | id, 219 | kind, 220 | content, 221 | sig, 222 | pubkey, 223 | createdAt, 224 | tags, 225 | subscriptionId, 226 | ]; 227 | } 228 | -------------------------------------------------------------------------------- /lib/nostr/model/export.dart: -------------------------------------------------------------------------------- 1 | export './notice.dart'; 2 | export './request/close.dart'; 3 | export './request/filter.dart'; 4 | export './request/request.dart'; 5 | export 'count.dart'; 6 | export 'event/event.dart'; 7 | export 'nostr_events_stream.dart'; 8 | -------------------------------------------------------------------------------- /lib/nostr/model/nostr_event_key.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | /// {@template nostr_event_key} 5 | /// This clas can be used to identify an event uniquely based on external factors such as the subscription id. 6 | /// {@endtemplate} 7 | class NostrEventKey extends Equatable { 8 | /// {@macro nostr_event_key} 9 | const NostrEventKey({ 10 | required this.eventId, 11 | required this.sourceSubscriptionId, 12 | required this.originalSourceEvent, 13 | }); 14 | 15 | /// The id of the event. 16 | final String eventId; 17 | 18 | /// The id of the subscription from where this event is got. 19 | final String sourceSubscriptionId; 20 | 21 | /// The source original event. 22 | final NostrEvent originalSourceEvent; 23 | 24 | @override 25 | List get props => [ 26 | eventId, 27 | sourceSubscriptionId, 28 | originalSourceEvent, 29 | ]; 30 | 31 | @override 32 | String toString() { 33 | return 'NostrEventKey{eventId: $eventId, sourceSubscriptionId: $sourceSubscriptionId, originalSourceEvent: $originalSourceEvent}'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/nostr/model/nostr_events_stream.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 2 | import 'package:dart_nostr/nostr/model/event/event.dart'; 3 | import 'package:dart_nostr/nostr/model/request/request.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | 6 | /// {@template nostr_events_stream} 7 | /// Represents a holde class for the stream of nostr events and the subscription id. 8 | /// {@endtemplate} 9 | class NostrEventsStream extends Equatable { 10 | /// {@macro nostr_events_stream} 11 | const NostrEventsStream({ 12 | required this.stream, 13 | required this.subscriptionId, 14 | required this.request, 15 | }); 16 | 17 | /// This the stream of nostr events that you can listen to and get the events. 18 | final Stream stream; 19 | 20 | /// This is the subscription id of the stream. You can use this to unsubscribe from the stream. 21 | final String subscriptionId; 22 | 23 | final NostrRequest request; 24 | 25 | /// {@macro close_events_subscription} 26 | void close() { 27 | return Nostr.instance.services.relays 28 | .closeEventsSubscription(subscriptionId); 29 | } 30 | 31 | @override 32 | List get props => [stream, subscriptionId, request]; 33 | } 34 | -------------------------------------------------------------------------------- /lib/nostr/model/notice.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'dart:convert'; 3 | 4 | import 'package:dart_nostr/nostr/core/constants.dart'; 5 | import 'package:equatable/equatable.dart'; 6 | 7 | class NostrNotice extends Equatable { 8 | final String message; 9 | 10 | const NostrNotice({ 11 | required this.message, 12 | }); 13 | 14 | @override 15 | List get props => [message]; 16 | 17 | static bool canBeDeserialized(String dataFromRelay) { 18 | final decoded = jsonDecode(dataFromRelay) as List; 19 | 20 | return decoded.first == NostrConstants.notice; 21 | } 22 | 23 | static NostrNotice fromRelayMessage(String data) { 24 | assert(canBeDeserialized(data)); 25 | 26 | final decoded = jsonDecode(data) as List; 27 | assert(decoded.first == NostrConstants.notice); 28 | final message = decoded[1] as String; 29 | 30 | return NostrNotice(message: message); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/nostr/model/ok.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_nostr/nostr/core/constants.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | 6 | /// {@template nostr_event_ok_command} 7 | /// The ok command that is sent to the server when an event is accepted or declined. 8 | /// {@endtemplate} 9 | class NostrEventOkCommand extends Equatable { 10 | /// {@macro nostr_event_ok_command} 11 | const NostrEventOkCommand({ 12 | required this.eventId, 13 | this.isEventAccepted, 14 | this.message, 15 | }); 16 | 17 | /// The event ID of which this ok command was sent. 18 | final String eventId; 19 | 20 | /// Wether the event is accepted, or not. 21 | final bool? isEventAccepted; 22 | 23 | /// The message that was sent with the ok command. 24 | final String? message; 25 | 26 | @override 27 | List get props => [ 28 | eventId, 29 | isEventAccepted, 30 | message, 31 | ]; 32 | 33 | static bool canBeDeserialized(String dataFromRelay) { 34 | final decoded = jsonDecode(dataFromRelay) as List; 35 | 36 | return decoded.first == NostrConstants.ok; 37 | } 38 | 39 | static NostrEventOkCommand fromRelayMessage(String data) { 40 | assert(canBeDeserialized(data)); 41 | 42 | final decoded = jsonDecode(data) as List; 43 | final eventId = decoded[1] as String; 44 | 45 | final isEventAccepted = decoded.length > 2 46 | ? decoded[2] is bool 47 | ? decoded[2] as bool 48 | : decoded[2] is String 49 | ? (decoded[2] as String).toLowerCase() == 'true' 50 | : null 51 | : null; 52 | 53 | final message = decoded.length > 3 ? decoded[3] as String? : null; 54 | 55 | return NostrEventOkCommand( 56 | eventId: eventId, 57 | isEventAccepted: isEventAccepted, 58 | message: message, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/nostr/model/relay.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:equatable/equatable.dart'; 6 | import 'package:web_socket_channel/web_socket_channel.dart'; 7 | 8 | /// {@template nostr_relay} 9 | /// A representation of a relay, it contains the [WebSocket] connection to the relay and the [url] of the relay. 10 | /// {@endtemplate} 11 | class NostrRelay extends Equatable { 12 | /// The [WebSocketChannel] connection to the relay. 13 | final WebSocketChannel socket; 14 | 15 | /// The url of the relay. 16 | final String url; 17 | 18 | /// {@macro nostr_relay} 19 | const NostrRelay({ 20 | required this.socket, 21 | required this.url, 22 | }); 23 | 24 | @override 25 | List get props => [socket, url]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/nostr/model/relay_informations.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class RelayInformations extends Equatable { 4 | const RelayInformations({ 5 | required this.contact, 6 | required this.description, 7 | required this.name, 8 | required this.pubkey, 9 | required this.software, 10 | required this.supportedNips, 11 | required this.version, 12 | }); 13 | 14 | factory RelayInformations.fromNip11Response(Map json) { 15 | final supportedNips = json['supported_nips'].cast(); 16 | 17 | return RelayInformations( 18 | contact: json['contact'] as String?, 19 | description: json['description'] as String?, 20 | name: json['name'] as String?, 21 | pubkey: json['pubkey'] as String?, 22 | software: json['software'] as String?, 23 | supportedNips: supportedNips as List?, 24 | version: json['version'] as String?, 25 | ); 26 | } 27 | final String? contact; 28 | final String? description; 29 | final String? name; 30 | final String? pubkey; 31 | final String? software; 32 | final List? supportedNips; 33 | final String? version; 34 | @override 35 | List get props => [ 36 | contact, 37 | description, 38 | name, 39 | pubkey, 40 | software, 41 | supportedNips, 42 | version, 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /lib/nostr/model/request/close.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_nostr/nostr/core/constants.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | 6 | /// {@template nostr_request_close} 7 | /// A request to close a subscription with a given subscription id. 8 | /// {@endtemplate} 9 | class NostrRequestClose extends Equatable { 10 | /// {@macro nostr_request_close} 11 | const NostrRequestClose({ 12 | required this.subscriptionId, 13 | }); 14 | 15 | /// The subscription id. 16 | final String subscriptionId; 17 | 18 | /// Serializes the request to a json string. 19 | String serialized() { 20 | return jsonEncode([NostrConstants.close, subscriptionId]); 21 | } 22 | 23 | @override 24 | List get props => [subscriptionId]; 25 | } 26 | -------------------------------------------------------------------------------- /lib/nostr/model/request/eose.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_nostr/nostr/core/constants.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | 6 | class NostrEOSE extends Equatable { 7 | const NostrEOSE({required this.subscriptionId}); 8 | final String subscriptionId; 9 | @override 10 | List get props => [subscriptionId]; 11 | 12 | bool canBeDeserialized(String message) { 13 | final decoded = jsonDecode(message) as List; 14 | 15 | return decoded.first == NostrConstants.eose; 16 | } 17 | 18 | NostrEOSE fromRelayMessage(String message) { 19 | final decoded = jsonDecode(message) as List; 20 | 21 | return NostrEOSE(subscriptionId: decoded.last as String); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/nostr/model/request/filter.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// {@template nostr_filter} 4 | /// NostrFilter is a filter that can be used to match events. 5 | /// {@endtemplate} 6 | class NostrFilter extends Equatable { 7 | /// {@macro nostr_filter} 8 | const NostrFilter({ 9 | this.ids, 10 | this.authors, 11 | this.kinds, 12 | this.e, 13 | this.p, 14 | this.t, 15 | this.since, 16 | this.until, 17 | this.limit, 18 | this.search, 19 | this.a, 20 | this.additionalFilters, 21 | }); 22 | 23 | /// Deserialize aNpstrFilter from a JSON 24 | factory NostrFilter.fromJson(Map json) { 25 | final ids = 26 | json['ids'] == null ? null : List.from(json['ids'] as List); 27 | 28 | final authors = json['authors'] == null 29 | ? null 30 | : List.from(json['authors'] as List); 31 | 32 | final kinds = 33 | json['kinds'] == null ? null : List.from(json['kinds'] as List); 34 | 35 | final e = json['#e'] == null ? null : List.from(json['#e'] as List); 36 | 37 | final p = json['#p'] == null ? null : List.from(json['#p'] as List); 38 | 39 | final t = json['#t'] == null ? null : List.from(json['#t'] as List); 40 | 41 | final a = json['#a'] == null ? null : List.from(json['#a'] as List); 42 | final since = 43 | DateTime.fromMillisecondsSinceEpoch((json['since'] as int) * 1000); 44 | 45 | final until = 46 | DateTime.fromMillisecondsSinceEpoch((json['until'] as int) * 1000); 47 | 48 | final limit = json['limit'] as int?; 49 | 50 | final search = json['search'] as String?; 51 | 52 | return NostrFilter( 53 | ids: ids, 54 | authors: authors, 55 | kinds: kinds, 56 | e: e, 57 | p: p, 58 | t: t, 59 | a: a, 60 | since: since, 61 | until: until, 62 | limit: limit, 63 | search: search, 64 | ); 65 | } 66 | 67 | /// a list of event ids to filter with. 68 | final List? ids; 69 | 70 | /// a list of pubkeys or prefixes to filter with. 71 | final List? authors; 72 | 73 | /// a list of a kind numbers to filter with. 74 | final List? kinds; 75 | 76 | /// a list of event ids that are referenced in an "e" tag to filter with. 77 | final List? e; 78 | 79 | /// a list of event ids that are referenced in an "e" tag to filter with. 80 | final List? t; 81 | 82 | /// a list of pubkeys that are referenced in a "p" tag to filter with. 83 | final List? p; 84 | 85 | /// a list of event ids referenced in an "a" tag to filter with. 86 | final List? a; 87 | 88 | /// the DateTime to start the filtering from 89 | final DateTime? since; 90 | 91 | /// the DateTime to end the filtering at 92 | final DateTime? until; 93 | 94 | /// the maximum number of events to return 95 | final int? limit; 96 | 97 | /// A search string to use to filter events 98 | final String? search; 99 | 100 | /// Additional filters to be used in the filter 101 | final Map? additionalFilters; 102 | 103 | /// Serialize a [NostrFilter] to a [Map] 104 | Map toMap() { 105 | return { 106 | if (ids != null) 'ids': ids, 107 | if (authors != null) 'authors': authors, 108 | if (kinds != null) 'kinds': kinds, 109 | if (e != null) '#e': e, 110 | if (p != null) '#p': p, 111 | if (t != null) '#t': t, 112 | if (a != null) '#a': a, 113 | if (since != null) 'since': since!.millisecondsSinceEpoch ~/ 1000, 114 | if (until != null) 'until': until!.millisecondsSinceEpoch ~/ 1000, 115 | if (limit != null) 'limit': limit, 116 | if (search != null) 'search': search, 117 | if (additionalFilters != null) ...additionalFilters!, 118 | }; 119 | } 120 | 121 | @override 122 | List get props => [ 123 | ids, 124 | authors, 125 | kinds, 126 | e, 127 | p, 128 | t, 129 | a, 130 | since, 131 | until, 132 | limit, 133 | search, 134 | additionalFilters, 135 | ]; 136 | } 137 | -------------------------------------------------------------------------------- /lib/nostr/model/request/request.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first, avoid_dynamic_calls, argument_type_not_assignable 2 | 3 | import 'dart:convert'; 4 | 5 | import 'package:dart_nostr/nostr/core/constants.dart'; 6 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 7 | import 'package:dart_nostr/nostr/model/request/filter.dart'; 8 | import 'package:equatable/equatable.dart'; 9 | 10 | /// {@template nostr_request} 11 | /// NostrRequest is a request to subscribe to a set of events that match a set of filters with a given [subscriptionId]. 12 | /// {@endtemplate} 13 | 14 | // ignore: must_be_immutable 15 | class NostrRequest extends Equatable { 16 | /// The subscription ID of the request. 17 | String? subscriptionId; 18 | 19 | /// A list of filters that the request will match. 20 | final List filters; 21 | 22 | /// {@macro nostr_request} 23 | NostrRequest({ 24 | required this.filters, 25 | this.subscriptionId, 26 | }); 27 | 28 | /// Serialize the request to send it to the remote relays websockets. 29 | String serialized({String? subscriptionId}) { 30 | this.subscriptionId = subscriptionId ?? 31 | this.subscriptionId ?? 32 | Nostr.instance.services.utils.consistent64HexChars( 33 | filters 34 | .map((e) => e.toMap().toString()) 35 | .reduce((value, element) => value + element), 36 | ); 37 | 38 | //! The old way of doing it is commented below 39 | // final decodedFilters = 40 | // jsonEncode(filters.map((item) => item.toMap()).toList()); 41 | // final header = jsonEncode([NostrConstants.request, subscriptionId]); 42 | // final result = 43 | // '${header.substring(0, header.length - 1)},${decodedFilters.substring(1, decodedFilters.length)}'; 44 | 45 | final encodedReq = jsonEncode([ 46 | NostrConstants.request, 47 | subscriptionId, 48 | ...filters.map((e) => e.toMap()), 49 | ]); 50 | 51 | return encodedReq; 52 | } 53 | 54 | /// Deserialize a request 55 | factory NostrRequest.deserialized(input) { 56 | final haveThreeElements = input is List && input.length >= 3; 57 | 58 | assert( 59 | haveThreeElements, 60 | 'Invalid request, must have at least 3 elements', 61 | ); 62 | 63 | assert( 64 | input[0] == NostrConstants.request, 65 | 'Invalid request, must start with ${NostrConstants.request}', 66 | ); 67 | 68 | final subscriptionId = input[1] as String; 69 | 70 | return NostrRequest( 71 | subscriptionId: subscriptionId, 72 | filters: List.generate( 73 | input.length - 2, 74 | (index) => NostrFilter.fromJson( 75 | input[index + 2], 76 | ), 77 | ), 78 | ); 79 | } 80 | 81 | @override 82 | List get props => [subscriptionId, filters]; 83 | 84 | NostrRequest copyWith({ 85 | String? subscriptionId, 86 | List? filters, 87 | }) { 88 | return NostrRequest( 89 | subscriptionId: subscriptionId ?? this.subscriptionId, 90 | filters: filters ?? this.filters, 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/nostr/model/tlv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | class TLV { 4 | TLV({ 5 | required this.type, 6 | required this.length, 7 | required this.value, 8 | }); 9 | final int type; 10 | final int length; 11 | final Uint8List value; 12 | } 13 | -------------------------------------------------------------------------------- /lib/nostr/service/services.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/nostr/core/utils.dart'; 2 | import 'package:dart_nostr/nostr/instance/bech32/bech32.dart'; 3 | import 'package:dart_nostr/nostr/instance/keys/keys.dart'; 4 | import 'package:dart_nostr/nostr/instance/relays/relays.dart'; 5 | import 'package:dart_nostr/nostr/instance/utils/utils.dart'; 6 | 7 | class NostrServices { 8 | NostrServices({ 9 | required this.logger, 10 | }); 11 | final NostrLogger logger; 12 | 13 | /// {@macro nostr_keys} 14 | late final keys = NostrKeys( 15 | logger: logger, 16 | ); 17 | 18 | /// {@macro nostr_relays} 19 | late final relays = NostrRelays( 20 | logger: logger, 21 | ); 22 | 23 | late final utils = NostrUtils( 24 | logger: logger, 25 | ); 26 | 27 | late final bech32 = NostrBech32( 28 | logger: logger, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_nostr 2 | description: Develop Scalable Dart/Flutter Nostr clients quickly and easily 3 | version: 9.1.1 4 | repository: https://github.com/anasfik/nostr/ 5 | 6 | environment: 7 | sdk: ">=3.0.0 <=3.9.9" 8 | 9 | dependencies: 10 | bip340: ^0.1.0 11 | equatable: ^2.0.5 12 | convert: ^3.1.1 13 | crypto: ^3.0.2 14 | http: ^1.1.0 15 | meta: ^1.8.0 16 | bech32: ^0.2.2 17 | hex: ^0.2.0 18 | bip32_bip44: ^1.0.0 19 | bip39: ^1.0.6 20 | web_socket_channel: ^2.4.0 21 | 22 | dev_dependencies: 23 | # dart_code_metrics: ^5.7.6 24 | lints: 25 | test: 26 | very_good_analysis: ^5.1.0 27 | -------------------------------------------------------------------------------- /test/nostr/instance/utils/utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_nostr/nostr/core/exceptions.dart'; 4 | import 'package:dart_nostr/nostr/dart_nostr.dart'; 5 | import 'package:http/http.dart' as http; 6 | import 'package:http/testing.dart' as http_testing; 7 | import 'package:test/test.dart'; 8 | 9 | void main() { 10 | late http.BaseClient successfullMockHttpClient; 11 | late http.BaseClient errorMockHttpClient; 12 | 13 | setUpAll( 14 | () { 15 | /// Mock HTTP client handler which returns a nip05-compliant response 16 | /// containing a mapping of the internet identifier to the public key 17 | successfullMockHttpClient = http_testing.MockClient( 18 | (request) { 19 | return Future.value( 20 | http.Response( 21 | jsonEncode( 22 | { 23 | 'names': { 24 | 'localpart': 'randompubkey', 25 | }, 26 | }, 27 | ), 28 | 200, 29 | ), 30 | ); 31 | }, 32 | ); 33 | 34 | errorMockHttpClient = http_testing.MockClient( 35 | (request) { 36 | return Future.value( 37 | http.Response( 38 | 'Server error', 39 | 500, 40 | ), 41 | ); 42 | }, 43 | ); 44 | }, 45 | ); 46 | 47 | group( 48 | 'nip05', 49 | () { 50 | test( 51 | 'returns pubkey when it is found in .well-known configuration', 52 | () { 53 | http.runWithClient( 54 | () async { 55 | const internetIdentifier = 'localpart@domain'; 56 | const expectedPubKey = 'randompubkey'; 57 | 58 | final result = 59 | await Nostr.instance.services.utils.pubKeyFromIdentifierNip05( 60 | internetIdentifier: internetIdentifier, 61 | ); 62 | 63 | expect(result, expectedPubKey); 64 | }, 65 | () => successfullMockHttpClient, 66 | ); 67 | }, 68 | ); 69 | 70 | test( 71 | 'returns null when pubkey is not found in .well-known configuration', 72 | () { 73 | http.runWithClient( 74 | () async { 75 | const internetIdentifier = 'nonexistentusername@domain'; 76 | 77 | final result = 78 | await Nostr.instance.services.utils.pubKeyFromIdentifierNip05( 79 | internetIdentifier: internetIdentifier, 80 | ); 81 | 82 | expect( 83 | result, 84 | null, 85 | ); 86 | }, 87 | () => successfullMockHttpClient, 88 | ); 89 | }, 90 | ); 91 | 92 | test('throws [Nip05VerificationException] if any exception was thrown', 93 | () { 94 | http.runWithClient( 95 | () async { 96 | const internetIdentifier = 'localpart@domain'; 97 | await expectLater( 98 | () async => 99 | Nostr.instance.services.utils.pubKeyFromIdentifierNip05( 100 | internetIdentifier: internetIdentifier, 101 | ), 102 | throwsA( 103 | isA(), 104 | ), 105 | ); 106 | }, 107 | () => errorMockHttpClient, 108 | ); 109 | }); 110 | }, 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /test/nostr/model/event/nostr_event_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nostr/dart_nostr.dart'; 2 | import 'package:test/expect.dart'; 3 | import 'package:test/scaffolding.dart'; 4 | 5 | void main() { 6 | const validInput = ''' 7 | [ 8 | "EVENT", 9 | "subscriptionId", 10 | { 11 | "id": "identifier", 12 | "pubkey": "author", 13 | "kind": 0, 14 | "created_at": 1740549558, 15 | "tags": [ 16 | [ 17 | "p", 18 | "pubkey" 19 | ] 20 | ], 21 | "content": "content", 22 | "sig": "event signature" 23 | } 24 | ] 25 | '''; 26 | 27 | const inputWithNullContent = ''' 28 | [ 29 | "EVENT", 30 | "subscriptionId", 31 | { 32 | "id": "identifier", 33 | "pubkey": "author", 34 | "kind": 0, 35 | "created_at": 1740549558, 36 | "tags": [ 37 | [ 38 | "p", 39 | "pubkey" 40 | ] 41 | ], 42 | "content": null, 43 | "sig": "event signature" 44 | } 45 | ] 46 | '''; 47 | 48 | final parsedEvent = NostrEvent( 49 | content: 'content', 50 | createdAt: DateTime.fromMillisecondsSinceEpoch(1740549558 * 1000), 51 | id: 'identifier', 52 | kind: 0, 53 | pubkey: 'author', 54 | sig: 'event signature', 55 | subscriptionId: 'subscriptionId', 56 | tags: const [ 57 | [ 58 | 'p', 59 | 'pubkey', 60 | ] 61 | ], 62 | ); 63 | 64 | final parsedEventWithNullContent = NostrEvent( 65 | content: '', 66 | createdAt: DateTime.fromMillisecondsSinceEpoch(1740549558 * 1000), 67 | id: 'identifier', 68 | kind: 0, 69 | pubkey: 'author', 70 | subscriptionId: 'subscriptionId', 71 | sig: 'event signature', 72 | tags: const [ 73 | [ 74 | 'p', 75 | 'pubkey', 76 | ] 77 | ], 78 | ); 79 | 80 | group('NostrEvent model test', () { 81 | test('event is correctly parsed from payload', () { 82 | final eventFromPayload = NostrEvent.deserialized(validInput); 83 | 84 | expect(eventFromPayload, parsedEvent); 85 | }); 86 | 87 | test('event is correctly parsed when content is null', () { 88 | final eventFromPayload = NostrEvent.deserialized(inputWithNullContent); 89 | 90 | expect(parsedEventWithNullContent, eventFromPayload); 91 | }); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /todo.extensionThatDoNotExist: -------------------------------------------------------------------------------- 1 | write documenttaion for count command impl. 2 | include code examples in docs site. --------------------------------------------------------------------------------