From ff0326b2705b5a7d6f7a9af77203de43905e38d6 Mon Sep 17 00:00:00 2001 From: sevenzing <41516657+sevenzing@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:15:50 +0700 Subject: [PATCH] [BENS] Add domain token support (#744) * Add domains token logic * Add tests for token id * More tests * try to fix dockerfile * Rename contract to contract hash * Fix unit tests * Add native_token_contract for RSK * Fix logs --- blockscout-ens/Cargo.lock | 98 ++++++---- blockscout-ens/Dockerfile | 30 ++- blockscout-ens/bens-logic/Cargo.toml | 1 + .../bens-logic/src/entity/subgraph/domain.rs | 4 +- .../src/subgraphs_reader/domain_tokens.rs | 183 ++++++++++++++++++ .../bens-logic/src/subgraphs_reader/mod.rs | 1 + .../bens-logic/src/subgraphs_reader/reader.rs | 38 ++-- .../bens-logic/src/subgraphs_reader/types.rs | 20 ++ .../20231019103631_push_mock_data.sql | 2 +- blockscout-ens/bens-proto/proto/bens.proto | 15 +- .../bens-proto/swagger/bens.swagger.yaml | 24 ++- blockscout-ens/bens-server/config/prod.json | 30 +++ .../bens-server/config/staging.json | 6 +- .../bens-server/src/conversion/domain.rs | 56 ++++-- blockscout-ens/bens-server/src/settings.rs | 5 +- blockscout-ens/bens-server/tests/domains.rs | 94 ++++++--- 16 files changed, 492 insertions(+), 115 deletions(-) create mode 100644 blockscout-ens/bens-logic/src/subgraphs_reader/domain_tokens.rs create mode 100644 blockscout-ens/bens-server/config/prod.json diff --git a/blockscout-ens/Cargo.lock b/blockscout-ens/Cargo.lock index d5f57f8af..d343bc8ee 100644 --- a/blockscout-ens/Cargo.lock +++ b/blockscout-ens/Cargo.lock @@ -90,7 +90,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -243,7 +243,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -467,7 +467,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -478,7 +478,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -614,6 +614,7 @@ name = "bens-logic" version = "0.1.0" dependencies = [ "anyhow", + "bigdecimal 0.4.2", "cached", "chrono", "ethers", @@ -692,6 +693,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bigdecimal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06619be423ea5bb86c95f087d5707942791a08a85530df0db2209a3ecfb8bc9" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1127,7 +1141,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -1458,7 +1472,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -1480,7 +1494,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -1899,7 +1913,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.38", + "syn 2.0.43", "toml 0.7.8", "walkdir", ] @@ -1917,7 +1931,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -1943,7 +1957,7 @@ dependencies = [ "serde", "serde_json", "strum", - "syn 2.0.38", + "syn 2.0.43", "tempfile", "thiserror", "tiny-keccak", @@ -2304,7 +2318,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -2816,7 +2830,7 @@ checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -3301,7 +3315,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -3367,7 +3381,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -3517,7 +3531,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -3721,7 +3735,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -3795,7 +3809,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -3833,7 +3847,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -3914,7 +3928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -4680,7 +4694,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -4691,7 +4705,7 @@ checksum = "cf9195a2b2a182cbee3f76cf2a97c20204022f91259bdf8a48b537788202775b" dependencies = [ "async-stream", "async-trait", - "bigdecimal", + "bigdecimal 0.3.1", "chrono", "futures", "log", @@ -4738,7 +4752,7 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.38", + "syn 2.0.43", "unicode-ident", ] @@ -4765,7 +4779,7 @@ version = "0.30.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e40446e3c048cec0802375f52462a05cc774b9ea6af1dffba6c646b7825e4cf9" dependencies = [ - "bigdecimal", + "bigdecimal 0.3.1", "chrono", "derivative", "inherent", @@ -4783,7 +4797,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9" dependencies = [ - "bigdecimal", + "bigdecimal 0.3.1", "chrono", "rust_decimal", "sea-query", @@ -4802,7 +4816,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", "thiserror", ] @@ -4910,7 +4924,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -4981,7 +4995,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -5196,7 +5210,7 @@ checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ "ahash 0.8.3", "atoi", - "bigdecimal", + "bigdecimal 0.3.1", "byteorder", "bytes", "chrono", @@ -5283,7 +5297,7 @@ checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ "atoi", "base64 0.21.4", - "bigdecimal", + "bigdecimal 0.3.1", "bitflags 2.4.0", "byteorder", "bytes", @@ -5330,7 +5344,7 @@ checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" dependencies = [ "atoi", "base64 0.21.4", - "bigdecimal", + "bigdecimal 0.3.1", "bitflags 2.4.0", "byteorder", "chrono", @@ -5447,7 +5461,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -5489,9 +5503,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -5588,22 +5602,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -5742,7 +5756,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -5946,7 +5960,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", ] [[package]] @@ -6311,7 +6325,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", "wasm-bindgen-shared", ] @@ -6345,7 +6359,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.43", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/blockscout-ens/Dockerfile b/blockscout-ens/Dockerfile index b481084ca..f7cd6bf90 100644 --- a/blockscout-ens/Dockerfile +++ b/blockscout-ens/Dockerfile @@ -1,13 +1,22 @@ -FROM lukemathwalker/cargo-chef:0.1.62-rust-1.72-buster as chef +FROM lukemathwalker/cargo-chef:0.1.62-rust-1.74-buster as chef + WORKDIR /app -RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip -O ./protoc.zip \ - && unzip protoc.zip \ - && mv ./include/* /usr/include/ \ - && mv ./bin/protoc /usr/bin/protoc +ARG TARGETARCH +RUN case ${TARGETARCH} in \ + "arm64") TARGETARCH=aarch_64 ;; \ + "amd64") TARGETARCH=x86_64 ;; \ + esac \ + && wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-$TARGETARCH.zip -O ./protoc.zip \ + && unzip protoc.zip \ + && mv ./include/* /usr/include/ \ + && mv ./bin/protoc /usr/bin/protoc -RUN wget https://github.com/grpc-ecosystem/grpc-gateway/releases/download/v2.15.0/protoc-gen-openapiv2-v2.15.0-linux-x86_64 -O ./protoc-gen-openapiv2 \ - && chmod +x protoc-gen-openapiv2 \ - && mv ./protoc-gen-openapiv2 /usr/bin/protoc-gen-openapiv2 +RUN case ${TARGETARCH} in \ + "amd64") TARGETARCH=x86_64 ;; \ + esac \ + && wget https://github.com/grpc-ecosystem/grpc-gateway/releases/download/v2.15.0/protoc-gen-openapiv2-v2.15.0-linux-$TARGETARCH -O ./protoc-gen-openapiv2 \ + && chmod +x protoc-gen-openapiv2 \ + && mv ./protoc-gen-openapiv2 /usr/bin/protoc-gen-openapiv2 FROM chef AS plan COPY . . @@ -29,5 +38,6 @@ RUN apt-get update && apt-get install -y libssl1.1 libssl-dev ca-certificates ENV APP_USER=app RUN groupadd $APP_USER \ && useradd -g $APP_USER $APP_USER -COPY --from=build /app/target/release/bens-server /usr/local/bin/bens-server -ENTRYPOINT ["/usr/local/bin/bens-server"] +COPY --from=build /app/target/release/bens-server /app/bens-server +COPY ./bens-server/config/ /app/config/ +ENTRYPOINT ["/app/bens-server"] diff --git a/blockscout-ens/bens-logic/Cargo.toml b/blockscout-ens/bens-logic/Cargo.toml index d5003589f..139d1ba9c 100644 --- a/blockscout-ens/bens-logic/Cargo.toml +++ b/blockscout-ens/bens-logic/Cargo.toml @@ -25,6 +25,7 @@ futures = "0.3" cached = { version = "0.46.1", features = ["proc_macro", "tokio", "async", "async_tokio_rt_multi_thread"] } wiremock = {version = "0.5", optional = true } sea-query = "0.30.5" +bigdecimal = "0.4" [dependencies.sqlx] version = "0.7" diff --git a/blockscout-ens/bens-logic/src/entity/subgraph/domain.rs b/blockscout-ens/bens-logic/src/entity/subgraph/domain.rs index 3f2de7225..fa9a41122 100644 --- a/blockscout-ens/bens-logic/src/entity/subgraph/domain.rs +++ b/blockscout-ens/bens-logic/src/entity/subgraph/domain.rs @@ -2,7 +2,7 @@ use chrono::Utc; use sqlx::types::BigDecimal; use std::collections::HashMap; -#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)] +#[derive(Debug, Clone, Default, PartialEq, Eq, sqlx::FromRow)] pub struct DetailedDomain { pub id: String, pub name: Option, @@ -25,7 +25,7 @@ pub struct DetailedDomain { pub other_addresses: sqlx::types::Json>, } -#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)] +#[derive(Debug, Clone, Default, PartialEq, Eq, sqlx::FromRow)] pub struct Domain { pub id: String, pub name: Option, diff --git a/blockscout-ens/bens-logic/src/subgraphs_reader/domain_tokens.rs b/blockscout-ens/bens-logic/src/subgraphs_reader/domain_tokens.rs new file mode 100644 index 000000000..54be5190b --- /dev/null +++ b/blockscout-ens/bens-logic/src/subgraphs_reader/domain_tokens.rs @@ -0,0 +1,183 @@ +use super::{DomainToken, DomainTokenType, SubgraphSettings}; +use crate::entity::subgraph::domain::DetailedDomain; +use anyhow::Context; +use bigdecimal::{num_bigint::BigInt, Num}; +use ethers::types::Address; +use std::str::FromStr; + +#[tracing::instrument( + level = "info", + skip(domain, subgraph_settings), + fields( + domain_name = domain.name, + native_token_contract =? subgraph_settings.native_token_contract, + ), + err, +)] +pub fn extract_tokens_from_domain( + domain: &DetailedDomain, + subgraph_settings: &SubgraphSettings, +) -> Result, anyhow::Error> { + let mut tokens = vec![]; + + if let Some(contract) = subgraph_settings.native_token_contract { + let is_second_level_domain = domain + .name + .as_ref() + .map(|name| name.matches('.').count() == 1) + .unwrap_or(true); + // native NFT exists only if domain is second level (like abc.eth and not abc.abc.eth) + if is_second_level_domain { + let labelhash = domain + .labelhash + .as_ref() + .ok_or_else(|| anyhow::anyhow!("no labelhash in database"))?; + + let id = token_id(&hex::encode(labelhash))?; + tokens.push(DomainToken { + id, + contract, + _type: DomainTokenType::Native, + }); + } + }; + + if domain.wrapped_owner.is_some() { + let id = token_id(&domain.id)?; + let contract = Address::from_str(&domain.owner).context("parse owner as address")?; + tokens.push(DomainToken { + id, + contract, + _type: DomainTokenType::Wrapped, + }); + }; + + Ok(tokens) +} + +fn token_id(hexed_id: &str) -> Result { + let id = BigInt::from_str_radix(hexed_id.trim_start_matches("0x"), 16) + .context("convert token_id to number")?; + Ok(id.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[inline] + fn domain( + name: &str, + id: &str, + labelhash: &str, + owner: &str, + maybe_wrapped_owner: Option<&str>, + ) -> DetailedDomain { + DetailedDomain { + id: id.to_string(), + name: Some(name.to_string()), + labelhash: Some( + hex::decode(labelhash.trim_start_matches("0x")) + .expect("invalid labelhash provided"), + ), + owner: owner.to_string(), + wrapped_owner: maybe_wrapped_owner.map(str::to_string), + ..Default::default() + } + } + + #[inline] + fn addr(a: &str) -> Option
{ + Address::from_str(a).ok() + } + + #[test] + fn it_works() { + let native_contract = "0x1234567890123456789012345678901234567890"; + let wrapped_contract = "0x0987654321098765432109876543210987654321"; + let owner = "0x1111111111111111111111111111111111111111"; + for (domain, native_token_contract, expected_tokens) in [ + // No native contract provided + ( + domain("levvv.eth", "0x0200", "0x0100", owner, None), + None, + vec![], + ), + // Native contract provided, but domain is third level + ( + domain( + "this_is_third_level_domain.levvv.eth", + "0x0200", + "0x0100", + owner, + None, + ), + addr(native_contract), + vec![], + ), + // Native contract provided, no wrapped owner + ( + domain("levvv.eth", "0x0200", "0x0100", owner, None), + addr(native_contract), + vec![DomainToken { + id: "256".to_string(), + contract: Address::from_str(native_contract) + .expect("invalid native_contract provided"), + _type: DomainTokenType::Native, + }], + ), + // Native contract provided, wrapped owner provided, but third level domain, so only wrapped token + ( + domain( + "this_is_third_level_domain.levvv.eth", + "0x0200", + "0x0100", + wrapped_contract, + Some(owner), + ), + addr(native_contract), + vec![DomainToken { + id: "512".to_string(), + contract: Address::from_str(wrapped_contract) + .expect("invalid wrapped_contract provided"), + _type: DomainTokenType::Wrapped, + }], + ), + // Everything is provided + ( + domain( + "levvv.eth", + "0x38a7804a53792b0cdefe3e7271b0b85422d620ea4a82df7b7bf750a6d4b297a4", + "0x1a8247ca2a4190d90c748b31fa6517e5560c1b7a680f03ff73dbbc3ed2c0ed66", + wrapped_contract, + Some(owner), + ), + addr(native_contract), + vec![ + DomainToken { + id: "11990319655936053415661126359086567018700354293176496925267203544835860524390".to_string(), + contract: Address::from_str(native_contract) + .expect("invalid native_contract provided"), + _type: DomainTokenType::Native, + }, + DomainToken { + id: "25625468407840116393736812939389551247551040926951238633020744494000165263268".to_string(), + contract: Address::from_str(wrapped_contract) + .expect("invalid wrapped_contract provided"), + _type: DomainTokenType::Wrapped, + }, + ], + ), + ] { + let settings = SubgraphSettings { + native_token_contract, + ..Default::default() + }; + let tokens = extract_tokens_from_domain(&domain, &settings) + .expect("failed to extract tokens from domain"); + + assert_eq!(tokens, expected_tokens); + } + } +} diff --git a/blockscout-ens/bens-logic/src/subgraphs_reader/mod.rs b/blockscout-ens/bens-logic/src/subgraphs_reader/mod.rs index ec76ea213..be660f9de 100644 --- a/blockscout-ens/bens-logic/src/subgraphs_reader/mod.rs +++ b/blockscout-ens/bens-logic/src/subgraphs_reader/mod.rs @@ -1,5 +1,6 @@ pub mod blockscout; mod domain_name; +mod domain_tokens; mod pagination; mod patch; mod reader; diff --git a/blockscout-ens/bens-logic/src/subgraphs_reader/reader.rs b/blockscout-ens/bens-logic/src/subgraphs_reader/reader.rs index 007e58e0c..797226055 100644 --- a/blockscout-ens/bens-logic/src/subgraphs_reader/reader.rs +++ b/blockscout-ens/bens-logic/src/subgraphs_reader/reader.rs @@ -1,11 +1,12 @@ use super::{ blockscout::{self, BlockscoutClient}, domain_name::DomainName, + domain_tokens::extract_tokens_from_domain, pagination::{PaginatedList, Paginator}, patch::{patch_detailed_domain, patch_domain}, schema_selector::subgraph_deployments, - sql, BatchResolveAddressNamesInput, GetDomainHistoryInput, GetDomainInput, LookupAddressInput, - LookupDomainInput, + sql, BatchResolveAddressNamesInput, GetDomainHistoryInput, GetDomainInput, GetDomainOutput, + LookupAddressInput, LookupDomainInput, }; use crate::{ entity::subgraph::{ @@ -15,7 +16,7 @@ use crate::{ hash_name::{domain_id, hex}, }; use anyhow::Context; -use ethers::types::{Bytes, TxHash, H160}; +use ethers::types::{Address, Bytes, TxHash, H160}; use sqlx::postgres::PgPool; use std::{ collections::{BTreeMap, HashMap, HashSet}, @@ -59,6 +60,7 @@ pub struct Subgraph { pub struct SubgraphSettings { pub use_cache: bool, pub empty_label_hash: Option, + pub native_token_contract: Option
, } #[derive(Debug, Clone)] @@ -180,7 +182,7 @@ impl SubgraphReader { pub async fn get_domain( &self, input: GetDomainInput, - ) -> Result, SubgraphReadError> { + ) -> Result, SubgraphReadError> { let network = self .networks .get(&input.network_id) @@ -189,7 +191,7 @@ impl SubgraphReader { let empty_label_hash = subgraph.settings.empty_label_hash.clone(); let domain_name = DomainName::new(&input.name, empty_label_hash) .map_err(|e| SubgraphReadError::Internal(e.to_string()))?; - let domain = sql::get_domain( + let maybe_domain: Option = sql::get_domain( self.pool.as_ref(), &domain_name, &subgraph.schema_name, @@ -204,7 +206,14 @@ impl SubgraphReader { &domain_name, ) }); - Ok(domain) + if let Some(domain) = maybe_domain { + let tokens = extract_tokens_from_domain(&domain, &subgraph.settings).map_err(|e| { + SubgraphReadError::Internal(format!("failed to extract domain tokens: {e}")) + })?; + Ok(Some(GetDomainOutput { tokens, domain })) + } else { + Ok(None) + } } pub async fn get_domain_history( @@ -407,9 +416,10 @@ mod tests { .await .expect("failed to get vitalik domain") .expect("domain not found"); - assert_eq!(result.name.as_deref(), Some("vitalik.eth")); + let domain = result.domain; + assert_eq!(domain.name.as_deref(), Some("vitalik.eth")); assert_eq!( - result.resolved_address.as_deref(), + domain.resolved_address.as_deref(), Some("0xd8da6bf26964af9d7eed9e03e53415d37aa96045") ); let other_addresses: HashMap = serde_json::from_value(serde_json::json!({ @@ -417,7 +427,7 @@ mod tests { "RSK": "0xf0d485009714cE586358E3761754929904D76B9D", })) .unwrap(); - assert_eq!(result.other_addresses, other_addresses.into()); + assert_eq!(domain.other_addresses, other_addresses.into()); // get expired domain let name = "expired.eth".to_string(); @@ -430,13 +440,14 @@ mod tests { .await .expect("failed to get expired domain") .expect("expired domain not found"); + let domain = result.domain; assert!( - result.is_expired, + domain.is_expired, "expired domain has is_expired=false: {:?}", - result + domain ); // since no info in multicoin_addr_changed - assert!(result.other_addresses.is_empty()); + assert!(domain.other_addresses.is_empty()); // get expired domain with only_active filter let result = reader @@ -722,7 +733,7 @@ mod tests { assert_eq!(domain.label_name, None,); // After reader requests domain should be resolved - let domain = reader + let result = reader .get_domain(GetDomainInput { network_id: DEFAULT_CHAIN_ID, name: unresolved.to_string(), @@ -731,6 +742,7 @@ mod tests { .await .expect("failed to get domain") .expect("unresolved domain not found using reader"); + let domain = result.domain; assert_eq!(domain.name.as_deref(), Some(unresolved)); assert_eq!(domain.label_name.as_deref(), Some(unresolved_label)); diff --git a/blockscout-ens/bens-logic/src/subgraphs_reader/types.rs b/blockscout-ens/bens-logic/src/subgraphs_reader/types.rs index 51ee64195..789cef79a 100644 --- a/blockscout-ens/bens-logic/src/subgraphs_reader/types.rs +++ b/blockscout-ens/bens-logic/src/subgraphs_reader/types.rs @@ -1,4 +1,5 @@ use super::pagination::{DomainPaginationInput, Order}; +use crate::entity::subgraph::domain::DetailedDomain; use ethers::types::Address; use sea_query::{Alias, IntoIden}; use serde::Deserialize; @@ -82,3 +83,22 @@ impl Display for EventSort { } } } + +#[derive(Debug, Clone)] +pub struct GetDomainOutput { + pub domain: DetailedDomain, + pub tokens: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DomainToken { + pub id: String, + pub contract: Address, + pub _type: DomainTokenType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DomainTokenType { + Native, + Wrapped, +} diff --git a/blockscout-ens/bens-logic/tests/migrations/20231019103631_push_mock_data.sql b/blockscout-ens/bens-logic/tests/migrations/20231019103631_push_mock_data.sql index 080f913af..b232e3646 100644 --- a/blockscout-ens/bens-logic/tests/migrations/20231019103631_push_mock_data.sql +++ b/blockscout-ens/bens-logic/tests/migrations/20231019103631_push_mock_data.sql @@ -56,7 +56,7 @@ VALUES (23,'[13695732,)','0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835','vitalik.eth','vitalik','\xAF2CAA1C2CA1D027F1AC823B529D0A67CD144264B2789FA2EA4D63A67C7103CC','0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae',0,'0xd8da6bf26964af9d7eed9e03e53415d37aa96045','0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41-0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835',NULL,true,1497775154,'0xd8da6bf26964af9d7eed9e03e53415d37aa96045','0x220866b1a2219f40e72f5c628b65d54268ca3a9d',NULL,1975009824), (24,'[13942919,)','0x68b620f61c87062cf680144f898582a631c90e39dd1badb35c241be0a7284fff','sashaxyz.eth','sashaxyz','\x5F5E95F7A849C60A514EB073C6FAFE97E835C0EE7B6DC15FC9D7DAA9E86F1A25','0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae',0,'0xd8da6bf26964af9d7eed9e03e53415d37aa96045','0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41-0x68b620f61c87062cf680144f898582a631c90e39dd1badb35c241be0a7284fff',NULL,true,1640341437,'0x66a6f7744ce4dea450910b81a7168588f992eafb','0x66a6f7744ce4dea450910b81a7168588f992eafb',NULL,1711231341), (25,'[12439863,)','0x86f0774249ae1b7dcb5873ac0ada288d09ec4b3bf8bbf672b67726793797142e','expired.eth','expired','\x64CA1AE50619F7F4AB23F4C22C6B85B70CFC49C072D731BE4F91487F95764C93','0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae',0,'0x9f7f7ddbfb8e14d1756580ba8037530da0880b99','0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41-0x86f0774249ae1b7dcb5873ac0ada288d09ec4b3bf8bbf672b67726793797142e',NULL,true,1496998132,'0x9f7f7ddbfb8e14d1756580ba8037530da0880b99','0x9f7f7ddbfb8e14d1756580ba8037530da0880b99',NULL,1688547600), -(26,'[13601083,)','0x5d438d292de31e08576d5bcd8a93aa41b401b9d9aeaba57da1a32c003e5fd5f5','wa🇬🇲i.eth','wa🇬🇲i','\x66F484A3530B784F2347DE3459625585DA2B38B429596364B98186B1C5E30180','0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae',0,'0x9c996076a85b46061d9a70ff81f013853a86b619','0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41-0x5d438d292de31e08576d5bcd8a93aa41b401b9d9aeaba57da1a32c003e5fd5f5',NULL,true,1636717006,'0x9c996076a85b46061d9a70ff81f013853a86b619','0x9c996076a85b46061d9a70ff81f013853a86b619','0x9c996076a85b46061d9a70ff81f013853a86b619',1802277766), +(26,'[13601083,)','0x5d438d292de31e08576d5bcd8a93aa41b401b9d9aeaba57da1a32c003e5fd5f5','wa🇬🇲i.eth','wa🇬🇲i','\x66F484A3530B784F2347DE3459625585DA2B38B429596364B98186B1C5E30180','0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae',0,'0x9c996076a85b46061d9a70ff81f013853a86b619','0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41-0x5d438d292de31e08576d5bcd8a93aa41b401b9d9aeaba57da1a32c003e5fd5f5',NULL,true,1636717006,'0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401','0x9c996076a85b46061d9a70ff81f013853a86b619','0x9c996076a85b46061d9a70ff81f013853a86b619',1802277766), -- this is unresolved domain `you-dont-know-this-label.eth` (27,'[13601083,)','0xbbe8c4a4631586c84c5b07cdfcbe27f5131ae9dc0176f8ecd324f9b6c5db777b','[0b0e081f36b3970ff8e337f0ff7bdfad321a702fa00916b6ccfc47877144f7ad].eth',NULL,'\x0b0e081f36b3970ff8e337f0ff7bdfad321a702fa00916b6ccfc47877144f7ad','0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae',0,'0x0101010101010101010101010101010101010101','0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41-0xbbe8c4a4631586c84c5b07cdfcbe27f5131ae9dc0176f8ecd324f9b6c5db777b',NULL,true,1636717006,'0x9c996076a85b46061d9a70ff81f013853a86b619','0x9c996076a85b46061d9a70ff81f013853a86b619',NULL,1802277766) ; diff --git a/blockscout-ens/bens-proto/proto/bens.proto b/blockscout-ens/bens-proto/proto/bens.proto index 29e8ee6ae..ff8da0cf3 100644 --- a/blockscout-ens/bens-proto/proto/bens.proto +++ b/blockscout-ens/bens-proto/proto/bens.proto @@ -39,8 +39,8 @@ message DetailedDomain { string id = 1; // The human readable name, if known. Unknown portions replaced with hash in square brackets (eg, foo.[1234].eth) string name = 2; - // Hex representation of labelhash - string token_id = 3; + // List of NFT tokens related to this domain + repeated Token tokens = 11; // The account that owns the domain Address owner = 4; // Optional. Resolved address of this domain @@ -69,6 +69,17 @@ message DomainEvent { optional string action = 4; } +message Token { + string id = 1; + string contract_hash = 2; + TokenType type = 3; +} + +enum TokenType { + NATIVE_DOMAIN_TOKEN = 0; + WRAPPED_DOMAIN_TOKEN = 1; +} + message Address { string hash = 1; } diff --git a/blockscout-ens/bens-proto/swagger/bens.swagger.yaml b/blockscout-ens/bens-proto/swagger/bens.swagger.yaml index ab8e0e326..1ef8a5815 100644 --- a/blockscout-ens/bens-proto/swagger/bens.swagger.yaml +++ b/blockscout-ens/bens-proto/swagger/bens.swagger.yaml @@ -315,9 +315,12 @@ definitions: name: type: string title: The human readable name, if known. Unknown portions replaced with hash in square brackets (eg, foo.[1234].eth) - token_id: - type: string - title: Hex representation of labelhash + tokens: + type: array + items: + type: object + $ref: '#/definitions/v1Token' + title: List of NFT tokens related to this domain owner: $ref: '#/definitions/v1Address' title: The account that owns the domain @@ -436,3 +439,18 @@ definitions: page_size: type: integer format: int64 + v1Token: + type: object + properties: + id: + type: string + contract_hash: + type: string + type: + $ref: '#/definitions/v1TokenType' + v1TokenType: + type: string + enum: + - NATIVE_DOMAIN_TOKEN + - WRAPPED_DOMAIN_TOKEN + default: NATIVE_DOMAIN_TOKEN diff --git a/blockscout-ens/bens-server/config/prod.json b/blockscout-ens/bens-server/config/prod.json new file mode 100644 index 000000000..27b46bf9c --- /dev/null +++ b/blockscout-ens/bens-server/config/prod.json @@ -0,0 +1,30 @@ +{ + "subgraphs_reader": { + "cache_enabled": true, + "refresh_cache_schedule": "0 0 * * * *", + "networks": { + "1": { + "blockscout": { + "url": "https://eth.blockscout.com" + }, + "subgraphs": { + "ens-subgraph": { + "use_cache": true, + "native_token_contract": "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" + } + } + }, + "30": { + "blockscout": { + "url": "https://rootstock.blockscout.com" + }, + "subgraphs": { + "rns-subgraph": { + "use_cache": true, + "native_token_contract": "0x45d3E4fB311982a06ba52359d44cB4f5980e0ef1" + } + } + } + } + } +} \ No newline at end of file diff --git a/blockscout-ens/bens-server/config/staging.json b/blockscout-ens/bens-server/config/staging.json index b2186fade..83776256b 100644 --- a/blockscout-ens/bens-server/config/staging.json +++ b/blockscout-ens/bens-server/config/staging.json @@ -9,7 +9,8 @@ }, "subgraphs": { "ens-subgraph": { - "use_cache": true + "use_cache": true, + "native_token_contract": "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" } } }, @@ -19,7 +20,8 @@ }, "subgraphs": { "genome-subgraph": { - "empty_label_hash": "0x1a13b687a5ff1d8ab1a9e189e1507a6abe834a9296cc8cff937905e3dee0c4f6" + "empty_label_hash": "0x1a13b687a5ff1d8ab1a9e189e1507a6abe834a9296cc8cff937905e3dee0c4f6", + "native_token_contract": "0xfd3d666dB2557983F3F04d61f90E35cc696f6D60" } } } diff --git a/blockscout-ens/bens-server/src/conversion/domain.rs b/blockscout-ens/bens-server/src/conversion/domain.rs index 7ca812a61..7eef76985 100644 --- a/blockscout-ens/bens-server/src/conversion/domain.rs +++ b/blockscout-ens/bens-server/src/conversion/domain.rs @@ -1,11 +1,10 @@ use super::ConversionError; use crate::conversion::order_direction_from_inner; use bens_logic::{ - entity::subgraph::domain::{DetailedDomain, Domain}, - hash_name::hex, + entity::subgraph::domain::Domain, subgraphs_reader::{ - BatchResolveAddressNamesInput, DomainPaginationInput, DomainSortField, GetDomainInput, - LookupAddressInput, LookupDomainInput, + BatchResolveAddressNamesInput, DomainPaginationInput, DomainSortField, DomainToken, + DomainTokenType, GetDomainInput, GetDomainOutput, LookupAddressInput, LookupDomainInput, }, }; use bens_proto::blockscout::bens::v1 as proto; @@ -89,29 +88,37 @@ pub fn batch_resolve_from_inner( } pub fn detailed_domain_from_logic( - d: DetailedDomain, + output: GetDomainOutput, ) -> Result { - let owner = Some(proto::Address { hash: d.owner }); - let resolved_address = d.resolved_address.map(|resolved_address| proto::Address { - hash: resolved_address, - }); - let wrapped_owner = d.wrapped_owner.map(|wrapped_owner| proto::Address { + let domain = output.domain; + let owner = Some(proto::Address { hash: domain.owner }); + let resolved_address = domain + .resolved_address + .map(|resolved_address| proto::Address { + hash: resolved_address, + }); + let wrapped_owner = domain.wrapped_owner.map(|wrapped_owner| proto::Address { hash: wrapped_owner, }); - let registrant = d + let registrant = domain .registrant .map(|registrant| proto::Address { hash: registrant }); + let tokens = output + .tokens + .into_iter() + .map(domain_token_from_logic) + .collect(); Ok(proto::DetailedDomain { - id: d.id, - name: d.name.unwrap_or_default(), - token_id: d.labelhash.map(hex).unwrap_or_default(), + id: domain.id, + name: domain.name.unwrap_or_default(), owner, resolved_address, registrant, wrapped_owner, - expiry_date: d.expiry_date.map(date_from_logic), - registration_date: date_from_logic(d.registration_date), - other_addresses: d.other_addresses.0.into_iter().collect(), + expiry_date: domain.expiry_date.map(date_from_logic), + registration_date: date_from_logic(domain.registration_date), + other_addresses: domain.other_addresses.0.into_iter().collect(), + tokens, }) } @@ -166,3 +173,18 @@ fn page_size_from_inner(page_size: Option) -> u32 { fn date_from_logic(d: chrono::DateTime) -> String { d.to_rfc3339_opts(chrono::SecondsFormat::Millis, true) } + +fn domain_token_from_logic(t: DomainToken) -> proto::Token { + proto::Token { + id: t.id, + contract_hash: format!("{:#x}", t.contract), + r#type: domain_token_type_from_logic(t._type).into(), + } +} + +fn domain_token_type_from_logic(t: DomainTokenType) -> proto::TokenType { + match t { + DomainTokenType::Native => proto::TokenType::NativeDomainToken, + DomainTokenType::Wrapped => proto::TokenType::WrappedDomainToken, + } +} diff --git a/blockscout-ens/bens-server/src/settings.rs b/blockscout-ens/bens-server/src/settings.rs index dca24f14e..a9a999dba 100644 --- a/blockscout-ens/bens-server/src/settings.rs +++ b/blockscout-ens/bens-server/src/settings.rs @@ -3,7 +3,7 @@ use blockscout_service_launcher::{ launcher::{ConfigSettings, MetricsSettings, ServerSettings}, tracing::{JaegerSettings, TracingSettings}, }; -use ethers::types::Bytes; +use ethers::types::{Address, Bytes}; use serde::Deserialize; use std::collections::HashMap; use url::Url; @@ -70,6 +70,8 @@ pub struct SubgraphSettings { pub use_cache: bool, #[serde(default)] pub empty_label_hash: Option, + #[serde(default)] + pub native_token_contract: Option
, } fn default_use_cache() -> bool { @@ -81,6 +83,7 @@ impl From for bens_logic::subgraphs_reader::SubgraphSettings { Self { use_cache: value.use_cache, empty_label_hash: value.empty_label_hash, + native_token_contract: value.native_token_contract, } } } diff --git a/blockscout-ens/bens-server/tests/domains.rs b/blockscout-ens/bens-server/tests/domains.rs index a0f3ab4c9..5680ec2f8 100644 --- a/blockscout-ens/bens-server/tests/domains.rs +++ b/blockscout-ens/bens-server/tests/domains.rs @@ -1,7 +1,5 @@ -use std::collections::HashMap; - use bens_logic::test_utils::*; -use bens_server::{BlockscoutSettings, NetworkSettings, Settings}; +use bens_server::Settings; use blockscout_service_launcher::{ launcher::ConfigSettings, test_server::{get_test_server_settings, init_server, send_get_request, send_post_request}, @@ -9,36 +7,39 @@ use blockscout_service_launcher::{ use pretty_assertions::assert_eq; use serde_json::{json, Value}; use sqlx::PgPool; +use std::collections::HashMap; use url::Url; #[sqlx::test(migrations = "../bens-logic/tests/migrations")] async fn basic_domain_extracting_works(pool: PgPool) { + let network_id = "1"; let postgres_url = std::env::var("DATABASE_URL").expect("env should be here from sqlx::test"); let db_url = format!( "{postgres_url}{}", pool.connect_options().get_database().unwrap() ); std::env::set_var("BENS__DATABASE__CONNECT__URL", db_url); - let clients = mocked_networks_with_blockscout().await; std::env::set_var("BENS__CONFIG", "./tests/config.test.toml"); let mut settings = Settings::build().expect("Failed to build settings"); let (server_settings, base) = get_test_server_settings(); + settings.server = server_settings; - settings.subgraphs_reader.networks = clients - .into_iter() - .map(|(id, client)| { - ( - id, - NetworkSettings { - blockscout: BlockscoutSettings { - url: client.blockscout_client.url().clone(), - ..Default::default() - }, - ..Default::default() + let eth_client = mocked_blockscout_client().await; + settings.subgraphs_reader.networks = serde_json::from_value(serde_json::json!( + { + network_id: { + "blockscout": { + "url": eth_client.url() }, - ) - }) - .collect(); + "subgraphs": { + "ens-subgraph": { + "native_token_contract": "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" + } + } + } + } + )) + .unwrap(); // first start with enabled cache check_basic_scenario_eth(settings.clone(), base.clone()).await; @@ -85,7 +86,49 @@ async fn check_basic_scenario_eth(settings: Settings, base: Url) { "resolved_address": { "hash": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", }, - "token_id": "0xaf2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc", + "tokens": [ + { + "id": "79233663829379634837589865448569342784712482819484549289560981379859480642508", + "contract_hash": "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", + "type": "NATIVE_DOMAIN_TOKEN", + } + ], + }) + ); + // 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; + assert_eq!( + request, + json!({ + "expiry_date": "2027-02-10T16:42:46.000Z", + "id": "0x5d438d292de31e08576d5bcd8a93aa41b401b9d9aeaba57da1a32c003e5fd5f5", + "name": "wa🇬🇲i.eth", + "other_addresses": {}, + "owner": { + "hash": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401", + }, + "registrant": { + "hash": "0x9c996076a85b46061d9a70ff81f013853a86b619", + }, + "registration_date": "2021-11-12T11:36:46.000Z", + "resolved_address": { + "hash": "0x9c996076a85b46061d9a70ff81f013853a86b619", + }, + "tokens": [ + { + "contract_hash": "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", + "id": "46567936673033819165815925923418529171479684343878036049875289456825310839168", + "type": "NATIVE_DOMAIN_TOKEN", + }, + { + "contract_hash": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401", + "id": "42184447928009120460686389475560276149795188091233200941948299907753855407605", + "type": "WRAPPED_DOMAIN_TOKEN", + }, + ], + "wrapped_owner": { + "hash": "0x9c996076a85b46061d9a70ff81f013853a86b619", + }, }) ); @@ -178,7 +221,7 @@ async fn check_basic_scenario_eth(settings: Settings, base: Url) { "id": "0x5d438d292de31e08576d5bcd8a93aa41b401b9d9aeaba57da1a32c003e5fd5f5", "name": "wa🇬🇲i.eth", "owner": { - "hash": "0x9c996076a85b46061d9a70ff81f013853a86b619", + "hash": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401", }, "wrapped_owner": { "hash": "0x9c996076a85b46061d9a70ff81f013853a86b619", @@ -346,7 +389,8 @@ async fn basic_gno_domain_extracting_works(pool: PgPool) { }, "subgraphs": { "genome-subgraph": { - "empty_label_hash": "0x1a13b687a5ff1d8ab1a9e189e1507a6abe834a9296cc8cff937905e3dee0c4f6" + "empty_label_hash": "0x1a13b687a5ff1d8ab1a9e189e1507a6abe834a9296cc8cff937905e3dee0c4f6", + "native_token_contract": "0xfd3d666dB2557983F3F04d61f90E35cc696f6D60" } } } @@ -384,7 +428,13 @@ async fn basic_gno_domain_extracting_works(pool: PgPool) { "resolved_address":{ "hash": "0xc0de20a37e2dac848f81a93bd85fe4acdde7c0de", }, - "token_id": "0x1a8247ca2a4190d90c748b31fa6517e5560c1b7a680f03ff73dbbc3ed2c0ed66", + "tokens": [ + { + "id": "11990319655936053415661126359086567018700354293176496925267203544835860524390", + "contract_hash": "0xfd3d666db2557983f3f04d61f90e35cc696f6d60", + "type": "NATIVE_DOMAIN_TOKEN", + } + ] }) ); }