Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7acac3fc8 | |||
| 039b93cfb3 | |||
| c9a0f7fd37 | |||
| 704110d509 | |||
| fb32ed03f8 | |||
| 4439589f0d | |||
| 1e1bc0d421 | |||
| bd26d4f636 | |||
| 2a7f3e8f40 | |||
| 1d37f04d63 | |||
| 1ccf11bcc1 | |||
| d06284fca2 | |||
| fff69ead9c | |||
| c93a833bac | |||
| 05e0e96083 | |||
| b57f796187 | |||
| ecfb9500eb | |||
| e14ce1c36f | |||
| 83cb861e54 | |||
| 1d1bc6a4d6 | |||
| c4636ac8b9 | |||
| f15448ff6e | |||
| bec1f2ec08 | |||
| f625ee5979 | |||
| e68f073505 | |||
| bffb4f9a31 | |||
| 9059a2dc45 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,10 +1,18 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# Changelog KSP
|
||||
|
||||
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
|
||||
|
||||
## 0.2.3 — HTTP Transactions — 2026-08-18
|
||||
|
||||
`0.2.3` complète la famille HTTP Transactions de `ksp-onchain-transport-lib` avec 11 wrappers typés, portant la surface HTTP typée courante à 37 méthodes : 4 foundation, 22 Accounts/Tokens/Cluster et 11 Transactions. La release stabilise la classification exacte `8 Read / RetrySafe`, `2 WriteSubmission / NeverAfterDispatch` (`requestAirdrop`, `sendTransaction`) et `1 Simulation / RetrySafe` (`simulateTransaction`), avec preuves end-to-end qu'une write submission n'est jamais resoumise après dispatch ambigu mais peut retenter lorsqu'un échec est prouvé `NotDispatched`. `getTransaction` couvre la config moderne complète et la forme bare encoding legacy dépréciée ; les variantes de transaction, metadata, version et `transactionIndex` restent préservées losslessly. `simulateTransaction` couvre sa config complète, ses invariants déterministes et son résultat riche sans introduire de client RPC Solana haut niveau ni de décodage transactionnel dans Transport. La release formalise `KSP-TRANSPORT-007` et réaudite rétroactivement les 37 wrappers courants sans remédiation fonctionnelle. Les canaries conservent 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et 15 Blocks/Economics réservées à `0.2.4`. Les graphes Cargo et les deux smokes Devnet — Transport pur read-only et composition Config -> Transport transitoire — ont été validés avant publication. Le prompt `prompts/009-V0_2_4_START_PROMPT.md` ouvre `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`.
|
||||
|
||||
## 0.2.2 — HTTP Accounts + Tokens + Cluster — 2026-08-18
|
||||
|
||||
`0.2.2` complète la surface HTTP typée de `ksp-onchain-transport-lib` avec 22 wrappers supplémentaires : 5 Accounts, 5 Tokens et 12 Cluster, portant la couverture typée totale à 26 méthodes avec les quatre canaris foundation de `0.2.1`. La release stabilise les DTOs/configs wire communs, préserve les formes Account encodées/`jsonParsed`, les `null` et champs optionnels, les selectors Token exclusifs, les contextes RPC, les structures Cluster/leader/vote et les limites locales auditées (`getMultipleAccounts <= 100`, `getProgramAccounts <= 4` filtres, `memcmp` raw <= 128 octets, `getSlotLeaders` entre 1 et 5000). Les 22 descriptors restent `Read / RetrySafe` et passent exclusivement par le flux central `descriptor -> execute_standard_rpc -> pool/admission -> executor HTTP`. La release n'ajoute aucune dépendance externe, ne crée aucun client HTTP parallèle et conserve le firewall Transport -> Config/Store/Program. Elle ajoute un smoke Devnet pur à Transport avec settings programmatiques couvrant Accounts/Tokens/Cluster, conserve séparément le smoke historique Config -> Transport comme exception transitoire, et valide les deux explicitement avant publication. Les canaries figent toujours 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et la partition typed `4 / 22 / 11 / 15`. Le prompt `prompts/008-V0_2_3_START_PROMPT.md` ouvre `0.2.3 — HTTP Transactions`.
|
||||
|
||||
## 0.2.1 — HTTP Solana foundation — 2026-08-17
|
||||
|
||||
`0.2.1` stabilise `ksp-onchain-transport-lib` comme foundation HTTP JSON-RPC Solana provider-neutral : settings publics, endpoints/pool/rôles, priorités et fairness, RPS/burst/concurrence/cooldown, deadline commune, retry/backoff, classification no-resend après dispatch ambigu, snapshots sûrs et exécution HTTP réelle via `reqwest`/rustls. La release fige un registre audité de 52 méthodes HTTP courantes et 14 méthodes historiques Deprecated/Removed, avec quatre wrappers typés canari (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) et une partition explicite des 48 méthodes restantes sur `0.2.2`–`0.2.4`. Elle ajoute `std.transport`, son schema/exemple et l'adapter `ksp-config-lib -> ksp-onchain-transport-lib`, sans dépendance inverse, ainsi que la redaction des URLs/provider credentials, la neutralisation des URLs contenues dans les `reqwest::Error`, un sink Logging Transport dédié à `info`, des fixtures HTTP déterministes, des canaries de complétude et un smoke Devnet opt-in validant la composition Config -> Transport. Le smoke cross-crates reste temporairement hébergé dans Config et doit migrer vers une future surface d'intégration/orchestration ; ce placement n'est pas un modèle pour les futurs smokes `Config + autre crate`. Le prompt `prompts/007-V0_2_2_START_PROMPT.md` ouvre `0.2.2 — HTTP Accounts + Tokens + Cluster`.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 109
|
||||
# version: 131
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.1"
|
||||
version = "0.2.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 34 -->
|
||||
<!-- version: 40 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -46,8 +46,8 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
||||
### Releases fonctionnelles décidées/pressenties
|
||||
|
||||
- [X] `0.2.1` — **HTTP transport foundation réduite par le gate `pre.001`** : crate/settings/JSON-RPC/registry 52 current + 14 deprecated historiques, pool/rôles/limites/retry, Config adapter, documentation et 4 méthodes typées canari (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) publiés stables.
|
||||
- [ ] `0.2.2` — Compléter HTTP Accounts + Tokens + Cluster : 5 méthodes Accounts restantes + 5 Tokens + 12 Cluster restantes, soit 22 méthodes.
|
||||
- [ ] `0.2.3` — Compléter les 11 méthodes HTTP Transactions, y compris write/submission technique avec politique no-resend ambigu.
|
||||
- [X] `0.2.2` — HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables.
|
||||
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
|
||||
- [ ] `0.2.4` — Compléter les 10 méthodes HTTP Blocks + 5 Economics et exécuter la compliance finale de toute la surface HTTP 52 current + 14 deprecated historiques.
|
||||
- [ ] `0.2.5` — Introduire `ksp-wallet-lib`, le format `.kspwallet`, la gestion sûre des secrets et une architecture d'import/export extensible ; exclure `WalletPolicy`.
|
||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -69,16 +69,21 @@ Le registre porte notamment :
|
||||
- remplacement historique éventuel ;
|
||||
- release de couverture typée KSP.
|
||||
|
||||
Les quatre wrappers typés de la foundation sont :
|
||||
La release stable `0.2.3` porte la surface typée à **37 méthodes courantes** :
|
||||
|
||||
```text
|
||||
getBalance
|
||||
getGenesisHash
|
||||
getHealth
|
||||
getVersion
|
||||
0.2.1 foundation : 4
|
||||
0.2.2 Accounts : 5
|
||||
0.2.2 Tokens : 5
|
||||
0.2.2 Cluster : 12
|
||||
0.2.3 Transactions : 11
|
||||
```
|
||||
|
||||
Les autres méthodes courantes peuvent déjà passer par l'exécuteur JSON-RPC standard générique lorsqu'un consumer fournit explicitement descriptor et paramètres JSON. Cette surface raw/générique **ne vaut pas couverture typée** : les wrappers et DTOs typés restants sont introduits selon la matrice HTTP KSP.
|
||||
Les quatre canaris foundation restent `getBalance`, `getGenesisHash`, `getHealth` et `getVersion`. `0.2.2` ajoute les 22 wrappers Accounts/Tokens/Cluster. `0.2.3` ajoute les 11 wrappers Transactions, dont `requestAirdrop` et `sendTransaction` en `WriteSubmission / NeverAfterDispatch` et `simulateTransaction` en `Simulation / RetrySafe`.
|
||||
|
||||
Les **15 méthodes Blocks/Economics** encore affectées à `0.2.4` peuvent déjà passer par l'exécuteur JSON-RPC standard générique lorsqu'un consumer fournit explicitement descriptor et paramètres JSON, mais cette surface raw/générique **ne vaut pas couverture typée**.
|
||||
|
||||
`KSP-TRANSPORT-007` impose qu'un wrapper typé couvre toutes les possibilités RPC supportées retenues par l'audit : paramètres/options, overloads et formes legacy encore supportées, contraintes déterministes utiles et variantes de réponse pertinentes sans perte. Le réaudit `0.2.3-pre.008` confirme cette complétude sur les 37 wrappers courants.
|
||||
|
||||
Les 14 méthodes historiques restent découvrables pour la compliance mais sont `Removed` et ne sont pas simulées comme appelables.
|
||||
|
||||
@@ -118,18 +123,27 @@ La configuration Logging de référence conserve un fichier dédié Transport à
|
||||
|
||||
Les tests par défaut sont déterministes et n'exigent pas Internet : fixtures JSON et serveur HTTP local couvrent requêtes, réponses, retry, 429, timeout, redaction et routing.
|
||||
|
||||
Un smoke Devnet live existe côté `ksp-config-lib` afin de tester la chaîne réelle :
|
||||
Deux smokes Devnet opt-in sont séparés par responsabilité :
|
||||
|
||||
```text
|
||||
Config -> std.transport/devnet_public -> HttpTransportPool
|
||||
-> getHealth/getGenesisHash/getVersion/getBalance
|
||||
Transport pur : settings programmatiques -> HttpTransportPool
|
||||
-> Accounts/Tokens/Cluster représentatifs
|
||||
-> getLatestBlockhash/isBlockhashValid/getTransactionCount
|
||||
|
||||
Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool
|
||||
-> getHealth/getGenesisHash/getVersion/getBalance
|
||||
```
|
||||
|
||||
Il est `ignored` par défaut et doit être exécuté explicitement.
|
||||
Le smoke Transport utilise pour sa branche Token la forme Devnet documentée `getTokenAccountsByOwner(owner, { programId }, { commitment: finalized, encoding: jsonParsed })`. L'owner est une Pubkey ordinaire de l'exemple officiel ; aucune présence de token account n'est exigée, donc une liste vide reste valide.
|
||||
|
||||
Les deux sont `ignored` par défaut. Le smoke Transport appartient durablement à cette crate ; le smoke cross-crates hébergé dans Config reste transitoire jusqu'à l'existence d'une surface KSP d'intégration/orchestration appropriée.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`USAGE.md`](USAGE.md) — consommation directe, Config -> Transport, API typed/raw et inspection runtime ;
|
||||
- [`../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md) — plan et matrice HTTP ;
|
||||
- [`../../docs/validation/003-V0_2_1_ONCHAIN_HTTP.md`](../../docs/validation/003-V0_2_1_ONCHAIN_HTTP.md) — matrice de clôture ;
|
||||
- [`USAGE.md`](USAGE.md) — consommation directe, Config -> Transport, API typed/raw, smokes et inspection runtime ;
|
||||
- [`../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md) — foundation HTTP stable ;
|
||||
- [`../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) — extension typed Accounts/Tokens/Cluster ;
|
||||
- [`../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) — matrice finale validée `0.2.2` ;
|
||||
- [`../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de `0.2.3` ;
|
||||
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée `0.2.3` ;
|
||||
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard HTTP.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# Utilisation de `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -86,17 +86,34 @@ let balance = pool
|
||||
.await;
|
||||
```
|
||||
|
||||
Les types de retour associés sont :
|
||||
Les quatre canaris `0.2.1` restent disponibles. `0.2.2` ajoute les wrappers typés Accounts, Tokens et Cluster. Exemples représentatifs :
|
||||
|
||||
```text
|
||||
SolanaNodeHealth
|
||||
SolanaGenesisHash
|
||||
SolanaNodeVersion
|
||||
GetBalanceResult
|
||||
SolanaRpcContext
|
||||
```rust
|
||||
let account = pool
|
||||
.get_account_info(&role, &ksp_core_lib::PRGIDPK_SOLANA_SYSTEM, None)
|
||||
.await;
|
||||
let epoch = pool.get_epoch_info(&role, None).await;
|
||||
let vote_accounts = pool.get_vote_accounts(&role, None).await;
|
||||
```
|
||||
|
||||
`GetBalanceResult::value()` renvoie les lamports et `context()` fournit le slot/API version retournés par Solana.
|
||||
La release stable `0.2.3` contient 37 wrappers typés : 4 foundation + 5 Accounts + 5 Tokens + 12 Cluster + 11 Transactions. Les DTOs Transport conservent les `null`, options et formes wire sans décodage Program/SPL métier.
|
||||
|
||||
Exemples Transaction représentatifs :
|
||||
|
||||
```rust
|
||||
let context = ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let latest = pool
|
||||
.get_latest_blockhash(&role, std::option::Option::Some(&context))
|
||||
.await;
|
||||
let transaction_count = pool
|
||||
.get_transaction_count(&role, std::option::Option::Some(&context))
|
||||
.await;
|
||||
```
|
||||
|
||||
`getTransaction` expose la config moderne complète et une forme bare-encoding legacy séparée et deprecated. `requestAirdrop` et `sendTransaction` sont des write submissions : elles utilisent la protection centrale `NeverAfterDispatch`. `simulateTransaction` reste une simulation retry-safe et conserve son résultat riche sans introduire de décodage Program.
|
||||
|
||||
## 4. Exécution JSON-RPC standard générique
|
||||
|
||||
@@ -153,12 +170,22 @@ Ne jamais journaliser l'URL complète, un token provider, un body massif, une tr
|
||||
|
||||
La configuration standard route les événements `info` de Transport vers un fichier dédié. Pour une investigation temporaire, élever uniquement ce target/sink à `debug` ou `trace`, puis revenir à `info` avant clôture du développement.
|
||||
|
||||
## 9. Smoke Devnet opt-in
|
||||
## 9. Smokes Devnet opt-in
|
||||
|
||||
Le smoke live est volontairement hors des tests par défaut :
|
||||
Le smoke **Transport pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif de `0.2.2` puis trois reads Transactions de `0.2.3` :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Il appelle `getAccountInfo`, `getTokenAccountsByOwner`, `getEpochInfo`, `getVoteAccounts`, puis `getLatestBlockhash`, `isBlockhashValid` et `getTransactionCount`. La branche Token suit la forme Devnet documentée : owner Pubkey ordinaire de l'exemple officiel, selector `programId` avec l'ID canonique du programme SPL Token, puis config explicite `commitment: finalized` + `encoding: jsonParsed`. Une réponse vide reste acceptable. La branche Transaction reste read-only : elle ne déclenche ni airdrop ni soumission de transaction et ne remplace pas les fixtures déterministes couvrant les 11 wrappers.
|
||||
|
||||
Le smoke historique de **composition Config -> Transport** reste également disponible :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Il charge le profil Config `devnet_public`, construit le pool puis appelle les quatre wrappers typés. Les endpoints publics Solana étant rate-limités et non destinés à la production, un échec réseau externe n'est pas interprété comme un échec déterministe de la suite locale.
|
||||
Il valide le profil committé `devnet_public` et les quatre canaris foundation. Il reste transitoirement hébergé dans Config : les futurs smokes cross-crates ne doivent pas faire de Config leur destination générale et devront migrer vers une surface d'intégration/orchestration dédiée lorsqu'elle existera.
|
||||
|
||||
Les endpoints publics Solana sont rate-limités et non destinés à la production. Un échec réseau externe n'est pas assimilé automatiquement à une régression locale ; les fixtures HTTP locales restent les gates reproductibles.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"lamports":1,"data":"3MN5","owner":"11111111111111111111111111111111","executable":false,"rentEpoch":0,"space":null},
|
||||
{"lamports":2,"data":["KLUv/Q==","base64+zstd"],"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":1,"space":4},
|
||||
{"lamports":3,"data":{"program":"spl-token","parsed":{"type":"account","info":{"state":"initialized"}},"space":165},"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":2,"space":165}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"pubkey":"11111111111111111111111111111111","featureSet":123,"gossip":"127.0.0.1:8001","pubsub":null,"rpc":"127.0.0.1:8899","serveRepair":"127.0.0.1:8003","shredVersion":456,"tpu":"127.0.0.1:8004","tpuForwards":null,"tpuForwardsQuic":"127.0.0.1:8006","tpuQuic":"127.0.0.1:8005","tpuVote":null,"tvu":"127.0.0.1:8002","version":"4.2.1","clientId":"Agave"}
|
||||
@@ -0,0 +1,36 @@
|
||||
[
|
||||
{
|
||||
"slot": 430000123,
|
||||
"blockTime": 1787000000,
|
||||
"transaction": ["AQIDBAUGBwg=", "base64"],
|
||||
"meta": {
|
||||
"err": null,
|
||||
"status": {"Ok": null},
|
||||
"fee": 5000,
|
||||
"preBalances": [10000],
|
||||
"postBalances": [5000]
|
||||
},
|
||||
"version": 0,
|
||||
"transactionIndex": 7
|
||||
},
|
||||
{
|
||||
"slot": 430000100,
|
||||
"blockTime": null,
|
||||
"transaction": "3Bxs4NN8M2Yn4TLb",
|
||||
"meta": null,
|
||||
"version": "legacy",
|
||||
"transactionIndex": null
|
||||
},
|
||||
{
|
||||
"slot": 430000050,
|
||||
"blockTime": null,
|
||||
"transaction": {
|
||||
"signatures": [],
|
||||
"message": {
|
||||
"accountKeys": [],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"instructions": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
"3Bxs4NN8M2Yn4TLb",
|
||||
["AQIDBAUGBwg=", "base64"],
|
||||
{
|
||||
"signatures": ["5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5"],
|
||||
"message": {
|
||||
"accountKeys": [],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"instructions": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000003},"value":{"data":["","base64"],"executable":false,"lamports":1,"owner":"not-a-pubkey","rentEpoch":0,"space":0}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000002},"value":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000001},"value":{"data":["AQIDBA==","base64"],"executable":false,"lamports":2039280,"owner":"11111111111111111111111111111111","rentEpoch":18446744073709551615,"space":4}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"pubkey":"not-a-pubkey","rpc":"127.0.0.1:8899"}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"pubkey":"11111111111111111111111111111111","featureSet":3073396398,"gossip":"127.0.0.1:8001","pubsub":null,"rpc":"127.0.0.1:8899","serveRepair":"127.0.0.1:8004","shredVersion":50093,"tpu":"127.0.0.1:8003","tpuForwards":"127.0.0.1:8004","tpuForwardsQuic":"127.0.0.1:8006","tpuQuic":"127.0.0.1:8009","tpuVote":"127.0.0.1:8005","tvu":"127.0.0.1:8000","version":"4.2.1","clientId":"Agave"},{"pubkey":"Stake11111111111111111111111111111111111111"}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"absoluteSlot":430000001,"blockHeight":429900000,"epoch":995,"slotIndex":12345,"slotsInEpoch":432000,"transactionCount":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"firstNormalEpoch":0,"firstNormalSlot":0,"leaderScheduleSlotOffset":432000,"slotsPerEpoch":432000,"warmup":false},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: unable to decode message"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000002},"value":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000001},"value":5000},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32008,"message":"No snapshot"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"full":429990000,"incremental":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"identity":"invalid-identity"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"identity":"ComputeBudget111111111111111111111111111111"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000005},"value":[{"address":"invalid-address","lamports":1}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000004},"value":[{"address":"11111111111111111111111111111111","lamports":999999999},{"address":"ComputeBudget111111111111111111111111111111","lamports":888888888}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000004},"value":{"blockhash":"","lastValidBlockHeight":431123456}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000003},"value":{"blockhash":"11111111111111111111111111111111","lastValidBlockHeight":431123456}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":null,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"11111111111111111111111111111111":[0,2,4],"ComputeBudget111111111111111111111111111111":[1,3]} ,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":430000010,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":430000011,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":890880,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000006},"value":[{"data":["","base64"],"executable":false,"lamports":10,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":0},null,{"data":{"program":"system","parsed":{"type":"nonce"},"space":80},"executable":false,"lamports":20,"owner":"11111111111111111111111111111111","rentEpoch":1,"space":80}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":42,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":3}}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000007},"value":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":42,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":3}}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"slot":431000030,"prioritizationFee":1200},{"slot":431000029,"prioritizationFee":0}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000052},"value":[null]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000051},"value":[]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000053},"value":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000050},"value":[null,{"slot":431000049,"confirmations":null,"status":{"Ok":null},"err":null,"confirmationStatus":"finalized"},{"slot":431000048,"confirmations":2,"status":{"Err":{"InstructionError":[0,"Custom"]}},"err":{"InstructionError":[0,"Custom"]},"confirmationStatus":"confirmed"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction history is not available from this node"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"signature":"newest-signature","slot":431000040,"err":null,"memo":"memo-newest","blockTime":1787000100,"confirmationStatus":"finalized","transactionIndex":9},{"signature":"older-signature","slot":431000039,"err":{"InstructionError":[0,"Custom"]},"memo":null,"blockTime":null,"confirmationStatus":"confirmed"}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":430000020,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":"not-a-pubkey","id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":"ComputeBudget111111111111111111111111111111","id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":["11111111111111111111111111111111","not-a-pubkey"],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":["11111111111111111111111111111111","ComputeBudget111111111111111111111111111111"],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: could not find account"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000001},"value":{"amount":"18446744073709551615","decimals":9,"uiAmount":null,"uiAmountString":"18446744073.709551615"}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000002},"value":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":{"program":"spl-token","parsed":{"type":"account","info":{"tokenAmount":{"amount":"1","decimals":1,"uiAmount":0.1,"uiAmountString":"0.1"}}},"space":165},"executable":false,"lamports":2039280,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":9,"space":165}}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"slot":420000004},"value":[{"pubkey":"not-a-pubkey","account":{"data":["","base64"],"executable":false,"lamports":1,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":0,"space":165}}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":null,"slot":420000003},"value":[{"pubkey":"Stake11111111111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":2039280,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":10,"space":165}}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"slot":420000006},"value":[{"address":"invalid-address","amount":"1","decimals":0,"uiAmount":1.0,"uiAmountString":"1"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000005},"value":[{"address":"11111111111111111111111111111111","amount":"9000","decimals":2,"uiAmount":90.0,"uiAmountString":"90"},{"address":"ComputeBudget111111111111111111111111111111","amount":"8000","decimals":2,"uiAmount":80.0,"uiAmountString":"80"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000007},"value":{"amount":"1000000000000000000000000","decimals":6,"uiAmount":1000000000000000000.0,"uiAmountString":"1000000000000000000"}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000061,"blockTime":1787000201,"transaction":["3Bxs4NN8M2Yn4TLb","base58"],"meta":null,"version":"legacy","transactionIndex":0},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000062,"blockTime":1787000202,"transaction":["AQIDBAUGBwg=","base64"],"meta":null,"version":0,"transactionIndex":1},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000060,"blockTime":null,"transaction":"3Bxs4NN8M2Yn4TLb","meta":null,"version":"legacy"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction version (1) is not supported by the requesting client. Please try the request again with the following configuration parameter: \"maxSupportedTransactionVersion\": 1"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000063,"blockTime":1787000203,"transaction":{"signatures":["fixture-signature"],"message":{"header":{"numRequiredSignatures":1,"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":1},"accountKeys":["11111111111111111111111111111111","SysvarRent111111111111111111111111111111111"],"recentBlockhash":"11111111111111111111111111111111","instructions":[{"programIdIndex":0,"accounts":[0,1],"data":"3Bxs4NN8","stackHeight":1}],"addressTableLookups":null}},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[100000,1],"postBalances":[95000,1],"innerInstructions":[],"logMessages":["Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"],"preTokenBalances":[],"postTokenBalances":[],"rewards":null,"loadedAddresses":{"writable":[],"readonly":[]},"returnData":{"programId":"11111111111111111111111111111111","data":["AQID","base64"]},"computeUnitsConsumed":150,"costUnits":190},"version":0,"transactionIndex":7},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000064,"blockTime":1787000204,"transaction":{"signatures":["fixture-signature-parsed"],"message":{"accountKeys":[{"pubkey":"11111111111111111111111111111111","signer":true,"writable":true,"source":"transaction"},{"pubkey":"SysvarRent111111111111111111111111111111111","signer":false,"writable":false,"source":"transaction"}],"recentBlockhash":"11111111111111111111111111111111","instructions":[{"program":"system","programId":"11111111111111111111111111111111","stackHeight":1,"parsed":{"type":"transfer","info":{"lamports":1,"source":"11111111111111111111111111111111","destination":"SysvarRent111111111111111111111111111111111"}}}]}},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[100000,1],"postBalances":[95000,1],"innerInstructions":null,"logMessages":null,"preTokenBalances":[],"postTokenBalances":[],"rewards":null,"returnData":null,"computeUnitsConsumed":150,"costUnits":190},"version":"legacy","transactionIndex":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":null,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":987654321,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"current":[{"votePubkey":"not-a-pubkey","nodePubkey":"11111111111111111111111111111111","activatedStake":1,"commission":5,"epochVoteAccount":true,"epochCredits":[],"lastVote":2,"rootSlot":1}],"delinquent":[]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"current":[{"votePubkey":"11111111111111111111111111111111","nodePubkey":"ComputeBudget111111111111111111111111111111","activatedStake":424242,"commission":8,"inflationRewardsCommissionBps":750,"epochVoteAccount":true,"epochCredits":[[700,100,90],[701,115,100]],"lastVote":999,"rootSlot":990}],"delinquent":[{"votePubkey":"ComputeBudget111111111111111111111111111111","nodePubkey":"11111111111111111111111111111111","activatedStake":1,"commission":5,"epochVoteAccount":false,"epochCredits":[],"lastVote":0,"rootSlot":0}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000005},"value":true},"id":1}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"blockhash": "11111111111111111111111111111111",
|
||||
"lastValidBlockHeight": 430123456
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{"slot": 430000123, "prioritizationFee": 1000},
|
||||
{"slot": 430000122, "prioritizationFee": 0}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: supplied blockhash is not valid"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":"airdrop-fixture-signature-111111111111111111111111111111111111111111111111","id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Error processing Instruction 0","data":{"err":{"InstructionError":[0,"Custom"]},"logs":["Program log: fixture preflight failure"],"unitsConsumed":123}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":"send-fixture-signature-22222222222222222222222222222222222222222222222222","id":1}
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"signature": "5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5",
|
||||
"slot": 430000123,
|
||||
"err": null,
|
||||
"memo": "fixture memo",
|
||||
"blockTime": 1787000000,
|
||||
"confirmationStatus": "finalized",
|
||||
"transactionIndex": 7
|
||||
},
|
||||
{
|
||||
"signature": "4Nd1mC5QfVhNq8xqP7z8d7K6a5m4n3b2v1c9x8z7y6w5u4t3s2r1q9p8o7n6m5L4K3J2H1G9F8E7D6C5B4A3",
|
||||
"slot": 430000100,
|
||||
"err": {"InstructionError": [0, "Custom"]},
|
||||
"memo": null,
|
||||
"blockTime": null,
|
||||
"confirmationStatus": "confirmed"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
null,
|
||||
{
|
||||
"slot": 430000123,
|
||||
"confirmations": null,
|
||||
"status": {"Ok": null},
|
||||
"err": null,
|
||||
"confirmationStatus": "finalized"
|
||||
},
|
||||
{
|
||||
"slot": 430000120,
|
||||
"confirmations": 3,
|
||||
"status": {"Err": {"InstructionError": [0, "Custom"]}},
|
||||
"err": {"InstructionError": [0, "Custom"]},
|
||||
"confirmationStatus": "confirmed"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "invalid transaction: failed to sanitize accounts offsets correctly"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"context": {
|
||||
"slot": 431000031,
|
||||
"apiVersion": "4.2.1"
|
||||
},
|
||||
"value": {
|
||||
"err": null,
|
||||
"logs": null,
|
||||
"accounts": null,
|
||||
"unitsConsumed": 99,
|
||||
"returnData": null,
|
||||
"replacementBlockhash": null
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"context": {
|
||||
"slot": 431000030,
|
||||
"apiVersion": "4.2.1"
|
||||
},
|
||||
"value": {
|
||||
"err": null,
|
||||
"logs": [
|
||||
"Program 11111111111111111111111111111111 invoke [1]",
|
||||
"Program 11111111111111111111111111111111 success"
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"lamports": 1000,
|
||||
"data": [
|
||||
"",
|
||||
"base64"
|
||||
],
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"executable": false,
|
||||
"rentEpoch": 0,
|
||||
"space": 0
|
||||
},
|
||||
null
|
||||
],
|
||||
"unitsConsumed": 150,
|
||||
"loadedAccountsDataSize": 64,
|
||||
"returnData": {
|
||||
"programId": "11111111111111111111111111111111",
|
||||
"data": [
|
||||
"AQI=",
|
||||
"base64"
|
||||
]
|
||||
},
|
||||
"innerInstructions": [],
|
||||
"replacementBlockhash": {
|
||||
"blockhash": "ComputeBudget111111111111111111111111111111",
|
||||
"lastValidBlockHeight": 430123999
|
||||
},
|
||||
"fee": 5000,
|
||||
"preBalances": [
|
||||
10000,
|
||||
0
|
||||
],
|
||||
"postBalances": [
|
||||
5000,
|
||||
5000
|
||||
],
|
||||
"preTokenBalances": [],
|
||||
"postTokenBalances": [],
|
||||
"loadedAddresses": {
|
||||
"writable": [],
|
||||
"readonly": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"err": null,
|
||||
"logs": ["Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"],
|
||||
"accounts": [
|
||||
{
|
||||
"lamports": 1000,
|
||||
"data": ["", "base64"],
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"executable": false,
|
||||
"rentEpoch": 0,
|
||||
"space": 0
|
||||
},
|
||||
null
|
||||
],
|
||||
"unitsConsumed": 150,
|
||||
"loadedAccountsDataSize": 64,
|
||||
"returnData": {
|
||||
"programId": "11111111111111111111111111111111",
|
||||
"data": ["AQI=", "base64"]
|
||||
},
|
||||
"innerInstructions": [],
|
||||
"replacementBlockhash": {
|
||||
"blockhash": "ComputeBudget111111111111111111111111111111",
|
||||
"lastValidBlockHeight": 430123999
|
||||
},
|
||||
"fee": 5000,
|
||||
"preBalances": [10000, 0],
|
||||
"postBalances": [5000, 5000],
|
||||
"preTokenBalances": [],
|
||||
"postTokenBalances": [],
|
||||
"loadedAddresses": {
|
||||
"writable": [],
|
||||
"readonly": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"amount":"18446744073709551615","decimals":9,"uiAmount":null,"uiAmountString":"18446744073.709551615"}
|
||||
@@ -0,0 +1 @@
|
||||
{"votePubkey":"11111111111111111111111111111111","nodePubkey":"11111111111111111111111111111111","activatedStake":424242,"commission":8,"inflationRewardsCommissionBps":750,"epochVoteAccount":true,"epochCredits":[[700,100,90],[701,115,100]],"lastVote":999,"rootSlot":990}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/error.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Error code used when HTTP transport runtime settings are invalid.
|
||||
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_settings");
|
||||
@@ -25,3 +25,5 @@ pub const ERROR_CODE_RPC_APPLICATION_ERROR: ksp_core_lib::ErrorCode = ksp_core_l
|
||||
pub const ERROR_CODE_METHOD_REMOVED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "method_removed");
|
||||
/// Error code used when a decoded response cannot satisfy the KSP transport contract expected by the caller.
|
||||
pub const ERROR_CODE_INVALID_RESPONSE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_response");
|
||||
/// Error code used when typed Solana RPC parameters violate a locally enforceable method contract.
|
||||
pub const ERROR_CODE_INVALID_RPC_PARAMETERS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_rpc_parameters");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 6
|
||||
// version: 16
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -9,8 +9,10 @@
|
||||
//! This crate owns runtime HTTP transport settings, Solana HTTP JSON-RPC envelopes and the audited standard method registry. It deliberately remains
|
||||
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config ->
|
||||
//! Transport adapter without creating a reverse dependency. Logical endpoint clients, priority-aware pools, bounded admission limits and retry/no-resend policy
|
||||
//! are available. The first typed Solana HTTP canaries execute real
|
||||
//! JSON-RPC requests while the remaining audited methods stay staged by subsequent `0.2.x` releases.
|
||||
//! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC
|
||||
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and all eleven Transaction wrappers through
|
||||
//! `pre.007`: eight reads, two write submissions with centralized no-resend protection, and retry-safe `simulateTransaction`, including complete
|
||||
//! modern/legacy `getTransaction` coverage. The `0.2.4` family remains staged.
|
||||
|
||||
mod client;
|
||||
mod constants;
|
||||
@@ -19,8 +21,13 @@ mod executor;
|
||||
mod json_rpc;
|
||||
mod pool;
|
||||
mod resilience;
|
||||
mod rpc_accounts;
|
||||
mod rpc_canary;
|
||||
mod rpc_cluster;
|
||||
mod rpc_common;
|
||||
mod rpc_method;
|
||||
mod rpc_tokens;
|
||||
mod rpc_transactions;
|
||||
mod settings;
|
||||
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
@@ -41,6 +48,8 @@ pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED;
|
||||
pub use self::error::ERROR_CODE_HTTP_REQUEST_FAILED;
|
||||
/// Error code used when a decoded response cannot satisfy the expected KSP transport contract.
|
||||
pub use self::error::ERROR_CODE_INVALID_RESPONSE;
|
||||
/// Error code used when typed Solana RPC parameters violate a locally enforceable method contract.
|
||||
pub use self::error::ERROR_CODE_INVALID_RPC_PARAMETERS;
|
||||
/// Error code used when HTTP transport runtime settings are invalid.
|
||||
pub use self::error::ERROR_CODE_INVALID_SETTINGS;
|
||||
/// Error code used when an HTTP JSON-RPC payload cannot be decoded as JSON.
|
||||
@@ -87,20 +96,82 @@ pub use self::resilience::HttpRetryCause;
|
||||
pub use self::resilience::HttpRetryDecision;
|
||||
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
|
||||
pub use self::resilience::evaluate_transport_retry;
|
||||
/// Typed transport-level Solana account without Program/SPL decoding.
|
||||
pub use self::rpc_accounts::SolanaAccount;
|
||||
/// Address and lamport balance returned by `getLargestAccounts`.
|
||||
pub use self::rpc_accounts::SolanaAccountBalance;
|
||||
/// Wire-preserving account data returned by Solana HTTP account methods.
|
||||
pub use self::rpc_accounts::SolanaAccountData;
|
||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||
pub use self::rpc_accounts::SolanaAccountEncoding;
|
||||
/// Shared account configuration used by account-info and token-account list methods.
|
||||
pub use self::rpc_accounts::SolanaAccountInfoConfig;
|
||||
/// Byte range requested from account data without decoding it locally.
|
||||
pub use self::rpc_accounts::SolanaDataSliceConfig;
|
||||
/// One public key plus its account returned by account-list RPC methods.
|
||||
pub use self::rpc_accounts::SolanaKeyedAccount;
|
||||
/// Optional configuration for `getLargestAccounts`.
|
||||
pub use self::rpc_accounts::SolanaLargestAccountsConfig;
|
||||
/// Filter accepted by `getLargestAccounts`.
|
||||
pub use self::rpc_accounts::SolanaLargestAccountsFilter;
|
||||
/// Bytes used by a `memcmp` program-account filter.
|
||||
pub use self::rpc_accounts::SolanaMemcmpBytes;
|
||||
/// One `memcmp` filter applied to account data.
|
||||
pub use self::rpc_accounts::SolanaMemcmpFilter;
|
||||
/// Parsed account payload returned by the RPC node for `jsonParsed` account data.
|
||||
pub use self::rpc_accounts::SolanaParsedAccountData;
|
||||
/// Filter accepted by the current `getProgramAccounts` implementation.
|
||||
pub use self::rpc_accounts::SolanaProgramAccountFilter;
|
||||
/// Configuration for `getProgramAccounts`.
|
||||
pub use self::rpc_accounts::SolanaProgramAccountsConfig;
|
||||
/// Result union returned by `getProgramAccounts` with or without an RPC context.
|
||||
pub use self::rpc_accounts::SolanaProgramAccountsResult;
|
||||
/// Optional typed configuration for the `getBalance` canary.
|
||||
pub use self::rpc_canary::GetBalanceConfig;
|
||||
/// Typed lamport balance returned by the `getBalance` canary.
|
||||
pub use self::rpc_canary::GetBalanceResult;
|
||||
/// Commitment level accepted by the initial typed Solana HTTP canary adapters.
|
||||
pub use self::rpc_canary::SolanaCommitment;
|
||||
/// Typed genesis hash returned by the `getGenesisHash` canary.
|
||||
pub use self::rpc_canary::SolanaGenesisHash;
|
||||
/// Typed healthy result returned by the `getHealth` canary.
|
||||
pub use self::rpc_canary::SolanaNodeHealth;
|
||||
/// Typed software-version response returned by the `getVersion` canary.
|
||||
pub use self::rpc_canary::SolanaNodeVersion;
|
||||
/// Typed Solana RPC context used by the initial account canary.
|
||||
pub use self::rpc_canary::SolanaRpcContext;
|
||||
/// Contact information returned for one cluster node.
|
||||
pub use self::rpc_cluster::SolanaClusterNode;
|
||||
/// Epoch-credit history entry returned by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaEpochCredits;
|
||||
/// Epoch information returned by `getEpochInfo`.
|
||||
pub use self::rpc_cluster::SolanaEpochInfo;
|
||||
/// Epoch schedule returned by `getEpochSchedule`.
|
||||
pub use self::rpc_cluster::SolanaEpochSchedule;
|
||||
/// Leader schedule mapping validator identities to relative epoch slot indices.
|
||||
pub use self::rpc_cluster::SolanaLeaderSchedule;
|
||||
/// Optional configuration accepted by `getLeaderSchedule`.
|
||||
pub use self::rpc_cluster::SolanaLeaderScheduleConfig;
|
||||
/// Typed parameter overload for `getLeaderSchedule`.
|
||||
pub use self::rpc_cluster::SolanaLeaderScheduleRequest;
|
||||
/// Highest full and optional incremental snapshot slots returned by `getHighestSnapshotSlot`.
|
||||
pub use self::rpc_cluster::SolanaSnapshotSlotInfo;
|
||||
/// One validator vote-account record returned by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaVoteAccountInfo;
|
||||
/// Current and delinquent validator vote-account groups returned by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaVoteAccountStatus;
|
||||
/// Configuration accepted by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaVoteAccountsConfig;
|
||||
/// Commitment level accepted by typed Solana HTTP RPC adapters.
|
||||
pub use self::rpc_common::SolanaCommitment;
|
||||
/// Optional commitment-only configuration shared by typed Solana HTTP RPC methods.
|
||||
pub use self::rpc_common::SolanaCommitmentConfig;
|
||||
/// Optional commitment and minimum-context configuration shared by typed Solana HTTP RPC methods.
|
||||
pub use self::rpc_common::SolanaContextConfig;
|
||||
/// Typed Solana RPC context shared by contextual HTTP responses.
|
||||
pub use self::rpc_common::SolanaRpcContext;
|
||||
/// Generic contextual result returned by typed Solana HTTP RPC adapters.
|
||||
pub use self::rpc_common::SolanaRpcResponse;
|
||||
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
|
||||
pub(crate) use self::rpc_common::decode_wire_json;
|
||||
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
|
||||
pub(crate) use self::rpc_common::parse_wire_pubkey;
|
||||
/// Functional category used by the audited Solana HTTP JSON-RPC registry.
|
||||
pub use self::rpc_method::HttpRpcCategory;
|
||||
/// Release that owns typed KSP coverage for one audited HTTP RPC method.
|
||||
@@ -123,6 +194,50 @@ pub use self::rpc_method::current_http_rpc_methods;
|
||||
pub use self::rpc_method::find_http_rpc_method;
|
||||
/// Returns historically documented deprecated HTTP RPC descriptors retained for compliance history.
|
||||
pub use self::rpc_method::historical_http_rpc_methods;
|
||||
/// Token-account balance entry returned by `getTokenLargestAccounts`.
|
||||
pub use self::rpc_tokens::SolanaTokenAccountBalance;
|
||||
/// Exclusive selector accepted by token-account list RPC methods.
|
||||
pub use self::rpc_tokens::SolanaTokenAccountSelector;
|
||||
/// Token amount returned by Solana HTTP token RPC methods.
|
||||
pub use self::rpc_tokens::SolanaTokenAmount;
|
||||
/// Confirmed transaction result returned by `getTransaction` when the RPC result is non-null.
|
||||
pub use self::rpc_transactions::SolanaConfirmedTransaction;
|
||||
/// Wire-preserving transaction payload returned by `getTransaction`.
|
||||
pub use self::rpc_transactions::SolanaEncodedTransaction;
|
||||
/// Modern configuration object accepted by `getTransaction`.
|
||||
pub use self::rpc_transactions::SolanaGetTransactionConfig;
|
||||
/// Blockhash information returned by `getLatestBlockhash` and optionally by simulation.
|
||||
pub use self::rpc_transactions::SolanaLatestBlockhash;
|
||||
/// One recent prioritization-fee sample returned by `getRecentPrioritizationFees`.
|
||||
pub use self::rpc_transactions::SolanaPrioritizationFee;
|
||||
/// Configuration accepted by `requestAirdrop`.
|
||||
pub use self::rpc_transactions::SolanaRequestAirdropConfig;
|
||||
/// Configuration accepted by `sendTransaction` without changing KSP transport retry semantics.
|
||||
pub use self::rpc_transactions::SolanaSendTransactionConfig;
|
||||
/// One ordered signature record returned by `getSignaturesForAddress`.
|
||||
pub use self::rpc_transactions::SolanaSignatureInfo;
|
||||
/// One non-null position returned by `getSignatureStatuses`.
|
||||
pub use self::rpc_transactions::SolanaSignatureStatus;
|
||||
/// Optional historical-search configuration accepted by `getSignatureStatuses`.
|
||||
pub use self::rpc_transactions::SolanaSignatureStatusesConfig;
|
||||
/// Pagination and context configuration accepted by `getSignaturesForAddress`.
|
||||
pub use self::rpc_transactions::SolanaSignaturesForAddressConfig;
|
||||
/// Configuration accepted by `simulateTransaction`.
|
||||
pub use self::rpc_transactions::SolanaSimulateTransactionConfig;
|
||||
/// Rich result payload returned inside the contextual `simulateTransaction` response.
|
||||
pub use self::rpc_transactions::SolanaSimulateTransactionResult;
|
||||
/// Account-return configuration nested under `simulateTransaction`.
|
||||
pub use self::rpc_transactions::SolanaSimulationAccountsConfig;
|
||||
/// Binary encoding accepted for serialized transaction input payloads.
|
||||
pub use self::rpc_transactions::SolanaTransactionBinaryEncoding;
|
||||
/// Confirmation state reported for a signature or transaction status.
|
||||
pub use self::rpc_transactions::SolanaTransactionConfirmationStatus;
|
||||
/// Encoding accepted by `getTransaction`, including its retained legacy `binary` alias.
|
||||
pub use self::rpc_transactions::SolanaTransactionEncoding;
|
||||
/// Transaction version reported by `getTransaction` when the version field is present.
|
||||
pub use self::rpc_transactions::SolanaTransactionVersion;
|
||||
/// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`.
|
||||
pub use self::rpc_transactions::SolanaWireField;
|
||||
/// Open cluster or network descriptor used by HTTP endpoint settings.
|
||||
pub use self::settings::HttpClusterName;
|
||||
/// Runtime settings for one role declared by an HTTP endpoint.
|
||||
|
||||
991
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
Normal file
991
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
Normal file
@@ -0,0 +1,991 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
// version: 4
|
||||
|
||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaAccountEncoding {
|
||||
/// Legacy binary/base58 request encoding.
|
||||
Binary,
|
||||
/// Base58 text encoding.
|
||||
Base58,
|
||||
/// Base64 text encoding.
|
||||
Base64,
|
||||
/// Parsed JSON representation when the RPC node has a parser for the account owner.
|
||||
JsonParsed,
|
||||
/// Base64 text containing zstd-compressed bytes.
|
||||
Base64Zstd,
|
||||
}
|
||||
|
||||
impl SolanaAccountEncoding {
|
||||
/// Returns the Solana JSON-RPC encoding string.
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Binary => "binary",
|
||||
Self::Base58 => "base58",
|
||||
Self::Base64 => "base64",
|
||||
Self::JsonParsed => "jsonParsed",
|
||||
Self::Base64Zstd => "base64+zstd",
|
||||
};
|
||||
}
|
||||
|
||||
fn from_wire(value: &str) -> std::option::Option<Self> {
|
||||
return match value {
|
||||
"binary" => std::option::Option::Some(Self::Binary),
|
||||
"base58" => std::option::Option::Some(Self::Base58),
|
||||
"base64" => std::option::Option::Some(Self::Base64),
|
||||
"jsonParsed" => std::option::Option::Some(Self::JsonParsed),
|
||||
"base64+zstd" => std::option::Option::Some(Self::Base64Zstd),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte range requested from account data without decoding it locally.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct SolanaDataSliceConfig {
|
||||
offset: usize,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl SolanaDataSliceConfig {
|
||||
/// Creates an account-data slice configuration.
|
||||
#[must_use]
|
||||
pub const fn new(offset: usize, length: usize) -> Self {
|
||||
return Self { offset, length };
|
||||
}
|
||||
|
||||
/// Returns the byte offset.
|
||||
#[must_use]
|
||||
pub const fn offset(&self) -> usize {
|
||||
return self.offset;
|
||||
}
|
||||
|
||||
/// Returns the requested byte length.
|
||||
#[must_use]
|
||||
pub const fn length(&self) -> usize {
|
||||
return self.length;
|
||||
}
|
||||
|
||||
fn to_json_value(self) -> serde_json::Value {
|
||||
return serde_json::json!({"offset": self.offset, "length": self.length});
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared account configuration used by account-info and token-account list methods.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaAccountInfoConfig {
|
||||
encoding: std::option::Option<crate::SolanaAccountEncoding>,
|
||||
data_slice: std::option::Option<crate::SolanaDataSliceConfig>,
|
||||
context: crate::SolanaContextConfig,
|
||||
}
|
||||
|
||||
impl SolanaAccountInfoConfig {
|
||||
/// Creates an explicit account-info configuration.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
encoding: std::option::Option<crate::SolanaAccountEncoding>,
|
||||
data_slice: std::option::Option<crate::SolanaDataSliceConfig>,
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
) -> Self {
|
||||
return Self { encoding, data_slice, context: crate::SolanaContextConfig::new(commitment, min_context_slot) };
|
||||
}
|
||||
|
||||
/// Returns the optional account-data encoding.
|
||||
#[must_use]
|
||||
pub const fn encoding(&self) -> std::option::Option<crate::SolanaAccountEncoding> {
|
||||
return self.encoding;
|
||||
}
|
||||
|
||||
/// Returns the optional account-data slice.
|
||||
#[must_use]
|
||||
pub const fn data_slice(&self) -> std::option::Option<crate::SolanaDataSliceConfig> {
|
||||
return self.data_slice;
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.context.commitment();
|
||||
}
|
||||
|
||||
/// Returns the optional minimum context slot.
|
||||
#[must_use]
|
||||
pub const fn min_context_slot(&self) -> std::option::Option<u64> {
|
||||
return self.context.min_context_slot();
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
return self.encoding.is_none() && self.data_slice.is_none() && self.commitment().is_none() && self.min_context_slot().is_none();
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let context_value = self.context.to_json_value();
|
||||
let mut object = match context_value {
|
||||
serde_json::Value::Object(object) => object,
|
||||
_ => serde_json::Map::new(),
|
||||
};
|
||||
if let std::option::Option::Some(encoding) = self.encoding {
|
||||
object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(data_slice) = self.data_slice {
|
||||
object.insert("dataSlice".to_owned(), data_slice.to_json_value());
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter accepted by `getLargestAccounts`.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaLargestAccountsFilter {
|
||||
/// Return only circulating accounts.
|
||||
Circulating,
|
||||
/// Return only non-circulating accounts.
|
||||
NonCirculating,
|
||||
}
|
||||
|
||||
impl SolanaLargestAccountsFilter {
|
||||
fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Circulating => "circulating",
|
||||
Self::NonCirculating => "nonCirculating",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional configuration for `getLargestAccounts`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaLargestAccountsConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
filter: std::option::Option<crate::SolanaLargestAccountsFilter>,
|
||||
sort_results: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
impl SolanaLargestAccountsConfig {
|
||||
/// Creates a largest-accounts configuration.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
filter: std::option::Option<crate::SolanaLargestAccountsFilter>,
|
||||
sort_results: std::option::Option<bool>,
|
||||
) -> Self {
|
||||
return Self { commitment, filter, sort_results };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Returns the optional circulating-account filter.
|
||||
#[must_use]
|
||||
pub const fn filter(&self) -> std::option::Option<crate::SolanaLargestAccountsFilter> {
|
||||
return self.filter;
|
||||
}
|
||||
|
||||
/// Returns the optional server-side result-sorting request.
|
||||
#[must_use]
|
||||
pub const fn sort_results(&self) -> std::option::Option<bool> {
|
||||
return self.sort_results;
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
return self.commitment.is_none() && self.filter.is_none() && self.sort_results.is_none();
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(filter) = self.filter {
|
||||
object.insert("filter".to_owned(), serde_json::Value::String(filter.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(sort_results) = self.sort_results {
|
||||
object.insert("sortResults".to_owned(), serde_json::Value::Bool(sort_results));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes used by a `memcmp` program-account filter.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaMemcmpBytes {
|
||||
/// Base58-encoded bytes.
|
||||
Base58(std::string::String),
|
||||
/// Base64-encoded bytes.
|
||||
Base64(std::string::String),
|
||||
/// Raw byte array.
|
||||
Bytes(std::vec::Vec<u8>),
|
||||
}
|
||||
|
||||
/// One `memcmp` filter applied to account data.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaMemcmpFilter {
|
||||
offset: usize,
|
||||
bytes: crate::SolanaMemcmpBytes,
|
||||
}
|
||||
|
||||
impl SolanaMemcmpFilter {
|
||||
/// Creates a `memcmp` filter without locally decoding encoded string data.
|
||||
#[must_use]
|
||||
pub fn new(offset: usize, bytes: crate::SolanaMemcmpBytes) -> Self {
|
||||
return Self { offset, bytes };
|
||||
}
|
||||
|
||||
/// Returns the account-data byte offset.
|
||||
#[must_use]
|
||||
pub const fn offset(&self) -> usize {
|
||||
return self.offset;
|
||||
}
|
||||
|
||||
/// Returns the encoded or raw bytes.
|
||||
#[must_use]
|
||||
pub const fn bytes(&self) -> &crate::SolanaMemcmpBytes {
|
||||
return &self.bytes;
|
||||
}
|
||||
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
return match &self.bytes {
|
||||
crate::SolanaMemcmpBytes::Base58(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "base58"}),
|
||||
crate::SolanaMemcmpBytes::Base64(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "base64"}),
|
||||
crate::SolanaMemcmpBytes::Bytes(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "bytes"}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter accepted by the current `getProgramAccounts` implementation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaProgramAccountFilter {
|
||||
/// Require an exact account data size.
|
||||
DataSize(u64),
|
||||
/// Compare bytes at one account-data offset.
|
||||
Memcmp(crate::SolanaMemcmpFilter),
|
||||
/// Require a valid SPL Token account-state layout according to the RPC implementation.
|
||||
TokenAccountState,
|
||||
}
|
||||
|
||||
impl SolanaProgramAccountFilter {
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
return match self {
|
||||
Self::DataSize(size) => serde_json::json!({"dataSize": size}),
|
||||
Self::Memcmp(filter) => serde_json::json!({"memcmp": filter.to_json_value()}),
|
||||
Self::TokenAccountState => serde_json::Value::String("tokenAccountState".to_owned()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for `getProgramAccounts` built from the shared account config plus program filters.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaProgramAccountsConfig {
|
||||
account_config: crate::SolanaAccountInfoConfig,
|
||||
filters: std::vec::Vec<crate::SolanaProgramAccountFilter>,
|
||||
with_context: std::option::Option<bool>,
|
||||
sort_results: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
impl SolanaProgramAccountsConfig {
|
||||
/// Creates a program-accounts configuration.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
account_config: crate::SolanaAccountInfoConfig,
|
||||
filters: std::vec::Vec<crate::SolanaProgramAccountFilter>,
|
||||
with_context: std::option::Option<bool>,
|
||||
sort_results: std::option::Option<bool>,
|
||||
) -> Self {
|
||||
return Self { account_config, filters, with_context, sort_results };
|
||||
}
|
||||
|
||||
/// Returns the shared account configuration.
|
||||
#[must_use]
|
||||
pub const fn account_config(&self) -> &crate::SolanaAccountInfoConfig {
|
||||
return &self.account_config;
|
||||
}
|
||||
|
||||
/// Returns the ordered program-account filters.
|
||||
#[must_use]
|
||||
pub fn filters(&self) -> &[crate::SolanaProgramAccountFilter] {
|
||||
return self.filters.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the optional context-wrapper request.
|
||||
#[must_use]
|
||||
pub const fn with_context(&self) -> std::option::Option<bool> {
|
||||
return self.with_context;
|
||||
}
|
||||
|
||||
/// Returns the optional server-side sorting request.
|
||||
#[must_use]
|
||||
pub const fn sort_results(&self) -> std::option::Option<bool> {
|
||||
return self.sort_results;
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
return self.account_config.is_empty() && self.filters.is_empty() && self.with_context.is_none() && self.sort_results.is_none();
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let account_value = self.account_config.to_json_value();
|
||||
let mut object = match account_value {
|
||||
serde_json::Value::Object(object) => object,
|
||||
_ => serde_json::Map::new(),
|
||||
};
|
||||
if !self.filters.is_empty() {
|
||||
let values = self.filters.iter().map(crate::SolanaProgramAccountFilter::to_json_value).collect::<std::vec::Vec<_>>();
|
||||
object.insert("filters".to_owned(), serde_json::Value::Array(values));
|
||||
}
|
||||
if let std::option::Option::Some(with_context) = self.with_context {
|
||||
object.insert("withContext".to_owned(), serde_json::Value::Bool(with_context));
|
||||
}
|
||||
if let std::option::Option::Some(sort_results) = self.sort_results {
|
||||
object.insert("sortResults".to_owned(), serde_json::Value::Bool(sort_results));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed account payload returned by the RPC node for `jsonParsed` account data.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaParsedAccountData {
|
||||
program: std::string::String,
|
||||
parsed: serde_json::Value,
|
||||
space: u64,
|
||||
}
|
||||
|
||||
impl SolanaParsedAccountData {
|
||||
/// Returns the parser/program label reported by the RPC node.
|
||||
#[must_use]
|
||||
pub fn program(&self) -> &str {
|
||||
return self.program.as_str();
|
||||
}
|
||||
|
||||
/// Returns the parsed JSON payload without converting it to a Program/SPL domain model.
|
||||
#[must_use]
|
||||
pub const fn parsed(&self) -> &serde_json::Value {
|
||||
return &self.parsed;
|
||||
}
|
||||
|
||||
/// Returns the account-data space reported inside the parsed payload.
|
||||
#[must_use]
|
||||
pub const fn space(&self) -> u64 {
|
||||
return self.space;
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire-preserving account data returned by Solana HTTP account methods.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SolanaAccountData {
|
||||
/// Legacy single-string binary form retained for backwards compatibility.
|
||||
LegacyBinary(std::string::String),
|
||||
/// Encoded tuple `[data, encoding]`.
|
||||
Encoded {
|
||||
/// Encoded account bytes.
|
||||
data: std::string::String,
|
||||
/// Encoding label returned by the RPC node.
|
||||
encoding: crate::SolanaAccountEncoding,
|
||||
},
|
||||
/// Parsed JSON object returned by the RPC node.
|
||||
JsonParsed(crate::SolanaParsedAccountData),
|
||||
}
|
||||
|
||||
impl SolanaAccountData {
|
||||
fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireAccountData>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match wire {
|
||||
WireAccountData::LegacyBinary(value) => std::result::Result::Ok(Self::LegacyBinary(value)),
|
||||
WireAccountData::JsonParsed(value) => {
|
||||
std::result::Result::Ok(Self::JsonParsed(crate::SolanaParsedAccountData { program: value.program, parsed: value.parsed, space: value.space }))
|
||||
},
|
||||
WireAccountData::Encoded((data, encoding)) => {
|
||||
let parsed = crate::SolanaAccountEncoding::from_wire(encoding.as_str());
|
||||
let encoding = match parsed {
|
||||
std::option::Option::Some(encoding) => encoding,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "account data tuple uses an unknown encoding")
|
||||
.with_context("rpc_method", method),
|
||||
);
|
||||
},
|
||||
};
|
||||
if encoding == crate::SolanaAccountEncoding::Binary || encoding == crate::SolanaAccountEncoding::JsonParsed {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "account data tuple uses an invalid tuple encoding")
|
||||
.with_context("rpc_method", method),
|
||||
);
|
||||
}
|
||||
std::result::Result::Ok(Self::Encoded { data, encoding })
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed transport-level Solana account without Program/SPL decoding.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaAccount {
|
||||
lamports: u64,
|
||||
data: crate::SolanaAccountData,
|
||||
owner: ksp_core_lib::Pubkey,
|
||||
executable: bool,
|
||||
rent_epoch: u64,
|
||||
space: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaAccount {
|
||||
/// Returns the account balance in lamports.
|
||||
#[must_use]
|
||||
pub const fn lamports(&self) -> u64 {
|
||||
return self.lamports;
|
||||
}
|
||||
|
||||
/// Returns the wire-preserving account data.
|
||||
#[must_use]
|
||||
pub const fn data(&self) -> &crate::SolanaAccountData {
|
||||
return &self.data;
|
||||
}
|
||||
|
||||
/// Returns the account owner program public key.
|
||||
#[must_use]
|
||||
pub const fn owner(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.owner;
|
||||
}
|
||||
|
||||
/// Returns whether the account is executable.
|
||||
#[must_use]
|
||||
pub const fn executable(&self) -> bool {
|
||||
return self.executable;
|
||||
}
|
||||
|
||||
/// Returns the rent epoch reported by the RPC node.
|
||||
#[must_use]
|
||||
pub const fn rent_epoch(&self) -> u64 {
|
||||
return self.rent_epoch;
|
||||
}
|
||||
|
||||
/// Returns the optional account data-space field.
|
||||
#[must_use]
|
||||
pub const fn space(&self) -> std::option::Option<u64> {
|
||||
return self.space;
|
||||
}
|
||||
|
||||
/// Decodes one account DTO from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireAccount>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner = crate::parse_wire_pubkey(method, "owner", wire.owner.as_str());
|
||||
let owner = match owner {
|
||||
std::result::Result::Ok(owner) => owner,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let data = crate::SolanaAccountData::decode_wire(method, wire.data);
|
||||
let data = match data {
|
||||
std::result::Result::Ok(data) => data,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self {
|
||||
lamports: wire.lamports,
|
||||
data,
|
||||
owner,
|
||||
executable: wire.executable,
|
||||
rent_epoch: wire.rent_epoch,
|
||||
space: wire.space,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// One public key plus its account returned by account-list RPC methods.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaKeyedAccount {
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
account: crate::SolanaAccount,
|
||||
}
|
||||
|
||||
impl SolanaKeyedAccount {
|
||||
/// Returns the account public key.
|
||||
#[must_use]
|
||||
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.pubkey;
|
||||
}
|
||||
|
||||
/// Returns the account payload.
|
||||
#[must_use]
|
||||
pub const fn account(&self) -> &crate::SolanaAccount {
|
||||
return &self.account;
|
||||
}
|
||||
|
||||
/// Decodes one keyed account from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireKeyedAccount>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pubkey = crate::parse_wire_pubkey(method, "pubkey", wire.pubkey.as_str());
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let account = crate::SolanaAccount::decode_wire(method, wire.account);
|
||||
let account = match account {
|
||||
std::result::Result::Ok(account) => account,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { pubkey, account });
|
||||
}
|
||||
}
|
||||
|
||||
/// Address and lamport balance returned by `getLargestAccounts`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaAccountBalance {
|
||||
address: ksp_core_lib::Pubkey,
|
||||
lamports: u64,
|
||||
}
|
||||
|
||||
impl SolanaAccountBalance {
|
||||
/// Returns the account address.
|
||||
#[must_use]
|
||||
pub const fn address(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.address;
|
||||
}
|
||||
|
||||
/// Returns the balance in lamports.
|
||||
#[must_use]
|
||||
pub const fn lamports(&self) -> u64 {
|
||||
return self.lamports;
|
||||
}
|
||||
|
||||
/// Decodes one account-balance entry from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireAccountBalance>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let address = crate::parse_wire_pubkey(method, "address", wire.address.as_str());
|
||||
return match address {
|
||||
std::result::Result::Ok(address) => std::result::Result::Ok(Self { address, lamports: wire.lamports }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Result union returned by `getProgramAccounts` with or without an RPC context.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SolanaProgramAccountsResult {
|
||||
/// Bare account list returned when `withContext` is false or absent.
|
||||
Accounts(std::vec::Vec<crate::SolanaKeyedAccount>),
|
||||
/// Contextual account list returned when `withContext` is true.
|
||||
Context(crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>),
|
||||
}
|
||||
|
||||
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
|
||||
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
|
||||
const MAX_MEMCMP_BYTES: usize = 128;
|
||||
|
||||
impl crate::HttpTransportPool {
|
||||
/// Executes typed `getAccountInfo` through the common KSP HTTP transport path.
|
||||
pub async fn get_account_info(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
account: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaAccountInfoConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::option::Option<crate::SolanaAccount>>> {
|
||||
let method_result = account_descriptor("getAccountInfo");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut params = std::vec![serde_json::Value::String(account.to_string())];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_account_info_response("getAccountInfo", value);
|
||||
}
|
||||
|
||||
/// Executes typed `getLargestAccounts` through the common KSP HTTP transport path.
|
||||
pub async fn get_largest_accounts(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
config: std::option::Option<&crate::SolanaLargestAccountsConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaAccountBalance>>> {
|
||||
let method_result = account_descriptor("getLargestAccounts");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut params = std::vec::Vec::new();
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_largest_accounts_response("getLargestAccounts", value);
|
||||
}
|
||||
|
||||
/// Executes typed `getMinimumBalanceForRentExemption` through the common KSP HTTP transport path.
|
||||
pub async fn get_minimum_balance_for_rent_exemption(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
data_len: usize,
|
||||
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||
) -> ksp_core_lib::Result<u64> {
|
||||
let method_result = account_descriptor("getMinimumBalanceForRentExemption");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let data_len_value = serde_json::to_value(data_len);
|
||||
let data_len_value = match data_len_value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_ENCODE_FAILED, "rent-exemption data length could not be encoded")
|
||||
.with_context("rpc_method", "getMinimumBalanceForRentExemption")
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut params = std::vec![data_len_value];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& config.commitment().is_some()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decoded = crate::decode_wire_json::<u64>("getMinimumBalanceForRentExemption", value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getMultipleAccounts` through the common KSP HTTP transport path.
|
||||
pub async fn get_multiple_accounts(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
accounts: &[ksp_core_lib::Pubkey],
|
||||
config: std::option::Option<&crate::SolanaAccountInfoConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<std::option::Option<crate::SolanaAccount>>>> {
|
||||
if accounts.len() > MAX_MULTIPLE_ACCOUNTS {
|
||||
return invalid_account_parameters("getMultipleAccounts", "getMultipleAccounts accepts at most 100 public keys", "account_count", accounts.len());
|
||||
}
|
||||
let method_result = account_descriptor("getMultipleAccounts");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let addresses = accounts.iter().map(std::string::ToString::to_string).map(serde_json::Value::String).collect::<std::vec::Vec<_>>();
|
||||
let mut params = std::vec![serde_json::Value::Array(addresses)];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_multiple_accounts_response("getMultipleAccounts", value, accounts.len());
|
||||
}
|
||||
|
||||
/// Executes typed `getProgramAccounts` through the common KSP HTTP transport path.
|
||||
pub async fn get_program_accounts(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
program_id: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaProgramAccountsConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaProgramAccountsResult> {
|
||||
if let std::option::Option::Some(config) = config {
|
||||
let validation = validate_program_account_filters(config.filters());
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
let method_result = account_descriptor("getProgramAccounts");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut params = std::vec![serde_json::Value::String(program_id.to_string())];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_program_accounts_response("getProgramAccounts", value);
|
||||
}
|
||||
}
|
||||
|
||||
fn account_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
||||
let descriptor = crate::find_http_rpc_method(method);
|
||||
return match descriptor {
|
||||
std::option::Option::Some(descriptor)
|
||||
if descriptor.category() == crate::HttpRpcCategory::Accounts && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
|
||||
{
|
||||
std::result::Result::Ok(descriptor)
|
||||
},
|
||||
_ => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Accounts descriptor is missing from the audited 0.2.2 registry")
|
||||
.with_context("rpc_method", method),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn invalid_account_parameters<T>(method: &str, message: &str, field: &'static str, value: usize) -> ksp_core_lib::Result<T> {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
|
||||
.with_context("rpc_method", method)
|
||||
.with_context(field, value.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_program_account_filters(filters: &[crate::SolanaProgramAccountFilter]) -> ksp_core_lib::Result<()> {
|
||||
if filters.len() > MAX_PROGRAM_ACCOUNT_FILTERS {
|
||||
return invalid_account_parameters(
|
||||
"getProgramAccounts",
|
||||
"getProgramAccounts accepts at most 4 filters on the targeted Agave runtime",
|
||||
"filter_count",
|
||||
filters.len(),
|
||||
);
|
||||
}
|
||||
for filter in filters {
|
||||
if let crate::SolanaProgramAccountFilter::Memcmp(memcmp) = filter
|
||||
&& let crate::SolanaMemcmpBytes::Bytes(bytes) = memcmp.bytes()
|
||||
&& bytes.len() > MAX_MEMCMP_BYTES
|
||||
{
|
||||
return invalid_account_parameters(
|
||||
"getProgramAccounts",
|
||||
"raw getProgramAccounts memcmp data accepts at most 128 bytes",
|
||||
"memcmp_byte_count",
|
||||
bytes.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn decode_account_info_response(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::option::Option<crate::SolanaAccount>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::option::Option<serde_json::Value>>>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let account = match wire.value {
|
||||
std::option::Option::Some(value) => {
|
||||
let account = crate::SolanaAccount::decode_wire(method, value);
|
||||
match account {
|
||||
std::result::Result::Ok(account) => std::option::Option::Some(account),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, account));
|
||||
}
|
||||
|
||||
fn decode_largest_accounts_response(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaAccountBalance>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::vec::Vec<serde_json::Value>>>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut values = std::vec::Vec::with_capacity(wire.value.len());
|
||||
for value in wire.value {
|
||||
let decoded = crate::SolanaAccountBalance::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(decoded) => values.push(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, values));
|
||||
}
|
||||
|
||||
fn decode_multiple_accounts_response(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
expected_count: usize,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<std::option::Option<crate::SolanaAccount>>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::vec::Vec<std::option::Option<serde_json::Value>>>>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut values = std::vec::Vec::with_capacity(wire.value.len());
|
||||
for value in wire.value {
|
||||
match value {
|
||||
std::option::Option::Some(value) => {
|
||||
let decoded = crate::SolanaAccount::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(decoded) => values.push(std::option::Option::Some(decoded)),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => values.push(std::option::Option::None),
|
||||
}
|
||||
}
|
||||
if values.len() != expected_count {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "getMultipleAccounts result count does not match the requested account count")
|
||||
.with_context("rpc_method", method)
|
||||
.with_context("expected_count", expected_count.to_string())
|
||||
.with_context("actual_count", values.len().to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, values));
|
||||
}
|
||||
|
||||
fn decode_program_accounts_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaProgramAccountsResult> {
|
||||
let decoded = crate::decode_wire_json::<WireProgramAccountsResult>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match wire {
|
||||
WireProgramAccountsResult::Accounts(values) => {
|
||||
let decoded = decode_keyed_accounts(method, values);
|
||||
match decoded {
|
||||
std::result::Result::Ok(values) => std::result::Result::Ok(crate::SolanaProgramAccountsResult::Accounts(values)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
WireProgramAccountsResult::Context(wire) => {
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let values = decode_keyed_accounts(method, wire.value);
|
||||
match values {
|
||||
std::result::Result::Ok(values) => {
|
||||
std::result::Result::Ok(crate::SolanaProgramAccountsResult::Context(crate::SolanaRpcResponse::new(context, values)))
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_keyed_accounts(method: &str, values: std::vec::Vec<serde_json::Value>) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaKeyedAccount>> {
|
||||
let mut decoded_values = std::vec::Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let decoded = crate::SolanaKeyedAccount::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(decoded) => decoded_values.push(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(decoded_values);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcResponse<T> {
|
||||
context: serde_json::Value,
|
||||
value: T,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireProgramAccountsResult {
|
||||
Context(WireRpcResponse<std::vec::Vec<serde_json::Value>>),
|
||||
Accounts(std::vec::Vec<serde_json::Value>),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireAccountData {
|
||||
LegacyBinary(std::string::String),
|
||||
JsonParsed(WireParsedAccountData),
|
||||
Encoded((std::string::String, std::string::String)),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireParsedAccountData {
|
||||
program: std::string::String,
|
||||
parsed: serde_json::Value,
|
||||
space: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccount {
|
||||
lamports: u64,
|
||||
data: serde_json::Value,
|
||||
owner: std::string::String,
|
||||
executable: bool,
|
||||
#[serde(rename = "rentEpoch")]
|
||||
rent_epoch: u64,
|
||||
#[serde(default)]
|
||||
space: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireKeyedAccount {
|
||||
pubkey: std::string::String,
|
||||
account: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccountBalance {
|
||||
address: std::string::String,
|
||||
lamports: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_accounts.rs"]
|
||||
mod tests;
|
||||
@@ -1,30 +1,7 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_canary.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Commitment level accepted by the initial typed Solana HTTP canary adapters.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaCommitment {
|
||||
/// Query the most recent processed bank.
|
||||
Processed,
|
||||
/// Query a bank confirmed by cluster vote.
|
||||
Confirmed,
|
||||
/// Query a finalized bank.
|
||||
Finalized,
|
||||
}
|
||||
|
||||
impl SolanaCommitment {
|
||||
/// Returns the Solana JSON-RPC commitment string.
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Processed => "processed",
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::Finalized => "finalized",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional typed configuration for `getBalance`.
|
||||
/// Optional typed configuration for `getBalance` retained for the `0.2.1` public canary contract.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct GetBalanceConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
@@ -55,14 +32,7 @@ impl GetBalanceConfig {
|
||||
}
|
||||
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
|
||||
object.insert("minContextSlot".to_owned(), serde_json::Value::Number(min_context_slot.into()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
return crate::SolanaContextConfig::new(self.commitment, self.min_context_slot).to_json_value();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,30 +78,6 @@ impl SolanaNodeVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed Solana RPC context used by the initial account canary.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaRpcContext {
|
||||
slot: u64,
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl SolanaRpcContext {
|
||||
/// Returns the context slot reported by the RPC node.
|
||||
#[must_use]
|
||||
pub const fn slot(&self) -> u64 {
|
||||
return self.slot;
|
||||
}
|
||||
|
||||
/// Returns the optional RPC API version reported by the node.
|
||||
#[must_use]
|
||||
pub fn api_version(&self) -> std::option::Option<&str> {
|
||||
return match self.api_version.as_ref() {
|
||||
std::option::Option::Some(value) => std::option::Option::Some(value.as_str()),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed lamport balance returned by the `getBalance` canary.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GetBalanceResult {
|
||||
@@ -245,10 +191,12 @@ impl crate::HttpTransportPool {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(error) => return invalid_canary_decode("getBalance", error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::GetBalanceResult {
|
||||
context: crate::SolanaRpcContext { slot: decoded.context.slot, api_version: decoded.context.api_version },
|
||||
value: decoded.value,
|
||||
});
|
||||
let context = crate::SolanaRpcContext::decode_wire("getBalance", decoded.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::GetBalanceResult { context, value: decoded.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,16 +208,9 @@ struct WireNodeVersion {
|
||||
feature_set: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcContext {
|
||||
slot: u64,
|
||||
#[serde(rename = "apiVersion", default)]
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireBalanceResult {
|
||||
context: WireRpcContext,
|
||||
context: serde_json::Value,
|
||||
value: u64,
|
||||
}
|
||||
|
||||
|
||||
943
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
Normal file
943
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
Normal file
@@ -0,0 +1,943 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
// version: 4
|
||||
|
||||
const MAX_GET_SLOT_LEADERS: u64 = 5_000;
|
||||
|
||||
/// Contact information returned for one cluster node.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaClusterNode {
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
feature_set: std::option::Option<u32>,
|
||||
gossip: std::option::Option<std::string::String>,
|
||||
pubsub: std::option::Option<std::string::String>,
|
||||
rpc: std::option::Option<std::string::String>,
|
||||
serve_repair: std::option::Option<std::string::String>,
|
||||
shred_version: std::option::Option<u16>,
|
||||
tpu: std::option::Option<std::string::String>,
|
||||
tpu_forwards: std::option::Option<std::string::String>,
|
||||
tpu_forwards_quic: std::option::Option<std::string::String>,
|
||||
tpu_quic: std::option::Option<std::string::String>,
|
||||
tpu_vote: std::option::Option<std::string::String>,
|
||||
tvu: std::option::Option<std::string::String>,
|
||||
version: std::option::Option<std::string::String>,
|
||||
client_id: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl SolanaClusterNode {
|
||||
/// Returns the node identity public key.
|
||||
#[must_use]
|
||||
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.pubkey;
|
||||
}
|
||||
/// Returns the optional feature-set identifier.
|
||||
#[must_use]
|
||||
pub const fn feature_set(&self) -> std::option::Option<u32> {
|
||||
return self.feature_set;
|
||||
}
|
||||
/// Returns the optional gossip endpoint.
|
||||
#[must_use]
|
||||
pub fn gossip(&self) -> std::option::Option<&str> {
|
||||
return self.gossip.as_deref();
|
||||
}
|
||||
/// Returns the optional PubSub endpoint.
|
||||
#[must_use]
|
||||
pub fn pubsub(&self) -> std::option::Option<&str> {
|
||||
return self.pubsub.as_deref();
|
||||
}
|
||||
/// Returns the optional JSON-RPC endpoint.
|
||||
#[must_use]
|
||||
pub fn rpc(&self) -> std::option::Option<&str> {
|
||||
return self.rpc.as_deref();
|
||||
}
|
||||
/// Returns the optional repair endpoint.
|
||||
#[must_use]
|
||||
pub fn serve_repair(&self) -> std::option::Option<&str> {
|
||||
return self.serve_repair.as_deref();
|
||||
}
|
||||
/// Returns the optional shred version.
|
||||
#[must_use]
|
||||
pub const fn shred_version(&self) -> std::option::Option<u16> {
|
||||
return self.shred_version;
|
||||
}
|
||||
/// Returns the optional TPU endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu(&self) -> std::option::Option<&str> {
|
||||
return self.tpu.as_deref();
|
||||
}
|
||||
/// Returns the optional TPU forwards endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_forwards(&self) -> std::option::Option<&str> {
|
||||
return self.tpu_forwards.as_deref();
|
||||
}
|
||||
/// Returns the optional TPU forwards QUIC endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_forwards_quic(&self) -> std::option::Option<&str> {
|
||||
return self.tpu_forwards_quic.as_deref();
|
||||
}
|
||||
/// Returns the optional TPU QUIC endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_quic(&self) -> std::option::Option<&str> {
|
||||
return self.tpu_quic.as_deref();
|
||||
}
|
||||
/// Returns the optional TPU vote endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_vote(&self) -> std::option::Option<&str> {
|
||||
return self.tpu_vote.as_deref();
|
||||
}
|
||||
/// Returns the optional TVU endpoint.
|
||||
#[must_use]
|
||||
pub fn tvu(&self) -> std::option::Option<&str> {
|
||||
return self.tvu.as_deref();
|
||||
}
|
||||
/// Returns the optional software-version string.
|
||||
#[must_use]
|
||||
pub fn version(&self) -> std::option::Option<&str> {
|
||||
return self.version.as_deref();
|
||||
}
|
||||
/// Returns the optional Agave client identifier extension.
|
||||
#[must_use]
|
||||
pub fn client_id(&self) -> std::option::Option<&str> {
|
||||
return self.client_id.as_deref();
|
||||
}
|
||||
|
||||
/// Decodes one cluster-node contact record from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireClusterNode>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pubkey = crate::parse_wire_pubkey(method, "pubkey", wire.pubkey.as_str());
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self {
|
||||
pubkey,
|
||||
feature_set: wire.feature_set,
|
||||
gossip: wire.gossip,
|
||||
pubsub: wire.pubsub,
|
||||
rpc: wire.rpc,
|
||||
serve_repair: wire.serve_repair,
|
||||
shred_version: wire.shred_version,
|
||||
tpu: wire.tpu,
|
||||
tpu_forwards: wire.tpu_forwards,
|
||||
tpu_forwards_quic: wire.tpu_forwards_quic,
|
||||
tpu_quic: wire.tpu_quic,
|
||||
tpu_vote: wire.tpu_vote,
|
||||
tvu: wire.tvu,
|
||||
version: wire.version,
|
||||
client_id: wire.client_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Epoch information returned by `getEpochInfo`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaEpochInfo {
|
||||
absolute_slot: u64,
|
||||
block_height: u64,
|
||||
epoch: u64,
|
||||
slot_index: u64,
|
||||
slots_in_epoch: u64,
|
||||
transaction_count: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaEpochInfo {
|
||||
/// Returns the absolute slot.
|
||||
#[must_use]
|
||||
pub const fn absolute_slot(&self) -> u64 {
|
||||
return self.absolute_slot;
|
||||
}
|
||||
/// Returns the block height.
|
||||
#[must_use]
|
||||
pub const fn block_height(&self) -> u64 {
|
||||
return self.block_height;
|
||||
}
|
||||
/// Returns the epoch number.
|
||||
#[must_use]
|
||||
pub const fn epoch(&self) -> u64 {
|
||||
return self.epoch;
|
||||
}
|
||||
/// Returns the slot index within the epoch.
|
||||
#[must_use]
|
||||
pub const fn slot_index(&self) -> u64 {
|
||||
return self.slot_index;
|
||||
}
|
||||
/// Returns the number of slots in the epoch.
|
||||
#[must_use]
|
||||
pub const fn slots_in_epoch(&self) -> u64 {
|
||||
return self.slots_in_epoch;
|
||||
}
|
||||
/// Returns the nullable transaction count.
|
||||
#[must_use]
|
||||
pub const fn transaction_count(&self) -> std::option::Option<u64> {
|
||||
return self.transaction_count;
|
||||
}
|
||||
|
||||
/// Decodes epoch information from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireEpochInfo>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
|
||||
absolute_slot: wire.absolute_slot,
|
||||
block_height: wire.block_height,
|
||||
epoch: wire.epoch,
|
||||
slot_index: wire.slot_index,
|
||||
slots_in_epoch: wire.slots_in_epoch,
|
||||
transaction_count: wire.transaction_count,
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Epoch schedule returned by `getEpochSchedule`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaEpochSchedule {
|
||||
first_normal_epoch: u64,
|
||||
first_normal_slot: u64,
|
||||
leader_schedule_slot_offset: u64,
|
||||
slots_per_epoch: u64,
|
||||
warmup: bool,
|
||||
}
|
||||
|
||||
impl SolanaEpochSchedule {
|
||||
/// Returns the first normal epoch.
|
||||
#[must_use]
|
||||
pub const fn first_normal_epoch(&self) -> u64 {
|
||||
return self.first_normal_epoch;
|
||||
}
|
||||
/// Returns the first normal slot.
|
||||
#[must_use]
|
||||
pub const fn first_normal_slot(&self) -> u64 {
|
||||
return self.first_normal_slot;
|
||||
}
|
||||
/// Returns the leader-schedule slot offset.
|
||||
#[must_use]
|
||||
pub const fn leader_schedule_slot_offset(&self) -> u64 {
|
||||
return self.leader_schedule_slot_offset;
|
||||
}
|
||||
/// Returns the number of slots per epoch.
|
||||
#[must_use]
|
||||
pub const fn slots_per_epoch(&self) -> u64 {
|
||||
return self.slots_per_epoch;
|
||||
}
|
||||
/// Returns whether epoch warmup is enabled.
|
||||
#[must_use]
|
||||
pub const fn warmup(&self) -> bool {
|
||||
return self.warmup;
|
||||
}
|
||||
|
||||
/// Decodes an epoch schedule from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireEpochSchedule>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
|
||||
first_normal_epoch: wire.first_normal_epoch,
|
||||
first_normal_slot: wire.first_normal_slot,
|
||||
leader_schedule_slot_offset: wire.leader_schedule_slot_offset,
|
||||
slots_per_epoch: wire.slots_per_epoch,
|
||||
warmup: wire.warmup,
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Highest full and optional incremental snapshot slots returned by `getHighestSnapshotSlot`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaSnapshotSlotInfo {
|
||||
full: u64,
|
||||
incremental: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaSnapshotSlotInfo {
|
||||
/// Returns the highest full snapshot slot.
|
||||
#[must_use]
|
||||
pub const fn full(&self) -> u64 {
|
||||
return self.full;
|
||||
}
|
||||
/// Returns the optional highest incremental snapshot slot.
|
||||
#[must_use]
|
||||
pub const fn incremental(&self) -> std::option::Option<u64> {
|
||||
return self.incremental;
|
||||
}
|
||||
|
||||
/// Decodes snapshot-slot information from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireSnapshotSlotInfo>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self { full: wire.full, incremental: wire.incremental }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional configuration accepted by `getLeaderSchedule`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaLeaderScheduleConfig {
|
||||
identity: std::option::Option<ksp_core_lib::Pubkey>,
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
}
|
||||
|
||||
impl SolanaLeaderScheduleConfig {
|
||||
/// Creates a leader-schedule configuration.
|
||||
#[must_use]
|
||||
pub const fn new(identity: std::option::Option<ksp_core_lib::Pubkey>, commitment: std::option::Option<crate::SolanaCommitment>) -> Self {
|
||||
return Self { identity, commitment };
|
||||
}
|
||||
/// Returns the optional validator identity filter.
|
||||
#[must_use]
|
||||
pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> {
|
||||
return self.identity.as_ref();
|
||||
}
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return self.identity.is_none() && self.commitment.is_none();
|
||||
}
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(identity) = self.identity.as_ref() {
|
||||
object.insert("identity".to_owned(), serde_json::Value::String(identity.to_string()));
|
||||
}
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed parameter overload for `getLeaderSchedule`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaLeaderScheduleRequest {
|
||||
/// Query the current epoch, optionally with a config object.
|
||||
CurrentEpoch(std::option::Option<crate::SolanaLeaderScheduleConfig>),
|
||||
/// Query the epoch containing one slot, optionally with a config object.
|
||||
Slot {
|
||||
/// Slot whose epoch should be queried.
|
||||
slot: u64,
|
||||
/// Optional leader-schedule config sent as the second positional parameter.
|
||||
config: std::option::Option<crate::SolanaLeaderScheduleConfig>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for SolanaLeaderScheduleRequest {
|
||||
fn default() -> Self {
|
||||
return Self::CurrentEpoch(std::option::Option::None);
|
||||
}
|
||||
}
|
||||
|
||||
impl SolanaLeaderScheduleRequest {
|
||||
/// Serializes the typed overload to the exact positional JSON-RPC params.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_params(&self) -> std::vec::Vec<serde_json::Value> {
|
||||
return match self {
|
||||
Self::CurrentEpoch(std::option::Option::None) => std::vec::Vec::new(),
|
||||
Self::CurrentEpoch(std::option::Option::Some(config)) if config.is_empty() => std::vec::Vec::new(),
|
||||
Self::CurrentEpoch(std::option::Option::Some(config)) => std::vec![config.to_json_value()],
|
||||
Self::Slot { slot, config: std::option::Option::None } => std::vec![serde_json::json!(slot)],
|
||||
Self::Slot { slot, config: std::option::Option::Some(config) } if config.is_empty() => std::vec![serde_json::json!(slot)],
|
||||
Self::Slot { slot, config: std::option::Option::Some(config) } => std::vec![serde_json::json!(slot), config.to_json_value()],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Leader schedule mapping validator identities to relative epoch slot indices.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaLeaderSchedule {
|
||||
entries: std::collections::BTreeMap<ksp_core_lib::Pubkey, std::vec::Vec<usize>>,
|
||||
}
|
||||
|
||||
impl SolanaLeaderSchedule {
|
||||
/// Returns the complete leader schedule map.
|
||||
#[must_use]
|
||||
pub const fn entries(&self) -> &std::collections::BTreeMap<ksp_core_lib::Pubkey, std::vec::Vec<usize>> {
|
||||
return &self.entries;
|
||||
}
|
||||
|
||||
/// Decodes a leader schedule map from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<std::collections::BTreeMap<std::string::String, std::vec::Vec<usize>>>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut entries = std::collections::BTreeMap::new();
|
||||
for (identity, slots) in wire {
|
||||
let pubkey = crate::parse_wire_pubkey(method, "leader_identity", identity.as_str());
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
entries.insert(pubkey, slots);
|
||||
}
|
||||
return std::result::Result::Ok(Self { entries });
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration accepted by `getVoteAccounts`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaVoteAccountsConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
vote_pubkey: std::option::Option<ksp_core_lib::Pubkey>,
|
||||
keep_unstaked_delinquents: std::option::Option<bool>,
|
||||
delinquent_slot_distance: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaVoteAccountsConfig {
|
||||
/// Creates a vote-accounts configuration.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
vote_pubkey: std::option::Option<ksp_core_lib::Pubkey>,
|
||||
keep_unstaked_delinquents: std::option::Option<bool>,
|
||||
delinquent_slot_distance: std::option::Option<u64>,
|
||||
) -> Self {
|
||||
return Self { commitment, vote_pubkey, keep_unstaked_delinquents, delinquent_slot_distance };
|
||||
}
|
||||
/// Returns the optional commitment.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
/// Returns the optional vote-account public key filter.
|
||||
#[must_use]
|
||||
pub const fn vote_pubkey(&self) -> std::option::Option<&ksp_core_lib::Pubkey> {
|
||||
return self.vote_pubkey.as_ref();
|
||||
}
|
||||
/// Returns whether unstaked delinquent validators should be kept.
|
||||
#[must_use]
|
||||
pub const fn keep_unstaked_delinquents(&self) -> std::option::Option<bool> {
|
||||
return self.keep_unstaked_delinquents;
|
||||
}
|
||||
/// Returns the optional delinquent slot distance.
|
||||
#[must_use]
|
||||
pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> {
|
||||
return self.delinquent_slot_distance;
|
||||
}
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return self.commitment.is_none() && self.vote_pubkey.is_none() && self.keep_unstaked_delinquents.is_none() && self.delinquent_slot_distance.is_none();
|
||||
}
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(vote_pubkey) = self.vote_pubkey.as_ref() {
|
||||
object.insert("votePubkey".to_owned(), serde_json::Value::String(vote_pubkey.to_string()));
|
||||
}
|
||||
if let std::option::Option::Some(value) = self.keep_unstaked_delinquents {
|
||||
object.insert("keepUnstakedDelinquents".to_owned(), serde_json::Value::Bool(value));
|
||||
}
|
||||
if let std::option::Option::Some(value) = self.delinquent_slot_distance {
|
||||
object.insert("delinquentSlotDistance".to_owned(), serde_json::Value::Number(value.into()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// One epoch-credit history entry returned by `getVoteAccounts`.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct SolanaEpochCredits {
|
||||
epoch: u64,
|
||||
credits: u64,
|
||||
previous_credits: u64,
|
||||
}
|
||||
|
||||
impl SolanaEpochCredits {
|
||||
/// Returns the epoch number.
|
||||
#[must_use]
|
||||
pub const fn epoch(&self) -> u64 {
|
||||
return self.epoch;
|
||||
}
|
||||
/// Returns cumulative credits at the end of the epoch.
|
||||
#[must_use]
|
||||
pub const fn credits(&self) -> u64 {
|
||||
return self.credits;
|
||||
}
|
||||
/// Returns cumulative credits before the epoch.
|
||||
#[must_use]
|
||||
pub const fn previous_credits(&self) -> u64 {
|
||||
return self.previous_credits;
|
||||
}
|
||||
}
|
||||
|
||||
/// One validator vote-account record returned by `getVoteAccounts`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaVoteAccountInfo {
|
||||
vote_pubkey: ksp_core_lib::Pubkey,
|
||||
node_pubkey: ksp_core_lib::Pubkey,
|
||||
activated_stake: u64,
|
||||
commission: u8,
|
||||
inflation_rewards_commission_bps: std::option::Option<u16>,
|
||||
epoch_vote_account: bool,
|
||||
epoch_credits: std::vec::Vec<crate::SolanaEpochCredits>,
|
||||
last_vote: u64,
|
||||
root_slot: u64,
|
||||
}
|
||||
|
||||
impl SolanaVoteAccountInfo {
|
||||
/// Returns the vote account public key.
|
||||
#[must_use]
|
||||
pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.vote_pubkey;
|
||||
}
|
||||
/// Returns the validator identity public key.
|
||||
#[must_use]
|
||||
pub const fn node_pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.node_pubkey;
|
||||
}
|
||||
/// Returns the activated stake in lamports.
|
||||
#[must_use]
|
||||
pub const fn activated_stake(&self) -> u64 {
|
||||
return self.activated_stake;
|
||||
}
|
||||
/// Returns the legacy/effective percentage commission field.
|
||||
#[must_use]
|
||||
pub const fn commission(&self) -> u8 {
|
||||
return self.commission;
|
||||
}
|
||||
/// Returns the optional raw inflation-rewards commission in basis points.
|
||||
#[must_use]
|
||||
pub const fn inflation_rewards_commission_bps(&self) -> std::option::Option<u16> {
|
||||
return self.inflation_rewards_commission_bps;
|
||||
}
|
||||
/// Returns whether the vote account is staked for the current epoch.
|
||||
#[must_use]
|
||||
pub const fn epoch_vote_account(&self) -> bool {
|
||||
return self.epoch_vote_account;
|
||||
}
|
||||
/// Returns the bounded RPC epoch-credit history.
|
||||
#[must_use]
|
||||
pub fn epoch_credits(&self) -> &[crate::SolanaEpochCredits] {
|
||||
return self.epoch_credits.as_slice();
|
||||
}
|
||||
/// Returns the latest voted slot or zero when no vote exists.
|
||||
#[must_use]
|
||||
pub const fn last_vote(&self) -> u64 {
|
||||
return self.last_vote;
|
||||
}
|
||||
/// Returns the current root slot or zero when no root exists.
|
||||
#[must_use]
|
||||
pub const fn root_slot(&self) -> u64 {
|
||||
return self.root_slot;
|
||||
}
|
||||
|
||||
/// Decodes one vote-account record from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireVoteAccountInfo>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let vote_pubkey = crate::parse_wire_pubkey(method, "votePubkey", wire.vote_pubkey.as_str());
|
||||
let vote_pubkey = match vote_pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let node_pubkey = crate::parse_wire_pubkey(method, "nodePubkey", wire.node_pubkey.as_str());
|
||||
let node_pubkey = match node_pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut epoch_credits = std::vec::Vec::with_capacity(wire.epoch_credits.len());
|
||||
for entry in wire.epoch_credits {
|
||||
epoch_credits.push(crate::SolanaEpochCredits { epoch: entry[0], credits: entry[1], previous_credits: entry[2] });
|
||||
}
|
||||
return std::result::Result::Ok(Self {
|
||||
vote_pubkey,
|
||||
node_pubkey,
|
||||
activated_stake: wire.activated_stake,
|
||||
commission: wire.commission,
|
||||
inflation_rewards_commission_bps: wire.inflation_rewards_commission_bps,
|
||||
epoch_vote_account: wire.epoch_vote_account,
|
||||
epoch_credits,
|
||||
last_vote: wire.last_vote,
|
||||
root_slot: wire.root_slot,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Current and delinquent validator vote-account groups returned by `getVoteAccounts`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaVoteAccountStatus {
|
||||
current: std::vec::Vec<crate::SolanaVoteAccountInfo>,
|
||||
delinquent: std::vec::Vec<crate::SolanaVoteAccountInfo>,
|
||||
}
|
||||
|
||||
impl SolanaVoteAccountStatus {
|
||||
/// Returns current vote accounts.
|
||||
#[must_use]
|
||||
pub fn current(&self) -> &[crate::SolanaVoteAccountInfo] {
|
||||
return self.current.as_slice();
|
||||
}
|
||||
/// Returns delinquent vote accounts.
|
||||
#[must_use]
|
||||
pub fn delinquent(&self) -> &[crate::SolanaVoteAccountInfo] {
|
||||
return self.delinquent.as_slice();
|
||||
}
|
||||
|
||||
/// Decodes the complete vote-account status response from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireVoteAccountStatus>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut current = std::vec::Vec::with_capacity(wire.current.len());
|
||||
for value in wire.current {
|
||||
let decoded = crate::SolanaVoteAccountInfo::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(info) => current.push(info),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
let mut delinquent = std::vec::Vec::with_capacity(wire.delinquent.len());
|
||||
for value in wire.delinquent {
|
||||
let decoded = crate::SolanaVoteAccountInfo::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(info) => delinquent.push(info),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(Self { current, delinquent });
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireIdentity {
|
||||
identity: std::string::String,
|
||||
}
|
||||
|
||||
impl crate::HttpTransportPool {
|
||||
/// Executes typed `getClusterNodes` through the common KSP HTTP transport path.
|
||||
pub async fn get_cluster_nodes(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaClusterNode>> {
|
||||
let value = self.execute_cluster_rpc("getClusterNodes", role, std::vec::Vec::new()).await;
|
||||
let value = match value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decoded = crate::decode_wire_json::<std::vec::Vec<serde_json::Value>>("getClusterNodes", value);
|
||||
let values = match decoded {
|
||||
std::result::Result::Ok(values) => values,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut nodes = std::vec::Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let node = crate::SolanaClusterNode::decode_wire("getClusterNodes", value);
|
||||
match node {
|
||||
std::result::Result::Ok(node) => nodes.push(node),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(nodes);
|
||||
}
|
||||
|
||||
/// Executes typed `getEpochInfo` through the common KSP HTTP transport path.
|
||||
pub async fn get_epoch_info(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
config: std::option::Option<&crate::SolanaContextConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaEpochInfo> {
|
||||
let mut params = std::vec::Vec::new();
|
||||
push_context_config(&mut params, config);
|
||||
let value = self.execute_cluster_rpc("getEpochInfo", role, params).await;
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => crate::SolanaEpochInfo::decode_wire("getEpochInfo", value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getEpochSchedule` through the common KSP HTTP transport path.
|
||||
pub async fn get_epoch_schedule(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<crate::SolanaEpochSchedule> {
|
||||
let value = self.execute_cluster_rpc("getEpochSchedule", role, std::vec::Vec::new()).await;
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => crate::SolanaEpochSchedule::decode_wire("getEpochSchedule", value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getHighestSnapshotSlot` through the common KSP HTTP transport path.
|
||||
pub async fn get_highest_snapshot_slot(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<crate::SolanaSnapshotSlotInfo> {
|
||||
let value = self.execute_cluster_rpc("getHighestSnapshotSlot", role, std::vec::Vec::new()).await;
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getIdentity` through the common KSP HTTP transport path.
|
||||
pub async fn get_identity(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||
let value = self.execute_cluster_rpc("getIdentity", role, std::vec::Vec::new()).await;
|
||||
let value = match value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decoded = crate::decode_wire_json::<WireIdentity>("getIdentity", value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::parse_wire_pubkey("getIdentity", "identity", wire.identity.as_str());
|
||||
}
|
||||
|
||||
/// Executes typed `getMaxRetransmitSlot` through the common KSP HTTP transport path.
|
||||
pub async fn get_max_retransmit_slot(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<u64> {
|
||||
return self.get_cluster_simple_slot("getMaxRetransmitSlot", role).await;
|
||||
}
|
||||
|
||||
/// Executes typed `getMaxShredInsertSlot` through the common KSP HTTP transport path.
|
||||
pub async fn get_max_shred_insert_slot(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<u64> {
|
||||
return self.get_cluster_simple_slot("getMaxShredInsertSlot", role).await;
|
||||
}
|
||||
|
||||
/// Executes typed `getLeaderSchedule` through the common KSP HTTP transport path.
|
||||
pub async fn get_leader_schedule(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
request: &crate::SolanaLeaderScheduleRequest,
|
||||
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaLeaderSchedule>> {
|
||||
let value = self.execute_cluster_rpc("getLeaderSchedule", role, request.to_json_params()).await;
|
||||
let value = match value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if value.is_null() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
let schedule = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", value);
|
||||
return match schedule {
|
||||
std::result::Result::Ok(schedule) => std::result::Result::Ok(std::option::Option::Some(schedule)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getSlot` through the common KSP HTTP transport path.
|
||||
pub async fn get_slot(&self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result<u64> {
|
||||
let mut params = std::vec::Vec::new();
|
||||
push_context_config(&mut params, config);
|
||||
let value = self.execute_cluster_rpc("getSlot", role, params).await;
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => crate::decode_wire_json::<u64>("getSlot", value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getSlotLeader` through the common KSP HTTP transport path.
|
||||
pub async fn get_slot_leader(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
config: std::option::Option<&crate::SolanaContextConfig>,
|
||||
) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||
let mut params = std::vec::Vec::new();
|
||||
push_context_config(&mut params, config);
|
||||
let value = self.execute_cluster_rpc("getSlotLeader", role, params).await;
|
||||
let value = match value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decoded = crate::decode_wire_json::<std::string::String>("getSlotLeader", value);
|
||||
let leader = match decoded {
|
||||
std::result::Result::Ok(leader) => leader,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::parse_wire_pubkey("getSlotLeader", "leader", leader.as_str());
|
||||
}
|
||||
|
||||
/// Executes typed `getSlotLeaders` through the common KSP HTTP transport path.
|
||||
pub async fn get_slot_leaders(&self, role: &crate::HttpRoleName, start_slot: u64, limit: u64) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
|
||||
if limit == 0 || limit > MAX_GET_SLOT_LEADERS {
|
||||
return invalid_cluster_parameters("getSlotLeaders", "getSlotLeaders limit must be between 1 and 5000", "limit", limit);
|
||||
}
|
||||
let params = std::vec![serde_json::json!(start_slot), serde_json::json!(limit)];
|
||||
let value = self.execute_cluster_rpc("getSlotLeaders", role, params).await;
|
||||
let value = match value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_pubkey_list("getSlotLeaders", "leader", value);
|
||||
}
|
||||
|
||||
/// Executes typed `getVoteAccounts` through the common KSP HTTP transport path.
|
||||
pub async fn get_vote_accounts(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
config: std::option::Option<&crate::SolanaVoteAccountsConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaVoteAccountStatus> {
|
||||
let mut params = std::vec::Vec::new();
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
let value = self.execute_cluster_rpc("getVoteAccounts", role, params).await;
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => crate::SolanaVoteAccountStatus::decode_wire("getVoteAccounts", value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
async fn get_cluster_simple_slot(&self, method_name: &'static str, role: &crate::HttpRoleName) -> ksp_core_lib::Result<u64> {
|
||||
let value = self.execute_cluster_rpc(method_name, role, std::vec::Vec::new()).await;
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => crate::decode_wire_json::<u64>(method_name, value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
async fn execute_cluster_rpc(
|
||||
&self,
|
||||
method_name: &'static str,
|
||||
role: &crate::HttpRoleName,
|
||||
params: std::vec::Vec<serde_json::Value>,
|
||||
) -> ksp_core_lib::Result<serde_json::Value> {
|
||||
let method = cluster_descriptor(method_name);
|
||||
let method = match method {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return self.execute_standard_rpc(role, method, params).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& (config.commitment().is_some() || config.min_context_slot().is_some())
|
||||
{
|
||||
params.push((*config).to_json_value());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
|
||||
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
|
||||
let values = match decoded {
|
||||
std::result::Result::Ok(values) => values,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
|
||||
match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(pubkeys);
|
||||
}
|
||||
|
||||
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
|
||||
.with_context("rpc_method", method)
|
||||
.with_context(field, value.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
||||
let descriptor = crate::find_http_rpc_method(method);
|
||||
return match descriptor {
|
||||
std::option::Option::Some(descriptor)
|
||||
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
|
||||
{
|
||||
std::result::Result::Ok(descriptor)
|
||||
},
|
||||
_ => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
|
||||
.with_context("rpc_method", method),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireClusterNode {
|
||||
pubkey: std::string::String,
|
||||
#[serde(default)]
|
||||
feature_set: std::option::Option<u32>,
|
||||
#[serde(default)]
|
||||
gossip: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
pubsub: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
rpc: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
serve_repair: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
shred_version: std::option::Option<u16>,
|
||||
#[serde(default)]
|
||||
tpu: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
tpu_forwards: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
tpu_forwards_quic: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
tpu_quic: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
tpu_vote: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
tvu: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
version: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
client_id: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireEpochInfo {
|
||||
absolute_slot: u64,
|
||||
block_height: u64,
|
||||
epoch: u64,
|
||||
slot_index: u64,
|
||||
slots_in_epoch: u64,
|
||||
transaction_count: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireEpochSchedule {
|
||||
first_normal_epoch: u64,
|
||||
first_normal_slot: u64,
|
||||
leader_schedule_slot_offset: u64,
|
||||
slots_per_epoch: u64,
|
||||
warmup: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireSnapshotSlotInfo {
|
||||
full: u64,
|
||||
incremental: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireVoteAccountInfo {
|
||||
vote_pubkey: std::string::String,
|
||||
node_pubkey: std::string::String,
|
||||
activated_stake: u64,
|
||||
commission: u8,
|
||||
#[serde(default)]
|
||||
inflation_rewards_commission_bps: std::option::Option<u16>,
|
||||
epoch_vote_account: bool,
|
||||
epoch_credits: std::vec::Vec<[u64; 3]>,
|
||||
last_vote: u64,
|
||||
root_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireVoteAccountStatus {
|
||||
current: std::vec::Vec<serde_json::Value>,
|
||||
delinquent: std::vec::Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_cluster.rs"]
|
||||
mod tests;
|
||||
193
crates/ksp-onchain-transport-lib/src/rpc_common.rs
Normal file
193
crates/ksp-onchain-transport-lib/src/rpc_common.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
// version: 4
|
||||
|
||||
/// Commitment level accepted by typed Solana HTTP RPC adapters.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaCommitment {
|
||||
/// Query the most recent processed bank.
|
||||
Processed,
|
||||
/// Query a bank confirmed by cluster vote.
|
||||
Confirmed,
|
||||
/// Query a finalized bank.
|
||||
Finalized,
|
||||
}
|
||||
|
||||
impl SolanaCommitment {
|
||||
/// Returns the Solana JSON-RPC commitment string.
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Processed => "processed",
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::Finalized => "finalized",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional commitment-only configuration shared by typed Solana HTTP RPC methods.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaCommitmentConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
}
|
||||
|
||||
impl SolanaCommitmentConfig {
|
||||
/// Creates an explicit commitment-only configuration.
|
||||
#[must_use]
|
||||
pub const fn new(commitment: std::option::Option<crate::SolanaCommitment>) -> Self {
|
||||
return Self { commitment };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional commitment and minimum-context configuration shared by typed Solana HTTP RPC methods.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaContextConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaContextConfig {
|
||||
/// Creates an explicit context-aware RPC configuration.
|
||||
#[must_use]
|
||||
pub const fn new(commitment: std::option::Option<crate::SolanaCommitment>, min_context_slot: std::option::Option<u64>) -> Self {
|
||||
return Self { commitment, min_context_slot };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Returns the optional minimum context slot.
|
||||
#[must_use]
|
||||
pub const fn min_context_slot(&self) -> std::option::Option<u64> {
|
||||
return self.min_context_slot;
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
|
||||
object.insert("minContextSlot".to_owned(), serde_json::Value::Number(min_context_slot.into()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed Solana RPC context shared by contextual HTTP responses.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaRpcContext {
|
||||
slot: u64,
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl SolanaRpcContext {
|
||||
/// Returns the context slot reported by the RPC node.
|
||||
#[must_use]
|
||||
pub const fn slot(&self) -> u64 {
|
||||
return self.slot;
|
||||
}
|
||||
|
||||
/// Returns the optional RPC API version reported by the node.
|
||||
#[must_use]
|
||||
pub fn api_version(&self) -> std::option::Option<&str> {
|
||||
return match self.api_version.as_ref() {
|
||||
std::option::Option::Some(value) => std::option::Option::Some(value.as_str()),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Decodes one RPC context from a parsed JSON value for typed RPC adapters.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = decode_wire_json::<WireRpcContext>(method, value);
|
||||
let context = match decoded {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { slot: context.slot, api_version: context.api_version });
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic contextual result returned by typed Solana HTTP RPC adapters.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaRpcResponse<T> {
|
||||
context: crate::SolanaRpcContext,
|
||||
value: T,
|
||||
}
|
||||
|
||||
impl<T> SolanaRpcResponse<T> {
|
||||
/// Returns the Solana response context.
|
||||
#[must_use]
|
||||
pub const fn context(&self) -> &crate::SolanaRpcContext {
|
||||
return &self.context;
|
||||
}
|
||||
|
||||
/// Returns the typed response value.
|
||||
#[must_use]
|
||||
pub const fn value(&self) -> &T {
|
||||
return &self.value;
|
||||
}
|
||||
|
||||
/// Creates a contextual response after wire decoding and validation.
|
||||
#[must_use]
|
||||
pub(crate) const fn new(context: crate::SolanaRpcContext, value: T) -> Self {
|
||||
return Self { context, value };
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes one private serde wire type and maps shape failures to the shared Transport error domain.
|
||||
pub(crate) fn decode_wire_json<T: serde::de::DeserializeOwned>(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<T> {
|
||||
let decoded = serde_json::from_value::<T>(value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana HTTP response has an invalid wire shape")
|
||||
.with_context("rpc_method", method)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses a base58 public key from one wire field without echoing its value into diagnostics.
|
||||
pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||
let parsed = value.parse::<ksp_core_lib::Pubkey>();
|
||||
return match parsed {
|
||||
std::result::Result::Ok(pubkey) => std::result::Result::Ok(pubkey),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana HTTP response contains an invalid public key")
|
||||
.with_context("rpc_method", method)
|
||||
.with_context("field", field),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcContext {
|
||||
slot: u64,
|
||||
#[serde(rename = "apiVersion", default)]
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_common.rs"]
|
||||
mod tests;
|
||||
354
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
Normal file
354
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
// version: 3
|
||||
|
||||
/// Exclusive selector accepted by token-account list RPC methods.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaTokenAccountSelector {
|
||||
/// Select token accounts for one mint.
|
||||
Mint(ksp_core_lib::Pubkey),
|
||||
/// Select token accounts owned by one token program.
|
||||
ProgramId(ksp_core_lib::Pubkey),
|
||||
}
|
||||
|
||||
impl SolanaTokenAccountSelector {
|
||||
/// Serializes the exclusive selector to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
return match self {
|
||||
Self::Mint(pubkey) => serde_json::json!({"mint": pubkey.to_string()}),
|
||||
Self::ProgramId(pubkey) => serde_json::json!({"programId": pubkey.to_string()}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Token amount returned by Solana HTTP token RPC methods.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaTokenAmount {
|
||||
amount: std::string::String,
|
||||
decimals: u8,
|
||||
ui_amount: std::option::Option<f64>,
|
||||
ui_amount_string: std::string::String,
|
||||
}
|
||||
|
||||
impl SolanaTokenAmount {
|
||||
/// Returns the integer token amount as an exact decimal string.
|
||||
#[must_use]
|
||||
pub fn amount(&self) -> &str {
|
||||
return self.amount.as_str();
|
||||
}
|
||||
|
||||
/// Returns the mint decimal precision.
|
||||
#[must_use]
|
||||
pub const fn decimals(&self) -> u8 {
|
||||
return self.decimals;
|
||||
}
|
||||
|
||||
/// Returns the nullable floating-point UI amount exactly as provided by RPC.
|
||||
#[must_use]
|
||||
pub const fn ui_amount(&self) -> std::option::Option<f64> {
|
||||
return self.ui_amount;
|
||||
}
|
||||
|
||||
/// Returns the exact UI amount string provided by RPC.
|
||||
#[must_use]
|
||||
pub fn ui_amount_string(&self) -> &str {
|
||||
return self.ui_amount_string.as_str();
|
||||
}
|
||||
|
||||
/// Decodes one token amount from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireTokenAmount>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
|
||||
amount: wire.amount,
|
||||
decimals: wire.decimals,
|
||||
ui_amount: wire.ui_amount,
|
||||
ui_amount_string: wire.ui_amount_string,
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Token-account balance entry returned by `getTokenLargestAccounts`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaTokenAccountBalance {
|
||||
address: ksp_core_lib::Pubkey,
|
||||
amount: crate::SolanaTokenAmount,
|
||||
}
|
||||
|
||||
impl SolanaTokenAccountBalance {
|
||||
/// Returns the token account address.
|
||||
#[must_use]
|
||||
pub const fn address(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.address;
|
||||
}
|
||||
|
||||
/// Returns the token amount fields.
|
||||
#[must_use]
|
||||
pub const fn amount(&self) -> &crate::SolanaTokenAmount {
|
||||
return &self.amount;
|
||||
}
|
||||
|
||||
/// Decodes one token-account balance entry from the Solana JSON wire shape.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireTokenAccountBalance>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let address = crate::parse_wire_pubkey(method, "address", wire.address.as_str());
|
||||
let address = match address {
|
||||
std::result::Result::Ok(address) => address,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let amount = crate::SolanaTokenAmount {
|
||||
amount: wire.amount,
|
||||
decimals: wire.decimals,
|
||||
ui_amount: wire.ui_amount,
|
||||
ui_amount_string: wire.ui_amount_string,
|
||||
};
|
||||
return std::result::Result::Ok(Self { address, amount });
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireTokenAmount {
|
||||
amount: std::string::String,
|
||||
decimals: u8,
|
||||
#[serde(rename = "uiAmount")]
|
||||
ui_amount: std::option::Option<f64>,
|
||||
#[serde(rename = "uiAmountString")]
|
||||
ui_amount_string: std::string::String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireTokenAccountBalance {
|
||||
address: std::string::String,
|
||||
amount: std::string::String,
|
||||
decimals: u8,
|
||||
#[serde(rename = "uiAmount")]
|
||||
ui_amount: std::option::Option<f64>,
|
||||
#[serde(rename = "uiAmountString")]
|
||||
ui_amount_string: std::string::String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcResponse<T> {
|
||||
context: serde_json::Value,
|
||||
value: T,
|
||||
}
|
||||
|
||||
impl crate::HttpTransportPool {
|
||||
/// Executes typed `getTokenAccountBalance` through the common KSP HTTP transport path.
|
||||
pub async fn get_token_account_balance(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
token_account: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
|
||||
let method_result = token_descriptor("getTokenAccountBalance");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut params = std::vec![serde_json::Value::String(token_account.to_string())];
|
||||
push_commitment_config(&mut params, config);
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_token_amount_response("getTokenAccountBalance", value);
|
||||
}
|
||||
|
||||
/// Executes typed `getTokenAccountsByDelegate` through the common KSP HTTP transport path.
|
||||
pub async fn get_token_accounts_by_delegate(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
delegate: &ksp_core_lib::Pubkey,
|
||||
selector: &crate::SolanaTokenAccountSelector,
|
||||
config: std::option::Option<&crate::SolanaAccountInfoConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
|
||||
return self.get_token_accounts_list("getTokenAccountsByDelegate", role, delegate, selector, config).await;
|
||||
}
|
||||
|
||||
/// Executes typed `getTokenAccountsByOwner` through the common KSP HTTP transport path.
|
||||
pub async fn get_token_accounts_by_owner(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
owner: &ksp_core_lib::Pubkey,
|
||||
selector: &crate::SolanaTokenAccountSelector,
|
||||
config: std::option::Option<&crate::SolanaAccountInfoConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
|
||||
return self.get_token_accounts_list("getTokenAccountsByOwner", role, owner, selector, config).await;
|
||||
}
|
||||
|
||||
/// Executes typed `getTokenLargestAccounts` through the common KSP HTTP transport path.
|
||||
pub async fn get_token_largest_accounts(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
mint: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaTokenAccountBalance>>> {
|
||||
let method_result = token_descriptor("getTokenLargestAccounts");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut params = std::vec![serde_json::Value::String(mint.to_string())];
|
||||
push_commitment_config(&mut params, config);
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_token_account_balances_response("getTokenLargestAccounts", value);
|
||||
}
|
||||
|
||||
/// Executes typed `getTokenSupply` through the common KSP HTTP transport path.
|
||||
pub async fn get_token_supply(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
mint: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
|
||||
let method_result = token_descriptor("getTokenSupply");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut params = std::vec![serde_json::Value::String(mint.to_string())];
|
||||
push_commitment_config(&mut params, config);
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_token_amount_response("getTokenSupply", value);
|
||||
}
|
||||
|
||||
async fn get_token_accounts_list(
|
||||
&self,
|
||||
method_name: &'static str,
|
||||
role: &crate::HttpRoleName,
|
||||
address: &ksp_core_lib::Pubkey,
|
||||
selector: &crate::SolanaTokenAccountSelector,
|
||||
config: std::option::Option<&crate::SolanaAccountInfoConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
|
||||
let method_result = token_descriptor(method_name);
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut params = std::vec![serde_json::Value::String(address.to_string()), selector.to_json_value()];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
let result = self.execute_standard_rpc(role, method, params).await;
|
||||
let value = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return decode_keyed_accounts_response(method_name, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn token_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
||||
let descriptor = crate::find_http_rpc_method(method);
|
||||
return match descriptor {
|
||||
std::option::Option::Some(descriptor)
|
||||
if descriptor.category() == crate::HttpRpcCategory::Tokens && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
|
||||
{
|
||||
std::result::Result::Ok(descriptor)
|
||||
},
|
||||
_ => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Tokens descriptor is missing from the audited 0.2.2 registry")
|
||||
.with_context("rpc_method", method),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn push_commitment_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaCommitmentConfig>) {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& config.commitment().is_some()
|
||||
{
|
||||
params.push(config.to_json_value());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn decode_token_amount_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<serde_json::Value>>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let amount = crate::SolanaTokenAmount::decode_wire(method, wire.value);
|
||||
let amount = match amount {
|
||||
std::result::Result::Ok(amount) => amount,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, amount));
|
||||
}
|
||||
|
||||
fn decode_keyed_accounts_response(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::vec::Vec<serde_json::Value>>>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut accounts = std::vec::Vec::with_capacity(wire.value.len());
|
||||
for value in wire.value {
|
||||
let account = crate::SolanaKeyedAccount::decode_wire(method, value);
|
||||
match account {
|
||||
std::result::Result::Ok(account) => accounts.push(account),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, accounts));
|
||||
}
|
||||
|
||||
fn decode_token_account_balances_response(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaTokenAccountBalance>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::vec::Vec<serde_json::Value>>>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let context = crate::SolanaRpcContext::decode_wire(method, wire.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut accounts = std::vec::Vec::with_capacity(wire.value.len());
|
||||
for value in wire.value {
|
||||
let account = crate::SolanaTokenAccountBalance::decode_wire(method, value);
|
||||
match account {
|
||||
std::result::Result::Ok(account) => accounts.push(account),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, accounts));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_tokens.rs"]
|
||||
mod tests;
|
||||
1807
crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
Normal file
1807
crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 5
|
||||
// version: 15
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -171,3 +171,179 @@ fn public_typed_canary_contracts_are_available_from_crate_root() {
|
||||
assert_eq!(config.min_context_slot(), std::option::Option::Some(42));
|
||||
assert_eq!(ksp_onchain_transport_lib::SolanaNodeHealth::Healthy, ksp_onchain_transport_lib::SolanaNodeHealth::Healthy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_002_shared_rpc_types_are_constructible_from_crate_root() {
|
||||
let context = ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(123),
|
||||
);
|
||||
assert_eq!(context.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized));
|
||||
assert_eq!(context.min_context_slot(), std::option::Option::Some(123));
|
||||
let account = ksp_onchain_transport_lib::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::JsonParsed),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(account.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::JsonParsed));
|
||||
let pubkey = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let selector = ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(pubkey);
|
||||
assert!(matches!(selector, ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(_)));
|
||||
let vote = ksp_onchain_transport_lib::SolanaVoteAccountsConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(pubkey),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(128),
|
||||
);
|
||||
assert_eq!(vote.vote_pubkey(), std::option::Option::Some(&pubkey));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_003_account_wrappers_are_available_from_crate_root() {
|
||||
let _get_account_info = ksp_onchain_transport_lib::HttpTransportPool::get_account_info;
|
||||
let _get_largest_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_largest_accounts;
|
||||
let _get_minimum_balance_for_rent_exemption = ksp_onchain_transport_lib::HttpTransportPool::get_minimum_balance_for_rent_exemption;
|
||||
let _get_multiple_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_multiple_accounts;
|
||||
let _get_program_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_program_accounts;
|
||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_INVALID_RPC_PARAMETERS.domain(), "onchain_transport");
|
||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_INVALID_RPC_PARAMETERS.code(), "invalid_rpc_parameters");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_004_token_wrappers_are_available_from_crate_root() {
|
||||
let _get_token_account_balance = ksp_onchain_transport_lib::HttpTransportPool::get_token_account_balance;
|
||||
let _get_token_accounts_by_delegate = ksp_onchain_transport_lib::HttpTransportPool::get_token_accounts_by_delegate;
|
||||
let _get_token_accounts_by_owner = ksp_onchain_transport_lib::HttpTransportPool::get_token_accounts_by_owner;
|
||||
let _get_token_largest_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_token_largest_accounts;
|
||||
let _get_token_supply = ksp_onchain_transport_lib::HttpTransportPool::get_token_supply;
|
||||
let mint = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let selector = ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(mint);
|
||||
assert!(matches!(selector, ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_005_simple_cluster_wrappers_are_available_from_crate_root() {
|
||||
let _get_cluster_nodes = ksp_onchain_transport_lib::HttpTransportPool::get_cluster_nodes;
|
||||
let _get_epoch_info = ksp_onchain_transport_lib::HttpTransportPool::get_epoch_info;
|
||||
let _get_epoch_schedule = ksp_onchain_transport_lib::HttpTransportPool::get_epoch_schedule;
|
||||
let _get_highest_snapshot_slot = ksp_onchain_transport_lib::HttpTransportPool::get_highest_snapshot_slot;
|
||||
let _get_identity = ksp_onchain_transport_lib::HttpTransportPool::get_identity;
|
||||
let _get_max_retransmit_slot = ksp_onchain_transport_lib::HttpTransportPool::get_max_retransmit_slot;
|
||||
let _get_max_shred_insert_slot = ksp_onchain_transport_lib::HttpTransportPool::get_max_shred_insert_slot;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_006_remaining_cluster_wrappers_are_available_from_crate_root() {
|
||||
let _get_leader_schedule = ksp_onchain_transport_lib::HttpTransportPool::get_leader_schedule;
|
||||
let _get_slot = ksp_onchain_transport_lib::HttpTransportPool::get_slot;
|
||||
let _get_slot_leader = ksp_onchain_transport_lib::HttpTransportPool::get_slot_leader;
|
||||
let _get_slot_leaders = ksp_onchain_transport_lib::HttpTransportPool::get_slot_leaders;
|
||||
let _get_vote_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_vote_accounts;
|
||||
let request = ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::default();
|
||||
assert!(matches!(request, ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::CurrentEpoch(std::option::Option::None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_002_shared_types_are_constructible_from_crate_root() {
|
||||
let get_transaction = ksp_onchain_transport_lib::SolanaGetTransactionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed),
|
||||
std::option::Option::Some(0),
|
||||
);
|
||||
assert_eq!(get_transaction.max_supported_transaction_version(), std::option::Option::Some(0));
|
||||
let send = ksp_onchain_transport_lib::SolanaSendTransactionConfig::new(
|
||||
std::option::Option::Some(false),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
|
||||
std::option::Option::Some(3),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(send.max_retries(), std::option::Option::Some(3));
|
||||
let pagination = ksp_onchain_transport_lib::SolanaSignaturesForAddressConfig::new(
|
||||
std::option::Option::Some("before".to_owned()),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(10),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(pagination.limit(), std::option::Option::Some(10));
|
||||
assert!(ksp_onchain_transport_lib::SolanaWireField::<u64>::Omitted.is_omitted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_003_read_wrappers_are_available_from_crate_root() {
|
||||
let _get_fee_for_message = ksp_onchain_transport_lib::HttpTransportPool::get_fee_for_message;
|
||||
let _get_latest_blockhash = ksp_onchain_transport_lib::HttpTransportPool::get_latest_blockhash;
|
||||
let _get_transaction_count = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_count;
|
||||
let _is_blockhash_valid = ksp_onchain_transport_lib::HttpTransportPool::is_blockhash_valid;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_004_additional_read_wrappers_are_available_from_crate_root() {
|
||||
let _get_recent_prioritization_fees = ksp_onchain_transport_lib::HttpTransportPool::get_recent_prioritization_fees;
|
||||
let _get_signatures_for_address = ksp_onchain_transport_lib::HttpTransportPool::get_signatures_for_address;
|
||||
let _get_signature_statuses = ksp_onchain_transport_lib::HttpTransportPool::get_signature_statuses;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn public_transaction_pre_005_get_transaction_complete_request_forms_are_available_from_crate_root() {
|
||||
let _get_transaction = ksp_onchain_transport_lib::HttpTransportPool::get_transaction;
|
||||
let _get_transaction_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_legacy;
|
||||
let config = ksp_onchain_transport_lib::SolanaGetTransactionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed),
|
||||
std::option::Option::Some(0),
|
||||
);
|
||||
assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed));
|
||||
assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_006_write_wrappers_and_complete_configs_are_available_from_crate_root() {
|
||||
let _request_airdrop = ksp_onchain_transport_lib::HttpTransportPool::request_airdrop;
|
||||
let _send_transaction = ksp_onchain_transport_lib::HttpTransportPool::send_transaction;
|
||||
let airdrop = ksp_onchain_transport_lib::SolanaRequestAirdropConfig::new(
|
||||
std::option::Option::Some("recent-blockhash".to_owned()),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
);
|
||||
assert_eq!(airdrop.recent_blockhash(), std::option::Option::Some("recent-blockhash"));
|
||||
let send = ksp_onchain_transport_lib::SolanaSendTransactionConfig::new(
|
||||
std::option::Option::Some(false),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
|
||||
std::option::Option::Some(5),
|
||||
std::option::Option::Some(431_000_000),
|
||||
);
|
||||
assert_eq!(send.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64));
|
||||
assert_eq!(send.max_retries(), std::option::Option::Some(5));
|
||||
assert_eq!(send.min_context_slot(), std::option::Option::Some(431_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_007_simulation_wrapper_and_complete_config_are_available_from_crate_root() {
|
||||
let _simulate_transaction = ksp_onchain_transport_lib::HttpTransportPool::simulate_transaction;
|
||||
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let accounts = ksp_onchain_transport_lib::SolanaSimulationAccountsConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
|
||||
std::vec![address],
|
||||
);
|
||||
let config = ksp_onchain_transport_lib::SolanaSimulateTransactionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
|
||||
std::option::Option::Some(false),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(431_000_000),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(accounts),
|
||||
);
|
||||
assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed));
|
||||
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64));
|
||||
assert_eq!(config.sig_verify(), std::option::Option::Some(true));
|
||||
assert_eq!(
|
||||
config.accounts().and_then(ksp_onchain_transport_lib::SolanaSimulationAccountsConfig::encoding),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 1
|
||||
// version: 13
|
||||
|
||||
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
||||
|
||||
@@ -46,3 +46,387 @@ fn release_foundation_canaries_and_historical_statuses_are_exact() {
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NotApplicable);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_003_accounts_subset_is_exact_without_advancing_other_v0_2_2_families() {
|
||||
let mut account_names = std::vec::Vec::<&str>::new();
|
||||
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() == ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2
|
||||
&& descriptor.category() == ksp_onchain_transport_lib::HttpRpcCategory::Accounts
|
||||
{
|
||||
account_names.push(descriptor.method());
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
}
|
||||
account_names.sort_unstable();
|
||||
assert_eq!(
|
||||
account_names,
|
||||
std::vec!["getAccountInfo", "getLargestAccounts", "getMinimumBalanceForRentExemption", "getMultipleAccounts", "getProgramAccounts",],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_004_tokens_subset_is_exact_and_retry_safe() {
|
||||
let mut token_names = std::vec::Vec::<&str>::new();
|
||||
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() == ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2
|
||||
&& descriptor.category() == ksp_onchain_transport_lib::HttpRpcCategory::Tokens
|
||||
{
|
||||
token_names.push(descriptor.method());
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
}
|
||||
token_names.sort_unstable();
|
||||
assert_eq!(
|
||||
token_names,
|
||||
std::vec!["getTokenAccountBalance", "getTokenAccountsByDelegate", "getTokenAccountsByOwner", "getTokenLargestAccounts", "getTokenSupply",],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_005_simple_cluster_subset_is_exact_and_retry_safe() {
|
||||
let expected = std::vec![
|
||||
"getClusterNodes",
|
||||
"getEpochInfo",
|
||||
"getEpochSchedule",
|
||||
"getHighestSnapshotSlot",
|
||||
"getIdentity",
|
||||
"getMaxRetransmitSlot",
|
||||
"getMaxShredInsertSlot",
|
||||
];
|
||||
let deferred = std::vec!["getLeaderSchedule", "getSlot", "getSlotLeader", "getSlotLeaders", "getVoteAccounts"];
|
||||
for method_name in &expected {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.005 cluster descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Cluster);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
for method_name in &deferred {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.006 cluster descriptor must remain registered");
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2);
|
||||
}
|
||||
assert_eq!(expected.len(), 7);
|
||||
assert_eq!(deferred.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_006_completes_cluster_subset_exactly_and_retry_safe() {
|
||||
let mut expected = std::vec![
|
||||
"getClusterNodes",
|
||||
"getEpochInfo",
|
||||
"getEpochSchedule",
|
||||
"getHighestSnapshotSlot",
|
||||
"getIdentity",
|
||||
"getLeaderSchedule",
|
||||
"getMaxRetransmitSlot",
|
||||
"getMaxShredInsertSlot",
|
||||
"getSlot",
|
||||
"getSlotLeader",
|
||||
"getSlotLeaders",
|
||||
"getVoteAccounts",
|
||||
];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() == ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2
|
||||
&& descriptor.category() == ksp_onchain_transport_lib::HttpRpcCategory::Cluster
|
||||
{
|
||||
actual.push(descriptor.method());
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_007_v0_2_2_exact_set_is_complete_without_advancing_future_releases() {
|
||||
let mut expected = std::vec![
|
||||
"getAccountInfo",
|
||||
"getClusterNodes",
|
||||
"getEpochInfo",
|
||||
"getEpochSchedule",
|
||||
"getHighestSnapshotSlot",
|
||||
"getIdentity",
|
||||
"getLargestAccounts",
|
||||
"getLeaderSchedule",
|
||||
"getMaxRetransmitSlot",
|
||||
"getMaxShredInsertSlot",
|
||||
"getMinimumBalanceForRentExemption",
|
||||
"getMultipleAccounts",
|
||||
"getProgramAccounts",
|
||||
"getSlot",
|
||||
"getSlotLeader",
|
||||
"getSlotLeaders",
|
||||
"getTokenAccountBalance",
|
||||
"getTokenAccountsByDelegate",
|
||||
"getTokenAccountsByOwner",
|
||||
"getTokenLargestAccounts",
|
||||
"getTokenSupply",
|
||||
"getVoteAccounts",
|
||||
];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
let mut future_transactions = 0_usize;
|
||||
let mut future_blocks_economics = 0_usize;
|
||||
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||
match descriptor.coverage_release() {
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2 => {
|
||||
actual.push(descriptor.method());
|
||||
assert_eq!(descriptor.documentation_status(), ksp_onchain_transport_lib::RpcDocumentationStatus::Stable);
|
||||
assert_eq!(descriptor.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Supported);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
},
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3 => future_transactions += 1,
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4 => future_blocks_economics += 1,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 22);
|
||||
assert_eq!(future_transactions, 11);
|
||||
assert_eq!(future_blocks_economics, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_3_descriptor_set_and_safety_classes_are_exact() {
|
||||
let mut expected = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
"requestAirdrop",
|
||||
"sendTransaction",
|
||||
"simulateTransaction",
|
||||
];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
let mut reads = 0_usize;
|
||||
let mut writes = 0_usize;
|
||||
let mut simulations = 0_usize;
|
||||
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() != ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3 {
|
||||
continue;
|
||||
}
|
||||
actual.push(descriptor.method());
|
||||
match descriptor.operation_kind() {
|
||||
ksp_onchain_transport_lib::RpcOperationKind::Read => {
|
||||
reads += 1;
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
},
|
||||
ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission => {
|
||||
writes += 1;
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
},
|
||||
ksp_onchain_transport_lib::RpcOperationKind::Simulation => {
|
||||
simulations += 1;
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
},
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(reads, 8);
|
||||
assert_eq!(writes, 2);
|
||||
assert_eq!(simulations, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_003_transaction_read_subset_is_exact_and_retry_safe() {
|
||||
let mut expected = std::vec!["getFeeForMessage", "getLatestBlockhash", "getTransactionCount", "isBlockhashValid"];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for method_name in &expected {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.003 Transaction descriptor must exist");
|
||||
actual.push(descriptor.method());
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_004_transaction_read_subset_is_exact_and_retry_safe() {
|
||||
let mut expected = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
];
|
||||
let deferred = std::vec!["getTransaction", "requestAirdrop", "sendTransaction", "simulateTransaction"];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for method_name in &expected {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.004 Transaction descriptor must exist");
|
||||
actual.push(descriptor.method());
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
for method_name in &deferred {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("deferred Transaction descriptor must remain registered");
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 7);
|
||||
assert_eq!(deferred.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_005_transaction_read_subset_adds_complete_get_transaction_without_advancing_writes_or_simulation() {
|
||||
let mut expected = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
];
|
||||
let deferred = std::vec!["requestAirdrop", "sendTransaction", "simulateTransaction"];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for method_name in &expected {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.005 Transaction descriptor must exist");
|
||||
actual.push(descriptor.method());
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
let get_transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
|
||||
assert!(get_transaction.request_form_status().has_deprecated_legacy());
|
||||
for method_name in &deferred {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("deferred Transaction descriptor must remain registered");
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 8);
|
||||
assert_eq!(deferred.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_006_transaction_subset_adds_both_write_submissions_without_advancing_simulation() {
|
||||
let reads = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
];
|
||||
let writes = std::vec!["requestAirdrop", "sendTransaction"];
|
||||
for method_name in &reads {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.006 read descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
for method_name in &writes {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.006 write descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
}
|
||||
let simulation = ksp_onchain_transport_lib::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must remain registered");
|
||||
assert_eq!(simulation.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Simulation);
|
||||
assert_eq!(reads.len(), 8);
|
||||
assert_eq!(writes.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_007_completes_all_v0_2_3_transaction_wrappers_with_exact_safety_classes() {
|
||||
let reads = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
];
|
||||
let writes = std::vec!["requestAirdrop", "sendTransaction"];
|
||||
let simulations = std::vec!["simulateTransaction"];
|
||||
for method_name in &reads {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 read descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
for method_name in &writes {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 write descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
}
|
||||
for method_name in &simulations {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 simulation descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Simulation);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
assert_eq!(reads.len(), 8);
|
||||
assert_eq!(writes.len(), 2);
|
||||
assert_eq!(simulations.len(), 1);
|
||||
assert_eq!(reads.len() + writes.len() + simulations.len(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_008_ksp_transport_007_retro_audit_covers_all_typed_current_methods() {
|
||||
let mut audited = std::vec::Vec::<&str>::new();
|
||||
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||
if matches!(
|
||||
descriptor.coverage_release(),
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_1
|
||||
| ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2
|
||||
| ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3
|
||||
) {
|
||||
audited.push(descriptor.method());
|
||||
assert_eq!(descriptor.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Supported);
|
||||
match descriptor.operation_kind() {
|
||||
ksp_onchain_transport_lib::RpcOperationKind::Read | ksp_onchain_transport_lib::RpcOperationKind::Simulation => {
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
},
|
||||
ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission => {
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
audited.sort_unstable();
|
||||
assert_eq!(audited.len(), 37);
|
||||
assert_eq!(audited.first(), std::option::Option::Some(&"getAccountInfo"));
|
||||
assert_eq!(audited.last(), std::option::Option::Some(&"simulateTransaction"));
|
||||
let get_transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
|
||||
assert!(get_transaction.request_form_status().has_deprecated_legacy());
|
||||
}
|
||||
|
||||
120
crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
Normal file
120
crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
|
||||
// version: 4
|
||||
|
||||
//! Opt-in live Devnet smoke for representative pure Transport Accounts/Tokens/Cluster/Transactions reads.
|
||||
|
||||
fn devnet_pool() -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTransportPool> {
|
||||
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
|
||||
let role_settings = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||
role,
|
||||
true,
|
||||
std::vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()],
|
||||
0,
|
||||
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
),
|
||||
);
|
||||
let endpoint_url_result = ksp_onchain_transport_lib::HttpEndpointUrl::parse("https://api.devnet.solana.com");
|
||||
let endpoint_url = match endpoint_url_result {
|
||||
std::result::Result::Ok(endpoint_url) => endpoint_url,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||
"solana_devnet_public",
|
||||
true,
|
||||
ksp_onchain_transport_lib::HttpProviderName::new("solana-public"),
|
||||
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
|
||||
endpoint_url,
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(20),
|
||||
std::option::Option::Some(4),
|
||||
std::vec![role_settings],
|
||||
);
|
||||
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
ksp_onchain_transport_lib::HttpRetrySettings::new(1, std::time::Duration::from_millis(250), std::time::Duration::from_secs(2)),
|
||||
);
|
||||
return ksp_onchain_transport_lib::HttpTransportPool::new(settings);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "opt-in live Solana Devnet smoke; performs external network requests"]
|
||||
async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_and_transaction_read_wrappers() {
|
||||
let pool = devnet_pool().expect("programmatic Devnet Transport settings must construct a pool");
|
||||
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
|
||||
|
||||
let account_config = ksp_onchain_transport_lib::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let account = pool
|
||||
.get_account_info(&role, &ksp_core_lib::PRGIDPK_SOLANA_SYSTEM, std::option::Option::Some(&account_config))
|
||||
.await
|
||||
.expect("Devnet getAccountInfo smoke must succeed");
|
||||
assert!(account.context().slot() > 0);
|
||||
assert!(account.value().is_some());
|
||||
|
||||
// Follow the current official Devnet example shape with an ordinary owner, the canonical SPL Token program selector,
|
||||
// and an explicit finalized/jsonParsed config. The owner need not retain any token account; an empty list remains valid.
|
||||
let token_owner = "A1TMhSGzQxMr1TboBKtgixKz1sS6REASMxPo1qsyTSJd"
|
||||
.parse::<ksp_core_lib::Pubkey>()
|
||||
.expect("official Devnet token-owner example must remain a valid Pubkey literal");
|
||||
let token_program = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
.parse::<ksp_core_lib::Pubkey>()
|
||||
.expect("canonical SPL Token program id must remain a valid Pubkey literal");
|
||||
let token_selector = ksp_onchain_transport_lib::SolanaTokenAccountSelector::ProgramId(token_program);
|
||||
let token_config = ksp_onchain_transport_lib::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::JsonParsed),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let token_accounts = pool
|
||||
.get_token_accounts_by_owner(&role, &token_owner, &token_selector, std::option::Option::Some(&token_config))
|
||||
.await
|
||||
.expect("Devnet getTokenAccountsByOwner smoke must succeed with the documented finalized/jsonParsed request shape");
|
||||
assert!(token_accounts.context().slot() > 0);
|
||||
|
||||
let epoch = pool
|
||||
.get_epoch_info(
|
||||
&role,
|
||||
std::option::Option::Some(&ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.expect("Devnet getEpochInfo smoke must succeed");
|
||||
assert!(epoch.absolute_slot() > 0);
|
||||
|
||||
let vote_accounts = pool.get_vote_accounts(&role, std::option::Option::None).await.expect("Devnet getVoteAccounts smoke must succeed");
|
||||
assert!(!vote_accounts.current().is_empty() || !vote_accounts.delinquent().is_empty());
|
||||
|
||||
let transaction_context = ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let latest_blockhash = pool
|
||||
.get_latest_blockhash(&role, std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet getLatestBlockhash smoke must succeed");
|
||||
assert!(!latest_blockhash.value().blockhash().is_empty());
|
||||
assert!(latest_blockhash.value().last_valid_block_height() > 0);
|
||||
|
||||
let blockhash_valid = pool
|
||||
.is_blockhash_valid(&role, latest_blockhash.value().blockhash(), std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet isBlockhashValid smoke must succeed");
|
||||
assert_eq!(blockhash_valid.value(), &true);
|
||||
|
||||
let transaction_count = pool
|
||||
.get_transaction_count(&role, std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet getTransactionCount smoke must succeed");
|
||||
assert!(transaction_count > 0);
|
||||
}
|
||||
446
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
Normal file
446
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn account_config_serializes_all_common_fields() {
|
||||
let config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaDataSliceConfig::new(8, 32)),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(99),
|
||||
);
|
||||
assert_eq!(
|
||||
config.to_json_value(),
|
||||
serde_json::json!({"encoding":"base64","dataSlice":{"offset":8,"length":32},"commitment":"finalized","minContextSlot":99})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_accounts_config_preserves_filter_variants_and_flags() {
|
||||
let filters = std::vec![
|
||||
crate::SolanaProgramAccountFilter::DataSize(165),
|
||||
crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(4, crate::SolanaMemcmpBytes::Base64("AQID".to_owned()))),
|
||||
crate::SolanaProgramAccountFilter::TokenAccountState,
|
||||
];
|
||||
let config = crate::SolanaProgramAccountsConfig::new(
|
||||
crate::SolanaAccountInfoConfig::default(),
|
||||
filters,
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
assert_eq!(
|
||||
config.to_json_value(),
|
||||
serde_json::json!({
|
||||
"filters":[{"dataSize":165},{"memcmp":{"offset":4,"bytes":"AQID","encoding":"base64"}},"tokenAccountState"],
|
||||
"withContext":true,
|
||||
"sortResults":true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_wire_fixture_preserves_legacy_encoded_and_json_parsed_data() {
|
||||
let values: std::vec::Vec<serde_json::Value> =
|
||||
serde_json::from_str(include_str!("../fixtures/http/account_data.variants.json")).expect("fixture must decode");
|
||||
let legacy = crate::SolanaAccount::decode_wire("fixture", values[0].clone()).expect("legacy account must decode");
|
||||
assert!(matches!(legacy.data(), crate::SolanaAccountData::LegacyBinary(_)));
|
||||
assert_eq!(legacy.space(), std::option::Option::None);
|
||||
let encoded = crate::SolanaAccount::decode_wire("fixture", values[1].clone()).expect("encoded account must decode");
|
||||
assert!(matches!(encoded.data(), crate::SolanaAccountData::Encoded { encoding: crate::SolanaAccountEncoding::Base64Zstd, .. }));
|
||||
let parsed = crate::SolanaAccount::decode_wire("fixture", values[2].clone()).expect("parsed account must decode");
|
||||
assert!(matches!(parsed.data(), crate::SolanaAccountData::JsonParsed(_)), "jsonParsed fixture must retain parsed data");
|
||||
if let crate::SolanaAccountData::JsonParsed(value) = parsed.data() {
|
||||
assert_eq!(value.program(), "spl-token");
|
||||
assert_eq!(value.space(), 165);
|
||||
assert_eq!(value.parsed()["type"], serde_json::json!("account"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_largest_and_keyed_account_helpers_match_wire_shapes() {
|
||||
let config = crate::SolanaLargestAccountsConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(crate::SolanaLargestAccountsFilter::NonCirculating),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
assert_eq!(config.to_json_value(), serde_json::json!({"commitment":"finalized","filter":"nonCirculating","sortResults":true}));
|
||||
|
||||
let keyed = crate::SolanaKeyedAccount::decode_wire(
|
||||
"fixture",
|
||||
serde_json::json!({
|
||||
"pubkey":"11111111111111111111111111111111",
|
||||
"account":{
|
||||
"lamports":42,
|
||||
"data":["", "base64"],
|
||||
"owner":"11111111111111111111111111111111",
|
||||
"executable":false,
|
||||
"rentEpoch":0,
|
||||
"space":0
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect("keyed account must decode");
|
||||
assert_eq!(keyed.pubkey().to_string(), "11111111111111111111111111111111");
|
||||
assert_eq!(keyed.account().lamports(), 42);
|
||||
|
||||
let balance = crate::SolanaAccountBalance::decode_wire("fixture", serde_json::json!({"address":"11111111111111111111111111111111","lamports":99}))
|
||||
.expect("account balance must decode");
|
||||
assert_eq!(balance.address().to_string(), "11111111111111111111111111111111");
|
||||
assert_eq!(balance.lamports(), 99);
|
||||
}
|
||||
|
||||
fn pool_for_url(url: &str) -> crate::HttpTransportPool {
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"fixture",
|
||||
true,
|
||||
crate::HttpProviderName::new("fixture"),
|
||||
crate::HttpClusterName::new("local"),
|
||||
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::Some(1),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
||||
);
|
||||
return crate::HttpTransportPool::new(settings).expect("fixture pool must build");
|
||||
}
|
||||
|
||||
fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHandle<std::string::String>) {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
|
||||
let address = listener.local_addr().expect("fixture listener address must resolve");
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("fixture server must accept one request");
|
||||
let request = read_request(&mut stream);
|
||||
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
|
||||
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
|
||||
return request;
|
||||
});
|
||||
return (format!("http://{address}"), handle);
|
||||
}
|
||||
|
||||
fn read_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
||||
let mut bytes = std::vec::Vec::new();
|
||||
let mut buffer = [0_u8; 1024];
|
||||
loop {
|
||||
let count = std::io::Read::read(stream, &mut buffer).expect("fixture request must read");
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
bytes.extend_from_slice(&buffer[..count]);
|
||||
if request_complete(bytes.as_slice()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return std::string::String::from_utf8(bytes).expect("fixture request must be UTF-8");
|
||||
}
|
||||
|
||||
fn request_complete(bytes: &[u8]) -> bool {
|
||||
let text = match std::str::from_utf8(bytes) {
|
||||
std::result::Result::Ok(text) => text,
|
||||
std::result::Result::Err(_) => return false,
|
||||
};
|
||||
let header_end = match text.find("\r\n\r\n") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
let mut content_length = 0_usize;
|
||||
for line in text[..header_end].lines() {
|
||||
let (name, value) = match line.split_once(':') {
|
||||
std::option::Option::Some(parts) => parts,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if name.eq_ignore_ascii_case("content-length") {
|
||||
content_length = value.trim().parse::<usize>().expect("content length must parse");
|
||||
}
|
||||
}
|
||||
return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length);
|
||||
}
|
||||
|
||||
fn request_body(request: &str) -> serde_json::Value {
|
||||
let body = request.split("\r\n\r\n").nth(1).expect("fixture request body must exist");
|
||||
return serde_json::from_str(body).expect("fixture request body must be JSON");
|
||||
}
|
||||
|
||||
fn fixture_pubkey(value: &str) -> ksp_core_lib::Pubkey {
|
||||
return value.parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_account_info_serializes_config_and_preserves_account_wire() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_account_info.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaDataSliceConfig::new(2, 4)),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(400_000_000),
|
||||
);
|
||||
let response = pool
|
||||
.get_account_info(&crate::HttpRoleName::new("default"), &account, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("getAccountInfo fixture must succeed");
|
||||
assert_eq!(response.context().slot(), 410_000_001);
|
||||
let returned = response.value().as_ref().expect("fixture account must be present");
|
||||
assert_eq!(returned.lamports(), 2_039_280);
|
||||
assert_eq!(returned.owner(), &account);
|
||||
assert_eq!(returned.space(), std::option::Option::Some(4));
|
||||
assert!(matches!(returned.data(), crate::SolanaAccountData::Encoded { encoding: crate::SolanaAccountEncoding::Base64, .. }));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getAccountInfo"));
|
||||
assert_eq!(body["params"][0], serde_json::json!(account.to_string()));
|
||||
assert_eq!(body["params"][1]["encoding"], serde_json::json!("base64"));
|
||||
assert_eq!(body["params"][1]["dataSlice"], serde_json::json!({"offset":2,"length":4}));
|
||||
assert_eq!(body["params"][1]["commitment"], serde_json::json!("finalized"));
|
||||
assert_eq!(body["params"][1]["minContextSlot"], serde_json::json!(400_000_000_u64));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_account_info_preserves_missing_account_as_none() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_account_info.null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let response = pool
|
||||
.get_account_info(&crate::HttpRoleName::new("default"), &account, std::option::Option::None)
|
||||
.await
|
||||
.expect("missing getAccountInfo fixture must succeed");
|
||||
assert_eq!(response.value(), &std::option::Option::None);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"].as_array().map(std::vec::Vec::len), std::option::Option::Some(1));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_account_info_rejects_invalid_owner_without_exposing_value() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_account_info.invalid_owner.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let result = pool.get_account_info(&crate::HttpRoleName::new("default"), &account, std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid account owner must fail typed decoding");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert!(!format!("{error:?}").contains("not-a-pubkey"));
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_largest_accounts_serializes_extended_config_and_decodes_order() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_largest_accounts.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaLargestAccountsConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(crate::SolanaLargestAccountsFilter::Circulating),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
let response = pool
|
||||
.get_largest_accounts(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("getLargestAccounts fixture must succeed");
|
||||
assert_eq!(response.value().len(), 2);
|
||||
assert_eq!(response.value()[0].lamports(), 999_999_999);
|
||||
assert_eq!(response.value()[1].lamports(), 888_888_888);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getLargestAccounts"));
|
||||
assert_eq!(body["params"][0], serde_json::json!({"commitment":"finalized","filter":"circulating","sortResults":true}));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_largest_accounts_rejects_invalid_address() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_largest_accounts.invalid_address.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_largest_accounts(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid largest-account address must fail typed decoding");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert!(!format!("{error:?}").contains("invalid-address"));
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_minimum_balance_for_rent_exemption_serializes_length_and_commitment() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_minimum_balance_for_rent_exemption.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Processed));
|
||||
let result = pool
|
||||
.get_minimum_balance_for_rent_exemption(&crate::HttpRoleName::new("default"), 50, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("rent-exemption fixture must succeed");
|
||||
assert_eq!(result, 890_880);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getMinimumBalanceForRentExemption"));
|
||||
assert_eq!(body["params"], serde_json::json!([50,{"commitment":"processed"}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_minimum_balance_for_rent_exemption_preserves_rpc_application_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_minimum_balance_for_rent_exemption.error.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_minimum_balance_for_rent_exemption(&crate::HttpRoleName::new("default"), usize::MAX, std::option::Option::None).await;
|
||||
let error = result.expect_err("remote invalid parameter must remain an RPC application error");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_multiple_accounts_preserves_order_nulls_and_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_multiple_accounts.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let accounts = std::vec![
|
||||
fixture_pubkey("11111111111111111111111111111111"),
|
||||
fixture_pubkey("ComputeBudget111111111111111111111111111111"),
|
||||
fixture_pubkey("Stake11111111111111111111111111111111111111"),
|
||||
];
|
||||
let config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::JsonParsed),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(400_000_001),
|
||||
);
|
||||
let response = pool
|
||||
.get_multiple_accounts(&crate::HttpRoleName::new("default"), accounts.as_slice(), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("getMultipleAccounts fixture must succeed");
|
||||
assert_eq!(response.value().len(), 3);
|
||||
assert_eq!(response.value()[0].as_ref().map(crate::SolanaAccount::lamports), std::option::Option::Some(10));
|
||||
assert!(response.value()[1].is_none());
|
||||
assert_eq!(response.value()[2].as_ref().map(crate::SolanaAccount::lamports), std::option::Option::Some(20));
|
||||
assert!(matches!(response.value()[2].as_ref().map(crate::SolanaAccount::data), std::option::Option::Some(crate::SolanaAccountData::JsonParsed(_))));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getMultipleAccounts"));
|
||||
assert_eq!(body["params"][0], serde_json::json!(accounts.iter().map(std::string::ToString::to_string).collect::<std::vec::Vec<_>>()));
|
||||
assert_eq!(body["params"][1]["encoding"], serde_json::json!("jsonParsed"));
|
||||
assert_eq!(body["params"][1]["commitment"], serde_json::json!("confirmed"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_multiple_accounts_rejects_more_than_100_before_io() {
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let accounts = std::vec![account; 101];
|
||||
let result = pool.get_multiple_accounts(&crate::HttpRoleName::new("default"), accounts.as_slice(), std::option::Option::None).await;
|
||||
let error = result.expect_err("more than 100 accounts must be rejected locally");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[0].key(), "rpc_method");
|
||||
assert_eq!(error.context()[1].key(), "account_count");
|
||||
assert_eq!(error.context()[1].value(), "101");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_program_accounts_serializes_filters_and_decodes_bare_result() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_program_accounts.bare.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let program_id = fixture_pubkey("11111111111111111111111111111111");
|
||||
let filters = std::vec![
|
||||
crate::SolanaProgramAccountFilter::DataSize(3),
|
||||
crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(0, crate::SolanaMemcmpBytes::Base64("AQID".to_owned()))),
|
||||
crate::SolanaProgramAccountFilter::TokenAccountState,
|
||||
];
|
||||
let account_config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaDataSliceConfig::new(0, 3)),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(400_000_002),
|
||||
);
|
||||
let config = crate::SolanaProgramAccountsConfig::new(account_config, filters, std::option::Option::Some(false), std::option::Option::Some(true));
|
||||
let result = pool
|
||||
.get_program_accounts(&crate::HttpRoleName::new("default"), &program_id, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("bare getProgramAccounts fixture must succeed");
|
||||
assert!(matches!(&result, crate::SolanaProgramAccountsResult::Accounts(_)), "bare fixture must preserve the bare account-list result");
|
||||
if let crate::SolanaProgramAccountsResult::Accounts(accounts) = result {
|
||||
assert_eq!(accounts.len(), 1);
|
||||
assert_eq!(accounts[0].account().lamports(), 42);
|
||||
}
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getProgramAccounts"));
|
||||
assert_eq!(
|
||||
body["params"][1]["filters"],
|
||||
serde_json::json!([{"dataSize":3},{"memcmp":{"offset":0,"bytes":"AQID","encoding":"base64"}},"tokenAccountState"]),
|
||||
);
|
||||
assert_eq!(body["params"][1]["withContext"], serde_json::json!(false));
|
||||
assert_eq!(body["params"][1]["sortResults"], serde_json::json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_program_accounts_preserves_contextual_result() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_program_accounts.context.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let program_id = fixture_pubkey("11111111111111111111111111111111");
|
||||
let config = crate::SolanaProgramAccountsConfig::new(
|
||||
crate::SolanaAccountInfoConfig::default(),
|
||||
std::vec::Vec::new(),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = pool
|
||||
.get_program_accounts(&crate::HttpRoleName::new("default"), &program_id, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("contextual getProgramAccounts fixture must succeed");
|
||||
assert!(matches!(&result, crate::SolanaProgramAccountsResult::Context(_)), "contextual fixture must preserve RpcResponse wrapper");
|
||||
if let crate::SolanaProgramAccountsResult::Context(response) = result {
|
||||
assert_eq!(response.context().slot(), 410_000_007);
|
||||
assert_eq!(response.value().len(), 1);
|
||||
}
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"][1]["withContext"], serde_json::json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_program_accounts_rejects_filter_cardinality_and_oversized_raw_memcmp_before_io() {
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let program_id = fixture_pubkey("11111111111111111111111111111111");
|
||||
let too_many = crate::SolanaProgramAccountsConfig::new(
|
||||
crate::SolanaAccountInfoConfig::default(),
|
||||
std::vec![
|
||||
crate::SolanaProgramAccountFilter::DataSize(1),
|
||||
crate::SolanaProgramAccountFilter::DataSize(2),
|
||||
crate::SolanaProgramAccountFilter::DataSize(3),
|
||||
crate::SolanaProgramAccountFilter::DataSize(4),
|
||||
crate::SolanaProgramAccountFilter::DataSize(5),
|
||||
],
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = pool.get_program_accounts(&crate::HttpRoleName::new("default"), &program_id, std::option::Option::Some(&too_many)).await;
|
||||
let error = result.expect_err("more than four program-account filters must be rejected locally");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].value(), "5");
|
||||
|
||||
let oversized = crate::SolanaProgramAccountsConfig::new(
|
||||
crate::SolanaAccountInfoConfig::default(),
|
||||
std::vec![crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(0, crate::SolanaMemcmpBytes::Bytes(std::vec![0_u8; 129]),))],
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = pool.get_program_accounts(&crate::HttpRoleName::new("default"), &program_id, std::option::Option::Some(&oversized)).await;
|
||||
let error = result.expect_err("raw memcmp data above 128 bytes must be rejected locally");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].value(), "129");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_get_multiple_accounts_rejects_response_count_mismatch() {
|
||||
let value = serde_json::json!({
|
||||
"context":{"apiVersion":"4.2.1","slot":410000008},
|
||||
"value":[]
|
||||
});
|
||||
let result = super::decode_multiple_accounts_response("getMultipleAccounts", value, 1);
|
||||
let error = result.expect_err("response count mismatch must fail typed decoding");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert_eq!(error.context()[1].key(), "expected_count");
|
||||
assert_eq!(error.context()[1].value(), "1");
|
||||
assert_eq!(error.context()[2].key(), "actual_count");
|
||||
assert_eq!(error.context()[2].value(), "0");
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_canary.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn pool_for_url(url: &str) -> crate::HttpTransportPool {
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
|
||||
487
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
Normal file
487
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
// version: 4
|
||||
|
||||
#[test]
|
||||
fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() {
|
||||
let value: serde_json::Value = serde_json::from_str(include_str!("../fixtures/http/cluster_node.v4_2_1.json")).expect("fixture must decode");
|
||||
let node = crate::SolanaClusterNode::decode_wire("getClusterNodes", value).expect("cluster node must decode");
|
||||
assert_eq!(node.client_id(), std::option::Option::Some("Agave"));
|
||||
assert_eq!(node.rpc(), std::option::Option::Some("127.0.0.1:8899"));
|
||||
assert_eq!(node.pubsub(), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_account_fixture_preserves_optional_basis_point_commission_and_epoch_credit_triples() {
|
||||
let value: serde_json::Value = serde_json::from_str(include_str!("../fixtures/http/vote_account.v4_2_1.json")).expect("fixture must decode");
|
||||
let info = crate::SolanaVoteAccountInfo::decode_wire("getVoteAccounts", value).expect("vote account must decode");
|
||||
assert_eq!(info.commission(), 8);
|
||||
assert_eq!(info.inflation_rewards_commission_bps(), std::option::Option::Some(750));
|
||||
assert_eq!(info.epoch_credits().len(), 2);
|
||||
assert_eq!(info.epoch_credits()[0].epoch(), 700);
|
||||
assert_eq!(info.epoch_credits()[0].previous_credits(), 90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_account_wire_accepts_nodes_predating_basis_point_commission_field() {
|
||||
let value = serde_json::json!({
|
||||
"votePubkey":"11111111111111111111111111111111",
|
||||
"nodePubkey":"11111111111111111111111111111111",
|
||||
"activatedStake":1,
|
||||
"commission":5,
|
||||
"epochVoteAccount":true,
|
||||
"epochCredits":[],
|
||||
"lastVote":2,
|
||||
"rootSlot":1
|
||||
});
|
||||
let info = crate::SolanaVoteAccountInfo::decode_wire("getVoteAccounts", value).expect("legacy node shape must decode");
|
||||
assert_eq!(info.inflation_rewards_commission_bps(), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leader_schedule_request_encodes_current_epoch_and_slot_overloads_without_ambiguous_arrays() {
|
||||
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let config = crate::SolanaLeaderScheduleConfig::new(std::option::Option::Some(identity), std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||
assert_eq!(
|
||||
crate::SolanaLeaderScheduleRequest::CurrentEpoch(std::option::Option::Some(config.clone())).to_json_params(),
|
||||
std::vec![serde_json::json!({"identity":"11111111111111111111111111111111","commitment":"finalized"})]
|
||||
);
|
||||
assert_eq!(
|
||||
crate::SolanaLeaderScheduleRequest::Slot { slot: 123, config: std::option::Option::Some(config) }.to_json_params(),
|
||||
std::vec![serde_json::json!(123), serde_json::json!({"identity":"11111111111111111111111111111111","commitment":"finalized"})]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_snapshot_and_leader_helpers_preserve_wire_shapes() {
|
||||
let epoch = crate::SolanaEpochInfo::decode_wire(
|
||||
"getEpochInfo",
|
||||
serde_json::json!({"absoluteSlot":10,"blockHeight":9,"epoch":2,"slotIndex":3,"slotsInEpoch":32,"transactionCount":null}),
|
||||
)
|
||||
.expect("epoch info must decode");
|
||||
assert_eq!(epoch.transaction_count(), std::option::Option::None);
|
||||
|
||||
let schedule = crate::SolanaEpochSchedule::decode_wire(
|
||||
"getEpochSchedule",
|
||||
serde_json::json!({"firstNormalEpoch":1,"firstNormalSlot":32,"leaderScheduleSlotOffset":32,"slotsPerEpoch":64,"warmup":false}),
|
||||
)
|
||||
.expect("epoch schedule must decode");
|
||||
assert_eq!(schedule.slots_per_epoch(), 64);
|
||||
|
||||
let snapshot = crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", serde_json::json!({"full":100,"incremental":null}))
|
||||
.expect("snapshot info must decode");
|
||||
assert_eq!(snapshot.full(), 100);
|
||||
assert_eq!(snapshot.incremental(), std::option::Option::None);
|
||||
|
||||
let leader = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", serde_json::json!({"11111111111111111111111111111111":[0,2,4]}))
|
||||
.expect("leader schedule must decode");
|
||||
assert_eq!(leader.entries().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_status_and_config_helpers_preserve_wire_shapes() {
|
||||
let vote_pubkey = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let config = crate::SolanaVoteAccountsConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(vote_pubkey),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(128),
|
||||
);
|
||||
assert_eq!(
|
||||
config.to_json_value(),
|
||||
serde_json::json!({"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128})
|
||||
);
|
||||
|
||||
let status = crate::SolanaVoteAccountStatus::decode_wire(
|
||||
"getVoteAccounts",
|
||||
serde_json::json!({
|
||||
"current":[{
|
||||
"votePubkey":"11111111111111111111111111111111",
|
||||
"nodePubkey":"11111111111111111111111111111111",
|
||||
"activatedStake":1,
|
||||
"commission":5,
|
||||
"epochVoteAccount":true,
|
||||
"epochCredits":[],
|
||||
"lastVote":2,
|
||||
"rootSlot":1
|
||||
}],
|
||||
"delinquent":[]
|
||||
}),
|
||||
)
|
||||
.expect("vote account status must decode");
|
||||
assert_eq!(status.current().len(), 1);
|
||||
assert!(status.delinquent().is_empty());
|
||||
}
|
||||
|
||||
fn pool_for_url(url: &str) -> crate::HttpTransportPool {
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"fixture",
|
||||
true,
|
||||
crate::HttpProviderName::new("fixture"),
|
||||
crate::HttpClusterName::new("local"),
|
||||
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::Some(1),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
||||
);
|
||||
return crate::HttpTransportPool::new(settings).expect("fixture pool must build");
|
||||
}
|
||||
|
||||
fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHandle<std::string::String>) {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
|
||||
let address = listener.local_addr().expect("fixture listener address must resolve");
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("fixture server must accept one request");
|
||||
let request = read_request(&mut stream);
|
||||
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
|
||||
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
|
||||
return request;
|
||||
});
|
||||
return (format!("http://{address}"), handle);
|
||||
}
|
||||
|
||||
fn read_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
||||
let mut bytes = std::vec::Vec::new();
|
||||
let mut buffer = [0_u8; 1024];
|
||||
loop {
|
||||
let count = std::io::Read::read(stream, &mut buffer).expect("fixture request must read");
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
bytes.extend_from_slice(&buffer[..count]);
|
||||
if request_complete(bytes.as_slice()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return std::string::String::from_utf8(bytes).expect("fixture request must be UTF-8");
|
||||
}
|
||||
|
||||
fn request_complete(bytes: &[u8]) -> bool {
|
||||
let text = match std::str::from_utf8(bytes) {
|
||||
std::result::Result::Ok(text) => text,
|
||||
std::result::Result::Err(_) => return false,
|
||||
};
|
||||
let header_end = match text.find("\r\n\r\n") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
let mut content_length = 0_usize;
|
||||
for line in text[..header_end].lines() {
|
||||
let (name, value) = match line.split_once(':') {
|
||||
std::option::Option::Some(parts) => parts,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if name.eq_ignore_ascii_case("content-length") {
|
||||
content_length = value.trim().parse::<usize>().expect("content length must parse");
|
||||
}
|
||||
}
|
||||
return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length);
|
||||
}
|
||||
|
||||
fn request_body(request: &str) -> serde_json::Value {
|
||||
let body = request.split("\r\n\r\n").nth(1).expect("fixture request body must exist");
|
||||
return serde_json::from_str(body).expect("fixture request body must be JSON");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_cluster_nodes_preserves_optional_v4_fields_and_omissions() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_cluster_nodes.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let nodes = pool.get_cluster_nodes(&crate::HttpRoleName::new("default")).await.expect("cluster nodes fixture must succeed");
|
||||
assert_eq!(nodes.len(), 2);
|
||||
assert_eq!(nodes[0].client_id(), std::option::Option::Some("Agave"));
|
||||
assert_eq!(nodes[0].serve_repair(), std::option::Option::Some("127.0.0.1:8004"));
|
||||
assert_eq!(nodes[1].rpc(), std::option::Option::None);
|
||||
assert_eq!(nodes[1].client_id(), std::option::Option::None);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getClusterNodes"));
|
||||
assert_eq!(body["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_cluster_nodes_rejects_invalid_node_pubkey() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_cluster_nodes.invalid_pubkey.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_cluster_nodes(&crate::HttpRoleName::new("default")).await;
|
||||
let error = result.expect_err("invalid cluster node pubkey must reject typed response");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_epoch_info_serializes_context_config_and_preserves_nullable_transaction_count() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_epoch_info.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(429_000_000));
|
||||
let info = pool
|
||||
.get_epoch_info(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("epoch info fixture must succeed");
|
||||
assert_eq!(info.absolute_slot(), 430_000_001);
|
||||
assert_eq!(info.transaction_count(), std::option::Option::None);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["params"], serde_json::json!([{"commitment":"finalized","minContextSlot":429000000}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_epoch_info_omits_explicitly_empty_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_epoch_info.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaContextConfig::default();
|
||||
let info = pool
|
||||
.get_epoch_info(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("epoch info fixture must succeed");
|
||||
assert_eq!(info.epoch(), 995);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_epoch_schedule_preserves_fixed_wire_shape() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_epoch_schedule.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let schedule = pool.get_epoch_schedule(&crate::HttpRoleName::new("default")).await.expect("epoch schedule fixture must succeed");
|
||||
assert_eq!(schedule.slots_per_epoch(), 432_000);
|
||||
assert!(!schedule.warmup());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["method"], serde_json::json!("getEpochSchedule"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_highest_snapshot_slot_preserves_nullable_incremental_slot() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_highest_snapshot_slot.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let snapshot = pool.get_highest_snapshot_slot(&crate::HttpRoleName::new("default")).await.expect("snapshot fixture must succeed");
|
||||
assert_eq!(snapshot.full(), 429_990_000);
|
||||
assert_eq!(snapshot.incremental(), std::option::Option::None);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_highest_snapshot_slot_preserves_no_snapshot_rpc_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_highest_snapshot_slot.error.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_highest_snapshot_slot(&crate::HttpRoleName::new("default")).await;
|
||||
let error = result.expect_err("no snapshot must remain an RPC application error");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_identity_decodes_pubkey_object() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_identity.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let identity = pool.get_identity(&crate::HttpRoleName::new("default")).await.expect("identity fixture must succeed");
|
||||
assert_eq!(identity.to_string(), "ComputeBudget111111111111111111111111111111");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["method"], serde_json::json!("getIdentity"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_identity_rejects_invalid_wire_pubkey() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_identity.invalid_pubkey.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_identity(&crate::HttpRoleName::new("default")).await;
|
||||
let error = result.expect_err("invalid identity pubkey must reject typed response");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_max_cluster_slots_decode_u64_without_params() {
|
||||
let (retransmit_url, retransmit_handle) = serve_once(include_str!("../fixtures/http/get_max_retransmit_slot.success.json"));
|
||||
let retransmit_pool = pool_for_url(retransmit_url.as_str());
|
||||
let retransmit = retransmit_pool.get_max_retransmit_slot(&crate::HttpRoleName::new("default")).await.expect("max retransmit slot fixture must succeed");
|
||||
assert_eq!(retransmit, 430_000_010);
|
||||
let retransmit_request = retransmit_handle.join().expect("fixture server must join");
|
||||
let retransmit_body = request_body(retransmit_request.as_str());
|
||||
assert_eq!(retransmit_body["method"], serde_json::json!("getMaxRetransmitSlot"));
|
||||
assert_eq!(retransmit_body["params"], serde_json::json!([]));
|
||||
let (shred_url, shred_handle) = serve_once(include_str!("../fixtures/http/get_max_shred_insert_slot.success.json"));
|
||||
let shred_pool = pool_for_url(shred_url.as_str());
|
||||
let shred = shred_pool.get_max_shred_insert_slot(&crate::HttpRoleName::new("default")).await.expect("max shred insert slot fixture must succeed");
|
||||
assert_eq!(shred, 430_000_011);
|
||||
let shred_request = shred_handle.join().expect("fixture server must join");
|
||||
let shred_body = request_body(shred_request.as_str());
|
||||
assert_eq!(shred_body["method"], serde_json::json!("getMaxShredInsertSlot"));
|
||||
assert_eq!(shred_body["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_leader_schedule_serializes_slot_config_and_decodes_map() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_leader_schedule.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let config = crate::SolanaLeaderScheduleConfig::new(std::option::Option::Some(identity), std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||
let request = crate::SolanaLeaderScheduleRequest::Slot { slot: 430_000_000, config: std::option::Option::Some(config) };
|
||||
let schedule = pool
|
||||
.get_leader_schedule(&crate::HttpRoleName::new("default"), &request)
|
||||
.await
|
||||
.expect("leader schedule fixture must succeed")
|
||||
.expect("leader schedule fixture must not be null");
|
||||
assert_eq!(schedule.entries().get(&identity), std::option::Option::Some(&std::vec![0, 2, 4]));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getLeaderSchedule"));
|
||||
assert_eq!(body["params"], serde_json::json!([430000000,{"identity":"11111111111111111111111111111111","commitment":"finalized"}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_leader_schedule_preserves_null_result() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_leader_schedule.null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let schedule = pool
|
||||
.get_leader_schedule(&crate::HttpRoleName::new("default"), &crate::SolanaLeaderScheduleRequest::default())
|
||||
.await
|
||||
.expect("nullable leader schedule fixture must succeed");
|
||||
assert_eq!(schedule, std::option::Option::None);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_slot_serializes_context_config_and_omits_empty_config() {
|
||||
let (configured_url, configured_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json"));
|
||||
let configured_pool = pool_for_url(configured_url.as_str());
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(429_999_999));
|
||||
let slot = configured_pool.get_slot(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)).await.expect("slot fixture must succeed");
|
||||
assert_eq!(slot, 430_000_020);
|
||||
let request = configured_handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"confirmed","minContextSlot":429999999}]));
|
||||
|
||||
let (empty_url, empty_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json"));
|
||||
let empty_pool = pool_for_url(empty_url.as_str());
|
||||
let empty = crate::SolanaContextConfig::default();
|
||||
let slot = empty_pool
|
||||
.get_slot(&crate::HttpRoleName::new("default"), std::option::Option::Some(&empty))
|
||||
.await
|
||||
.expect("slot fixture with empty config must succeed");
|
||||
assert_eq!(slot, 430_000_020);
|
||||
let request = empty_handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_slot_leader_serializes_context_config_and_decodes_pubkey() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leader.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(430_000_000));
|
||||
let leader = pool
|
||||
.get_slot_leader(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("slot leader fixture must succeed");
|
||||
assert_eq!(leader.to_string(), "ComputeBudget111111111111111111111111111111");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"finalized","minContextSlot":430000000}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_slot_leader_rejects_invalid_wire_pubkey() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leader.invalid_pubkey.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_slot_leader(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid slot leader must reject typed response");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_slot_leaders_preserves_order_and_serializes_range() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leaders.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let leaders = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 430_000_020, 2).await.expect("slot leaders fixture must succeed");
|
||||
assert_eq!(leaders.len(), 2);
|
||||
assert_eq!(leaders[0].to_string(), "11111111111111111111111111111111");
|
||||
assert_eq!(leaders[1].to_string(), "ComputeBudget111111111111111111111111111111");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getSlotLeaders"));
|
||||
assert_eq!(body["params"], serde_json::json!([430000020, 2]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_slot_leaders_rejects_out_of_range_limit_before_io() {
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let zero = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 0).await;
|
||||
let zero_error = zero.expect_err("zero slot-leader limit must reject before I/O");
|
||||
assert_eq!(zero_error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
let above = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 5_001).await;
|
||||
let above_error = above.expect_err("slot-leader limit above 5000 must reject before I/O");
|
||||
assert_eq!(above_error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_slot_leaders_rejects_invalid_wire_pubkey() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leaders.invalid_pubkey.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 2).await;
|
||||
let error = result.expect_err("invalid slot leader list item must reject typed response");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_vote_accounts_serializes_full_config_and_preserves_optional_bps() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let vote_pubkey = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let config = crate::SolanaVoteAccountsConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(vote_pubkey),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(128),
|
||||
);
|
||||
let status = pool
|
||||
.get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("vote accounts fixture must succeed");
|
||||
assert_eq!(status.current().len(), 1);
|
||||
assert_eq!(status.delinquent().len(), 1);
|
||||
assert_eq!(status.current()[0].inflation_rewards_commission_bps(), std::option::Option::Some(750));
|
||||
assert_eq!(status.current()[0].epoch_credits().len(), 2);
|
||||
assert_eq!(status.delinquent()[0].inflation_rewards_commission_bps(), std::option::Option::None);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(
|
||||
request_body(request.as_str())["params"],
|
||||
serde_json::json!([{"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128}])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_vote_accounts_omits_explicitly_empty_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaVoteAccountsConfig::default();
|
||||
let status = pool
|
||||
.get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("vote accounts fixture with empty config must succeed");
|
||||
assert_eq!(status.current().len(), 1);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_vote_accounts_rejects_invalid_wire_pubkey() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.invalid_pubkey.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid vote-account pubkey must reject typed response");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
21
crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
Normal file
21
crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn shared_context_configs_preserve_commitment_and_min_context_slot() {
|
||||
let commitment = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||
assert_eq!(commitment.to_json_value(), serde_json::json!({"commitment":"finalized"}));
|
||||
let context = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(42));
|
||||
assert_eq!(context.to_json_value(), serde_json::json!({"commitment":"confirmed","minContextSlot":42}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_context_preserves_nullable_api_version() {
|
||||
let with_version = crate::SolanaRpcContext::decode_wire("fixture", serde_json::json!({"slot":10,"apiVersion":"4.2.1"})).expect("context must decode");
|
||||
assert_eq!(with_version.slot(), 10);
|
||||
assert_eq!(with_version.api_version(), std::option::Option::Some("4.2.1"));
|
||||
let response = crate::SolanaRpcResponse::new(with_version, 7_u64);
|
||||
assert_eq!(response.value(), &7_u64);
|
||||
let without_version = crate::SolanaRpcContext::decode_wire("fixture", serde_json::json!({"slot":11})).expect("context must decode");
|
||||
assert_eq!(without_version.api_version(), std::option::Option::None);
|
||||
}
|
||||
257
crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
Normal file
257
crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn token_selector_is_exclusive_by_construction() {
|
||||
let mint = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
assert_eq!(crate::SolanaTokenAccountSelector::Mint(mint).to_json_value(), serde_json::json!({"mint":"11111111111111111111111111111111"}));
|
||||
assert_eq!(crate::SolanaTokenAccountSelector::ProgramId(mint).to_json_value(), serde_json::json!({"programId":"11111111111111111111111111111111"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_amount_fixture_preserves_nullable_ui_amount() {
|
||||
let value: serde_json::Value = serde_json::from_str(include_str!("../fixtures/http/token_amount.null_ui.json")).expect("fixture must decode");
|
||||
let amount = crate::SolanaTokenAmount::decode_wire("fixture", value).expect("token amount must decode");
|
||||
assert_eq!(amount.amount(), "18446744073709551615");
|
||||
assert_eq!(amount.decimals(), 9);
|
||||
assert_eq!(amount.ui_amount(), std::option::Option::None);
|
||||
assert_eq!(amount.ui_amount_string(), "18446744073.709551615");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_token_account_balance_helper_preserves_address_and_amount() {
|
||||
let value = serde_json::json!({
|
||||
"address":"11111111111111111111111111111111",
|
||||
"amount":"10",
|
||||
"decimals":2,
|
||||
"uiAmount":0.1,
|
||||
"uiAmountString":"0.1"
|
||||
});
|
||||
let balance = crate::SolanaTokenAccountBalance::decode_wire("fixture", value).expect("token account balance must decode");
|
||||
assert_eq!(balance.address().to_string(), "11111111111111111111111111111111");
|
||||
assert_eq!(balance.amount().amount(), "10");
|
||||
}
|
||||
|
||||
fn pool_for_url(url: &str) -> crate::HttpTransportPool {
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"fixture",
|
||||
true,
|
||||
crate::HttpProviderName::new("fixture"),
|
||||
crate::HttpClusterName::new("local"),
|
||||
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::Some(1),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
||||
);
|
||||
return crate::HttpTransportPool::new(settings).expect("fixture pool must build");
|
||||
}
|
||||
|
||||
fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHandle<std::string::String>) {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
|
||||
let address = listener.local_addr().expect("fixture listener address must resolve");
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("fixture server must accept one request");
|
||||
let request = read_request(&mut stream);
|
||||
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
|
||||
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
|
||||
return request;
|
||||
});
|
||||
return (format!("http://{address}"), handle);
|
||||
}
|
||||
|
||||
fn read_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
||||
let mut bytes = std::vec::Vec::new();
|
||||
let mut buffer = [0_u8; 1024];
|
||||
loop {
|
||||
let count = std::io::Read::read(stream, &mut buffer).expect("fixture request must read");
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
bytes.extend_from_slice(&buffer[..count]);
|
||||
if request_complete(bytes.as_slice()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return std::string::String::from_utf8(bytes).expect("fixture request must be UTF-8");
|
||||
}
|
||||
|
||||
fn request_complete(bytes: &[u8]) -> bool {
|
||||
let text = match std::str::from_utf8(bytes) {
|
||||
std::result::Result::Ok(text) => text,
|
||||
std::result::Result::Err(_) => return false,
|
||||
};
|
||||
let header_end = match text.find("\r\n\r\n") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
let mut content_length = 0_usize;
|
||||
for line in text[..header_end].lines() {
|
||||
let (name, value) = match line.split_once(':') {
|
||||
std::option::Option::Some(parts) => parts,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if name.eq_ignore_ascii_case("content-length") {
|
||||
content_length = value.trim().parse::<usize>().expect("content length must parse");
|
||||
}
|
||||
}
|
||||
return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length);
|
||||
}
|
||||
|
||||
fn request_body(request: &str) -> serde_json::Value {
|
||||
let body = request.split("\r\n\r\n").nth(1).expect("fixture request body must exist");
|
||||
return serde_json::from_str(body).expect("fixture request body must be JSON");
|
||||
}
|
||||
|
||||
fn fixture_pubkey(value: &str) -> ksp_core_lib::Pubkey {
|
||||
return value.parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_token_account_balance_serializes_commitment_and_preserves_nullable_ui_amount() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_account_balance.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let config = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||
let response = pool
|
||||
.get_token_account_balance(&crate::HttpRoleName::new("default"), &account, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("token balance fixture must succeed");
|
||||
assert_eq!(response.context().slot(), 420_000_001);
|
||||
assert_eq!(response.value().amount(), "18446744073709551615");
|
||||
assert_eq!(response.value().ui_amount(), std::option::Option::None);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getTokenAccountBalance"));
|
||||
assert_eq!(body["params"], serde_json::json!([account.to_string(),{"commitment":"finalized"}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_token_account_balance_preserves_rpc_application_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_account_balance.error.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let result = pool.get_token_account_balance(&crate::HttpRoleName::new("default"), &account, std::option::Option::None).await;
|
||||
let error = result.expect_err("remote invalid token account must remain an RPC application error");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_token_accounts_by_delegate_serializes_program_selector_and_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_accounts_by_delegate.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let delegate = fixture_pubkey("11111111111111111111111111111111");
|
||||
let program_id = fixture_pubkey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
||||
let selector = crate::SolanaTokenAccountSelector::ProgramId(program_id);
|
||||
let config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::JsonParsed),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(420_000_000),
|
||||
);
|
||||
let response = pool
|
||||
.get_token_accounts_by_delegate(&crate::HttpRoleName::new("default"), &delegate, &selector, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("delegate token accounts fixture must succeed");
|
||||
assert_eq!(response.context().slot(), 420_000_002);
|
||||
assert_eq!(response.value().len(), 1);
|
||||
assert!(matches!(response.value()[0].account().data(), crate::SolanaAccountData::JsonParsed(_)));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getTokenAccountsByDelegate"));
|
||||
assert_eq!(body["params"][0], serde_json::json!(delegate.to_string()));
|
||||
assert_eq!(body["params"][1], serde_json::json!({"programId":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"}));
|
||||
assert_eq!(body["params"][2], serde_json::json!({"encoding":"jsonParsed","commitment":"finalized","minContextSlot":420000000}));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_token_accounts_by_owner_serializes_mint_selector_and_omits_empty_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_accounts_by_owner.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let owner = fixture_pubkey("11111111111111111111111111111111");
|
||||
let mint = fixture_pubkey("ComputeBudget111111111111111111111111111111");
|
||||
let selector = crate::SolanaTokenAccountSelector::Mint(mint);
|
||||
let config = crate::SolanaAccountInfoConfig::default();
|
||||
let response = pool
|
||||
.get_token_accounts_by_owner(&crate::HttpRoleName::new("default"), &owner, &selector, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("owner token accounts fixture must succeed");
|
||||
assert_eq!(response.context().api_version(), std::option::Option::None);
|
||||
assert_eq!(response.value().len(), 1);
|
||||
assert_eq!(response.value()[0].pubkey().to_string(), "Stake11111111111111111111111111111111111111");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getTokenAccountsByOwner"));
|
||||
assert_eq!(body["params"], serde_json::json!([owner.to_string(),{"mint":"ComputeBudget111111111111111111111111111111"}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_token_accounts_by_owner_rejects_invalid_account_pubkey_response() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let owner = fixture_pubkey("11111111111111111111111111111111");
|
||||
let mint = fixture_pubkey("ComputeBudget111111111111111111111111111111");
|
||||
let selector = crate::SolanaTokenAccountSelector::Mint(mint);
|
||||
let result = pool.get_token_accounts_by_owner(&crate::HttpRoleName::new("default"), &owner, &selector, std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid account pubkey must reject the typed response");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_token_largest_accounts_decodes_order_and_commitment() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_largest_accounts.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let mint = fixture_pubkey("11111111111111111111111111111111");
|
||||
let config = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed));
|
||||
let response = pool
|
||||
.get_token_largest_accounts(&crate::HttpRoleName::new("default"), &mint, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("largest token accounts fixture must succeed");
|
||||
assert_eq!(response.value().len(), 2);
|
||||
assert_eq!(response.value()[0].amount().amount(), "9000");
|
||||
assert_eq!(response.value()[1].amount().amount(), "8000");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([mint.to_string(),{"commitment":"confirmed"}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_token_largest_accounts_rejects_invalid_address() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_largest_accounts.invalid_address.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let mint = fixture_pubkey("11111111111111111111111111111111");
|
||||
let result = pool.get_token_largest_accounts(&crate::HttpRoleName::new("default"), &mint, std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid token-account address must reject the typed response");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_token_supply_preserves_exact_amount_and_omits_absent_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_supply.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let mint = fixture_pubkey("11111111111111111111111111111111");
|
||||
let response = pool
|
||||
.get_token_supply(&crate::HttpRoleName::new("default"), &mint, std::option::Option::None)
|
||||
.await
|
||||
.expect("token supply fixture must succeed");
|
||||
assert_eq!(response.context().slot(), 420_000_007);
|
||||
assert_eq!(response.value().amount(), "1000000000000000000000000");
|
||||
assert_eq!(response.value().ui_amount_string(), "1000000000000000000");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getTokenSupply"));
|
||||
assert_eq!(body["params"], serde_json::json!([mint.to_string()]));
|
||||
}
|
||||
1367
crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
Normal file
1367
crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
Normal file
File diff suppressed because it is too large
Load Diff
224
deltas/0.2.2/pre.001-fix.001.md
Normal file
224
deltas/0.2.2/pre.001-fix.001.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<!-- file: deltas/0.2.2/pre.001-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.001-fix.001` — réaudit Agave `v4.2.1` et complétude des DTOs Cluster
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente :
|
||||
|
||||
```text
|
||||
0.2.2-pre.001
|
||||
workspace.package.version = "0.2.2-pre.1"
|
||||
```
|
||||
|
||||
Le fichier `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` fourni pour correction porte déjà `version: 2`; le présent correctif
|
||||
l'incrémente à `version: 3` conformément à `GEN-FILE-004`.
|
||||
|
||||
Le delta historique :
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.001.md
|
||||
```
|
||||
|
||||
reste inchangé. Conformément à `KSP-REL-003` et `VER-DELTA-007`, l'audit incomplet de `pre.001` n'est pas réécrit silencieusement : ce fix
|
||||
trace explicitement sa correction avant `pre.002`.
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger la source Agave complémentaire utilisée par le plan `0.2.2` et réauditer les contrats wire concernés avant l'implémentation des DTOs.
|
||||
|
||||
`pre.001` avait pris Agave `v3.1.8` comme source complémentaire parce que les liens `Source` actuellement exposés par les pages RPC de
|
||||
`solana.com` pointent encore vers ce snapshot. Ce constat reste utile pour expliquer la documentation publiée, mais il ne suffit pas à qualifier
|
||||
`v3.1.8` de source Agave actuelle.
|
||||
|
||||
Le présent fix recoupe donc les pages Solana avec le tag Agave plus récent :
|
||||
|
||||
```text
|
||||
v4.2.1
|
||||
```
|
||||
|
||||
et utilise directement ses sources primaires pour les structures/configurations RPC qui intéressent `0.2.2`.
|
||||
|
||||
## Sources réauditées
|
||||
|
||||
Documentation RPC :
|
||||
|
||||
```text
|
||||
https://solana.com/docs/rpc/http
|
||||
https://solana.com/docs/rpc/json-structures
|
||||
```
|
||||
|
||||
Source primaire Agave `v4.2.1` :
|
||||
|
||||
```text
|
||||
https://github.com/anza-xyz/agave/blob/v4.2.1/rpc/src/rpc.rs
|
||||
https://github.com/anza-xyz/agave/blob/v4.2.1/rpc-client-types/src/config.rs
|
||||
https://github.com/anza-xyz/agave/blob/v4.2.1/rpc-client-types/src/filter.rs
|
||||
https://github.com/anza-xyz/agave/blob/v4.2.1/rpc-client-types/src/request.rs
|
||||
https://github.com/anza-xyz/agave/blob/v4.2.1/rpc-client-types/src/response.rs
|
||||
```
|
||||
|
||||
Le `Cargo.toml` racine du tag `v4.2.1` déclare lui-même `workspace.package.version = "4.2.1"`.
|
||||
|
||||
## Résultat du réaudit
|
||||
|
||||
### Invariants confirmés
|
||||
|
||||
Le passage de la référence complémentaire `v3.1.8` à `v4.2.1` ne remet pas en cause le périmètre `0.2.2` ni les principales limites déjà
|
||||
retenues :
|
||||
|
||||
```text
|
||||
MAX_MULTIPLE_ACCOUNTS = 100
|
||||
MAX_GET_PROGRAM_ACCOUNT_FILTERS = 4
|
||||
MAX_GET_SLOT_LEADERS = 5000
|
||||
MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY = 5
|
||||
```
|
||||
|
||||
Les filtres Program Accounts restent :
|
||||
|
||||
```text
|
||||
DataSize
|
||||
Memcmp
|
||||
TokenAccountState
|
||||
```
|
||||
|
||||
et `Memcmp` conserve les formes `base58`, `base64` et octets bruts, avec 128 octets décodés maximum et rejet de l'ancien libellé `binary`.
|
||||
|
||||
Les configurations Accounts, Token selector, Leader Schedule et Vote Accounts retenues dans le plan restent également compatibles avec
|
||||
`v4.2.1`.
|
||||
|
||||
### Évolution 1 — `getClusterNodes`
|
||||
|
||||
Agave `v4.2.1` expose dans `RpcContactInfo` le champ supplémentaire :
|
||||
|
||||
```text
|
||||
clientId Option<String>
|
||||
```
|
||||
|
||||
La page RPC Solana courante ne le liste pas encore. KSP doit néanmoins pouvoir accepter et préserver ce champ lorsqu'un noeud/provider le
|
||||
retourne, sans le rendre obligatoire pour les réponses qui ne le contiennent pas.
|
||||
|
||||
Le DTO Cluster prévu par le plan est donc complété avec `clientId: Option<String>`.
|
||||
|
||||
### Évolution 2 — `getVoteAccounts`
|
||||
|
||||
Agave `v4.2.1` expose dans `RpcVoteAccountInfo` :
|
||||
|
||||
```text
|
||||
inflationRewardsCommissionBps Option<u16>
|
||||
```
|
||||
|
||||
La source précise que ce champ est absent/`None` pour les noeuds antérieurs à son introduction. KSP doit le préserver comme optionnel et ne doit
|
||||
pas le déduire artificiellement du champ historique `commission: u8`.
|
||||
|
||||
Le DTO Vote Account prévu par le plan est donc complété avec `inflationRewardsCommissionBps: Option<u16>`.
|
||||
|
||||
### Tests à préserver dans la suite
|
||||
|
||||
La stratégie de tests du plan exige désormais explicitement :
|
||||
|
||||
- `getClusterNodes` avec présence et absence de `clientId` ;
|
||||
- `getVoteAccounts` avec présence et absence de `inflationRewardsCommissionBps` ;
|
||||
- conservation des tests déjà prévus sur les autres champs optionnels/nullables et sur les limites `100 / 4 / 5000 / 5`.
|
||||
|
||||
## Périmètre inchangé
|
||||
|
||||
Le correctif ne modifie pas :
|
||||
|
||||
- les 22 méthodes affectées à `0.2.2` ;
|
||||
- la partition globale `4 / 22 / 11 / 15` ;
|
||||
- le gate de sizing positif de `0.2.2` ;
|
||||
- l'architecture `wrapper -> descriptor -> execute_standard_rpc -> pool -> reqwest -> JSON-RPC -> decode` ;
|
||||
- Config ;
|
||||
- les dépendances Cargo ;
|
||||
- les sources Rust ;
|
||||
- les canaris `0.2.1` ;
|
||||
- le planning `pre.002` à `pre.007`.
|
||||
|
||||
Aucun nouveau besoin `base64`, `bs58`, SPL, `solana-client` ou SDK RPC haut niveau n'est introduit par ce réaudit.
|
||||
|
||||
## Fichier modifié
|
||||
|
||||
```text
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.001-fix.001.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
ROADMAP.md
|
||||
CHANGELOG.md
|
||||
deltas/0.2.2/pre.001.md
|
||||
docs/000-README.md
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md
|
||||
docs/validation/003-V0_2_1_ONCHAIN_HTTP.md
|
||||
crates/ksp-onchain-transport-lib/**
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
Le plan `009` reste le fichier canonique ; aucun fichier parallèle `*.corrected.md` n'est ajouté au dépôt.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Ce correctif est exclusivement documentaire et ne touche aucun fichier consommé par le code, le build, le runtime, la configuration exécutable
|
||||
ou une migration. Conformément à `VER-ID-008`, `workspace.package.version` reste :
|
||||
|
||||
```text
|
||||
0.2.2-pre.1
|
||||
```
|
||||
|
||||
L'identifiant de livraison est :
|
||||
|
||||
```text
|
||||
0.2.2-pre.001-fix.001
|
||||
```
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- relecture de `RULES.md`, `docs/rules/VERSION_WORKFLOW.md`, `docs/rules/FILE_CONTRACTS.md`, `docs/rules/RULES_GENERAL.md`,
|
||||
`docs/rules/RULES_KSP.md` et `docs/rules/PROMPT_STRUCTURE.md` ;
|
||||
- contrôle des règles `GEN-FILE-004`, `VER-ID-003`, `VER-ID-008`, `VER-DELTA-003`, `VER-DELTA-007`, `VER-ARCHIVE-002`,
|
||||
`VER-ARCHIVE-004` et `KSP-REL-003` applicables à ce correctif ;
|
||||
- comparaison de la source Agave `v3.1.8` utilisée par `pre.001` avec Agave `v4.2.1` sur les structures/configs RPC nécessaires à
|
||||
Accounts/Tokens/Cluster ;
|
||||
- confirmation dans Agave `v4.2.1` des limites `100 / 4 / 5000 / 5` et des variantes Program Account filters ;
|
||||
- confirmation dans Agave `v4.2.1` de `RpcContactInfo.client_id: Option<String>` ;
|
||||
- confirmation dans Agave `v4.2.1` de `RpcVoteAccountInfo.inflation_rewards_commission_bps: Option<u16>` ;
|
||||
- contrôle que le plan canonique référence désormais `v4.2.1` pour les cinq sources Agave complémentaires ;
|
||||
- contrôle que les seules mentions restantes de `v3.1.8` expliquent le snapshot encore lié par le site Solana ;
|
||||
- contrôle que `clientId` et `inflationRewardsCommissionBps` sont présents dans les DTOs prévus et dans la stratégie de tests ;
|
||||
- contrôle qu'aucune autre famille de fichier n'est modifiée par ce fix.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Aucune validation Cargo n'est requise spécifiquement pour ce fix documentaire : aucune source Rust, dépendance, feature, configuration runtime ou
|
||||
manifest Cargo n'est modifié.
|
||||
|
||||
Les validations Rust prévues pour `0.2.2-pre.002` restent inchangées et devront être réellement exécutées après ses modifications de code.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante nouvelle.
|
||||
|
||||
Le plan garde comme règle de mise en oeuvre que les champs optionnels/versionnés observés dans Agave doivent être représentés sans rendre leur
|
||||
présence obligatoire chez tous les providers.
|
||||
|
||||
## Suite
|
||||
|
||||
Après application/commit de `0.2.2-pre.001-fix.001`, reprendre `0.2.2-pre.002` sur le plan `009` corrigé, en incluant dès la conception des DTOs
|
||||
Cluster/Vote les deux champs optionnels confirmés par Agave `v4.2.1`.
|
||||
180
deltas/0.2.2/pre.001.md
Normal file
180
deltas/0.2.2/pre.001.md
Normal file
@@ -0,0 +1,180 @@
|
||||
<!-- file: deltas/0.2.2/pre.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.001` — réaudit HTTP Accounts/Tokens/Cluster, DTOs et sizing
|
||||
|
||||
## Base requise
|
||||
|
||||
Release stable attendue :
|
||||
|
||||
```text
|
||||
v0.2.1
|
||||
```
|
||||
|
||||
L'archive KSP fournie porte `workspace.package.version = "0.2.1"`. Elle ne contient pas `.git`; le tag `v0.2.1` n'est donc pas revérifiable
|
||||
localement depuis le zip.
|
||||
|
||||
## Objectif
|
||||
|
||||
Exécuter la première tranche obligatoire de `0.2.2` sans implémentation fonctionnelle lourde : relire les contrats KSP, réauditer la surface
|
||||
HTTP Solana officielle actuelle, confirmer la matrice des 22 méthodes Accounts/Tokens/Cluster, cadrer les DTOs wire, évaluer les dépendances et
|
||||
trancher le gate de sizing.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Conformément à `VER-ID-009`, la prerelease non-fix synchronise le signal technique :
|
||||
|
||||
```text
|
||||
0.2.1 -> 0.2.2-pre.1
|
||||
```
|
||||
|
||||
Aucune source Rust, dépendance Cargo ou configuration runtime n'est modifiée dans cette tranche.
|
||||
|
||||
## Résultats principaux
|
||||
|
||||
- index HTTP Solana courant réaudité : **52 méthodes** ;
|
||||
- navigation Deprecated officielle : **14 méthodes historiques**, inchangées ;
|
||||
- familles de la release : Accounts `6 = 1 acquis + 5 ciblés`, Tokens `5 = 5 ciblés`, Cluster `15 = 3 acquis + 12 ciblés` ;
|
||||
- aucune méthode ajoutée/supprimée/déplacée détectée depuis l'audit `0.2.1` du 2026-08-17 ;
|
||||
- les 22 méthodes ciblées restent des méthodes courantes non marquées Deprecated/Unstable et les descriptors KSP restent `Read + RetrySafe` ;
|
||||
- `getMultipleAccounts` conserve la limite documentée de 100 adresses ;
|
||||
- `getSlotLeaders` conserve la limite documentée `1..=5000` ;
|
||||
- `getLargestAccounts` et `getProgramAccounts` documentent toujours `sortResults` ;
|
||||
- `getProgramAccounts` conserve sa réponse bare par défaut et contextualisée avec `withContext=true` ;
|
||||
- la source primaire Agave confirme trois filtres Program Accounts (`dataSize`, `memcmp`, `tokenAccountState`), 4 filtres max et 128 octets décodés max pour `memcmp` ;
|
||||
- le wire Account requiert une union pour `data`, y compris le fallback `jsonParsed -> [base64]`, et `space` reste nullable ;
|
||||
- `TokenAmount.uiAmount` reste nullable ;
|
||||
- `getHighestSnapshotSlot` conserve `incremental: null` et la source officielle Agave confirme une erreur `NoSnapshot` en absence de snapshot ;
|
||||
- `getLeaderSchedule` conserve son overload slot/config particulier ;
|
||||
- `getVoteAccounts` conserve `current/delinquent` et les epoch credits triples; la source Agave courante borne cet historique RPC à 5 entrées par validator.
|
||||
|
||||
## Gate de sizing
|
||||
|
||||
Question obligatoire :
|
||||
|
||||
```text
|
||||
Les 22 wrappers typés + DTOs partagés + tests + documentation peuvent-ils être clôturés proprement dans cette session ?
|
||||
```
|
||||
|
||||
Réponse :
|
||||
|
||||
```text
|
||||
OUI.
|
||||
```
|
||||
|
||||
Aucun split de release n'est nécessaire. Le plan conserve `0.2.2` à 22 méthodes et répartit le travail sur DTOs communs, Accounts, Tokens,
|
||||
Cluster simple, Cluster schedule/slot/vote puis clôture. Si une tranche réelle dépasse le budget KSP, elle sera scindée par une prerelease
|
||||
supplémentaire sans déplacer de méthode hors `0.2.2`.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- conserver exactement la partition `4 / 22 / 11 / 15` de `0.2.1`–`0.2.4` ;
|
||||
- ne pas reconstruire un transport par famille ; tous les wrappers passent par descriptor + `execute_standard_rpc` ;
|
||||
- mutualiser `SolanaCommitment` et `SolanaRpcContext` sans casser les réexports `0.2.1` ;
|
||||
- représenter Account data comme une forme wire discriminée, sans décodage Program/SPL ;
|
||||
- conserver `jsonParsed.parsed` comme `serde_json::Value` dans Transport ;
|
||||
- utiliser `ksp_core_lib::Pubkey` dans les DTOs publics, avec wire structs privés puis conversion explicite ;
|
||||
- représenter le selector Token comme une union exclusive `mint` / `programId` ;
|
||||
- représenter le résultat `getProgramAccounts` comme bare ou contextualisé selon `withContext` ;
|
||||
- conserver les endpoints de `getClusterNodes` comme chaînes wire optionnelles au contrat public ;
|
||||
- conserver les `null` documentés (`Account`, `space`, `uiAmount`, `transactionCount`, snapshot incremental, leader schedule) ;
|
||||
- ne pas modifier Config : le contrat `std.transport` existant suffit ;
|
||||
- ne pas ajouter `base64`, `bs58`, SPL, `solana-client` ou SDK RPC haut niveau à `pre.001`; les formes encodées `memcmp` peuvent rester des chaînes wire tant qu’aucun décodage local n’est requis.
|
||||
|
||||
## Prévision de travail
|
||||
|
||||
```text
|
||||
pre.001 audit officiel + matrice + DTOs + dépendances + sizing
|
||||
pre.002 primitives/configs/results partagés + fixtures de base
|
||||
pre.003 5 Accounts + tests
|
||||
pre.004 5 Tokens + tests
|
||||
pre.005 Cluster simple (7 méthodes) + tests
|
||||
pre.006 Cluster schedule/slot/vote (5 méthodes) + tests
|
||||
pre.007 canaries + smoke opt-in si utile + docs + prompt 0.2.3 + préparation stable
|
||||
```
|
||||
|
||||
Une prerelease supplémentaire reste autorisée si le budget réel d'une tranche l'exige.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
deltas/0.2.2/pre.001.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
ROADMAP.md
|
||||
docs/000-README.md
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md
|
||||
docs/validation/003-V0_2_1_ONCHAIN_HTTP.md
|
||||
crates/ksp-onchain-transport-lib/src/**
|
||||
crates/ksp-onchain-transport-lib/unit_tests/**
|
||||
crates/ksp-onchain-transport-lib/tests/**
|
||||
crates/ksp-config-lib/src/transport.rs
|
||||
config/std.transport.json
|
||||
config/schemas/std.transport.schema.json
|
||||
```
|
||||
|
||||
Les deltas `0.2.1` restent historiques et ne sont pas réécrits.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- relecture des sources KSP obligatoires du prompt `0.2.2` ;
|
||||
- inspection de l'archive stable KSP fournie et de la foundation Transport `0.2.1` ;
|
||||
- confirmation locale que le registry assigne déjà exactement les 22 méthodes ciblées à `HttpRpcCoverageRelease::V0_2_2` ;
|
||||
- confirmation locale que ces 22 descriptors sont `Stable / Supported / Stable`, `Read`, `RetrySafe` ;
|
||||
- consultation de l'index HTTP Solana officiel courant et des 22 pages de méthodes ciblées ;
|
||||
- consultation de la navigation Deprecated officielle courante ;
|
||||
- consultation de `Solana RPC JSON Structures` pour Account data et TokenAmount ;
|
||||
- consultation des sources Agave `v3.1.8` liées par les pages officielles pour les ambiguïtés Account, filters/memcmp, cardinalités RPC, largest accounts, snapshot, leader schedule,
|
||||
cluster node et vote accounts ;
|
||||
- contrôle documentaire de la matrice `52 current / 14 historical / 4+22+11+15` ;
|
||||
- contrôle de l'absence de nouvelle dépendance ou de besoin Config dans le design de `pre.001`.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Le sandbox courant ne fournit pas le binaire `cargo` (`command -v cargo` ne retourne aucun chemin). Les validations Cargo suivantes n'ont donc pas
|
||||
été exécutées ici et doivent être rejouées sur le checkout de développement avant commit :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Aucun `cargo tree` n'est nécessaire dans cette tranche car aucune dépendance ni feature n'a changé. Les quatre vues `cargo tree` du prompt restent
|
||||
obligatoires dès qu'une prerelease change une dépendance/feature et à la clôture.
|
||||
|
||||
L'archive KSP fournie ne contient pas `.git`; le commit attendu après application et validations suit `VER-GIT-001` :
|
||||
|
||||
```text
|
||||
v0.2.2-pre.001
|
||||
```
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante pour `pre.002`.
|
||||
|
||||
Les détails exacts de nommage Rust des DTOs restent ajustables pendant leur implémentation, mais les invariants wire et frontières fixés par le
|
||||
plan `009` ne doivent pas être relâchés pour simplifier artificiellement les wrappers.
|
||||
|
||||
## Suite
|
||||
|
||||
`0.2.2-pre.002` : introduire les primitives/configs/results partagés, mutualiser Context/Commitment, fixer les conversions wire et installer les
|
||||
fixtures déterministes communes avant les wrappers Accounts.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user