use ed25519_dalek::SigningKey; use near_kit::FinalExecutionOutcome; use near_mpc_contract_interface::types::ProtocolContractState; use serde::de::DeserializeOwned; use crate::conversions::ToNearKey; const MAX_GAS: near_kit::Gas = near_kit::Gas::from_tgas(1000); /// RPC client for any NEAR network (sandbox or testnet). /// /// Wraps `near_kit::Near` client signed as the root/funder account. /// Whether the RPC URL points to a local Docker sandbox or NEAR testnet, /// the code path is identical. pub struct NearBlockchain { root_client: near_kit::Near, rpc_url: String, } /// A `near_kit::Near` client bound to a specific account. /// Wrapped in a struct to keep `near_kit` out of the public API surface, /// making it easier to swap the underlying client (e.g. for testnet). pub struct ClientHandle { inner: near_kit::Near, } impl NearBlockchain { pub fn new( rpc_url: &str, chain_id: &str, root_account: &str, root_secret_key: near_kit::SecretKey, ) -> anyhow::Result { let signer = near_kit::InMemorySigner::from_secret_key(root_account, root_secret_key) .map_err(|e| anyhow::anyhow!("failed to create root signer: {e}"))?; let client = near_kit::Near::custom(rpc_url, chain_id) .signer(signer) .build(); Ok(Self { root_client: client, rpc_url: rpc_url.to_string(), }) } pub async fn create_account_with_keys( &self, name: &str, balance_near: u128, keys: &[SigningKey], ) -> anyhow::Result<()> { let mut tx = self .root_client .transaction(name) .create_account() .transfer(near_kit::NearToken::from_near(balance_near)); for key in keys { tx = tx.add_full_access_key(key.to_near_public_key()); } tx.wait_until(near_kit::Final) .await .map_err(|e| anyhow::anyhow!("failed to create account {name}: {e}"))?; Ok(()) } pub async fn create_account_and_deploy( &self, name: &str, balance_near: u128, key: &SigningKey, wasm: &[u8], ) -> anyhow::Result { self.root_client .transaction(name) .create_account() .transfer(near_kit::NearToken::from_near(balance_near)) .add_full_access_key(key.to_near_public_key()) .deploy(wasm.to_vec()) .wait_until(near_kit::Final) .await .map_err(|e| anyhow::anyhow!("failed to create account and deploy to {name}: {e}"))?; let client = self.make_client(name, key)?; Ok(DeployedContract { client, contract_id: name.to_string(), }) } pub fn client_for(&self, account_id: &str, key: &SigningKey) -> anyhow::Result { Ok(ClientHandle { inner: self.make_client(account_id, key)?, }) } pub fn rpc_url(&self) -> &str { &self.rpc_url } fn make_client(&self, account_id: &str, key: &SigningKey) -> anyhow::Result { let sk = key.to_near_secret_key(); let signer = near_kit::InMemorySigner::from_secret_key(account_id, sk) .map_err(|e| anyhow::anyhow!("failed to create signer for {account_id}: {e}"))?; Ok(self.root_client.with_signer(signer)) } } /// Handle to a deployed MPC signer contract. pub struct DeployedContract { client: near_kit::Near, contract_id: String, } impl DeployedContract { pub fn contract_id(&self) -> &str { &self.contract_id } pub async fn call( &self, method: &str, args: serde_json::Value, ) -> anyhow::Result { self.client .call(&self.contract_id, method) .args(args) .gas(MAX_GAS) .send() .await .map_err(|e| anyhow::anyhow!("contract call `{method}` failed: {e}")) } pub async fn call_from( &self, client: &ClientHandle, method: &str, args: serde_json::Value, ) -> anyhow::Result { client .inner .call(&self.contract_id, method) .args(args) .gas(MAX_GAS) .send() .await .map_err(|e| anyhow::anyhow!("contract call `{method}` (external signer) failed: {e}")) } pub async fn call_from_with_deposit( &self, client: &ClientHandle, method: &str, args: serde_json::Value, gas: near_kit::Gas, deposit: near_kit::NearToken, ) -> anyhow::Result { client .inner .call(&self.contract_id, method) .args(args) .gas(gas) .deposit(deposit) .send() .await .map_err(|e| anyhow::anyhow!("contract call `{method}` (with deposit) failed: {e}")) } /// Call a method whose arguments are borsh-serialized (e.g. `propose_update`). pub async fn call_from_borsh_with_deposit( &self, client: &ClientHandle, method: &str, args: A, gas: near_kit::Gas, deposit: near_kit::NearToken, ) -> anyhow::Result { client .inner .call(&self.contract_id, method) .args_borsh(args) .gas(gas) .deposit(deposit) .send() .await .map_err(|e| { anyhow::anyhow!("contract call `{method}` (borsh args, with deposit) failed: {e}") }) } pub async fn view( &self, method: &str, ) -> anyhow::Result { self.client .view::(&self.contract_id, method) .await .map_err(|e| anyhow::anyhow!("contract view `{method}` failed: {e}")) } pub async fn state(&self) -> anyhow::Result { self.view("state").await } /// SHA-256 hash of the contract code currently deployed at this account. pub async fn code_hash(&self) -> anyhow::Result { let view = self .client .account(self.contract_id.as_str()) .await .map_err(|e| anyhow::anyhow!("view_account for `{}` failed: {e}", self.contract_id))?; Ok(view.code_hash) } }