This commit is contained in:
2026-07-23 16:37:12 +02:00
parent 99c345f2f2
commit 0da75c1311
2159 changed files with 230833 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
# file: kb_rpc/Cargo.toml
# version: 5
[package]
name = "kb_rpc"
version.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
base64.workspace = true
bs58.workspace = true
kb_config = { path = "../kb_config" }
kb_core = { path = "../kb_core" }
kb_execution_api = { path = "../kb_execution_api" }
kb_model = { path = "../kb_model" }
futures-util.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tokio-tungstenite.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,209 @@
<!-- file: kb_rpc/README.md -->
<!-- version: 21 -->
# kb_rpc
Ce crate contient les clients et adaptateurs Solana JSON-RPC HTTP/WebSocket du workspace. Il reste indépendant des stores concrets, des décodeurs, des matérialiseurs, du wallet et de Tauri.
## API publique principale
| Famille dexports | Usage |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `RpcEndpoint`, `SolanaRpcClient` | Configuration dun endpoint et abstraction minimale dun client RPC. |
| `HttpClient`, `HttpEndpointPool` | Appels JSON-RPC HTTP et routage déterministe par rôle/request kind. |
| `WsClient`, `WsEndpointPool` | Configuration WebSocket et sélection dun endpoint compatible par rôle. |
| `StandardWsRequest`, `StandardWsNotification`, `WsSession` | Neuf contrats PubSub typés et runtime persistant multiplexé avec unsubscribe, reconnexion et réabonnement bornés. |
| `JsonRpcRequest`, `JsonRpcResponse`, `parse_json_rpc_text(...)` | Contrats et parsing JSON-RPC 2.0 communs aux transports. |
| `GetTransactionConfig`, `GetTransactionAdapter`, `CanonicalTransactionAdapter` | Acquisition et adaptation dune transaction vers le modèle canonique. |
| `GetSignaturesForAddressConfig`, `AddressSignatureInfo` | Pagination bornée des signatures pour les backfills. |
| configurations/résultats `execution_rpc` | Paramètres et réponses typés pour cluster, blockhash, frais, simulation, comptes, airdrop, envoi et confirmation. |
| fonctions `adapt_*` | Conversion explicite dune valeur JSON-RPC en contrat typé validé. |
| `validate_solana_hash_text`, `validate_solana_pubkey_text`, `validate_transaction_signature_text` | Validation base58 et longueur des identifiants Solana. |
| `request_kind_from_method(...)` | Normalisation stable du nom dune méthode pour le routage dendpoint. |
| `StandardHttpRequest`, requêtes `Get*Request`, configurations `Rpc*Config` | Contrats configurables propres à `kb_rpc` pour toutes les méthodes HTTP standard. |
Les méthodes réseau retournent `kb_core::Result`. Les fonctions `adapt_*` sont utiles aux tests, aux transports alternatifs et à lanalyse de réponses déjà acquises ; elles neffectuent aucun I/O.
### Exemple minimal
```rust
let config = kb_rpc::GetLatestBlockhashConfig::confirmed();
let result = client.get_latest_blockhash(&config).await?;
let blockhash = result.blockhash;
```
Lapplication choisit un `HttpClient` direct ou un `HttpEndpointPool`. Le pool vérifie les rôles et request kinds avant dappeler le client ; il ne modifie pas la sémantique du résultat.
## Transports et routage
- client HTTP JSON-RPC standard basé sur `reqwest` ;
- pool HTTP sélectionné par `role` et `request_kind` ;
- client WebSocket standard basé sur `tokio-tungstenite` ;
- pool WebSocket par rôle ;
- contrats JSON-RPC 2.0 communs ;
- tracing canonique `kb_rpc` sans journalisation des secrets intégrés aux URL.
## Acquisition canonique
### `getTransaction`
`GetTransactionConfig`, `GetTransactionAdapter` et `CanonicalTransactionAdapter` transforment une réponse JSON-RPC standard en `kb_model::CanonicalTransaction` indépendante du fournisseur.
### `getSignaturesForAddress`
`GetSignaturesForAddressConfig` construit les pages bornées `before`/`until` et `AddressSignatureInfo` conserve les métadonnées nécessaires au backfill.
## RPC dexécution — `0.4.2-pre.004` à `pre.013`
Les méthodes suivantes possèdent maintenant des configurations, résultats et adaptateurs typés :
| Méthode | Entrée principale | Résultat |
|------------------------------------------------------|---------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| `HttpClient::get_genesis_hash` | aucune | `GenesisHashResult` avec hash et classification Devnet/Testnet/Mainnet éventuelle. |
| `HttpClient::get_latest_blockhash` | `GetLatestBlockhashConfig` | `LatestBlockhashResult` avec contexte, blockhash et dernière block height valide. |
| `HttpClient::get_fee_for_message` | message legacy/v0 sérialisé en base64, `GetFeeForMessageConfig` | `FeeForMessageResult`, dont la valeur peut être absente si le blockhash nest plus valide. |
| `HttpClient::simulate_transaction` | transaction sérialisée en base64, `SimulateTransactionConfig` | `SimulateTransactionResult` avec erreur runtime, logs, unités, frais, blockhash de remplacement et diagnostics optionnels. |
| `HttpClient::get_account_info` | public key, `GetAccountInfoConfig` | `AccountInfoResult` avec contexte, métadonnées, `space` et données complètes optionnelles bornées. |
| `HttpClient::get_minimum_balance_for_rent_exemption` | longueur de données, `GetMinimumBalanceForRentExemptionConfig` | minimum rent-exempt en lamports pour cette taille. |
| `HttpClient::get_balance` | public key, `GetBalanceConfig` | `BalanceResult` avec contexte et lamports. |
| `HttpClient::request_airdrop` | public key, lamports, `RequestAirdropConfig` | `AirdropResult` avec signature du faucet. |
| `HttpClient::send_transaction` | transaction signée base64, signature primaire attendue, `SendTransactionConfig` | `SendTransactionResult` uniquement si la signature RPC correspond à la transaction locale. |
| `HttpClient::get_signature_statuses` | une à 256 signatures, `GetSignatureStatusesConfig` | `SignatureStatusesResult` positionnel. |
| `HttpClient::get_block_height` | `GetBlockHeightConfig` | `BlockHeightResult`. |
Chaque méthode existe également sur `HttpEndpointPool` avec le suffixe `_for_role` afin de conserver le routage configuré.
`SimulateTransactionConfig::unsigned_with_replacement()` prépare une simulation sans vérification de signature avec remplacement du recent blockhash. Elle permet de simuler avant la phase de signature, conformément au pipeline dexécution du projet.
`SimulateTransactionResult::to_execution_result(...)` convertit le résultat RPC vers `kb_execution_api::ExecutionSimulationResult`. Le contexte de cluster, de blockhash récent ou de nonce durable reste fourni explicitement par lorchestrateur ; `kb_rpc` ne linvente pas.
Les adaptateurs appliquent :
- validation base58 des genesis hashes et blockhashes ;
- validation base64 non vide et bornée des messages/transactions ;
- limite locale du nombre de snapshots de comptes demandés ;
- conservation provider-neutral des erreurs, return data, inner instructions et comptes simulés ;
- lecture metadata-only de lexistence, du solde, de lowner, de `space` et du statut executable avec `dataSlice.length = 0` ;
- lecture optionnelle des données complètes en base64 avec limite locale, contrôle de lencodage et égalité stricte entre `space` et longueur décodée ;
- conservation explicite du minimum rent-exempt et de la longueur de données demandée ;
- distinction entre erreur de transport/JSON-RPC et erreur runtime de la transaction simulée.
### Modes `getAccountInfo`
`GetAccountInfoConfig::confirmed()` conserve le mode léger historique. La requête utilise `dataSlice = { offset: 0, length: 0 }` et ladaptateur exige un payload décodé vide tout en conservant le champ `space` complet.
`GetAccountInfoConfig::confirmed_with_data(max_data_bytes)` demande les données complètes en omettant `dataSlice`. La limite doit être comprise entre 1 et 65 536 octets. Ladaptateur refuse avant utilisation :
- un `space` supérieur à la limite demandée ;
- un tuple `data` mal formé ;
- un encodage différent de `base64` ;
- un payload encodé ou décodé trop grand ;
- une longueur décodée différente de `space`.
Ce mode est utilisé pour charger exactement les 80 octets dun compte durable nonce avant validation dans `kb_execution_solana`. Le RPC ne décode pas lui-même la sémantique du compte.
## Classification du cluster
Les genesis hashes officiels Devnet, Testnet et Mainnet sont reconnus. Le hash Mainnet na pas changé ; `mainnet-beta` reste un alias historique encore utilisé par les endpoints RPC et la CLI. Le contrat public utilise `ExecutionCluster::Mainnet`, sérialisé `mainnet`, tout en acceptant `mainnet_beta` à la désérialisation pour compatibilité. Un hash différent reste non classifié : il peut correspondre à un validateur local ou à un cluster privé et doit être rapproché du contexte attendu par lorchestrateur.
Pour le chemin de signature, `SimulateTransactionConfig::unsigned_exact()` conserve le blockhash du message. `unsigned_with_replacement()` reste disponible pour le diagnostic, mais son résultat est marqué par le contrat commun et ne peut pas autoriser la signature ou lenvoi du message original.
## Envoi et confirmation bornée
`SendTransactionConfig` interdit `skipPreflight = true` dans le chemin dexécution. Il peut être construit depuis `ExecutionConfig`, transmet `maxRetries` et `minContextSlot`, puis vérifie que la signature retournée par le nœud est exactement la signature primaire calculée localement.
`ConfirmTransactionConfig` borne lintervalle et le nombre de polls. `HttpEndpointPool::confirm_transaction_for_roles` interroge `getSignatureStatuses`, contrôle lerreur runtime, compare le commitment demandé et utilise `getBlockHeight` lorsquune dernière block height valide est connue. Le résultat commun contient le statut, le slot éventuel, le nombre de tentatives, la dernière block height observée et le diagnostic éventuel.
## Frontières
`kb_rpc` ne construit pas le message, ne résout aucun signer et ne signe rien. Il accepte uniquement un payload déjà signé et conserve lenvoi séparé de la confirmation :
```text
PreparedExecutionPlan
-> kb_execution_solana build/simulation/signature
-> kb_rpc sendTransaction
-> kb_rpc confirmation bornée
-> orchestration post-exécution
```
## Validation
Les tests offline vérifient les paramètres JSON-RPC, les réponses valides/nulles, les erreurs runtime, les hashes invalides, les payloads base64 invalides, les limites des données de compte, les divergences `space`/longueur, la classification de cluster et la conversion vers le contrat dexécution commun.
## Informations dépoque pour lexécution stateful
`GetEpochInfoConfig`, `EpochInfoResult`, `HttpClient::get_epoch_info` et `HttpEndpointPool::get_epoch_info_for_role` encapsulent `getEpochInfo` avec commitment et `minContextSlot` optionnel. Cette primitive est utilisée pour les fenêtres de slot ALT/Slashing et les délais de fermeture. Elle reste indépendante de toute logique métier dexécution.
## Inventaire HTTP standard configurable — `0.4.2-pre.024`
`STANDARD_HTTP_METHODS` enregistre exactement les 52 méthodes HTTP de la référence Solana. Elles possèdent maintenant toutes un contrat typé : les 14 adaptateurs spécialisés existants restent inchangés et les 38 méthodes auparavant limitées au JSON brut utilisent des requêtes `Get*Request` propres à `kb_rpc`.
La couche configurable ne réexporte aucun DTO dAgave. Les options sont des champs `Option` indépendants : une valeur absente nest pas sérialisée, tandis que `Some(false)`, `Some(0)` ou `Some([])` reste distinguable lorsquil sagit dune valeur valide du contrat RPC. Les types officiels peuvent servir doracle dans les tests, mais ne constituent pas lAPI publique du crate.
```rust
let request = kb_rpc::GetProgramAccountsRequest {
program_id,
config: Some(kb_rpc::RpcProgramAccountsConfig {
filters: Some(vec![kb_rpc::RpcProgramAccountFilter::DataSize(165)]),
encoding: Some(kb_rpc::RpcAccountEncoding::Base64Zstd),
data_slice: None,
commitment: Some(kb_rpc::RpcCommitmentLevel::Confirmed),
min_context_slot: Some(anchor_slot),
with_context: Some(true),
sort_results: Some(true),
}),
};
let response = client.execute_standard_request(&request).await?;
```
Le même contrat est disponible via `HttpEndpointPool::execute_standard_request_for_role`. La requête fournit son nom de méthode, construit le tableau positionnel exact et définit son type de réponse associé. Le transport refuse un type absent du registre ou déclaré avec un contrat autre que `typed_adapter`.
Les validations locales couvrent notamment :
- 100 comptes maximum pour `getMultipleAccounts` ;
- 128 comptes verrouillés maximum pour `getRecentPrioritizationFees` ;
- 500 000 slots maximum pour les plages et limites de blocs ;
- 720 entrées maximum pour `getRecentPerformanceSamples` ;
- 128 octets décodés maximum pour un filtre memcmp ;
- refus de `processed` pour les méthodes historiques qui ne lacceptent pas ;
- incompatibilité `jsonParsed` / `dataSlice` ;
- validation base58 de toutes les clés et blockhashes structurants.
| Surface | Inventaire | Contrat actuel |
|--------------------|-----------------------:|-----------------------------------------------------------------------------------|
| HTTP | 52 méthodes | 52 adaptateurs typés, 0 contrat standard limité au JSON brut |
| WebSocket | 9 paires / 18 méthodes | 9 requêtes typées, 9 notifications typées et runtime persistant réutilisable |
| WebSocket instable | 3 paires | `block`, `slotsUpdates` et `vote`, refusées sans capacité explicite de lendpoint |
`execute_standard_method_raw` reste disponible pour les outils bas niveau qui manipulent explicitement un `StandardHttpMethodSpec`, mais nest plus nécessaire pour couvrir une méthode HTTP standard.
## PubSub WebSocket standard — `0.4.2-pre.025`
`StandardWsRequest` représente exactement les neuf familles officielles. Chaque variante construit son tableau de paramètres sans valeur implicite imposée par la bibliothèque : encodage, `dataSlice`, filtres, commitment, détails de bloc, rewards, version maximale et notification anticipée de signature restent sélectionnables indépendamment.
`adapt_standard_ws_notification` transforme les neuf méthodes de notification en `StandardWsNotification`. Les réponses contextuelles, les comptes directs ou contextualisés de `programNotification`, le marqueur `receivedSignature`, les mises à jour de slots taguées et les votes gossip sont conservés dans des types propres à `kb_rpc`.
```rust
let client = pool.select_client_for_role_and_method("slot_notifications", "slotSubscribe")?;
let session = kb_rpc::WsSession::connect(
client,
kb_rpc::StandardWsCapabilities::default(),
kb_rpc::WsReconnectPolicy::bounded(3, 250, 2_000)?,
)
.await?;
let mut events = session.subscribe_events();
let acknowledgement = session
.subscribe(kb_rpc::StandardWsRequest::Slot(kb_rpc::SlotSubscribeRequest))
.await?;
let remote_id = acknowledgement.subscription.remote_subscription_id;
```
La session possède un identifiant local stable par souscription et suit séparément lidentifiant distant attribué par le nœud. Après reconnexion, elle réémet les requêtes actives, remappe les IDs distants et publie `SubscriptionRemapped`. Lunsubscribe explicite attend une réponse booléenne vraie ; les requêtes et désabonnements ont leurs timeouts propres. Une notification terminale `signatureNotification` retire automatiquement la souscription one-shot, tandis que `receivedSignature` la conserve jusquau statut terminal. La fermeture envoie des désabonnements best-effort avant la frame Close.
Les méthodes instables `blockSubscribe`, `slotsUpdatesSubscribe` et `voteSubscribe` ne sont jamais déduites dun simple nom de méthode demandé : `StandardWsCapabilities::from_endpoint` exige quun rôle activé de lendpoint annonce explicitement le request kind correspondant, ou un wildcard volontaire.
La première souscription réelle sert de probe du nœud. Lorsquune réponse JSON-RPC indique que la méthode est absente, non activée, indisponible ou non supportée, `WsSession` désactive uniquement la capacité concernée, met à jour son snapshot et refuse localement les appels suivants. Une erreur de paramètres, dauthentification ou de rate limit ne désactive pas la capacité.
La validation finale `0.4.2` compte 113 tests `kb_rpc`. Les logs Tauri réels confirment des souscriptions et désabonnements `slot`, `root` et `program`, ainsi que le garde-fou interdisant de changer dendpoint sans fermer la session active. Létat détaillé est documenté dans `docs/SOLANA_NATIVE_RPC_CLOSURE_AUDIT.md`.

View File

@@ -0,0 +1,22 @@
// file: kb_rpc/src/client.rs
// version: 2
//! RPC client scaffold for Solana ingestion.
/// RPC endpoint configuration.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RpcEndpoint {
/// HTTP RPC URL.
pub http_url: std::string::String,
/// Optional WebSocket RPC URL.
pub ws_url: std::option::Option<std::string::String>,
}
/// Minimal Solana RPC client abstraction.
pub trait SolanaRpcClient {
/// Fetches a raw transaction payload by signature.
fn get_transaction_raw_json(
&self,
signature: &kb_model::Signature,
) -> kb_core::Result<std::option::Option<std::string::String>>;
}

View File

@@ -0,0 +1,41 @@
// file: kb_rpc/src/constants.rs
// version: 6
//! Local constants for the `kb_rpc` crate.
/// Canonical tracing target for this crate.
pub(crate) const TRACING_TARGET: &str = "kb_rpc";
/// Official Devnet genesis hash.
pub(crate) const DEVNET_GENESIS_HASH: &str = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG";
/// Official Testnet genesis hash.
pub(crate) const TESTNET_GENESIS_HASH: &str = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY";
/// Official Mainnet genesis hash; RPC and CLI endpoints retain the legacy `mainnet-beta` alias.
pub(crate) const MAINNET_GENESIS_HASH: &str = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
/// Local defensive maximum for one base64 message or transaction request value.
pub(crate) const MAX_EXECUTION_RPC_BASE64_LENGTH: usize = 65_536;
/// Local defensive maximum for decoded account data returned by one execution RPC request.
pub(crate) const MAX_EXECUTION_ACCOUNT_DATA_BYTES: usize = 65_536;
/// Local defensive maximum for account snapshots requested from one simulation.
pub(crate) const MAX_SIMULATION_ACCOUNT_COUNT: usize = 128;
/// Maximum signatures accepted by one `getSignatureStatuses` request.
pub(crate) const MAX_SIGNATURE_STATUS_COUNT: usize = 256;
/// Maximum confirmation polling attempts accepted by the bounded helper.
pub(crate) const MAX_CONFIRMATION_ATTEMPTS: u32 = 10_000;
/// Maximum delay between confirmation polls.
pub(crate) const MAX_CONFIRMATION_POLL_INTERVAL_MS: u64 = 60_000;
/// Maximum number of public keys accepted by `getMultipleAccounts`.
pub(crate) const MAX_MULTIPLE_ACCOUNT_COUNT: usize = 100;
/// Maximum writable-account set accepted by `getRecentPrioritizationFees`.
pub(crate) const MAX_PRIORITIZATION_FEE_ACCOUNT_COUNT: usize = 128;
/// Maximum sample count accepted by `getRecentPerformanceSamples`.
pub(crate) const MAX_PERFORMANCE_SAMPLE_COUNT: usize = 720;
/// Maximum consecutive slot leaders accepted by `getSlotLeaders`.
pub(crate) const MAX_SLOT_LEADER_COUNT: u64 = 5_000;
/// Maximum slot span or result limit accepted by block-range methods.
pub(crate) const MAX_BLOCK_RANGE: u64 = 500_000;
/// Maximum decoded memcmp payload accepted by the standard RPC contract.
pub(crate) const MAX_MEMCMP_DECODED_BYTES: usize = 128;
/// Maximum base58 text length for a 128-byte memcmp payload.
pub(crate) const MAX_MEMCMP_BASE58_LENGTH: usize = 175;
/// Maximum base64 text length for a 128-byte memcmp payload.
pub(crate) const MAX_MEMCMP_BASE64_LENGTH: usize = 172;

View File

@@ -0,0 +1,190 @@
// file: kb_rpc/src/endpoint_role.rs
// version: 2
//! Endpoint role helpers shared by HTTP and WebSocket pools.
/// Snapshot of one endpoint role and its local limits.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EndpointRoleSnapshot {
/// Role code used by endpoint pools.
pub role: std::string::String,
/// Enables this role on the endpoint.
pub enabled: bool,
/// Request or subscription kinds handled by this role.
pub request_kinds: std::vec::Vec<std::string::String>,
/// Role priority where lower values are preferred.
pub priority: u32,
/// Requests per second allowed for this role on this URL.
pub requests_per_second: u32,
/// Burst capacity allowed for this role on this URL.
pub burst_capacity: u32,
/// Maximum concurrent requests allowed for this role on this URL.
pub max_concurrent_requests: u32,
/// Maximum subscriptions allowed for this role on this URL.
pub max_subscriptions: u32,
/// Pause after a rate limit response in milliseconds.
pub pause_after_rate_limit_ms: u64,
}
impl crate::EndpointRoleSnapshot {
/// Builds a serializable snapshot from configuration.
pub fn from_config(config: &kb_config::EndpointRoleConfig) -> Self {
return Self {
role: config.role.clone(),
enabled: config.enabled,
request_kinds: config.request_kinds.clone(),
priority: config.priority,
requests_per_second: config.requests_per_second,
burst_capacity: config.burst_capacity,
max_concurrent_requests: config.max_concurrent_requests,
max_subscriptions: config.max_subscriptions,
pause_after_rate_limit_ms: config.pause_after_rate_limit_ms,
};
}
}
/// Converts a JSON-RPC method name into a stable snake_case request kind.
pub fn request_kind_from_method(method: &str) -> std::string::String {
let trimmed = method.trim();
if trimmed == "logsSubscribe" {
return "logs_subscribe_mentions".to_string();
}
return crate::endpoint_role::camel_or_pascal_to_snake(trimmed);
}
/// Returns true when one endpoint role can handle the requested role and kind.
pub(crate) fn role_matches(
role_config: &kb_config::EndpointRoleConfig,
required_role: &str,
request_kind: &str,
) -> bool {
if !role_config.enabled {
return false;
}
if role_config.role != required_role {
return false;
}
for configured_kind in &role_config.request_kinds {
if configured_kind == request_kind {
return true;
}
if configured_kind == "*" {
return true;
}
}
return false;
}
fn camel_or_pascal_to_snake(value: &str) -> std::string::String {
let mut output = std::string::String::new();
let mut previous_was_lower_or_digit = false;
for character in value.chars() {
if character == '-' || character == ' ' || character == '.' {
if !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
previous_was_lower_or_digit = false;
continue;
}
if character.is_ascii_uppercase() {
if previous_was_lower_or_digit && !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
output.push(character.to_ascii_lowercase());
previous_was_lower_or_digit = false;
continue;
}
if character == '_' {
if !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
previous_was_lower_or_digit = false;
continue;
}
output.push(character);
previous_was_lower_or_digit = character.is_ascii_lowercase() || character.is_ascii_digit();
}
return output.trim_matches('_').to_string();
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
enabled: bool,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
#[test]
fn request_kind_converts_camel_case_methods() {
assert_eq!(crate::request_kind_from_method("getLatestBlockhash"), "get_latest_blockhash");
assert_eq!(crate::request_kind_from_method("sendTransaction"), "send_transaction");
}
#[test]
fn request_kind_keeps_existing_snake_case_methods() {
assert_eq!(crate::request_kind_from_method("get_latest_blockhash"), "get_latest_blockhash");
assert_eq!(
crate::request_kind_from_method("logs_subscribe_mentions"),
"logs_subscribe_mentions"
);
}
#[test]
fn request_kind_normalizes_separators() {
assert_eq!(crate::request_kind_from_method("get-Block"), "get_block");
assert_eq!(crate::request_kind_from_method("program.Subscribe"), "program_subscribe");
}
#[test]
fn request_kind_maps_logs_subscribe_to_mentions_role_kind() {
assert_eq!(crate::request_kind_from_method("logsSubscribe"), "logs_subscribe_mentions");
}
#[test]
fn role_matches_exact_request_kind() {
let role = role_config("http_queries", true, std::vec!["get_version".to_string()]);
assert!(crate::endpoint_role::role_matches(&role, "http_queries", "get_version"));
}
#[test]
fn role_matches_wildcard_request_kind() {
let role = role_config("http_queries", true, std::vec!["*".to_string()]);
assert!(crate::endpoint_role::role_matches(&role, "http_queries", "get_block"));
}
#[test]
fn role_does_not_match_when_disabled() {
let role = role_config("http_queries", false, std::vec!["*".to_string()]);
assert!(!crate::endpoint_role::role_matches(&role, "http_queries", "get_version"));
}
#[test]
fn role_does_not_match_different_role() {
let role = role_config("http_queries", true, std::vec!["*".to_string()]);
assert!(!crate::endpoint_role::role_matches(&role, "http_heavy", "get_block"));
}
#[test]
fn endpoint_role_snapshot_preserves_limits() {
let role = role_config("http_queries", true, std::vec!["get_version".to_string()]);
let snapshot = crate::EndpointRoleSnapshot::from_config(&role);
assert_eq!(snapshot.role, "http_queries");
assert_eq!(snapshot.request_kinds, std::vec!["get_version".to_string()]);
assert_eq!(snapshot.requests_per_second, 10);
assert_eq!(snapshot.max_subscriptions, 16);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,236 @@
// file: kb_rpc/src/get_signatures_for_address.rs
// version: 1
//! Standard Solana `getSignaturesForAddress` request and response contracts.
/// One transaction signature summary returned by `getSignaturesForAddress`.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AddressSignatureInfo {
/// Transaction signature.
pub signature: std::string::String,
/// Slot containing the transaction.
pub slot: u64,
/// Optional transaction error returned by the RPC node.
pub err: std::option::Option<serde_json::Value>,
/// Optional memo associated with the transaction.
pub memo: std::option::Option<std::string::String>,
/// Optional block time as a Unix timestamp.
pub block_time: std::option::Option<i64>,
/// Optional confirmation status reported by the RPC node.
pub confirmation_status: std::option::Option<std::string::String>,
}
/// Configuration for one standard Solana `getSignaturesForAddress` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetSignaturesForAddressConfig {
/// Requested commitment, restricted to `confirmed` or `finalized`.
pub commitment: std::string::String,
/// Maximum number of signatures requested from the node.
pub limit: u16,
/// Optional exclusive cursor toward older signatures.
pub before: std::option::Option<std::string::String>,
/// Optional exclusive lower boundary toward newer signatures.
pub until: std::option::Option<std::string::String>,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
}
impl GetSignaturesForAddressConfig {
/// Creates and validates one signature history request configuration.
pub fn new(
commitment: impl std::convert::Into<std::string::String>,
limit: u16,
before: std::option::Option<std::string::String>,
until: std::option::Option<std::string::String>,
min_context_slot: std::option::Option<u64>,
) -> kb_core::Result<Self> {
let commitment_value = commitment.into();
if commitment_value != "confirmed" && commitment_value != "finalized" {
return std::result::Result::Err(kb_core::Error::config(
"getSignaturesForAddress commitment must be 'confirmed' or 'finalized'",
));
}
if limit == 0 || limit > 1000 {
return std::result::Result::Err(kb_core::Error::config(
"getSignaturesForAddress limit must be between 1 and 1000",
));
}
let before_result = crate::get_signatures_for_address::validate_optional_signature(
before.as_ref(),
"getSignaturesForAddress before signature",
);
if let std::result::Result::Err(error) = before_result {
return std::result::Result::Err(error);
}
let until_result = crate::get_signatures_for_address::validate_optional_signature(
until.as_ref(),
"getSignaturesForAddress until signature",
);
if let std::result::Result::Err(error) = until_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(Self {
commitment: commitment_value,
limit,
before,
until,
min_context_slot,
});
}
fn request_params(&self, address: &str) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let address_result =
crate::validate_solana_pubkey_text(address, "getSignaturesForAddress address");
if let std::result::Result::Err(error) = address_result {
return std::result::Result::Err(error);
}
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert("commitment".to_string(), serde_json::Value::String(self.commitment.clone()));
config.insert(
"limit".to_string(),
serde_json::Value::Number(serde_json::Number::from(self.limit)),
);
if let std::option::Option::Some(before) = &self.before {
config.insert("before".to_string(), serde_json::Value::String(before.clone()));
}
if let std::option::Option::Some(until) = &self.until {
config.insert("until".to_string(), serde_json::Value::String(until.clone()));
}
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
return std::result::Result::Ok(std::vec![
serde_json::Value::String(address.to_string()),
serde_json::Value::Object(config),
]);
}
}
impl crate::HttpClient {
/// Fetches one page of signatures associated with an account or program address.
pub async fn get_signatures_for_address(
&self,
address: &str,
config: &crate::GetSignaturesForAddressConfig,
) -> kb_core::Result<std::vec::Vec<crate::AddressSignatureInfo>> {
let params_result = config.request_params(address);
let params = match params_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self
.execute_json_rpc_result_raw("getSignaturesForAddress".to_string(), params)
.await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_signatures_for_address_result(&value);
}
}
/// Adapts a raw standard Solana `getSignaturesForAddress` result value.
pub fn adapt_get_signatures_for_address_result(
source: &serde_json::Value,
) -> kb_core::Result<std::vec::Vec<crate::AddressSignatureInfo>> {
let parse_result =
serde_json::from_value::<std::vec::Vec<crate::AddressSignatureInfo>>(source.clone());
let parsed = match parse_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::json(format!(
"cannot parse getSignaturesForAddress result: {error}"
)));
},
};
for item in &parsed {
let validation_result = crate::validate_transaction_signature_text(
item.signature.as_str(),
"getSignaturesForAddress result signature",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(parsed);
}
fn validate_optional_signature(
value: std::option::Option<&std::string::String>,
field_name: &str,
) -> kb_core::Result<()> {
if let std::option::Option::Some(signature) = value {
return crate::validate_transaction_signature_text(signature.as_str(), field_name);
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
fn signature(byte: u8) -> std::string::String {
return bs58::encode([byte; 64]).into_string();
}
fn pubkey(byte: u8) -> std::string::String {
return bs58::encode([byte; 32]).into_string();
}
#[test]
fn config_supports_before_and_until_for_bounded_pagination() {
let config_result = crate::GetSignaturesForAddressConfig::new(
"confirmed",
1000,
std::option::Option::Some(signature(1)),
std::option::Option::Some(signature(2)),
std::option::Option::Some(44),
);
let config = match config_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected config error: {error}"),
};
let params_result = config.request_params(pubkey(3).as_str());
let params = match params_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected params error: {error}"),
};
assert_eq!(params.len(), 2);
assert_eq!(params[1]["limit"], serde_json::json!(1000));
assert_eq!(params[1]["minContextSlot"], serde_json::json!(44));
}
#[test]
fn config_rejects_limit_above_rpc_maximum() {
let result = crate::GetSignaturesForAddressConfig::new(
"confirmed",
1001,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
assert!(result.is_err());
}
#[test]
fn result_adapter_preserves_signature_metadata() {
let source = serde_json::json!([{
"signature": signature(4),
"slot": 123,
"err": null,
"memo": "hello",
"blockTime": 456,
"confirmationStatus": "finalized"
}]);
let result = crate::adapt_get_signatures_for_address_result(&source);
let values = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected adapter error: {error}"),
};
assert_eq!(values.len(), 1);
assert_eq!(values[0].slot, 123);
assert_eq!(values[0].memo.as_deref(), std::option::Option::Some("hello"));
}
}

View File

@@ -0,0 +1,736 @@
// file: kb_rpc/src/get_transaction.rs
// version: 7
//! Standard Solana `getTransaction` adapter for the canonical transaction contract.
use base64::Engine; // rust-rules: trait-import
/// Common adapter contract for transport-specific transaction payloads.
pub trait CanonicalTransactionAdapter {
/// Source payload type accepted by the adapter.
type Source;
/// Adapts one source payload into an optional canonical transaction.
fn adapt(
&self,
source: &Self::Source,
) -> kb_core::Result<std::option::Option<kb_model::CanonicalTransaction>>;
}
/// Configuration for a standard Solana `getTransaction` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTransactionConfig {
/// Requested commitment, restricted to `confirmed` or `finalized`.
pub commitment: std::string::String,
/// Maximum supported numbered transaction version.
pub max_supported_transaction_version: u8,
}
impl GetTransactionConfig {
/// Creates and validates a `getTransaction` request configuration.
pub fn new(
commitment: impl std::convert::Into<std::string::String>,
max_supported_transaction_version: u8,
) -> kb_core::Result<Self> {
let commitment_value = commitment.into();
if commitment_value != "confirmed" && commitment_value != "finalized" {
return std::result::Result::Err(kb_core::Error::config(
"getTransaction commitment must be 'confirmed' or 'finalized'",
));
}
return std::result::Result::Ok(Self {
commitment: commitment_value,
max_supported_transaction_version,
});
}
/// Creates the default confirmed configuration supporting transaction version zero.
pub fn confirmed_v0() -> Self {
return Self {
commitment: "confirmed".to_string(),
max_supported_transaction_version: 0,
};
}
fn request_params(&self, signature: &str) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let signature_result =
crate::validate_transaction_signature_text(signature, "getTransaction signature");
if let std::result::Result::Err(error) = signature_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(std::vec![
serde_json::Value::String(signature.to_string()),
serde_json::json!({
"commitment": self.commitment.clone(),
"encoding": "json",
"maxSupportedTransactionVersion": self.max_supported_transaction_version
}),
]);
}
}
/// Source payload and canonical result returned by one `getTransaction` request.
#[derive(Clone, Debug, PartialEq)]
pub struct GetTransactionAcquisition {
/// Source-specific JSON result retained only for immediate metrics and hashing.
pub source_json: serde_json::Value,
/// Canonical source-independent transaction when the RPC result is not null.
pub canonical_transaction: std::option::Option<kb_model::CanonicalTransaction>,
}
/// Adapter from the standard Solana `getTransaction` JSON result to the canonical model.
#[derive(Clone, Copy, Debug, Default)]
pub struct GetTransactionAdapter;
impl crate::CanonicalTransactionAdapter for crate::GetTransactionAdapter {
type Source = serde_json::Value;
fn adapt(
&self,
source: &Self::Source,
) -> kb_core::Result<std::option::Option<kb_model::CanonicalTransaction>> {
return crate::get_transaction::adapt_get_transaction_result(source);
}
}
impl crate::HttpClient {
/// Fetches one source payload and its canonical transaction adaptation.
pub async fn get_transaction_acquisition(
&self,
signature: &str,
config: &crate::GetTransactionConfig,
) -> kb_core::Result<crate::GetTransactionAcquisition> {
let params_result = config.request_params(signature);
let params = match params_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self.execute_json_rpc_result_raw("getTransaction".to_string(), params).await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let adapter = crate::GetTransactionAdapter;
let canonical_result = crate::CanonicalTransactionAdapter::adapt(&adapter, &value);
let canonical_transaction = match canonical_result {
std::result::Result::Ok(canonical) => canonical,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::GetTransactionAcquisition {
source_json: value,
canonical_transaction,
});
}
/// Fetches and adapts one standard Solana transaction into the canonical model.
pub async fn get_transaction_canonical(
&self,
signature: &str,
config: &crate::GetTransactionConfig,
) -> kb_core::Result<std::option::Option<kb_model::CanonicalTransaction>> {
let acquisition_result = self.get_transaction_acquisition(signature, config).await;
let acquisition = match acquisition_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(acquisition.canonical_transaction);
}
}
/// Adapts a raw standard Solana `getTransaction` result value.
pub fn adapt_get_transaction_result(
source: &serde_json::Value,
) -> kb_core::Result<std::option::Option<kb_model::CanonicalTransaction>> {
if source.is_null() {
return std::result::Result::Ok(std::option::Option::None);
}
let parse_result = serde_json::from_value::<RpcGetTransactionResult>(source.clone());
let parsed = match parse_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::json(format!(
"cannot parse getTransaction result: {error}"
)));
},
};
let canonical_result = crate::get_transaction::adapt_parsed_transaction(parsed);
return match canonical_result {
std::result::Result::Ok(transaction) => {
std::result::Result::Ok(std::option::Option::Some(transaction))
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcGetTransactionResult {
slot: u64,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
meta: std::option::Option<RpcTransactionMeta>,
transaction: RpcJsonTransaction,
#[serde(default)]
version: std::option::Option<RpcTransactionVersion>,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(untagged)]
enum RpcTransactionVersion {
Legacy(std::string::String),
Number(u8),
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RpcJsonTransaction {
signatures: std::vec::Vec<std::string::String>,
message: RpcRawMessage,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcRawMessage {
header: RpcMessageHeader,
account_keys: std::vec::Vec<std::string::String>,
recent_blockhash: std::string::String,
instructions: std::vec::Vec<RpcCompiledInstruction>,
#[serde(default)]
address_table_lookups: std::vec::Vec<RpcAddressTableLookup>,
}
#[derive(Clone, Copy, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcMessageHeader {
num_required_signatures: u8,
num_readonly_signed_accounts: u8,
num_readonly_unsigned_accounts: u8,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcCompiledInstruction {
program_id_index: u16,
accounts: std::vec::Vec<u16>,
data: std::string::String,
#[serde(default)]
stack_height: std::option::Option<u32>,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcAddressTableLookup {
account_key: std::string::String,
writable_indexes: std::vec::Vec<u8>,
readonly_indexes: std::vec::Vec<u8>,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcTransactionMeta {
#[serde(default)]
err: std::option::Option<serde_json::Value>,
fee: u64,
pre_balances: std::vec::Vec<u64>,
post_balances: std::vec::Vec<u64>,
#[serde(default)]
inner_instructions: std::option::Option<std::vec::Vec<RpcInnerInstructionGroup>>,
#[serde(default)]
log_messages: std::option::Option<std::vec::Vec<std::string::String>>,
#[serde(default)]
pre_token_balances: std::option::Option<std::vec::Vec<RpcTokenBalance>>,
#[serde(default)]
post_token_balances: std::option::Option<std::vec::Vec<RpcTokenBalance>>,
#[serde(default)]
rewards: std::option::Option<std::vec::Vec<RpcReward>>,
#[serde(default)]
loaded_addresses: std::option::Option<RpcLoadedAddresses>,
#[serde(default)]
return_data: std::option::Option<RpcReturnData>,
#[serde(default)]
compute_units_consumed: std::option::Option<u64>,
#[serde(default)]
cost_units: std::option::Option<u64>,
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RpcInnerInstructionGroup {
index: u16,
instructions: std::vec::Vec<RpcCompiledInstruction>,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcTokenBalance {
account_index: u16,
mint: std::string::String,
#[serde(default)]
owner: std::option::Option<std::string::String>,
#[serde(default)]
program_id: std::option::Option<std::string::String>,
ui_token_amount: RpcUiTokenAmount,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcUiTokenAmount {
amount: std::string::String,
decimals: u8,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcReward {
pubkey: std::string::String,
lamports: i64,
post_balance: u64,
#[serde(default)]
reward_type: std::option::Option<std::string::String>,
#[serde(default)]
commission: std::option::Option<u8>,
}
#[derive(Clone, Debug, Default, serde::Deserialize)]
struct RpcLoadedAddresses {
#[serde(default)]
writable: std::vec::Vec<std::string::String>,
#[serde(default)]
readonly: std::vec::Vec<std::string::String>,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcReturnData {
program_id: std::string::String,
data: std::vec::Vec<std::string::String>,
}
fn adapt_parsed_transaction(
parsed: RpcGetTransactionResult,
) -> kb_core::Result<kb_model::CanonicalTransaction> {
let primary_signature = match parsed.transaction.signatures.first() {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::json(
"getTransaction result contains no signatures",
));
},
};
let version_result = crate::get_transaction::adapt_version(parsed.version);
let version = match version_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let loaded_addresses = match &parsed.meta {
std::option::Option::Some(metadata) => {
crate::get_transaction::adapt_loaded_addresses(metadata.loaded_addresses.clone())
},
std::option::Option::None => kb_model::CanonicalLoadedAddresses::default(),
};
let instructions_result =
crate::get_transaction::adapt_instructions(parsed.transaction.message.instructions);
let instructions = match instructions_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata_result = crate::get_transaction::adapt_metadata(parsed.meta);
let metadata = match metadata_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transaction = kb_model::CanonicalTransaction {
format_version: kb_model::CANONICAL_TRANSACTION_FORMAT_VERSION,
primary_signature,
slot: parsed.slot,
block_time: parsed.block_time,
version,
signatures: parsed.transaction.signatures,
message: kb_model::CanonicalTransactionMessage {
header: kb_model::CanonicalMessageHeader {
num_required_signatures: parsed.transaction.message.header.num_required_signatures,
num_readonly_signed_accounts: parsed
.transaction
.message
.header
.num_readonly_signed_accounts,
num_readonly_unsigned_accounts: parsed
.transaction
.message
.header
.num_readonly_unsigned_accounts,
},
static_account_keys: parsed.transaction.message.account_keys,
recent_blockhash: parsed.transaction.message.recent_blockhash,
instructions,
address_table_lookups: parsed
.transaction
.message
.address_table_lookups
.into_iter()
.map(|lookup| {
return kb_model::CanonicalAddressTableLookup {
account_key: lookup.account_key,
writable_indexes: lookup.writable_indexes,
readonly_indexes: lookup.readonly_indexes,
};
})
.collect(),
loaded_addresses,
},
metadata,
};
let validation_result = transaction.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(transaction);
}
fn adapt_version(
version: std::option::Option<RpcTransactionVersion>,
) -> kb_core::Result<kb_model::CanonicalTransactionVersion> {
return match version {
std::option::Option::Some(RpcTransactionVersion::Legacy(value)) => {
if value != "legacy" {
return std::result::Result::Err(kb_core::Error::json(format!(
"unsupported textual transaction version: {value}"
)));
}
std::result::Result::Ok(kb_model::CanonicalTransactionVersion::Legacy)
},
std::option::Option::Some(RpcTransactionVersion::Number(value)) => {
std::result::Result::Ok(kb_model::CanonicalTransactionVersion::Number(value))
},
std::option::Option::None => {
std::result::Result::Ok(kb_model::CanonicalTransactionVersion::Legacy)
},
};
}
fn adapt_instructions(
instructions: std::vec::Vec<RpcCompiledInstruction>,
) -> kb_core::Result<std::vec::Vec<kb_model::CanonicalCompiledInstruction>> {
let mut adapted = std::vec::Vec::with_capacity(instructions.len());
for instruction in instructions {
let instruction_result = crate::get_transaction::adapt_instruction(instruction);
let canonical = match instruction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
adapted.push(canonical);
}
return std::result::Result::Ok(adapted);
}
fn adapt_instruction(
instruction: RpcCompiledInstruction,
) -> kb_core::Result<kb_model::CanonicalCompiledInstruction> {
let decode_result = bs58::decode(instruction.data.as_str()).into_vec();
let bytes = match decode_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::json(format!(
"getTransaction instruction data is not valid base58: {error}"
)));
},
};
let data_base64 = base64::engine::general_purpose::STANDARD.encode(bytes);
return std::result::Result::Ok(kb_model::CanonicalCompiledInstruction {
program_id_index: instruction.program_id_index,
account_indexes: instruction.accounts,
data_base64,
stack_height: instruction.stack_height,
});
}
fn adapt_loaded_addresses(
loaded_addresses: std::option::Option<RpcLoadedAddresses>,
) -> kb_model::CanonicalLoadedAddresses {
return match loaded_addresses {
std::option::Option::Some(value) => kb_model::CanonicalLoadedAddresses {
writable: value.writable,
readonly: value.readonly,
},
std::option::Option::None => kb_model::CanonicalLoadedAddresses::default(),
};
}
fn adapt_metadata(
metadata: std::option::Option<RpcTransactionMeta>,
) -> kb_core::Result<std::option::Option<kb_model::CanonicalTransactionMetadata>> {
let source = match metadata {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let inner_result = crate::get_transaction::adapt_inner_instructions(
source.inner_instructions.unwrap_or_default(),
);
let inner_instructions = match inner_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let return_result = crate::get_transaction::adapt_return_data(source.return_data);
let return_data = match return_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pre_token_result =
crate::get_transaction::adapt_token_balances(source.pre_token_balances.unwrap_or_default());
let pre_token_balances = match pre_token_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let post_token_result = crate::get_transaction::adapt_token_balances(
source.post_token_balances.unwrap_or_default(),
);
let post_token_balances = match post_token_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let status = if source.err.is_some() {
kb_model::CanonicalTransactionStatus::Failed
} else {
kb_model::CanonicalTransactionStatus::Success
};
return std::result::Result::Ok(std::option::Option::Some(
kb_model::CanonicalTransactionMetadata {
status,
error: source.err,
fee: source.fee,
pre_balances: source.pre_balances,
post_balances: source.post_balances,
inner_instructions,
log_messages: source.log_messages.unwrap_or_default(),
pre_token_balances,
post_token_balances,
rewards: source
.rewards
.unwrap_or_default()
.into_iter()
.map(|reward| {
return kb_model::CanonicalReward {
pubkey: reward.pubkey,
lamports: reward.lamports,
post_balance: reward.post_balance,
reward_type: reward.reward_type,
commission: reward.commission,
};
})
.collect(),
return_data,
compute_units_consumed: source.compute_units_consumed,
cost_units: source.cost_units,
},
));
}
fn adapt_inner_instructions(
groups: std::vec::Vec<RpcInnerInstructionGroup>,
) -> kb_core::Result<std::vec::Vec<kb_model::CanonicalInnerInstructionGroup>> {
let mut adapted = std::vec::Vec::with_capacity(groups.len());
for group in groups {
let instructions_result = crate::get_transaction::adapt_instructions(group.instructions);
let instructions = match instructions_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
adapted.push(kb_model::CanonicalInnerInstructionGroup {
parent_instruction_index: group.index,
instructions,
});
}
return std::result::Result::Ok(adapted);
}
fn adapt_token_balances(
balances: std::vec::Vec<RpcTokenBalance>,
) -> kb_core::Result<std::vec::Vec<kb_model::CanonicalTokenBalance>> {
let mut adapted = std::vec::Vec::with_capacity(balances.len());
for balance in balances {
let balance_result = kb_model::CanonicalTokenBalance::new(
balance.account_index,
balance.mint,
balance.owner,
balance.program_id,
balance.ui_token_amount.amount,
balance.ui_token_amount.decimals,
);
let canonical = match balance_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
adapted.push(canonical);
}
return std::result::Result::Ok(adapted);
}
fn adapt_return_data(
return_data: std::option::Option<RpcReturnData>,
) -> kb_core::Result<std::option::Option<kb_model::CanonicalReturnData>> {
let source = match return_data {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
if source.data.len() != 2 {
return std::result::Result::Err(kb_core::Error::json(
"getTransaction return data must contain payload and encoding",
));
}
if source.data[1] != "base64" {
return std::result::Result::Err(kb_core::Error::json(format!(
"unsupported getTransaction return data encoding: {}",
source.data[1]
)));
}
return std::result::Result::Ok(std::option::Option::Some(kb_model::CanonicalReturnData {
program_id: source.program_id,
data_base64: source.data[0].clone(),
}));
}
#[cfg(test)]
mod tests {
fn fixture(name: &str) -> serde_json::Value {
let text = match name {
"legacy_success" => {
include_str!("../tests/fixtures/get_transaction_legacy_success.json")
},
"legacy_failure" => {
include_str!("../tests/fixtures/get_transaction_legacy_failure.json")
},
"v0_alt" => {
include_str!("../tests/fixtures/get_transaction_v0_alt_cpi_token_2022.json")
},
"optional_absent" => {
include_str!("../tests/fixtures/get_transaction_optional_absent.json")
},
"equivalent_a" => include_str!("../tests/fixtures/get_transaction_equivalent_a.json"),
"equivalent_b" => include_str!("../tests/fixtures/get_transaction_equivalent_b.json"),
_ => panic!("unknown fixture: {name}"),
};
let parse_result = serde_json::from_str(text);
match parse_result {
std::result::Result::Ok(value) => return value,
std::result::Result::Err(error) => panic!("fixture parse failed: {error}"),
}
}
fn adapt_fixture(name: &str) -> kb_model::CanonicalTransaction {
let value = fixture(name);
let result = crate::adapt_get_transaction_result(&value);
match result {
std::result::Result::Ok(std::option::Option::Some(transaction)) => return transaction,
std::result::Result::Ok(std::option::Option::None) => {
panic!("fixture unexpectedly adapted to none");
},
std::result::Result::Err(error) => panic!("fixture adaptation failed: {error}"),
}
}
#[test]
fn null_result_adapts_to_none() {
let result = crate::adapt_get_transaction_result(&serde_json::Value::Null);
assert_eq!(result, std::result::Result::Ok(std::option::Option::None));
}
#[test]
fn legacy_success_preserves_token_balances_and_return_data() {
let transaction = adapt_fixture("legacy_success");
assert_eq!(transaction.version, kb_model::CanonicalTransactionVersion::Legacy);
assert_eq!(transaction.block_time, std::option::Option::Some(1_700_000_001));
let metadata = match transaction.metadata {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("metadata missing"),
};
assert_eq!(metadata.status, kb_model::CanonicalTransactionStatus::Success);
assert_eq!(transaction.message.instructions[0].data_base64, "AQ==");
assert_eq!(metadata.post_token_balances.len(), 1);
assert_eq!(metadata.post_token_balances[0].amount, "12345678901234567890");
assert_eq!(metadata.post_token_balances[0].decimal_amount, "12345678901.234567890");
assert!(metadata.return_data.is_some());
assert_eq!(metadata.inner_instructions.len(), 1);
}
#[test]
fn legacy_failure_preserves_error() {
let transaction = adapt_fixture("legacy_failure");
let metadata = match transaction.metadata {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("metadata missing"),
};
assert_eq!(metadata.status, kb_model::CanonicalTransactionStatus::Failed);
assert!(metadata.error.is_some());
}
#[test]
fn version_zero_preserves_alt_cpi_and_token_2022() {
let transaction = adapt_fixture("v0_alt");
assert_eq!(transaction.version, kb_model::CanonicalTransactionVersion::Number(0));
assert_eq!(transaction.message.address_table_lookups.len(), 1);
assert_eq!(transaction.message.loaded_addresses.writable.len(), 1);
let metadata = match transaction.metadata {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("metadata missing"),
};
assert_eq!(metadata.inner_instructions.len(), 1);
assert_eq!(
metadata.post_token_balances[0].program_id.as_deref(),
std::option::Option::Some("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
);
}
#[test]
fn optional_fields_and_block_time_can_be_absent() {
let transaction = adapt_fixture("optional_absent");
assert_eq!(transaction.block_time, std::option::Option::None);
let metadata = match transaction.metadata {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("metadata missing"),
};
assert!(metadata.log_messages.is_empty());
assert!(metadata.rewards.is_empty());
assert!(metadata.return_data.is_none());
}
#[test]
fn equivalent_provider_shapes_produce_identical_hashes() {
let first = adapt_fixture("equivalent_a");
let second = adapt_fixture("equivalent_b");
let first_hash = match first.canonical_json_hash() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("first hash failed: {error}"),
};
let second_hash = match second.canonical_json_hash() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("second hash failed: {error}"),
};
assert_eq!(first_hash, second_hash);
}
#[test]
fn request_config_rejects_public_key_length_signature() {
let config = crate::GetTransactionConfig::confirmed_v0();
let result = config.request_params("3Bxs4NN8M2Yn4TLb7gR6Xy7n2D1Q8VjQqWcnpX8C6pQw");
assert!(result.is_err());
}
#[test]
fn request_config_rejects_processed_commitment() {
let result = crate::GetTransactionConfig::new("processed", 0);
assert!(result.is_err());
}
#[test]
fn request_config_uses_json_encoding_and_version_zero() {
let config = crate::GetTransactionConfig::confirmed_v0();
let params_result = config.request_params(
"2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T",
);
let params = match params_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("request params failed: {error}"),
};
assert_eq!(params[1]["encoding"], serde_json::Value::String("json".to_string()));
assert_eq!(params[1]["maxSupportedTransactionVersion"], serde_json::Value::from(0));
}
}

View File

@@ -0,0 +1,383 @@
// file: kb_rpc/src/http_client.rs
// version: 10
//! HTTP JSON-RPC client for standard Solana RPC endpoints.
/// Local HTTP method class used for routing diagnostics.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
pub enum HttpMethodClass {
/// Standard RPC reads and generic methods.
GeneralRpc,
/// Transaction submission methods.
SendTransaction,
/// Resource-intensive read methods.
HeavyRead,
}
/// Snapshot of one pooled HTTP endpoint.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpPoolClientSnapshot {
/// Logical endpoint name.
pub endpoint_name: std::string::String,
/// Provider name.
pub provider: std::string::String,
/// Endpoint URL.
pub endpoint_url: std::string::String,
/// Supported roles.
pub roles: std::vec::Vec<crate::EndpointRoleSnapshot>,
/// Status string.
pub status: std::string::String,
}
/// HTTP JSON-RPC client bound to one configured endpoint.
#[derive(Clone, Debug)]
pub struct HttpClient {
endpoint: kb_config::HttpEndpointConfig,
client: reqwest::Client,
next_request_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
impl crate::HttpClient {
/// Creates a new HTTP client bound to one endpoint.
pub fn new(endpoint: kb_config::HttpEndpointConfig) -> kb_core::Result<Self> {
if !endpoint.enabled {
tracing::error!(target: crate::TRACING_TARGET, action = "create_http_client", endpoint_name = %endpoint.name, provider = %endpoint.provider, error_code = "http_endpoint_disabled", "cannot create HTTP client for disabled endpoint");
return std::result::Result::Err(kb_core::Error::config(format!(
"http endpoint '{}' is disabled",
endpoint.name
)));
}
let timeout = std::time::Duration::from_millis(endpoint.request_timeout_ms);
let connect_timeout = std::time::Duration::from_millis(endpoint.connect_timeout_ms);
let client_result = reqwest::Client::builder()
.timeout(timeout)
.connect_timeout(connect_timeout)
.pool_max_idle_per_host(endpoint.max_idle_connections_per_host as usize)
.build();
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "create_http_client", endpoint_name = %endpoint.name, provider = %endpoint.provider, error = %error, "HTTP client construction failed");
return std::result::Result::Err(kb_core::Error::http(format!(
"cannot build http client for endpoint '{}': {error}",
endpoint.name
)));
},
};
tracing::debug!(target: crate::TRACING_TARGET, action = "create_http_client", endpoint_name = %endpoint.name, provider = %endpoint.provider, role_count = endpoint.roles.len(), "HTTP client created");
return std::result::Result::Ok(Self {
endpoint,
client,
next_request_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)),
});
}
/// Returns the endpoint name.
pub fn endpoint_name(&self) -> &str {
return self.endpoint.name.as_str();
}
/// Returns the provider name.
pub fn provider(&self) -> &str {
return self.endpoint.provider.as_str();
}
/// Returns the endpoint URL.
pub fn endpoint_url(&self) -> &str {
return self.endpoint.url.as_str();
}
/// Returns the endpoint configuration.
pub fn endpoint_config(&self) -> &kb_config::HttpEndpointConfig {
return &self.endpoint;
}
/// Returns true when this endpoint supports the required role and request kind.
pub fn can_handle(&self, required_role: &str, request_kind: &str) -> bool {
if !self.endpoint.enabled {
return false;
}
for role in &self.endpoint.roles {
if crate::endpoint_role::role_matches(role, required_role, request_kind) {
return true;
}
}
return false;
}
/// Returns a serializable endpoint snapshot.
pub fn snapshot(&self) -> crate::HttpPoolClientSnapshot {
let mut roles = std::vec::Vec::new();
for role in &self.endpoint.roles {
roles.push(crate::EndpointRoleSnapshot::from_config(role));
}
return crate::HttpPoolClientSnapshot {
endpoint_name: self.endpoint.name.clone(),
provider: self.endpoint.provider.clone(),
endpoint_url: self.endpoint.url.clone(),
roles,
status: "active".to_string(),
};
}
/// Classifies a Solana HTTP method into a broad local class.
pub fn classify_method(method: &str) -> crate::HttpMethodClass {
let standard = crate::standard_http_method(method);
return match standard {
std::option::Option::Some(specification) => specification.method_class(),
std::option::Option::None => crate::HttpMethodClass::GeneralRpc,
};
}
/// Executes one typed standard HTTP request and decodes its method-specific result.
pub async fn execute_standard_request<Request>(
&self,
request: &Request,
) -> kb_core::Result<<Request as crate::StandardHttpRequest>::Response>
where
Request: crate::StandardHttpRequest,
{
let specification =
match crate::standard_http_method(<Request as crate::StandardHttpRequest>::METHOD) {
std::option::Option::Some(specification) => specification,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"typed standard request '{}' is absent from the canonical registry",
<Request as crate::StandardHttpRequest>::METHOD
)));
},
};
if specification.contract != crate::StandardRpcContract::TypedAdapter {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"standard request '{}' is not declared as a typed adapter",
<Request as crate::StandardHttpRequest>::METHOD
)));
}
let params = match request.params() {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let raw = match self
.execute_json_rpc_result_raw(
<Request as crate::StandardHttpRequest>::METHOD.to_string(),
params,
)
.await
{
std::result::Result::Ok(raw) => raw,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match serde_json::from_value::<<Request as crate::StandardHttpRequest>::Response>(
raw,
) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::json(format!(
"cannot decode standard JSON-RPC result for '{}': {error}",
<Request as crate::StandardHttpRequest>::METHOD
)))
},
};
}
/// Executes one explicitly registered standard HTTP method and returns its raw result.
pub async fn execute_standard_method_raw(
&self,
method: &crate::StandardHttpMethodSpec,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<serde_json::Value> {
return self.execute_json_rpc_result_raw(method.method.to_string(), params).await;
}
/// Executes one JSON-RPC request and returns the raw result value.
pub async fn execute_json_rpc_result_raw(
&self,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<serde_json::Value> {
let request_id = self.next_request_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let parameter_count = params.len();
let method_class = crate::HttpClient::classify_method(method.as_str());
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, method_class = ?method_class, parameter_count, "send HTTP JSON-RPC request");
let request = crate::JsonRpcRequest::new_with_u64_id(request_id, method.clone(), params);
let response_result =
self.client.post(self.endpoint.url.as_str()).json(&request).send().await;
let response = match response_result {
std::result::Result::Ok(response) => response,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, error = %error, "HTTP JSON-RPC transport failed");
return std::result::Result::Err(kb_core::Error::http(format!(
"http json-rpc request '{}' failed on endpoint '{}': {error}",
method, self.endpoint.name
)));
},
};
let status = response.status();
let text_result = response.text().await;
let text = match text_result {
std::result::Result::Ok(text) => text,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, error = %error, "HTTP JSON-RPC response body read failed");
return std::result::Result::Err(kb_core::Error::http(format!(
"cannot read http json-rpc response '{}' from endpoint '{}': {error}",
method, self.endpoint.name
)));
},
};
if !status.is_success() {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, response_byte_length = text.len(), "HTTP JSON-RPC endpoint returned non-success status");
return std::result::Result::Err(kb_core::Error::http(format!(
"http json-rpc endpoint '{}' returned status {}: {}",
self.endpoint.name, status, text
)));
}
let parsed = match crate::parse_json_rpc_text(&text) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, response_byte_length = text.len(), error = %error, "HTTP JSON-RPC response parsing failed");
return std::result::Result::Err(error);
},
};
return match parsed {
crate::JsonRpcResponse::Success(success) => {
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, response_byte_length = text.len(), outcome = "success", "HTTP JSON-RPC request completed");
std::result::Result::Ok(success.result)
},
crate::JsonRpcResponse::Error(error_response) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, rpc_error_code = error_response.error.code, rpc_error_message = %error_response.error.message, "HTTP JSON-RPC endpoint returned an RPC error");
std::result::Result::Err(kb_core::Error::http(format!(
"json-rpc error {} from '{}': {}",
error_response.error.code, self.endpoint.name, error_response.error.message
)))
},
crate::JsonRpcResponse::Notification(_) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, "HTTP JSON-RPC response was an unexpected notification");
std::result::Result::Err(kb_core::Error::http(
"http json-rpc response cannot be a notification".to_string(),
))
},
};
}
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled: true,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
fn endpoint(enabled: bool) -> kb_config::HttpEndpointConfig {
return kb_config::HttpEndpointConfig {
name: "http_a".to_string(),
enabled,
provider: "test".to_string(),
cluster: "devnet".to_string(),
url: "https://example.invalid".to_string(),
connect_timeout_ms: 100,
request_timeout_ms: 100,
max_idle_connections_per_host: 2,
roles: std::vec![
role_config("http_queries", std::vec!["get_version".to_string()]),
role_config("http_heavy", std::vec!["get_block".to_string()]),
role_config("http_any", std::vec!["*".to_string()]),
],
};
}
#[test]
fn new_rejects_disabled_endpoint() {
let result = crate::HttpClient::new(endpoint(false));
assert!(result.is_err());
}
#[test]
fn can_handle_matches_exact_role_and_kind() {
let client = match crate::HttpClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
assert!(client.can_handle("http_queries", "get_version"));
assert!(!client.can_handle("http_queries", "get_block"));
}
#[test]
fn can_handle_matches_wildcard_kind() {
let client = match crate::HttpClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
assert!(client.can_handle("http_any", "send_transaction"));
}
#[test]
fn snapshot_preserves_endpoint_metadata() {
let client = match crate::HttpClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
let snapshot = client.snapshot();
assert_eq!(snapshot.endpoint_name, "http_a");
assert_eq!(snapshot.provider, "test");
assert_eq!(snapshot.endpoint_url, "https://example.invalid");
assert_eq!(snapshot.roles.len(), 3);
}
#[test]
fn classify_method_detects_transaction_submission() {
for method in ["requestAirdrop", "sendTransaction"] {
assert_eq!(
crate::HttpClient::classify_method(method),
crate::HttpMethodClass::SendTransaction
);
}
}
#[test]
fn classify_method_detects_heavy_reads() {
assert_eq!(
crate::HttpClient::classify_method("getBlock"),
crate::HttpMethodClass::HeavyRead
);
assert_eq!(
crate::HttpClient::classify_method("getProgramAccounts"),
crate::HttpMethodClass::HeavyRead
);
assert_eq!(
crate::HttpClient::classify_method("getSignaturesForAddress"),
crate::HttpMethodClass::HeavyRead
);
assert_eq!(
crate::HttpClient::classify_method("simulateTransaction"),
crate::HttpMethodClass::HeavyRead
);
}
#[test]
fn every_standard_method_uses_its_declared_routing_class() {
for method in &crate::STANDARD_HTTP_METHODS {
assert_eq!(crate::HttpClient::classify_method(method.method), method.method_class());
}
}
#[test]
fn classify_method_defaults_to_general_rpc() {
assert_eq!(
crate::HttpClient::classify_method("getVersion"),
crate::HttpMethodClass::GeneralRpc
);
}
}

View File

@@ -0,0 +1,279 @@
// file: kb_rpc/src/http_pool.rs
// version: 6
//! HTTP endpoint pool and role-based routing.
/// Pool of HTTP JSON-RPC endpoints.
#[derive(Clone, Debug)]
pub struct HttpEndpointPool {
clients: std::vec::Vec<crate::HttpClient>,
next_index: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl crate::HttpEndpointPool {
/// Builds a pool from the active profile HTTP endpoint list.
pub fn from_profile(profile: &kb_config::ProfileConfig) -> kb_core::Result<Self> {
let mut clients = std::vec::Vec::new();
for endpoint in &profile.solana.http_endpoints {
if !endpoint.enabled {
continue;
}
let client = match crate::HttpClient::new(endpoint.clone()) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
clients.push(client);
}
return crate::HttpEndpointPool::new(clients);
}
/// Creates a pool from already constructed clients.
pub fn new(clients: std::vec::Vec<crate::HttpClient>) -> kb_core::Result<Self> {
if clients.is_empty() {
tracing::error!(target: crate::TRACING_TARGET, action = "create_http_pool", error_code = "http_pool_empty", "HTTP endpoint pool has no enabled endpoint");
return std::result::Result::Err(kb_core::Error::config(
"http endpoint pool requires at least one enabled endpoint".to_string(),
));
}
tracing::debug!(target: crate::TRACING_TARGET, action = "create_http_pool", endpoint_count = clients.len(), "HTTP endpoint pool created");
return std::result::Result::Ok(Self {
clients,
next_index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
});
}
/// Returns a serializable snapshot of every endpoint in the pool.
pub fn snapshot(&self) -> std::vec::Vec<crate::HttpPoolClientSnapshot> {
let mut snapshots = std::vec::Vec::new();
for client in &self.clients {
snapshots.push(client.snapshot());
}
return snapshots;
}
/// Selects one endpoint for the requested role and method.
pub fn select_client_for_role_and_method(
&self,
required_role: &str,
method: &str,
) -> kb_core::Result<crate::HttpClient> {
let request_kind = crate::request_kind_from_method(method);
return self.select_client_for_role_and_kind(required_role, &request_kind);
}
/// Selects one endpoint for the requested role and request kind.
pub fn select_client_for_role_and_kind(
&self,
required_role: &str,
request_kind: &str,
) -> kb_core::Result<crate::HttpClient> {
if self.clients.is_empty() {
return std::result::Result::Err(kb_core::Error::not_connected(
"http endpoint pool has no clients".to_string(),
));
}
let start_index = self.next_index.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let client_count = self.clients.len();
let mut offset = 0_usize;
while offset < client_count {
let index = (start_index + offset) % client_count;
let client = self.clients[index].clone();
if client.can_handle(required_role, request_kind) {
tracing::debug!(target: crate::TRACING_TARGET, action = "select_http_endpoint", required_role, request_kind, endpoint_name = %client.endpoint_name(), provider = %client.provider(), "selected HTTP endpoint");
return std::result::Result::Ok(client);
}
offset += 1;
}
tracing::error!(target: crate::TRACING_TARGET, action = "select_http_endpoint", required_role, request_kind, endpoint_count = self.clients.len(), error_code = "http_endpoint_not_found", "no HTTP endpoint supports requested role and kind");
return std::result::Result::Err(kb_core::Error::config(format!(
"no http endpoint supports role '{}' and request kind '{}'",
required_role, request_kind
)));
}
/// Executes one typed standard HTTP request through an endpoint selected by role.
pub async fn execute_standard_request_for_role<Request>(
&self,
required_role: &str,
request: &Request,
) -> kb_core::Result<<Request as crate::StandardHttpRequest>::Response>
where
Request: crate::StandardHttpRequest,
{
let client = match self.select_client_for_role_and_method(
required_role,
<Request as crate::StandardHttpRequest>::METHOD,
) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.execute_standard_request(request).await;
}
/// Executes one explicitly registered standard HTTP method through the selected endpoint.
pub async fn execute_standard_method_raw_for_role(
&self,
required_role: &str,
method: &crate::StandardHttpMethodSpec,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<serde_json::Value> {
let client = match self.select_client_for_role_and_method(required_role, method.method) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.execute_standard_method_raw(method, params).await;
}
/// Executes one JSON-RPC request through the selected endpoint.
pub async fn execute_json_rpc_result_raw_for_role(
&self,
required_role: &str,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<serde_json::Value> {
let client = match self.select_client_for_role_and_method(required_role, &method) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.execute_json_rpc_result_raw(method, params).await;
}
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled: true,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
fn endpoint(
name: &str,
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::HttpEndpointConfig {
return kb_config::HttpEndpointConfig {
name: name.to_string(),
enabled: true,
provider: "test".to_string(),
cluster: "devnet".to_string(),
url: format!("https://{name}.invalid"),
connect_timeout_ms: 100,
request_timeout_ms: 100,
max_idle_connections_per_host: 2,
roles: std::vec![role_config(role, request_kinds)],
};
}
fn client(endpoint: kb_config::HttpEndpointConfig) -> crate::HttpClient {
match crate::HttpClient::new(endpoint) {
std::result::Result::Ok(client) => return client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
}
}
#[test]
fn new_rejects_empty_pool() {
let result = crate::HttpEndpointPool::new(std::vec::Vec::new());
assert!(result.is_err());
}
#[test]
fn snapshot_lists_every_client() {
let pool = match crate::HttpEndpointPool::new(std::vec![
client(endpoint("a", "http_queries", std::vec!["get_version".to_string()])),
client(endpoint("b", "http_queries", std::vec!["get_version".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let snapshot = pool.snapshot();
assert_eq!(snapshot.len(), 2);
assert_eq!(snapshot[0].endpoint_name, "a");
assert_eq!(snapshot[1].endpoint_name, "b");
}
#[test]
fn select_client_round_robins_matching_clients() {
let pool = match crate::HttpEndpointPool::new(std::vec![
client(endpoint("a", "http_queries", std::vec!["get_version".to_string()])),
client(endpoint("b", "http_queries", std::vec!["get_version".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let first = match pool.select_client_for_role_and_method("http_queries", "getVersion") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
let second = match pool.select_client_for_role_and_method("http_queries", "getVersion") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(first.endpoint_name(), "a");
assert_eq!(second.endpoint_name(), "b");
}
#[test]
fn select_client_skips_unsupported_clients() {
let pool = match crate::HttpEndpointPool::new(std::vec![
client(endpoint("a", "http_heavy", std::vec!["get_block".to_string()])),
client(endpoint("b", "http_queries", std::vec!["get_version".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let selected = match pool.select_client_for_role_and_method("http_queries", "getVersion") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(selected.endpoint_name(), "b");
}
#[test]
fn standard_method_selection_uses_the_canonical_request_kind() {
let pool = match crate::HttpEndpointPool::new(std::vec![client(endpoint(
"a",
"http_queries",
std::vec!["get_version".to_string()],
))]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let method = match crate::standard_http_method("getVersion") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("getVersion specification missing"),
};
let selected = match pool.select_client_for_role_and_method("http_queries", method.method) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(selected.endpoint_name(), "a");
}
#[test]
fn select_client_returns_error_for_missing_role() {
let pool = match crate::HttpEndpointPool::new(std::vec![client(endpoint(
"a",
"http_queries",
std::vec!["get_version".to_string()]
)),])
{
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let selected = pool.select_client_for_role_and_kind("http_heavy", "get_block");
assert!(selected.is_err());
}
}

View File

@@ -0,0 +1,315 @@
// file: kb_rpc/src/json_rpc.rs
// version: 3
//! JSON-RPC 2.0 envelopes used by Solana HTTP and WebSocket transports.
/// Generic JSON-RPC 2.0 request.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcRequest {
/// JSON-RPC version, expected to be `"2.0"`.
pub jsonrpc: std::string::String,
/// Client request identifier.
pub id: serde_json::Value,
/// RPC method name.
pub method: std::string::String,
/// Ordered method parameters.
pub params: std::vec::Vec<serde_json::Value>,
}
impl crate::JsonRpcRequest {
/// Creates a request with a numeric identifier.
pub fn new_with_u64_id(
id: u64,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> Self {
return Self {
jsonrpc: "2.0".to_string(),
id: serde_json::Value::from(id),
method,
params,
};
}
/// Serializes the request into a compact JSON string.
pub fn to_json_string(&self) -> kb_core::Result<std::string::String> {
let serialization_result = serde_json::to_string(self);
return match serialization_result {
std::result::Result::Ok(text) => std::result::Result::Ok(text),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(
format!("cannot serialize json-rpc request '{}': {error}", self.method),
)),
};
}
}
/// JSON-RPC 2.0 success response.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcSuccessResponse {
/// JSON-RPC version, expected to be `"2.0"`.
pub jsonrpc: std::string::String,
/// Result payload.
pub result: serde_json::Value,
/// Request identifier echoed by the server.
pub id: serde_json::Value,
}
/// JSON-RPC 2.0 error object.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcErrorObject {
/// Numeric JSON-RPC error code.
pub code: i64,
/// Human-readable error message.
pub message: std::string::String,
/// Optional server-provided payload.
pub data: std::option::Option<serde_json::Value>,
}
/// JSON-RPC 2.0 error response.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcErrorResponse {
/// JSON-RPC version, expected to be `"2.0"`.
pub jsonrpc: std::string::String,
/// Error payload.
pub error: crate::JsonRpcErrorObject,
/// Request identifier echoed by the server.
pub id: serde_json::Value,
}
/// JSON-RPC 2.0 notification parameters.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcNotificationParams {
/// Method-specific result payload.
pub result: serde_json::Value,
/// Remote subscription identifier.
pub subscription: u64,
}
/// JSON-RPC 2.0 notification message.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcNotification {
/// JSON-RPC version, expected to be `"2.0"`.
pub jsonrpc: std::string::String,
/// Notification method name.
pub method: std::string::String,
/// Notification payload.
pub params: crate::JsonRpcNotificationParams,
}
/// Parsed JSON-RPC response or notification.
#[derive(Clone, Debug, PartialEq)]
pub enum JsonRpcResponse {
/// Success response.
Success(crate::JsonRpcSuccessResponse),
/// Error response.
Error(crate::JsonRpcErrorResponse),
/// Notification message.
Notification(crate::JsonRpcNotification),
}
impl crate::JsonRpcResponse {
/// Returns a stable diagnostic kind name.
pub fn kind_name(&self) -> &'static str {
return match self {
Self::Success(_) => "success",
Self::Error(_) => "error",
Self::Notification(_) => "notification",
};
}
/// Converts the parsed response into a JSON value for UI display.
pub fn to_value(&self) -> kb_core::Result<serde_json::Value> {
return match self {
Self::Success(response) => {
let value_result = serde_json::to_value(response);
match value_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::json(error.to_string()))
},
}
},
Self::Error(response) => {
let value_result = serde_json::to_value(response);
match value_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::json(error.to_string()))
},
}
},
Self::Notification(notification) => {
let value_result = serde_json::to_value(notification);
match value_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::json(error.to_string()))
},
}
},
};
}
}
/// Parses an incoming JSON-RPC text payload.
pub fn parse_json_rpc_text(text: &str) -> kb_core::Result<crate::JsonRpcResponse> {
let value = match serde_json::from_str::<serde_json::Value>(text) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::json(format!(
"cannot parse json-rpc text: {error}"
)));
},
};
let object = match value.as_object() {
std::option::Option::Some(object) => object,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::json(
"json-rpc payload must be an object".to_string(),
));
},
};
let has_method = object.contains_key("method");
let has_params = object.contains_key("params");
let has_result = object.contains_key("result");
let has_error = object.contains_key("error");
let has_id = object.contains_key("id");
if has_method && has_params && !has_id {
let parse_result = serde_json::from_value::<crate::JsonRpcNotification>(value);
return match parse_result {
std::result::Result::Ok(notification) => {
std::result::Result::Ok(crate::JsonRpcResponse::Notification(notification))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(
format!("cannot parse json-rpc notification: {error}"),
)),
};
}
if has_id && has_result && !has_error {
let parse_result = serde_json::from_value::<crate::JsonRpcSuccessResponse>(value);
return match parse_result {
std::result::Result::Ok(response) => {
std::result::Result::Ok(crate::JsonRpcResponse::Success(response))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(
format!("cannot parse json-rpc success response: {error}"),
)),
};
}
if has_id && has_error && !has_result {
let parse_result = serde_json::from_value::<crate::JsonRpcErrorResponse>(value);
return match parse_result {
std::result::Result::Ok(response) => {
std::result::Result::Ok(crate::JsonRpcResponse::Error(response))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(
format!("cannot parse json-rpc error response: {error}"),
)),
};
}
return std::result::Result::Err(kb_core::Error::json(
"unsupported json-rpc response shape".to_string(),
));
}
#[cfg(test)]
mod tests {
#[test]
fn request_serialization_contains_jsonrpc_version() {
let request = crate::JsonRpcRequest::new_with_u64_id(
1,
"getVersion".to_string(),
std::vec::Vec::new(),
);
let text = match request.to_json_string() {
std::result::Result::Ok(text) => text,
std::result::Result::Err(error) => panic!("request serialization failed: {error}"),
};
assert!(text.contains("\"jsonrpc\":\"2.0\""));
assert!(text.contains("\"method\":\"getVersion\""));
}
#[test]
fn request_serialization_preserves_params() {
let request = crate::JsonRpcRequest::new_with_u64_id(
9,
"getBalance".to_string(),
std::vec![serde_json::Value::String("account".to_string())],
);
let text = match request.to_json_string() {
std::result::Result::Ok(text) => text,
std::result::Result::Err(error) => panic!("request serialization failed: {error}"),
};
assert!(text.contains("\"params\":[\"account\"]"));
}
#[test]
fn parses_success_response() {
let parsed = match crate::parse_json_rpc_text("{\"jsonrpc\":\"2.0\",\"result\":7,\"id\":1}")
{
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => panic!("response parsing failed: {error}"),
};
assert_eq!(parsed.kind_name(), "success");
}
#[test]
fn parses_error_response() {
let parsed = match crate::parse_json_rpc_text(
"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"invalid params\"},\"id\":2}",
) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => panic!("error response parsing failed: {error}"),
};
match parsed {
crate::JsonRpcResponse::Error(response) => {
assert_eq!(response.error.code, -32602);
assert_eq!(response.error.message, "invalid params");
},
_ => panic!("expected error response"),
}
}
#[test]
fn parses_notification_response() {
let text = "{\"jsonrpc\":\"2.0\",\"method\":\"slotNotification\",\"params\":{\"result\":123,\"subscription\":77}}";
let parsed = match crate::parse_json_rpc_text(text) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => panic!("notification parsing failed: {error}"),
};
match parsed {
crate::JsonRpcResponse::Notification(notification) => {
assert_eq!(notification.method, "slotNotification");
assert_eq!(notification.params.subscription, 77);
},
_ => panic!("expected notification"),
}
}
#[test]
fn parses_rejects_non_object_payload() {
let parsed = crate::parse_json_rpc_text("[1,2,3]");
assert!(parsed.is_err());
}
#[test]
fn parses_rejects_unsupported_shape() {
let parsed = crate::parse_json_rpc_text("{\"jsonrpc\":\"2.0\",\"method\":\"x\",\"id\":1}");
assert!(parsed.is_err());
}
#[test]
fn response_to_value_preserves_success_result() {
let parsed = match crate::parse_json_rpc_text(
"{\"jsonrpc\":\"2.0\",\"result\":{\"solana-core\":\"x\"},\"id\":1}",
) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => panic!("response parsing failed: {error}"),
};
let value = match parsed.to_value() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("response value conversion failed: {error}"),
};
assert_eq!(value["result"]["solana-core"].as_str(), std::option::Option::Some("x"));
}
}

View File

@@ -0,0 +1,462 @@
// file: kb_rpc/src/lib.rs
// version: 16
//! Solana HTTP JSON-RPC and standard WebSocket client layer.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
mod client;
mod constants;
mod endpoint_role;
mod execution_rpc;
mod get_signatures_for_address;
mod get_transaction;
mod http_client;
mod http_pool;
mod json_rpc;
mod standard_http;
mod standard_http_accounts;
mod standard_http_blocks;
mod standard_http_cluster;
mod standard_http_economics;
mod standard_http_tokens;
mod standard_http_transactions;
mod standard_methods;
mod standard_ws;
mod validation;
mod ws_client;
mod ws_pool;
mod ws_session;
/// Canonical tracing target for this crate.
pub(crate) use crate::constants::TRACING_TARGET;
/// RPC endpoint configuration.
pub use crate::client::RpcEndpoint;
/// Minimal Solana RPC client abstraction.
pub use crate::client::SolanaRpcClient;
/// Endpoint role snapshot shared by HTTP and WebSocket pools.
pub use crate::endpoint_role::EndpointRoleSnapshot;
/// Converts a JSON-RPC method name into a stable request kind.
pub use crate::endpoint_role::request_kind_from_method;
/// Contextual account information result.
pub use crate::execution_rpc::AccountInfoResult;
/// Bounded account metadata and optional complete decoded data returned by `getAccountInfo`.
pub use crate::execution_rpc::AccountInfoValue;
/// Signature returned by a faucet airdrop request.
pub use crate::execution_rpc::AirdropResult;
/// Lamport balance returned for one account.
pub use crate::execution_rpc::BalanceResult;
/// Current block height returned by the node.
pub use crate::execution_rpc::BlockHeightResult;
/// Bounded transaction confirmation policy.
pub use crate::execution_rpc::ConfirmTransactionConfig;
/// Current epoch and slot progression returned by `getEpochInfo`.
pub use crate::execution_rpc::EpochInfoResult;
/// Fee estimate returned for one serialized message.
pub use crate::execution_rpc::FeeForMessageResult;
/// Genesis hash and known public-cluster classification.
pub use crate::execution_rpc::GenesisHashResult;
/// Configuration for `getAccountInfo`.
pub use crate::execution_rpc::GetAccountInfoConfig;
/// Configuration for `getBalance`.
pub use crate::execution_rpc::GetBalanceConfig;
/// Configuration for `getBlockHeight`.
pub use crate::execution_rpc::GetBlockHeightConfig;
/// Configuration for `getEpochInfo`.
pub use crate::execution_rpc::GetEpochInfoConfig;
/// Configuration for `getFeeForMessage`.
pub use crate::execution_rpc::GetFeeForMessageConfig;
/// Configuration for `getLatestBlockhash`.
pub use crate::execution_rpc::GetLatestBlockhashConfig;
/// Configuration for `getMinimumBalanceForRentExemption`.
pub use crate::execution_rpc::GetMinimumBalanceForRentExemptionConfig;
/// Configuration for `getSignatureStatuses`.
pub use crate::execution_rpc::GetSignatureStatusesConfig;
/// Latest recent blockhash returned by the cluster.
pub use crate::execution_rpc::LatestBlockhashResult;
/// Rent-exempt minimum for one account data length.
pub use crate::execution_rpc::MinimumBalanceForRentExemptionResult;
/// Configuration for `requestAirdrop`.
pub use crate::execution_rpc::RequestAirdropConfig;
/// Commitment level accepted by execution-oriented RPC methods.
pub use crate::execution_rpc::RpcCommitmentLevel;
/// Standard context attached to Solana RPC responses.
pub use crate::execution_rpc::RpcResponseContext;
/// Configuration for `sendTransaction`.
pub use crate::execution_rpc::SendTransactionConfig;
/// Result returned after a node accepts a signed transaction.
pub use crate::execution_rpc::SendTransactionResult;
/// Current status for one submitted signature.
pub use crate::execution_rpc::SignatureStatus;
/// Positional status response for submitted signatures.
pub use crate::execution_rpc::SignatureStatusesResult;
/// Configuration for `simulateTransaction`.
pub use crate::execution_rpc::SimulateTransactionConfig;
/// Typed result returned by `simulateTransaction`.
pub use crate::execution_rpc::SimulateTransactionResult;
/// Optional accounts requested from `simulateTransaction`.
pub use crate::execution_rpc::SimulationAccountsConfig;
/// Replacement blockhash returned by simulation.
pub use crate::execution_rpc::SimulationReplacementBlockhash;
/// Adapts a raw `getAccountInfo` result.
pub use crate::execution_rpc::adapt_get_account_info_result;
/// Adapts a raw `getBalance` result.
pub use crate::execution_rpc::adapt_get_balance_result;
/// Adapts a raw `getBlockHeight` result.
pub use crate::execution_rpc::adapt_get_block_height_result;
/// Adapts a raw `getEpochInfo` result.
pub use crate::execution_rpc::adapt_get_epoch_info_result;
/// Adapts a raw `getFeeForMessage` result.
pub use crate::execution_rpc::adapt_get_fee_for_message_result;
/// Adapts a raw `getGenesisHash` result.
pub use crate::execution_rpc::adapt_get_genesis_hash_result;
/// Adapts a raw `getLatestBlockhash` result.
pub use crate::execution_rpc::adapt_get_latest_blockhash_result;
/// Adapts a raw `getMinimumBalanceForRentExemption` result.
pub use crate::execution_rpc::adapt_get_minimum_balance_for_rent_exemption_result;
/// Adapts a raw `getSignatureStatuses` result.
pub use crate::execution_rpc::adapt_get_signature_statuses_result;
/// Adapts a raw `requestAirdrop` result.
pub use crate::execution_rpc::adapt_request_airdrop_result;
/// Adapts a raw `sendTransaction` result.
pub use crate::execution_rpc::adapt_send_transaction_result;
/// Adapts a raw `simulateTransaction` result.
pub use crate::execution_rpc::adapt_simulate_transaction_result;
/// Classifies a genesis hash as an official public cluster.
pub use crate::execution_rpc::classify_genesis_hash;
/// One signature summary returned by `getSignaturesForAddress`.
pub use crate::get_signatures_for_address::AddressSignatureInfo;
/// Standard Solana `getSignaturesForAddress` request configuration.
pub use crate::get_signatures_for_address::GetSignaturesForAddressConfig;
/// Adapts a standard Solana `getSignaturesForAddress` result value.
pub use crate::get_signatures_for_address::adapt_get_signatures_for_address_result;
/// Common adapter contract for transport-specific transaction payloads.
pub use crate::get_transaction::CanonicalTransactionAdapter;
/// Source payload and canonical result from one `getTransaction` acquisition.
pub use crate::get_transaction::GetTransactionAcquisition;
/// Standard Solana `getTransaction` JSON adapter.
pub use crate::get_transaction::GetTransactionAdapter;
/// Standard Solana `getTransaction` request configuration.
pub use crate::get_transaction::GetTransactionConfig;
/// Adapts a standard Solana `getTransaction` result value.
pub use crate::get_transaction::adapt_get_transaction_result;
/// HTTP JSON-RPC client bound to one endpoint.
pub use crate::http_client::HttpClient;
/// HTTP method class used for routing diagnostics.
pub use crate::http_client::HttpMethodClass;
/// Snapshot of one HTTP pool endpoint.
pub use crate::http_client::HttpPoolClientSnapshot;
/// HTTP endpoint pool with role-based routing.
pub use crate::http_pool::HttpEndpointPool;
/// JSON-RPC 2.0 error object.
pub use crate::json_rpc::JsonRpcErrorObject;
/// JSON-RPC 2.0 error response.
pub use crate::json_rpc::JsonRpcErrorResponse;
/// JSON-RPC 2.0 notification.
pub use crate::json_rpc::JsonRpcNotification;
/// JSON-RPC 2.0 notification parameters.
pub use crate::json_rpc::JsonRpcNotificationParams;
/// JSON-RPC 2.0 request.
pub use crate::json_rpc::JsonRpcRequest;
/// JSON-RPC 2.0 response parsed from HTTP or WebSocket text.
pub use crate::json_rpc::JsonRpcResponse;
/// JSON-RPC 2.0 success response.
pub use crate::json_rpc::JsonRpcSuccessResponse;
/// Parses an incoming JSON-RPC text payload.
pub use crate::json_rpc::parse_json_rpc_text;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcAccountBalance;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcAccountEncoding;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcAccountInfoConfig;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcCommitmentConfig;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcContextConfig;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcDataSlice;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcKeyedAccount;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcMemcmp;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcMemcmpEncodedBytes;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcOptionalContext;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcProgramAccountFilter;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcProgramAccountsConfig;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcResponse;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcTokenAccountBalance;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcTokenAccountsFilter;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcTokenAmount;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcTransactionDetails;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcTransactionEncoding;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::RpcUiAccount;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use crate::standard_http::StandardHttpRequest;
/// Configurable account-oriented standard HTTP requests.
pub use crate::standard_http_accounts::GetLargestAccountsRequest;
/// Configurable account-oriented standard HTTP requests.
pub use crate::standard_http_accounts::GetMultipleAccountsRequest;
/// Configurable account-oriented standard HTTP requests.
pub use crate::standard_http_accounts::GetProgramAccountsRequest;
/// Configurable account-oriented standard HTTP requests.
pub use crate::standard_http_accounts::RpcLargestAccountsConfig;
/// Configurable account-oriented standard HTTP requests.
pub use crate::standard_http_accounts::RpcLargestAccountsFilter;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::GetBlockCommitmentRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::GetBlockProductionRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::GetBlockRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::GetBlockTimeRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::GetBlocksRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::GetBlocksWithLimitRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::GetFirstAvailableBlockRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::GetRecentPerformanceSamplesRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::MinimumLedgerSlotRequest;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcBlockCommitment;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcBlockConfig;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcBlockProduction;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcBlockProductionConfig;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcBlockProductionConfigRange;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcBlockProductionRange;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcConfirmedBlock;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcPerformanceSample;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcReward;
/// Configurable block and ledger standard HTTP requests.
pub use crate::standard_http_blocks::RpcRewardType;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetClusterNodesRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetEpochScheduleRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetHealthRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetHighestSnapshotSlotRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetIdentityRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetLeaderScheduleRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetMaxRetransmitSlotRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetMaxShredInsertSlotRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetSlotLeaderRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetSlotLeadersRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetSlotRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetVersionRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::GetVoteAccountsRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcContactInfo;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcEpochSchedule;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcGetVoteAccountsConfig;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcIdentity;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcLeaderSchedule;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcLeaderScheduleConfig;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcSnapshotSlotInfo;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcVersionInfo;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcVoteAccountInfo;
/// Configurable cluster-oriented standard HTTP requests.
pub use crate::standard_http_cluster::RpcVoteAccountStatus;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::GetInflationGovernorRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::GetInflationRateRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::GetInflationRewardRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::GetStakeMinimumDelegationRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::GetSupplyRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::RpcEpochConfig;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::RpcInflationGovernor;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::RpcInflationGovernorConfig;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::RpcInflationRate;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::RpcInflationReward;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::RpcSupply;
/// Configurable economics-oriented standard HTTP requests.
pub use crate::standard_http_economics::RpcSupplyConfig;
/// Configurable token-oriented standard HTTP requests.
pub use crate::standard_http_tokens::GetTokenAccountBalanceRequest;
/// Configurable token-oriented standard HTTP requests.
pub use crate::standard_http_tokens::GetTokenAccountsByDelegateRequest;
/// Configurable token-oriented standard HTTP requests.
pub use crate::standard_http_tokens::GetTokenAccountsByOwnerRequest;
/// Configurable token-oriented standard HTTP requests.
pub use crate::standard_http_tokens::GetTokenLargestAccountsRequest;
/// Configurable token-oriented standard HTTP requests.
pub use crate::standard_http_tokens::GetTokenSupplyRequest;
/// Configurable transaction-oriented standard HTTP requests.
pub use crate::standard_http_transactions::GetRecentPrioritizationFeesRequest;
/// Configurable transaction-oriented standard HTTP requests.
pub use crate::standard_http_transactions::GetTransactionCountRequest;
/// Configurable transaction-oriented standard HTTP requests.
pub use crate::standard_http_transactions::IsBlockhashValidRequest;
/// Configurable transaction-oriented standard HTTP requests.
pub use crate::standard_http_transactions::RpcPrioritizationFee;
/// Every standard Solana HTTP JSON-RPC method.
pub use crate::standard_methods::STANDARD_HTTP_METHODS;
/// Every standard Solana WebSocket subscription pair.
pub use crate::standard_methods::STANDARD_WS_SUBSCRIPTIONS;
/// Official category used to group standard Solana HTTP methods.
pub use crate::standard_methods::StandardHttpCategory;
/// One canonical standard Solana HTTP method specification.
pub use crate::standard_methods::StandardHttpMethodSpec;
/// Documentation-level contract exposed for a standard RPC method.
pub use crate::standard_methods::StandardRpcContract;
/// Stability of one standard Solana WebSocket subscription surface.
pub use crate::standard_methods::StandardWsStability;
/// One canonical standard Solana WebSocket subscribe/unsubscribe pair.
pub use crate::standard_methods::StandardWsSubscriptionSpec;
/// Finds one exact standard HTTP method specification.
pub use crate::standard_methods::standard_http_method;
/// Finds one standard WebSocket subscription from its subscribe or unsubscribe method.
pub use crate::standard_methods::standard_ws_subscription;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::AccountSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::BlockSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::LogsSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::ProgramSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::RootSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::SignatureSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::SlotSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::SlotsUpdatesSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::StandardWsCapabilities;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::StandardWsNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::StandardWsRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::VoteSubscribeRequest;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsAccountNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsAccountSubscribeConfig;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsBlockFilter;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsBlockNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsBlockSubscribeConfig;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsBlockUpdate;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsLogsFilter;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsLogsNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsLogsSubscribeConfig;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsLogsValue;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsProgramNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsProgramSubscribeConfig;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsRootNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsSignatureNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsSignatureSubscribeConfig;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsSignatureValue;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsSlotInfo;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsSlotNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsSlotTransactionStats;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsSlotUpdate;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsSlotsUpdatesNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsVoteNotification;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::WsVoteValue;
/// Typed contracts for all standard Solana WebSocket subscriptions.
pub use crate::standard_ws::adapt_standard_ws_notification;
/// Validates one base58 Solana blockhash or genesis hash.
pub use crate::validation::validate_solana_hash_text;
/// Validates one base58 Solana public key.
pub use crate::validation::validate_solana_pubkey_text;
/// Validates one base58 Solana transaction signature.
pub use crate::validation::validate_transaction_signature_text;
/// Standard Solana WebSocket client bound to one endpoint.
pub use crate::ws_client::WsClient;
/// Snapshot of one WebSocket pool endpoint.
pub use crate::ws_client::WsPoolClientSnapshot;
/// WebSocket endpoint pool with role-based routing.
pub use crate::ws_pool::WsEndpointPool;
/// Persistent WebSocket session contracts.
pub use crate::ws_session::WsReconnectPolicy;
/// Persistent WebSocket session contracts.
pub use crate::ws_session::WsSession;
/// Persistent WebSocket session contracts.
pub use crate::ws_session::WsSessionEvent;
/// Persistent WebSocket session contracts.
pub use crate::ws_session::WsSessionSnapshot;
/// Persistent WebSocket session contracts.
pub use crate::ws_session::WsSessionState;
/// Persistent WebSocket session contracts.
pub use crate::ws_session::WsSubscriptionAck;
/// Persistent WebSocket session contracts.
pub use crate::ws_session::WsSubscriptionSnapshot;
/// Persistent WebSocket session contracts.
pub use crate::ws_session::WsUnsubscribeAck;

View File

@@ -0,0 +1,499 @@
// file: kb_rpc/src/standard_http.rs
// version: 2
//! Shared contracts for configurable standard Solana HTTP JSON-RPC requests.
use base64::Engine; // rust-rules: trait-import
/// Typed request contract for one standard Solana HTTP JSON-RPC method.
pub trait StandardHttpRequest {
/// Method-specific response decoded from the JSON-RPC `result` value.
type Response: serde::de::DeserializeOwned;
/// Exact standard Solana JSON-RPC method name.
const METHOD: &'static str;
/// Builds the exact positional JSON-RPC parameter array.
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>>;
}
/// Standard contextual Solana RPC response.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcResponse<T> {
/// Slot and optional API version used by the node.
pub context: crate::RpcResponseContext,
/// Method-specific response value.
pub value: T,
}
/// Response that may be returned directly or wrapped with an RPC context.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum RpcOptionalContext<T> {
/// Contextual response form.
Context(crate::RpcResponse<T>),
/// Backward-compatible response form without context.
Value(T),
}
/// Commitment and minimum-context options shared by standard read methods.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcContextConfig {
/// Optional commitment level. Absence delegates the default to the endpoint.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional minimum slot at which the request may be evaluated.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub min_context_slot: std::option::Option<u64>,
}
/// Commitment-only options used by methods that do not accept `minContextSlot`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcCommitmentConfig {
/// Optional commitment level. Absence delegates the default to the endpoint.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
/// Account-data encoding accepted by standard account RPC methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum RpcAccountEncoding {
/// Legacy binary account data encoding alias.
#[serde(rename = "binary")]
Binary,
/// Legacy base58 account data encoding.
#[serde(rename = "base58")]
Base58,
/// Base64 account data encoding.
#[serde(rename = "base64")]
Base64,
/// Zstandard-compressed base64 account data encoding.
#[serde(rename = "base64+zstd")]
Base64Zstd,
/// Program-aware parsed JSON account data.
#[serde(rename = "jsonParsed")]
JsonParsed,
}
/// Transaction encoding accepted by block and transaction RPC methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum RpcTransactionEncoding {
/// Legacy binary transaction encoding.
#[serde(rename = "binary")]
Binary,
/// Base58 transaction encoding.
#[serde(rename = "base58")]
Base58,
/// Base64 transaction encoding.
#[serde(rename = "base64")]
Base64,
/// Structured JSON transaction encoding.
#[serde(rename = "json")]
Json,
/// Program-aware parsed JSON transaction encoding.
#[serde(rename = "jsonParsed")]
JsonParsed,
}
/// Transaction detail level accepted by block-oriented RPC methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcTransactionDetails {
/// Full transactions and metadata.
Full,
/// Signatures only.
Signatures,
/// No transaction entries.
None,
/// Account lists without full transaction data.
Accounts,
}
/// Optional byte range requested from account data.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcDataSlice {
/// Byte offset from the start of account data.
pub offset: usize,
/// Number of bytes requested.
pub length: usize,
}
impl crate::RpcDataSlice {
fn validate(&self) -> kb_core::Result<()> {
if self.offset.checked_add(self.length).is_none() {
return std::result::Result::Err(kb_core::Error::config(
"account data slice offset and length overflow usize",
));
}
return std::result::Result::Ok(());
}
}
/// Configurable account representation shared by account and token methods.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcAccountInfoConfig {
/// Optional account-data encoding.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub encoding: std::option::Option<crate::RpcAccountEncoding>,
/// Optional account-data byte slice.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub data_slice: std::option::Option<crate::RpcDataSlice>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional minimum context slot.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub min_context_slot: std::option::Option<u64>,
}
impl crate::RpcAccountInfoConfig {
/// Validates combinations that the standard account RPC contract cannot represent.
pub fn validate(&self) -> kb_core::Result<()> {
if let std::option::Option::Some(data_slice) = self.data_slice {
let slice_result = data_slice.validate();
if let std::result::Result::Err(error) = slice_result {
return std::result::Result::Err(error);
}
if self.encoding == std::option::Option::Some(crate::RpcAccountEncoding::JsonParsed) {
return std::result::Result::Err(kb_core::Error::config(
"jsonParsed account encoding cannot be combined with dataSlice",
));
}
}
return std::result::Result::Ok(());
}
}
/// Encoded bytes used by a program-account memcmp filter.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase", tag = "encoding", content = "bytes")]
pub enum RpcMemcmpEncodedBytes {
/// Base58-encoded bytes.
Base58(std::string::String),
/// Base64-encoded bytes.
Base64(std::string::String),
/// Explicit raw byte array.
Bytes(std::vec::Vec<u8>),
}
impl crate::RpcMemcmpEncodedBytes {
fn validate(&self) -> kb_core::Result<()> {
let decoded_length = match self {
Self::Base58(value) => {
if value.len() > crate::constants::MAX_MEMCMP_BASE58_LENGTH {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base58 value must not exceed {} characters",
crate::constants::MAX_MEMCMP_BASE58_LENGTH
)));
}
let decoded = match bs58::decode(value).into_vec() {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base58 value is invalid: {error}"
)));
},
};
decoded.len()
},
Self::Base64(value) => {
if value.len() > crate::constants::MAX_MEMCMP_BASE64_LENGTH {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base64 value must not exceed {} characters",
crate::constants::MAX_MEMCMP_BASE64_LENGTH
)));
}
let decoded = match base64::prelude::BASE64_STANDARD.decode(value) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base64 value is invalid: {error}"
)));
},
};
decoded.len()
},
Self::Bytes(value) => value.len(),
};
if decoded_length > crate::constants::MAX_MEMCMP_DECODED_BYTES {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp value must not exceed {} decoded bytes",
crate::constants::MAX_MEMCMP_DECODED_BYTES
)));
}
return std::result::Result::Ok(());
}
}
/// Offset and encoded bytes used by a memcmp account filter.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcMemcmp {
/// Data offset at which the comparison starts.
pub offset: usize,
/// Bytes compared at the requested offset.
#[serde(flatten)]
pub bytes: crate::RpcMemcmpEncodedBytes,
}
/// One filter accepted by `getProgramAccounts` and `programSubscribe`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcProgramAccountFilter {
/// Exact account data size.
DataSize(u64),
/// Byte comparison at an account-data offset.
Memcmp(crate::RpcMemcmp),
/// Standard SPL Token account state filter.
TokenAccountState,
}
impl crate::RpcProgramAccountFilter {
fn validate(&self) -> kb_core::Result<()> {
if let Self::Memcmp(memcmp) = self {
return memcmp.bytes.validate();
}
return std::result::Result::Ok(());
}
}
/// Configurable `getProgramAccounts` request options.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcProgramAccountsConfig {
/// Optional account filters, evaluated by the node in the provided order.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub filters: std::option::Option<std::vec::Vec<crate::RpcProgramAccountFilter>>,
/// Optional account-data encoding.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub encoding: std::option::Option<crate::RpcAccountEncoding>,
/// Optional account-data byte slice.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub data_slice: std::option::Option<crate::RpcDataSlice>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional minimum context slot.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub min_context_slot: std::option::Option<u64>,
/// Whether the response must include a context wrapper.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub with_context: std::option::Option<bool>,
/// Optional validator-side deterministic result sorting.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub sort_results: std::option::Option<bool>,
}
impl crate::RpcProgramAccountsConfig {
/// Validates account encoding, data slicing and every filter.
pub fn validate(&self) -> kb_core::Result<()> {
let account_config = crate::RpcAccountInfoConfig {
encoding: self.encoding,
data_slice: self.data_slice,
commitment: self.commitment,
min_context_slot: self.min_context_slot,
};
let account_result = account_config.validate();
if let std::result::Result::Err(error) = account_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(filters) = &self.filters {
for filter in filters {
let filter_result = filter.validate();
if let std::result::Result::Err(error) = filter_result {
return std::result::Result::Err(error);
}
}
}
return std::result::Result::Ok(());
}
}
/// Mint or Token Program selector used by token-account queries.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcTokenAccountsFilter {
/// Select token accounts for one mint.
Mint(std::string::String),
/// Select accounts owned by one Token Program generation.
ProgramId(std::string::String),
}
impl crate::RpcTokenAccountsFilter {
/// Validates the public key embedded in the selected filter.
pub fn validate(&self) -> kb_core::Result<()> {
let value = match self {
Self::Mint(value) | Self::ProgramId(value) => value,
};
return crate::validate_solana_pubkey_text(value, "token account filter public key");
}
}
/// Account representation returned by configurable standard account methods.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcUiAccount {
/// Account lamports.
pub lamports: u64,
/// Owner Program ID.
pub owner: std::string::String,
/// Whether the account is executable.
pub executable: bool,
/// Rent epoch reported by the node.
pub rent_epoch: u64,
/// Account data length when exposed by the node.
#[serde(default)]
pub space: std::option::Option<u64>,
/// Encoding-dependent account data payload.
pub data: serde_json::Value,
}
/// Public key and account pair returned by program and token-account scans.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcKeyedAccount {
/// Account public key.
pub pubkey: std::string::String,
/// Account state and data.
pub account: crate::RpcUiAccount,
}
/// Token amount with exact integer and decimal string representations.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcTokenAmount {
/// Raw token amount as an unsigned decimal string.
pub amount: std::string::String,
/// Mint decimal precision.
pub decimals: u8,
/// Optional floating representation retained for wire compatibility.
pub ui_amount: std::option::Option<f64>,
/// Exact decimal display string.
pub ui_amount_string: std::string::String,
}
/// Token account address and balance returned by `getTokenLargestAccounts`.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcTokenAccountBalance {
/// Token account public key.
pub address: std::string::String,
/// Raw token amount as an unsigned decimal string.
pub amount: std::string::String,
/// Mint decimal precision.
pub decimals: u8,
/// Optional floating representation retained for wire compatibility.
pub ui_amount: std::option::Option<f64>,
/// Exact decimal display string.
pub ui_amount_string: std::string::String,
}
/// One lamport-ranked account returned by `getLargestAccounts`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcAccountBalance {
/// Account public key.
pub address: std::string::String,
/// Lamport balance.
pub lamports: u64,
}
pub(crate) fn serialize_parameter<T: serde::Serialize>(
method: &str,
value: &T,
) -> kb_core::Result<serde_json::Value> {
return match serde_json::to_value(value) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(format!(
"cannot serialize {method} parameter: {error}"
))),
};
}
pub(crate) fn validate_pubkey_list(
values: &[std::string::String],
field: &str,
maximum: usize,
) -> kb_core::Result<()> {
if values.len() > maximum {
return std::result::Result::Err(kb_core::Error::config(format!(
"{field} must not exceed {maximum} entries"
)));
}
for value in values {
let validation_result = crate::validate_solana_pubkey_text(value, field);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn optional_account_options_serialize_only_selected_fields() {
let config = crate::RpcAccountInfoConfig {
encoding: std::option::Option::Some(crate::RpcAccountEncoding::Base64Zstd),
data_slice: std::option::Option::Some(crate::RpcDataSlice { offset: 32, length: 64 }),
commitment: std::option::Option::None,
min_context_slot: std::option::Option::Some(91),
};
let value = match serde_json::to_value(config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("config serialization failed: {error}"),
};
assert_eq!(
value,
serde_json::json!({
"encoding": "base64+zstd",
"dataSlice": { "offset": 32, "length": 64 },
"minContextSlot": 91
})
);
let binary = match serde_json::to_value(crate::RpcAccountEncoding::Binary) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("binary encoding serialization failed: {error}")
},
};
assert_eq!(binary, serde_json::Value::String("binary".to_string()));
}
#[test]
fn memcmp_filter_preserves_encoding_and_enforces_decoded_bound() {
let filter = crate::RpcProgramAccountFilter::Memcmp(crate::RpcMemcmp {
offset: 8,
bytes: crate::RpcMemcmpEncodedBytes::Bytes(std::vec![1_u8, 2_u8, 3_u8]),
});
let value = match serde_json::to_value(&filter) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("filter serialization failed: {error}"),
};
assert_eq!(
value,
serde_json::json!({
"memcmp": { "offset": 8, "encoding": "bytes", "bytes": [1, 2, 3] }
})
);
let oversized = crate::RpcProgramAccountFilter::Memcmp(crate::RpcMemcmp {
offset: 0,
bytes: crate::RpcMemcmpEncodedBytes::Bytes(std::vec![0_u8; 129]),
});
assert!(oversized.validate().is_err());
}
#[test]
fn json_parsed_account_data_rejects_data_slice() {
let config = crate::RpcAccountInfoConfig {
encoding: std::option::Option::Some(crate::RpcAccountEncoding::JsonParsed),
data_slice: std::option::Option::Some(crate::RpcDataSlice { offset: 0, length: 1 }),
commitment: std::option::Option::None,
min_context_slot: std::option::Option::None,
};
assert!(config.validate().is_err());
}
}

View File

@@ -0,0 +1,215 @@
// file: kb_rpc/src/standard_http_accounts.rs
// version: 1
//! Configurable standard account-oriented Solana HTTP JSON-RPC requests.
/// Optional circulating-supply filter for `getLargestAccounts`.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcLargestAccountsFilter {
/// Accounts included in circulating supply.
Circulating,
/// Accounts excluded from circulating supply.
NonCirculating,
}
/// Options accepted by `getLargestAccounts`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcLargestAccountsConfig {
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional circulating-supply filter.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub filter: std::option::Option<crate::RpcLargestAccountsFilter>,
/// Optional validator-side deterministic sorting flag.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub sort_results: std::option::Option<bool>,
}
/// Typed `getLargestAccounts` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetLargestAccountsRequest {
/// Optional request configuration. `None` emits no parameter.
pub config: std::option::Option<crate::RpcLargestAccountsConfig>,
}
impl crate::StandardHttpRequest for crate::GetLargestAccountsRequest {
type Response = crate::RpcResponse<std::vec::Vec<crate::RpcAccountBalance>>;
const METHOD: &'static str = "getLargestAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getMultipleAccounts` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetMultipleAccountsRequest {
/// Account public keys in positional response order.
pub addresses: std::vec::Vec<std::string::String>,
/// Optional account encoding, slicing and context options.
pub config: std::option::Option<crate::RpcAccountInfoConfig>,
}
impl crate::StandardHttpRequest for crate::GetMultipleAccountsRequest {
type Response = crate::RpcResponse<std::vec::Vec<std::option::Option<crate::RpcUiAccount>>>;
const METHOD: &'static str = "getMultipleAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result = crate::standard_http::validate_pubkey_list(
&self.addresses,
"getMultipleAccounts address",
crate::constants::MAX_MULTIPLE_ACCOUNT_COUNT,
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(config) = &self.config {
let config_result = config.validate();
if let std::result::Result::Err(error) = config_result {
return std::result::Result::Err(error);
}
}
let addresses =
match crate::standard_http::serialize_parameter(Self::METHOD, &self.addresses) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut params = std::vec![addresses];
if let std::option::Option::Some(config) = &self.config {
let config_value = match crate::standard_http::serialize_parameter(Self::METHOD, config)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(config_value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getProgramAccounts` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetProgramAccountsRequest {
/// Program ID whose owned accounts are requested.
pub program_id: std::string::String,
/// Optional filters, account representation and contextual response options.
pub config: std::option::Option<crate::RpcProgramAccountsConfig>,
}
impl crate::StandardHttpRequest for crate::GetProgramAccountsRequest {
type Response = crate::RpcOptionalContext<std::vec::Vec<crate::RpcKeyedAccount>>;
const METHOD: &'static str = "getProgramAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
crate::validate_solana_pubkey_text(&self.program_id, "getProgramAccounts program id");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(config) = &self.config {
let config_result = config.validate();
if let std::result::Result::Err(error) = config_result {
return std::result::Result::Err(error);
}
}
let mut params = std::vec![serde_json::Value::String(self.program_id.clone())];
if let std::option::Option::Some(config) = &self.config {
let config_value = match crate::standard_http::serialize_parameter(Self::METHOD, config)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(config_value);
}
return std::result::Result::Ok(params);
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn multiple_accounts_preserves_optional_encoding_slice_and_context() {
let request = crate::GetMultipleAccountsRequest {
addresses: std::vec![pubkey(1), pubkey(2)],
config: std::option::Option::Some(crate::RpcAccountInfoConfig {
encoding: std::option::Option::Some(crate::RpcAccountEncoding::Base64),
data_slice: std::option::Option::Some(crate::RpcDataSlice { offset: 4, length: 8 }),
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Confirmed),
min_context_slot: std::option::Option::Some(99),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params.len(), 2);
assert_eq!(params[0].as_array().map(std::vec::Vec::len), std::option::Option::Some(2));
assert_eq!(params[1]["encoding"], serde_json::Value::String("base64".to_string()));
assert_eq!(params[1]["minContextSlot"], serde_json::Value::from(99_u64));
}
#[test]
fn multiple_accounts_enforces_official_request_bound() {
let request = crate::GetMultipleAccountsRequest {
addresses: (0_u8..101_u8).map(pubkey).collect(),
config: std::option::Option::None,
};
assert!(crate::StandardHttpRequest::params(&request).is_err());
let empty = crate::GetMultipleAccountsRequest {
addresses: std::vec::Vec::new(),
config: std::option::Option::None,
};
let empty_params = match crate::StandardHttpRequest::params(&empty) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("empty params failed: {error}"),
};
assert_eq!(empty_params, std::vec![serde_json::json!([])]);
}
#[test]
fn program_accounts_preserves_filters_and_context_switch() {
let request = crate::GetProgramAccountsRequest {
program_id: pubkey(7),
config: std::option::Option::Some(crate::RpcProgramAccountsConfig {
filters: std::option::Option::Some(std::vec![
crate::RpcProgramAccountFilter::DataSize(165),
crate::RpcProgramAccountFilter::TokenAccountState,
]),
encoding: std::option::Option::Some(crate::RpcAccountEncoding::JsonParsed),
data_slice: std::option::Option::None,
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
min_context_slot: std::option::Option::None,
with_context: std::option::Option::Some(true),
sort_results: std::option::Option::Some(true),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1]["withContext"], serde_json::Value::Bool(true));
assert_eq!(params[1]["sortResults"], serde_json::Value::Bool(true));
assert_eq!(
params[1]["filters"][1],
serde_json::Value::String("tokenAccountState".to_string())
);
}
}

View File

@@ -0,0 +1,548 @@
// file: kb_rpc/src/standard_http_blocks.rs
// version: 1
//! Configurable standard block and ledger Solana HTTP JSON-RPC requests.
/// Options accepted by `getBlock`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockConfig {
/// Optional transaction encoding.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub encoding: std::option::Option<crate::RpcTransactionEncoding>,
/// Optional transaction detail level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub transaction_details: std::option::Option<crate::RpcTransactionDetails>,
/// Whether rewards must be included.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub rewards: std::option::Option<bool>,
/// Optional confirmed or finalized commitment.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Highest transaction version the caller can decode.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub max_supported_transaction_version: std::option::Option<u8>,
}
impl crate::RpcBlockConfig {
/// Rejects the processed commitment unsupported by block-history methods.
pub fn validate(&self) -> kb_core::Result<()> {
if self.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed) {
return std::result::Result::Err(kb_core::Error::config(
"getBlock does not support processed commitment",
));
}
return std::result::Result::Ok(());
}
}
/// Inclusive slot range accepted by `getBlockProduction`.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockProductionConfigRange {
/// First slot included in the range.
pub first_slot: u64,
/// Optional final slot included in the range.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub last_slot: std::option::Option<u64>,
}
/// Options accepted by `getBlockProduction`.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockProductionConfig {
/// Optional validator identity filter.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub identity: std::option::Option<std::string::String>,
/// Optional inclusive slot range.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub range: std::option::Option<crate::RpcBlockProductionConfigRange>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
impl crate::RpcBlockProductionConfig {
/// Validates identity and range ordering.
pub fn validate(&self) -> kb_core::Result<()> {
if let std::option::Option::Some(identity) = &self.identity {
let identity_result =
crate::validate_solana_pubkey_text(identity, "getBlockProduction identity");
if let std::result::Result::Err(error) = identity_result {
return std::result::Result::Err(error);
}
}
if let std::option::Option::Some(range) = self.range {
if let std::option::Option::Some(last_slot) = range.last_slot {
if last_slot < range.first_slot {
return std::result::Result::Err(kb_core::Error::config(
"getBlockProduction last slot must not precede first slot",
));
}
}
}
return std::result::Result::Ok(());
}
}
/// Reward category attached to a block reward entry.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcRewardType {
/// Transaction fee reward.
Fee,
/// Rent reward.
Rent,
/// Staking reward.
Staking,
/// Vote reward.
Voting,
}
/// One reward entry returned with a block.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcReward {
/// Recipient public key.
pub pubkey: std::string::String,
/// Signed lamport balance change.
pub lamports: i64,
/// Recipient balance after the reward.
pub post_balance: u64,
/// Optional reward category.
pub reward_type: std::option::Option<crate::RpcRewardType>,
/// Optional validator commission percentage.
pub commission: std::option::Option<u8>,
/// Optional validator commission in basis points.
#[serde(default, skip_serializing_if = "std::option::Option::is_none")]
pub commission_bps: std::option::Option<u16>,
}
/// Encoding-dependent confirmed block returned by `getBlock`.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcConfirmedBlock {
/// Block hash.
pub blockhash: std::string::String,
/// Previous block hash.
pub previous_blockhash: std::string::String,
/// Parent slot.
pub parent_slot: u64,
/// Encoding-dependent transaction entries when requested.
#[serde(default)]
pub transactions: std::option::Option<std::vec::Vec<serde_json::Value>>,
/// Signature list when signature-only details are requested.
#[serde(default)]
pub signatures: std::option::Option<std::vec::Vec<std::string::String>>,
/// Rewards when requested.
#[serde(default)]
pub rewards: std::option::Option<std::vec::Vec<crate::RpcReward>>,
/// Unix block time when available.
#[serde(default)]
pub block_time: std::option::Option<i64>,
/// Block height when available.
#[serde(default)]
pub block_height: std::option::Option<u64>,
/// Number of reward partitions when available.
#[serde(default)]
pub num_reward_partitions: std::option::Option<u64>,
}
/// Stake commitment information returned for one block.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockCommitment {
/// Commitment stake by lockout depth, or `None` when unavailable.
pub commitment: std::option::Option<std::vec::Vec<u64>>,
/// Total active stake used for the commitment calculation.
pub total_stake: u64,
}
/// Actual slot range represented by a block-production response.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockProductionRange {
/// First represented slot.
pub first_slot: u64,
/// Last represented slot.
pub last_slot: u64,
}
/// Block-production counts grouped by validator identity.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockProduction {
/// Validator identity to `(leader slots, blocks produced)` map.
pub by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
/// Actual represented slot range.
pub range: crate::RpcBlockProductionRange,
}
/// Recent cluster performance sample.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcPerformanceSample {
/// Slot at the end of the sample window.
pub slot: u64,
/// Total transactions processed during the sample.
pub num_transactions: u64,
/// Optional count excluding vote transactions.
#[serde(default)]
pub num_non_vote_transactions: std::option::Option<u64>,
/// Slots processed during the sample.
pub num_slots: u64,
/// Sample period in seconds.
pub sample_period_secs: u16,
}
/// Typed `getBlock` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBlockRequest {
/// Block slot.
pub slot: u64,
/// Optional encoding, details, rewards, commitment and version options.
pub config: std::option::Option<crate::RpcBlockConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlockRequest {
type Response = std::option::Option<crate::RpcConfirmedBlock>;
const METHOD: &'static str = "getBlock";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let mut params = std::vec![serde_json::Value::from(self.slot)];
if let std::option::Option::Some(config) = &self.config {
let config_result = config.validate();
if let std::result::Result::Err(error) = config_result {
return std::result::Result::Err(error);
}
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getBlockCommitment` request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GetBlockCommitmentRequest {
/// Block slot.
pub slot: u64,
}
impl crate::StandardHttpRequest for crate::GetBlockCommitmentRequest {
type Response = crate::RpcBlockCommitment;
const METHOD: &'static str = "getBlockCommitment";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec![serde_json::Value::from(self.slot)]);
}
}
/// Typed `getBlockProduction` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetBlockProductionRequest {
/// Optional identity, range and commitment options.
pub config: std::option::Option<crate::RpcBlockProductionConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlockProductionRequest {
type Response = crate::RpcResponse<crate::RpcBlockProduction>;
const METHOD: &'static str = "getBlockProduction";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let validation_result = config.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getBlocks` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBlocksRequest {
/// First slot included in the scan.
pub start_slot: u64,
/// Optional final slot included in the scan.
pub end_slot: std::option::Option<u64>,
/// Optional confirmed or finalized context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlocksRequest {
type Response = std::vec::Vec<u64>;
const METHOD: &'static str = "getBlocks";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(end_slot) = self.end_slot {
if end_slot >= self.start_slot
&& end_slot.saturating_sub(self.start_slot) > crate::constants::MAX_BLOCK_RANGE
{
return std::result::Result::Err(kb_core::Error::config(format!(
"getBlocks range must not exceed {} slots",
crate::constants::MAX_BLOCK_RANGE
)));
}
}
if let std::option::Option::Some(config) = &self.config {
if config.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed)
{
return std::result::Result::Err(kb_core::Error::config(
"getBlocks does not support processed commitment",
));
}
}
let mut params = std::vec![serde_json::Value::from(self.start_slot)];
if let std::option::Option::Some(end_slot) = self.end_slot {
params.push(serde_json::Value::from(end_slot));
}
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getBlocksWithLimit` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBlocksWithLimitRequest {
/// First slot considered by the scan.
pub start_slot: u64,
/// Maximum number of block slots returned.
pub limit: u64,
/// Optional confirmed or finalized context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlocksWithLimitRequest {
type Response = std::vec::Vec<u64>;
const METHOD: &'static str = "getBlocksWithLimit";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if self.limit > crate::constants::MAX_BLOCK_RANGE {
return std::result::Result::Err(kb_core::Error::config(format!(
"getBlocksWithLimit limit must not exceed {}",
crate::constants::MAX_BLOCK_RANGE
)));
}
if let std::option::Option::Some(config) = &self.config {
if config.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed)
{
return std::result::Result::Err(kb_core::Error::config(
"getBlocksWithLimit does not support processed commitment",
));
}
}
let mut params = std::vec![
serde_json::Value::from(self.start_slot),
serde_json::Value::from(self.limit),
];
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getBlockTime` request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GetBlockTimeRequest {
/// Block slot.
pub slot: u64,
}
impl crate::StandardHttpRequest for crate::GetBlockTimeRequest {
type Response = std::option::Option<i64>;
const METHOD: &'static str = "getBlockTime";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec![serde_json::Value::from(self.slot)]);
}
}
/// Typed `getFirstAvailableBlock` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetFirstAvailableBlockRequest;
impl crate::StandardHttpRequest for crate::GetFirstAvailableBlockRequest {
type Response = u64;
const METHOD: &'static str = "getFirstAvailableBlock";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getRecentPerformanceSamples` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetRecentPerformanceSamplesRequest {
/// Optional sample count. Absence delegates the default to the endpoint.
pub limit: std::option::Option<usize>,
}
impl crate::StandardHttpRequest for crate::GetRecentPerformanceSamplesRequest {
type Response = std::vec::Vec<crate::RpcPerformanceSample>;
const METHOD: &'static str = "getRecentPerformanceSamples";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(limit) = self.limit {
if limit > crate::constants::MAX_PERFORMANCE_SAMPLE_COUNT {
return std::result::Result::Err(kb_core::Error::config(format!(
"getRecentPerformanceSamples limit must not exceed {}",
crate::constants::MAX_PERFORMANCE_SAMPLE_COUNT
)));
}
return std::result::Result::Ok(std::vec![serde_json::Value::from(limit)]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `minimumLedgerSlot` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct MinimumLedgerSlotRequest;
impl crate::StandardHttpRequest for crate::MinimumLedgerSlotRequest {
type Response = u64;
const METHOD: &'static str = "minimumLedgerSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
#[cfg(test)]
mod tests {
#[test]
fn block_options_remain_independently_selectable() {
let request = crate::GetBlockRequest {
slot: 55,
config: std::option::Option::Some(crate::RpcBlockConfig {
encoding: std::option::Option::Some(crate::RpcTransactionEncoding::JsonParsed),
transaction_details: std::option::Option::Some(
crate::RpcTransactionDetails::Accounts,
),
rewards: std::option::Option::Some(false),
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Confirmed),
max_supported_transaction_version: std::option::Option::Some(0),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1]["encoding"], serde_json::Value::String("jsonParsed".to_string()));
assert_eq!(
params[1]["transactionDetails"],
serde_json::Value::String("accounts".to_string())
);
assert_eq!(params[1]["rewards"], serde_json::Value::Bool(false));
assert_eq!(params[1]["maxSupportedTransactionVersion"], serde_json::Value::from(0_u64));
let reward = match serde_json::from_value::<crate::RpcReward>(serde_json::json!({
"pubkey": "validator",
"lamports": 42,
"postBalance": 84,
"rewardType": "voting",
"commission": 5,
"commissionBps": 550
})) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("reward parsing failed: {error}"),
};
assert_eq!(reward.commission, std::option::Option::Some(5));
assert_eq!(reward.commission_bps, std::option::Option::Some(550));
}
#[test]
fn blocks_without_end_slot_places_config_in_second_position() {
let request = crate::GetBlocksRequest {
start_slot: 10,
end_slot: std::option::Option::None,
config: std::option::Option::Some(crate::RpcContextConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
min_context_slot: std::option::Option::Some(9),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params.len(), 2);
assert_eq!(params[1]["commitment"], serde_json::Value::String("finalized".to_string()));
}
#[test]
fn block_ranges_and_performance_samples_are_bounded() {
let range = crate::GetBlocksRequest {
start_slot: 0,
end_slot: std::option::Option::Some(500_001),
config: std::option::Option::None,
};
let samples =
crate::GetRecentPerformanceSamplesRequest { limit: std::option::Option::Some(721) };
assert!(crate::StandardHttpRequest::params(&range).is_err());
assert!(crate::StandardHttpRequest::params(&samples).is_err());
let reversed_range = crate::GetBlocksRequest {
start_slot: 10,
end_slot: std::option::Option::Some(9),
config: std::option::Option::None,
};
let zero_blocks = crate::GetBlocksWithLimitRequest {
start_slot: 10,
limit: 0,
config: std::option::Option::None,
};
let zero_samples =
crate::GetRecentPerformanceSamplesRequest { limit: std::option::Option::Some(0) };
let reversed_params = match crate::StandardHttpRequest::params(&reversed_range) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("reversed range failed: {error}"),
};
let zero_block_params = match crate::StandardHttpRequest::params(&zero_blocks) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("zero block limit failed: {error}"),
};
let zero_sample_params = match crate::StandardHttpRequest::params(&zero_samples) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("zero sample limit failed: {error}"),
};
assert_eq!(
reversed_params,
std::vec![serde_json::Value::from(10_u64), serde_json::Value::from(9_u64),]
);
assert_eq!(
zero_block_params,
std::vec![serde_json::Value::from(10_u64), serde_json::Value::from(0_u64),]
);
assert_eq!(zero_sample_params, std::vec![serde_json::Value::from(0_u64)]);
}
}

View File

@@ -0,0 +1,505 @@
// file: kb_rpc/src/standard_http_cluster.rs
// version: 1
//! Configurable standard cluster-oriented Solana HTTP JSON-RPC requests.
/// Contact information returned for one cluster node.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcContactInfo {
/// Node identity public key.
pub pubkey: std::string::String,
/// Gossip socket address.
pub gossip: std::option::Option<std::string::String>,
/// TVU UDP socket address.
pub tvu: std::option::Option<std::string::String>,
/// TPU UDP socket address.
pub tpu: std::option::Option<std::string::String>,
/// TPU QUIC socket address.
pub tpu_quic: std::option::Option<std::string::String>,
/// TPU forwarding UDP socket address.
pub tpu_forwards: std::option::Option<std::string::String>,
/// TPU forwarding QUIC socket address.
pub tpu_forwards_quic: std::option::Option<std::string::String>,
/// TPU vote socket address.
pub tpu_vote: std::option::Option<std::string::String>,
/// Repair service socket address.
pub serve_repair: std::option::Option<std::string::String>,
/// JSON-RPC socket address.
pub rpc: std::option::Option<std::string::String>,
/// PubSub socket address.
pub pubsub: std::option::Option<std::string::String>,
/// Validator software version.
pub version: std::option::Option<std::string::String>,
/// Validator client identifier.
pub client_id: std::option::Option<std::string::String>,
/// Feature-set identifier prefix.
pub feature_set: std::option::Option<u32>,
/// Shred version.
pub shred_version: std::option::Option<u16>,
}
/// Epoch schedule derived from the cluster genesis configuration.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcEpochSchedule {
/// Slots in a normal epoch.
pub slots_per_epoch: u64,
/// Leader-schedule offset in slots.
pub leader_schedule_slot_offset: u64,
/// Whether warmup epochs are enabled.
pub warmup: bool,
/// First epoch using the normal slot count.
pub first_normal_epoch: u64,
/// First slot of the first normal epoch.
pub first_normal_slot: u64,
}
/// Highest complete and incremental snapshot slots available from a node.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcSnapshotSlotInfo {
/// Highest full snapshot slot.
pub full: u64,
/// Highest incremental snapshot slot when available.
pub incremental: std::option::Option<u64>,
}
/// Node identity response.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcIdentity {
/// Node identity public key.
pub identity: std::string::String,
}
/// Options accepted by `getLeaderSchedule`.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcLeaderScheduleConfig {
/// Optional validator identity whose schedule is requested.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub identity: std::option::Option<std::string::String>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
impl crate::RpcLeaderScheduleConfig {
/// Validates the optional identity public key.
pub fn validate(&self) -> kb_core::Result<()> {
if let std::option::Option::Some(identity) = &self.identity {
return crate::validate_solana_pubkey_text(identity, "getLeaderSchedule identity");
}
return std::result::Result::Ok(());
}
}
/// Leader schedule keyed by validator identity.
pub type RpcLeaderSchedule = std::collections::BTreeMap<std::string::String, std::vec::Vec<usize>>;
/// Validator software version information.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RpcVersionInfo {
/// Validator software version.
pub solana_core: std::string::String,
/// Feature-set identifier prefix.
pub feature_set: std::option::Option<u32>,
}
/// Options accepted by `getVoteAccounts`.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcGetVoteAccountsConfig {
/// Optional vote account public key filter.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub vote_pubkey: std::option::Option<std::string::String>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Whether unstaked delinquent validators must be retained.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub keep_unstaked_delinquents: std::option::Option<bool>,
/// Optional delinquency threshold in slots.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub delinquent_slot_distance: std::option::Option<u64>,
}
impl crate::RpcGetVoteAccountsConfig {
/// Validates the optional vote account public key.
pub fn validate(&self) -> kb_core::Result<()> {
if let std::option::Option::Some(vote_pubkey) = &self.vote_pubkey {
return crate::validate_solana_pubkey_text(vote_pubkey, "getVoteAccounts vote account");
}
return std::result::Result::Ok(());
}
}
/// Vote account information returned by `getVoteAccounts`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcVoteAccountInfo {
/// Vote account public key.
pub vote_pubkey: std::string::String,
/// Validator identity public key.
pub node_pubkey: std::string::String,
/// Activated stake in lamports.
pub activated_stake: u64,
/// Vote commission percentage.
pub commission: u8,
/// Vote inflation-reward commission in basis points when exposed by the node.
#[serde(default, skip_serializing_if = "std::option::Option::is_none")]
pub inflation_rewards_commission_bps: std::option::Option<u16>,
/// Whether the vote account is staked in the current epoch.
pub epoch_vote_account: bool,
/// `(epoch, credits, previous credits)` history.
pub epoch_credits: std::vec::Vec<(u64, u64, u64)>,
/// Most recent voted slot.
pub last_vote: u64,
/// Current root slot.
pub root_slot: u64,
}
/// Current and delinquent validator vote accounts.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcVoteAccountStatus {
/// Current validator vote accounts.
pub current: std::vec::Vec<crate::RpcVoteAccountInfo>,
/// Delinquent validator vote accounts.
pub delinquent: std::vec::Vec<crate::RpcVoteAccountInfo>,
}
/// Typed `getClusterNodes` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetClusterNodesRequest;
impl crate::StandardHttpRequest for crate::GetClusterNodesRequest {
type Response = std::vec::Vec<crate::RpcContactInfo>;
const METHOD: &'static str = "getClusterNodes";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getEpochSchedule` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetEpochScheduleRequest;
impl crate::StandardHttpRequest for crate::GetEpochScheduleRequest {
type Response = crate::RpcEpochSchedule;
const METHOD: &'static str = "getEpochSchedule";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getHealth` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetHealthRequest;
impl crate::StandardHttpRequest for crate::GetHealthRequest {
type Response = std::string::String;
const METHOD: &'static str = "getHealth";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getHighestSnapshotSlot` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetHighestSnapshotSlotRequest;
impl crate::StandardHttpRequest for crate::GetHighestSnapshotSlotRequest {
type Response = crate::RpcSnapshotSlotInfo;
const METHOD: &'static str = "getHighestSnapshotSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getIdentity` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetIdentityRequest;
impl crate::StandardHttpRequest for crate::GetIdentityRequest {
type Response = crate::RpcIdentity;
const METHOD: &'static str = "getIdentity";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getLeaderSchedule` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetLeaderScheduleRequest {
/// Optional slot selecting the epoch whose schedule is requested.
pub slot: std::option::Option<u64>,
/// Optional identity and commitment options.
pub config: std::option::Option<crate::RpcLeaderScheduleConfig>,
}
impl crate::StandardHttpRequest for crate::GetLeaderScheduleRequest {
type Response = std::option::Option<crate::RpcLeaderSchedule>;
const METHOD: &'static str = "getLeaderSchedule";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let validation_result = config.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
let mut params = std::vec::Vec::new();
if let std::option::Option::Some(slot) = self.slot {
params.push(serde_json::Value::from(slot));
} else if self.config.is_some() {
params.push(serde_json::Value::Null);
}
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getMaxRetransmitSlot` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetMaxRetransmitSlotRequest;
impl crate::StandardHttpRequest for crate::GetMaxRetransmitSlotRequest {
type Response = u64;
const METHOD: &'static str = "getMaxRetransmitSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getMaxShredInsertSlot` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetMaxShredInsertSlotRequest;
impl crate::StandardHttpRequest for crate::GetMaxShredInsertSlotRequest {
type Response = u64;
const METHOD: &'static str = "getMaxShredInsertSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getSlot` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetSlotRequest {
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetSlotRequest {
type Response = u64;
const METHOD: &'static str = "getSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getSlotLeader` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetSlotLeaderRequest {
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetSlotLeaderRequest {
type Response = std::string::String;
const METHOD: &'static str = "getSlotLeader";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getSlotLeaders` request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GetSlotLeadersRequest {
/// First slot whose leader is requested.
pub start_slot: u64,
/// Number of consecutive leaders requested.
pub limit: u64,
}
impl crate::StandardHttpRequest for crate::GetSlotLeadersRequest {
type Response = std::vec::Vec<std::string::String>;
const METHOD: &'static str = "getSlotLeaders";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if self.limit == 0 || self.limit > crate::constants::MAX_SLOT_LEADER_COUNT {
return std::result::Result::Err(kb_core::Error::config(format!(
"getSlotLeaders limit must be between 1 and {}",
crate::constants::MAX_SLOT_LEADER_COUNT
)));
}
return std::result::Result::Ok(std::vec![
serde_json::Value::from(self.start_slot),
serde_json::Value::from(self.limit),
]);
}
}
/// Typed `getVersion` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetVersionRequest;
impl crate::StandardHttpRequest for crate::GetVersionRequest {
type Response = crate::RpcVersionInfo;
const METHOD: &'static str = "getVersion";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getVoteAccounts` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetVoteAccountsRequest {
/// Optional vote-account, commitment and delinquency options.
pub config: std::option::Option<crate::RpcGetVoteAccountsConfig>,
}
impl crate::StandardHttpRequest for crate::GetVoteAccountsRequest {
type Response = crate::RpcVoteAccountStatus;
const METHOD: &'static str = "getVoteAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let validation_result = config.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn leader_schedule_uses_null_slot_placeholder_when_only_config_is_selected() {
let request = crate::GetLeaderScheduleRequest {
slot: std::option::Option::None,
config: std::option::Option::Some(crate::RpcLeaderScheduleConfig {
identity: std::option::Option::Some(pubkey(1)),
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Processed),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[0], serde_json::Value::Null);
assert_eq!(params[1]["identity"], serde_json::Value::String(pubkey(1)));
let maximum = crate::GetSlotLeadersRequest {
start_slot: 1,
limit: crate::constants::MAX_SLOT_LEADER_COUNT,
};
let too_many = crate::GetSlotLeadersRequest {
start_slot: 1,
limit: crate::constants::MAX_SLOT_LEADER_COUNT + 1,
};
assert!(crate::StandardHttpRequest::params(&maximum).is_ok());
assert!(crate::StandardHttpRequest::params(&too_many).is_err());
}
#[test]
fn vote_account_options_are_independently_selectable() {
let request = crate::GetVoteAccountsRequest {
config: std::option::Option::Some(crate::RpcGetVoteAccountsConfig {
vote_pubkey: std::option::Option::Some(pubkey(2)),
commitment: std::option::Option::None,
keep_unstaked_delinquents: std::option::Option::Some(true),
delinquent_slot_distance: std::option::Option::Some(512),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert!(params[0].get("commitment").is_none());
assert_eq!(params[0]["keepUnstakedDelinquents"], serde_json::Value::Bool(true));
assert_eq!(params[0]["delinquentSlotDistance"], serde_json::Value::from(512_u64));
let vote_account =
match serde_json::from_value::<crate::RpcVoteAccountInfo>(serde_json::json!({
"votePubkey": pubkey(3),
"nodePubkey": pubkey(4),
"activatedStake": 1,
"commission": 5,
"inflationRewardsCommissionBps": 525,
"epochVoteAccount": true,
"epochCredits": [[1, 2, 1]],
"lastVote": 8,
"rootSlot": 7
})) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("vote account parsing failed: {error}"),
};
assert_eq!(vote_account.inflation_rewards_commission_bps, std::option::Option::Some(525));
}
#[test]
fn version_response_accepts_kebab_case_wire_fields() {
let value = serde_json::json!({ "solana-core": "4.0.0", "feature-set": 123 });
let parsed = match serde_json::from_value::<crate::RpcVersionInfo>(value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("version parsing failed: {error}"),
};
assert_eq!(parsed.solana_core, "4.0.0");
assert_eq!(parsed.feature_set, std::option::Option::Some(123));
}
}

View File

@@ -0,0 +1,309 @@
// file: kb_rpc/src/standard_http_economics.rs
// version: 1
//! Configurable standard inflation, supply and stake-economics HTTP requests.
/// Inflation governor parameters.
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationGovernor {
/// Initial inflation rate.
pub initial: f64,
/// Terminal inflation rate.
pub terminal: f64,
/// Annual taper rate.
pub taper: f64,
/// Foundation allocation rate.
pub foundation: f64,
/// Foundation allocation term in years.
pub foundation_term: f64,
}
/// Inflation rates for the current epoch.
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationRate {
/// Total inflation rate.
pub total: f64,
/// Validator inflation rate.
pub validator: f64,
/// Foundation inflation rate.
pub foundation: f64,
/// Epoch represented by the rates.
pub epoch: u64,
}
/// Inflation reward credited to one requested address.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationReward {
/// Reward epoch.
pub epoch: u64,
/// First effective slot of the rewarded epoch.
pub effective_slot: u64,
/// Reward amount in lamports.
pub amount: u64,
/// Account balance after the reward.
pub post_balance: u64,
/// Legacy vote commission percentage when applicable.
pub commission: std::option::Option<u8>,
/// Vote commission in basis points when exposed by the node.
#[serde(default, skip_serializing_if = "std::option::Option::is_none")]
pub commission_bps: std::option::Option<u16>,
}
/// Current lamport supply breakdown.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcSupply {
/// Total lamport supply.
pub total: u64,
/// Circulating lamport supply.
pub circulating: u64,
/// Non-circulating lamport supply.
pub non_circulating: u64,
/// Non-circulating account list when requested.
#[serde(default)]
pub non_circulating_accounts: std::vec::Vec<std::string::String>,
}
/// Optional commitment accepted by `getInflationGovernor`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationGovernorConfig {
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
/// Epoch and contextual options accepted by `getInflationReward`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcEpochConfig {
/// Optional reward epoch.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub epoch: std::option::Option<u64>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional minimum context slot.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub min_context_slot: std::option::Option<u64>,
}
/// Options accepted by `getSupply`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcSupplyConfig {
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional omission of the potentially large non-circulating account list.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub exclude_non_circulating_accounts_list: std::option::Option<bool>,
}
/// Typed `getInflationGovernor` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetInflationGovernorRequest {
/// Optional commitment configuration.
pub config: std::option::Option<crate::RpcInflationGovernorConfig>,
}
impl crate::StandardHttpRequest for crate::GetInflationGovernorRequest {
type Response = crate::RpcInflationGovernor;
const METHOD: &'static str = "getInflationGovernor";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getInflationRate` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetInflationRateRequest;
impl crate::StandardHttpRequest for crate::GetInflationRateRequest {
type Response = crate::RpcInflationRate;
const METHOD: &'static str = "getInflationRate";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getInflationReward` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetInflationRewardRequest {
/// Account public keys whose rewards are requested, in response order.
pub addresses: std::vec::Vec<std::string::String>,
/// Optional epoch, commitment and minimum-context options.
pub config: std::option::Option<crate::RpcEpochConfig>,
}
impl crate::StandardHttpRequest for crate::GetInflationRewardRequest {
type Response = std::vec::Vec<std::option::Option<crate::RpcInflationReward>>;
const METHOD: &'static str = "getInflationReward";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result = crate::standard_http::validate_pubkey_list(
&self.addresses,
"getInflationReward address",
usize::MAX,
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let addresses =
match crate::standard_http::serialize_parameter(Self::METHOD, &self.addresses) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut params = std::vec![addresses];
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getStakeMinimumDelegation` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetStakeMinimumDelegationRequest {
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetStakeMinimumDelegationRequest {
type Response = crate::RpcResponse<u64>;
const METHOD: &'static str = "getStakeMinimumDelegation";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getSupply` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetSupplyRequest {
/// Optional commitment and non-circulating-list options.
pub config: std::option::Option<crate::RpcSupplyConfig>,
}
impl crate::StandardHttpRequest for crate::GetSupplyRequest {
type Response = crate::RpcResponse<crate::RpcSupply>;
const METHOD: &'static str = "getSupply";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn supply_distinguishes_omitted_and_explicit_false_option() {
let omitted = crate::GetSupplyRequest {
config: std::option::Option::Some(crate::RpcSupplyConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
exclude_non_circulating_accounts_list: std::option::Option::None,
}),
};
let explicit = crate::GetSupplyRequest {
config: std::option::Option::Some(crate::RpcSupplyConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
exclude_non_circulating_accounts_list: std::option::Option::Some(false),
}),
};
let omitted_params = match crate::StandardHttpRequest::params(&omitted) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
let explicit_params = match crate::StandardHttpRequest::params(&explicit) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert!(omitted_params[0].get("excludeNonCirculatingAccountsList").is_none());
assert_eq!(
explicit_params[0]["excludeNonCirculatingAccountsList"],
serde_json::Value::Bool(false)
);
}
#[test]
fn inflation_reward_preserves_epoch_commitment_and_minimum_context() {
let request = crate::GetInflationRewardRequest {
addresses: std::vec![pubkey(1), pubkey(2)],
config: std::option::Option::Some(crate::RpcEpochConfig {
epoch: std::option::Option::Some(44),
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Confirmed),
min_context_slot: std::option::Option::Some(99),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1]["epoch"], serde_json::Value::from(44_u64));
assert_eq!(params[1]["commitment"], serde_json::Value::String("confirmed".to_string()));
assert_eq!(params[1]["minContextSlot"], serde_json::Value::from(99_u64));
let reward = match serde_json::from_value::<crate::RpcInflationReward>(serde_json::json!({
"epoch": 44,
"effectiveSlot": 100,
"amount": 200,
"postBalance": 300,
"commission": 5,
"commissionBps": 575
})) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("inflation reward parsing failed: {error}"),
};
assert_eq!(reward.commission, std::option::Option::Some(5));
assert_eq!(reward.commission_bps, std::option::Option::Some(575));
let empty = crate::GetInflationRewardRequest {
addresses: std::vec::Vec::new(),
config: std::option::Option::None,
};
let empty_params = match crate::StandardHttpRequest::params(&empty) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("empty params failed: {error}"),
};
assert_eq!(empty_params, std::vec![serde_json::json!([])]);
}
}

View File

@@ -0,0 +1,241 @@
// file: kb_rpc/src/standard_http_tokens.rs
// version: 1
//! Configurable standard SPL Token-oriented Solana HTTP JSON-RPC requests.
fn pubkey_with_optional_commitment_params(
method: &str,
address: &str,
field: &str,
config: &std::option::Option<crate::RpcCommitmentConfig>,
) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result = crate::validate_solana_pubkey_text(address, field);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut params = std::vec![serde_json::Value::String(address.to_string())];
if let std::option::Option::Some(config) = config {
let config_value = match crate::standard_http::serialize_parameter(method, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(config_value);
}
return std::result::Result::Ok(params);
}
fn token_accounts_query_params(
method: &str,
authority: &str,
field: &str,
filter: &crate::RpcTokenAccountsFilter,
config: &std::option::Option<crate::RpcAccountInfoConfig>,
) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let authority_result = crate::validate_solana_pubkey_text(authority, field);
if let std::result::Result::Err(error) = authority_result {
return std::result::Result::Err(error);
}
let filter_result = filter.validate();
if let std::result::Result::Err(error) = filter_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(config) = config {
let config_result = config.validate();
if let std::result::Result::Err(error) = config_result {
return std::result::Result::Err(error);
}
}
let filter_value = match crate::standard_http::serialize_parameter(method, filter) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut params = std::vec![serde_json::Value::String(authority.to_string()), filter_value,];
if let std::option::Option::Some(config) = config {
let config_value = match crate::standard_http::serialize_parameter(method, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(config_value);
}
return std::result::Result::Ok(params);
}
/// Typed `getTokenAccountBalance` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenAccountBalanceRequest {
/// Token account public key.
pub address: std::string::String,
/// Optional commitment option.
pub config: std::option::Option<crate::RpcCommitmentConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenAccountBalanceRequest {
type Response = crate::RpcResponse<crate::RpcTokenAmount>;
const METHOD: &'static str = "getTokenAccountBalance";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::pubkey_with_optional_commitment_params(
Self::METHOD,
&self.address,
"getTokenAccountBalance address",
&self.config,
);
}
}
/// Typed `getTokenAccountsByDelegate` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenAccountsByDelegateRequest {
/// Delegate public key.
pub delegate: std::string::String,
/// Mint or Token Program selector.
pub filter: crate::RpcTokenAccountsFilter,
/// Optional account representation and context options.
pub config: std::option::Option<crate::RpcAccountInfoConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenAccountsByDelegateRequest {
type Response = crate::RpcResponse<std::vec::Vec<crate::RpcKeyedAccount>>;
const METHOD: &'static str = "getTokenAccountsByDelegate";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::token_accounts_query_params(
Self::METHOD,
&self.delegate,
"getTokenAccountsByDelegate delegate",
&self.filter,
&self.config,
);
}
}
/// Typed `getTokenAccountsByOwner` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenAccountsByOwnerRequest {
/// Owner public key.
pub owner: std::string::String,
/// Mint or Token Program selector.
pub filter: crate::RpcTokenAccountsFilter,
/// Optional account representation and context options.
pub config: std::option::Option<crate::RpcAccountInfoConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenAccountsByOwnerRequest {
type Response = crate::RpcResponse<std::vec::Vec<crate::RpcKeyedAccount>>;
const METHOD: &'static str = "getTokenAccountsByOwner";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::token_accounts_query_params(
Self::METHOD,
&self.owner,
"getTokenAccountsByOwner owner",
&self.filter,
&self.config,
);
}
}
/// Typed `getTokenLargestAccounts` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenLargestAccountsRequest {
/// Mint public key.
pub mint: std::string::String,
/// Optional commitment option.
pub config: std::option::Option<crate::RpcCommitmentConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenLargestAccountsRequest {
type Response = crate::RpcResponse<std::vec::Vec<crate::RpcTokenAccountBalance>>;
const METHOD: &'static str = "getTokenLargestAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::pubkey_with_optional_commitment_params(
Self::METHOD,
&self.mint,
"getTokenLargestAccounts mint",
&self.config,
);
}
}
/// Typed `getTokenSupply` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenSupplyRequest {
/// Mint public key.
pub mint: std::string::String,
/// Optional commitment option.
pub config: std::option::Option<crate::RpcCommitmentConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenSupplyRequest {
type Response = crate::RpcResponse<crate::RpcTokenAmount>;
const METHOD: &'static str = "getTokenSupply";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::pubkey_with_optional_commitment_params(
Self::METHOD,
&self.mint,
"getTokenSupply mint",
&self.config,
);
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn token_owner_query_preserves_filter_and_independent_account_options() {
let request = crate::GetTokenAccountsByOwnerRequest {
owner: pubkey(1),
filter: crate::RpcTokenAccountsFilter::ProgramId(pubkey(2)),
config: std::option::Option::Some(crate::RpcAccountInfoConfig {
encoding: std::option::Option::Some(crate::RpcAccountEncoding::Base64Zstd),
data_slice: std::option::Option::None,
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Processed),
min_context_slot: std::option::Option::Some(42),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1], serde_json::json!({ "programId": pubkey(2) }));
assert_eq!(params[2]["encoding"], serde_json::Value::String("base64+zstd".to_string()));
assert_eq!(params[2]["commitment"], serde_json::Value::String("processed".to_string()));
}
#[test]
fn token_balance_omits_unselected_options() {
let request = crate::GetTokenAccountBalanceRequest {
address: pubkey(3),
config: std::option::Option::None,
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params, std::vec![serde_json::Value::String(pubkey(3))]);
let configured = crate::GetTokenSupplyRequest {
mint: pubkey(4),
config: std::option::Option::Some(crate::RpcCommitmentConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
}),
};
let configured_params = match crate::StandardHttpRequest::params(&configured) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(configured_params[1], serde_json::json!({ "commitment": "finalized" }));
assert!(configured_params[1].get("minContextSlot").is_none());
}
}

View File

@@ -0,0 +1,155 @@
// file: kb_rpc/src/standard_http_transactions.rs
// version: 2
//! Configurable standard transaction-oriented Solana HTTP JSON-RPC requests.
/// Prioritization fee observed for one recent slot.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcPrioritizationFee {
/// Slot from which the fee sample was retained.
pub slot: u64,
/// Minimum compute-unit price in micro-lamports for the requested writable set.
pub prioritization_fee: u64,
}
/// Typed `getRecentPrioritizationFees` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetRecentPrioritizationFeesRequest {
/// Optional writable account set. `None` omits the parameter; `Some([])` sends an explicit empty set.
pub locked_writable_accounts: std::option::Option<std::vec::Vec<std::string::String>>,
}
impl crate::StandardHttpRequest for crate::GetRecentPrioritizationFeesRequest {
type Response = std::vec::Vec<crate::RpcPrioritizationFee>;
const METHOD: &'static str = "getRecentPrioritizationFees";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(accounts) = &self.locked_writable_accounts {
let validation_result = crate::standard_http::validate_pubkey_list(
accounts,
"getRecentPrioritizationFees writable account",
crate::constants::MAX_PRIORITIZATION_FEE_ACCOUNT_COUNT,
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let value = match crate::standard_http::serialize_parameter(Self::METHOD, accounts) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getTransactionCount` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetTransactionCountRequest {
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetTransactionCountRequest {
type Response = u64;
const METHOD: &'static str = "getTransactionCount";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `isBlockhashValid` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IsBlockhashValidRequest {
/// Base58 blockhash being checked.
pub blockhash: std::string::String,
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::IsBlockhashValidRequest {
type Response = crate::RpcResponse<bool>;
const METHOD: &'static str = "isBlockhashValid";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
crate::validate_solana_hash_text(&self.blockhash, "isBlockhashValid blockhash");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut params = std::vec![serde_json::Value::String(self.blockhash.clone())];
if let std::option::Option::Some(config) = &self.config {
let value = match crate::standard_http::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn prioritization_fee_request_distinguishes_omitted_and_explicit_empty_sets() {
let omitted = crate::GetRecentPrioritizationFeesRequest {
locked_writable_accounts: std::option::Option::None,
};
let explicit = crate::GetRecentPrioritizationFeesRequest {
locked_writable_accounts: std::option::Option::Some(std::vec::Vec::new()),
};
let omitted_params = match crate::StandardHttpRequest::params(&omitted) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("unexpected omitted params error: {error}"),
};
let explicit_params = match crate::StandardHttpRequest::params(&explicit) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("unexpected explicit params error: {error}"),
};
assert_eq!(omitted_params, std::vec::Vec::<serde_json::Value>::new());
assert_eq!(explicit_params, std::vec![serde_json::json!([])]);
}
#[test]
fn prioritization_fee_request_enforces_official_account_bound() {
let request = crate::GetRecentPrioritizationFeesRequest {
locked_writable_accounts: std::option::Option::Some(
(0_u16..129_u16).map(|value| return pubkey(value as u8)).collect(),
),
};
assert!(crate::StandardHttpRequest::params(&request).is_err());
}
#[test]
fn blockhash_validity_preserves_minimum_context_slot() {
let request = crate::IsBlockhashValidRequest {
blockhash: pubkey(9),
config: std::option::Option::Some(crate::RpcContextConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
min_context_slot: std::option::Option::Some(123),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1]["minContextSlot"], serde_json::Value::from(123_u64));
}
}

View File

@@ -0,0 +1,670 @@
// file: kb_rpc/src/standard_methods.rs
// version: 3
//! Canonical inventory of standard Solana HTTP and WebSocket JSON-RPC methods.
/// Strongest implementation contract exposed for one standard RPC method.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StandardRpcContract {
/// The crate exposes a dedicated request/result adapter in addition to raw JSON transport.
TypedAdapter,
/// The method is explicitly registered and callable through validated raw JSON transport.
RawJson,
}
impl crate::StandardRpcContract {
/// Returns the stable matrix code for this contract.
pub const fn code(self) -> &'static str {
return match self {
Self::TypedAdapter => "typed_adapter",
Self::RawJson => "raw_json",
};
}
}
/// Local category used to group standard Solana HTTP methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StandardHttpCategory {
/// Account state and account ownership reads.
Accounts,
/// SPL Token account and mint reads.
Tokens,
/// Transaction, signature, fee, simulation and submission methods.
Transactions,
/// Block, ledger and performance methods.
Blocks,
/// Cluster, node, epoch, slot and validator methods.
Cluster,
/// Inflation, supply and stake-economics methods.
Economics,
}
impl crate::StandardHttpCategory {
/// Returns the stable matrix code for this category.
pub const fn code(self) -> &'static str {
return match self {
Self::Accounts => "accounts",
Self::Tokens => "tokens",
Self::Transactions => "transactions",
Self::Blocks => "blocks",
Self::Cluster => "cluster",
Self::Economics => "economics",
};
}
}
/// One standard Solana HTTP JSON-RPC method specification.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StandardHttpMethodSpec {
/// Exact JSON-RPC method name.
pub method: &'static str,
/// Local inventory category.
pub category: crate::StandardHttpCategory,
/// Strongest contract currently exposed by `kb_rpc`.
pub contract: crate::StandardRpcContract,
}
impl crate::StandardHttpMethodSpec {
/// Returns the local HTTP routing class for this method.
pub fn method_class(&self) -> crate::HttpMethodClass {
return match self.method {
"requestAirdrop" | "sendTransaction" => crate::HttpMethodClass::SendTransaction,
"getBlock"
| "getBlocks"
| "getBlocksWithLimit"
| "getProgramAccounts"
| "getSignaturesForAddress"
| "getTransaction"
| "simulateTransaction" => crate::HttpMethodClass::HeavyRead,
_ => crate::HttpMethodClass::GeneralRpc,
};
}
}
/// Every standard Solana HTTP JSON-RPC method documented by the canonical RPC reference.
pub const STANDARD_HTTP_METHODS: [crate::StandardHttpMethodSpec; 52] = [
crate::StandardHttpMethodSpec {
method: "getAccountInfo",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBalance",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getLargestAccounts",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getMinimumBalanceForRentExemption",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getMultipleAccounts",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getProgramAccounts",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenAccountBalance",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenAccountsByDelegate",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenAccountsByOwner",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenLargestAccounts",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenSupply",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getFeeForMessage",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getLatestBlockhash",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getRecentPrioritizationFees",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSignaturesForAddress",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSignatureStatuses",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTransaction",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTransactionCount",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "isBlockhashValid",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "requestAirdrop",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "sendTransaction",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "simulateTransaction",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlock",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlockCommitment",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlockHeight",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlockProduction",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlocks",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlocksWithLimit",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlockTime",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getFirstAvailableBlock",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getRecentPerformanceSamples",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "minimumLedgerSlot",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getClusterNodes",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getEpochInfo",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getEpochSchedule",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getGenesisHash",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getHealth",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getHighestSnapshotSlot",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getIdentity",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getLeaderSchedule",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getMaxRetransmitSlot",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getMaxShredInsertSlot",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSlot",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSlotLeader",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSlotLeaders",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getVersion",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getVoteAccounts",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getInflationGovernor",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getInflationRate",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getInflationReward",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getStakeMinimumDelegation",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSupply",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
];
/// Returns the canonical specification for one exact standard HTTP method name.
pub fn standard_http_method(
method: &str,
) -> std::option::Option<&'static crate::StandardHttpMethodSpec> {
return crate::STANDARD_HTTP_METHODS.iter().find(|entry| return entry.method == method);
}
/// Stability of one standard Solana WebSocket subscription surface.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StandardWsStability {
/// Stable standard PubSub method.
Stable,
/// Method documented as unstable and potentially gated by validator flags.
Unstable,
}
impl crate::StandardWsStability {
/// Returns the stable matrix code for this stability class.
pub const fn code(self) -> &'static str {
return match self {
Self::Stable => "stable",
Self::Unstable => "unstable",
};
}
}
/// One standard Solana WebSocket subscribe/unsubscribe pair.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StandardWsSubscriptionSpec {
/// Exact subscribe method name.
pub subscribe_method: &'static str,
/// Exact unsubscribe method name.
pub unsubscribe_method: &'static str,
/// Exact notification method name emitted by the server.
pub notification_method: &'static str,
/// Stability declared by the canonical Solana RPC documentation.
pub stability: crate::StandardWsStability,
/// Strongest typed request contract exposed by `kb_rpc`.
pub request_contract: crate::StandardRpcContract,
/// Strongest typed notification contract exposed by `kb_rpc`.
pub notification_contract: crate::StandardRpcContract,
/// Whether the pair is supported by the reusable persistent session runtime.
pub persistent_runtime: bool,
}
/// Every standard Solana WebSocket subscription pair documented by the canonical RPC reference.
pub const STANDARD_WS_SUBSCRIPTIONS: [crate::StandardWsSubscriptionSpec; 9] = [
crate::StandardWsSubscriptionSpec {
subscribe_method: "accountSubscribe",
unsubscribe_method: "accountUnsubscribe",
notification_method: "accountNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "blockSubscribe",
unsubscribe_method: "blockUnsubscribe",
notification_method: "blockNotification",
stability: crate::StandardWsStability::Unstable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "logsSubscribe",
unsubscribe_method: "logsUnsubscribe",
notification_method: "logsNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "programSubscribe",
unsubscribe_method: "programUnsubscribe",
notification_method: "programNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "rootSubscribe",
unsubscribe_method: "rootUnsubscribe",
notification_method: "rootNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "signatureSubscribe",
unsubscribe_method: "signatureUnsubscribe",
notification_method: "signatureNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "slotSubscribe",
unsubscribe_method: "slotUnsubscribe",
notification_method: "slotNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "slotsUpdatesSubscribe",
unsubscribe_method: "slotsUpdatesUnsubscribe",
notification_method: "slotsUpdatesNotification",
stability: crate::StandardWsStability::Unstable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "voteSubscribe",
unsubscribe_method: "voteUnsubscribe",
notification_method: "voteNotification",
stability: crate::StandardWsStability::Unstable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
];
/// Returns the canonical subscription specification matching a subscribe or unsubscribe method.
pub fn standard_ws_subscription(
method: &str,
) -> std::option::Option<&'static crate::StandardWsSubscriptionSpec> {
return crate::STANDARD_WS_SUBSCRIPTIONS.iter().find(|entry| {
return entry.subscribe_method == method || entry.unsubscribe_method == method;
});
}
#[cfg(test)]
mod tests {
#[test]
fn standard_http_inventory_is_exact_unique_and_classified() {
assert_eq!(crate::STANDARD_HTTP_METHODS.len(), 52);
let mut names = std::collections::BTreeSet::new();
let mut typed_count = 0_usize;
for method in &crate::STANDARD_HTTP_METHODS {
assert!(names.insert(method.method));
if method.contract == crate::StandardRpcContract::TypedAdapter {
typed_count = typed_count.saturating_add(1);
}
assert_eq!(
crate::standard_http_method(method.method),
std::option::Option::Some(method)
);
}
assert_eq!(typed_count, 52);
}
#[test]
fn configurable_request_registry_covers_the_former_thirty_eight_raw_methods() {
fn method<Request: crate::StandardHttpRequest>() -> &'static str {
return <Request as crate::StandardHttpRequest>::METHOD;
}
let methods = [
method::<crate::GetLargestAccountsRequest>(),
method::<crate::GetMultipleAccountsRequest>(),
method::<crate::GetProgramAccountsRequest>(),
method::<crate::GetTokenAccountBalanceRequest>(),
method::<crate::GetTokenAccountsByDelegateRequest>(),
method::<crate::GetTokenAccountsByOwnerRequest>(),
method::<crate::GetTokenLargestAccountsRequest>(),
method::<crate::GetTokenSupplyRequest>(),
method::<crate::GetRecentPrioritizationFeesRequest>(),
method::<crate::GetTransactionCountRequest>(),
method::<crate::IsBlockhashValidRequest>(),
method::<crate::GetBlockRequest>(),
method::<crate::GetBlockCommitmentRequest>(),
method::<crate::GetBlockProductionRequest>(),
method::<crate::GetBlocksRequest>(),
method::<crate::GetBlocksWithLimitRequest>(),
method::<crate::GetBlockTimeRequest>(),
method::<crate::GetFirstAvailableBlockRequest>(),
method::<crate::GetRecentPerformanceSamplesRequest>(),
method::<crate::MinimumLedgerSlotRequest>(),
method::<crate::GetClusterNodesRequest>(),
method::<crate::GetEpochScheduleRequest>(),
method::<crate::GetHealthRequest>(),
method::<crate::GetHighestSnapshotSlotRequest>(),
method::<crate::GetIdentityRequest>(),
method::<crate::GetLeaderScheduleRequest>(),
method::<crate::GetMaxRetransmitSlotRequest>(),
method::<crate::GetMaxShredInsertSlotRequest>(),
method::<crate::GetSlotRequest>(),
method::<crate::GetSlotLeaderRequest>(),
method::<crate::GetSlotLeadersRequest>(),
method::<crate::GetVersionRequest>(),
method::<crate::GetVoteAccountsRequest>(),
method::<crate::GetInflationGovernorRequest>(),
method::<crate::GetInflationRateRequest>(),
method::<crate::GetInflationRewardRequest>(),
method::<crate::GetStakeMinimumDelegationRequest>(),
method::<crate::GetSupplyRequest>(),
];
assert_eq!(methods.len(), 38);
let mut unique = std::collections::BTreeSet::new();
for method_name in methods {
assert!(unique.insert(method_name));
let specification = match crate::standard_http_method(method_name) {
std::option::Option::Some(specification) => specification,
std::option::Option::None => panic!("typed method absent from registry"),
};
assert_eq!(specification.contract, crate::StandardRpcContract::TypedAdapter);
}
}
#[test]
fn standard_ws_inventory_contains_nine_pairs_and_eighteen_unique_methods() {
assert_eq!(crate::STANDARD_WS_SUBSCRIPTIONS.len(), 9);
let mut names = std::collections::BTreeSet::new();
let mut unstable_count = 0_usize;
for subscription in &crate::STANDARD_WS_SUBSCRIPTIONS {
assert!(names.insert(subscription.subscribe_method));
assert!(names.insert(subscription.unsubscribe_method));
if subscription.stability == crate::StandardWsStability::Unstable {
unstable_count = unstable_count.saturating_add(1);
}
assert_eq!(subscription.request_contract, crate::StandardRpcContract::TypedAdapter);
assert_eq!(
subscription.notification_contract,
crate::StandardRpcContract::TypedAdapter
);
assert!(subscription.persistent_runtime);
assert_eq!(
crate::standard_ws_subscription(subscription.subscribe_method),
std::option::Option::Some(subscription)
);
assert_eq!(
crate::standard_ws_subscription(subscription.unsubscribe_method),
std::option::Option::Some(subscription)
);
}
assert_eq!(names.len(), 18);
assert_eq!(unstable_count, 3);
}
#[test]
fn standard_rpc_matrix_matches_compiled_inventory() {
let raw = include_str!("../../docs/SOLANA_STANDARD_RPC_MATRIX.json");
let parsed = match serde_json::from_str::<serde_json::Value>(raw) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("rpc matrix parsing failed: {error}"),
};
let http_methods = match parsed.get("http_methods").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("rpc matrix http_methods missing"),
};
let ws_subscriptions =
match parsed.get("ws_subscriptions").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("rpc matrix ws_subscriptions missing"),
};
assert_eq!(http_methods.len(), crate::STANDARD_HTTP_METHODS.len());
assert_eq!(ws_subscriptions.len(), crate::STANDARD_WS_SUBSCRIPTIONS.len());
for method in &crate::STANDARD_HTTP_METHODS {
let matrix_entry = http_methods.iter().find(|entry| {
return entry.get("method").and_then(serde_json::Value::as_str)
== std::option::Option::Some(method.method);
});
let matrix_entry = match matrix_entry {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("rpc matrix HTTP method missing"),
};
assert_eq!(
matrix_entry.get("category").and_then(serde_json::Value::as_str),
std::option::Option::Some(method.category.code())
);
assert_eq!(
matrix_entry.get("contract").and_then(serde_json::Value::as_str),
std::option::Option::Some(method.contract.code())
);
}
for subscription in &crate::STANDARD_WS_SUBSCRIPTIONS {
let matrix_entry = ws_subscriptions.iter().find(|entry| {
return entry.get("subscribe_method").and_then(serde_json::Value::as_str)
== std::option::Option::Some(subscription.subscribe_method);
});
let matrix_entry = match matrix_entry {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("rpc matrix WS subscription missing"),
};
assert_eq!(
matrix_entry.get("unsubscribe_method").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.unsubscribe_method)
);
assert_eq!(
matrix_entry.get("notification_method").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.notification_method)
);
assert_eq!(
matrix_entry.get("stability").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.stability.code())
);
assert_eq!(
matrix_entry.get("request_contract").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.request_contract.code())
);
assert_eq!(
matrix_entry.get("notification_contract").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.notification_contract.code())
);
assert_eq!(
matrix_entry.get("persistent_runtime").and_then(serde_json::Value::as_bool),
std::option::Option::Some(subscription.persistent_runtime)
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
// file: kb_rpc/src/validation.rs
// version: 2
//! Shared validation helpers for Solana RPC addresses, hashes and signatures.
/// Validates one base58 Solana transaction signature.
pub fn validate_transaction_signature_text(value: &str, field_name: &str) -> kb_core::Result<()> {
return crate::validation::validate_base58_length(value, field_name, 64);
}
/// Validates one base58 Solana public key or account address.
pub fn validate_solana_pubkey_text(value: &str, field_name: &str) -> kb_core::Result<()> {
return crate::validation::validate_base58_length(value, field_name, 32);
}
/// Validates one base58 Solana blockhash or genesis hash.
pub fn validate_solana_hash_text(value: &str, field_name: &str) -> kb_core::Result<()> {
return crate::validation::validate_base58_length(value, field_name, 32);
}
fn validate_base58_length(
value: &str,
field_name: &str,
expected_length: usize,
) -> kb_core::Result<()> {
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(format!(
"{field_name} must not be empty"
)));
}
let decode_result = bs58::decode(value).into_vec();
let decoded = match decode_result {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"{field_name} is not valid base58: {error}"
)));
},
};
if decoded.len() != expected_length {
return std::result::Result::Err(kb_core::Error::config(format!(
"{field_name} must decode to {expected_length} bytes"
)));
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn signature_validation_accepts_sixty_four_bytes() {
let signature = bs58::encode([7_u8; 64]).into_string();
let result = crate::validate_transaction_signature_text(&signature, "signature");
assert!(result.is_ok());
}
#[test]
fn pubkey_validation_accepts_thirty_two_bytes() {
let pubkey = bs58::encode([9_u8; 32]).into_string();
let result = crate::validate_solana_pubkey_text(&pubkey, "pubkey");
assert!(result.is_ok());
}
#[test]
fn hash_validation_accepts_thirty_two_bytes() {
let hash = bs58::encode([10_u8; 32]).into_string();
let result = crate::validate_solana_hash_text(&hash, "hash");
assert!(result.is_ok());
}
#[test]
fn signature_validation_rejects_public_key_length() {
let pubkey = bs58::encode([11_u8; 32]).into_string();
let result = crate::validate_transaction_signature_text(&pubkey, "signature");
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,386 @@
// file: kb_rpc/src/ws_client.rs
// version: 6
//! Standard Solana WebSocket client helpers.
use futures_util::SinkExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
/// Snapshot of one pooled WebSocket endpoint.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WsPoolClientSnapshot {
/// Logical endpoint name.
pub endpoint_name: std::string::String,
/// Provider name.
pub provider: std::string::String,
/// Endpoint URL.
pub endpoint_url: std::string::String,
/// Supported roles.
pub roles: std::vec::Vec<crate::EndpointRoleSnapshot>,
/// Status string.
pub status: std::string::String,
}
/// Standard Solana WebSocket client bound to one configured endpoint.
#[derive(Clone, Debug)]
pub struct WsClient {
endpoint: kb_config::WsEndpointConfig,
next_request_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
impl crate::WsClient {
/// Creates a new WebSocket client bound to one endpoint.
pub fn new(endpoint: kb_config::WsEndpointConfig) -> kb_core::Result<Self> {
if !endpoint.enabled {
tracing::error!(target: crate::TRACING_TARGET, action = "create_ws_client", endpoint_name = %endpoint.name, provider = %endpoint.provider, error_code = "ws_endpoint_disabled", "cannot create WebSocket client for disabled endpoint");
return std::result::Result::Err(kb_core::Error::config(format!(
"ws endpoint '{}' is disabled",
endpoint.name
)));
}
tracing::debug!(target: crate::TRACING_TARGET, action = "create_ws_client", endpoint_name = %endpoint.name, provider = %endpoint.provider, role_count = endpoint.roles.len(), "WebSocket client created");
return std::result::Result::Ok(Self {
endpoint,
next_request_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)),
});
}
/// Returns the endpoint name.
pub fn endpoint_name(&self) -> &str {
return self.endpoint.name.as_str();
}
/// Returns the provider name.
pub fn provider(&self) -> &str {
return self.endpoint.provider.as_str();
}
/// Returns the endpoint URL.
pub fn endpoint_url(&self) -> &str {
return self.endpoint.url.as_str();
}
/// Returns the endpoint configuration.
pub fn endpoint_config(&self) -> &kb_config::WsEndpointConfig {
return &self.endpoint;
}
/// Returns true when this endpoint supports the required role and request kind.
pub fn can_handle(&self, required_role: &str, request_kind: &str) -> bool {
if !self.endpoint.enabled {
return false;
}
for role in &self.endpoint.roles {
if crate::endpoint_role::role_matches(role, required_role, request_kind) {
return true;
}
}
return false;
}
/// Returns a serializable endpoint snapshot.
pub fn snapshot(&self) -> crate::WsPoolClientSnapshot {
let mut roles = std::vec::Vec::new();
for role in &self.endpoint.roles {
roles.push(crate::EndpointRoleSnapshot::from_config(role));
}
return crate::WsPoolClientSnapshot {
endpoint_name: self.endpoint.name.clone(),
provider: self.endpoint.provider.clone(),
endpoint_url: self.endpoint.url.clone(),
roles,
status: "idle".to_string(),
};
}
/// Builds a JSON-RPC request with a generated id.
pub fn build_json_rpc_request(
&self,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> crate::JsonRpcRequest {
let request_id = self.next_request_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return crate::JsonRpcRequest::new_with_u64_id(request_id, method, params);
}
/// Builds a subscribe request for one explicitly registered standard subscription.
pub fn build_standard_subscribe_request(
&self,
subscription: &crate::StandardWsSubscriptionSpec,
params: std::vec::Vec<serde_json::Value>,
) -> crate::JsonRpcRequest {
return self.build_json_rpc_request(subscription.subscribe_method.to_string(), params);
}
/// Builds an unsubscribe request for one explicitly registered standard subscription.
pub fn build_standard_unsubscribe_request(
&self,
subscription: &crate::StandardWsSubscriptionSpec,
subscription_id: u64,
) -> crate::JsonRpcRequest {
return self.build_json_rpc_request(
subscription.unsubscribe_method.to_string(),
std::vec![serde_json::Value::from(subscription_id)],
);
}
/// Connects, sends one JSON-RPC request, waits for one response and closes.
pub async fn execute_json_rpc_once(
&self,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<crate::JsonRpcResponse> {
let parameter_count = params.len();
let request = self.build_json_rpc_request(method.clone(), params);
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_ws_json_rpc_once", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, parameter_count, "start one-shot WebSocket JSON-RPC request");
let request_text = match request.to_json_string() {
std::result::Result::Ok(text) => text,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "serialize_ws_json_rpc_request", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, error = %error, "WebSocket JSON-RPC request serialization failed");
return std::result::Result::Err(error);
},
};
let connect_timeout = std::time::Duration::from_millis(self.endpoint.connect_timeout_ms);
let connect_future = tokio_tungstenite::connect_async(self.endpoint.url.as_str());
let connect_timeout_result = tokio::time::timeout(connect_timeout, connect_future).await;
let connect_result = match connect_timeout_result {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => {
tracing::error!(target: crate::TRACING_TARGET, action = "connect_ws_endpoint", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, timeout_ms = self.endpoint.connect_timeout_ms, error_code = "ws_connect_timeout", "WebSocket endpoint connection timed out");
return std::result::Result::Err(kb_core::Error::ws(format!(
"websocket connect timed out for endpoint '{}'",
self.endpoint.name
)));
},
};
let (mut stream, _response) = match connect_result {
std::result::Result::Ok(pair) => pair,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "connect_ws_endpoint", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, error = %error, "WebSocket endpoint connection failed");
return std::result::Result::Err(kb_core::Error::ws(format!(
"cannot connect websocket endpoint '{}': {error}",
self.endpoint.name
)));
},
};
let send_result = stream
.send(tokio_tungstenite::tungstenite::Message::Text(request_text.into()))
.await;
if let std::result::Result::Err(error) = send_result {
tracing::error!(target: crate::TRACING_TARGET, action = "send_ws_json_rpc_request", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, error = %error, "WebSocket JSON-RPC request send failed");
return std::result::Result::Err(kb_core::Error::ws(format!(
"cannot send websocket request '{}' to endpoint '{}': {error}",
method, self.endpoint.name
)));
}
let response_timeout = std::time::Duration::from_millis(self.endpoint.request_timeout_ms);
let next_timeout_result = tokio::time::timeout(response_timeout, stream.next()).await;
let next_result = match next_timeout_result {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => {
tracing::error!(target: crate::TRACING_TARGET, action = "read_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, timeout_ms = self.endpoint.request_timeout_ms, error_code = "ws_response_timeout", "WebSocket JSON-RPC response timed out");
return std::result::Result::Err(kb_core::Error::ws(format!(
"websocket response timed out for endpoint '{}'",
self.endpoint.name
)));
},
};
let message = match next_result {
std::option::Option::Some(result) => match result {
std::result::Result::Ok(message) => message,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "read_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, error = %error, "WebSocket JSON-RPC response read failed");
return std::result::Result::Err(kb_core::Error::ws(format!(
"websocket read failed for endpoint '{}': {error}",
self.endpoint.name
)));
},
},
std::option::Option::None => {
tracing::error!(target: crate::TRACING_TARGET, action = "read_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, error_code = "ws_closed_before_response", "WebSocket endpoint closed before response");
return std::result::Result::Err(kb_core::Error::ws(format!(
"websocket endpoint '{}' closed before response",
self.endpoint.name
)));
},
};
let close_result = stream
.send(tokio_tungstenite::tungstenite::Message::Close(std::option::Option::None))
.await;
if let std::result::Result::Err(error) = close_result {
tracing::debug!(target: crate::TRACING_TARGET, action = "close_ws_endpoint", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, error = %error, "WebSocket close send failed");
}
return match message {
tokio_tungstenite::tungstenite::Message::Text(text) => {
let parse_result = crate::parse_json_rpc_text(text.as_str());
match parse_result {
std::result::Result::Ok(response) => {
if let crate::JsonRpcResponse::Error(error_response) = &response {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_ws_json_rpc_once", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, response_kind = response.kind_name(), rpc_error_code = error_response.error.code, rpc_error_message = %error_response.error.message, "WebSocket JSON-RPC endpoint returned an RPC error");
} else {
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_ws_json_rpc_once", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, response_kind = response.kind_name(), "one-shot WebSocket JSON-RPC request completed");
}
std::result::Result::Ok(response)
},
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "parse_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, response_byte_length = text.len(), error = %error, "WebSocket JSON-RPC response parsing failed");
std::result::Result::Err(error)
},
}
},
tokio_tungstenite::tungstenite::Message::Binary(binary) => {
tracing::error!(target: crate::TRACING_TARGET, action = "read_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, message_kind = "binary", response_byte_length = binary.len(), "WebSocket endpoint returned binary data before JSON response");
std::result::Result::Err(kb_core::Error::ws(format!(
"websocket endpoint '{}' returned binary message with {} bytes",
self.endpoint.name,
binary.len()
)))
},
tokio_tungstenite::tungstenite::Message::Ping(_) => {
tracing::error!(target: crate::TRACING_TARGET, action = "read_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, message_kind = "ping", "WebSocket endpoint returned ping before JSON response");
std::result::Result::Err(kb_core::Error::ws(format!(
"websocket endpoint '{}' returned ping before json response",
self.endpoint.name
)))
},
tokio_tungstenite::tungstenite::Message::Pong(_) => {
tracing::error!(target: crate::TRACING_TARGET, action = "read_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, message_kind = "pong", "WebSocket endpoint returned pong before JSON response");
std::result::Result::Err(kb_core::Error::ws(format!(
"websocket endpoint '{}' returned pong before json response",
self.endpoint.name
)))
},
tokio_tungstenite::tungstenite::Message::Close(_) => {
tracing::error!(target: crate::TRACING_TARGET, action = "read_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, message_kind = "close", "WebSocket endpoint closed before JSON response");
std::result::Result::Err(kb_core::Error::ws(format!(
"websocket endpoint '{}' closed before json response",
self.endpoint.name
)))
},
tokio_tungstenite::tungstenite::Message::Frame(_) => {
tracing::error!(target: crate::TRACING_TARGET, action = "read_ws_json_rpc_response", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id = ?request.id, method = %method, message_kind = "frame", "WebSocket endpoint returned raw frame before JSON response");
std::result::Result::Err(kb_core::Error::ws(format!(
"websocket endpoint '{}' returned raw frame before json response",
self.endpoint.name
)))
},
};
}
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled: true,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
fn endpoint(enabled: bool) -> kb_config::WsEndpointConfig {
return kb_config::WsEndpointConfig {
name: "ws_a".to_string(),
enabled,
provider: "test".to_string(),
cluster: "devnet".to_string(),
url: "wss://example.invalid".to_string(),
connect_timeout_ms: 100,
request_timeout_ms: 100,
unsubscribe_timeout_ms: 100,
write_channel_capacity: 8,
event_channel_capacity: 16,
auto_reconnect: false,
roles: std::vec![
role_config("slot_notifications", std::vec!["slot_subscribe".to_string()]),
role_config("program_subscribe", std::vec!["program_subscribe".to_string()]),
role_config("ws_any", std::vec!["*".to_string()]),
],
};
}
#[test]
fn new_rejects_disabled_endpoint() {
let result = crate::WsClient::new(endpoint(false));
assert!(result.is_err());
}
#[test]
fn can_handle_matches_exact_role_and_kind() {
let client = match crate::WsClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
assert!(client.can_handle("slot_notifications", "slot_subscribe"));
assert!(!client.can_handle("slot_notifications", "root_subscribe"));
}
#[test]
fn can_handle_matches_wildcard_kind() {
let client = match crate::WsClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
assert!(client.can_handle("ws_any", "logs_subscribe_mentions"));
}
#[test]
fn snapshot_preserves_endpoint_metadata() {
let client = match crate::WsClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
let snapshot = client.snapshot();
assert_eq!(snapshot.endpoint_name, "ws_a");
assert_eq!(snapshot.provider, "test");
assert_eq!(snapshot.endpoint_url, "wss://example.invalid");
assert_eq!(snapshot.roles.len(), 3);
}
#[test]
fn standard_subscription_builders_preserve_exact_methods_and_subscription_id() {
let client = match crate::WsClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
let subscription = match crate::standard_ws_subscription("slotSubscribe") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("slot subscription missing"),
};
let subscribe = client.build_standard_subscribe_request(
subscription,
std::vec![serde_json::json!({"commitment": "confirmed"})],
);
let unsubscribe = client.build_standard_unsubscribe_request(subscription, 42);
assert_eq!(subscribe.method, "slotSubscribe");
assert_eq!(subscribe.params.len(), 1);
assert_eq!(unsubscribe.method, "slotUnsubscribe");
assert_eq!(unsubscribe.params, std::vec![serde_json::Value::from(42)]);
assert_eq!(subscribe.id, serde_json::Value::from(1));
assert_eq!(unsubscribe.id, serde_json::Value::from(2));
}
#[test]
fn build_json_rpc_request_increments_ids() {
let client = match crate::WsClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
let first =
client.build_json_rpc_request("slotSubscribe".to_string(), std::vec::Vec::new());
let second =
client.build_json_rpc_request("rootSubscribe".to_string(), std::vec::Vec::new());
assert_eq!(first.id, serde_json::Value::from(1));
assert_eq!(second.id, serde_json::Value::from(2));
}
}

View File

@@ -0,0 +1,266 @@
// file: kb_rpc/src/ws_pool.rs
// version: 5
//! WebSocket endpoint pool and role-based routing.
/// Pool of standard Solana WebSocket endpoints.
#[derive(Clone, Debug)]
pub struct WsEndpointPool {
clients: std::vec::Vec<crate::WsClient>,
next_index: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl crate::WsEndpointPool {
/// Builds a pool from the active profile WebSocket endpoint list.
pub fn from_profile(profile: &kb_config::ProfileConfig) -> kb_core::Result<Self> {
let mut clients = std::vec::Vec::new();
for endpoint in &profile.solana.ws_endpoints {
if !endpoint.enabled {
continue;
}
let client = match crate::WsClient::new(endpoint.clone()) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
clients.push(client);
}
return crate::WsEndpointPool::new(clients);
}
/// Creates a pool from already constructed clients.
pub fn new(clients: std::vec::Vec<crate::WsClient>) -> kb_core::Result<Self> {
if clients.is_empty() {
tracing::error!(target: crate::TRACING_TARGET, action = "create_ws_pool", error_code = "ws_pool_empty", "WebSocket endpoint pool has no enabled endpoint");
return std::result::Result::Err(kb_core::Error::config(
"ws endpoint pool requires at least one enabled endpoint".to_string(),
));
}
tracing::debug!(target: crate::TRACING_TARGET, action = "create_ws_pool", endpoint_count = clients.len(), "WebSocket endpoint pool created");
return std::result::Result::Ok(Self {
clients,
next_index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
});
}
/// Returns a serializable snapshot of every endpoint in the pool.
pub fn snapshot(&self) -> std::vec::Vec<crate::WsPoolClientSnapshot> {
let mut snapshots = std::vec::Vec::new();
for client in &self.clients {
snapshots.push(client.snapshot());
}
return snapshots;
}
/// Selects one endpoint for the requested role and method.
pub fn select_client_for_role_and_method(
&self,
required_role: &str,
method: &str,
) -> kb_core::Result<crate::WsClient> {
let request_kind = crate::request_kind_from_method(method);
return self.select_client_for_role_and_kind(required_role, &request_kind);
}
/// Selects one endpoint for the requested role and request kind.
pub fn select_client_for_role_and_kind(
&self,
required_role: &str,
request_kind: &str,
) -> kb_core::Result<crate::WsClient> {
if self.clients.is_empty() {
tracing::error!(target: crate::TRACING_TARGET, action = "select_ws_endpoint", required_role, request_kind, error_code = "ws_pool_empty", "WebSocket endpoint pool has no clients");
return std::result::Result::Err(kb_core::Error::not_connected(
"ws endpoint pool has no clients".to_string(),
));
}
let start_index = self.next_index.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let client_count = self.clients.len();
let mut offset = 0_usize;
while offset < client_count {
let index = (start_index + offset) % client_count;
let client = self.clients[index].clone();
if client.can_handle(required_role, request_kind) {
tracing::debug!(target: crate::TRACING_TARGET, action = "select_ws_endpoint", required_role, request_kind, endpoint_name = %client.endpoint_name(), provider = %client.provider(), "selected WebSocket endpoint");
return std::result::Result::Ok(client);
}
offset += 1;
}
tracing::error!(target: crate::TRACING_TARGET, action = "select_ws_endpoint", required_role, request_kind, endpoint_count = self.clients.len(), error_code = "ws_endpoint_not_found", "no WebSocket endpoint supports requested role and kind");
return std::result::Result::Err(kb_core::Error::config(format!(
"no ws endpoint supports role '{}' and request kind '{}'",
required_role, request_kind
)));
}
/// Selects one endpoint for an explicitly registered standard WebSocket subscription.
pub fn select_client_for_standard_subscription(
&self,
required_role: &str,
subscription: &crate::StandardWsSubscriptionSpec,
) -> kb_core::Result<crate::WsClient> {
return self
.select_client_for_role_and_method(required_role, subscription.subscribe_method);
}
/// Executes one short WebSocket JSON-RPC request through the selected endpoint.
pub async fn execute_json_rpc_once_for_role(
&self,
required_role: &str,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<crate::JsonRpcResponse> {
let client = match self.select_client_for_role_and_method(required_role, &method) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.execute_json_rpc_once(method, params).await;
}
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled: true,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
fn endpoint(
name: &str,
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::WsEndpointConfig {
return kb_config::WsEndpointConfig {
name: name.to_string(),
enabled: true,
provider: "test".to_string(),
cluster: "devnet".to_string(),
url: format!("wss://{name}.invalid"),
connect_timeout_ms: 100,
request_timeout_ms: 100,
unsubscribe_timeout_ms: 100,
write_channel_capacity: 8,
event_channel_capacity: 16,
auto_reconnect: false,
roles: std::vec![role_config(role, request_kinds)],
};
}
fn client(endpoint: kb_config::WsEndpointConfig) -> crate::WsClient {
match crate::WsClient::new(endpoint) {
std::result::Result::Ok(client) => return client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
}
}
#[test]
fn new_rejects_empty_pool() {
let result = crate::WsEndpointPool::new(std::vec::Vec::new());
assert!(result.is_err());
}
#[test]
fn snapshot_lists_every_client() {
let pool = match crate::WsEndpointPool::new(std::vec![
client(endpoint("a", "slot_notifications", std::vec!["slot_subscribe".to_string()])),
client(endpoint("b", "slot_notifications", std::vec!["slot_subscribe".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let snapshot = pool.snapshot();
assert_eq!(snapshot.len(), 2);
assert_eq!(snapshot[0].endpoint_name, "a");
assert_eq!(snapshot[1].endpoint_name, "b");
}
#[test]
fn select_client_round_robins_matching_clients() {
let pool = match crate::WsEndpointPool::new(std::vec![
client(endpoint("a", "slot_notifications", std::vec!["slot_subscribe".to_string()])),
client(endpoint("b", "slot_notifications", std::vec!["slot_subscribe".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let first =
match pool.select_client_for_role_and_method("slot_notifications", "slotSubscribe") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
let second =
match pool.select_client_for_role_and_method("slot_notifications", "slotSubscribe") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(first.endpoint_name(), "a");
assert_eq!(second.endpoint_name(), "b");
}
#[test]
fn select_client_skips_unsupported_clients() {
let pool = match crate::WsEndpointPool::new(std::vec![
client(endpoint("a", "program_subscribe", std::vec!["program_subscribe".to_string()])),
client(endpoint("b", "slot_notifications", std::vec!["slot_subscribe".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let selected =
match pool.select_client_for_role_and_method("slot_notifications", "slotSubscribe") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(selected.endpoint_name(), "b");
}
#[test]
fn standard_subscription_selection_uses_the_subscribe_method() {
let pool = match crate::WsEndpointPool::new(std::vec![client(endpoint(
"a",
"slot_notifications",
std::vec!["slot_subscribe".to_string()],
))]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let subscription = match crate::standard_ws_subscription("slotSubscribe") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("slot subscription missing"),
};
let selected = match pool
.select_client_for_standard_subscription("slot_notifications", subscription)
{
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(selected.endpoint_name(), "a");
}
#[test]
fn select_client_returns_error_for_missing_role() {
let pool = match crate::WsEndpointPool::new(std::vec![client(endpoint(
"a",
"slot_notifications",
std::vec!["slot_subscribe".to_string()]
)),])
{
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let selected =
pool.select_client_for_role_and_kind("program_subscribe", "program_subscribe");
assert!(selected.is_err());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"slot": 104,
"blockTime": null,
"version": "legacy",
"transaction": {
"signatures": ["6psR5zDjAeh7HyLceXFQkWTJeRxjor8asYrG1AeUrrKVNXGE7DkvDqvVo98R9rPPQKHGm6TW3PBMSLrw2fHfxaX"],
"message": {
"header": {
"numRequiredSignatures": 1,
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1
},
"accountKeys": [
"11111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111"
],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": [
{"programIdIndex": 1, "accounts": [0], "data": "8", "stackHeight": null}
],
"addressTableLookups": []
}
},
"meta": {
"err": null,
"fee": 5000,
"preBalances": [10000, 1],
"postBalances": [5000, 1],
"innerInstructions": null,
"logMessages": null,
"preTokenBalances": null,
"postTokenBalances": null,
"rewards": null,
"loadedAddresses": null,
"returnData": null,
"computeUnitsConsumed": null,
"costUnits": null
}
}

View File

@@ -0,0 +1,30 @@
{
"meta": {
"postBalances": [5000, 1],
"fee": 5000,
"status": {"Ok": null},
"err": null,
"preBalances": [10000, 1]
},
"transaction": {
"message": {
"instructions": [
{"data": "8", "accounts": [0], "programIdIndex": 1}
],
"recentBlockhash": "11111111111111111111111111111111",
"accountKeys": [
"11111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111"
],
"header": {
"numReadonlyUnsignedAccounts": 1,
"numReadonlySignedAccounts": 0,
"numRequiredSignatures": 1
}
},
"signatures": ["6psR5zDjAeh7HyLceXFQkWTJeRxjor8asYrG1AeUrrKVNXGE7DkvDqvVo98R9rPPQKHGm6TW3PBMSLrw2fHfxaX"]
},
"blockTime": null,
"slot": 104,
"providerSpecificField": "ignored"
}

View File

@@ -0,0 +1,37 @@
{
"blockTime": 1700000002,
"meta": {
"err": {"InstructionError": [0, {"Custom": 6001}]},
"fee": 5000,
"innerInstructions": null,
"loadedAddresses": {"readonly": [], "writable": []},
"logMessages": ["Program 11111111111111111111111111111111 failed: custom program error: 0x1771"],
"postBalances": [5000, 1],
"postTokenBalances": [],
"preBalances": [10000, 1],
"preTokenBalances": [],
"rewards": null,
"status": {"Err": {"InstructionError": [0, {"Custom": 6001}]}}
},
"slot": 101,
"transaction": {
"message": {
"accountKeys": [
"11111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111"
],
"addressTableLookups": [],
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"instructions": [
{"accounts": [0], "data": "4", "programIdIndex": 1}
],
"recentBlockhash": "11111111111111111111111111111111"
},
"signatures": ["3LJnHMv3ygbULcerbSFVsk2Jo33Qv8DoyUJQh2E9UZAGYXcv7Y7KGZ3hPw5F5j9C9tBhVcYqipRnNEugKV1rnSU"]
},
"version": "legacy"
}

View File

@@ -0,0 +1,80 @@
{
"slot": 100,
"blockTime": 1700000001,
"version": "legacy",
"transaction": {
"signatures": ["2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T"],
"message": {
"header": {
"numRequiredSignatures": 1,
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 2
},
"accountKeys": [
"11111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": [
{
"programIdIndex": 1,
"accounts": [0],
"data": "2",
"stackHeight": 1
}
],
"addressTableLookups": []
}
},
"meta": {
"err": null,
"status": {"Ok": null},
"fee": 5000,
"preBalances": [10000, 1, 1],
"postBalances": [5000, 1, 1],
"innerInstructions": [
{
"index": 0,
"instructions": [
{
"programIdIndex": 2,
"accounts": [0],
"data": "3",
"stackHeight": 2
}
]
}
],
"logMessages": [
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success"
],
"preTokenBalances": [],
"postTokenBalances": [
{
"accountIndex": 0,
"mint": "So11111111111111111111111111111111111111112",
"owner": "11111111111111111111111111111111",
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"uiTokenAmount": {
"amount": "12345678901234567890",
"decimals": 9,
"uiAmount": null,
"uiAmountString": "12345678901.23456789"
}
}
],
"rewards": [],
"loadedAddresses": {
"writable": [],
"readonly": []
},
"returnData": {
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"data": ["AQI=", "base64"]
},
"computeUnitsConsumed": 1200,
"costUnits": 1300
}
}

View File

@@ -0,0 +1,28 @@
{
"slot": 103,
"blockTime": null,
"transaction": {
"signatures": ["5fMCpSnW6zfEJrShJAFSTaye2dexqwVfErfeETB34kbkRriTSfD3uQxtzjn2ToyKeqakLbpcbrvq5eDBTbs4uCW"],
"message": {
"header": {
"numRequiredSignatures": 1,
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1
},
"accountKeys": [
"11111111111111111111111111111111",
"ComputeBudget111111111111111111111111111111"
],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": [
{"programIdIndex": 1, "accounts": [0], "data": "7"}
]
}
},
"meta": {
"err": null,
"fee": 5000,
"preBalances": [10000, 1],
"postBalances": [5000, 1]
}
}

View File

@@ -0,0 +1,88 @@
{
"slot": 102,
"blockTime": 1700000003,
"version": 0,
"transaction": {
"signatures": ["4VpzYuMH3LdMKjYmwoFUAfVyQqMBt2rjcAV2TjhbGet1VCAgn6fBaz1JCLRdmmZFuMtDv7BjALgJiwZRtYSTqpV"],
"message": {
"header": {
"numRequiredSignatures": 1,
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 2
},
"accountKeys": [
"11111111111111111111111111111111",
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"ComputeBudget111111111111111111111111111111"
],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": [
{
"programIdIndex": 3,
"accounts": [0, 4],
"data": "5",
"stackHeight": 1
}
],
"addressTableLookups": [
{
"accountKey": "AddressLookupTab1e1111111111111111111111111",
"writableIndexes": [0],
"readonlyIndexes": [1]
}
]
}
},
"meta": {
"err": null,
"fee": 7000,
"preBalances": [100000, 1, 1, 1, 1],
"postBalances": [93000, 1, 1, 1, 1],
"innerInstructions": [
{
"index": 0,
"instructions": [
{
"programIdIndex": 4,
"accounts": [0, 1],
"data": "6",
"stackHeight": 2
}
]
}
],
"logMessages": ["Program log: Token-2022 CPI"],
"preTokenBalances": [
{
"accountIndex": 1,
"mint": "So11111111111111111111111111111111111111112",
"owner": "11111111111111111111111111111111",
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"uiTokenAmount": {
"amount": "10",
"decimals": 0,
"uiAmountString": "10"
}
}
],
"postTokenBalances": [
{
"accountIndex": 1,
"mint": "So11111111111111111111111111111111111111112",
"owner": "11111111111111111111111111111111",
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"uiTokenAmount": {
"amount": "9",
"decimals": 0,
"uiAmountString": "9"
}
}
],
"rewards": [],
"loadedAddresses": {
"writable": ["SysvarRent111111111111111111111111111111111"],
"readonly": ["SysvarC1ock11111111111111111111111111111111"]
},
"computeUnitsConsumed": 9999
}
}