use std::collections::HashMap;
use std::str::FromStr;
use color_eyre::eyre::{ContextCompat, WrapErr};
use console::{style, Style};
use futures::StreamExt;
use glob::glob;
use near_cli_rs::common::{CallResultExt, JsonRpcClientExt, RpcQueryResponseExt};
use serde::de::{Deserialize, Deserializer};
use similar::{ChangeTag, TextDiff};
struct Line(Option<usize>);
impl std::fmt::Display for Line {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.0 {
None => write!(f, " "),
Some(idx) => write!(f, "{:<4}", idx + 1),
}
}
}
pub struct DiffCodeError;
pub fn diff_code(old_code: &str, new_code: &str) -> Result<(), DiffCodeError> {
let old_code = old_code.trim();
let new_code = new_code.trim();
if old_code == new_code {
return Ok(());
}
let diff = TextDiff::from_lines(old_code, new_code);
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
println!("{:-^1$}", "-", 80);
}
for op in group {
for change in diff.iter_inline_changes(op) {
let (sign, s) = match change.tag() {
ChangeTag::Delete => ("-", Style::new().red()),
ChangeTag::Insert => ("+", Style::new().green()),
ChangeTag::Equal => (" ", Style::new().dim()),
};
print!(
"{}{} |{}",
style(Line(change.old_index())).dim(),
style(Line(change.new_index())).dim(),
s.apply_to(sign).bold(),
);
for (emphasized, value) in change.iter_strings_lossy() {
if emphasized {
print!("{}", s.apply_to(value).underlined().on_black());
} else {
print!("{}", s.apply_to(value));
}
}
if change.missing_newline() {
println!();
}
}
}
}
Err(DiffCodeError)
}
pub fn get_local_components(
) -> color_eyre::eyre::Result<HashMap<String, crate::socialdb_types::SocialDbComponent>> {
let mut components = HashMap::new();
for component_filepath in glob("./src/**/*.jsx")?.filter_map(Result::ok) {
let component_name: crate::socialdb_types::ComponentName = component_filepath
.strip_prefix("src")?
.with_extension("")
.to_str()
.wrap_err_with(|| {
format!(
"Component name cannot be presented as UTF-8: {}",
component_filepath.display()
)
})?
.replace('/', ".");
let code = std::fs::read_to_string(&component_filepath).wrap_err_with(|| {
format!(
"Failed to read component source code from {}",
component_filepath.display()
)
})?;
let metadata_filepath = component_filepath.with_extension("metadata.json");
let metadata = if let Ok(metadata_json) = std::fs::read_to_string(&metadata_filepath) {
Some(serde_json::from_str(&metadata_json).wrap_err_with(|| {
format!(
"Failed to parse component metadata from {}",
metadata_filepath.display()
)
})?)
} else {
None
};
components.insert(
component_name,
crate::socialdb_types::SocialDbComponent::CodeWithMetadata { code, metadata },
);
}
Ok(components)
}
pub fn get_remote_components(
network_config: &near_cli_rs::config::NetworkConfig,
component_name_list: Vec<&String>,
near_social_account_id: &near_primitives::types::AccountId,
account_id: &near_primitives::types::AccountId,
) -> color_eyre::eyre::Result<
HashMap<crate::socialdb_types::ComponentName, crate::socialdb_types::SocialDbComponent>,
> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let chunk_size = 15;
let concurrency = 10;
runtime
.block_on(
futures::stream::iter(component_name_list.chunks(chunk_size))
.map(|components_name_batch| async {
get_components(
network_config,
near_social_account_id,
account_id,
components_name_batch,
)
.await
})
.buffer_unordered(concurrency)
.collect::<Vec<Result<_, _>>>(),
)
.into_iter()
.try_fold(HashMap::new(), |mut acc, x| {
acc.extend(x?);
Ok::<_, color_eyre::eyre::Error>(acc)
})
}
async fn get_components(
network_config: &near_cli_rs::config::NetworkConfig,
near_social_account_id: &near_primitives::types::AccountId,
account_id: &near_primitives::types::AccountId,
components_names_batch: &[&crate::socialdb_types::ComponentName],
) -> color_eyre::Result<
HashMap<crate::socialdb_types::ComponentName, crate::socialdb_types::SocialDbComponent>,
> {
let args = serde_json::to_string(&crate::socialdb_types::SocialDbQuery {
keys: components_names_batch
.iter()
.map(|name| format!("{account_id}/widget/{name}/**"))
.collect(),
})
.wrap_err("Internal error: could not serialize SocialDB input args")?
.into_bytes();
match network_config
.json_rpc_client()
.call(near_jsonrpc_client::methods::query::RpcQueryRequest {
block_reference: near_primitives::types::Finality::Final.into(),
request: near_primitives::views::QueryRequest::CallFunction {
account_id: near_social_account_id.clone(),
method_name: "get".to_string(),
args: near_primitives::types::FunctionArgs::from(args),
},
})
.await
.wrap_err("Failed to query batch of components from Social DB")?
.kind
{
near_jsonrpc_primitives::types::query::QueryResponseKind::CallResult(call_result) => {
Ok(call_result
.parse_result_from_json::<crate::socialdb_types::SocialDb>()
.wrap_err("ERROR: failed to parse Social DB response")?
.accounts
.remove(account_id)
.map(|crate::socialdb_types::SocialDbAccountMetadata { components }| components)
.unwrap_or_default())
}
_ => unreachable!("ERROR: unexpected response type from JSON RPC client"),
}
}
pub fn get_updated_components(
local_components: HashMap<String, crate::socialdb_types::SocialDbComponent>,
remote_components: &HashMap<
crate::socialdb_types::ComponentName,
crate::socialdb_types::SocialDbComponent,
>,
) -> HashMap<String, crate::socialdb_types::SocialDbComponent> {
local_components
.into_iter()
.filter(|(component_name, new_component)| {
if let Some(old_component) = remote_components.get(component_name) {
let has_code_changed = crate::common::diff_code(old_component.code(), new_component.code()).is_err();
let has_metadata_changed = old_component.metadata() != new_component.metadata() && new_component.metadata().is_some();
if !has_code_changed {
println!("Code for component <{component_name}> has not changed");
}
if has_metadata_changed {
println!(
"Metadata for component <{component_name}> changed:\n - old metadata: {:?}\n - new metadata: {:?}",
old_component.metadata(), new_component.metadata()
);
} else {
println!("Metadata for component <{component_name}> has not changed");
}
has_code_changed || has_metadata_changed
} else {
println!("Found new component <{component_name}> to deploy");
true
}
})
.collect()
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum PermissionKey {
#[serde(rename = "predecessor_id")]
PredecessorId(near_primitives::types::AccountId),
#[serde(rename = "public_key")]
PublicKey(near_crypto::PublicKey),
}
impl From<near_primitives::types::AccountId> for PermissionKey {
fn from(predecessor_id: near_primitives::types::AccountId) -> Self {
Self::PredecessorId(predecessor_id)
}
}
impl From<near_crypto::PublicKey> for PermissionKey {
fn from(public_key: near_crypto::PublicKey) -> Self {
Self::PublicKey(public_key)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct IsWritePermissionGrantedInputArgs {
key: String,
#[serde(flatten)]
permission_key: PermissionKey,
}
pub fn is_write_permission_granted<P: Into<PermissionKey>>(
network_config: &near_cli_rs::config::NetworkConfig,
near_social_account_id: &near_primitives::types::AccountId,
permission_key: P,
key: String,
) -> color_eyre::eyre::Result<bool> {
let function_args = serde_json::to_string(&IsWritePermissionGrantedInputArgs {
key,
permission_key: permission_key.into(),
})
.wrap_err("Internal error: could not serialize `is_write_permission_granted` input args")?;
let call_result = network_config
.json_rpc_client()
.blocking_call_view_function(
near_social_account_id,
"is_write_permission_granted",
function_args.into_bytes(),
near_primitives::types::Finality::Final.into(),
)
.wrap_err_with(|| "Failed to fetch query for view method: 'is_write_permission_granted'")?;
let serde_call_result: serde_json::Value = call_result.parse_result_from_json()?;
let result = serde_call_result.as_bool().expect("Unexpected response");
Ok(result)
}
pub fn is_signer_access_key_function_call_access_can_call_set_on_social_db_account(
near_social_account_id: &near_primitives::types::AccountId,
access_key_permission: &near_primitives::views::AccessKeyPermissionView,
) -> color_eyre::eyre::Result<bool> {
if let near_primitives::views::AccessKeyPermissionView::FunctionCall {
allowance: _,
receiver_id,
method_names,
} = access_key_permission
{
Ok(receiver_id == &near_social_account_id.to_string()
&& (method_names.contains(&"set".to_string()) || method_names.is_empty()))
} else {
Ok(false)
}
}
pub fn get_access_key_permission(
network_config: &near_cli_rs::config::NetworkConfig,
account_id: &near_primitives::types::AccountId,
public_key: &near_crypto::PublicKey,
) -> color_eyre::eyre::Result<near_primitives::views::AccessKeyPermissionView> {
let permission = network_config
.json_rpc_client()
.blocking_call_view_access_key(
account_id,
public_key,
near_primitives::types::Finality::Final.into(),
)
.wrap_err_with(|| format!("Failed to fetch query 'view access key' for <{public_key}>",))?
.access_key_view()?
.permission;
Ok(permission)
}
pub fn get_deposit(
network_config: &near_cli_rs::config::NetworkConfig,
signer_account_id: &near_primitives::types::AccountId,
signer_public_key: &near_crypto::PublicKey,
deploy_to_account_id: &near_primitives::types::AccountId,
key: &str,
near_social_account_id: &near_primitives::types::AccountId,
required_deposit: near_cli_rs::common::NearBalance,
) -> color_eyre::eyre::Result<near_cli_rs::common::NearBalance> {
let signer_access_key_permission = crate::common::get_access_key_permission(
network_config,
signer_account_id,
signer_public_key,
)?;
let is_signer_access_key_full_access = matches!(
signer_access_key_permission,
near_primitives::views::AccessKeyPermissionView::FullAccess
);
let is_write_permission_granted_to_public_key = crate::common::is_write_permission_granted(
network_config,
near_social_account_id,
signer_public_key.clone(),
format!("{deploy_to_account_id}/{key}"),
)?;
let is_write_permission_granted_to_signer = crate::common::is_write_permission_granted(
network_config,
near_social_account_id,
signer_account_id.clone(),
format!("{deploy_to_account_id}/{key}"),
)?;
let deposit = if is_signer_access_key_full_access
|| crate::common::is_signer_access_key_function_call_access_can_call_set_on_social_db_account(
near_social_account_id,
&signer_access_key_permission
)?
{
if is_write_permission_granted_to_public_key || is_write_permission_granted_to_signer {
if required_deposit.is_zero()
{
near_cli_rs::common::NearBalance::from_str("0 NEAR").unwrap()
} else if is_signer_access_key_full_access {
required_deposit
} else {
color_eyre::eyre::bail!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key")
}
} else if signer_account_id == deploy_to_account_id {
if is_signer_access_key_full_access {
if required_deposit.is_zero()
{
near_cli_rs::common::NearBalance::from_str("1 yoctoNEAR").unwrap()
} else {
required_deposit
}
} else {
color_eyre::eyre::bail!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key")
}
} else {
color_eyre::eyre::bail!(
"ERROR: the signer is not allowed to modify the components of this account_id."
)
}
} else {
color_eyre::eyre::bail!("ERROR: signer access key cannot be used to sign a transaction to update components in Social DB.")
};
Ok(deposit)
}
pub fn required_deposit(
network_config: &near_cli_rs::config::NetworkConfig,
near_social_account_id: &near_primitives::types::AccountId,
account_id: &near_primitives::types::AccountId,
data: &serde_json::Value,
prev_data: Option<&serde_json::Value>,
) -> color_eyre::eyre::Result<near_cli_rs::common::NearBalance> {
const STORAGE_COST_PER_BYTE: i128 = 10i128.pow(19);
const MIN_STORAGE_BALANCE: u128 = STORAGE_COST_PER_BYTE as u128 * 2000;
const INITIAL_ACCOUNT_STORAGE_BALANCE: i128 = STORAGE_COST_PER_BYTE * 500;
const EXTRA_STORAGE_BALANCE: i128 = STORAGE_COST_PER_BYTE * 5000;
let call_result_storage_balance = network_config
.json_rpc_client()
.blocking_call_view_function(
near_social_account_id,
"storage_balance_of",
serde_json::json!({
"account_id": account_id,
})
.to_string()
.into_bytes(),
near_primitives::types::Finality::Final.into(),
);
let storage_balance_result: color_eyre::eyre::Result<StorageBalance> =
call_result_storage_balance
.wrap_err_with(|| "Failed to fetch query for view method: 'storage_balance_of'")?
.parse_result_from_json()
.wrap_err_with(|| {
"Failed to parse return value of view function call for StorageBalance."
});
let (available_storage, initial_account_storage_balance, min_storage_balance) =
if let Ok(storage_balance) = storage_balance_result {
(storage_balance.available, 0, 0)
} else {
(0, INITIAL_ACCOUNT_STORAGE_BALANCE, MIN_STORAGE_BALANCE)
};
let estimated_storage_balance = u128::try_from(
STORAGE_COST_PER_BYTE * estimate_data_size(data, prev_data) as i128
+ initial_account_storage_balance
+ EXTRA_STORAGE_BALANCE,
)
.unwrap_or(0)
.saturating_sub(available_storage);
Ok(near_cli_rs::common::NearBalance::from_yoctonear(
std::cmp::max(estimated_storage_balance, min_storage_balance),
))
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct StorageBalance {
#[serde(deserialize_with = "parse_u128_string")]
pub available: u128,
#[serde(deserialize_with = "parse_u128_string")]
pub total: u128,
}
fn parse_u128_string<'de, D>(deserializer: D) -> color_eyre::eyre::Result<u128, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse::<u128>()
.map_err(serde::de::Error::custom)
}
fn estimate_data_size(data: &serde_json::Value, prev_data: Option<&serde_json::Value>) -> isize {
const ESTIMATED_KEY_VALUE_SIZE: isize = 40 * 3 + 8 + 12;
const ESTIMATED_NODE_SIZE: isize = 40 * 2 + 8 + 10;
match data {
serde_json::Value::Object(data) => {
let inner_data_size = data
.iter()
.map(|(key, value)| {
let prev_value = if let Some(serde_json::Value::Object(prev_data)) = prev_data {
prev_data.get(key)
} else {
None
};
if prev_value.is_some() {
estimate_data_size(value, prev_value)
} else {
key.len() as isize * 2
+ estimate_data_size(value, None)
+ ESTIMATED_KEY_VALUE_SIZE
}
})
.sum();
if prev_data.map(serde_json::Value::is_object).unwrap_or(false) {
inner_data_size
} else {
ESTIMATED_NODE_SIZE + inner_data_size
}
}
serde_json::Value::String(data) => {
data.len().max(8) as isize
- prev_data
.and_then(serde_json::Value::as_str)
.map(str::len)
.unwrap_or(0) as isize
}
_ => {
unreachable!("estimate_data_size expects only Object or String values");
}
}
}
pub fn mark_leaf_values_as_null(data: &mut serde_json::Value) {
match data {
serde_json::Value::Object(object_data) => {
for value in object_data.values_mut() {
mark_leaf_values_as_null(value);
}
}
data => {
*data = serde_json::Value::Null;
}
}
}
pub fn social_db_data_from_key(full_key: &str, data_to_set: &mut serde_json::Value) {
if let Some((prefix, key)) = full_key.rsplit_once('/') {
*data_to_set = serde_json::json!({ key: data_to_set });
social_db_data_from_key(prefix, data_to_set)
} else {
*data_to_set = serde_json::json!({ full_key: data_to_set });
}
}