Skip to content

Commit 118180c

Browse files
committed
feat: add collection whitelist
1 parent 817cb7a commit 118180c

15 files changed

+182
-183
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ thiserror = "1.0"
1515
tokio = { version = "1", features = ["rt-multi-thread"] }
1616
tracing-subscriber = "0.3.18"
1717
reqwest = "0.12.4"
18+
log = "0.4.21"

abi/AvatarService.json

+1-143
Original file line numberDiff line numberDiff line change
@@ -1,143 +1 @@
1-
[
2-
{
3-
"anonymous": false,
4-
"inputs": [
5-
{
6-
"indexed": true,
7-
"internalType": "address",
8-
"name": "walletAddress",
9-
"type": "address"
10-
},
11-
{
12-
"indexed": true,
13-
"internalType": "address",
14-
"name": "tokenAddress",
15-
"type": "address"
16-
},
17-
{
18-
"indexed": true,
19-
"internalType": "uint256",
20-
"name": "tokenId",
21-
"type": "uint256"
22-
}
23-
],
24-
"name": "AvatarSet",
25-
"type": "event"
26-
},
27-
{
28-
"inputs": [
29-
{
30-
"internalType": "address",
31-
"name": "",
32-
"type": "address"
33-
}
34-
],
35-
"name": "avatars",
36-
"outputs": [
37-
{
38-
"internalType": "address",
39-
"name": "tokenAddress",
40-
"type": "address"
41-
},
42-
{
43-
"internalType": "uint256",
44-
"name": "tokenId",
45-
"type": "uint256"
46-
}
47-
],
48-
"stateMutability": "view",
49-
"type": "function"
50-
},
51-
{
52-
"inputs": [
53-
{
54-
"internalType": "address",
55-
"name": "walletAddress",
56-
"type": "address"
57-
}
58-
],
59-
"name": "getAvatar",
60-
"outputs": [
61-
{
62-
"components": [
63-
{
64-
"internalType": "address",
65-
"name": "tokenAddress",
66-
"type": "address"
67-
},
68-
{
69-
"internalType": "uint256",
70-
"name": "tokenId",
71-
"type": "uint256"
72-
}
73-
],
74-
"internalType": "struct AvatarService.Avatar",
75-
"name": "",
76-
"type": "tuple"
77-
}
78-
],
79-
"stateMutability": "view",
80-
"type": "function"
81-
},
82-
{
83-
"inputs": [
84-
{
85-
"internalType": "address",
86-
"name": "walletAddress",
87-
"type": "address"
88-
}
89-
],
90-
"name": "getAvatarInfo",
91-
"outputs": [
92-
{
93-
"components": [
94-
{
95-
"components": [
96-
{
97-
"internalType": "address",
98-
"name": "tokenAddress",
99-
"type": "address"
100-
},
101-
{
102-
"internalType": "uint256",
103-
"name": "tokenId",
104-
"type": "uint256"
105-
}
106-
],
107-
"internalType": "struct AvatarService.Avatar",
108-
"name": "avatar",
109-
"type": "tuple"
110-
},
111-
{
112-
"internalType": "bool",
113-
"name": "owned",
114-
"type": "bool"
115-
}
116-
],
117-
"internalType": "struct AvatarService.AvatarInfo",
118-
"name": "",
119-
"type": "tuple"
120-
}
121-
],
122-
"stateMutability": "view",
123-
"type": "function"
124-
},
125-
{
126-
"inputs": [
127-
{
128-
"internalType": "address",
129-
"name": "tokenAddress",
130-
"type": "address"
131-
},
132-
{
133-
"internalType": "uint256",
134-
"name": "tokenId",
135-
"type": "uint256"
136-
}
137-
],
138-
"name": "setAvatar",
139-
"outputs": [],
140-
"stateMutability": "nonpayable",
141-
"type": "function"
142-
}
143-
]
1+
[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"walletAddress","type":"address"},{"indexed":true,"internalType":"address","name":"tokenAddress","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"AvatarSet","type":"event"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"avatars","outputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"walletAddress","type":"address"}],"name":"getAvatar","outputs":[{"components":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"internalType":"struct AvatarService.Avatar","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"walletAddress","type":"address"}],"name":"getAvatarInfo","outputs":[{"components":[{"components":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"internalType":"struct AvatarService.Avatar","name":"avatar","type":"tuple"},{"internalType":"bool","name":"owned","type":"bool"},{"internalType":"string","name":"uri","type":"string"}],"internalType":"struct AvatarService.AvatarInfo","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"setAvatar","outputs":[],"stateMutability":"nonpayable","type":"function"}]

src/handlers/avatar.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
use std::sync::Arc;
2+
use axum::extract::State;
13
use axum::Json;
24

35
use crate::extractors::ethereum_address::EthereumAddress;
46
use crate::response::avatar::AvatarInfoWithMetadataResponse;
57
use crate::response::error::AppResult;
6-
use crate::services;
8+
use crate::services::avatar::AvatarService;
79
use crate::supported_networks::SupportedNetworks;
810

911
#[allow(clippy::missing_errors_doc)]
10-
pub async fn get(EthereumAddress(address): EthereumAddress, ) -> AppResult<Json<AvatarInfoWithMetadataResponse>> {
11-
let response = services::avatar::get_info_with_metadata(&address, SupportedNetworks::all()).await?;
12+
pub async fn get(State(avatar_service): State<Arc<AvatarService>>, EthereumAddress(address): EthereumAddress) -> AppResult<Json<AvatarInfoWithMetadataResponse>> {
13+
let response = avatar_service.get_info_with_metadata(&address, SupportedNetworks::all()).await?;
1214

1315
Ok(Json(response))
1416
}

src/handlers/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
pub mod avatar;
1+
pub mod avatar;
2+
pub mod whitelist;

src/handlers/whitelist.rs

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use std::collections::HashMap;
2+
use std::sync::Arc;
3+
4+
use alloy::primitives::Address;
5+
use axum::extract::State;
6+
use axum::Json;
7+
8+
use crate::models::avatar::AvatarCollection;
9+
use crate::response::error::AppResult;
10+
use crate::services::avatar::AvatarService;
11+
use crate::supported_networks::SupportedNetworks;
12+
13+
#[allow(clippy::missing_errors_doc)]
14+
pub async fn get(State(avatar_service): State<Arc<AvatarService>>) -> AppResult<Json<HashMap<SupportedNetworks, HashMap<Address, AvatarCollection>>>> {
15+
let response = avatar_service.cache.verified_collections.read().await.clone();
16+
17+
Ok(Json(response))
18+
}

src/main.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
1+
use std::sync::Arc;
12
use axum::{
23
routing::get,
34
Router,
45
};
56
use dotenv::dotenv;
67

78
use eas_api::handlers;
9+
use eas_api::services::avatar::AvatarService;
810

911
#[tokio::main]
1012
async fn main() {
1113
dotenv().ok();
1214

1315
// initialize tracing
1416
tracing_subscriber::fmt::init();
17+
18+
let avatar_service = Arc::new(AvatarService::new());
19+
20+
// Load verified collections from GitHub: https://github.com/ethereum-avatar-service/eas-api-whitelist
21+
avatar_service.reload_verified_collections().await;
1522

16-
let app = Router::new().route("/avatar/:wallet_address", get(handlers::avatar::get));
23+
let app = Router::new()
24+
.route("/avatar/:wallet_address", get(handlers::avatar::get))
25+
.route("/whitelist", get(handlers::whitelist::get))
26+
.with_state(avatar_service);
1727

1828
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
1929
axum::serve(listener, app).await.unwrap();

src/models/avatar.rs

+11-3
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,22 @@ impl From<services::rpc::AvatarService::AvatarInfo> for AvatarInfo {
4343
}
4444

4545
#[allow(clippy::module_name_repetitions)]
46-
#[derive(Serialize)]
47-
pub struct AvatarMetadata {
48-
pub image: String,
46+
#[derive(Serialize, Clone)]
47+
pub struct AvatarCollection {
48+
pub name: Option<String>,
4949
pub author: Option<String>,
5050
pub website: Option<String>,
51+
pub opensea: Option<String>,
5152
pub verified: bool
5253
}
5354

55+
#[allow(clippy::module_name_repetitions)]
56+
#[derive(Serialize)]
57+
pub struct AvatarMetadata {
58+
pub image: String,
59+
pub collection: Option<AvatarCollection>
60+
}
61+
5462
#[allow(clippy::module_name_repetitions)]
5563
#[derive(Serialize)]
5664
pub struct AvatarInfoWithMetadata {

src/models/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod avatar;
2-
pub mod nft;
2+
pub mod nft;
3+
pub mod whitelist;

src/models/whitelist.rs

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use std::collections::HashMap;
2+
3+
use serde::Deserialize;
4+
5+
#[derive(Deserialize, Debug)]
6+
pub struct Collection {
7+
pub contract: String,
8+
pub name: String,
9+
pub author: String,
10+
pub website: String,
11+
pub opensea: Option<String>
12+
}
13+
14+
#[derive(Deserialize, Debug)]
15+
pub struct Collections(pub HashMap<String, Vec<Collection>>);

src/services/avatar.rs

+79-16
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,92 @@
1+
use std::collections::HashMap;
2+
use std::sync::Arc;
3+
14
use alloy::primitives::Address;
5+
use tokio::sync::RwLock;
26

7+
use crate::models::avatar::AvatarCollection;
8+
use crate::models::whitelist;
39
use crate::response::avatar::AvatarInfoWithMetadataResponse;
410
use crate::services::rpc;
511
use crate::supported_networks::SupportedNetworks;
612

7-
#[allow(clippy::missing_errors_doc)]
8-
#[allow(clippy::missing_panics_doc)]
9-
pub async fn get_info_with_metadata(address: &Address, networks: impl IntoIterator<Item =SupportedNetworks>) -> eyre::Result<AvatarInfoWithMetadataResponse> {
10-
let mut response = AvatarInfoWithMetadataResponse::default();
11-
12-
let networks: Vec<SupportedNetworks> = networks.into_iter().collect();
13+
#[allow(clippy::module_name_repetitions)]
14+
pub struct AvatarServiceCache {
15+
pub verified_collections: RwLock<HashMap<SupportedNetworks, HashMap<Address, AvatarCollection>>>
16+
}
1317

14-
if networks.contains(&SupportedNetworks::Sepolia) {
15-
let provider = rpc::sepolia::new();
16-
let maybe_avatar_info = provider.get_avatar_info_with_metadata(address).await.ok();
18+
#[allow(clippy::module_name_repetitions)]
19+
pub struct AvatarService {
20+
pub cache: Arc<AvatarServiceCache>
21+
}
1722

18-
response.networks.insert("sepolia".to_string(), maybe_avatar_info);
23+
impl AvatarService {
24+
pub fn new() -> Self {
25+
Self {
26+
cache: Arc::new(AvatarServiceCache {
27+
verified_collections: RwLock::default()
28+
}),
29+
}
1930
}
2031

21-
if networks.contains(&SupportedNetworks::Polygon) {
22-
let provider = rpc::polygon::new();
23-
let maybe_avatar_info = provider.get_avatar_info_with_metadata(address).await.ok();
24-
25-
response.networks.insert("polygon".to_string(), maybe_avatar_info);
32+
pub async fn reload_verified_collections(&self) {
33+
let result = reqwest::get("https://raw.githubusercontent.com/ethereum-avatar-service/eas-api-whitelist/main/collections.json").await;
34+
35+
let mut verified_collections = self.cache.verified_collections.write().await;
36+
37+
if let Ok(response) = result {
38+
match response.json::<whitelist::Collections>().await {
39+
Ok(collections) => {
40+
for (network, network_collections) in collections.0 {
41+
for collection in network_collections {
42+
if let Ok(address) = collection.contract.parse::<Address>() {
43+
let chain = match network.to_lowercase().as_str() {
44+
"sepolia" => SupportedNetworks::Sepolia,
45+
"polygon" => SupportedNetworks::Polygon,
46+
_ => { continue; }
47+
};
48+
49+
let entry = verified_collections.entry(chain).or_default();
50+
51+
entry.insert(address, AvatarCollection {
52+
name: Some(collection.name.to_string()),
53+
author: Some(collection.author.to_string()),
54+
website: Some(collection.website.to_string()),
55+
opensea: collection.opensea.clone(),
56+
verified: true,
57+
});
58+
}
59+
}
60+
}
61+
}
62+
Err(err) => {
63+
println!("{err}");
64+
}
65+
}
66+
}
2667
}
2768

28-
Ok(response)
69+
#[allow(clippy::missing_errors_doc)]
70+
#[allow(clippy::missing_panics_doc)]
71+
pub async fn get_info_with_metadata(&self, address: &Address, networks: impl IntoIterator<Item=SupportedNetworks>) -> eyre::Result<AvatarInfoWithMetadataResponse> {
72+
let mut response = AvatarInfoWithMetadataResponse::default();
73+
74+
let networks: Vec<SupportedNetworks> = networks.into_iter().collect();
75+
76+
if networks.contains(&SupportedNetworks::Sepolia) {
77+
let provider = rpc::sepolia::new();
78+
let maybe_avatar_info = provider.get_avatar_info_with_metadata(address, self.cache.clone()).await.ok();
79+
80+
response.networks.insert("sepolia".to_string(), maybe_avatar_info);
81+
}
82+
83+
if networks.contains(&SupportedNetworks::Polygon) {
84+
let provider = rpc::polygon::new();
85+
let maybe_avatar_info = provider.get_avatar_info_with_metadata(address, self.cache.clone()).await.ok();
86+
87+
response.networks.insert("polygon".to_string(), maybe_avatar_info);
88+
}
89+
90+
Ok(response)
91+
}
2992
}

0 commit comments

Comments
 (0)