1use anyhow::{Context, Result};
2use near_crypto::{InMemorySigner, SecretKey};
3use near_jsonrpc_client::{methods, JsonRpcClient};
4use near_primitives::hash::CryptoHash;
5use near_primitives::transaction::{Action, FunctionCallAction, Transaction, TransactionV0};
6use near_primitives::types::{AccountId, BlockReference, Finality};
7use near_primitives::views::FinalExecutionOutcomeView;
8use serde_json::Value;
9
10use crate::api::ApiClient;
11use crate::config::{self, Credentials, NetworkConfig};
12
13pub struct NearClient {
16 client: JsonRpcClient,
17 pub network: NetworkConfig,
18}
19
20impl NearClient {
21 pub fn new(network: &NetworkConfig) -> Self {
22 let client = JsonRpcClient::connect(&network.rpc_url);
23 Self {
24 client,
25 network: network.clone(),
26 }
27 }
28
29 pub async fn view_call<T: serde::de::DeserializeOwned>(
30 &self,
31 method: &str,
32 args: Value,
33 ) -> Result<T> {
34 let contract_id: AccountId = self
35 .network
36 .contract_id
37 .parse()
38 .context("Invalid contract_id")?;
39
40 let request = methods::query::RpcQueryRequest {
41 block_reference: BlockReference::Finality(Finality::Final),
42 request: near_primitives::views::QueryRequest::CallFunction {
43 account_id: contract_id,
44 method_name: method.to_string(),
45 args: args.to_string().into_bytes().into(),
46 },
47 };
48
49 let response = self
50 .client
51 .call(request)
52 .await
53 .with_context(|| format!("Failed to call view method '{method}'"))?;
54
55 if let near_jsonrpc_primitives::types::query::QueryResponseKind::CallResult(result) =
56 response.kind
57 {
58 serde_json::from_slice(&result.result)
59 .with_context(|| format!("Failed to parse response from '{method}'"))
60 } else {
61 anyhow::bail!("Unexpected response kind from '{method}'");
62 }
63 }
64
65 pub async fn get_project(&self, project_id: &str) -> Result<Option<ProjectView>> {
66 let result: Option<ProjectView> = self
67 .view_call("get_project", serde_json::json!({ "project_id": project_id }))
68 .await?;
69 Ok(result)
70 }
71
72 pub async fn get_next_payment_key_nonce(&self, account_id: &str) -> Result<u32> {
73 let result: u32 = self
74 .view_call(
75 "get_next_payment_key_nonce",
76 serde_json::json!({ "account_id": account_id }),
77 )
78 .await?;
79 Ok(result)
80 }
81
82 pub async fn list_user_secrets(&self, account_id: &str) -> Result<Vec<UserSecretInfo>> {
83 self.view_call(
84 "list_user_secrets",
85 serde_json::json!({ "account_id": account_id }),
86 )
87 .await
88 }
89
90 pub async fn list_versions(
91 &self,
92 project_id: &str,
93 from_index: Option<u64>,
94 limit: Option<u64>,
95 ) -> Result<Vec<VersionView>> {
96 self.view_call(
97 "list_versions",
98 serde_json::json!({
99 "project_id": project_id,
100 "from_index": from_index,
101 "limit": limit
102 }),
103 )
104 .await
105 }
106
107 pub async fn get_developer_earnings(&self, account_id: &str) -> Result<String> {
108 self.view_call(
109 "get_developer_earnings",
110 serde_json::json!({ "account_id": account_id }),
111 )
112 .await
113 }
114
115 pub async fn estimate_execution_cost(
116 &self,
117 resource_limits: Option<Value>,
118 ) -> Result<String> {
119 self.view_call(
120 "estimate_execution_cost",
121 serde_json::json!({ "resource_limits": resource_limits }),
122 )
123 .await
124 }
125
126 pub async fn get_version(
127 &self,
128 project_id: &str,
129 version_key: &str,
130 ) -> Result<Option<VersionView>> {
131 self.view_call(
132 "get_version",
133 serde_json::json!({
134 "project_id": project_id,
135 "version_key": version_key
136 }),
137 )
138 .await
139 }
140}
141
142pub struct NearSigner {
145 client: JsonRpcClient,
146 signer: InMemorySigner,
147 contract_id: AccountId,
148}
149
150impl NearSigner {
151 pub fn new(network: &NetworkConfig, account_id: &str, private_key: &str) -> Result<Self> {
152 let account_id: AccountId = account_id.parse().context("Invalid account_id")?;
153 let contract_id: AccountId = network.contract_id.parse().context("Invalid contract_id")?;
154 let secret_key: SecretKey = private_key.parse().context("Invalid private key")?;
155 let signer = InMemorySigner::from_secret_key(account_id, secret_key);
156 let client = JsonRpcClient::connect(&network.rpc_url);
157
158 Ok(Self {
159 client,
160 signer,
161 contract_id,
162 })
163 }
164
165 pub async fn call_contract(
166 &self,
167 method_name: &str,
168 args: Value,
169 gas: u64,
170 deposit: u128,
171 ) -> Result<FinalExecutionOutcomeView> {
172 let access_key_query = methods::query::RpcQueryRequest {
174 block_reference: BlockReference::Finality(Finality::Final),
175 request: near_primitives::views::QueryRequest::ViewAccessKey {
176 account_id: self.signer.account_id.clone(),
177 public_key: self.signer.public_key(),
178 },
179 };
180
181 let access_key_response = self
182 .client
183 .call(access_key_query)
184 .await
185 .context("Failed to query access key")?;
186
187 let current_nonce = match access_key_response.kind {
188 near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKey(access_key) => {
189 access_key.nonce
190 }
191 _ => anyhow::bail!("Unexpected query response for access key"),
192 };
193
194 let block = self
196 .client
197 .call(methods::block::RpcBlockRequest {
198 block_reference: BlockReference::Finality(Finality::Final),
199 })
200 .await
201 .context("Failed to query block")?;
202
203 let block_hash = block.header.hash;
204
205 let transaction_v0 = TransactionV0 {
207 signer_id: self.signer.account_id.clone(),
208 public_key: self.signer.public_key(),
209 nonce: current_nonce + 1,
210 receiver_id: self.contract_id.clone(),
211 block_hash,
212 actions: vec![Action::FunctionCall(Box::new(FunctionCallAction {
213 method_name: method_name.to_string(),
214 args: args.to_string().into_bytes(),
215 gas,
216 deposit,
217 }))],
218 };
219
220 let transaction = Transaction::V0(transaction_v0);
221
222 let signature = self
224 .signer
225 .sign(transaction.get_hash_and_size().0.as_ref());
226 let signed_transaction =
227 near_primitives::transaction::SignedTransaction::new(signature, transaction);
228
229 let outcome = self
231 .client
232 .call(methods::broadcast_tx_commit::RpcBroadcastTxCommitRequest {
233 signed_transaction,
234 })
235 .await
236 .context("Transaction failed")?;
237
238 Ok(outcome)
239 }
240
241 pub async fn get_tx_context(&self) -> Result<(u64, CryptoHash)> {
243 let access_key_query = methods::query::RpcQueryRequest {
244 block_reference: BlockReference::Finality(Finality::Final),
245 request: near_primitives::views::QueryRequest::ViewAccessKey {
246 account_id: self.signer.account_id.clone(),
247 public_key: self.signer.public_key(),
248 },
249 };
250
251 let access_key_response = self
252 .client
253 .call(access_key_query)
254 .await
255 .context("Failed to query access key")?;
256
257 let current_nonce = match access_key_response.kind {
258 near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKey(access_key) => {
259 access_key.nonce
260 }
261 _ => anyhow::bail!("Unexpected query response for access key"),
262 };
263
264 let block = self
265 .client
266 .call(methods::block::RpcBlockRequest {
267 block_reference: BlockReference::Finality(Finality::Final),
268 })
269 .await
270 .context("Failed to query block")?;
271
272 Ok((current_nonce, block.header.hash))
273 }
274
275 pub async fn send_function_call_async(
278 &self,
279 receiver_id: &AccountId,
280 method_name: &str,
281 args: Vec<u8>,
282 gas: u64,
283 deposit: u128,
284 nonce: u64,
285 block_hash: CryptoHash,
286 ) -> Result<CryptoHash> {
287 let transaction_v0 = TransactionV0 {
288 signer_id: self.signer.account_id.clone(),
289 public_key: self.signer.public_key(),
290 nonce,
291 receiver_id: receiver_id.clone(),
292 block_hash,
293 actions: vec![Action::FunctionCall(Box::new(FunctionCallAction {
294 method_name: method_name.to_string(),
295 args,
296 gas,
297 deposit,
298 }))],
299 };
300
301 let transaction = Transaction::V0(transaction_v0);
302 let signature = self
303 .signer
304 .sign(transaction.get_hash_and_size().0.as_ref());
305 let signed_transaction =
306 near_primitives::transaction::SignedTransaction::new(signature, transaction);
307
308 let tx_hash = self
309 .client
310 .call(methods::broadcast_tx_async::RpcBroadcastTxAsyncRequest {
311 signed_transaction,
312 })
313 .await
314 .context("Failed to broadcast transaction")?;
315
316 Ok(tx_hash)
317 }
318}
319
320pub struct ContractCallResult {
324 pub value: Option<Value>,
325 pub tx_hash: Option<String>,
326}
327
328pub enum ContractCaller {
331 Local(NearSigner),
332 Wallet {
333 api: ApiClient,
334 wallet_key: String,
335 contract_id: String,
336 },
337}
338
339impl ContractCaller {
340 pub fn from_credentials(creds: &Credentials, network: &NetworkConfig) -> Result<Self> {
342 if creds.is_wallet_key() {
343 let wk = creds
344 .wallet_key
345 .as_ref()
346 .context("wallet_key missing from credentials")?;
347 Ok(Self::Wallet {
348 api: ApiClient::new(network),
349 wallet_key: wk.clone(),
350 contract_id: network.contract_id.clone(),
351 })
352 } else {
353 let pk = config::load_private_key(&network.network_id, &creds.account_id, creds)?;
354 Ok(Self::Local(NearSigner::new(
355 network,
356 &creds.account_id,
357 &pk,
358 )?))
359 }
360 }
361
362 pub async fn call_contract(
364 &self,
365 method_name: &str,
366 args: Value,
367 gas: u64,
368 deposit: u128,
369 ) -> Result<ContractCallResult> {
370 match self {
371 Self::Local(signer) => {
372 let outcome = signer.call_contract(method_name, args, gas, deposit).await?;
373 let tx_hash = Some(outcome.transaction_outcome.id.to_string());
374 match &outcome.status {
375 near_primitives::views::FinalExecutionStatus::SuccessValue(bytes) => {
376 Ok(ContractCallResult {
377 value: serde_json::from_slice::<Value>(bytes).ok(),
378 tx_hash,
379 })
380 }
381 near_primitives::views::FinalExecutionStatus::Failure(err) => {
382 anyhow::bail!(
383 "Transaction failed (tx: {}): {:?}",
384 outcome.transaction_outcome.id, err
385 );
386 }
387 _ => Ok(ContractCallResult {
388 value: None,
389 tx_hash,
390 }),
391 }
392 }
393 Self::Wallet {
394 api,
395 wallet_key,
396 contract_id,
397 } => {
398 let resp = api
399 .wallet_call(wallet_key, contract_id, method_name, args, gas, deposit)
400 .await?;
401 if resp.status == "pending_approval" {
402 if let Some(id) = &resp.approval_id {
403 anyhow::bail!(
404 "Transaction requires approval (approval_id: {}). \
405 Approve it in the wallet dashboard.",
406 id
407 );
408 }
409 anyhow::bail!("Transaction requires approval.");
410 }
411 if resp.status != "success" {
412 let detail = resp.result.as_ref().map(|v| v.to_string()).unwrap_or_default();
413 let tx_info = resp.tx_hash.as_deref().unwrap_or("unknown");
414 anyhow::bail!(
415 "Transaction failed (tx: {}, status: {}): {}",
416 tx_info, resp.status, detail
417 );
418 }
419 Ok(ContractCallResult {
420 tx_hash: resp.tx_hash,
421 value: resp.result,
422 })
423 }
424 }
425 }
426}
427
428#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
431pub struct ProjectView {
432 pub uuid: String,
433 pub owner: String,
434 pub name: String,
435 pub project_id: String,
436 pub active_version: String,
437 #[serde(default)]
438 pub created_at: Option<u64>,
439 #[serde(default)]
440 pub storage_deposit: Option<String>,
441}
442
443#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
444pub struct VersionView {
445 pub wasm_hash: String,
446 pub source: Value,
447 pub added_at: u64,
448 pub is_active: bool,
449}
450
451#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
452pub struct UserSecretInfo {
453 pub accessor: Value,
454 pub profile: String,
455 pub created_at: u64,
456 pub updated_at: u64,
457 pub storage_deposit: String,
458 pub access: Value,
459}