aeko-rust-sdk 2.0.2

AEKO Chain high-level Rust developer SDK
Documentation
use {
    crate::{
        builders::{
            default_token_721_program_id, default_wallet_permissions_program_id, Aeko721Collection,
            Aeko721Token, WalletPermissionAccount, WalletPermissionAuditLogAccount,
        },
        error::{AekoRustSdkError, AekoRustSdkResult},
    },
    base64::{engine::general_purpose::STANDARD as BASE64, Engine},
    borsh::BorshDeserialize,
    reqwest::Client,
    serde::de::DeserializeOwned,
    serde::Deserialize,
    serde_json::{json, Value},
};

#[derive(Clone, Debug, Deserialize)]
pub struct AccountInfoValue {
    pub data: Value,
    pub executable: bool,
    pub lamports: u64,
    pub owner: String,
}

#[derive(Clone, Debug, Deserialize)]
pub struct ProgramAccount {
    pub pubkey: String,
    pub account: AccountInfoValue,
}

#[derive(Clone, Debug, Deserialize)]
pub struct SignatureStatus {
    pub slot: u64,
    pub confirmations: Option<u64>,
    pub err: Option<Value>,
    #[serde(rename = "confirmationStatus")]
    pub confirmation_status: Option<String>,
}

pub struct AekoDeveloperClient {
    http: Client,
    endpoint: String,
}

impl AekoDeveloperClient {
    pub fn new(url: String) -> Self {
        Self {
            http: Client::new(),
            endpoint: url,
        }
    }

    pub async fn get_latest_blockhash(&self) -> AekoRustSdkResult<String> {
        let value: RpcValueResponse<LatestBlockhashValue> =
            self.rpc("getLatestBlockhash", json!([])).await?;
        Ok(value.value.blockhash)
    }

    pub async fn get_balance(&self, pubkey: &str) -> AekoRustSdkResult<u64> {
        let value: RpcValueResponse<u64> = self.rpc("getBalance", json!([pubkey])).await?;
        Ok(value.value)
    }

    pub async fn get_account_info(
        &self,
        pubkey: &str,
    ) -> AekoRustSdkResult<Option<AccountInfoValue>> {
        let value: RpcValueResponse<Option<AccountInfoValue>> = self
            .rpc("getAccountInfo", json!([pubkey, {"encoding": "base64"}]))
            .await?;
        Ok(value.value)
    }

    pub async fn get_program_accounts(
        &self,
        program_id: &str,
    ) -> AekoRustSdkResult<Vec<ProgramAccount>> {
        self.rpc(
            "getProgramAccounts",
            json!([program_id, {"encoding": "base64"}]),
        )
        .await
    }

    pub async fn get_signature_statuses(
        &self,
        signatures: &[String],
    ) -> AekoRustSdkResult<Vec<Option<SignatureStatus>>> {
        let value: RpcValueResponse<Vec<Option<SignatureStatus>>> = self
            .rpc("getSignatureStatuses", json!([signatures]))
            .await?;
        Ok(value.value)
    }

    pub async fn send_transaction_base64(
        &self,
        signed_transaction_base64: &str,
    ) -> AekoRustSdkResult<String> {
        self.rpc(
            "sendTransaction",
            json!([signed_transaction_base64, {"encoding": "base64"}]),
        )
        .await
    }

    pub async fn get_wallet_permission_account(
        &self,
        pubkey: &str,
    ) -> AekoRustSdkResult<WalletPermissionAccount> {
        let account = self
            .get_account_info(pubkey)
            .await?
            .ok_or(AekoRustSdkError::DecodeAccount {
                label: "wallet permission state",
            })?;
        ensure_owner(
            &account,
            &default_wallet_permissions_program_id(),
            "wallet permission state",
        )?;
        decode_account(&account, "wallet permission state")
    }

    pub async fn get_wallet_permission_audit_log(
        &self,
        pubkey: &str,
    ) -> AekoRustSdkResult<WalletPermissionAuditLogAccount> {
        let account = self
            .get_account_info(pubkey)
            .await?
            .ok_or(AekoRustSdkError::DecodeAccount {
                label: "wallet permission audit log",
            })?;
        ensure_owner(
            &account,
            &default_wallet_permissions_program_id(),
            "wallet permission audit log",
        )?;
        decode_account(&account, "wallet permission audit log")
    }

    pub async fn get_token_721_collection(
        &self,
        pubkey: &str,
    ) -> AekoRustSdkResult<Aeko721Collection> {
        let account = self
            .get_account_info(pubkey)
            .await?
            .ok_or(AekoRustSdkError::DecodeAccount {
                label: "AEKO-721 collection",
            })?;
        ensure_owner(&account, &default_token_721_program_id(), "AEKO-721 collection")?;
        decode_account(&account, "AEKO-721 collection")
    }

    pub async fn get_token_721_token(
        &self,
        pubkey: &str,
    ) -> AekoRustSdkResult<Aeko721Token> {
        let account = self
            .get_account_info(pubkey)
            .await?
            .ok_or(AekoRustSdkError::DecodeAccount {
                label: "AEKO-721 token",
            })?;
        ensure_owner(&account, &default_token_721_program_id(), "AEKO-721 token")?;
        decode_account(&account, "AEKO-721 token")
    }

    async fn rpc<T: DeserializeOwned>(&self, method: &str, params: Value) -> AekoRustSdkResult<T> {
        let response: JsonRpcEnvelope<T> = self
            .http
            .post(&self.endpoint)
            .json(&json!({
                "jsonrpc": "2.0",
                "id": 1,
                "method": method,
                "params": params,
            }))
            .send()
            .await?
            .json()
            .await?;

        match (response.result, response.error) {
            (Some(result), None) => Ok(result),
            (_, Some(error)) => Err(AekoRustSdkError::Rpc(error.message)),
            _ => Err(AekoRustSdkError::Rpc("missing JSON-RPC result".to_string())),
        }
    }
}

fn ensure_owner(
    account: &AccountInfoValue,
    expected_owner: &str,
    label: &'static str,
) -> AekoRustSdkResult<()> {
    if account.owner != expected_owner {
        return Err(AekoRustSdkError::InvalidAccountOwner {
            label,
            expected: expected_owner.to_string(),
            found: account.owner.clone(),
        });
    }
    Ok(())
}

fn decode_account<T: BorshDeserialize>(
    account: &AccountInfoValue,
    label: &'static str,
) -> AekoRustSdkResult<T> {
    let data = match &account.data {
        Value::Array(values) if !values.is_empty() => values[0]
            .as_str()
            .ok_or(AekoRustSdkError::DecodeAccount { label })?,
        Value::String(value) => value.as_str(),
        _ => return Err(AekoRustSdkError::DecodeAccount { label }),
    };
    let bytes = BASE64
        .decode(data)
        .map_err(|_| AekoRustSdkError::DecodeAccount { label })?;
    let mut slice = bytes.as_slice();
    T::deserialize(&mut slice).map_err(|_| AekoRustSdkError::DecodeAccount { label })
}

#[derive(Debug, Deserialize)]
struct JsonRpcEnvelope<T> {
    result: Option<T>,
    error: Option<JsonRpcError>,
}

#[derive(Debug, Deserialize)]
struct JsonRpcError {
    message: String,
}

#[derive(Debug, Deserialize)]
struct RpcValueResponse<T> {
    value: T,
}

#[derive(Debug, Deserialize)]
struct LatestBlockhashValue {
    blockhash: String,
}