Skip to content

Commit

Permalink
feat(ens): updating name address handler
Browse files Browse the repository at this point in the history
  • Loading branch information
geekbrother committed Feb 8, 2024
1 parent e8fab3a commit 1f10ef8
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 16 deletions.
45 changes: 43 additions & 2 deletions integration/names.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe('Account profile names', () => {
expect(resp.data.attributes['bio']).toBe(attributes['bio'])
expect(typeof resp.data.addresses).toBe('object')
// ENSIP-11 using the 60 for the Ethereum mainnet
const first = resp.data.addresses["60"]
const first = resp.data.addresses[coin_type]
expect(first.address).toBe(address)
})

Expand All @@ -122,7 +122,7 @@ describe('Account profile names', () => {
expect(first_name.name).toBe(name)
expect(typeof first_name.addresses).toBe('object')
// ENSIP-11 using the 60 for the Ethereum mainnet
const first_address = first_name.addresses["60"]
const first_address = first_name.addresses[coin_type]
expect(first_address.address).toBe(address)
})

Expand Down Expand Up @@ -156,4 +156,45 @@ describe('Account profile names', () => {
expect(resp.status).toBe(200)
expect(resp.data['bio']).toBe(updatedAttributes['bio'])
})

it('update name address', async () => {
// Generate a new eth wallet
const new_address = ethers.Wallet.createRandom().address;

// Prepare updated address payload
const UpdateAddressMessageObject = {
coin_type,
address: new_address,
timestamp: Math.round(Date.now() / 1000)
};
const updateMessage = JSON.stringify(UpdateAddressMessageObject);

// Sign the message
const signature = await wallet.signMessage(updateMessage);

const payload = {
message: updateMessage,
signature,
coin_type,
address,
};

// Update the address
let resp: any = await httpClient.post(
`${baseUrl}/v1/profile/account/${name}/address`,
payload
);
expect(resp.status).toBe(200)
expect(resp.data[coin_type].address).toBe(new_address)

// Query the name to see if the address was updated
resp = await httpClient.get(
`${baseUrl}/v1/profile/account/${name}`
)
expect(resp.status).toBe(200)
expect(resp.data.name).toBe(name)
expect(typeof resp.data.addresses).toBe('object')
const first = resp.data.addresses[coin_type]
expect(first.address).toBe(new_address)
})
})
37 changes: 25 additions & 12 deletions src/database/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub async fn insert_name(
.await?;

for address in addresses {
insert_address(
insert_or_update_address(
name.clone(),
namespace.clone(),
format!("{}", address.0),
Expand Down Expand Up @@ -247,23 +247,36 @@ pub async fn delete_address(
}

#[instrument(skip(postgres))]
pub async fn insert_address<'e>(
pub async fn insert_or_update_address<'e>(
name: String,
namespace: types::SupportedNamespaces,
chain_id: String,
address: String,
postgres: impl sqlx::PgExecutor<'e>,
) -> Result<sqlx::postgres::PgQueryResult, sqlx::error::Error> {
let query = sqlx::query::<Postgres>(
"
) -> Result<types::ENSIP11AddressesMap, sqlx::error::Error> {
let insert_or_update_query = "
INSERT INTO addresses (name, namespace, chain_id, address)
VALUES ($1, $2, $3, $4)
",
)
.bind(&name)
.bind(&namespace)
.bind(chain_id)
.bind(&address);
ON CONFLICT (name, namespace, chain_id, address) DO UPDATE
SET address = EXCLUDED.address, created_at = NOW()
RETURNING *
";
let row_result = sqlx::query_as::<Postgres, RowAddress>(insert_or_update_query)
.bind(&name)
.bind(&namespace)
.bind(chain_id)
.bind(&address)
.fetch_one(postgres)
.await?;

let mut result_map = types::ENSIP11AddressesMap::new();
result_map.insert(
row_result.chain_id.parse::<u32>().unwrap_or_default(),
types::Address {
address: row_result.address,
created_at: Some(row_result.created_at),
},
);

query.execute(postgres).await
Ok(result_map)
}
178 changes: 178 additions & 0 deletions src/handlers/profile/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use {
super::{
super::HANDLER_TASK_METRICS,
utils::is_timestamp_within_interval,
Eip155SupportedChains,
RegisterRequest,
UpdateAddressPayload,
UNIXTIMESTAMP_SYNC_THRESHOLD,
},
crate::{
database::{
helpers::{get_name_and_addresses_by_name, insert_or_update_address},
types::SupportedNamespaces,
},
error::RpcError,
state::AppState,
utils::crypto::{constant_time_eq, verify_message_signature},
},
axum::{
extract::{Path, State},
response::{IntoResponse, Response},
Json,
},
ethers::types::H160,
hyper::StatusCode,
num_enum::TryFromPrimitive,
std::{str::FromStr, sync::Arc},
tracing::log::{error, info},
wc::future::FutureExt,
};

pub async fn handler(
state: State<Arc<AppState>>,
name: Path<String>,
Json(request_payload): Json<RegisterRequest>,
) -> Result<Response, RpcError> {
handler_internal(state, name, request_payload)
.with_metrics(HANDLER_TASK_METRICS.with_name("profile_address_update"))
.await
}

#[tracing::instrument(skip(state))]
pub async fn handler_internal(
state: State<Arc<AppState>>,
Path(name): Path<String>,
request_payload: RegisterRequest,
) -> Result<Response, RpcError> {
let raw_payload = &request_payload.message;
let payload = match serde_json::from_str::<UpdateAddressPayload>(raw_payload) {
Ok(payload) => payload,
Err(e) => {
info!("Failed to deserialize update address payload: {}", e);
return Ok((StatusCode::BAD_REQUEST, "").into_response());
}
};

// Check for the supported ENSIP-11 coin type
if Eip155SupportedChains::try_from_primitive(request_payload.coin_type).is_err() {
info!("Unsupported coin type {}", request_payload.coin_type);
return Ok((
StatusCode::BAD_REQUEST,
"Unsupported coin type for name address update",
)
.into_response());
}

// Check is name registered
let name_addresses =
match get_name_and_addresses_by_name(name.clone(), &state.postgres.clone()).await {
Ok(result) => result,
Err(_) => {
info!(
"Update address request for not registered name {}",
name.clone()
);
return Ok((StatusCode::BAD_REQUEST, "Name is not registered").into_response());
}
};

// Check the timestamp is within the sync threshold interval
if !is_timestamp_within_interval(payload.timestamp, UNIXTIMESTAMP_SYNC_THRESHOLD) {
return Ok((
StatusCode::BAD_REQUEST,
"Timestamp is too old or in the future",
)
.into_response());
}

let payload_owner = match H160::from_str(&request_payload.address) {
Ok(owner) => owner,
Err(e) => {
info!("Failed to parse H160 address: {}", e);
return Ok((StatusCode::BAD_REQUEST, "Invalid H160 address format").into_response());
}
};

// Check the signature
let sinature_check =
match verify_message_signature(raw_payload, &request_payload.signature, &payload_owner) {
Ok(sinature_check) => sinature_check,
Err(e) => {
info!("Invalid signature: {}", e);
return Ok((
StatusCode::UNAUTHORIZED,
"Invalid signature or message format",
)
.into_response());
}
};
if !sinature_check {
return Ok((StatusCode::UNAUTHORIZED, "Signature verification error").into_response());
}

// Check for the name address ownership and address from the signed payload
let name_owner = match name_addresses.addresses.get(&60) {
Some(address_entry) => match H160::from_str(&address_entry.address) {
Ok(owner) => owner,
Err(e) => {
info!("Failed to parse H160 address: {}", e);
return Ok((StatusCode::BAD_REQUEST, "Invalid H160 address format").into_response());
}
},
None => {
info!("Address entry not found for key 60");
return Ok((
StatusCode::BAD_REQUEST,
"Address entry not found for key 60",
)
.into_response());
}
};
if !constant_time_eq(payload_owner, name_owner) {
return Ok((
StatusCode::UNAUTHORIZED,
"Address is not the owner of the name",
)
.into_response());
}

// Check for supported chain id and address format
if Eip155SupportedChains::try_from_primitive(payload.coin_type).is_err() {
info!(
"Unsupported coin type for name address update {}",
payload.coin_type
);
return Ok((
StatusCode::BAD_REQUEST,
"Unsupported coin type for name address update",
)
.into_response());
}

// Check the new address format
if let Err(e) = H160::from_str(&payload.address) {
info!("Failed to parse H160 address for update: {}", e);
return Ok((StatusCode::BAD_REQUEST, "Invalid H160 address format").into_response());
}

match insert_or_update_address(
name.clone(),
SupportedNamespaces::Eip155,
format!("{}", payload.coin_type),
payload.address,
&state.postgres.clone(),
)
.await
{
Ok(response) => Ok(Json(response).into_response()),
Err(e) => {
error!("Failed to update address: {}", e);
Ok((
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to update address",
)
.into_response())
}
}
}
12 changes: 12 additions & 0 deletions src/handlers/profile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use {
std::collections::HashMap,
};

pub mod address;
pub mod attributes;
pub mod lookup;
pub mod register;
Expand Down Expand Up @@ -56,6 +57,17 @@ pub struct UpdateAttributesPayload {
pub timestamp: u64,
}

/// Payload to update name address that should be serialized to JSON and signed
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateAddressPayload {
/// Coin type ENSIP-11
pub coin_type: u32,
/// Address
pub address: String,
/// Unixtime
pub timestamp: u64,
}

/// Data structure representing a request to register a name
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RegisterRequest {
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ pub async fn bootstrap(config: Config) -> RpcResult<()> {
"/v1/profile/account/:name/attributes",
post(handlers::profile::attributes::handler),
)
// Update account name address
.route(
"/v1/profile/account/:name/address",
post(handlers::profile::address::handler),
)
// Forward address lookup
.route(
"/v1/profile/account/:name",
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use {
get_name_and_addresses_by_name,
get_names_by_address,
get_names_by_address_and_namespace,
insert_address,
insert_name,
insert_or_update_address,
update_name_attributes,
},
types,
Expand Down Expand Up @@ -293,7 +293,7 @@ async fn insert_delete_two_addresses() {
// Inserting a new address
chain_id = 137;
let new_address = format!("0x{}", generate_random_string(16));
let insert_address_result = insert_address(
let insert_address_result = insert_or_update_address(
name.clone(),
types::SupportedNamespaces::Eip155,
format!("{}", chain_id),
Expand Down

0 comments on commit 1f10ef8

Please sign in to comment.