1use eyre::WrapErr;
2
3pub mod types;
4
5use serde::de::{Deserialize, Deserializer};
6
7#[derive(Debug, Clone, serde::Deserialize)]
8pub struct StorageBalance {
9 #[serde(deserialize_with = "parse_u128_string")]
10 pub available: u128,
11 #[serde(deserialize_with = "parse_u128_string")]
12 pub total: u128,
13}
14
15fn parse_u128_string<'de, D>(deserializer: D) -> eyre::Result<u128, D::Error>
16where
17 D: Deserializer<'de>,
18{
19 <std::string::String as Deserialize>::deserialize(deserializer)?
20 .parse::<u128>()
21 .map_err(serde::de::Error::custom)
22}
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
25pub enum PermissionKey {
26 #[serde(rename = "predecessor_id")]
27 PredecessorId(near_primitives::types::AccountId),
28 #[serde(rename = "public_key")]
29 PublicKey(near_crypto::PublicKey),
30}
31
32impl From<near_primitives::types::AccountId> for PermissionKey {
33 fn from(predecessor_id: near_primitives::types::AccountId) -> Self {
34 Self::PredecessorId(predecessor_id)
35 }
36}
37
38impl From<near_crypto::PublicKey> for PermissionKey {
39 fn from(public_key: near_crypto::PublicKey) -> Self {
40 Self::PublicKey(public_key)
41 }
42}
43
44#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
45struct IsWritePermissionGrantedInputArgs {
46 key: String,
47 #[serde(flatten)]
48 permission_key: PermissionKey,
49}
50
51pub async fn is_write_permission_granted<P: Into<PermissionKey>>(
52 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
53 near_social_account_id: &near_primitives::types::AccountId,
54 permission_key: P,
55 key: String,
56) -> eyre::Result<bool> {
57 let function_args = serde_json::to_string(&IsWritePermissionGrantedInputArgs {
58 key,
59 permission_key: permission_key.into(),
60 })
61 .wrap_err("Internal error: could not serialize `is_write_permission_granted` input args")?;
62 let call_result = match json_rpc_client
63 .call(near_jsonrpc_client::methods::query::RpcQueryRequest {
64 block_reference: near_primitives::types::Finality::Final.into(),
65 request: near_primitives::views::QueryRequest::CallFunction {
66 account_id: near_social_account_id.clone(),
67 method_name: "is_write_permission_granted".to_string(),
68 args: near_primitives::types::FunctionArgs::from(function_args.into_bytes()),
69 },
70 })
71 .await
72 .wrap_err_with(|| "Failed to fetch query for view method: 'is_write_permission_granted'")?
73 .kind
74 {
75 near_jsonrpc_primitives::types::query::QueryResponseKind::CallResult(call_result) => {
76 call_result
77 }
78 _ => eyre::bail!("ERROR: unexpected response type from JSON RPC client"),
79 };
80
81 let serde_call_result: serde_json::Value = serde_json::from_slice(&call_result.result)
82 .wrap_err_with(|| {
83 format!(
84 "Failed to parse view-function call return value: {}",
85 String::from_utf8_lossy(&call_result.result)
86 )
87 })?;
88 let result = serde_call_result.as_bool().expect("Unexpected response");
89 Ok(result)
90}
91
92pub fn is_signer_access_key_function_call_access_can_call_set_on_social_db_account(
93 near_social_account_id: &near_primitives::types::AccountId,
94 access_key_permission: &near_primitives::views::AccessKeyPermissionView,
95) -> eyre::Result<bool> {
96 if let near_primitives::views::AccessKeyPermissionView::FunctionCall {
97 allowance: _,
98 receiver_id,
99 method_names,
100 } = access_key_permission
101 {
102 Ok(receiver_id == &near_social_account_id.to_string()
103 && (method_names.contains(&"set".to_string()) || method_names.is_empty()))
104 } else {
105 Ok(false)
106 }
107}
108
109pub async fn get_access_key_permission(
110 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
111 account_id: &near_primitives::types::AccountId,
112 public_key: &near_crypto::PublicKey,
113) -> eyre::Result<near_primitives::views::AccessKeyPermissionView> {
114 let permission = match json_rpc_client
115 .call(near_jsonrpc_client::methods::query::RpcQueryRequest {
116 block_reference: near_primitives::types::Finality::Final.into(),
117 request: near_primitives::views::QueryRequest::ViewAccessKey {
118 account_id: account_id.clone(),
119 public_key: public_key.clone(),
120 },
121 })
122 .await
123 .wrap_err_with(|| format!("Failed to fetch query 'view access key' for <{public_key}>",))?
124 .kind
125 {
126 near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKey(
127 access_key_view,
128 ) => access_key_view.permission,
129 _ => eyre::bail!(
130 "Internal error: Received unexpected query kind in response to a View Access Key query call",
131 )
132 };
133
134 Ok(permission)
135}
136
137pub async fn get_deposit(
138 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
139 signer_account_id: &near_primitives::types::AccountId,
140 signer_public_key: &near_crypto::PublicKey,
141 account_id: &near_primitives::types::AccountId,
142 key: &str,
143 near_social_account_id: &near_primitives::types::AccountId,
144 required_deposit: near_token::NearToken,
145) -> eyre::Result<near_token::NearToken> {
146 let signer_access_key_permission =
147 get_access_key_permission(json_rpc_client, signer_account_id, signer_public_key).await?;
148
149 let is_signer_access_key_full_access = matches!(
150 signer_access_key_permission,
151 near_primitives::views::AccessKeyPermissionView::FullAccess
152 );
153
154 let is_write_permission_granted_to_public_key = is_write_permission_granted(
155 json_rpc_client,
156 near_social_account_id,
157 signer_public_key.clone(),
158 format!("{account_id}/{key}"),
159 )
160 .await?;
161
162 let is_write_permission_granted_to_signer = is_write_permission_granted(
163 json_rpc_client,
164 near_social_account_id,
165 signer_account_id.clone(),
166 format!("{account_id}/{key}"),
167 )
168 .await?;
169
170 let deposit = if is_signer_access_key_full_access
171 || is_signer_access_key_function_call_access_can_call_set_on_social_db_account(
172 near_social_account_id,
173 &signer_access_key_permission,
174 )? {
175 if is_write_permission_granted_to_public_key || is_write_permission_granted_to_signer {
176 if required_deposit.is_zero() {
177 near_token::NearToken::from_near(0)
178 } else if is_signer_access_key_full_access {
179 required_deposit
180 } else {
181 eyre::bail!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key")
182 }
183 } else if signer_account_id == account_id {
184 if is_signer_access_key_full_access {
185 if required_deposit.is_zero() {
186 near_token::NearToken::from_yoctonear(1)
187 } else {
188 required_deposit
189 }
190 } else if required_deposit.is_zero() {
191 required_deposit
192 } else {
193 eyre::bail!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key")
194 }
195 } else {
196 eyre::bail!(
197 "ERROR: the signer is not allowed to modify the components of this account_id."
198 )
199 }
200 } else {
201 eyre::bail!("ERROR: signer access key cannot be used to sign a transaction to update components in Social DB.")
202 };
203 Ok(deposit)
204}
205
206pub async fn required_deposit(
207 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
208 near_social_account_id: &near_primitives::types::AccountId,
209 account_id: &near_primitives::types::AccountId,
210 data: &serde_json::Value,
211 prev_data: Option<&serde_json::Value>,
212) -> eyre::Result<near_token::NearToken> {
213 const STORAGE_COST_PER_BYTE: i128 = 10i128.pow(19);
214 const MIN_STORAGE_BALANCE: u128 = STORAGE_COST_PER_BYTE as u128 * 2000;
215 const INITIAL_ACCOUNT_STORAGE_BALANCE: i128 = STORAGE_COST_PER_BYTE * 500;
216 const EXTRA_STORAGE_BALANCE: i128 = STORAGE_COST_PER_BYTE * 5000;
217
218 let call_result_storage_balance = match json_rpc_client
219 .call(near_jsonrpc_client::methods::query::RpcQueryRequest {
220 block_reference: near_primitives::types::Finality::Final.into(),
221 request: near_primitives::views::QueryRequest::CallFunction {
222 account_id: near_social_account_id.clone(),
223 method_name: "storage_balance_of".to_string(),
224 args: near_primitives::types::FunctionArgs::from(
225 serde_json::json!({
226 "account_id": account_id,
227 })
228 .to_string()
229 .into_bytes(),
230 ),
231 },
232 })
233 .await
234 .wrap_err_with(|| "Failed to fetch query for view method: 'storage_balance_of'")?
235 .kind
236 {
237 near_jsonrpc_primitives::types::query::QueryResponseKind::CallResult(call_result) => {
238 call_result
239 }
240 _ => eyre::bail!("ERROR: unexpected response type from JSON RPC client"),
241 };
242
243 let storage_balance_result: eyre::Result<StorageBalance> =
244 serde_json::from_slice(&call_result_storage_balance.result).wrap_err_with(|| {
245 format!(
246 "Failed to parse view-function call return value: {}",
247 String::from_utf8_lossy(&call_result_storage_balance.result)
248 )
249 });
250
251 let (available_storage, initial_account_storage_balance, min_storage_balance) =
252 if let Ok(storage_balance) = storage_balance_result {
253 (storage_balance.available, 0, 0)
254 } else {
255 (0, INITIAL_ACCOUNT_STORAGE_BALANCE, MIN_STORAGE_BALANCE)
256 };
257
258 let estimated_storage_balance = u128::try_from(
259 STORAGE_COST_PER_BYTE * estimate_data_size(data, prev_data) as i128
260 + initial_account_storage_balance
261 + EXTRA_STORAGE_BALANCE,
262 )
263 .unwrap_or(0)
264 .saturating_sub(available_storage);
265 Ok(near_token::NearToken::from_yoctonear(std::cmp::max(
266 estimated_storage_balance,
267 min_storage_balance,
268 )))
269}
270
271fn estimate_data_size(data: &serde_json::Value, prev_data: Option<&serde_json::Value>) -> isize {
273 const ESTIMATED_KEY_VALUE_SIZE: isize = 40 * 3 + 8 + 12;
274 const ESTIMATED_NODE_SIZE: isize = 40 * 2 + 8 + 10;
275
276 match data {
277 serde_json::Value::Object(data) => {
278 let inner_data_size = data
279 .iter()
280 .map(|(key, value)| {
281 let prev_value = if let Some(serde_json::Value::Object(prev_data)) = prev_data {
282 prev_data.get(key)
283 } else {
284 None
285 };
286 if prev_value.is_some() {
287 estimate_data_size(value, prev_value)
288 } else {
289 key.len() as isize * 2
290 + estimate_data_size(value, None)
291 + ESTIMATED_KEY_VALUE_SIZE
292 }
293 })
294 .sum();
295 if prev_data.map(serde_json::Value::is_object).unwrap_or(false) {
296 inner_data_size
297 } else {
298 ESTIMATED_NODE_SIZE + inner_data_size
299 }
300 }
301 serde_json::Value::String(data) => {
302 data.len().max(8) as isize
303 - prev_data
304 .and_then(serde_json::Value::as_str)
305 .map(str::len)
306 .unwrap_or(0) as isize
307 }
308 _ => {
309 unreachable!("estimate_data_size expects only Object or String values");
310 }
311 }
312}
313
314pub fn mark_leaf_values_as_null(data: &mut serde_json::Value) {
316 match data {
317 serde_json::Value::Object(object_data) => {
318 for value in object_data.values_mut() {
319 mark_leaf_values_as_null(value);
320 }
321 }
322 data => {
323 *data = serde_json::Value::Null;
324 }
325 }
326}
327
328pub fn social_db_data_from_key(full_key: &str, data_to_set: &mut serde_json::Value) {
329 if let Some((prefix, key)) = full_key.rsplit_once('/') {
330 *data_to_set = serde_json::json!({ key: data_to_set });
331 social_db_data_from_key(prefix, data_to_set)
332 } else {
333 *data_to_set = serde_json::json!({ full_key: data_to_set });
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use httpmock::prelude::*;
340 use near_jsonrpc_client::JsonRpcClient;
341 use near_primitives::types::AccountId;
342 use near_token::NearToken;
343 use serde_json::{json, Value};
344
345 use crate::get_deposit;
346
347 fn mock_rpc(write_permission: bool) -> String {
348 let server = MockServer::start();
349
350 server.mock(|when, then| {
351 when.body_contains("view_access_key");
352 then.json_body(json!({
353 "jsonrpc": "2.0",
354 "result": {
355 "nonce": 85,
356 "permission": {
357 "FunctionCall": {
358 "allowance": "18501534631167209000000000",
359 "receiver_id": "social.near",
360 "method_names": ["set"]
361 }
362 },
363 "block_height": 19884918,
364 "block_hash": "GGJQ8yjmo7aEoj8ZpAhGehnq9BSWFx4xswHYzDwwAP2n"
365 },
366 "id": "dontcare"
367 }));
368 });
369
370 server.mock(|when, then| {
371 when.body_contains("is_write_permission_granted");
372 let write_permission_json_str =
373 serde_json::to_string(&json!(write_permission)).unwrap();
374 let binary_write_permission = write_permission_json_str.as_bytes().to_vec();
375 then.json_body(json!({
376 "jsonrpc": "2.0",
377 "result": {
378 "result": binary_write_permission,
379 "logs": [],
380 "block_height": 17817336,
381 "block_hash": "4qkA4sUUG8opjH5Q9bL5mWJTnfR4ech879Db1BZXbx6P"
382 },
383 "id": "dontcare"
384 }));
385 });
386
387 server.mock(|when, then| {
388 when.matches(|req| {
389 if let Some(body_bytes) = &req.body {
390 let body_str = String::from_utf8_lossy(body_bytes);
392 if let Ok(json_body) = serde_json::from_str::<Value>(&body_str) {
393 println!(
394 "No mock for request: {}",
395 serde_json::to_string_pretty(&json_body).unwrap()
396 );
397 } else {
398 println!("Failed to parse JSON body");
399 }
400 }
401 true
402 });
403 then.status(500);
404 });
405 server.url("/")
406 }
407
408 #[tokio::test]
409 pub async fn test_get_deposit_own_account_explicit_write_permission() {
410 let key_pair = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519);
411
412 let server_url = mock_rpc(true);
413 let json_rpc_client: JsonRpcClient = JsonRpcClient::connect(&server_url);
414
415 let signer_account_id: AccountId = "devhub.near".parse().unwrap();
416 let public_key = key_pair.public_key();
417
418 let deposit = get_deposit(
419 &json_rpc_client,
420 &signer_account_id,
421 &public_key,
422 &"devhub.near".parse().unwrap(),
423 "devhub.near/widget/app",
424 &"social.near".parse().unwrap(),
425 NearToken::from_near(0),
426 )
427 .await;
428
429 match deposit {
430 Ok(deposit_value) => {
431 assert_eq!(NearToken::from_near(0), deposit_value);
432 }
433 Err(e) => {
434 println!("Error: {:?}", e);
435 panic!("get_deposit failed");
436 }
437 }
438 }
439
440 #[tokio::test]
441 pub async fn test_get_deposit_own_account_no_explicit_write_permission() {
442 let key_pair = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519);
443
444 let server_url = mock_rpc(false);
445 let json_rpc_client: JsonRpcClient = JsonRpcClient::connect(&server_url);
446
447 let signer_account_id: AccountId = "devhub.near".parse().unwrap();
448 let public_key = key_pair.public_key();
449
450 let deposit = get_deposit(
451 &json_rpc_client,
452 &signer_account_id,
453 &public_key,
454 &"devhub.near".parse().unwrap(),
455 "devhub.near/widget/app",
456 &"social.near".parse().unwrap(),
457 NearToken::from_near(0),
458 )
459 .await;
460
461 match deposit {
462 Ok(deposit_value) => {
463 assert_eq!(NearToken::from_near(0), deposit_value);
464 }
465 Err(e) => {
466 println!("Error: {:?}", e);
467 panic!("get_deposit should not fail when using a public key belonging to the target account, even without explicit write permission. Error Message:\n{}", e);
468 }
469 }
470 }
471
472 #[tokio::test]
473 pub async fn test_get_deposit_other_account_no_explicit_write_permission() {
474 let key_pair = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519);
475
476 let server_url = mock_rpc(false);
477 let json_rpc_client: JsonRpcClient = JsonRpcClient::connect(&server_url);
478
479 let signer_account_id: AccountId = "notdevhub.near".parse().unwrap();
480 let public_key = key_pair.public_key();
481
482 let deposit = get_deposit(
483 &json_rpc_client,
484 &signer_account_id,
485 &public_key,
486 &"devhub.near".parse().unwrap(),
487 "devhub.near/widget/app",
488 &"social.near".parse().unwrap(),
489 NearToken::from_near(0),
490 )
491 .await;
492
493 match deposit {
494 Ok(_deposit_value) => {
495 panic!("get_deposit should fail when using a public key belonging to a different account without explicit write permission");
496 }
497 Err(e) => {
498 assert_eq!(
499 "ERROR: the signer is not allowed to modify the components of this account_id.",
500 e.to_string()
501 );
502 }
503 }
504 }
505
506 #[tokio::test]
507 pub async fn test_get_deposit_same_account_function_access_key_with_required_deposit() {
508 let key_pair = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519);
509
510 let server_url = mock_rpc(false);
511 let json_rpc_client: JsonRpcClient = JsonRpcClient::connect(&server_url);
512
513 let signer_account_id: AccountId = "devhub.near".parse().unwrap();
514 let public_key = key_pair.public_key();
515
516 let deposit = get_deposit(
517 &json_rpc_client,
518 &signer_account_id,
519 &public_key,
520 &"devhub.near".parse().unwrap(),
521 "devhub.near/widget/app",
522 &"social.near".parse().unwrap(),
523 NearToken::from_near(1),
524 )
525 .await;
526
527 match deposit {
528 Ok(_deposit_value) => {
529 panic!("get_deposit should fail when using a public key belonging for a function access key from the owner account when there is a required deposit");
530 }
531 Err(e) => {
532 assert_eq!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key", e.to_string());
533 }
534 }
535 }
536
537 #[tokio::test]
538 pub async fn test_get_deposit_write_permission_function_access_key_with_required_deposit() {
539 let key_pair = near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519);
540
541 let server_url = mock_rpc(true);
542 let json_rpc_client: JsonRpcClient = JsonRpcClient::connect(&server_url);
543
544 let signer_account_id: AccountId = "devhub.near".parse().unwrap();
545 let public_key = key_pair.public_key();
546
547 let deposit = get_deposit(
548 &json_rpc_client,
549 &signer_account_id,
550 &public_key,
551 &"devhub.near".parse().unwrap(),
552 "devhub.near/widget/app",
553 &"social.near".parse().unwrap(),
554 NearToken::from_near(1),
555 )
556 .await;
557
558 match deposit {
559 Ok(_deposit_value) => {
560 panic!("get_deposit should fail when using a public key with write permission when there is a required deposit");
561 }
562 Err(e) => {
563 assert_eq!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key", e.to_string());
564 }
565 }
566 }
567}