diff --git a/src/rpc_client.rs b/src/rpc_client.rs index 2cfe1bc..13649a1 100644 --- a/src/rpc_client.rs +++ b/src/rpc_client.rs @@ -7,7 +7,7 @@ use crate::error::Result; use crate::request_handler::RequestHandler; use crate::types::types::{RpcRequest, RpcResponse}; use crate::types::{ - Asset, AssetList, AssetProof, GetAsset, GetAssetBatch, GetAssetProof, GetAssetProofBatch, GetAssetsByAuthority, GetAssetsByCreator, GetAssetsByGroup, GetAssetsByOwner, SearchAssets + Asset, AssetList, AssetProof, GetAsset, GetAssetBatch, GetAssetProof, GetAssetProofBatch, GetAssetSignatures, GetAssetsByAuthority, GetAssetsByCreator, GetAssetsByGroup, GetAssetsByOwner, SearchAssets, TransactionSignatureList }; use reqwest::{Client, Method, Url}; @@ -93,4 +93,9 @@ impl RpcClient { pub async fn search_assets(&self, request: SearchAssets) -> Result { self.post_rpc_request("searchAssets", request).await } + + /// Gets transaction signatures for a given asset + pub async fn get_asset_signatures(&self, request: GetAssetSignatures) -> Result { + self.post_rpc_request("getSignaturesForAsset", request).await + } } diff --git a/src/types/types.rs b/src/types/types.rs index 19d19b7..880f078 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -117,63 +117,6 @@ pub struct GetAssetsByCreator { pub cursor: Option, } -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct AssetSorting { - pub sort_by: AssetSortBy, - pub sort_direction: Option, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct ApiResponse { - pub jsonrpc: String, - pub result: T, - pub id: String, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct AssetList { - #[serde(skip_serializing_if = "Option::is_none")] - pub grand_total: Option, - pub total: u32, - pub limit: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub page: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub before: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub after: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cursor: Option, - pub items: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub errors: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -#[serde(default)] -pub struct AssetError { - pub id: String, - pub error: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct GetAssetResponse { - pub interface: Interface, - pub id: String, - pub content: Option, - pub authorities: Option>, - pub compression: Option, - pub grouping: Option>, - pub royalty: Option, - pub ownership: Ownership, - pub creators: Option>, - pub uses: Option, - pub supply: Option, - pub mutable: bool, - pub burnt: bool, -} - #[derive(Serialize, Deserialize, Debug)] pub struct GetAssetBatch { pub ids: Vec, @@ -265,6 +208,76 @@ pub struct SearchAssets { pub collection_nft: Option, } +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct GetAssetSignatures { + pub id: Option, + pub limit: Option, + pub page: Option, + pub before: Option, + pub after: Option, + pub tree: Option, + pub leaf_index: Option, + #[serde(default)] + pub cursor: Option, + #[serde(default)] + pub sort_direction: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AssetSorting { + pub sort_by: AssetSortBy, + pub sort_direction: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct ApiResponse { + pub jsonrpc: String, + pub result: T, + pub id: String, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct AssetList { + #[serde(skip_serializing_if = "Option::is_none")] + pub grand_total: Option, + pub total: u32, + pub limit: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub before: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, + pub items: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub errors: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default)] +pub struct TransactionSignatureList { + pub total: u32, + pub limit: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub before: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, + pub items: Vec<(String, String)>, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default)] +pub struct AssetError { + pub id: String, + pub error: String, +} + #[derive(Serialize, Deserialize, Debug)] pub struct Asset { pub interface: Interface, diff --git a/tests/rpc/test_get_asset_signatures.rs b/tests/rpc/test_get_asset_signatures.rs new file mode 100644 index 0000000..b64ce36 --- /dev/null +++ b/tests/rpc/test_get_asset_signatures.rs @@ -0,0 +1,117 @@ +use std::sync::Arc; + +use helius_sdk::client::Helius; +use helius_sdk::config::Config; +use helius_sdk::error::HeliusError; +use helius_sdk::rpc_client::RpcClient; +use helius_sdk::types::*; + +use mockito::{self, Server}; +use reqwest::Client; +use serde_json::Value; + +#[tokio::test] +async fn test_get_asset_signatures_success() { + let mut server: Server = Server::new_with_opts_async(mockito::ServerOpts::default()).await; + let url: String = server.url(); + + let mock_response: ApiResponse> = ApiResponse { + jsonrpc: "2.0".to_string(), + result: Some(TransactionSignatureList { + total: 1, + limit: 1000, + page: Some( + 1, + ), + before: None, + after: None, + items: vec![ + ( + "3uLpAGykcJmC4cvPoURqAKKktLLFeZBXid6SeXji6Pnd7YAtDxEG3PRXLXpUBdt1N6W18nUGeKv6eNUPb7Po7u3v".to_string(), + "MintToCollectionV1".to_string(), + ), + ], + }), + id: "1".to_string(), + }; + + server + .mock("POST", "/?api-key=fake_api_key") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(serde_json::to_string(&mock_response).unwrap()) + .create(); + + let config: Arc = Arc::new(Config { + api_key: "fake_api_key".to_string(), + cluster: Cluster::Devnet, + endpoints: HeliusEndpoints { + api: url.to_string(), + rpc: url.to_string(), + }, + }); + + let client: Client = Client::new(); + let rpc_client: Arc = Arc::new(RpcClient::new(Arc::new(client.clone()), Arc::clone(&config)).unwrap()); + let helius: Helius = Helius { + config, + client, + rpc_client, + }; + + let request = GetAssetSignatures { + id: Some("8qjkHtHsqww1rac6Uctj4V7Z5yHoTyQj3iJ5vc4Aka8".to_string()), + page: Some(1), + ..Default::default() + }; + + let response: Result = helius.rpc().get_asset(request).await; + assert!(response.is_ok(), "API call failed with error: {:?}", response.err()); + + let signatures: TransactionSignatureList = response.unwrap(); + assert!(signatures.items.is_some(), "No signature returned when one was expected"); + + assert_eq!( + signatures.items[0], "3uLpAGykcJmC4cvPoURqAKKktLLFeZBXid6SeXji6Pnd7YAtDxEG3PRXLXpUBdt1N6W18nUGeKv6eNUPb7Po7u3v", + "signature does not match expected value" + ); +} + +#[tokio::test] +async fn test_get_asset_signatures_failure() { + let mut server: Server = Server::new_with_opts_async(mockito::ServerOpts::default()).await; + let url: String = server.url(); + + server + .mock("POST", "/?api-key=fake_api_key") + .with_status(500) + .with_header("content-type", "application/json") + .with_body(r#"{"error": "Internal Server Error"}"#) + .create(); + + let config: Arc = Arc::new(Config { + api_key: "fake_api_key".to_string(), + cluster: Cluster::Devnet, + endpoints: HeliusEndpoints { + api: url.to_string(), + rpc: url.to_string(), + }, + }); + + let client: Client = Client::new(); + let rpc_client: Arc = Arc::new(RpcClient::new(Arc::new(client.clone()), Arc::clone(&config)).unwrap()); + let helius: Helius = Helius { + config, + client, + rpc_client, + }; + + let request = GetAssetSignatures { + id: Some("8qjkHtHsqww1rac6Uctj4V7Z5yHoTyQj3iJ5vc4Aka8".to_string()), + page: Some(1), + ..Default::default() + }; + + let response: Result = helius.rpc().get_asset_signatures(request).await; + assert!(response.is_err(), "Expected an error but got success"); +}