Skip to content

Commit

Permalink
Merge pull request #82 from starknet-id/feat/add_ens_integration
Browse files Browse the repository at this point in the history
feat: add support for all text records
  • Loading branch information
Th0rgal authored May 14, 2024
2 parents 2bfcd04 + b5907ae commit b133ede
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 4 deletions.
24 changes: 23 additions & 1 deletion config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ argent_multicall = "0xXXXXXXXXXXXX"
rpc_url = "xxxxxx"
refresh_delay = 60 # in seconds
ipfs_gateway = "https://gateway.pinata.cloud/ipfs/" # or https://ipfs.io/ipfs/
discord_token = "xxxxxx"
discord_api_url = "https://discord.com/api"
twitter_api_key = "xxxxxx"
twitter_api_url = "xxxxxx"
github_api_url = "https://api.github.com"

[starkscan]
api_url = "https://api-testnet.starkscan.co/api/v0"
Expand Down Expand Up @@ -83,4 +88,21 @@ private_key = "0xXXXXXXXXXXXX"
polygon = 2147483785
optimism = 2147483658
base = 2147492101
arbitrum = 2147525809
arbitrum = 2147525809

[evm_records_verifiers]

[evm_records_verifiers."com.discord"]
verifier_contract = "0x0182EcE8173C216A395f4828e1523541b7e3600bf190CB252E1a1A0cE219d184"
field = "discord"
handler = "GetDiscordName"

[evm_records_verifiers."com.github"]
verifier_contract = "0x0182EcE8173C216A395f4828e1523541b7e3600bf190CB252E1a1A0cE219d184"
field = "github"
handler = "GetGithubName"

[evm_records_verifiers."com.twitter"]
verifier_contract = "0x0182EcE8173C216A395f4828e1523541b7e3600bf190CB252E1a1A0cE219d184"
field = "twitter"
handler = "GetTwitterName"
16 changes: 16 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::collections::HashMap;
use std::env;
use std::fs;

use crate::endpoints::crosschain::ethereum::text_records::HandlerType;

macro_rules! pub_struct {
($($derive:path),*; $name:ident {$($field:ident: $t:ty),* $(,)?}) => {
#[derive($($derive),*)]
Expand Down Expand Up @@ -73,6 +75,11 @@ pub_struct!(Clone, Debug, Deserialize; Variables {
rpc_url: String,
refresh_delay: f64,
ipfs_gateway: String,
discord_token: String,
discord_api_url: String,
twitter_api_key: String,
twitter_api_url: String,
github_api_url: String,
});

#[derive(Deserialize)]
Expand All @@ -94,6 +101,12 @@ pub_struct!(Clone, Debug, Deserialize; Evm {
#[derive(Debug, Clone)]
pub struct OffchainResolvers(HashMap<String, OffchainResolver>);

pub_struct!(Clone, Debug, Deserialize; EvmRecordVerifier {
verifier_contract: FieldElement,
field: String,
handler: HandlerType,
});

#[derive(Deserialize)]
struct RawConfig {
server: Server,
Expand All @@ -107,6 +120,7 @@ struct RawConfig {
offchain_resolvers: OffchainResolvers,
evm: Evm,
evm_networks: HashMap<String, u64>,
evm_records_verifiers: HashMap<String, EvmRecordVerifier>,
}

pub_struct!(Clone, Deserialize; Config {
Expand All @@ -122,6 +136,7 @@ pub_struct!(Clone, Deserialize; Config {
offchain_resolvers: OffchainResolvers,
evm: Evm,
evm_networks: HashMap<u64, FieldElement>,
evm_records_verifiers: HashMap<String, EvmRecordVerifier>,
});

impl Altcoins {
Expand Down Expand Up @@ -229,6 +244,7 @@ impl From<RawConfig> for Config {
offchain_resolvers: raw.offchain_resolvers,
evm: raw.evm,
evm_networks: reversed_evm_networks,
evm_records_verifiers: raw.evm_records_verifiers,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/endpoints/crosschain/ethereum/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod lookup;
pub mod resolve;
pub mod text_records;
pub mod utils;
49 changes: 46 additions & 3 deletions src/endpoints/crosschain/ethereum/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ use starknet::{
};
use starknet_id::encode;

use super::text_records::{get_unbounded_user_data, get_verifier_data};

#[derive(Deserialize, Debug, Clone)]
pub struct ResolveQuery {
data: String, // data encoded
Expand Down Expand Up @@ -147,7 +149,6 @@ pub async fn handler(State(state): State<Arc<AppState>>, query: Query) -> impl I
let payload: Vec<Token> = match resolver_function_call {
ResolverFunctionCall::Text(_alt_hash, record) => {
match record.as_str() {
// Records available "com.discord" "com.github" "com.twitter"
"avatar" => {
match get_profile_picture(
&state.conf,
Expand All @@ -170,8 +171,50 @@ pub async fn handler(State(state): State<Arc<AppState>>, query: Query) -> impl I
}
}
_ => {
println!("Record not implemented: {:?}", record);
return get_error(format!("Record not implemented {}", record));
// we check if this data was added through a verifier
// let record_config = state.conf.evm_records_verifiers.get(&record).unwrap();
match state.conf.evm_records_verifiers.get(&record) {
Some(record_config) => {
let record_data = get_verifier_data(
&state.conf,
&provider,
id,
record_config,
)
.await;
match record_data {
Some(record_data) => {
vec![Token::String(record_data)]
}
None => {
return get_error(format!(
"No data found for record: {}",
record
));
}
}
}
None => {
// if not we fetch user data for this record
// existing records : header (image url), display, name, url, description, email, mail, notice, location, phone
match get_unbounded_user_data(
&state.conf,
&provider,
id,
&record,
)
.await
{
Some(data) => vec![Token::String(data)],
None => {
return get_error(format!(
"No data found for record: {}",
record
));
}
}
}
}
}
}
}
Expand Down
203 changes: 203 additions & 0 deletions src/endpoints/crosschain/ethereum/text_records.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use anyhow::{anyhow, Context, Result};
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use starknet::{
core::{
types::{BlockId, BlockTag, FieldElement, FunctionCall},
utils::{cairo_short_string_to_felt, parse_cairo_short_string},
},
macros::selector,
providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider},
};

use crate::config::{Config, EvmRecordVerifier};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum HandlerType {
Static,
GetDiscordName,
GetGithubName,
GetTwitterName,
}

#[derive(Deserialize, Debug)]
struct GithubUser {
login: String,
}

#[derive(Deserialize, Debug)]
struct DiscordUser {
username: String,
}

impl EvmRecordVerifier {
pub async fn execute_handler(&self, config: &Config, id: FieldElement) -> Result<String> {
match self.handler {
HandlerType::Static => Ok(FieldElement::to_string(&id)),
HandlerType::GetDiscordName => self.get_discord_name(config, id).await,
HandlerType::GetGithubName => self.get_github_name(config, id).await,
HandlerType::GetTwitterName => self.get_twitter_name(config, id).await,
}
}

async fn get_discord_name(&self, config: &Config, id: FieldElement) -> Result<String> {
let social_id = FieldElement::to_string(&id);
let url = format!("{}/users/{}", config.variables.discord_api_url, social_id);
let client = Client::new();
let resp = client
.get(&url)
.header("Content-Type", "application/json")
.header(
"Authorization",
format!("Bot {}", config.variables.discord_token),
)
.send()
.await?
.json::<DiscordUser>()
.await
.context("Failed to parse JSON response from Discord API")?;

Ok(resp.username)
}
async fn get_github_name(&self, config: &Config, id: FieldElement) -> Result<String> {
let social_id = FieldElement::to_string(&id);
let url = format!("{}/user/{}", config.variables.github_api_url, social_id);
let client = Client::builder()
.user_agent("request")
.build()
.context("Failed to build HTTP client")?;
let response = client
.get(&url)
.send()
.await
.context("Failed to send request to GitHub")?;

if response.status() != StatusCode::OK {
anyhow::bail!("GitHub API returned non-OK status: {}", response.status());
}

let text = response
.text()
.await
.context("Failed to read response text")?;
let user: GithubUser =
serde_json::from_str(&text).context("Failed to deserialize GitHub response")?;

Ok(user.login)
}

async fn get_twitter_name(&self, config: &Config, id: FieldElement) -> Result<String> {
let social_id = FieldElement::to_string(&id);
let client = Client::new();
let response = client
.get(format!(
"{}/get-user-by-id",
config.variables.twitter_api_url
))
.header("X-RapidAPI-Key", config.variables.twitter_api_key.clone())
.header("X-RapidAPI-Host", "twttrapi.p.rapidapi.com")
.query(&[("user_id", &social_id)])
.send()
.await?;

if response.status() != StatusCode::OK {
anyhow::bail!("Twitter API returned non-OK status: {}", response.status());
}
let response_body = response.text().await?;
let json: Value = serde_json::from_str(&response_body)?;
let screen_name = json
.get("data")
.and_then(|data| data.get("user_result"))
.and_then(|user_result| user_result.get("result"))
.and_then(|result| result.get("legacy"))
.and_then(|legacy| legacy.get("screen_name"))
.and_then(|screen_name| screen_name.as_str())
.ok_or_else(|| anyhow!("Failed to extract screen name"));

Ok(screen_name.map(|name| name.to_string()).unwrap())
}
}

pub async fn get_verifier_data(
config: &Config,
provider: &JsonRpcClient<HttpTransport>,
id: FieldElement,
record_config: &EvmRecordVerifier,
) -> Option<String> {
let call_result = provider
.call(
FunctionCall {
contract_address: config.contracts.starknetid,
entry_point_selector: selector!("get_verifier_data"),
calldata: vec![
id,
cairo_short_string_to_felt(&record_config.field).unwrap(),
record_config.verifier_contract,
FieldElement::ZERO,
],
},
BlockId::Tag(BlockTag::Latest),
)
.await;

match call_result {
Ok(result) => {
if result[0] == FieldElement::ZERO {
return None;
}

match record_config.execute_handler(config, result[0]).await {
Ok(name) => Some(name),
Err(e) => {
println!("Error while executing handler: {:?}", e);
None
}
}
}
Err(e) => {
println!("Error while fetchingverifier data: {:?}", e);
None
}
}
}

pub async fn get_unbounded_user_data(
config: &Config,
provider: &JsonRpcClient<HttpTransport>,
id: FieldElement,
field: &str,
) -> Option<String> {
let call_result = provider
.call(
FunctionCall {
contract_address: config.contracts.starknetid,
entry_point_selector: selector!("get_unbounded_user_data"),
calldata: vec![
id,
cairo_short_string_to_felt(field).unwrap(),
FieldElement::ZERO,
],
},
BlockId::Tag(BlockTag::Latest),
)
.await;
match call_result {
Ok(result) => {
if result[0] == FieldElement::ZERO {
return None;
}
let res = result
.iter()
.skip(1)
.filter_map(|val| parse_cairo_short_string(val).ok())
.collect::<Vec<String>>() // Collect into a vector of strings
.join("");
Some(res)
}
Err(e) => {
println!("Error while fetchingverifier data: {:?}", e);
None
}
}
}

0 comments on commit b133ede

Please sign in to comment.