near/near-cli-rs

Introduce teach-me mode

frol opened this issue · 0 comments

frol commented

near-cli-rs can be an awesome tool to educate NEAR developers about how things actually work in NEAR.

For example, if would be great to provide more details about RPC calls that are executed and their parameters when tokens view-ft-balance is executed, which queries RPC for the token metadata and then the balance for the specific user. This would help answering questions like this much easier, I would just ask them to run near CLI with --teach-me flag.

Here is a rough illustration of the process when near --teach-me flag is set:

$ near --teach-me
> What are you up to? (select one of the options with the up-down arrows on your keyboard and press Enter) tokens      - Manage token assets such as NEAR, FT, NFT
> What is your account ID? frol.near
> Select actions with tokens: view-ft-balance   - View the balance of FT tokens
> What is the ft-contract account ID? wrap.near
> What is the name of the network? mainnet
> Сhoose block for view: now               - View properties in the final block
TEACH-ME: In order to display the FT symbol and format the decimal amount of owned tokens, we need to make a view-function call `ft_metadata` to the <wrap.near> contract, and we can do that with the query request to NEAR RPC node (https://docs.near.org/api/rpc/contracts#call-a-contract-function):
HTTP POST https://rpc.mainnet.near.org/
JSON-BODY: {
  "jsonrpc": "2.0",
  "id": "dontcare",
  "method": "query",
  "params": {
    "request_type": "call_function",
    "finality": "final",
    "account_id": "wrap.near",
    "method_name": "ft_metadata",
    "args_base64": "e30="
  }
}
RESPONSE: {
    "id": "dontcare",
    "jsonrpc": "2.0",
    "result": {
        "block_hash": "E4oQ8YhUcMSve7kRMvNeoM3ZSjJopJc1tPLztg3BWkmg",
        "block_height": 93496936,
        "logs": [],
        "result": [123,34,115,112,101,99,34,58,34,102,116,45,49,46,48,46,48,34,44,34,110,97,109,101,34,58,34,87,114,97,112,112,101,100,32,78,69,65,82,32,102,117,110,103,105,98,108,101,32,116,111,107,101,110,34,44,34,115,121,109,98,111,108,34,58,34,119,78,69,65,82,34,44,34,105,99,111,110,34,58,110,117,108,108,44,34,114,101,102,101,114,101,110,99,101,34,58,110,117,108,108,44,34,114,101,102,101,114,101,110,99,101,95,104,97,115,104,34,58,110,117,108,108,44,34,100,101,99,105,109,97,108,115,34,58,50,52,125]
    }
}
TEACH-ME: The `args_base64` (`e30=`) is a base64-encoded serialized arguments to `ft_metadata` function (`{}`), and the output `result` is an array of bytes (`{"spec":"ft-1.0.0","name":"Wrapped NEAR fungible token","symbol":"wNEAR","icon":null,"reference":null,"reference_hash":null,"decimals":24}`).

TEACH-ME: In order to get the FT balance for the specific account, we need to make a view-function call `ft_balance_of` to the <wrap.near> contract, and we can do that with the query request to NEAR RPC node (https://docs.near.org/api/rpc/contracts#call-a-contract-function):
HTTP POST https://rpc.mainnet.near.org/
JSON-BODY: {
  "jsonrpc": "2.0",
  "id": "dontcare",
  "method": "query",
  "params": {
    "request_type": "call_function",
    "finality": "final",
    "account_id": "wrap.near",
    "method_name": "ft_balance_of",
    "args_base64": "eyJhY2NvdW50X2lkIjogImZyb2wubmVhciJ9"
  }
}
RESPONSE: {
    "id": "dontcare",
    "jsonrpc": "2.0",
    "result": {
        "block_hash": "E4oQ8YhUcMSve7kRMvNeoM3ZSjJopJc1tPLztg3BWkmg",
        "block_height": 93496936,
        "logs": [],
        "result": [34, 57, 56, 55, 53, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 34]
    }
}
TEACH-ME: The `args_base64` (`eyJhY2NvdW50X2lkIjogImZyb2wubmVhciJ9`) is a base64-encoded serialized arguments to `ft_balance_of` function (`{"account_id": "frol.near"}`), and the output `result` is an array of bytes (`"98750000000000000000000"`).

--------------
No logs
--------------

<frol.near> account has 0.09875 wNEAR  (FT-contract: wrap.near)

Implementation hints

Follow offline flag implementation that is part of GlobalContext to add --teach-me flag throughout the project.

We can easily introduce TEACH-ME to the RPC helper functions:

near-cli-rs/src/common.rs

Lines 1576 to 1667 in 4f8576e

#[easy_ext::ext(JsonRpcClientExt)]
pub impl near_jsonrpc_client::JsonRpcClient {
fn blocking_call<M>(
&self,
method: M,
) -> near_jsonrpc_client::MethodCallResult<M::Response, M::Error>
where
M: near_jsonrpc_client::methods::RpcMethod,
{
tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.call(method))
}
/// A helper function to make a view-funcation call using JSON encoding for the function
/// arguments and function return value.
fn blocking_call_view_function(
&self,
account_id: &near_primitives::types::AccountId,
method_name: &str,
args: Vec<u8>,
block_reference: near_primitives::types::BlockReference,
) -> Result<near_primitives::views::CallResult, color_eyre::eyre::Error> {
let query_view_method_response = self
.blocking_call(near_jsonrpc_client::methods::query::RpcQueryRequest {
block_reference,
request: near_primitives::views::QueryRequest::CallFunction {
account_id: account_id.clone(),
method_name: method_name.to_owned(),
args: near_primitives::types::FunctionArgs::from(args),
},
})
.wrap_err("Failed to make a view-function call")?;
query_view_method_response.call_result()
}
fn blocking_call_view_access_key(
&self,
account_id: &near_primitives::types::AccountId,
public_key: &near_crypto::PublicKey,
block_reference: near_primitives::types::BlockReference,
) -> Result<
near_jsonrpc_primitives::types::query::RpcQueryResponse,
near_jsonrpc_client::errors::JsonRpcError<
near_jsonrpc_primitives::types::query::RpcQueryError,
>,
> {
self.blocking_call(near_jsonrpc_client::methods::query::RpcQueryRequest {
block_reference,
request: near_primitives::views::QueryRequest::ViewAccessKey {
account_id: account_id.clone(),
public_key: public_key.clone(),
},
})
}
fn blocking_call_view_access_key_list(
&self,
account_id: &near_primitives::types::AccountId,
block_reference: near_primitives::types::BlockReference,
) -> Result<
near_jsonrpc_primitives::types::query::RpcQueryResponse,
near_jsonrpc_client::errors::JsonRpcError<
near_jsonrpc_primitives::types::query::RpcQueryError,
>,
> {
self.blocking_call(near_jsonrpc_client::methods::query::RpcQueryRequest {
block_reference,
request: near_primitives::views::QueryRequest::ViewAccessKeyList {
account_id: account_id.clone(),
},
})
}
fn blocking_call_view_account(
&self,
account_id: &near_primitives::types::AccountId,
block_reference: near_primitives::types::BlockReference,
) -> Result<
near_jsonrpc_primitives::types::query::RpcQueryResponse,
near_jsonrpc_client::errors::JsonRpcError<
near_jsonrpc_primitives::types::query::RpcQueryError,
>,
> {
self.blocking_call(near_jsonrpc_client::methods::query::RpcQueryRequest {
block_reference,
request: near_primitives::views::QueryRequest::ViewAccount {
account_id: account_id.clone(),
},
})
}
}

near-cli-rs/src/common.rs

Lines 1688 to 1767 in 4f8576e

#[easy_ext::ext(RpcQueryResponseExt)]
pub impl near_jsonrpc_primitives::types::query::RpcQueryResponse {
fn access_key_view(&self) -> color_eyre::eyre::Result<near_primitives::views::AccessKeyView> {
if let near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKey(
access_key_view,
) = &self.kind
{
Ok(access_key_view.clone())
} else {
color_eyre::eyre::bail!(
"Internal error: Received unexpected query kind in response to a View Access Key query call",
);
}
}
fn access_key_list_view(
&self,
) -> color_eyre::eyre::Result<near_primitives::views::AccessKeyList> {
if let near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKeyList(
access_key_list,
) = &self.kind
{
Ok(access_key_list.clone())
} else {
color_eyre::eyre::bail!(
"Internal error: Received unexpected query kind in response to a View Access Key List query call",
);
}
}
fn account_view(&self) -> color_eyre::eyre::Result<near_primitives::views::AccountView> {
if let near_jsonrpc_primitives::types::query::QueryResponseKind::ViewAccount(account_view) =
&self.kind
{
Ok(account_view.clone())
} else {
color_eyre::eyre::bail!(
"Internal error: Received unexpected query kind in response to a View Account query call",
);
}
}
fn call_result(&self) -> color_eyre::eyre::Result<near_primitives::views::CallResult> {
if let near_jsonrpc_primitives::types::query::QueryResponseKind::CallResult(result) =
&self.kind
{
Ok(result.clone())
} else {
color_eyre::eyre::bail!(
"Internal error: Received unexpected query kind in response to a view-function query call",
);
}
}
}
#[easy_ext::ext(CallResultExt)]
pub impl near_primitives::views::CallResult {
fn parse_result_from_json<T>(&self) -> Result<T, color_eyre::eyre::Error>
where
T: for<'de> serde::Deserialize<'de>,
{
serde_json::from_slice(&self.result).wrap_err_with(|| {
format!(
"Failed to parse view-function call return value: {}",
String::from_utf8_lossy(&self.result)
)
})
}
fn print_logs(&self) {
eprintln!("--------------");
if self.logs.is_empty() {
eprintln!("No logs")
} else {
eprintln!("Logs:");
eprintln!(" {}", self.logs.join("\n "));
}
eprintln!("--------------");
}
}

That would already get us 80% through. Let's start with this and iterate from here.