Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bens): Added TLD-based fallback for name search in networks #1175

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c661327
updated function with serch by picking 5 names+tld
Ilyak777 Jan 6, 2025
f38fd92
deleted unnecessary empty lines
Ilyak777 Jan 6, 2025
a113537
Merge branch 'main' into feat/bens/add-tld-for-name-in-network-search
Ilyak777 Jan 6, 2025
e4376da
fixed spelling problems
Ilyak777 Jan 6, 2025
67cf6d7
Merge branch 'feat/bens/add-tld-for-name-in-network-search' of github…
Ilyak777 Jan 6, 2025
a2b3fed
used fmt to solve spelling issues
Ilyak777 Jan 6, 2025
99b159b
updated core function
Ilyak777 Jan 16, 2025
213c5ff
fixed spellings with rust rules
Ilyak777 Jan 16, 2025
4a7da33
updated code with clippy suggestions
Ilyak777 Jan 16, 2025
9e2e224
added first test cases for tld function
Ilyak777 Jan 16, 2025
34a79d7
added printsLn to capture moment where app crashes
Ilyak777 Jan 22, 2025
5e0e2ad
added new func to extract the exact domain
Ilyak777 Jan 23, 2025
46d13c1
updated after fmt command
Ilyak777 Jan 23, 2025
a9d0009
updated mock data
Ilyak777 Jan 23, 2025
3f746a5
added map_err and fixed with fmt
Ilyak777 Jan 23, 2025
5d5755b
removed Rabbit suggestion with mapping error
Ilyak777 Jan 23, 2025
57c745d
deleted all pintLn from protocoler and domain
Ilyak777 Jan 23, 2025
6c41165
returned variables to readme
Ilyak777 Jan 23, 2025
5b461ba
Merge branch 'main' into feat/bens/add-tld-for-name-in-network-search
Ilyak777 Jan 23, 2025
9bfeb5d
Update blockscout-ens/bens-logic/src/protocols/protocoler.rs
Ilyak777 Jan 24, 2025
506c310
updated due to comments in review
Ilyak777 Jan 27, 2025
64213c9
pulled main into this branch
Ilyak777 Jan 27, 2025
062a4ec
added abcnews with another domain to seed file and added prints
Ilyak777 Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 37 additions & 39 deletions blockscout-ens/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,32 @@ Service is **multi-chain**, meaning that only one instance of `graph-node`, `pos

## Current supported domains

| Subgraph Name | Network | TLD | Note |
|--------------|---------|-----|------|
| ens-subgraph | Ethereum | .eth | |
| rns-subgraph | Rootstock | .rsk | |
| genome-subgraph | Gnosis | .gno | SpaceID contracts |
| bns-subgraph | Base | .base | |
| mode-subgraph | Mode | .mode | SpaceID contracts |
| lightlink-subgraph | Lightlink | .ll | SpaceID contracts |
| zns-subgraph | Polygon | .poly | |
| d3-connect-subgraph | Shibarium | .shib | |

| Subgraph Name | Network | TLD | Note |
| ------------------- | --------- | ----- | ----------------- |
| ens-subgraph | Ethereum | .eth | |
| rns-subgraph | Rootstock | .rsk | |
| genome-subgraph | Gnosis | .gno | SpaceID contracts |
| bns-subgraph | Base | .base | |
| mode-subgraph | Mode | .mode | SpaceID contracts |
| lightlink-subgraph | Lightlink | .ll | SpaceID contracts |
| zns-subgraph | Polygon | .poly | |
| d3-connect-subgraph | Shibarium | .shib | |

## Envs

[anchor]: <> (anchors.envs.start.envs_main)

| Variable | Req&#x200B;uir&#x200B;ed | Description | Default value |
| --- | --- | --- | --- |
| `BENS__DATABASE__CONNECT__URL` | true | e.g. `postgresql://postgres:postgres@localhost:5432/postgres` | |
| `BENS__DATABASE__CREATE_DATABASE` | | | `false` |
| `BENS__DATABASE__RUN_MIGRATIONS` | | | `false` |
| `BENS__SERVER__HTTP__ADDR` | | | `0.0.0.0:8050` |
| `BENS__SERVER__HTTP__ENABLED` | | | `true` |
| `BENS__SERVER__HTTP__MAX_BODY_SIZE` | | | `2097152` |
| `BENS__SUBGRAPHS_READER__REFRESH_CACHE_SCHEDULE` | | | `0 0 * * * *` |
| `BENS__TRACING__ENABLED` | | | `true` |
| `BENS__TRACING__FORMAT` | | | `default` |
| Variable | Req&#x200B;uir&#x200B;ed | Description | Default value |
| ------------------------------------------------ | ------------------------ | ------------------------------------------------------------- | -------------- |
| `BENS__DATABASE__CONNECT__URL` | true | e.g. `postgresql://postgres:postgres@localhost:5432/postgres` | |
| `BENS__DATABASE__CREATE_DATABASE` | | | `false` |
| `BENS__DATABASE__RUN_MIGRATIONS` | | | `false` |
| `BENS__SERVER__HTTP__ADDR` | | | `0.0.0.0:8050` |
| `BENS__SERVER__HTTP__ENABLED` | | | `true` |
| `BENS__SERVER__HTTP__MAX_BODY_SIZE` | | | `2097152` |
| `BENS__SUBGRAPHS_READER__REFRESH_CACHE_SCHEDULE` | | | `0 0 * * * *` |
| `BENS__TRACING__ENABLED` | | | `true` |
| `BENS__TRACING__FORMAT` | | | `default` |

[anchor]: <> (anchors.envs.end.envs_main)

Expand All @@ -45,12 +44,11 @@ Service is **multi-chain**, meaning that only one instance of `graph-node`, `pos
1. Install [just](https://github.com/casey/just), [dotenv-cli](https://www.npmjs.com/package/dotenv-cli)

2. Run commands:
```bash
just graph-node-start
just deploy-subgraph ens-sepolia
just run-dev
```

```bash
just graph-node-start
just deploy-subgraph ens-sepolia
just run-dev
```

## Contribute

Expand All @@ -67,15 +65,16 @@ If you want to add your name service procol to blockscout you should:
```

6. Deploy subgraph to graph-node (read more in [how to deploy subgraphs guide](./graph-node/README.md#deploy-subgraph-to-graph-node))
```bash
just deploy-subgraph <protocol_name>
```

```bash
just deploy-subgraph <protocol_name>
```

7. Add protocol to [dev.json](./bens-server/config/dev.json) config and start `bens-server` connected to common database (read more in [how to start bens guide](./bens-server/README.md#to-start-locally))

```bash
just run-dev
```
```bash
just run-dev
```

8. Check that `bens-server` responses with valid domains. You can find swagger docs at [https://blockscout.github.io/swaggers/services/bens/main/index.html](https://blockscout.github.io/swaggers/services/bens/main/index.html)

Expand All @@ -84,8 +83,7 @@ If you want to add your name service procol to blockscout you should:
10. Update default config of BENS server for [production](./bens-server/config/prod.json) and [staging](./bens-server/config/staging.json)

11. Finally, create PR with:
* New directory inside `blockscout-ens/graph-node/subgraphs` with your subgraph code
* Updated BENS config
* Updated supported domains list
* Result of indexed data: proof that your indexed subgraph contains correct amount of domains, resolved_addresses and so on

- New directory inside `blockscout-ens/graph-node/subgraphs` with your subgraph code
- Updated BENS config
- Updated supported domains list
- Result of indexed data: proof that your indexed subgraph contains correct amount of domains, resolved_addresses and so on
131 changes: 116 additions & 15 deletions blockscout-ens/bens-logic/src/protocols/protocoler.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
blockscout::BlockscoutClient,
protocols::{DomainNameOnProtocol, ProtocolError},
protocols::{DomainName, DomainNameOnProtocol, ProtocolError},
};
use alloy::primitives::{Address, B256};
use anyhow::anyhow;
Expand Down Expand Up @@ -83,8 +83,8 @@ impl ProtocolSpecific {

pub fn empty_label_hash(&self) -> Option<B256> {
match self {
ProtocolSpecific::EnsLike(ens) => ens.empty_label_hash,
ProtocolSpecific::D3Connect(_) => None,
ProtocolSpecific::EnsLike(ens_like) => ens_like.empty_label_hash,
ProtocolSpecific::D3Connect(d3_connect) => d3_connect.empty_label_hash,
}
}

Expand Down Expand Up @@ -120,6 +120,7 @@ pub struct D3ConnectProtocol {
pub native_token_contract: Address,
#[serde(default)]
pub disable_offchain_resolve: bool,
pub empty_label_hash: Option<B256>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
Expand All @@ -145,6 +146,7 @@ pub enum AddressResolveTechnique {
#[serde(rename = "addr2name")]
Addr2Name,
}
const MAX_NAMES_LIMIT: usize = 5;

impl Tld {
pub fn new(tld: &str) -> Tld {
Expand Down Expand Up @@ -283,22 +285,84 @@ impl Protocoler {
Ok(protocols)
}

pub fn fetch_domain_options(
&self,
name_with_tld: &str,
network_id: i64,
maybe_filter: Option<NonEmpty<String>>,
) -> Result<Vec<DomainNameOnProtocol>, ProtocolError> {
let tld =
Tld::from_domain_name(name_with_tld).ok_or_else(|| ProtocolError::InvalidName {
name: name_with_tld.to_string(),
reason: "Invalid TLD".to_string(),
})?;

let protocols = self.protocols_of_network_for_tld(network_id, tld, maybe_filter)?;

let mut results = Vec::new();
for deployed_protocol in protocols {
let empty_label_hash = deployed_protocol
.protocol
.info
.protocol_specific
.empty_label_hash();

let domain_name = DomainName::new(name_with_tld, empty_label_hash)?;
results.push(DomainNameOnProtocol {
inner: domain_name,
deployed_protocol,
});
}

Ok(results)
}

pub fn names_options_in_network(
&self,
name: &str,
network_id: i64,
maybe_filter: Option<NonEmpty<String>>,
) -> Result<Vec<DomainNameOnProtocol>, ProtocolError> {
let tld = Tld::from_domain_name(name).ok_or_else(|| ProtocolError::InvalidName {
name: name.to_string(),
reason: "no tld found".to_string(),
})?;
let protocols = self.protocols_of_network_for_tld(network_id, tld, maybe_filter)?;
let names_with_protocols = protocols
.into_iter()
.map(|p| DomainNameOnProtocol::from_str(name, p))
.collect::<Result<_, _>>()?;
Ok(names_with_protocols)
if name.contains('.') {
let direct = self.fetch_domain_options(name, network_id, maybe_filter)?;
if direct.is_empty() {
Err(ProtocolError::InvalidName {
name: name.to_string(),
reason: "No matching protocols for given TLD".to_string(),
})
} else {
Ok(direct.into_iter().take(1).collect())
}
} else {
let tlds = self
.networks
.get(&network_id)
.ok_or_else(|| ProtocolError::NetworkNotFound(network_id))?
.use_protocols
.iter()
.filter_map(|protocol_name| self.protocols.get(protocol_name))
.flat_map(|protocol| protocol.info.tld_list.iter().cloned())
.collect::<Vec<Tld>>();

let all_names_with_protocols: Vec<_> = tlds
.into_iter()
.map(|tld| format!("{}.{}", name, tld.0))
.flat_map(|name_with_tld| {
self.fetch_domain_options(&name_with_tld, network_id, maybe_filter.clone())
.unwrap_or_default()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to log an error or all Err(_) variants represent a normal program flow?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice deduplication btw

})
.take(MAX_NAMES_LIMIT)
.collect();

if all_names_with_protocols.is_empty() {
Err(ProtocolError::InvalidName {
name: name.to_string(),
reason: "No valid TLDs".to_string(),
})
} else {
Ok(all_names_with_protocols)
}
}
}

pub fn main_name_in_network(
Expand All @@ -308,8 +372,8 @@ impl Protocoler {
maybe_filter: Option<NonEmpty<String>>,
) -> Result<DomainNameOnProtocol, ProtocolError> {
let maybe_name = self
.names_options_in_network(name, network_id, maybe_filter)
.map(|mut names| names.pop())?;
.names_options_in_network(name, network_id, maybe_filter)?
.pop();
let name = maybe_name.ok_or_else(|| ProtocolError::InvalidName {
name: name.to_string(),
reason: "no protocol found".to_string(),
Expand Down Expand Up @@ -351,3 +415,40 @@ impl Protocol {
})
}
}

#[cfg(test)]
mod tld_tests {
use super::Tld;

#[test]
fn tld_new_trims_dot() {
let tld = Tld::new(".eth");
assert_eq!(tld.0, "eth");
}

#[test]
fn tld_new_no_dot() {
let tld = Tld::new("eth");
assert_eq!(tld.0, "eth");
}

#[test]
fn from_domain_name_works() {
let domain = "vitalik.eth";
let tld = Tld::from_domain_name(domain).unwrap();
assert_eq!(tld.0, "eth");
}

#[test]
fn from_domain_name_empty() {
let domain = ".";
let tld = Tld::from_domain_name(domain);
assert!(tld.is_none());
}

#[test]
fn reverse_works() {
let rev = Tld::reverse();
assert_eq!(rev.0, "reverse");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"expiry_date": null,
"id": "0x7a68d23f9d7e32e79f09e024d21e2e12b66f74cbbc4aff0e5a36043a6a42778d",
"name": "abcnews.eth",
"other_addresses": {},
"owner": {
"hash": "0xe95Ca676DE726AEe5eD7F6cd4537CDE478cC5020"
},
"registrant": null,
"registration_date": "2017-06-14T01:21:44.000Z",
"resolved_address": null,
"tokens": [
{
"contract_hash": "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85",
"id": "73184468325383835900744378213252247472075144956610402274611094281335906491053",
"type": "NATIVE_DOMAIN_TOKEN"
}
],
"wrapped_owner": null,
"protocol": {{ ens_protocol | json_encode() | safe }},
"resolved_with_wildcard": false,
"resolver_address": null,
"stored_offchain": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@
"hash": "0x6D3B3F99177FB2A5de7F9E928a9BD807bF7b5BAD"
},
"stored_offchain": false
}
}
17 changes: 12 additions & 5 deletions blockscout-ens/bens-server/tests/domains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,29 @@ async fn eth_protocol_scenario(base: Url, settings: &Settings) {
let context = utils::settings_context(settings);

// get detailed domain
let request: Value = send_get_request(&base, "/api/v1/1/domains/vitalik.eth").await;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we leave at least one test case where tld is preserved?

let request: Value = send_get_request(&base, "/api/v1/1/domains/vitalik").await;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding another test case seems better, as the previous is still useful + more tests is better than less :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for all below

let vitalik_detailed_json = data_file_as_json!("domains/vitalik_eth/detailed.json", &context);
assert_eq!(request, vitalik_detailed_json.clone());
// get detailed domain with emojied name and with wrapped token
let request: Value = send_get_request(&base, "/api/v1/1/domains/wa🇬🇲i.eth").await;
let request: Value = send_get_request(&base, "/api/v1/1/domains/wa🇬🇲i").await;
assert_eq!(
request,
data_file_as_json!("domains/wai_eth/detailed.json", &context)
);

let request: Value = send_get_request(&base, "/api/v1/1/domains/abcnews").await;

assert_eq!(
request,
data_file_as_json!("domains/abcnews_eth/detailed.json", &context)
);

// get events
let expected_events = data_file_as_json!("domains/vitalik_eth/events.json", &context);
let expected_events = expected_events.as_array().unwrap().clone();
let (actual, expected) = check_list_result(
&base,
"/api/v1/1/domains/vitalik.eth/events",
"/api/v1/1/domains/vitalik/events",
Default::default(),
expected_events.clone(),
None,
Expand All @@ -79,7 +86,7 @@ async fn eth_protocol_scenario(base: Url, settings: &Settings) {
assert_eq!(actual, expected);
let (actual, expected) = check_list_result(
&base,
"/api/v1/1/domains/vitalik.eth/events",
"/api/v1/1/domains/vitalik/events",
HashMap::from_iter([("sort".to_owned(), "timestamp".to_owned())]),
expected_events.clone(),
None,
Expand Down Expand Up @@ -265,7 +272,7 @@ async fn genome_protocol_scenario(base: Url, settings: &Settings) {

async fn different_protocols_scenario(base: Url, settings: &Settings) {
let context = utils::settings_context(settings);
let request: Value = send_get_request(&base, "/api/v1/1337/domains/levvv.gno").await;
let request: Value = send_get_request(&base, "/api/v1/1337/domains/levvv").await;
assert_eq!(
request,
data_file_as_json!("domains/levvv_gno/detailed.json", &context)
Expand Down
Loading