42 |
43 | {% endblock %}
--------------------------------------------------------------------------------
/nomen-cli/src/nostr.rs:
--------------------------------------------------------------------------------
1 | use std::str::FromStr;
2 |
3 | use derive_more::{AsRef, From, Into};
4 | use nostr_sdk::{
5 | prelude::{FromPkStr, FromSkStr},
6 | Keys,
7 | };
8 | use secp256k1::{SecretKey, XOnlyPublicKey};
9 |
10 | #[derive(Debug, Clone, PartialEq, Eq, From, Into, AsRef)]
11 | pub struct Nsec(SecretKey);
12 |
13 | impl FromStr for Nsec {
14 | type Err = anyhow::Error;
15 |
16 | fn from_str(s: &str) -> Result {
17 | let keys = Keys::from_sk_str(s)?;
18 | let sk = keys.secret_key()?;
19 | Ok(Nsec(sk))
20 | }
21 | }
22 |
23 | #[derive(Debug, Clone, PartialEq, Eq, From, Into, AsRef)]
24 | pub struct Npub(XOnlyPublicKey);
25 |
26 | impl FromStr for Npub {
27 | type Err = anyhow::Error;
28 |
29 | fn from_str(s: &str) -> Result {
30 | let keys = Keys::from_pk_str(s)?;
31 | let pk = keys.public_key();
32 | Ok(Npub(pk))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nomen_core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nomen_core"
3 | version = "0.4.0"
4 | edition = "2021"
5 | rust-version = "1.71"
6 | repository = "https://github.com/ursuscamp/nomen"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [dependencies]
11 | bitcoin = {version = "0.30.0", features = ["base64", "serde"] }
12 | bitcoin_hashes = { version = "0.12.0", features = ["serde"] }
13 | derive_more = "0.99.17"
14 | hex = { version = "0.4.3", features = ["serde"] }
15 | itertools = "0.10.5"
16 | tracing = "0.1.37"
17 | nostr-sdk = "0.24.0"
18 | rand = "0.8.5"
19 | regex = "1.7.1"
20 | ripemd = "0.1.3"
21 | secp256k1 = { version = "0.27.0", features = ["rand-std"] }
22 | serde = { version = "1.0.152", features = ["derive"] }
23 | serde-hex = "0.1.0"
24 | serde_json = "1.0.94"
25 | sha2 = "0.10.6"
26 | time = { version = "0.3.20", features = ["formatting", "macros"] }
27 | thiserror = "1.0.49"
28 | serde_with = { version = "*", features = ["macros"] }
29 |
--------------------------------------------------------------------------------
/docs/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | # Setting up a dev environment
2 |
3 | 1. Clone the repo.
4 | 2. Create a branch from `develop`.
5 |
6 | ## Bitcoin
7 |
8 | 1. Start bitcoin in regtest: `make bitcoin-local`. This sets up a local Bitcoin regtest environment just for Nomen.
9 | - If you ever need to reset your local regtest: Stop `bitcoin` and run `make bitcoin-reset`.
10 | 2. Run `make bitcoin-wallet` to setup the default wallet for Bitcoin.
11 | 3. Create an alias like `regtest` to `bitcoin-cli -datadir=.bitcoin/ -chain=regtest`.
12 |
13 | ## Nostr Relay
14 |
15 | 1. In a separate folder, clone `https://github.com/scsibug/nostr-rs-relay`.
16 | 2. Run `cargo build --release`.
17 | 3. Run `RUST_LOG=info target/release/nostr-rs-relay`.
18 | - This will start a local Nostr relay for Nomen to use.
19 | - If you ever need to reset, just `rm nostr.db` and run the command again.
20 |
21 | ## Nomen
22 |
23 | Back in your Nomen folder:
24 |
25 | 1. Copy [development.nomen.toml](./development.nomen.toml) to `nomen.toml` in the root folder.
26 | 2. Run `cargo run -- server` to start the Nomen indexer.
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
1 | # Release Notes - 0.3.0
2 |
3 | ## Highlights
4 |
5 | Version 0.4.0 brings a few new features:
6 |
7 | - [NOM-04](https://github.com/ursuscamp/noms/blob/master/nom-04.md) support: indexes are now publised to relays!
8 | - `rebroadcast` CLI command which rebroadcasts all known records events to relays (useful to keep indexer network healthy)
9 | - `publish` command will publish the full set of indexed names to relays
10 |
11 | ## Upgrading from 0.3
12 |
13 | 1. Backup your `nomen.db` file prior to upgrading.
14 | 2. Repalce your `nomen` executable.
15 | 3. Update your `nomen.toml` config file with the following new keys under the `[nostr]` section (if you wish to publish your index):
16 | 1. `secret = "nsec..."` is the `nsec` encoded private key that your indexer will use to publish events
17 | 2. `publish = true` will tell your node to publish index events to your Nostr relays
18 | 3. `well-known = true` will make sure the indexer serves the `.well-known/nomen.json` file per the NOM-04 specification
19 | 4. Run `nomen publish` to publish a full index (again, only if you wish to publish)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2023 ursuscamp
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the “Software”), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/nomen/templates/transfer/initiate.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 |
Transfer Name
6 |
7 |
8 | To transfer a name, two transfer transactions must be broadcast containting data for the new owner, and a signature
9 | authorizing the transfer, signed by the original owner.
10 |
11 |
12 |
13 | To initiate a transfer, enter the name you wish to transfer, and the pubkeys of the previous (current) owner and
14 | the new owner.
15 |
16 |
17 |
37 |
38 | {% endblock %}
--------------------------------------------------------------------------------
/nomen/src/config/cli.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use clap::Parser;
4 |
5 | #[derive(Parser, Debug, Clone)]
6 | pub struct Cli {
7 | /// Location of config file: Default: nomen.toml
8 | #[arg(short, long, default_value = "nomen.toml")]
9 | pub config: PathBuf,
10 |
11 | #[command(subcommand)]
12 | pub subcommand: Subcommand,
13 | }
14 |
15 | #[derive(clap::Subcommand, Debug, Clone)]
16 | pub enum Subcommand {
17 | /// Output example config file.
18 | Init,
19 |
20 | /// Scan and index the blockchain.
21 | Index,
22 |
23 | /// Start the HTTP server.
24 | Server,
25 |
26 | /// Force the indexer to re-index, given an optional starting blockheight. This operation is fast, it does NOT force a blockchain rescan.
27 | Reindex { blockheight: Option },
28 |
29 | /// Rescan the blockchain, given an optional starting blockheight. This operation is slow, it redownloads blocks.
30 | Rescan { blockheight: Option },
31 |
32 | /// Rebroadcast Nostr record events
33 | Rebroadcast,
34 |
35 | /// Publish full name index to relay servers
36 | Publish,
37 |
38 | /// Prints the current version of application
39 | Version,
40 | }
41 |
--------------------------------------------------------------------------------
/nomen_core/src/nsid_builder.rs:
--------------------------------------------------------------------------------
1 | use bitcoin::secp256k1::XOnlyPublicKey;
2 |
3 | use crate::Hash160;
4 |
5 | use super::Nsid;
6 |
7 | pub struct NsidBuilder {
8 | root_name: String,
9 | pk: XOnlyPublicKey,
10 | }
11 |
12 | impl NsidBuilder {
13 | pub fn new(root_name: &str, root_pk: &XOnlyPublicKey) -> NsidBuilder {
14 | NsidBuilder {
15 | root_name: root_name.to_owned(),
16 | pk: *root_pk,
17 | }
18 | }
19 |
20 | pub fn finalize(self) -> Nsid {
21 | let mut hasher = Hash160::default();
22 | hasher.update(self.root_name.as_bytes());
23 | hasher.update(&self.pk.serialize());
24 | hasher.finalize().into()
25 | }
26 | }
27 |
28 | #[cfg(test)]
29 | mod tests {
30 | use super::*;
31 |
32 | #[test]
33 | fn test_nsid_builder() {
34 | let pk: XOnlyPublicKey = "60de6fbc4a78209942c62706d904ff9592c2e856f219793f7f73e62fc33bfc18"
35 | .parse()
36 | .unwrap();
37 | let nsid = NsidBuilder::new("hello-world", &pk).finalize();
38 |
39 | assert_eq!(
40 | nsid,
41 | "273968a1e7be2ef0acbcae6f61d53e73101e2983".parse().unwrap()
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/nomen/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 |
6 |
Nomen
7 |
8 |
9 | Nomen is an open protocol for global names, like a decentralized DNS, built with Bitcoin and Nostr. The goals of
10 | the Nomen protocol are decentralization and self-sovereignty.
11 |
12 |
13 |
14 | Nomen Explorer is an indexer. It catalogues the Bitcoin blockchain and associated Nomen events on Nostr.
15 |
16 |
17 |
More Information
18 |
19 |
20 | Check out some more information below, starting with the FAQ if you want details. Check out the Explorer if you
21 | want to see
22 | what names already exist out there.
23 |
31 |
32 |
33 |
34 | {% endblock %}
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BITCOINDIR=.bitcoin
2 | REGTESTCLI=bitcoin-cli -datadir=$(BITCOINDIR) -chain=regtest
3 |
4 | .PHONY: mac-aarch64 linux-amd64 windows-amd64 release
5 |
6 | mac-aarch64:
7 | cargo build --release --target aarch64-apple-darwin
8 |
9 | linux-amd64:
10 | TARGET_CC=x86_64-linux-musl-gcc cargo build --release --target x86_64-unknown-linux-musl
11 |
12 | # Setup: https://gist.github.com/Mefistophell/9787e1b6d2d9441c16d2ac79d6a505e6
13 | windows-amd64:
14 | TARGET_CC=x86_64-w64-mingw32-gcc cargo build --release --target x86_64-pc-windows-gnu
15 |
16 | release: mac-aarch64 linux-amd64 windows-amd64
17 | mkdir -p release
18 | zip release/nomen-mac-aarch64-$(VERSION).zip target/aarch64-apple-darwin/release/nomen
19 | zip release/nomen-linux-amd64-$(VERSION).zip target/x86_64-unknown-linux-musl/release/nomen
20 | zip release/nomen-windows-amd64-$(VERSION).zip target/x86_64-pc-windows-gnu/release/nomen.exe
21 |
22 | bitcoin-local:
23 | mkdir -p .bitcoin
24 | bitcoind -datadir=$(BITCOINDIR) -chain=regtest -fallbackfee=0.001 -txindex -rpcuser=regtest -rpcpassword=regtest
25 |
26 | bitcoin-wallet:
27 | $(REGTESTCLI) createwallet regtest
28 | $(REGTESTCLI) generatetoaddress 101 $$($(REGTESTCLI) getnewaddress)
29 |
30 | bitcoin-reset:
31 | rm -rf .bitcoin
--------------------------------------------------------------------------------
/nomen_core/src/name.rs:
--------------------------------------------------------------------------------
1 | use std::str::FromStr;
2 |
3 | use derive_more::{AsRef, Display, Into};
4 | use regex::Regex;
5 |
6 | #[derive(Display, AsRef, Debug, Clone, PartialEq, Eq, Default)]
7 | pub struct Name(String);
8 |
9 | impl FromStr for Name {
10 | type Err = super::UtilError;
11 |
12 | fn from_str(s: &str) -> Result {
13 | let r = Regex::new(r#"\A[0-9a-z\-]{3,43}\z"#)?;
14 | if r.is_match(s) {
15 | return Ok(Name(s.into()));
16 | }
17 |
18 | Err(super::UtilError::NameValidation)
19 | }
20 | }
21 |
22 | #[cfg(test)]
23 | mod tests {
24 | use std::{any, collections::HashMap};
25 |
26 | use crate::UtilError;
27 |
28 | use super::*;
29 |
30 | #[test]
31 | fn test_valid() {
32 | let r = ["hello-world", "123abc"]
33 | .into_iter()
34 | .map(Name::from_str)
35 | .all(|r| r.is_ok());
36 | assert!(r);
37 | }
38 |
39 | #[test]
40 | fn test_invalid() {
41 | let r = [
42 | "hello!",
43 | "ld",
44 | "abcdefghijklmnopqrztuvwxyzabcdefghijklmnopqrztuvwxyz",
45 | ]
46 | .into_iter()
47 | .map(Name::from_str)
48 | .all(|r| r.is_err());
49 | assert!(r);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/nomen/src/util/npub.rs:
--------------------------------------------------------------------------------
1 | use std::{fmt::Display, str::FromStr};
2 |
3 | use nostr_sdk::{prelude::FromPkStr, Keys};
4 | use secp256k1::XOnlyPublicKey;
5 | use serde::Serialize;
6 |
7 | #[derive(Debug, Clone, Copy, Serialize, serde_with::DeserializeFromStr)]
8 |
9 | pub struct Npub(XOnlyPublicKey);
10 |
11 | impl AsRef for Npub {
12 | fn as_ref(&self) -> &XOnlyPublicKey {
13 | &self.0
14 | }
15 | }
16 |
17 | impl FromStr for Npub {
18 | type Err = anyhow::Error;
19 |
20 | fn from_str(s: &str) -> Result {
21 | let keys = Keys::from_pk_str(s)?;
22 | Ok(Npub(keys.public_key()))
23 | }
24 | }
25 |
26 | impl Display for Npub {
27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 | f.write_str(&self.0.to_string())
29 | }
30 | }
31 |
32 | #[cfg(test)]
33 | mod tests {
34 | use super::*;
35 |
36 | #[test]
37 | fn test_npub() {
38 | let _pubkey: Npub = "npub1u50q2x85utgcgqrmv607crvmk8x3k2nvyun84dxlj6034kajje0s2cm3r0"
39 | .parse()
40 | .unwrap();
41 | }
42 |
43 | #[test]
44 | fn test_hex() {
45 | let _pubkey: Npub = "e51e0518f4e2d184007b669fec0d9bb1cd1b2a6c27267ab4df969f1adbb2965f"
46 | .parse()
47 | .unwrap();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [0.4.0] - 2023-11-24
6 |
7 | ### Bug Fixes
8 |
9 | - do not redownload last event every index
10 |
11 | - Re-download record events after reindex
12 |
13 | - Better relay handling
14 |
15 | - publis command should not use queue
16 |
17 |
18 | ### Features
19 |
20 | - Updated config file format for index publishing.
21 |
22 | - Validate config file on startup.
23 |
24 | - NOM-04 support, relay publishing + .well-known
25 |
26 | - Version subcomand #18
27 |
28 | - record v1 upgrade block info
29 |
30 | - UI will now warn users when attempting a transfer on a name that doesn't exist or shouldn't be transferred
31 |
32 | - Added relays key to .well-known/nomen.json, per NOM-04 addition.
33 |
34 | - "rebroadcast" command will rebroadcast known record events
35 |
36 | - publish command to publish full relay index
37 |
38 |
39 | ### Other
40 |
41 | - Preparing release v0.4.0
42 |
43 | ### Refactor
44 |
45 | - Refactor: Refactored db module to sub-modules (#25)
46 |
47 | - Refactor: Some tweaks to db submodule refactoring
48 |
49 | - Refactor: Additional factoring on the db module
50 |
51 |
52 | ### Testing
53 |
54 | - Test: Rewrote and refactored tests to base them on official test vectors (#24)
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/nomen/templates/transfer/sign.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 | {% if error.is_some() %}
6 | {{ error.clone().unwrap_or_default() }}
7 | {% else %}
8 |
Sign Transfer
9 |
10 |
This transfer must be authorized by the current owner. A signature will be generated by signing a dummy Nostr event
11 | with your NIP-07 extension.
12 |
13 |
23 |
24 | {% endif %}
25 |
26 |
27 |
40 | {% endblock %}
--------------------------------------------------------------------------------
/nomen/src/subcommands/util.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use bitcoin::{
4 | psbt::{Output, Psbt},
5 | script::PushBytesBuf,
6 | ScriptBuf, TxOut,
7 | };
8 | use nomen_core::{CreateBuilder, NameKind, NsidBuilder};
9 | use nostr_sdk::{EventBuilder, Tag, TagKind, UnsignedEvent};
10 | use secp256k1::XOnlyPublicKey;
11 |
12 | pub fn extend_psbt(psbt: &mut Psbt, name: &str, pubkey: &XOnlyPublicKey) {
13 | let data = CreateBuilder::new(pubkey, name).v1_op_return();
14 | let mut pb = PushBytesBuf::new();
15 | pb.extend_from_slice(&data).expect("OP_RETURN fail");
16 | let data = ScriptBuf::new_op_return(&pb);
17 | psbt.outputs.push(Output {
18 | witness_script: Some(data.clone()),
19 | ..Default::default()
20 | });
21 | psbt.unsigned_tx.output.push(TxOut {
22 | value: 0,
23 | script_pubkey: data,
24 | });
25 | }
26 |
27 | pub fn name_event(
28 | pubkey: XOnlyPublicKey,
29 | records: &HashMap,
30 | name: &str,
31 | ) -> anyhow::Result {
32 | let records = serde_json::to_string(&records)?;
33 | let nsid = NsidBuilder::new(name, &pubkey).finalize();
34 | let event = EventBuilder::new(
35 | NameKind::Name.into(),
36 | records,
37 | &[
38 | Tag::Identifier(nsid.to_string()),
39 | Tag::Generic(TagKind::Custom("nom".to_owned()), vec![name.to_owned()]),
40 | ],
41 | )
42 | .to_unsigned_event(pubkey);
43 |
44 | Ok(event)
45 | }
46 |
--------------------------------------------------------------------------------
/docs/HOWTO.md:
--------------------------------------------------------------------------------
1 | # How To Get A Name
2 |
3 | What you will need:
4 |
5 | 1. A Bitcoin UTXO
6 | 2. A wallet to sign a PSBT
7 | 3. A keypair (any schnorr-compatible Bitcoin or Nostr keypair will work)
8 | * If you need one, use optional step below.
9 |
10 | ## Using the Explorer
11 |
12 | 1. Construct an unsigned PSBT with your Bitcoin wallet.
13 | 2. Visit https://nomenexplorer.com
14 | 3. Click `New Name`.
15 | 3. Paste the base64-encoded PSBT into the form.
16 | 6. Enter the name you wish to reserve and the pubkey of the owner.
17 | * __Note:__ If you have a NIP-07 compatible browser extension, you can click "Use NIP-07" and it will obtain the public key from your browser extension.
18 | 7. Click `Submit` and it will build a new, unsigned transaction for you. Copy the transaction to sign and broadcast it with your wallet.
19 | 8. After broadcasting the transaction, click `setup your records` to build a new nostr records event.
20 | 9. Enter the records you wish to include. Each record must be on its own line and look like this `KEY=value`.
21 | 10. Enter you public key again, or use your NIP-07 extension.
22 | 11. Click `Create Event` and you will be presented with an unsigned Nostr event.
23 | 12. Clicking `Sign and Broadcast` will use your NIP-07 extension to sign the event and broadcast it to relays.
24 |
25 | Alternatively, if you don't want or have an unsigned PSBT, you can skip filling in the PSBT. If you don't fill it in, the form will just return a hex-encoded `OP_RETURN` script. You can paste this into a wallet that is compatible with `OP_RETURN` outputs like Bitcoin Core, Electrum, Trezor, etc.
--------------------------------------------------------------------------------
/nomen/src/util/nsec.rs:
--------------------------------------------------------------------------------
1 | use std::{fmt::Display, str::FromStr};
2 |
3 | use nostr_sdk::{prelude::FromSkStr, Keys, ToBech32};
4 | use secp256k1::SecretKey;
5 | use serde::Serialize;
6 |
7 | #[derive(Debug, Clone, Copy, Serialize, serde_with::DeserializeFromStr)]
8 |
9 | pub struct Nsec(SecretKey);
10 |
11 | impl AsRef for Nsec {
12 | fn as_ref(&self) -> &SecretKey {
13 | &self.0
14 | }
15 | }
16 |
17 | impl FromStr for Nsec {
18 | type Err = anyhow::Error;
19 |
20 | fn from_str(s: &str) -> Result {
21 | let keys = Keys::from_sk_str(s)?;
22 | Ok(Nsec(keys.secret_key().expect("Secret key required")))
23 | }
24 | }
25 |
26 | impl Display for Nsec {
27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 | f.write_str(&self.0.to_bech32().expect("Unable to format as bech32"))
29 | }
30 | }
31 |
32 | impl From for Nsec {
33 | fn from(value: SecretKey) -> Self {
34 | Nsec(value)
35 | }
36 | }
37 |
38 | impl From for SecretKey {
39 | fn from(value: Nsec) -> Self {
40 | value.0
41 | }
42 | }
43 |
44 | #[cfg(test)]
45 | mod tests {
46 | use super::*;
47 |
48 | #[test]
49 | fn test_nsec() {
50 | let _nsec: Nsec = "nsec18meshnlpsyl6qpq4jkwh9hks3v4uprp44las83akz6xfndc9tx2q646wuk"
51 | .parse()
52 | .unwrap();
53 | }
54 |
55 | #[test]
56 | fn test_hex() {
57 | let _nsec: Nsec = "3ef30bcfe1813fa00415959d72ded08b2bc08c35affb03c7b6168c99b7055994"
58 | .parse()
59 | .unwrap();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/nomen/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nomen"
3 | version = "0.4.0"
4 | edition = "2021"
5 | build = "build.rs"
6 | default-run = "nomen"
7 | rust-version = "1.71"
8 | repository = "https://github.com/ursuscamp/nomen"
9 |
10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
11 |
12 | [dependencies]
13 | anyhow = "1.0.75"
14 | askama = {version = "0.12.0", features = ["with-axum", "serde-json"]}
15 | askama_axum = "0.3.0"
16 | axum = {version = "0.6.11"}
17 | axum-extra = "0.7.4"
18 | clap = { version = "4.1.8", features = ["derive"] }
19 | hex = { version = "0.4.3", features = ["serde"] }
20 | nomen_core = { path = "../nomen_core" }
21 | nostr-sdk = "0.24.0"
22 | rand = { version = "0.8.5", features = ["serde"] }
23 | secp256k1 = { version = "0.27.0", features = ["rand-std", "bitcoin-hashes"] }
24 | serde = { version = "1.0.188", features = ["derive"] }
25 | serde_json = "1.0.107"
26 | tokio = { version = "1.32.0", features = ["full"] }
27 | toml = "0.8.0"
28 | yansi = "0.5.1"
29 | sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite"] }
30 | bitcoin = { version = "0.30.1", features = ["base64", "rand", "serde"] }
31 | elegant-departure = { version = "0.2.1", features = ["tokio"] }
32 | itertools = "0.11.0"
33 | bitcoincore-rpc = "0.17.0"
34 | futures = "0.3.28"
35 | tracing = "0.1.37"
36 | tracing-subscriber = "0.3.17"
37 | time = { version = "0.3.20", features = ["formatting", "macros"] }
38 | tower-http = { version = "0.4.4", features = ["cors"] }
39 | serde_with = "3.4.0"
40 |
41 |
42 | [build-dependencies]
43 | vergen = { version = "8.0.0", features = ["build", "git", "gitcl"] }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nomen Explorer
2 |
3 | Nomen is a protocol for globally unique, decentralized "domain names". The Nomen Explorer is the first indexer (or name server) for this protocol.
4 |
5 | Try it [here](https://nomenexplorer.com)! You can explore existing names or create a new one. Note: You will need to sign and broadcast a Bitcoin transaction with your wallet to do it.
6 |
7 | If you download the project yourself, you can build it and run the indexer for your own use, or use the CLI to experiment with Nomen.
8 |
9 | ## What is Nomen?
10 |
11 | Nomen is a protocol for globally unique names, like DNS, except decentralized and based on Bitcoin and Nostr. Instead of a central authority deciding who controls a name, the protocol provides simple rules to determine the owner.
12 |
13 | At a high level, claims to a name are published to the Bitcoin blockchain (think of this as registering a domain name). Bitcoin provides the ordering guarantees. The first to claim a name owns it. Published along with the name is the public key of the owner.
14 |
15 | Owners then publish Nostr events signed with the same key to update their records (like their domain DNS records).
16 |
17 | With Bitcoin, there is no need to create a new blockchain or have a trusted third party. With Nostr, there's no need to bootstrap a new P2P transport layer.
18 |
19 | Read [the specs](https://github.com/ursuscamp/noms) for more details about the protocol itself. It's very simple.
20 |
21 | ## Documentation
22 |
23 | - [Changelog](CHANGELOG.md)
24 | - [Release Notes](RELEASE_NOTES.md)
25 |
26 | ## Setting up a dev environment
27 |
28 | Follow the steps in [DEVELOPMENT.md](./docs/DEVELOPMENT.md).
--------------------------------------------------------------------------------
/nomen/src/db/relay_index.rs:
--------------------------------------------------------------------------------
1 | use sqlx::Sqlite;
2 |
3 | pub async fn queue(
4 | conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy,
5 | name: &str,
6 | ) -> anyhow::Result<()> {
7 | sqlx::query("INSERT OR IGNORE INTO relay_index_queue (name) VALUES (?)")
8 | .bind(name)
9 | .execute(conn)
10 | .await?;
11 | Ok(())
12 | }
13 |
14 | #[derive(sqlx::FromRow, Debug)]
15 | pub struct Name {
16 | pub name: String,
17 | pub pubkey: String,
18 | pub records: String,
19 | }
20 |
21 | pub async fn fetch_all_queued(
22 | conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy,
23 | ) -> anyhow::Result> {
24 | let results = sqlx::query_as::<_, Name>(
25 | "SELECT vnr.name, vnr.pubkey, COALESCE(vnr.records, '{}') as records
26 | FROM valid_names_records_vw vnr
27 | JOIN relay_index_queue riq ON vnr.name = riq.name;",
28 | )
29 | .fetch_all(conn)
30 | .await?;
31 | Ok(results)
32 | }
33 |
34 | pub async fn fetch_all(
35 | conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy,
36 | ) -> anyhow::Result> {
37 | let results = sqlx::query_as::<_, Name>(
38 | "SELECT vnr.name, vnr.pubkey, COALESCE(vnr.records, '{}') as records
39 | FROM valid_names_records_vw vnr;",
40 | )
41 | .fetch_all(conn)
42 | .await?;
43 | Ok(results)
44 | }
45 |
46 | pub async fn delete(
47 | conn: impl sqlx::Executor<'_, Database = Sqlite> + Copy,
48 | name: &str,
49 | ) -> anyhow::Result<()> {
50 | sqlx::query("DELETE FROM relay_index_queue WHERE name = ?;")
51 | .bind(name)
52 | .execute(conn)
53 | .await?;
54 | Ok(())
55 | }
56 |
--------------------------------------------------------------------------------
/nomen_core/src/create.rs:
--------------------------------------------------------------------------------
1 | use crate::NomenKind;
2 | use nostr_sdk::{EventId, UnsignedEvent};
3 | use secp256k1::XOnlyPublicKey;
4 |
5 | use super::{CreateV0, CreateV1, Hash160, NsidBuilder};
6 |
7 | pub struct CreateBuilder<'a> {
8 | pub pubkey: &'a XOnlyPublicKey,
9 | pub name: &'a str,
10 | }
11 |
12 | impl<'a> CreateBuilder<'a> {
13 | pub fn new(pubkey: &'a XOnlyPublicKey, name: &'a str) -> CreateBuilder<'a> {
14 | CreateBuilder { pubkey, name }
15 | }
16 |
17 | pub fn v0_op_return(&self) -> Vec {
18 | let fingerprint = Hash160::default()
19 | .chain_update(self.name.as_bytes())
20 | .fingerprint();
21 | let nsid = NsidBuilder::new(self.name, self.pubkey).finalize();
22 | CreateV0 { fingerprint, nsid }.serialize()
23 | }
24 |
25 | pub fn v1_op_return(&self) -> Vec {
26 | CreateV1 {
27 | pubkey: *self.pubkey,
28 | name: self.name.to_string(),
29 | }
30 | .serialize()
31 | }
32 | }
33 |
34 | #[cfg(test)]
35 | mod tests {
36 | use super::*;
37 | #[test]
38 | fn test_op_returns() {
39 | let pk = "60de6fbc4a78209942c62706d904ff9592c2e856f219793f7f73e62fc33bfc18"
40 | .parse()
41 | .unwrap();
42 | let cb = CreateBuilder::new(&pk, "hello-world");
43 |
44 | assert_eq!(
45 | hex::encode(cb.v0_op_return()),
46 | "4e4f4d0000e5401df4b4273968a1e7be2ef0acbcae6f61d53e73101e2983"
47 | );
48 |
49 | assert_eq!(hex::encode(cb.v1_op_return()), "4e4f4d010060de6fbc4a78209942c62706d904ff9592c2e856f219793f7f73e62fc33bfc1868656c6c6f2d776f726c64");
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/nomen/templates/transfer/complete.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 |
6 | Here are two OP_RETURNs they must be included in Bitcoin blocks, in this order. The first
7 | OP_RETURN
8 | contains the information for the new owner, and the second contains the signature that authorizes the transfer.
9 | For now, Bitcoin standardness rules prevent multiple OP_RETURNs in a single transaction. Unless you
10 | have a miner connection, they will need to broadcast in separate transactions. The best way to ensure that they
11 | are mined in the correct order to include the first OP_RETURN in a transaction, then do a CPFP
12 | (Child-Pays-For-Parent) transaction from the new UTXO and include the second OP_RETURN
13 |
14 |
15 |
{{ data1 }}
16 |
17 |
18 |
19 |
20 |
{{ data2 }}
21 |
22 |
23 |
24 |
25 |
48 | {% endblock %}
--------------------------------------------------------------------------------
/nomen/src/db/raw.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::module_name_repetitions)]
2 |
3 | use bitcoin::{BlockHash, Txid};
4 | use sqlx::{sqlite::SqliteRow, Executor, FromRow, Row, Sqlite};
5 | use std::str::FromStr;
6 |
7 | pub struct RawBlockchain {
8 | pub blockhash: BlockHash,
9 | pub txid: Txid,
10 | pub blocktime: usize,
11 | pub blockheight: usize,
12 | pub txheight: usize,
13 | pub vout: usize,
14 | pub data: Vec,
15 | }
16 |
17 | impl FromRow<'_, SqliteRow> for RawBlockchain {
18 | fn from_row(row: &'_ SqliteRow) -> Result {
19 | Ok(RawBlockchain {
20 | blockhash: BlockHash::from_str(row.try_get("blockhash")?)
21 | .map_err(|e| sqlx::Error::Decode(Box::new(e)))?,
22 | txid: Txid::from_str(row.try_get("txid")?)
23 | .map_err(|e| sqlx::Error::Decode(Box::new(e)))?,
24 | blocktime: row.try_get::("blocktime")? as usize,
25 | blockheight: row.try_get::("blockheight")? as usize,
26 | txheight: row.try_get::("txheight")? as usize,
27 | vout: row.try_get::("vout")? as usize,
28 | data: hex::decode(row.try_get::("data")?)
29 | .map_err(|e| sqlx::Error::Decode(Box::new(e)))?,
30 | })
31 | }
32 | }
33 |
34 | pub async fn insert_raw_blockchain(
35 | conn: impl Executor<'_, Database = Sqlite>,
36 | raw: &RawBlockchain,
37 | ) -> anyhow::Result<()> {
38 | sqlx::query(include_str!("./queries/insert_raw_blockchain.sql"))
39 | .bind(raw.blockhash.to_string())
40 | .bind(raw.txid.to_string())
41 | .bind(raw.blocktime as i64)
42 | .bind(raw.blockheight as i64)
43 | .bind(raw.txheight as i64)
44 | .bind(raw.vout as i64)
45 | .bind(hex::encode(&raw.data))
46 | .execute(conn)
47 | .await?;
48 | Ok(())
49 | }
50 |
--------------------------------------------------------------------------------
/nomen_core/src/nsid.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt::{Debug, Display},
3 | io::Read,
4 | str::FromStr,
5 | };
6 |
7 | use bitcoin::secp256k1::XOnlyPublicKey;
8 | use derive_more::{AsMut, AsRef, Deref, DerefMut, From};
9 | use nostr_sdk::Event;
10 |
11 | use super::{EventExtractor, NameKind, NsidBuilder};
12 |
13 | #[derive(
14 | Clone, Copy, Deref, DerefMut, AsRef, AsMut, From, Eq, PartialEq, serde_with::DeserializeFromStr,
15 | )]
16 | pub struct Nsid([u8; 20]);
17 |
18 | impl Nsid {
19 | #[allow(dead_code)]
20 | pub fn from_slice(bytes: &[u8]) -> Result {
21 | Ok(Nsid(bytes.try_into()?))
22 | }
23 | }
24 |
25 | impl TryFrom<&[u8]> for Nsid {
26 | type Error = super::UtilError;
27 |
28 | fn try_from(value: &[u8]) -> Result {
29 | Nsid::from_slice(value)
30 | }
31 | }
32 |
33 | impl TryFrom for Nsid {
34 | type Error = super::UtilError;
35 |
36 | fn try_from(event: Event) -> Result {
37 | let nk: NameKind = event.kind.try_into()?;
38 | let name = event.extract_name()?;
39 | let builder = match nk {
40 | NameKind::Name => NsidBuilder::new(&name, &event.pubkey),
41 | };
42 | Ok(builder.finalize())
43 | }
44 | }
45 |
46 | impl FromStr for Nsid {
47 | type Err = super::UtilError;
48 |
49 | fn from_str(s: &str) -> Result {
50 | let mut out = [0u8; 20];
51 | hex::decode_to_slice(s, &mut out)?;
52 | Ok(Nsid(out))
53 | }
54 | }
55 |
56 | impl Debug for Nsid {
57 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 | f.debug_tuple("Pubkey").field(&hex::encode(self.0)).finish()
59 | }
60 | }
61 |
62 | impl Display for Nsid {
63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 | write!(f, "{}", hex::encode(self.0))
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.2.0.md:
--------------------------------------------------------------------------------
1 | ## 0.2.0
2 |
3 | This release includes a database migration, so make sure to back up your index before upgrading.
4 |
5 | Features:
6 | - Transfers have been removed, and names have been limited to 43 characters for vesion `0x00`. They will be enabled in the next version with a better designed.
7 | - Primal.net is now used to npub links.
8 | - New page to list blockchain claims for which there are no indexed record events.
9 | - Index statistic page.
10 |
11 | Bugs:
12 | - Fixed a bug where a name was double-indexed because the same `OP_RETURN` was uploaded twice
13 |
14 | ## 0.2.0
15 |
16 | This release includes a database migration, so make sure to back up your index before upgrading.
17 |
18 | Features:
19 | - Transfers have been removed, and names have been limited to 43 characters for vesion `0x00`. They will be enabled in the next version with a better designed.
20 | - Primal.net is now used to npub links.
21 | - New page to list blockchain claims for which there are no indexed record events.
22 | - Index statistic page.
23 |
24 | Bugs:
25 | - Fixed a bug where a name was double-indexed because the same `OP_RETURN` was uploaded twice
26 |
27 | ## 0.1.1
28 |
29 | Features:
30 | - Explorer now links to a name instead of a NSID. This simply makes it easier for a something to be bookmarked, even after a transfer.
31 | - Explorer web UI and CLI both automatically capitalizes the keys in records now.
32 | - Name page: Update Records link added, which automatically preloads data for user to update, including most recent record set.
33 | - Name page: Blockhash and Txid link to block explorer mempool.space.
34 | - Name page: Links for different record types. For example, `WEB` record links to actual webpage.
35 | - Name page: MOTD records now have a little but of decorative quoting.
36 | - The Search bar strips whitespace.
37 |
38 | Bugs:
39 | - Indexer will not longer stop randomly.
40 |
41 | Other:
42 | - Added `WEB` record type to spec.
43 | - Changes "New Records" to "Update Records" everywhere.
44 | - More detailed help instructions.
45 |
46 | ## 0.1.0
47 |
48 | - Initial release.
--------------------------------------------------------------------------------
/nomen/src/main.rs:
--------------------------------------------------------------------------------
1 | #![warn(
2 | clippy::suspicious,
3 | clippy::complexity,
4 | clippy::perf,
5 | clippy::style,
6 | clippy::pedantic
7 | )]
8 | #![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
9 |
10 | mod config;
11 | mod db;
12 | mod subcommands;
13 | mod util;
14 |
15 | use anyhow::bail;
16 | use clap::Parser;
17 |
18 | use config::Config;
19 |
20 | #[tokio::main]
21 | async fn main() -> anyhow::Result<()> {
22 | // No log output by default
23 | if std::env::var("RUST_LOG").is_err() {
24 | std::env::set_var("RUST_LOG", "off");
25 | }
26 |
27 | tracing_subscriber::fmt::init();
28 | let config = parse_config()?;
29 |
30 | let pool = db::initialize(&config).await?;
31 |
32 | match &config.cli.subcommand {
33 | config::Subcommand::Init => subcommands::init()?,
34 | config::Subcommand::Index => subcommands::index(&config).await?,
35 | config::Subcommand::Server => subcommands::start(&config, &pool).await?,
36 | config::Subcommand::Reindex { blockheight } => {
37 | subcommands::reindex(&config, &pool, blockheight.unwrap_or_default()).await?;
38 | }
39 | config::Subcommand::Rescan { blockheight } => {
40 | subcommands::rescan(&config, &pool, blockheight.unwrap_or_default()).await?;
41 | }
42 | config::Subcommand::Rebroadcast => {
43 | subcommands::rebroadcast(&config, &pool).await?;
44 | }
45 | config::Subcommand::Publish => subcommands::publish(&config, &pool).await?,
46 | config::Subcommand::Version => {
47 | subcommands::version();
48 | }
49 | }
50 |
51 | Ok(())
52 | }
53 |
54 | fn parse_config() -> anyhow::Result {
55 | let cli = config::Cli::parse();
56 |
57 | let file = if cli.config.is_file() {
58 | let config_str = std::fs::read_to_string(&cli.config)?;
59 |
60 | toml::from_str(&config_str)?
61 | } else {
62 | tracing::error!("Config file not found.");
63 | bail!("Missing config file.")
64 | };
65 |
66 | let config = Config::new(cli, file);
67 | config.validate()?;
68 |
69 | tracing::debug!("Config loaded: {config:?}");
70 |
71 | Ok(config)
72 | }
73 |
--------------------------------------------------------------------------------
/nomen/src/subcommands/index/events/records.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use nomen_core::NameKind;
4 | use nostr_sdk::{Event, Filter};
5 | use sqlx::SqlitePool;
6 |
7 | use crate::{config::Config, db, subcommands::index::events::EventData};
8 |
9 | pub async fn records(config: &Config, pool: &SqlitePool) -> anyhow::Result<()> {
10 | tracing::info!("Beginning indexing record events.");
11 | let events = latest_events(config, pool).await?;
12 | for event in events {
13 | match EventData::from_event(&event) {
14 | Ok(ed) => save_event(pool, ed).await?,
15 | Err(err) => tracing::debug!("Invalid event: {err}"),
16 | }
17 | }
18 |
19 | tracing::info!("Records events indexing complete.");
20 | Ok(())
21 | }
22 |
23 | async fn save_event(pool: &SqlitePool, ed: EventData) -> anyhow::Result<()> {
24 | tracing::info!("Saving valid event {}", ed.event_id);
25 | let EventData {
26 | event_id,
27 | fingerprint,
28 | nsid: _,
29 | calculated_nsid,
30 | pubkey,
31 | name,
32 | created_at,
33 | raw_content,
34 | records: _,
35 | raw_event,
36 | } = ed;
37 | db::name::insert_name_event(
38 | pool,
39 | name.clone(),
40 | fingerprint,
41 | calculated_nsid,
42 | pubkey,
43 | created_at,
44 | event_id,
45 | raw_content,
46 | raw_event,
47 | )
48 | .await?;
49 |
50 | db::index::update_v0_index(pool, name.as_ref(), &pubkey, calculated_nsid).await?;
51 |
52 | db::relay_index::queue(pool, name.as_ref()).await?;
53 |
54 | Ok(())
55 | }
56 |
57 | async fn latest_events(
58 | config: &Config,
59 | pool: &sqlx::Pool,
60 | ) -> anyhow::Result> {
61 | let records_time = db::name::last_records_time(pool).await? + 1;
62 | let filter = Filter::new()
63 | .kind(NameKind::Name.into())
64 | .since(records_time.into());
65 |
66 | let (_keys, client) = config.nostr_random_client().await?;
67 | let events = client
68 | .get_events_of(vec![filter], Some(Duration::from_secs(10)))
69 | .await?;
70 | client.disconnect().await?;
71 | Ok(events)
72 | }
73 |
--------------------------------------------------------------------------------
/nomen_core/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(unused)]
2 |
3 | mod create;
4 | mod extractor;
5 | mod hash160;
6 | mod kind;
7 | mod name;
8 | mod nsid;
9 | mod nsid_builder;
10 | mod transfer;
11 |
12 | pub use create::*;
13 | pub use extractor::*;
14 | pub use hash160::*;
15 | pub use kind::*;
16 | pub use name::*;
17 | pub use nsid::*;
18 | pub use nsid_builder::*;
19 | pub use transfer::*;
20 |
21 | #[derive(thiserror::Error, Debug)]
22 | pub enum UtilError {
23 | #[error("not a nomen transaction")]
24 | NotNomenError,
25 | #[error("unsupported nomen version")]
26 | UnsupportedNomenVersion,
27 | #[error("unexpectex tx type")]
28 | UnexpectedNomenTxType,
29 | #[error("name validation")]
30 | NameValidation,
31 | #[error("unknown nomen kind: {:?}", .0)]
32 | NomenKind(String),
33 | #[error("invalid Key=Value")]
34 | InvalidKeyVal(String),
35 | #[error("invalid event kind")]
36 | InvalidEventKind(nostr_sdk::Kind),
37 | #[error("nostr event signing error")]
38 | UnsignedEventError(#[from] nostr_sdk::event::unsigned::Error),
39 | #[error("slice conversion")]
40 | TryFromSliceError(#[from] std::array::TryFromSliceError),
41 | #[error("hex conversion")]
42 | HexDecode(#[from] hex::FromHexError),
43 | #[error("nostr key")]
44 | NostrKeyError(#[from] nostr_sdk::key::Error),
45 | #[error("regex")]
46 | RegexError(#[from] regex::Error),
47 | #[error("secp256k1")]
48 | Secp256k1Error(#[from] secp256k1::Error),
49 | #[error("string error")]
50 | StringError(#[from] std::string::FromUtf8Error),
51 | #[error(transparent)]
52 | ExtractorError(#[from] ExtractorError),
53 | }
54 |
55 | pub enum NameKind {
56 | Name = 38300,
57 | }
58 |
59 | impl From for nostr_sdk::Kind {
60 | fn from(value: NameKind) -> Self {
61 | nostr_sdk::Kind::ParameterizedReplaceable(value as u16)
62 | }
63 | }
64 |
65 | impl TryFrom for NameKind {
66 | type Error = UtilError;
67 |
68 | fn try_from(value: nostr_sdk::Kind) -> Result {
69 | let nk = match value {
70 | nostr_sdk::Kind::ParameterizedReplaceable(38300) => NameKind::Name,
71 | _ => return Err(UtilError::InvalidEventKind(value)),
72 | };
73 | Ok(nk)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/nomen_core/src/hash160.rs:
--------------------------------------------------------------------------------
1 | use ripemd::{Digest, Ripemd160};
2 | use sha2::Sha256;
3 |
4 | #[derive(Default)]
5 | pub struct Hash160 {
6 | hasher: Sha256,
7 | }
8 |
9 | #[allow(unused)]
10 | impl Hash160 {
11 | pub fn update(&mut self, data: &[u8]) {
12 | self.hasher.update(data);
13 | }
14 |
15 | pub fn chain_update(mut self, data: &[u8]) -> Hash160 {
16 | self.update(data);
17 | self
18 | }
19 |
20 | #[allow(dead_code)]
21 | pub fn chain_optional(mut self, data: &Option<&[u8]>) -> Hash160 {
22 | if let Some(data) = data {
23 | self.update(data);
24 | }
25 | self
26 | }
27 |
28 | pub fn finalize(self) -> [u8; 20] {
29 | let f = self.hasher.finalize();
30 | Ripemd160::digest(f)
31 | .try_into()
32 | .expect("Hash160 struct should return 20 bytes")
33 | }
34 |
35 | pub fn fingerprint(self) -> [u8; 5] {
36 | let h = self.finalize();
37 | h[..5].try_into().unwrap()
38 | }
39 |
40 | pub fn digest(data: &[u8]) -> [u8; 20] {
41 | Hash160::default().chain_update(data).finalize()
42 | }
43 |
44 | pub fn digest_slices(data: &[&[u8]]) -> [u8; 20] {
45 | data.iter()
46 | .fold(Hash160::default(), |acc, d| acc.chain_update(d))
47 | .finalize()
48 | }
49 | }
50 |
51 | #[cfg(test)]
52 | mod tests {
53 |
54 | use super::*;
55 |
56 | #[test]
57 | fn test_update() {
58 | let mut h = Hash160::default();
59 | h.update(b"hello");
60 | let d = hex::encode(h.finalize());
61 | assert_eq!(d, "b6a9c8c230722b7c748331a8b450f05566dc7d0f");
62 | }
63 |
64 | #[test]
65 | fn test_fingerprint() {
66 | let mut h = Hash160::default();
67 | h.update(b"hello");
68 | let d = hex::encode(h.fingerprint());
69 | assert_eq!(d, "b6a9c8c230");
70 | }
71 |
72 | #[test]
73 | fn test_digest() {
74 | assert_eq!(
75 | hex::encode(Hash160::digest(b"hello")),
76 | "b6a9c8c230722b7c748331a8b450f05566dc7d0f"
77 | );
78 | }
79 |
80 | #[test]
81 | fn test_digest_slices() {
82 | let hashed = hex::encode(Hash160::digest_slices(&[b"hello", b"world"]));
83 | assert_eq!(hashed, "b36c87f1c6d9182eb826d7d987f9081adf15b772");
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/nomen_core/src/transfer.rs:
--------------------------------------------------------------------------------
1 | use crate::NomenKind;
2 | use nostr_sdk::{EventId, UnsignedEvent};
3 | use secp256k1::{schnorr::Signature, XOnlyPublicKey};
4 |
5 | use super::{SignatureV1, TransferV1};
6 |
7 | pub struct TransferBuilder<'a> {
8 | pub new_pubkey: &'a XOnlyPublicKey,
9 | pub name: &'a str,
10 | }
11 |
12 | impl<'a> TransferBuilder<'a> {
13 | pub fn transfer_op_return(&self) -> Vec {
14 | TransferV1 {
15 | pubkey: *self.new_pubkey,
16 | name: self.name.to_string(),
17 | }
18 | .serialize()
19 | }
20 |
21 | pub fn unsigned_event(&self, prev_owner: &XOnlyPublicKey) -> nostr_sdk::UnsignedEvent {
22 | let created_at = 1u64.into();
23 | let kind: nostr_sdk::Kind = 1u64.into();
24 | let content = format!("{}{}", hex::encode(prev_owner.serialize()), self.name);
25 | let id = EventId::new(prev_owner, created_at, &kind, &[], &content);
26 |
27 | UnsignedEvent {
28 | id,
29 | pubkey: *prev_owner,
30 | created_at,
31 | kind,
32 | tags: vec![],
33 | content,
34 | }
35 | }
36 |
37 | pub fn signature_op_return(&self, keys: nostr_sdk::Keys) -> Result, super::UtilError> {
38 | let unsigned_event = self.unsigned_event(&keys.public_key());
39 | let event = unsigned_event.sign(&keys)?;
40 | Ok(SignatureV1 {
41 | signature: event.sig,
42 | }
43 | .serialize())
44 | }
45 |
46 | pub fn signature_provided_op_return(&self, signature: Signature) -> Vec {
47 | SignatureV1 { signature }.serialize()
48 | }
49 | }
50 |
51 | #[cfg(test)]
52 | mod tests {
53 | use std::str::FromStr;
54 |
55 | use super::*;
56 |
57 | #[test]
58 | fn test_op_returns() {
59 | let new_pubkey = XOnlyPublicKey::from_str(
60 | "74301b9c5d30b764bca8d3eb4febb06862f558d292fde93b4a290d90850bac91",
61 | )
62 | .unwrap();
63 | let tb = TransferBuilder {
64 | new_pubkey: &new_pubkey,
65 | name: "hello-world",
66 | };
67 |
68 | assert_eq!(hex::encode(tb.transfer_op_return()), "4e4f4d010174301b9c5d30b764bca8d3eb4febb06862f558d292fde93b4a290d90850bac9168656c6c6f2d776f726c64");
69 |
70 | // Signatures are not consistent, so they can't really be tested here.
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/nomen/src/subcommands/mod.rs:
--------------------------------------------------------------------------------
1 | mod index;
2 | mod server;
3 | pub mod util;
4 |
5 | pub use index::*;
6 | use nostr_sdk::Event;
7 | pub use server::*;
8 | use sqlx::SqlitePool;
9 |
10 | use crate::{
11 | config::{Config, ConfigFile},
12 | db,
13 | };
14 |
15 | pub(crate) fn init() -> anyhow::Result<()> {
16 | let config_file = ConfigFile::example();
17 | let cfg = toml::to_string(&config_file)?;
18 | println!("{cfg} ");
19 | Ok(())
20 | }
21 |
22 | pub(crate) async fn reindex(
23 | _config: &Config,
24 | pool: &SqlitePool,
25 | blockheight: i64,
26 | ) -> anyhow::Result<()> {
27 | println!("Re-indexing blockchain from blockheight {blockheight}.");
28 | db::index::reindex(pool, blockheight).await?;
29 | Ok(())
30 | }
31 |
32 | pub(crate) async fn rescan(
33 | _config: &Config,
34 | pool: &SqlitePool,
35 | blockheight: i64,
36 | ) -> anyhow::Result<()> {
37 | println!("Re-scanning blockchain from blockheight {blockheight}.");
38 | db::index::reindex(pool, blockheight).await?;
39 | sqlx::query("DELETE FROM index_height WHERE blockheight >= ?;")
40 | .bind(blockheight)
41 | .execute(pool)
42 | .await?;
43 | sqlx::query("DELETE FROM raw_blockchain WHERE blockheight >= ?;")
44 | .bind(blockheight)
45 | .execute(pool)
46 | .await?;
47 |
48 | Ok(())
49 | }
50 |
51 | pub(crate) fn version() {
52 | let version = env!("CARGO_PKG_VERSION");
53 | println!("Current version is {version}");
54 | }
55 |
56 | pub(crate) async fn rebroadcast(config: &Config, pool: &SqlitePool) -> anyhow::Result<()> {
57 | let events = sqlx::query_as::<_, (String,)>(
58 | "select ne.raw_event from valid_names_vw vn join name_events ne on vn.nsid = ne.nsid;",
59 | )
60 | .fetch_all(pool)
61 | .await?;
62 | println!(
63 | "Rebroadcasing {} events to {} relays",
64 | events.len(),
65 | config.relays().len()
66 | );
67 | let (_, client) = config.nostr_random_client().await?;
68 | for (event,) in events {
69 | let event = Event::from_json(event)?;
70 | client.send_event(event).await?;
71 | }
72 |
73 | Ok(())
74 | }
75 |
76 | pub(crate) async fn publish(config: &Config, pool: &SqlitePool) -> anyhow::Result<()> {
77 | println!("Publishing full relay index");
78 | index::events::relay_index::publish(config, pool, false).await
79 | }
80 |
--------------------------------------------------------------------------------
/nomen-cli/src/main.rs:
--------------------------------------------------------------------------------
1 | #![allow(unused)]
2 |
3 | mod nostr;
4 |
5 | use clap::Parser;
6 | use nomen_core::TransferBuilder;
7 | use nostr::{Npub, Nsec};
8 | use nostr_sdk::{Keys, ToBech32, UnsignedEvent};
9 | use secp256k1::{Secp256k1, SecretKey, XOnlyPublicKey};
10 |
11 | pub fn main() -> anyhow::Result<()> {
12 | let ops = Ops::parse();
13 |
14 | handle_ops(ops)?;
15 |
16 | Ok(())
17 | }
18 |
19 | fn handle_ops(ops: Ops) -> anyhow::Result<()> {
20 | match ops.command {
21 | Commands::Keys { pubkey, nostr } => cmd_keys(pubkey, nostr)?,
22 | Commands::Transfer { old, new, name } => cmd_transfer(old, new, name)?,
23 | }
24 |
25 | Ok(())
26 | }
27 |
28 | fn cmd_keys(pubkey: bool, nostr: bool) -> anyhow::Result<()> {
29 | let keys = nostr_sdk::Keys::generate();
30 | let (sk, pk) = if nostr {
31 | (
32 | keys.secret_key()?.to_bech32()?,
33 | keys.public_key().to_bech32()?,
34 | )
35 | } else {
36 | (
37 | keys.secret_key()?.display_secret().to_string(),
38 | keys.public_key().to_string(),
39 | )
40 | };
41 | println!("SK: {sk}");
42 | if pubkey {
43 | println!("PK: {pk}");
44 | }
45 | Ok(())
46 | }
47 |
48 | fn cmd_transfer(old: Nsec, new: Npub, name: String) -> anyhow::Result<()> {
49 | let tb = TransferBuilder {
50 | new_pubkey: new.as_ref(),
51 | name: &name,
52 | };
53 | let keys = nostr_sdk::Keys::new(*old.as_ref());
54 | let or1 = tb.transfer_op_return();
55 | let or2 = tb.signature_op_return(keys)?;
56 | println!("{}\n{}", hex::encode(or1), hex::encode(or2));
57 | Ok(())
58 | }
59 |
60 | #[derive(clap::Parser)]
61 | struct Ops {
62 | #[command(subcommand)]
63 | command: Commands,
64 | }
65 |
66 | #[derive(clap::Subcommand)]
67 | enum Commands {
68 | /// Generate Schnorr keypairs.
69 | Keys {
70 | #[arg(short, long)]
71 | pubkey: bool,
72 |
73 | #[arg(short, long)]
74 | nostr: bool,
75 | },
76 |
77 | /// Generate properly formatted OP_RETURNs for a name transfer.
78 | Transfer {
79 | /// Hex-encoded or bech32 (nsec) secret key for the current (previous) owner
80 | old: Nsec,
81 |
82 | /// Hex-encodced or bech32 (npub) public key for the new owner
83 | new: Npub,
84 |
85 | /// Name to transfer
86 | name: String,
87 | },
88 | }
89 |
--------------------------------------------------------------------------------
/nomen/src/subcommands/index/events/relay_index.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use nostr_sdk::{EventBuilder, Keys, Tag};
4 | use secp256k1::SecretKey;
5 | use serde::Serialize;
6 | use sqlx::SqlitePool;
7 |
8 | use crate::{
9 | config::Config,
10 | db::{self, relay_index::Name},
11 | };
12 |
13 | pub async fn publish(config: &Config, pool: &SqlitePool, use_queue: bool) -> anyhow::Result<()> {
14 | if !config.publish_index() {
15 | return Ok(());
16 | }
17 | let sk: SecretKey = config
18 | .secret_key()
19 | .expect("Missing config validation for secret")
20 | .into();
21 | let keys = Keys::new(sk);
22 | let (_, client) = config.nostr_random_client().await?;
23 |
24 | tracing::info!("Publishing relay index.");
25 | let names = if use_queue {
26 | db::relay_index::fetch_all_queued(pool).await?
27 | } else {
28 | db::relay_index::fetch_all(pool).await?
29 | };
30 | send_events(pool, names, keys, &client).await?;
31 | tracing::info!("Publishing relay index complete.");
32 |
33 | client.disconnect().await.ok();
34 | Ok(())
35 | }
36 |
37 | async fn send_events(
38 | conn: &SqlitePool,
39 | names: Vec,
40 | keys: Keys,
41 | client: &nostr_sdk::Client,
42 | ) -> Result<(), anyhow::Error> {
43 | for name in names {
44 | let records: HashMap = serde_json::from_str(&name.records)?;
45 | let content = Content {
46 | name: name.name.clone(),
47 | pubkey: name.pubkey,
48 | records,
49 | };
50 | let content_serialize = serde_json::to_string(&content)?;
51 | let event = EventBuilder::new(
52 | nostr_sdk::Kind::ParameterizedReplaceable(38301),
53 | content_serialize,
54 | &[Tag::Identifier(name.name.clone())],
55 | )
56 | .to_event(&keys)?;
57 |
58 | match client.send_event(event.clone()).await {
59 | Ok(s) => {
60 | tracing::info!("Broadcast event id {s}");
61 | db::relay_index::delete(conn, &name.name).await?;
62 | }
63 | Err(e) => {
64 | tracing::error!(
65 | "Unable to broadcast event {} during relay index publish: {e}",
66 | event.id
67 | );
68 | }
69 | }
70 | }
71 | Ok(())
72 | }
73 |
74 | #[derive(Serialize)]
75 | struct Content {
76 | name: String,
77 | pubkey: String,
78 | records: HashMap,
79 | }
80 |
--------------------------------------------------------------------------------
/nomen/templates/updaterecords.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 |
6 |
7 |
Update Records
8 | {% if !unsigned_event.is_empty() %}
9 |
10 |
The following event was created. You can use a NIP-07 browser extension to sign and broadcast this event, using the
11 | same keypair that was used to register the name on the blockchain.
33 | You can upgrade an old-style v0 name to v1 by simply recreating it. As long as the name and pubkey match, the
34 | protocol will treat it as an upgrade.
35 |
36 | {% endif %}
37 |
38 |
39 | You have two options:
40 |
41 |
Create an unsigned PSBT (partially signed Bitcoin transaction) and paste it below. This will modify the PSBT by
42 | adding an
43 | additional zero value OP_RETURN output. Make sure to slightly over-estimate the fee to account for
44 | the bit of extra data, and check the transaction before you sign and broadcast it!
45 |
Leave the PSBT field blank, and you will be given a hex-encoded OP_RETURN value which you can use
46 | in a Bitcoin wallet of your choice which supports it (Bitcoin Core, Electrum, etc).
47 |
48 |
49 |
50 |
51 | Once it is mined and has {{ confirmations }} confirmations, it will be indexed.
52 |
53 |
54 |
55 | In order for the indexer to properly index your name, you also need to send your records after you broadcast your
56 | transaction!
57 | You can comeback anytime and click on Update Records in the navigation menu.
58 |