Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fff69ead9c | |||
| c93a833bac | |||
| 05e0e96083 | |||
| b57f796187 | |||
| ecfb9500eb | |||
| e14ce1c36f | |||
| 83cb861e54 | |||
| 1d1bc6a4d6 | |||
| c4636ac8b9 | |||
| f15448ff6e | |||
| bec1f2ec08 | |||
| f625ee5979 | |||
| e68f073505 | |||
| bffb4f9a31 | |||
| 9059a2dc45 |
@@ -1,10 +1,14 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# 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.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: 119
|
||||
|
||||
[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.2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 34 -->
|
||||
<!-- version: 37 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -46,7 +46,7 @@ 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.
|
||||
- [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.
|
||||
- [ ] `0.2.3` — Compléter les 11 méthodes HTTP Transactions, y compris write/submission technique avec politique no-resend ambigu.
|
||||
- [ ] `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`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -69,16 +69,16 @@ Le registre porte notamment :
|
||||
- remplacement historique éventuel ;
|
||||
- release de couverture typée KSP.
|
||||
|
||||
Les quatre wrappers typés de la foundation sont :
|
||||
La surface typée stable après `0.2.2` contient **26 méthodes** :
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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 affectés à `HttpRpcCoverageRelease::V0_2_2`. Les 26 méthodes Transactions/Blocks/Economics encore affectées à `0.2.3`–`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**.
|
||||
|
||||
Les 14 méthodes historiques restent découvrables pour la compliance mais sont `Removed` et ne sont pas simulées comme appelables.
|
||||
|
||||
@@ -118,18 +118,24 @@ 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
|
||||
-> getAccountInfo/getTokenAccountsByOwner/getEpochInfo/getVoteAccounts
|
||||
|
||||
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` ;
|
||||
- [`../../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: 5 -->
|
||||
|
||||
# Utilisation de `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -86,17 +86,17 @@ 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 surface stable `0.2.2` contient 26 wrappers typés au total : 4 foundation + 5 Accounts + 5 Tokens + 12 Cluster. Les DTOs Transport conservent les `null`, options et formes wire : données Account encodées/`jsonParsed`, `TokenAmount.uiAmount`, contexte RPC, nodes, epoch, leader schedule et vote accounts. Aucun décodage Program/SPL métier n'est effectué ici.
|
||||
|
||||
## 4. Exécution JSON-RPC standard générique
|
||||
|
||||
@@ -153,12 +153,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` :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Il appelle `getAccountInfo`, `getTokenAccountsByOwner`, `getEpochInfo` et `getVoteAccounts`. 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 : le smoke valide ainsi la route Token sans dépendre de la persistance d'un mint ou d'un token account Devnet particulier.
|
||||
|
||||
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 @@
|
||||
{"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":-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":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":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":{"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 @@
|
||||
{"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: 11
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -9,8 +9,8 @@
|
||||
//! 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 while the `0.2.3` and `0.2.4` audited families remain staged.
|
||||
|
||||
mod client;
|
||||
mod constants;
|
||||
@@ -19,8 +19,12 @@ 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 settings;
|
||||
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
@@ -41,6 +45,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 +93,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 +191,12 @@ 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;
|
||||
/// 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;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 5
|
||||
// version: 9
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -171,3 +171,75 @@ 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)));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 1
|
||||
// version: 6
|
||||
|
||||
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
||||
|
||||
@@ -46,3 +46,151 @@ 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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
|
||||
// version: 3
|
||||
|
||||
//! Opt-in live Devnet smoke for the pure Transport Accounts/Tokens/Cluster path.
|
||||
|
||||
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_and_cluster_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());
|
||||
}
|
||||
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()]));
|
||||
}
|
||||
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.
|
||||
213
deltas/0.2.2/pre.002-fix.001.md
Normal file
213
deltas/0.2.2/pre.002-fix.001.md
Normal file
@@ -0,0 +1,213 @@
|
||||
<!-- file: deltas/0.2.2/pre.002-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.002-fix.001` — nettoyage des helpers RPC préparatoires et Clippy
|
||||
|
||||
## Base requise
|
||||
|
||||
```text
|
||||
livraison : 0.2.2-pre.002
|
||||
workspace.package.version = "0.2.2-pre.2"
|
||||
```
|
||||
|
||||
Le correctif est ouvert après la validation locale communiquée le 2026-08-18. Les tests de `pre.002` passent, mais `cargo check` et Clippy
|
||||
révèlent que plusieurs helpers privés de sérialisation/décodage ont été compilés trop tôt alors qu'aucun wrapper `0.2.2` ne les consomme encore.
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger exclusivement les warnings de qualité de `pre.002`, sans commencer les wrappers Accounts de `pre.003`, sans modifier les DTOs publics
|
||||
et sans masquer les lints avec `#[allow(dead_code)]`.
|
||||
|
||||
Les catégories observées sont :
|
||||
|
||||
```text
|
||||
dead_code helpers/wire structs préparatoires Accounts/Tokens/Cluster/Common
|
||||
clippy::assertions_on_constants assert!(false, ...) dans un test Account
|
||||
clippy::wrong_self_convention 2 serializers de configs Copy prenant &self
|
||||
```
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Le correctif modifie du Rust de production/test. Conformément à `VER-ID-007` et `VER-ID-010` :
|
||||
|
||||
```text
|
||||
0.2.2-pre.2 -> 0.2.2-pre.2.fix.1
|
||||
```
|
||||
|
||||
Aucune dépendance ni feature Cargo n'est ajoutée ou retirée.
|
||||
|
||||
## Correction du staging privé
|
||||
|
||||
`pre.002` doit établir les DTOs/configs publics et les fixtures communes, mais les wrappers qui consommeront réellement les helpers privés sont
|
||||
répartis sur `pre.003` à `pre.006`.
|
||||
|
||||
Les serializers/décodeurs privés et leurs structs wire encore utilisés uniquement par les tests sont donc compilés sous :
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
```
|
||||
|
||||
jusqu'à la prerelease qui les active réellement dans un wrapper de production.
|
||||
|
||||
Cette correction :
|
||||
|
||||
- conserve tous les types, constructeurs, getters, enums et réexports publics introduits par `pre.002` ;
|
||||
- conserve les fixtures Account/Token/Cluster/Vote ;
|
||||
- ne rend aucun helper privé artificiellement public ;
|
||||
- n'ajoute aucun `#[allow(dead_code)]` ;
|
||||
- évite de porter du code privé mort dans la bibliothèque normale entre deux tranches ;
|
||||
- impose aux prereleases suivantes de retirer `#[cfg(test)]` uniquement sur les helpers qu'elles commencent effectivement à consommer.
|
||||
|
||||
Les deux helpers `is_empty()` qui n'ont aucun consommateur ni test utile à ce stade sont supprimés et seront réintroduits seulement si un wrapper
|
||||
les nécessite réellement.
|
||||
|
||||
## Correction Clippy
|
||||
|
||||
### Configs `Copy`
|
||||
|
||||
Les serializers internes de :
|
||||
|
||||
```text
|
||||
SolanaCommitmentConfig
|
||||
SolanaContextConfig
|
||||
```
|
||||
|
||||
prennent désormais `self` par valeur, ce qui correspond à leur nature `Copy` et supprime `clippy::wrong_self_convention` sans modifier la forme JSON.
|
||||
|
||||
`SolanaContextConfig::to_json_value()` reste production-live parce que le canari `getBalance` de `0.2.1` le consomme déjà.
|
||||
|
||||
### Assertion constante
|
||||
|
||||
Le test `account_wire_fixture_preserves_legacy_encoded_and_json_parsed_data` remplace le `match` avec `assert!(false, ...)` par :
|
||||
|
||||
1. une assertion `matches!` sur `JsonParsed` ;
|
||||
2. un `if let` pour vérifier le contenu.
|
||||
|
||||
Aucun `panic!()` ou `unreachable!()` explicite n'est introduit.
|
||||
|
||||
## Complément de tests préparatoires
|
||||
|
||||
Des tests unitaires sont ajoutés pour exercer les helpers privés désormais explicitement test-only qui n'étaient pas couverts par `pre.002` :
|
||||
|
||||
- sérialisation `SolanaLargestAccountsConfig` ;
|
||||
- décodage `SolanaKeyedAccount` et `SolanaAccountBalance` ;
|
||||
- décodage `SolanaTokenAccountBalance` ;
|
||||
- décodage `SolanaEpochInfo`, `SolanaEpochSchedule`, `SolanaSnapshotSlotInfo` et `SolanaLeaderSchedule` ;
|
||||
- sérialisation `SolanaVoteAccountsConfig` ;
|
||||
- décodage `SolanaVoteAccountStatus` ;
|
||||
- construction test-only de `SolanaRpcResponse<T>`.
|
||||
|
||||
Les fixtures et invariants `clientId`, `inflationRewardsCommissionBps`, `uiAmount: null`, `space: null` et Account data restent couverts.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.002-fix.001.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.002.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
ROADMAP.md
|
||||
CHANGELOG.md
|
||||
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_method.rs
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
Le delta `pre.002.md` reste une trace historique et n'est pas réécrit.
|
||||
|
||||
## Validation de `pre.002` ayant déclenché le correctif
|
||||
|
||||
Les commandes suivantes ont été exécutées par l'utilisateur sur `0.2.2-pre.002` avant création de ce fix :
|
||||
|
||||
```text
|
||||
cargo fmt --all : exécuté
|
||||
cargo check --workspace : OK, avec 42 warnings dead_code Transport
|
||||
cargo clippy --workspace --all-targets : OK, avec les warnings dead_code + assertions_on_constants + wrong_self_convention
|
||||
cargo test -p ksp-onchain-transport-lib : OK
|
||||
```
|
||||
|
||||
Résultats de tests communiqués :
|
||||
|
||||
```text
|
||||
81 tests unitaires : passés
|
||||
9 tests public_api : passés
|
||||
2 tests release_completeness : passés
|
||||
0 échec
|
||||
```
|
||||
|
||||
Ces résultats prouvent la fonctionnalité de la base `pre.002`, mais ne constituent pas une validation du présent fix.
|
||||
|
||||
## Contrôles statiques effectués pendant la préparation du fix
|
||||
|
||||
- aucune occurrence `#[allow(dead_code)]` ajoutée ;
|
||||
- aucune occurrence `assert!(false` restante dans les fichiers modifiés ;
|
||||
- les deux serializers de configs `Copy` prennent `self` par valeur ;
|
||||
- les helpers privés préparatoires non consommés par la bibliothèque normale sont explicitement `#[cfg(test)]` ;
|
||||
- les réexports crate-private de support wire sont eux aussi test-only tant qu'aucun wrapper ne les consomme ;
|
||||
- les helpers test-only identifiés disposent d'un consommateur dans les tests unitaires ;
|
||||
- aucune méthode RPC `0.2.2` n'est ajoutée à la surface typed-complete ;
|
||||
- aucune dépendance/feature/configuration Transport n'est modifiée ;
|
||||
- headers/version des fichiers modifiés incrémentés ;
|
||||
- `pre.002.md` et le plan `009` restent inchangés.
|
||||
|
||||
## Validations du correctif non exécutées dans le sandbox
|
||||
|
||||
Le sandbox de préparation ne fournit pas `cargo`, `rustc` ni `rustfmt`. Les validations Rust du fix ne sont donc pas déclarées réussies.
|
||||
|
||||
Après application, exécuter au minimum :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Conformément aux règles Rust KSP, `cargo test --workspace` reste requis au checkpoint de clôture approprié. Aucun `cargo tree` supplémentaire n'est
|
||||
requis pour ce fix puisqu'aucune dépendance ni feature n'a changé.
|
||||
|
||||
Résultat attendu immédiatement pour les commandes ci-dessus : aucune régression des canaris `0.2.1`, aucun warning ayant motivé ce fix et quatre
|
||||
tests unitaires Transport supplémentaires par rapport à `pre.002`.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- corriger la cause des warnings plutôt que les masquer avec un lint allowance ;
|
||||
- conserver les DTOs et fixtures publics de `pre.002` ;
|
||||
- considérer les serializers/décodeurs privés non encore consommés comme support de test jusqu'à leur activation réelle ;
|
||||
- ne pas rendre `serde_json::Value` public uniquement pour éviter `dead_code` ;
|
||||
- ne pas commencer les cinq wrappers Accounts dans ce fix ;
|
||||
- conserver `pre.003` comme prochaine tranche fonctionnelle après validation du correctif.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation et commit de `0.2.2-pre.002-fix.001`, poursuivre avec `0.2.2-pre.003` pour les cinq wrappers Accounts. Les helpers Account
|
||||
nécessaires seront alors sortis de `#[cfg(test)]` au fur et à mesure de leur consommation réelle.
|
||||
146
deltas/0.2.2/pre.002-fix.002.md
Normal file
146
deltas/0.2.2/pre.002-fix.002.md
Normal file
@@ -0,0 +1,146 @@
|
||||
<!-- file: deltas/0.2.2/pre.002-fix.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.002-fix.002` — restauration du décodeur de contexte utilisé par `getBalance`
|
||||
|
||||
## Base requise
|
||||
|
||||
```text
|
||||
livraison : 0.2.2-pre.002-fix.001
|
||||
workspace.package.version = "0.2.2-pre.2.fix.1"
|
||||
```
|
||||
|
||||
Le correctif est ouvert après validation locale communiquée le 2026-08-18. `pre.002-fix.001` a correctement réduit les warnings préparatoires,
|
||||
mais a classé à tort `SolanaRpcContext::decode_wire` comme helper exclusivement test alors que le canari de production `getBalance`, acquis en
|
||||
`0.2.1`, l'utilise déjà dans `rpc_canary.rs`.
|
||||
|
||||
La conséquence est une régression de compilation `E0599` sur `cargo check`, `cargo clippy` et `cargo test`.
|
||||
|
||||
## Objectif
|
||||
|
||||
Restaurer uniquement le chemin de décodage de contexte déjà nécessaire au runtime du canari `getBalance`, sans annuler le nettoyage ciblé de
|
||||
`pre.002-fix.001`, sans commencer les wrappers Accounts de `pre.003` et sans élargir l'API publique.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Le correctif modifie du Rust de production. Conformément à `VER-ID-007` et `VER-ID-010` :
|
||||
|
||||
```text
|
||||
0.2.2-pre.2.fix.1 -> 0.2.2-pre.2.fix.2
|
||||
```
|
||||
|
||||
Aucune dépendance ni feature Cargo n'est ajoutée ou retirée.
|
||||
|
||||
## Correction
|
||||
|
||||
Les trois éléments nécessaires au chemin production `rpc_canary::get_balance` redeviennent compilés hors `#[cfg(test)]` :
|
||||
|
||||
```text
|
||||
SolanaRpcContext::decode_wire
|
||||
rpc_common::decode_wire_json
|
||||
WireRpcContext
|
||||
```
|
||||
|
||||
`SolanaRpcContext::decode_wire` appelle désormais directement le helper privé de son module au lieu de dépendre du réexport crate-private
|
||||
`crate::decode_wire_json`, qui reste volontairement réservé aux tests dans `lib.rs` tant qu'aucun autre module de production ne le consomme.
|
||||
|
||||
Tous les autres serializers/décodeurs préparatoires rendus test-only par `pre.002-fix.001` restent inchangés. Ce fix ne réintroduit donc pas les
|
||||
warnings `dead_code` que `fix.001` cherchait à supprimer.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.002-fix.002.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.002.md
|
||||
deltas/0.2.2/pre.002-fix.001.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
ROADMAP.md
|
||||
CHANGELOG.md
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_canary.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/**
|
||||
crates/ksp-onchain-transport-lib/tests/**
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
Les deltas déjà publiés restent des traces historiques et ne sont pas réécrits.
|
||||
|
||||
## Validation ayant déclenché le correctif
|
||||
|
||||
Les commandes suivantes ont été exécutées par l'utilisateur sur `0.2.2-pre.002-fix.001` :
|
||||
|
||||
```text
|
||||
cargo fmt --all : exécuté
|
||||
cargo check --workspace : ÉCHEC E0599
|
||||
cargo clippy --workspace --all-targets : ÉCHEC E0599
|
||||
cargo test -p ksp-onchain-transport-lib : ÉCHEC E0599
|
||||
```
|
||||
|
||||
Erreur commune :
|
||||
|
||||
```text
|
||||
no function or associated item named `decode_wire` found for struct `SolanaRpcContext`
|
||||
rpc_canary.rs:194:48
|
||||
```
|
||||
|
||||
## Contrôles statiques effectués pendant la préparation du fix
|
||||
|
||||
- `SolanaRpcContext::decode_wire` n'est plus conditionné par `#[cfg(test)]` ;
|
||||
- `decode_wire_json` et `WireRpcContext`, ses dépendances directes, sont également production-live ;
|
||||
- l'appel interne n'utilise plus le réexport test-only `crate::decode_wire_json` ;
|
||||
- aucun autre `#[cfg(test)]` introduit par `pre.002-fix.001` n'est retiré ;
|
||||
- aucun `#[allow(dead_code)]` n'est ajouté ;
|
||||
- aucune API publique, méthode RPC, dépendance, feature ou configuration n'est modifiée ;
|
||||
- `workspace.package.version` vaut `0.2.2-pre.2.fix.2` ;
|
||||
- les deltas `pre.002.md` et `pre.002-fix.001.md` restent inchangés.
|
||||
|
||||
## Validations du correctif non exécutées dans le sandbox
|
||||
|
||||
Le sandbox de préparation ne fournit pas `cargo`, `rustc` ni `rustfmt`. Les validations Rust du présent fix ne sont donc pas déclarées réussies.
|
||||
|
||||
Après application, exécuter :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Le résultat attendu est le retour à une compilation réussie du canari `getBalance`, sans réapparition des warnings ciblés par `pre.002-fix.001`.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- traiter `SolanaRpcContext::decode_wire` comme code de production parce qu'un consommateur runtime existe déjà ;
|
||||
- conserver test-only uniquement les helpers qui n'ont réellement aucun consommateur production à cette étape ;
|
||||
- ne pas modifier `rpc_canary.rs`, dont l'utilisation du décodeur est correcte ;
|
||||
- ne pas élargir `lib.rs` avec un nouveau réexport crate-private production inutile ;
|
||||
- ne pas commencer `pre.003` avant validation de ce second fix.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation et commit de `0.2.2-pre.002-fix.002`, poursuivre avec `0.2.2-pre.003` pour les cinq wrappers Accounts.
|
||||
227
deltas/0.2.2/pre.002.md
Normal file
227
deltas/0.2.2/pre.002.md
Normal file
@@ -0,0 +1,227 @@
|
||||
<!-- file: deltas/0.2.2/pre.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.002` — primitives RPC Accounts/Tokens/Cluster et wire commun
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente corrigée :
|
||||
|
||||
```text
|
||||
0.2.2-pre.001-fix.001
|
||||
workspace.package.version = "0.2.2-pre.1"
|
||||
```
|
||||
|
||||
Le plan canonique attendu est `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` version 3, qui inclut le réaudit Agave `v4.2.1`.
|
||||
|
||||
## Objectif
|
||||
|
||||
Installer les primitives/configurations/résultats partagés nécessaires aux 22 wrappers `0.2.2`, sans encore implémenter les cinq wrappers
|
||||
Accounts prévus pour `pre.003`. La tranche mutualise les contrats nés avec les canaris `0.2.1`, fixe les formes wire Account/Token/Cluster et
|
||||
ajoute des fixtures déterministes pour les variantes nullable/versionnées identifiées pendant `pre.001` et son fix.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Conformément à `VER-ID-009`, la nouvelle prerelease synchronise le signal technique :
|
||||
|
||||
```text
|
||||
0.2.2-pre.1 -> 0.2.2-pre.2
|
||||
```
|
||||
|
||||
Aucune dépendance ou feature Cargo n'est ajoutée.
|
||||
|
||||
## Implémentation
|
||||
|
||||
### RPC commun
|
||||
|
||||
Un nouveau module commun possède désormais :
|
||||
|
||||
```text
|
||||
SolanaCommitment
|
||||
SolanaCommitmentConfig
|
||||
SolanaContextConfig
|
||||
SolanaRpcContext
|
||||
SolanaRpcResponse<T>
|
||||
```
|
||||
|
||||
`SolanaCommitment` et `SolanaRpcContext` ont été déplacés hors du module canari sans casser leurs réexports crate-root. `GetBalanceConfig`
|
||||
conserve son nom, ses champs internes, son constructeur, ses getters et sa forme `Debug` `0.2.1`; seule sa sérialisation réutilise désormais
|
||||
`SolanaContextConfig`.
|
||||
|
||||
Les helpers internes de décodage :
|
||||
|
||||
- convertissent les erreurs serde vers `ERROR_CODE_INVALID_RESPONSE` ;
|
||||
- convertissent explicitement les chaînes de public key vers `ksp_core_lib::Pubkey` ;
|
||||
- n'incluent pas la valeur de public key invalide dans les diagnostics.
|
||||
|
||||
### Accounts
|
||||
|
||||
Les primitives suivantes sont ajoutées :
|
||||
|
||||
```text
|
||||
SolanaAccountEncoding
|
||||
SolanaDataSliceConfig
|
||||
SolanaAccountInfoConfig
|
||||
SolanaLargestAccountsFilter
|
||||
SolanaLargestAccountsConfig
|
||||
SolanaMemcmpBytes
|
||||
SolanaMemcmpFilter
|
||||
SolanaProgramAccountFilter
|
||||
SolanaProgramAccountsConfig
|
||||
SolanaParsedAccountData
|
||||
SolanaAccountData
|
||||
SolanaAccount
|
||||
SolanaKeyedAccount
|
||||
SolanaAccountBalance
|
||||
SolanaProgramAccountsResult
|
||||
```
|
||||
|
||||
`SolanaAccountData` préserve les trois familles de wire : chaîne legacy, tuple encodé et `jsonParsed`. Le contenu `parsed` reste un
|
||||
`serde_json::Value`; aucun décodage Program/SPL n'entre dans Transport. `space` reste `Option<u64>`.
|
||||
|
||||
`SolanaProgramAccountFilter` conserve `dataSize`, `memcmp` et `tokenAccountState`. Les bytes `memcmp` peuvent rester base58/base64 textuels ou
|
||||
raw sans ajouter `bs58`/`base64`; les limites de cardinalité et de taille seront appliquées par les wrappers/validation `pre.003` conformément au
|
||||
plan.
|
||||
|
||||
### Tokens
|
||||
|
||||
Les primitives suivantes sont ajoutées :
|
||||
|
||||
```text
|
||||
SolanaTokenAccountSelector
|
||||
SolanaTokenAmount
|
||||
SolanaTokenAccountBalance
|
||||
```
|
||||
|
||||
Le selector est un enum `Mint | ProgramId`, ce qui interdit par construction un objet contenant simultanément les deux clés. `uiAmount` reste
|
||||
`Option<f64>` et `amount`/`uiAmountString` restent des chaînes exactes du wire.
|
||||
|
||||
### Cluster
|
||||
|
||||
Les DTOs/configs partagés suivants sont ajoutés :
|
||||
|
||||
```text
|
||||
SolanaClusterNode
|
||||
SolanaEpochInfo
|
||||
SolanaEpochSchedule
|
||||
SolanaSnapshotSlotInfo
|
||||
SolanaLeaderScheduleConfig
|
||||
SolanaLeaderScheduleRequest
|
||||
SolanaLeaderSchedule
|
||||
SolanaVoteAccountsConfig
|
||||
SolanaEpochCredits
|
||||
SolanaVoteAccountInfo
|
||||
SolanaVoteAccountStatus
|
||||
```
|
||||
|
||||
Le DTO node conserve tous les endpoints comme chaînes optionnelles et inclut `clientId: Option<String>` confirmé dans Agave `v4.2.1`.
|
||||
`SolanaVoteAccountInfo` inclut `inflationRewardsCommissionBps: Option<u16>` sans le dériver de `commission`, ainsi que les triples epoch credits
|
||||
sous un petit DTO nommé. `SolanaLeaderScheduleRequest` encode explicitement les overloads current-epoch/config et slot/config.
|
||||
|
||||
## Fixtures déterministes ajoutées
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/account_data.variants.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/token_amount.null_ui.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/cluster_node.v4_2_1.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/vote_account.v4_2_1.json
|
||||
```
|
||||
|
||||
Elles couvrent notamment legacy/encoded/jsonParsed Account data, `space: null`, `uiAmount: null`, `clientId`, champs Cluster absents et présence
|
||||
de `inflationRewardsCommissionBps`. Un test supplémentaire vérifie l'absence de ce dernier champ pour une forme de noeud plus ancienne.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/account_data.variants.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/token_amount.null_ui.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/cluster_node.v4_2_1.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/vote_account.v4_2_1.json
|
||||
deltas/0.2.2/pre.002.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_canary.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_canary.rs
|
||||
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/executor.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_method.rs
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
Le plan ne change pas : `pre.003` reste consacré aux cinq wrappers Accounts.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- reconstruction locale de l'état `v0.2.1 + pre.001 + pre.001-fix.001` à partir des archives fournies ;
|
||||
- relecture de `RULES_RUST.md`, `FILE_CONTRACTS.md`, `VERSION_WORKFLOW.md` et du plan `009` version 3 ;
|
||||
- recoupement des structures account/token Agave `v4.2.1` (`UiAccount`, `UiAccountData`, `UiAccountEncoding`, `UiTokenAmount`) ;
|
||||
- recoupement des extensions Cluster/Vote Agave `v4.2.1` (`client_id`, `inflation_rewards_commission_bps`) ;
|
||||
- contrôle statique des réexports crate-root et de l'absence de nouvelle dépendance Cargo ;
|
||||
- contrôle statique de l'absence de wrapper `0.2.2` ajouté prématurément dans cette tranche ;
|
||||
- contrôle des fixtures JSON avec un parseur JSON local ;
|
||||
- contrôle du contenu de l'archive d'échange après génération.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Le sandbox ne fournit pas `cargo` ni `rustfmt`. Les commandes suivantes n'ont donc pas été déclarées comme réussies et doivent être exécuté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
|
||||
```
|
||||
|
||||
Conformément à `RUST-*`, `cargo test --workspace` reste requis au point de contrôle de session/clôture approprié. Aucun `cargo tree` supplémentaire
|
||||
n'est requis par ce delta, puisqu'aucune dépendance ni feature n'a changé.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- conserver les quatre canaris `0.2.1` et leurs noms publics ;
|
||||
- centraliser les primitives réellement communes plutôt que créer un mega DTO ;
|
||||
- utiliser `ksp_core_lib::Pubkey` dans les DTOs publics, avec conversion wire explicite ;
|
||||
- ne pas activer `serde` sur `solana-pubkey` uniquement pour les réponses RPC ;
|
||||
- ne pas ajouter `base64`, `bs58`, SPL ou client RPC haut niveau ;
|
||||
- conserver les champs provider/versionnés optionnels au lieu de les rendre obligatoires ;
|
||||
- ne pas ajouter de logs par DTO : l'observabilité reste au niveau du transport/executor.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante pour `pre.003`.
|
||||
|
||||
Le formatage final exact doit être produit par le `rustfmt` canonique du dépôt lors de l'application locale, puisque le binaire n'est pas disponible
|
||||
dans le sandbox d'échange.
|
||||
|
||||
## Suite
|
||||
|
||||
`0.2.2-pre.003` : implémenter les cinq wrappers Accounts (`getAccountInfo`, `getLargestAccounts`, `getMinimumBalanceForRentExemption`,
|
||||
`getMultipleAccounts`, `getProgramAccounts`) avec validation de cardinalité/filtres, décodage via les DTOs de cette tranche, serveur HTTP local et
|
||||
fixtures déterministes.
|
||||
154
deltas/0.2.2/pre.003-fix.001.md
Normal file
154
deltas/0.2.2/pre.003-fix.001.md
Normal file
@@ -0,0 +1,154 @@
|
||||
<!-- file: deltas/0.2.2/pre.003-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.003-fix.001` — activation production des helpers wire Accounts partagés
|
||||
|
||||
## Base requise
|
||||
|
||||
```text
|
||||
livraison : 0.2.2-pre.003
|
||||
workspace.package.version = "0.2.2-pre.3"
|
||||
```
|
||||
|
||||
Le correctif est ouvert après validation locale communiquée le 2026-08-18. `pre.003` active en production les cinq wrappers Accounts, mais leurs
|
||||
chemins de décodage utilisent `crate::decode_wire_json` et `crate::parse_wire_pubkey` alors que les deux réexports crate-private sont restés
|
||||
conditionnés par `#[cfg(test)]` dans `lib.rs` depuis le nettoyage préparatoire de `pre.002-fix.001`.
|
||||
|
||||
La conséquence est une régression de compilation `E0425` sur `cargo check`, `cargo clippy` et `cargo test`.
|
||||
|
||||
## Objectif
|
||||
|
||||
Rendre production-live uniquement les deux réexports crate-private désormais réellement consommés par les wrappers Accounts, sans modifier leur
|
||||
visibilité externe, sans changer les contrats publics `pre.003`, sans réécrire le delta historique `pre.003.md` et sans commencer la famille
|
||||
Tokens de `pre.004`.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Le correctif modifie du Rust de production. Conformément à `VER-ID-007` et `VER-ID-010` :
|
||||
|
||||
```text
|
||||
0.2.2-pre.3 -> 0.2.2-pre.3.fix.1
|
||||
```
|
||||
|
||||
Aucune dépendance ni feature Cargo n'est ajoutée ou retirée.
|
||||
|
||||
## Correction
|
||||
|
||||
Dans `crates/ksp-onchain-transport-lib/src/lib.rs`, les deux réexports internes suivants ne sont plus conditionnés par `#[cfg(test)]` :
|
||||
|
||||
```text
|
||||
pub(crate) use self::rpc_common::decode_wire_json;
|
||||
pub(crate) use self::rpc_common::parse_wire_pubkey;
|
||||
```
|
||||
|
||||
Leurs commentaires sont ajustés pour décrire leur rôle réel dans les adapters RPC typés, et non plus uniquement dans les tests préparatoires.
|
||||
|
||||
La visibilité reste strictement `pub(crate)` :
|
||||
|
||||
- aucune nouvelle API publique externe n'est créée ;
|
||||
- `rpc_common` reste propriétaire des implémentations ;
|
||||
- les wrappers Accounts peuvent continuer à utiliser la façade interne `crate::...` ;
|
||||
- aucun callsite Accounts n'est modifié ;
|
||||
- aucun `#[allow(...)]` n'est ajouté.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.003-fix.001.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.003.md
|
||||
deltas/0.2.2/pre.002-fix.002.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
ROADMAP.md
|
||||
CHANGELOG.md
|
||||
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/**
|
||||
crates/ksp-onchain-transport-lib/tests/**
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
Les deltas déjà publiés restent des traces historiques et ne sont pas réécrits.
|
||||
|
||||
## Validation ayant déclenché le correctif
|
||||
|
||||
Les commandes suivantes ont été exécutées par l'utilisateur sur `0.2.2-pre.003` :
|
||||
|
||||
```text
|
||||
cargo fmt --all : exécuté
|
||||
cargo check --workspace : ÉCHEC E0425
|
||||
cargo clippy --workspace --all-targets : ÉCHEC E0425
|
||||
cargo test -p ksp-onchain-transport-lib : ÉCHEC E0425
|
||||
```
|
||||
|
||||
Les erreurs concernent douze usages production dans `rpc_accounts.rs`. Les deux symboles absents sont :
|
||||
|
||||
```text
|
||||
crate::decode_wire_json
|
||||
crate::parse_wire_pubkey
|
||||
```
|
||||
|
||||
Le compilateur confirme que leurs réexports existent dans `lib.rs`, mais sont configurés hors build normal par `#[cfg(test)]`.
|
||||
|
||||
## Contrôles statiques effectués pendant la préparation du fix
|
||||
|
||||
- `decode_wire_json` reste défini production-live dans `rpc_common.rs` ;
|
||||
- `parse_wire_pubkey` reste défini production-live dans `rpc_common.rs` ;
|
||||
- seuls leurs deux réexports crate-private perdent `#[cfg(test)]` ;
|
||||
- leur visibilité reste `pub(crate)` ;
|
||||
- les douze callsites `rpc_accounts.rs` restent inchangés ;
|
||||
- aucun `#[allow(dead_code)]` ou autre suppression de lint n'est ajouté ;
|
||||
- aucun wrapper Token/Cluster supplémentaire n'est activé ;
|
||||
- aucune dépendance, feature ou configuration n'est modifiée ;
|
||||
- `workspace.package.version` vaut `0.2.2-pre.3.fix.1` ;
|
||||
- le delta `pre.003.md` reste inchangé.
|
||||
|
||||
## Validations du correctif non exécutées dans le sandbox
|
||||
|
||||
Le sandbox de préparation ne fournit pas la toolchain Cargo utilisée par le checkout opérateur. Les validations Rust du présent fix ne sont donc
|
||||
pas déclarées réussies.
|
||||
|
||||
Après application, exécuter :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Le résultat attendu est une compilation normale des cinq wrappers Accounts, sans les `E0425` signalées et sans nouveau warning.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- promouvoir les deux réexports internes parce qu'ils ont maintenant des consommateurs production réels ;
|
||||
- conserver `pub(crate)` plutôt que d'élargir l'API publique ;
|
||||
- ne pas modifier les douze callsites Accounts alors que leur chemin via la façade interne crate est cohérent ;
|
||||
- ne pas réintroduire de gating test sur des helpers consommés au runtime ;
|
||||
- ne pas commencer `pre.004` avant validation de ce fix.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation et commit de `0.2.2-pre.003-fix.001`, clôturer `pre.003` puis poursuivre avec `0.2.2-pre.004` pour les cinq wrappers Tokens.
|
||||
335
deltas/0.2.2/pre.003.md
Normal file
335
deltas/0.2.2/pre.003.md
Normal file
@@ -0,0 +1,335 @@
|
||||
<!-- file: deltas/0.2.2/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.003` — cinq wrappers HTTP Accounts typés
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente corrigée et validée localement par l'opérateur :
|
||||
|
||||
```text
|
||||
0.2.2-pre.002-fix.002
|
||||
workspace.package.version = "0.2.2-pre.2.fix.2"
|
||||
```
|
||||
|
||||
La validation opérateur du 2026-08-18 a confirmé :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test -p ksp-onchain-transport-lib OK
|
||||
```
|
||||
|
||||
Résultats Transport de cette base :
|
||||
|
||||
```text
|
||||
85 unit tests
|
||||
9 public API tests
|
||||
2 release completeness tests
|
||||
0 warning signalé par check/clippy
|
||||
```
|
||||
|
||||
Le plan canonique reste `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` version 3.
|
||||
|
||||
## Objectif
|
||||
|
||||
Clôturer la famille Accounts attribuée à `0.2.2` avec cinq wrappers publics typés :
|
||||
|
||||
```text
|
||||
getAccountInfo
|
||||
getLargestAccounts
|
||||
getMinimumBalanceForRentExemption
|
||||
getMultipleAccounts
|
||||
getProgramAccounts
|
||||
```
|
||||
|
||||
Tous passent par la foundation HTTP `0.2.1` :
|
||||
|
||||
```text
|
||||
wrapper typé
|
||||
-> descriptor audité
|
||||
-> execute_standard_rpc
|
||||
-> pool/admission/retry/deadline
|
||||
-> reqwest HTTP
|
||||
-> JSON-RPC validation
|
||||
-> décodage DTO KSP
|
||||
```
|
||||
|
||||
Aucun wrapper ne contacte `reqwest` directement.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Conformément à `VER-ID-009` :
|
||||
|
||||
```text
|
||||
0.2.2-pre.2.fix.2 -> 0.2.2-pre.3
|
||||
```
|
||||
|
||||
Aucune dépendance ni feature Cargo n'est ajoutée.
|
||||
|
||||
## Surface typée Accounts
|
||||
|
||||
### `getAccountInfo`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + Pubkey + Option<SolanaAccountInfoConfig>
|
||||
-> SolanaRpcResponse<Option<SolanaAccount>>
|
||||
```
|
||||
|
||||
Le wrapper :
|
||||
|
||||
- sérialise `encoding`, `dataSlice`, `commitment` et `minContextSlot` ;
|
||||
- omet l'objet config lorsqu'il est vide ;
|
||||
- conserve l'absence du compte sous `None` ;
|
||||
- conserve les formes Account data legacy, tuple encodé et `jsonParsed` ;
|
||||
- convertit l'owner en `ksp_core_lib::Pubkey` sans recopier une valeur invalide dans le diagnostic.
|
||||
|
||||
### `getLargestAccounts`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + Option<SolanaLargestAccountsConfig>
|
||||
-> SolanaRpcResponse<Vec<SolanaAccountBalance>>
|
||||
```
|
||||
|
||||
Le wrapper conserve l'ordre du wire et sérialise les champs KSP déjà audités dans `pre.002`, y compris `filter` et `sortResults` lorsqu'ils sont
|
||||
explicitement renseignés.
|
||||
|
||||
### `getMinimumBalanceForRentExemption`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + usize data_len + Option<SolanaCommitmentConfig>
|
||||
-> u64 lamports
|
||||
```
|
||||
|
||||
Le wrapper encode la longueur de données sans valeur sentinelle et conserve les erreurs JSON-RPC applicatives comme telles.
|
||||
|
||||
### `getMultipleAccounts`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + &[Pubkey] + Option<SolanaAccountInfoConfig>
|
||||
-> SolanaRpcResponse<Vec<Option<SolanaAccount>>>
|
||||
```
|
||||
|
||||
Le wrapper :
|
||||
|
||||
- refuse localement plus de 100 public keys ;
|
||||
- sérialise les adresses dans l'ordre fourni ;
|
||||
- conserve les `null` individuels ;
|
||||
- vérifie que le nombre d'éléments retournés correspond au nombre d'adresses demandées, afin de ne pas fabriquer silencieusement un mapping
|
||||
positionnel incohérent.
|
||||
|
||||
### `getProgramAccounts`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + program Pubkey + Option<SolanaProgramAccountsConfig>
|
||||
-> SolanaProgramAccountsResult
|
||||
```
|
||||
|
||||
Le résultat préserve les deux formes wire :
|
||||
|
||||
```text
|
||||
Accounts(Vec<SolanaKeyedAccount>)
|
||||
Context(SolanaRpcResponse<Vec<SolanaKeyedAccount>>)
|
||||
```
|
||||
|
||||
Le wrapper sérialise `dataSize`, `memcmp`, `tokenAccountState`, `withContext`, `sortResults` et la config Account commune.
|
||||
|
||||
## Validation locale des paramètres
|
||||
|
||||
Nouveau code d'erreur public Transport :
|
||||
|
||||
```text
|
||||
ERROR_CODE_INVALID_RPC_PARAMETERS
|
||||
onchain_transport.invalid_rpc_parameters
|
||||
```
|
||||
|
||||
Il distingue une requête KSP localement invalide d'une erreur JSON-RPC distante ou d'une réponse invalide.
|
||||
|
||||
Les invariants appliqués avant I/O sont :
|
||||
|
||||
```text
|
||||
getMultipleAccounts : <= 100 public keys
|
||||
getProgramAccounts : <= 4 filters
|
||||
memcmp raw bytes : <= 128 bytes
|
||||
```
|
||||
|
||||
Pour `memcmp` encodé sous forme base58/base64, KSP ne décode toujours pas localement la chaîne : la limite des 128 octets décodés reste donc
|
||||
contrôlée par le serveur. Cette décision évite d'ajouter `bs58` ou `base64` uniquement pour une validation anticipée.
|
||||
|
||||
## Wire Account activé en production
|
||||
|
||||
Les helpers privés préparés par `pre.002` et réellement nécessaires à ces wrappers deviennent production-live :
|
||||
|
||||
- sérialisation des configs Account/Largest/Program ;
|
||||
- conversion des encodings Account ;
|
||||
- décodage `SolanaAccountData` ;
|
||||
- décodage Account/KeyedAccount/AccountBalance ;
|
||||
- conversion `Pubkey` wire ;
|
||||
- construction interne de `SolanaRpcResponse<T>`.
|
||||
|
||||
Les helpers Cluster/Token préparatoires restent inchangés et ne deviennent pas production-live avant leurs tranches respectives.
|
||||
|
||||
## Fixtures HTTP déterministes ajoutées
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.null.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.invalid_owner.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_largest_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_largest_accounts.invalid_address.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_minimum_balance_for_rent_exemption.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_minimum_balance_for_rent_exemption.error.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_multiple_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_program_accounts.bare.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_program_accounts.context.success.json
|
||||
```
|
||||
|
||||
Les tests locaux utilisent un serveur HTTP loopback et n'accèdent pas à Internet.
|
||||
|
||||
## Couverture de tests ajoutée
|
||||
|
||||
Les tests Accounts couvrent :
|
||||
|
||||
- request exacte pour chaque méthode ;
|
||||
- config et omission de config vide ;
|
||||
- succès typé ;
|
||||
- account absent ;
|
||||
- `null` dans `getMultipleAccounts` ;
|
||||
- ordre de `getMultipleAccounts` ;
|
||||
- résultat bare et contextualisé de `getProgramAccounts` ;
|
||||
- invalid owner/address ;
|
||||
- erreur JSON-RPC applicative ;
|
||||
- limite des 100 comptes ;
|
||||
- limite des 4 filtres ;
|
||||
- limite raw `memcmp` de 128 octets ;
|
||||
- mismatch cardinalité request/result de `getMultipleAccounts`.
|
||||
|
||||
Le test public compile explicitement les cinq méthodes sur `HttpTransportPool` et vérifie le nouveau code d'erreur.
|
||||
|
||||
Un canari release supplémentaire fige l'ensemble Accounts `0.2.2` exact :
|
||||
|
||||
```text
|
||||
getAccountInfo
|
||||
getLargestAccounts
|
||||
getMinimumBalanceForRentExemption
|
||||
getMultipleAccounts
|
||||
getProgramAccounts
|
||||
```
|
||||
|
||||
Il vérifie aussi que ces descriptors restent `Read + RetrySafe`. Aucune famille Tokens/Cluster n'est déclarée typed-complete par cette tranche.
|
||||
|
||||
Après application, la cible Transport attendue devient :
|
||||
|
||||
```text
|
||||
98 unit tests
|
||||
10 public API tests
|
||||
3 release completeness tests
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.null.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.invalid_owner.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_largest_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_largest_accounts.invalid_address.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_minimum_balance_for_rent_exemption.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_minimum_balance_for_rent_exemption.error.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_multiple_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_program_accounts.bare.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_program_accounts.context.success.json
|
||||
deltas/0.2.2/pre.003.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/error.rs
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/executor.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_method.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
La tranche suivante reste `pre.004 — cinq wrappers Tokens`.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- prise en compte de la validation opérateur propre de `pre.002-fix.002` comme base ;
|
||||
- relecture des règles `VERSION_WORKFLOW.md` et `RULES_RUST.md` applicables ;
|
||||
- revérification de la documentation HTTP Solana actuelle pour les cinq méthodes Accounts ;
|
||||
- recoupement des signatures/limites avec la source Agave `v4.2.1` auditée par le plan ;
|
||||
- validation JSON syntaxique des dix nouvelles fixtures ;
|
||||
- contrôle statique de l'absence de `unwrap`, `expect`, `panic!` et `#[allow(dead_code)]` dans le code de production modifié ;
|
||||
- contrôle statique que les cinq wrappers utilisent `execute_standard_rpc` et aucun appel `reqwest` direct ;
|
||||
- contrôle statique de l'absence de nouvelle dépendance/feature ;
|
||||
- contrôle exact des fichiers inclus dans l'archive d'échange.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Le sandbox d'échange ne fournit pas `cargo`, `rustc` ou `rustfmt`. Les commandes suivantes doivent être exécutées sur le checkout opérateur avant
|
||||
commit :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
`cargo test --workspace` reste réservé au checkpoint de clôture/session conformément au workflow KSP. Aucun `cargo tree` n'est requis par cette
|
||||
tranche puisqu'aucune dépendance ni feature n'a changé.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- utiliser les DTOs `pre.002` sans créer de seconde couche RPC ;
|
||||
- conserver `execute_standard_rpc()` comme unique entrée d'exécution HTTP standard ;
|
||||
- introduire un code d'erreur dédié aux paramètres typed invalides plutôt que détourner `invalid_settings` ;
|
||||
- vérifier localement les limites dont KSP peut connaître la violation sans dépendance de décodage supplémentaire ;
|
||||
- préserver les `null`, l'ordre et la dualité bare/context au lieu de normaliser artificiellement les réponses ;
|
||||
- ne pas modifier Config ;
|
||||
- ne pas ajouter de logging spécifique par méthode.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante pour `pre.004`.
|
||||
|
||||
## Suite
|
||||
|
||||
`0.2.2-pre.004` : implémenter les cinq wrappers Tokens (`getTokenAccountBalance`, `getTokenAccountsByDelegate`, `getTokenAccountsByOwner`,
|
||||
`getTokenLargestAccounts`, `getTokenSupply`) avec les DTOs préparés, selector `Mint | ProgramId`, fixtures HTTP déterministes et conservation de
|
||||
`uiAmount: null`.
|
||||
324
deltas/0.2.2/pre.004.md
Normal file
324
deltas/0.2.2/pre.004.md
Normal file
@@ -0,0 +1,324 @@
|
||||
<!-- file: deltas/0.2.2/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.004` — cinq wrappers HTTP Tokens typés
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente corrigée et validée localement par l'opérateur :
|
||||
|
||||
```text
|
||||
0.2.2-pre.003-fix.001
|
||||
workspace.package.version = "0.2.2-pre.3.fix.1"
|
||||
```
|
||||
|
||||
La validation opérateur du 2026-08-18 a confirmé :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test -p ksp-onchain-transport-lib OK
|
||||
```
|
||||
|
||||
Résultats Transport de cette base :
|
||||
|
||||
```text
|
||||
98 unit tests
|
||||
10 public API tests
|
||||
3 release completeness tests
|
||||
0 warning signalé par check/clippy
|
||||
```
|
||||
|
||||
Le plan canonique reste `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` version 3.
|
||||
|
||||
## Objectif
|
||||
|
||||
Clôturer la famille Tokens attribuée à `0.2.2` avec cinq wrappers publics typés :
|
||||
|
||||
```text
|
||||
getTokenAccountBalance
|
||||
getTokenAccountsByDelegate
|
||||
getTokenAccountsByOwner
|
||||
getTokenLargestAccounts
|
||||
getTokenSupply
|
||||
```
|
||||
|
||||
Tous passent par la foundation HTTP commune :
|
||||
|
||||
```text
|
||||
wrapper typé
|
||||
-> descriptor audité
|
||||
-> execute_standard_rpc
|
||||
-> pool/admission/retry/deadline
|
||||
-> reqwest HTTP
|
||||
-> JSON-RPC validation
|
||||
-> décodage DTO KSP
|
||||
```
|
||||
|
||||
Aucun wrapper Token ne contacte `reqwest` directement et aucune crate SPL n'est ajoutée.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Conformément à `VER-ID-009` :
|
||||
|
||||
```text
|
||||
0.2.2-pre.3.fix.1 -> 0.2.2-pre.4
|
||||
```
|
||||
|
||||
Aucune dépendance ni feature Cargo n'est ajoutée ou retirée.
|
||||
|
||||
## Réaudit ciblé du contrat courant
|
||||
|
||||
Les pages RPC Solana courantes ont été revérifiées le 2026-08-18 pour cette tranche :
|
||||
|
||||
```text
|
||||
https://solana.com/docs/rpc/http/gettokenaccountbalance
|
||||
https://solana.com/docs/rpc/http/gettokenaccountsbydelegate
|
||||
https://solana.com/docs/rpc/http/gettokenaccountsbyowner
|
||||
https://solana.com/docs/rpc/http/gettokenlargestaccounts
|
||||
https://solana.com/docs/rpc/http/gettokensupply
|
||||
```
|
||||
|
||||
Le contrat reste cohérent avec le plan `009` :
|
||||
|
||||
- `getTokenAccountBalance`, `getTokenLargestAccounts` et `getTokenSupply` acceptent un `commitment` optionnel ;
|
||||
- `getTokenAccountsByDelegate` et `getTokenAccountsByOwner` prennent un selector exclusif `{mint}` ou `{programId}` puis une config Account optionnelle ;
|
||||
- les cinq réponses sont contextualisées ;
|
||||
- les deux méthodes de liste renvoient des `pubkey + account` et réutilisent donc le DTO Account générique ;
|
||||
- `TokenAmount.uiAmount` reste nullable ; `uiAmountString` reste la représentation textuelle à préserver ;
|
||||
- `getTokenLargestAccounts` renvoie les 20 plus gros token accounts côté RPC, sans que KSP invente un tri supplémentaire.
|
||||
|
||||
Le réaudit complémentaire Agave `v4.2.1` déjà fixé en `pre.001-fix.001` reste la référence d'implémentation ciblée ; aucune divergence
|
||||
nouvelle nécessitant un changement de DTO n'a été identifiée pour cette tranche.
|
||||
|
||||
## Surface typée Tokens
|
||||
|
||||
### `getTokenAccountBalance`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + token account Pubkey + Option<SolanaCommitmentConfig>
|
||||
-> SolanaRpcResponse<SolanaTokenAmount>
|
||||
```
|
||||
|
||||
Le wrapper préserve `amount`, `decimals`, `uiAmount` nullable et `uiAmountString`, et conserve une erreur JSON-RPC distante comme erreur
|
||||
applicative RPC.
|
||||
|
||||
### `getTokenAccountsByDelegate`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + delegate Pubkey + SolanaTokenAccountSelector + Option<SolanaAccountInfoConfig>
|
||||
-> SolanaRpcResponse<Vec<SolanaKeyedAccount>>
|
||||
```
|
||||
|
||||
Le selector KSP empêche par construction les objets contenant simultanément `mint` et `programId`. La config Account réutilise les encodings,
|
||||
`dataSlice`, `commitment` et `minContextSlot` activés en `pre.003`.
|
||||
|
||||
### `getTokenAccountsByOwner`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + owner Pubkey + SolanaTokenAccountSelector + Option<SolanaAccountInfoConfig>
|
||||
-> SolanaRpcResponse<Vec<SolanaKeyedAccount>>
|
||||
```
|
||||
|
||||
Le wrapper partage le même chemin privé de sérialisation/décodage que la variante Delegate afin d'éviter deux implémentations de wire parallèles.
|
||||
Un config Account explicitement vide est omis du tableau `params`.
|
||||
|
||||
### `getTokenLargestAccounts`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + mint Pubkey + Option<SolanaCommitmentConfig>
|
||||
-> SolanaRpcResponse<Vec<SolanaTokenAccountBalance>>
|
||||
```
|
||||
|
||||
L'ordre retourné par le serveur est conservé. Chaque `address` est validée et convertie en `ksp_core_lib::Pubkey` sans recopier une valeur wire
|
||||
invalide dans le diagnostic.
|
||||
|
||||
### `getTokenSupply`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + mint Pubkey + Option<SolanaCommitmentConfig>
|
||||
-> SolanaRpcResponse<SolanaTokenAmount>
|
||||
```
|
||||
|
||||
`amount` reste une chaîne exacte et KSP ne convertit pas la supply en type métier SPL ni en entier borné localement.
|
||||
|
||||
## Mutualisation activée en production
|
||||
|
||||
Les helpers Token préparés en `pre.002` deviennent production-live uniquement parce qu'ils ont maintenant des consommateurs runtime réels :
|
||||
|
||||
- sérialisation de `SolanaTokenAccountSelector` ;
|
||||
- décodage `SolanaTokenAmount` ;
|
||||
- décodage `SolanaTokenAccountBalance` ;
|
||||
- wire structs Token privés.
|
||||
|
||||
`SolanaAccountInfoConfig::is_empty` devient `pub(crate)` pour permettre aux deux wrappers de liste Token de réutiliser la règle d'omission de
|
||||
config vide déjà utilisée par Accounts. Cette modification reste interne à la crate et n'élargit pas l'API publique externe.
|
||||
|
||||
Les helpers Cluster préparatoires restent inchangés et ne deviennent pas production-live avant `pre.005`/`pre.006`.
|
||||
|
||||
## Validation locale des paramètres
|
||||
|
||||
Aucun nouveau code d'erreur local n'est nécessaire dans cette tranche :
|
||||
|
||||
- le selector `Mint | ProgramId` garantit exactement une variante par construction ;
|
||||
- les owner/delegate/mint/token-account sont des `Pubkey` typées avant l'appel ;
|
||||
- les configs Account/Commitment réutilisent les types déjà validés ;
|
||||
- aucune cardinalité supplémentaire documentée ne doit être imposée côté client.
|
||||
|
||||
`ERROR_CODE_INVALID_RPC_PARAMETERS` introduit en `pre.003` reste inchangé.
|
||||
|
||||
## Fixtures HTTP déterministes ajoutées
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.error.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_delegate.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.invalid_address.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_supply.success.json
|
||||
```
|
||||
|
||||
Les tests utilisent un serveur HTTP loopback et n'accèdent pas à Internet.
|
||||
|
||||
## Couverture de tests ajoutée
|
||||
|
||||
Les tests Token couvrent notamment :
|
||||
|
||||
- request exacte de chacune des cinq méthodes ;
|
||||
- selector `programId` pour Delegate ;
|
||||
- selector `mint` pour Owner ;
|
||||
- config Account complète et omission d'un config explicitement vide ;
|
||||
- commitment optionnel ;
|
||||
- `uiAmount: null` ;
|
||||
- montant/supply textuel exact ;
|
||||
- conservation de l'ordre pour `getTokenLargestAccounts` ;
|
||||
- décodage `jsonParsed` des accounts sous forme JSON opaque Transport ;
|
||||
- erreur JSON-RPC applicative ;
|
||||
- rejet d'une `pubkey` ou `address` invalide dans une réponse typée.
|
||||
|
||||
Le test public compile explicitement les cinq méthodes depuis `HttpTransportPool`.
|
||||
|
||||
Un canari release fige l'ensemble Tokens `0.2.2` exact :
|
||||
|
||||
```text
|
||||
getTokenAccountBalance
|
||||
getTokenAccountsByDelegate
|
||||
getTokenAccountsByOwner
|
||||
getTokenLargestAccounts
|
||||
getTokenSupply
|
||||
```
|
||||
|
||||
Il vérifie également `Read + RetrySafe` pour chacun. Les 12 wrappers Cluster restent hors de cette tranche.
|
||||
|
||||
Après application, la cible Transport attendue devient :
|
||||
|
||||
```text
|
||||
106 unit tests
|
||||
11 public API tests
|
||||
4 release completeness tests
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.error.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_delegate.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.invalid_address.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_token_supply.success.json
|
||||
deltas/0.2.2/pre.004.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.003.md
|
||||
deltas/0.2.2/pre.003-fix.001.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
ROADMAP.md
|
||||
CHANGELOG.md
|
||||
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_method.rs
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
Les deltas déjà publiés restent des traces historiques et ne sont pas réécrits.
|
||||
|
||||
## Contrôles statiques effectués pendant la préparation
|
||||
|
||||
- `workspace.package.version` vaut `0.2.2-pre.4` ;
|
||||
- les huit nouvelles fixtures JSON sont syntaxiquement valides ;
|
||||
- les cinq wrappers utilisent `execute_standard_rpc` ;
|
||||
- aucun `reqwest` direct n'est ajouté dans `rpc_tokens.rs` ;
|
||||
- aucun `#[allow(dead_code)]` ou suppression de lint n'est ajouté ;
|
||||
- aucun helper Cluster n'est activé en production ;
|
||||
- aucune dépendance ou feature Cargo n'est ajoutée ;
|
||||
- aucune crate SPL n'est importée ;
|
||||
- la surface publique Accounts de `pre.003` reste présente ;
|
||||
- les deltas historiques restent inchangés.
|
||||
|
||||
## Validations Rust non exécutées dans le sandbox
|
||||
|
||||
Le sandbox de préparation ne fournit pas la toolchain Cargo utilisée par le checkout opérateur. Les commandes suivantes doivent donc être rejouées
|
||||
après application :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Les validations ne sont pas déclarées réussies tant qu'elles n'ont pas été exécutées sur le checkout opérateur.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation et commit de `0.2.2-pre.004`, poursuivre avec `0.2.2-pre.005` pour les sept wrappers Cluster simples :
|
||||
|
||||
```text
|
||||
getClusterNodes
|
||||
getEpochInfo
|
||||
getEpochSchedule
|
||||
getHighestSnapshotSlot
|
||||
getIdentity
|
||||
getMaxRetransmitSlot
|
||||
getMaxShredInsertSlot
|
||||
```
|
||||
269
deltas/0.2.2/pre.005.md
Normal file
269
deltas/0.2.2/pre.005.md
Normal file
@@ -0,0 +1,269 @@
|
||||
<!-- file: deltas/0.2.2/pre.005.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.005` — sept wrappers HTTP Cluster simples typés
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée localement par l'opérateur :
|
||||
|
||||
```text
|
||||
0.2.2-pre.004
|
||||
workspace.package.version = "0.2.2-pre.4"
|
||||
```
|
||||
|
||||
La validation opérateur du 2026-08-18 a confirmé :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test -p ksp-onchain-transport-lib OK
|
||||
```
|
||||
|
||||
Résultats Transport de cette base :
|
||||
|
||||
```text
|
||||
106 unit tests
|
||||
11 public API tests
|
||||
4 release completeness tests
|
||||
0 warning signalé par check/clippy
|
||||
```
|
||||
|
||||
Le plan canonique reste `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` version 3.
|
||||
|
||||
## Contrôle documentaire avant nouvelle tranche
|
||||
|
||||
Le contrôle de cohérence demandé entre prereleases confirme :
|
||||
|
||||
- `ROADMAP.md` conserve correctement `0.2.2` en cours `[/]` ;
|
||||
- le plan `009` attribue exactement à `pre.005` les sept méthodes Cluster simples implémentées ici ;
|
||||
- `pre.006` conserve exactement cinq méthodes différées : `getLeaderSchedule`, `getSlot`, `getSlotLeader`, `getSlotLeaders`, `getVoteAccounts` ;
|
||||
- les index `docs/000-README.md`, `docs/plans/000-README.md` et `docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` restent cohérents et ne nécessitent aucune modification.
|
||||
|
||||
## Objectif
|
||||
|
||||
Implémenter les sept wrappers publics typés Cluster simples prévus par le plan :
|
||||
|
||||
```text
|
||||
getClusterNodes
|
||||
getEpochInfo
|
||||
getEpochSchedule
|
||||
getHighestSnapshotSlot
|
||||
getIdentity
|
||||
getMaxRetransmitSlot
|
||||
getMaxShredInsertSlot
|
||||
```
|
||||
|
||||
Tous passent par la foundation HTTP commune :
|
||||
|
||||
```text
|
||||
wrapper typé
|
||||
-> descriptor audité
|
||||
-> execute_standard_rpc
|
||||
-> pool/admission/retry/deadline
|
||||
-> reqwest HTTP
|
||||
-> JSON-RPC validation
|
||||
-> décodage DTO KSP
|
||||
```
|
||||
|
||||
Aucun wrapper Cluster ne contacte `reqwest` directement.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Conformément au cycle prerelease KSP :
|
||||
|
||||
```text
|
||||
0.2.2-pre.4 -> 0.2.2-pre.5
|
||||
```
|
||||
|
||||
Aucune dépendance ni feature Cargo n'est ajoutée ou retirée.
|
||||
|
||||
## Réaudit ciblé du contrat courant
|
||||
|
||||
Les pages RPC Solana courantes ont été revérifiées le 2026-08-18 :
|
||||
|
||||
```text
|
||||
https://solana.com/docs/rpc/http/getclusternodes
|
||||
https://solana.com/docs/rpc/http/getepochinfo
|
||||
https://solana.com/docs/rpc/http/getepochschedule
|
||||
https://solana.com/docs/rpc/http/gethighestsnapshotslot
|
||||
https://solana.com/docs/rpc/http/getidentity
|
||||
https://solana.com/docs/rpc/http/getmaxretransmitslot
|
||||
https://solana.com/docs/rpc/http/getmaxshredinsertslot
|
||||
```
|
||||
|
||||
Le contrat reste cohérent avec le plan `009` :
|
||||
|
||||
- `getClusterNodes` ne prend aucun paramètre et renvoie une liste de contacts de noeuds ;
|
||||
- `getEpochInfo` accepte uniquement la config commune `commitment/minContextSlot` et conserve `transactionCount` nullable ;
|
||||
- `getEpochSchedule` ne prend aucun paramètre et renvoie la structure fixe d'epoch schedule ;
|
||||
- `getHighestSnapshotSlot` ne prend aucun paramètre, conserve `incremental` nullable et laisse l'absence de snapshot comme erreur JSON-RPC distante ;
|
||||
- `getIdentity` ne prend aucun paramètre et renvoie l'objet `{ identity }`, converti en `Pubkey` KSP ;
|
||||
- `getMaxRetransmitSlot` et `getMaxShredInsertSlot` ne prennent aucun paramètre et renvoient chacun un `u64`.
|
||||
|
||||
Le champ Agave `v4.2.1` `clientId: Option<String>` identifié par `pre.001-fix.001` reste conservé par `SolanaClusterNode`, sans devenir obligatoire.
|
||||
|
||||
## Surface typée Cluster simple
|
||||
|
||||
### `getClusterNodes`
|
||||
|
||||
```text
|
||||
role -> Vec<SolanaClusterNode>
|
||||
```
|
||||
|
||||
Chaque `pubkey` est validée et convertie en `ksp_core_lib::Pubkey`. Les endpoints réseau restent des chaînes optionnelles. Les champs optionnels absents, y compris `clientId`, restent `None`.
|
||||
|
||||
### `getEpochInfo`
|
||||
|
||||
```text
|
||||
role + Option<SolanaContextConfig> -> SolanaEpochInfo
|
||||
```
|
||||
|
||||
Un config explicitement vide est omis de `params`. `transactionCount: null` reste `None`.
|
||||
|
||||
### `getEpochSchedule`
|
||||
|
||||
```text
|
||||
role -> SolanaEpochSchedule
|
||||
```
|
||||
|
||||
Aucune transformation métier n'est introduite.
|
||||
|
||||
### `getHighestSnapshotSlot`
|
||||
|
||||
```text
|
||||
role -> SolanaSnapshotSlotInfo
|
||||
```
|
||||
|
||||
`incremental` reste optionnel. Une erreur RPC « no snapshot » n'est pas convertie en valeur sentinelle.
|
||||
|
||||
### `getIdentity`
|
||||
|
||||
```text
|
||||
role -> Pubkey
|
||||
```
|
||||
|
||||
Le wrapper valide la chaîne `identity` avant de l'exposer comme `Pubkey`, sans recopier une valeur wire invalide dans le diagnostic.
|
||||
|
||||
### `getMaxRetransmitSlot` / `getMaxShredInsertSlot`
|
||||
|
||||
```text
|
||||
role -> u64
|
||||
```
|
||||
|
||||
Les deux wrappers partagent un chemin privé simple de décodage `u64` et ne créent aucun DTO artificiel.
|
||||
|
||||
## Activation ciblée des helpers préparés en `pre.002`
|
||||
|
||||
Les helpers Cluster nécessaires à cette tranche deviennent production-live uniquement maintenant qu'ils ont des consommateurs runtime :
|
||||
|
||||
- `SolanaClusterNode::decode_wire` + `WireClusterNode` ;
|
||||
- `SolanaEpochInfo::decode_wire` + `WireEpochInfo` ;
|
||||
- `SolanaEpochSchedule::decode_wire` + `WireEpochSchedule` ;
|
||||
- `SolanaSnapshotSlotInfo::decode_wire` + `WireSnapshotSlotInfo`.
|
||||
|
||||
Les helpers spécifiques à `getLeaderSchedule` et `getVoteAccounts` restent sous `#[cfg(test)]` jusqu'à `pre.006`. Aucun `#[allow(dead_code)]` n'est ajouté.
|
||||
|
||||
## Fixtures HTTP déterministes ajoutées
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_cluster_nodes.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_cluster_nodes.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_epoch_info.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_epoch_schedule.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_highest_snapshot_slot.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_highest_snapshot_slot.error.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_identity.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_identity.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_max_retransmit_slot.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_max_shred_insert_slot.success.json
|
||||
```
|
||||
|
||||
Les tests utilisent uniquement un serveur HTTP loopback local.
|
||||
|
||||
## Couverture de tests ajoutée
|
||||
|
||||
Les tests couvrent notamment :
|
||||
|
||||
- requête sans paramètres pour les six méthodes sans config ;
|
||||
- sérialisation exacte `commitment + minContextSlot` pour `getEpochInfo` ;
|
||||
- omission d'un `SolanaContextConfig` explicitement vide ;
|
||||
- `transactionCount: null` ;
|
||||
- champs ClusterNode optionnels présents et absents ;
|
||||
- conservation de `clientId` Agave `v4.2.1` ;
|
||||
- rejet d'une `pubkey` de noeud invalide ;
|
||||
- `incremental: null` ;
|
||||
- propagation d'une erreur RPC « no snapshot » ;
|
||||
- validation de l'identity `Pubkey` ;
|
||||
- décodage des deux slots max comme `u64`.
|
||||
|
||||
Le test public compile explicitement les sept nouvelles méthodes depuis `HttpTransportPool`.
|
||||
|
||||
Un canari release fige le sous-ensemble `pre.005` exact et vérifie `Read + RetrySafe` pour les sept descriptors, tout en laissant les cinq méthodes Cluster de `pre.006` enregistrées mais différées.
|
||||
|
||||
Après application, la cible Transport attendue devient :
|
||||
|
||||
```text
|
||||
116 unit tests
|
||||
12 public API tests
|
||||
5 release completeness tests
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_cluster_nodes.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_cluster_nodes.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_epoch_info.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_epoch_schedule.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_highest_snapshot_slot.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_highest_snapshot_slot.error.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_identity.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_identity.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_max_retransmit_slot.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_max_shred_insert_slot.success.json
|
||||
deltas/0.2.2/pre.005.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Documentation durable
|
||||
|
||||
Aucune modification de ROADMAP/plan/index n'est requise par cette tranche après le contrôle documentaire : les documents existants décrivent déjà exactement ce découpage.
|
||||
|
||||
`CHANGELOG.md` reste réservé à la clôture stable.
|
||||
|
||||
## Validation à exécuter sur le checkout opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
La livraison n'affirme pas que ces commandes ont été exécutées dans l'environnement de génération.
|
||||
|
||||
## Critères de validation de `pre.005`
|
||||
|
||||
`pre.005` est validable lorsque :
|
||||
|
||||
- les quatre commandes ci-dessus passent sans warning nouveau ;
|
||||
- les 116 unit tests passent ;
|
||||
- les 12 tests public API passent ;
|
||||
- les 5 tests release completeness passent ;
|
||||
- aucun helper Cluster simple production-live n'est `dead_code` ;
|
||||
- les cinq wrappers complexes de `pre.006` restent hors de cette tranche.
|
||||
95
deltas/0.2.2/pre.006-fix.001.md
Normal file
95
deltas/0.2.2/pre.006-fix.001.md
Normal file
@@ -0,0 +1,95 @@
|
||||
<!-- file: deltas/0.2.2/pre.006-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.006-fix.001` — correction Clippy du test LeaderSchedule
|
||||
|
||||
## Base requise
|
||||
|
||||
Cette correction s'applique après :
|
||||
|
||||
```text
|
||||
0.2.2-pre.006
|
||||
workspace.package.version = "0.2.2-pre.6"
|
||||
```
|
||||
|
||||
La validation opérateur du 2026-08-18 a confirmé :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo test -p ksp-onchain-transport-lib OK
|
||||
```
|
||||
|
||||
Résultats Transport :
|
||||
|
||||
```text
|
||||
127 unit tests
|
||||
13 public API tests
|
||||
6 release completeness tests
|
||||
0 échec
|
||||
```
|
||||
|
||||
Le contrôle Clippy a terminé avec un unique warning dans le test `rpc_cluster` :
|
||||
|
||||
```text
|
||||
clippy::clone_on_copy
|
||||
```
|
||||
|
||||
Le `Pubkey` `identity` était cloné alors que ce type implémente `Copy`.
|
||||
|
||||
## Nature du fix
|
||||
|
||||
Le correctif retire uniquement le `.clone()` inutile :
|
||||
|
||||
```text
|
||||
Some(identity.clone()) -> Some(identity)
|
||||
```
|
||||
|
||||
Aucun comportement runtime n'est modifié.
|
||||
|
||||
Aucun wrapper RPC, DTO, fixture, contrat public, dépendance ou configuration n'est modifié.
|
||||
|
||||
Le delta historique `pre.006.md` reste inchangé.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Ce fix modifie une source Rust de test ; le signal technique devient donc :
|
||||
|
||||
```text
|
||||
0.2.2-pre.6 -> 0.2.2-pre.6.fix.1
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.006-fix.001.md
|
||||
```
|
||||
|
||||
## Validation à rejouer
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Le résultat attendu est :
|
||||
|
||||
```text
|
||||
0 warning Clippy
|
||||
127 unit tests OK
|
||||
13 public API tests OK
|
||||
6 release completeness tests OK
|
||||
```
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation locale de ce fix, `0.2.2-pre.007` peut reprendre la clôture de prerelease prévue : complétude globale, smoke déterministe pertinent, documentation finale et préparation de la release stable `0.2.2`.
|
||||
298
deltas/0.2.2/pre.006.md
Normal file
298
deltas/0.2.2/pre.006.md
Normal file
@@ -0,0 +1,298 @@
|
||||
<!-- file: deltas/0.2.2/pre.006.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.006` — cinq wrappers HTTP Cluster restants typés
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente validée localement par l'opérateur :
|
||||
|
||||
```text
|
||||
0.2.2-pre.005
|
||||
workspace.package.version = "0.2.2-pre.5"
|
||||
```
|
||||
|
||||
La validation opérateur du 2026-08-18 a confirmé :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test -p ksp-onchain-transport-lib OK
|
||||
```
|
||||
|
||||
Résultats Transport de cette base :
|
||||
|
||||
```text
|
||||
116 unit tests
|
||||
12 public API tests
|
||||
5 release completeness tests
|
||||
0 warning signalé par check/clippy
|
||||
```
|
||||
|
||||
Le plan canonique reste `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` version 3.
|
||||
|
||||
## Contrôle documentaire avant nouvelle tranche
|
||||
|
||||
Le découpage durable reste cohérent :
|
||||
|
||||
- `ROADMAP.md` conserve `0.2.2` en cours `[/]` ;
|
||||
- le plan `009` attribue exactement à `pre.006` les cinq méthodes Cluster restantes ;
|
||||
- les 5 Accounts de `pre.003`, les 5 Tokens de `pre.004` et les 7 Cluster simples de `pre.005` restent inchangés ;
|
||||
- `pre.007` reste la tranche de complétude/smoke/docs/préparation stable prévue ;
|
||||
- aucun index documentaire durable ne nécessite de modification à cette étape.
|
||||
|
||||
## Objectif
|
||||
|
||||
Implémenter les cinq wrappers publics typés Cluster restants :
|
||||
|
||||
```text
|
||||
getLeaderSchedule
|
||||
getSlot
|
||||
getSlotLeader
|
||||
getSlotLeaders
|
||||
getVoteAccounts
|
||||
```
|
||||
|
||||
Après cette tranche, les 22 méthodes HTTP Accounts + Tokens + Cluster attribuées à `0.2.2` possèdent toutes leur wrapper typé KSP.
|
||||
|
||||
Tous les wrappers passent par la foundation HTTP commune :
|
||||
|
||||
```text
|
||||
wrapper typé
|
||||
-> descriptor audité
|
||||
-> execute_standard_rpc
|
||||
-> pool/admission/retry/deadline
|
||||
-> reqwest HTTP
|
||||
-> JSON-RPC validation
|
||||
-> décodage DTO KSP
|
||||
```
|
||||
|
||||
Aucun wrapper Cluster ne contacte `reqwest` directement.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Conformément au cycle prerelease KSP :
|
||||
|
||||
```text
|
||||
0.2.2-pre.5 -> 0.2.2-pre.6
|
||||
```
|
||||
|
||||
Aucune dépendance ni feature Cargo n'est ajoutée ou retirée.
|
||||
|
||||
## Réaudit ciblé du contrat courant
|
||||
|
||||
Le contrat préparé dans le plan `009` a été recroisé avec la source primaire Agave `v4.2.1`, notamment :
|
||||
|
||||
```text
|
||||
rpc-client-types/src/config.rs
|
||||
rpc/src/rpc.rs
|
||||
```
|
||||
|
||||
Les invariants utiles à cette tranche restent :
|
||||
|
||||
- `getLeaderSchedule` accepte l'overload slot/config déjà modélisé par `SolanaLeaderScheduleRequest` ;
|
||||
- `RpcLeaderScheduleConfig` contient `identity` et `commitment` ;
|
||||
- le résultat de `getLeaderSchedule` reste nullable ;
|
||||
- `getSlot` et `getSlotLeader` utilisent la config commune `commitment/minContextSlot` ;
|
||||
- `getSlotLeaders` prend `startSlot + limit`, avec la limite `1..=5000` retenue par le plan ;
|
||||
- `RpcGetVoteAccountsConfig` contient `votePubkey`, `commitment`, `keepUnstakedDelinquents`, `delinquentSlotDistance` ;
|
||||
- `getVoteAccounts` renvoie les groupes `current` et `delinquent` ;
|
||||
- `inflationRewardsCommissionBps` reste optionnel pour compatibilité avec les noeuds antérieurs ;
|
||||
- l'historique `epochCredits` reste exposé comme historique RPC borné, sans être interprété comme historique complet du vote account.
|
||||
|
||||
## Surface typée Cluster finale
|
||||
|
||||
### `getLeaderSchedule`
|
||||
|
||||
```text
|
||||
role + SolanaLeaderScheduleRequest -> Option<SolanaLeaderSchedule>
|
||||
```
|
||||
|
||||
`SolanaLeaderScheduleRequest` conserve les deux formes cohérentes :
|
||||
|
||||
```text
|
||||
CurrentEpoch(config?)
|
||||
Slot { slot, config? }
|
||||
```
|
||||
|
||||
Un config vide est omis. Un résultat JSON `null` devient `None`. Une map non nulle valide chaque identité et la convertit en `ksp_core_lib::Pubkey`.
|
||||
|
||||
### `getSlot`
|
||||
|
||||
```text
|
||||
role + Option<SolanaContextConfig> -> u64
|
||||
```
|
||||
|
||||
La config commune est sérialisée uniquement lorsqu'elle contient `commitment` et/ou `minContextSlot`. Une erreur distante `MinContextSlotNotReached` reste une erreur RPC applicative.
|
||||
|
||||
### `getSlotLeader`
|
||||
|
||||
```text
|
||||
role + Option<SolanaContextConfig> -> Pubkey
|
||||
```
|
||||
|
||||
La valeur texte du leader est validée avant exposition comme `Pubkey`, sans recopier une valeur wire invalide dans le diagnostic.
|
||||
|
||||
### `getSlotLeaders`
|
||||
|
||||
```text
|
||||
role + startSlot u64 + limit u64 -> Vec<Pubkey>
|
||||
```
|
||||
|
||||
KSP rejette localement avant I/O :
|
||||
|
||||
```text
|
||||
limit == 0
|
||||
limit > 5000
|
||||
```
|
||||
|
||||
L'ordre du tableau retourné est conservé, car chaque position correspond aux slots successifs demandés.
|
||||
|
||||
### `getVoteAccounts`
|
||||
|
||||
```text
|
||||
role + Option<SolanaVoteAccountsConfig> -> SolanaVoteAccountStatus
|
||||
```
|
||||
|
||||
Un config vide est omis. Les deux groupes `current` et `delinquent` sont conservés, ainsi que :
|
||||
|
||||
```text
|
||||
votePubkey
|
||||
nodePubkey
|
||||
activatedStake
|
||||
commission
|
||||
inflationRewardsCommissionBps Option<u16>
|
||||
epochVoteAccount
|
||||
epochCredits Vec<SolanaEpochCredits>
|
||||
lastVote
|
||||
rootSlot
|
||||
```
|
||||
|
||||
Aucune conversion métier staking/rewards n'est introduite dans Transport.
|
||||
|
||||
## Activation finale des helpers préparés en `pre.002`
|
||||
|
||||
Les helpers spécifiques aux cinq méthodes de cette tranche deviennent production-live maintenant qu'ils ont des consommateurs runtime :
|
||||
|
||||
- `SolanaLeaderScheduleConfig::{is_empty,to_json_value}` ;
|
||||
- `SolanaLeaderScheduleRequest::to_json_params` ;
|
||||
- `SolanaLeaderSchedule::decode_wire` ;
|
||||
- `SolanaVoteAccountsConfig::{is_empty,to_json_value}` ;
|
||||
- `SolanaVoteAccountInfo::decode_wire` ;
|
||||
- `SolanaVoteAccountStatus::decode_wire` ;
|
||||
- `WireVoteAccountInfo` et `WireVoteAccountStatus`.
|
||||
|
||||
Aucun `#[allow(dead_code)]` n'est ajouté.
|
||||
|
||||
Le helper privé d'exécution Cluster est renommé de `execute_cluster_simple_rpc` vers `execute_cluster_rpc`, car il dessert désormais les douze wrappers Cluster.
|
||||
|
||||
## Fixtures HTTP déterministes ajoutées
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_leader_schedule.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_leader_schedule.null.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leader.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leader.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leaders.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leaders.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_vote_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_vote_accounts.invalid_pubkey.json
|
||||
```
|
||||
|
||||
Les tests utilisent uniquement un serveur HTTP loopback local.
|
||||
|
||||
## Couverture de tests ajoutée
|
||||
|
||||
Les tests couvrent notamment :
|
||||
|
||||
- sérialisation slot + config de `getLeaderSchedule` ;
|
||||
- résultat nullable de `getLeaderSchedule` ;
|
||||
- sérialisation `commitment/minContextSlot` de `getSlot` et `getSlotLeader` ;
|
||||
- omission d'un `SolanaContextConfig` vide ;
|
||||
- validation de la `Pubkey` de `getSlotLeader` ;
|
||||
- conservation de l'ordre de `getSlotLeaders` ;
|
||||
- rejet local des limites `0` et `5001` avant I/O ;
|
||||
- rejet d'une identité invalide dans la liste `getSlotLeaders` ;
|
||||
- sérialisation complète de `SolanaVoteAccountsConfig` ;
|
||||
- omission d'un config vote accounts vide ;
|
||||
- conservation de `inflationRewardsCommissionBps` présent et absent ;
|
||||
- conservation des triples `epochCredits` ;
|
||||
- rejet d'une `votePubkey` invalide.
|
||||
|
||||
Le test public compile explicitement les cinq nouvelles méthodes depuis `HttpTransportPool`.
|
||||
|
||||
Un canari release fige désormais les douze méthodes Cluster exactes de `0.2.2` et vérifie `Read + RetrySafe` sur tout le sous-ensemble.
|
||||
|
||||
Après application, la cible Transport attendue devient :
|
||||
|
||||
```text
|
||||
127 unit tests
|
||||
13 public API tests
|
||||
6 release completeness tests
|
||||
```
|
||||
|
||||
## Rustdoc de crate
|
||||
|
||||
La documentation de tête de `ksp-onchain-transport-lib` est ajustée pour ne plus présenter Cluster comme une famille encore staged : après `pre.006`, les 22 wrappers `0.2.2` sont implémentés. Les familles `0.2.3` et `0.2.4` restent staged.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_leader_schedule.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_leader_schedule.null.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leader.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leader.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leaders.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leaders.invalid_pubkey.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_vote_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_vote_accounts.invalid_pubkey.json
|
||||
deltas/0.2.2/pre.006.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Documentation durable
|
||||
|
||||
Aucune modification de ROADMAP/plan/index n'est requise dans cette tranche : le plan existant décrit déjà ce découpage et `pre.007` reste la tranche de clôture prerelease/documentation.
|
||||
|
||||
`CHANGELOG.md` reste réservé à la clôture stable.
|
||||
|
||||
## Validation à exécuter sur le checkout opérateur
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
La livraison n'affirme pas que ces commandes ont été exécutées dans l'environnement de génération.
|
||||
|
||||
## Critères de validation de `pre.006`
|
||||
|
||||
`pre.006` est validable lorsque :
|
||||
|
||||
- les quatre commandes ci-dessus passent sans warning nouveau ;
|
||||
- les 127 unit tests passent ;
|
||||
- les 13 tests public API passent ;
|
||||
- les 6 tests release completeness passent ;
|
||||
- aucun helper Cluster production-live n'est `dead_code` ;
|
||||
- les douze wrappers Cluster `0.2.2` sont accessibles depuis `HttpTransportPool` ;
|
||||
- le total `0.2.2` reste exactement 5 Accounts + 5 Tokens + 12 Cluster = 22 wrappers ;
|
||||
- aucune méthode `0.2.3`/`0.2.4` n'est activée prématurément.
|
||||
162
deltas/0.2.2/pre.007-fix.001.md
Normal file
162
deltas/0.2.2/pre.007-fix.001.md
Normal file
@@ -0,0 +1,162 @@
|
||||
<!-- file: deltas/0.2.2/pre.007-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `v0.2.2-pre.007-fix.001`
|
||||
|
||||
## Base
|
||||
|
||||
Base attendue :
|
||||
|
||||
```text
|
||||
v0.2.2-pre.007
|
||||
```
|
||||
|
||||
Version Cargo de la base :
|
||||
|
||||
```text
|
||||
0.2.2-pre.7
|
||||
```
|
||||
|
||||
Ce fix modifie du Rust de test et synchronise donc le signal technique Cargo conformément à `VER-ID-007` / `VER-ID-010` :
|
||||
|
||||
```text
|
||||
0.2.2-pre.7.fix.1
|
||||
```
|
||||
|
||||
Le delta historique `deltas/0.2.2/pre.007.md` reste inchangé conformément à `KSP-REL-003` et `VER-DELTA-007`.
|
||||
|
||||
## Motif du fix
|
||||
|
||||
La validation opérateur du 2026-08-18 a révélé deux défauts du nouveau smoke Transport de `pre.007`.
|
||||
|
||||
### 1. Clippy `expect_used` hors corps de test
|
||||
|
||||
`cargo clippy --workspace --all-targets` échoue sur deux `expect()` placés dans le helper `devnet_pool()` :
|
||||
|
||||
```text
|
||||
HttpEndpointUrl::parse(...).expect(...)
|
||||
HttpTransportPool::new(...).expect(...)
|
||||
```
|
||||
|
||||
Le workspace autorise les assertions/`expect` dans les corps de tests via sa configuration Clippy, mais le helper non annoté comme test reste soumis à `expect_used = deny`.
|
||||
|
||||
Correction : `devnet_pool()` retourne désormais `ksp_core_lib::Result<HttpTransportPool>` et propage explicitement l'erreur par `match`, sans `?`, `unwrap` ni `expect` dans le helper.
|
||||
|
||||
### 2. Fixture live Token trop fragile
|
||||
|
||||
L'exécution opt-in du smoke Transport a atteint Devnet mais `getTokenSupply` a renvoyé :
|
||||
|
||||
```text
|
||||
onchain_transport.rpc_application_error
|
||||
rpc_code = -32602
|
||||
```
|
||||
|
||||
Le mint utilisé reste affiché par la documentation officielle courante, mais son état réel n'est donc pas une dépendance live suffisamment durable pour une gate de candidate KSP.
|
||||
|
||||
Correction : la branche Token du smoke utilise désormais :
|
||||
|
||||
```text
|
||||
getTokenAccountsByOwner
|
||||
owner = System Program (Pubkey valide et stable)
|
||||
selector = programId
|
||||
programId = SPL Token canonique (Tokenkeg...)
|
||||
config = absent
|
||||
```
|
||||
|
||||
Une liste vide est acceptable. Le test vérifie le contexte RPC et la traversée du wrapper Token sans dépendre de l'existence d'un mint ou d'un token account Devnet particulier.
|
||||
|
||||
La documentation officielle `getTokenAccountsByOwner` confirme que le selector accepte `programId` et que la méthode cible Devnet.
|
||||
|
||||
## Résultats opérateur de la base
|
||||
|
||||
Avant ce fix, l'opérateur a communiqué :
|
||||
|
||||
- `cargo fmt --all` : OK ;
|
||||
- `cargo check --workspace` : OK ;
|
||||
- `cargo clippy --workspace --all-targets` : **FAIL**, uniquement les deux `expect_used` ci-dessus ;
|
||||
- `cargo test -p ksp-onchain-transport-lib` : 127 unit + 13 public API + 7 release completeness, OK ; smoke ignoré par défaut ;
|
||||
- `cargo test -p ksp-config-lib` : OK ;
|
||||
- `cargo test -p ksp-core-lib` : OK ;
|
||||
- `cargo test -p ksp-app-config-desk` : OK ;
|
||||
- `cargo test --workspace` : OK ;
|
||||
- inspections `cargo tree` demandées : exécutées ;
|
||||
- smoke Config -> Transport live : OK ;
|
||||
- smoke Transport live : **FAIL** uniquement sur `getTokenSupply` avec RPC `-32602`.
|
||||
|
||||
Ces échecs empêchent `pre.007` de devenir la base de `rel.001` sans fix.
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
|
||||
crates/ksp-onchain-transport-lib/README.md
|
||||
crates/ksp-onchain-transport-lib/USAGE.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.2.2/pre.007-fix.001.md
|
||||
```
|
||||
|
||||
Aucun fichier supprimé.
|
||||
|
||||
## Inchangé
|
||||
|
||||
Ce fix ne modifie pas :
|
||||
|
||||
- les 22 wrappers `0.2.2` ;
|
||||
- les DTOs publics ;
|
||||
- les descriptors ou politiques retry ;
|
||||
- Config ;
|
||||
- les fixtures HTTP déterministes ;
|
||||
- les canaries de complétude ;
|
||||
- les dépendances/features Cargo ;
|
||||
- `ROADMAP.md` ;
|
||||
- `CHANGELOG.md` ;
|
||||
- le prompt `0.2.3` ;
|
||||
- le delta historique `pre.007.md`.
|
||||
|
||||
Le périmètre reste strictement la correction de la candidate `pre.007`.
|
||||
|
||||
## Validations exécutées pendant la préparation
|
||||
|
||||
Contrôles statiques exécutés :
|
||||
|
||||
- version Cargo `0.2.2-pre.7.fix.1` ;
|
||||
- versions de tous les fichiers modifiés incrémentées ;
|
||||
- aucun `expect()` dans `devnet_pool()` ;
|
||||
- absence du mint d'exemple `3wyAj7...` dans le smoke corrigé ;
|
||||
- présence de `getTokenAccountsByOwner` + selector `ProgramId` ;
|
||||
- aucune dépendance/feature ajoutée ;
|
||||
- payload du fix limité aux sept fichiers listés ;
|
||||
- archive réextraite et comparée au payload préparé.
|
||||
|
||||
Cargo n'est pas disponible dans l'environnement de préparation.
|
||||
|
||||
## Validations opérateur à rejouer
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test --workspace
|
||||
|
||||
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Résultat attendu :
|
||||
|
||||
```text
|
||||
clippy propre
|
||||
127 unit + 13 public API + 7 release completeness OK
|
||||
smoke Transport live OK
|
||||
smoke Config -> Transport live OK
|
||||
```
|
||||
|
||||
Si ces gates passent, `0.2.2-rel.001` peut rester strictement publicationnel comme prévu.
|
||||
88
deltas/0.2.2/pre.007-fix.002.md
Normal file
88
deltas/0.2.2/pre.007-fix.002.md
Normal file
@@ -0,0 +1,88 @@
|
||||
<!-- file: deltas/0.2.2/pre.007-fix.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.007-fix.002`
|
||||
|
||||
## Base requise
|
||||
|
||||
- `0.2.2-pre.007-fix.001` appliqué et commité ;
|
||||
- `workspace.package.version = "0.2.2-pre.7.fix.1"` avant application de ce delta.
|
||||
|
||||
## Objectif
|
||||
|
||||
Corriger le deuxième échec opérateur du smoke Devnet Transport introduit en `pre.007`, sans modifier les wrappers HTTP typés ni la surface publique `0.2.2`.
|
||||
|
||||
La passe `pre.007-fix.001` a validé `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, les tests Transport et `cargo test --workspace`. Le smoke historique `Config -> Transport` a également réussi. En revanche, le smoke Transport pur a reçu RPC `-32600` sur `getTokenAccountsByOwner` lorsqu'il utilisait `System Program` comme owner, le selector `{ programId: SPL Token }` et omettait le troisième paramètre de config.
|
||||
|
||||
## Décision
|
||||
|
||||
Le wrapper conserve son contrat existant : la config Account reste optionnelle, conformément à la surface RPC auditée et aux tests déterministes.
|
||||
|
||||
Le scénario live adopte la forme la plus conservatrice de l'exemple RPC public Devnet :
|
||||
|
||||
- owner Pubkey ordinaire de l'exemple officiel ;
|
||||
- selector `{ programId: SPL Token }` ;
|
||||
- config explicite `{ commitment: "finalized", encoding: "jsonParsed" }`.
|
||||
|
||||
Le smoke ne dépend toujours pas de l'existence d'un token account particulier : une liste vide reste un succès valide.
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `Cargo.toml`
|
||||
- version de fichier `117 -> 118` ;
|
||||
- `workspace.package.version`: `0.2.2-pre.7.fix.1 -> 0.2.2-pre.7.fix.2`.
|
||||
- `crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs`
|
||||
- version `2 -> 3` ;
|
||||
- remplace le scénario Token `System Program + selector seul` par la forme documentée `owner ordinaire + selector + finalized/jsonParsed`.
|
||||
- `crates/ksp-onchain-transport-lib/README.md`
|
||||
- version `3 -> 4` ;
|
||||
- précise la forme du smoke Token.
|
||||
- `crates/ksp-onchain-transport-lib/USAGE.md`
|
||||
- version `3 -> 4` ;
|
||||
- synchronise la commande et la sémantique du smoke Token.
|
||||
- `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`
|
||||
- version `5 -> 6` ;
|
||||
- consigne l'échec live `-32600` et la correction du scénario.
|
||||
- `docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`
|
||||
- version `2 -> 3` ;
|
||||
- synchronise la matrice de validation avec le scénario corrigé.
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
- `deltas/0.2.2/pre.007-fix.002.md`.
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Validations observées avant ce fix
|
||||
|
||||
Réussies sur le checkout opérateur `0.2.2-pre.7.fix.1` :
|
||||
|
||||
- `cargo fmt --all` ;
|
||||
- `cargo check --workspace` ;
|
||||
- `cargo clippy --workspace --all-targets` ;
|
||||
- `cargo test -p ksp-onchain-transport-lib` : `127 + 13 + 7`, smoke live ignoré par défaut ;
|
||||
- `cargo test --workspace` : succès ;
|
||||
- `cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture` : succès.
|
||||
|
||||
Échec observé :
|
||||
|
||||
- `cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture` ;
|
||||
- `getTokenAccountsByOwner` a reçu `onchain_transport.rpc_application_error`, `rpc_code = -32600`.
|
||||
|
||||
## Validations à exécuter après application
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test --workspace
|
||||
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question d'architecture nouvelle. Si la forme RPC publique documentée échoue encore sur Devnet, le prochain diagnostic devra capturer/Comparer le wire live ou considérer explicitement une limitation du RPC public, plutôt que remplacer une troisième fois arbitrairement le scénario Token.
|
||||
248
deltas/0.2.2/pre.007.md
Normal file
248
deltas/0.2.2/pre.007.md
Normal file
@@ -0,0 +1,248 @@
|
||||
<!-- file: deltas/0.2.2/pre.007.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `v0.2.2-pre.007`
|
||||
|
||||
## Base
|
||||
|
||||
Base attendue :
|
||||
|
||||
```text
|
||||
v0.2.2-pre.006-fix.001
|
||||
```
|
||||
|
||||
Cette base a été validée localement par l'opérateur le 2026-08-18 avec :
|
||||
|
||||
- `cargo fmt --all` ;
|
||||
- `cargo check --workspace` ;
|
||||
- `cargo clippy --workspace --all-targets` ;
|
||||
- `cargo test -p ksp-onchain-transport-lib`.
|
||||
|
||||
Résultats Transport communiqués :
|
||||
|
||||
```text
|
||||
127 unit tests passed
|
||||
13 public API tests passed
|
||||
6 release completeness tests passed
|
||||
0 warning clippy signalé
|
||||
```
|
||||
|
||||
Version Cargo cible :
|
||||
|
||||
```text
|
||||
0.2.2-pre.7
|
||||
```
|
||||
|
||||
## Objectif
|
||||
|
||||
Fermer la dernière prerelease fonctionnelle de `0.2.2` sans ajouter de nouvelle méthode HTTP :
|
||||
|
||||
- revérifier l'inventaire officiel Solana ;
|
||||
- figer la complétude exacte des 22 wrappers `0.2.2` ;
|
||||
- ajouter un smoke Devnet opt-in **Transport pur** avec settings programmatiques ;
|
||||
- conserver séparément le smoke historique de composition Config -> Transport ;
|
||||
- synchroniser README/USAGE et la documentation d'architecture/planification ;
|
||||
- produire la matrice finale `0.2.2` ;
|
||||
- produire le prompt de démarrage `0.2.3 — HTTP Transactions` ;
|
||||
- préparer un `rel.001` strictement publicationnel.
|
||||
|
||||
Aucun 23e wrapper `0.2.2` et aucune méthode Transactions n'est introduit dans cette tranche.
|
||||
|
||||
## Réaudit officiel final
|
||||
|
||||
La documentation officielle Solana a été revérifiée le 2026-08-18 :
|
||||
|
||||
```text
|
||||
https://solana.com/docs/rpc/http
|
||||
https://solana.com/docs/rpc/deprecated/confirmtransaction
|
||||
```
|
||||
|
||||
L'index courant contient toujours :
|
||||
|
||||
```text
|
||||
52 méthodes HTTP courantes
|
||||
14 méthodes historiques Deprecated
|
||||
```
|
||||
|
||||
Les familles restent :
|
||||
|
||||
```text
|
||||
Accounts : 6 current
|
||||
Tokens : 5 current
|
||||
Cluster : 15 current
|
||||
```
|
||||
|
||||
En retranchant les quatre canaris `0.2.1` (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`), la partition `0.2.2` reste exactement `5 + 5 + 12 = 22`. Aucun recalibrage n'est nécessaire.
|
||||
|
||||
## Canaries de complétude
|
||||
|
||||
`tests/release_completeness.rs` gagne un canary final qui fige depuis l'API publique :
|
||||
|
||||
```text
|
||||
V0_2_2 exact set == 22 méthodes du plan 009
|
||||
22 => Stable + Supported + Read + RetrySafe
|
||||
V0_2_3 count == 11
|
||||
V0_2_4 count == 15
|
||||
```
|
||||
|
||||
Les canaries antérieures `52 + 14`, `4 / 22 / 11 / 15`, foundation exacte, Accounts, Tokens et Cluster restent inchangées.
|
||||
|
||||
La candidate attend donc :
|
||||
|
||||
```text
|
||||
127 tests unitaires Transport
|
||||
13 tests public API
|
||||
7 tests release completeness
|
||||
1 smoke Devnet Transport ignored par défaut
|
||||
```
|
||||
|
||||
## Smoke Devnet Transport pur
|
||||
|
||||
Nouveau test :
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
|
||||
```
|
||||
|
||||
Contrairement au smoke historique de `0.2.1`, il ne dépend pas de Config :
|
||||
|
||||
```text
|
||||
settings programmatiques
|
||||
-> https://api.devnet.solana.com
|
||||
-> HttpTransportPool
|
||||
-> getAccountInfo(System Program)
|
||||
-> getTokenSupply(mint de l'exemple Devnet officiel courant)
|
||||
-> getEpochInfo
|
||||
-> getVoteAccounts
|
||||
```
|
||||
|
||||
Exécution opt-in :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Ce smoke appartient durablement à Transport parce qu'il ne valide que Transport. Il reste `ignored` car il dépend d'un réseau externe et d'un état Devnet.
|
||||
|
||||
Le smoke `crates/ksp-config-lib/tests/transport_devnet_smoke.rs` reste séparé et valide uniquement la composition historique :
|
||||
|
||||
```text
|
||||
Config -> Transport -> quatre canaris foundation
|
||||
```
|
||||
|
||||
Il reste transitoire et devra migrer vers une future surface d'intégration/orchestration/demo ; Config ne devient pas la destination générale des smokes cross-crates.
|
||||
|
||||
## Documentation durable
|
||||
|
||||
Mises à jour :
|
||||
|
||||
- README Transport : 26 wrappers typed candidats, répartition 4 + 5 + 5 + 12 et ownership des deux smokes ;
|
||||
- USAGE Transport : exemples typed `0.2.2` et commandes des deux smokes ;
|
||||
- inventaire composant : état candidate `0.2.2` ;
|
||||
- séquence fonctionnelle : `pre.002`–`pre.006` exécutées et `pre.007` candidate ;
|
||||
- plan `009` : état candidate, réaudit final, smoke et préparation stable ;
|
||||
- ROADMAP : `0.2.2` passe à `[/]`, pas à `[X]` avant stable ;
|
||||
- index docs/plans/validation/prompts synchronisés.
|
||||
|
||||
Nouveaux documents :
|
||||
|
||||
```text
|
||||
docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md
|
||||
prompts/008-V0_2_3_START_PROMPT.md
|
||||
```
|
||||
|
||||
`CHANGELOG.md` n'est volontairement pas modifié dans cette prerelease.
|
||||
|
||||
## Prompt `0.2.3`
|
||||
|
||||
Le prompt suivant cible exactement les 11 méthodes Transactions actuellement affectées à `V0_2_3` :
|
||||
|
||||
```text
|
||||
8 Read / RetrySafe
|
||||
2 WriteSubmission / NeverAfterDispatch
|
||||
1 Simulation / RetrySafe
|
||||
```
|
||||
|
||||
Il impose un nouveau `pre.001` d'audit/sizing et met au premier plan la règle no-resend de `requestAirdrop` et `sendTransaction`.
|
||||
|
||||
## Préparation de `rel.001`
|
||||
|
||||
Si la candidate passe les validations opérateur, `rel.001` doit rester publicationnel :
|
||||
|
||||
```text
|
||||
workspace.package.version -> 0.2.2
|
||||
ROADMAP : 0.2.2 -> [X]
|
||||
CHANGELOG : synthèse stable 0.2.2
|
||||
plan 009 / validation 004 : clôture + preuves opérateur
|
||||
deltas/0.2.2/rel.001.md
|
||||
commit v0.2.2-rel.001
|
||||
tag v0.2.2 après validation du commit
|
||||
```
|
||||
|
||||
Aucun développement fonctionnel nouveau ne doit entrer dans `rel.001`.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
|
||||
docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md
|
||||
prompts/008-V0_2_3_START_PROMPT.md
|
||||
deltas/0.2.2/pre.007.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
ROADMAP.md
|
||||
crates/ksp-onchain-transport-lib/README.md
|
||||
crates/ksp-onchain-transport-lib/USAGE.md
|
||||
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
docs/000-README.md
|
||||
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
docs/validation/000-README.md
|
||||
prompts/000-README.md
|
||||
```
|
||||
|
||||
Aucun ancien delta et aucun `CHANGELOG.md` ne sont modifiés.
|
||||
|
||||
## Validations exécutées dans la préparation de livraison
|
||||
|
||||
Contrôles statiques exécutés :
|
||||
|
||||
- version Cargo `0.2.2-pre.7` ;
|
||||
- présence exacte des 22 noms dans le canary final ;
|
||||
- fixture live absente des suites par défaut via `#[ignore]` ;
|
||||
- smoke Transport sans dépendance Config ni lecture d'environnement ;
|
||||
- index docs/plans/validation/prompts synchronisés ;
|
||||
- versions de fichiers modifiés incrémentées ;
|
||||
- aucun ajout de dépendance/feature Cargo ;
|
||||
- archive réextraite et comparée au payload préparé.
|
||||
|
||||
Cargo n'est pas disponible dans l'environnement de préparation. Les validations suivantes restent donc à exécuter par l'opérateur sur le dépôt canonique :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo test -p ksp-core-lib
|
||||
cargo test -p ksp-app-config-desk
|
||||
cargo test --workspace
|
||||
|
||||
cargo tree -p ksp-onchain-transport-lib
|
||||
cargo tree -p ksp-onchain-transport-lib -d
|
||||
cargo tree -p ksp-onchain-transport-lib -e features
|
||||
cargo tree -p ksp-onchain-transport-lib -e normal
|
||||
cargo tree -p ksp-config-lib
|
||||
cargo tree -p ksp-config-lib -d
|
||||
cargo tree -p ksp-config-lib -e features
|
||||
cargo tree -p ksp-config-lib -e normal
|
||||
|
||||
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
154
deltas/0.2.2/rel.001.md
Normal file
154
deltas/0.2.2/rel.001.md
Normal file
@@ -0,0 +1,154 @@
|
||||
<!-- file: deltas/0.2.2/rel.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `v0.2.2-rel.001`
|
||||
|
||||
## Base
|
||||
|
||||
Base attendue :
|
||||
|
||||
```text
|
||||
v0.2.2-pre.007-fix.002
|
||||
```
|
||||
|
||||
Version Cargo cible :
|
||||
|
||||
```text
|
||||
0.2.2
|
||||
```
|
||||
|
||||
La candidate corrigée a été validée localement par l'opérateur le 2026-08-18 avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test -p ksp-onchain-transport-lib`, `cargo test --workspace` et les deux smokes Devnet opt-in.
|
||||
|
||||
## Objectif
|
||||
|
||||
Publier `0.2.2 — HTTP Accounts + Tokens + Cluster` sans ajouter de capacité fonctionnelle après `pre.007-fix.002`.
|
||||
|
||||
`rel.001` est strictement publicationnelle :
|
||||
|
||||
- passage du workspace à la version stable `0.2.2` ;
|
||||
- clôture `[X]` de `0.2.2` dans le ROADMAP ;
|
||||
- ajout de l'entrée stable `0.2.2` au CHANGELOG ;
|
||||
- clôture du plan `009` et de la matrice `validation/004` avec les preuves opérateur ;
|
||||
- synchronisation des index, de l'inventaire composant et des README/USAGE Transport vers le statut stable ;
|
||||
- conservation du prompt `prompts/008-V0_2_3_START_PROMPT.md` comme point d'ouverture de `0.2.3`.
|
||||
|
||||
Aucun fichier Rust de production, aucune API publique, aucune configuration runtime, aucune dépendance et aucune feature Cargo ne changent dans cette livraison.
|
||||
|
||||
## Surface stable publiée
|
||||
|
||||
`0.2.2` ajoute exactement 22 wrappers typés :
|
||||
|
||||
```text
|
||||
Accounts : 5
|
||||
Tokens : 5
|
||||
Cluster : 12
|
||||
```
|
||||
|
||||
Avec les quatre wrappers foundation de `0.2.1`, la surface typée HTTP atteint :
|
||||
|
||||
```text
|
||||
26 wrappers typed
|
||||
52 méthodes HTTP courantes enregistrées
|
||||
14 méthodes historiques Deprecated / Removed
|
||||
partition restante : 11 Transactions + 15 Blocks/Economics
|
||||
```
|
||||
|
||||
Les 22 descriptors `0.2.2` restent `Read / RetrySafe` et utilisent tous le flux central `descriptor -> execute_standard_rpc -> pool/admission -> executor HTTP`.
|
||||
|
||||
Contrats stabilisés notamment : Account legacy/encoded/`jsonParsed`, comptes absents `null`, Program Accounts bare/contextualisés, Token selector `Mint | ProgramId`, `TokenAmount.uiAmount` nullable, `ClusterNode.clientId` optionnel, epoch/snapshot nullable, leader schedule nullable et Vote Accounts avec `inflationRewardsCommissionBps` optionnel.
|
||||
|
||||
Limites locales publiées :
|
||||
|
||||
```text
|
||||
getMultipleAccounts <= 100
|
||||
getProgramAccounts <= 4 filtres
|
||||
memcmp raw <= 128 octets
|
||||
getSlotLeaders 1..=5000
|
||||
```
|
||||
|
||||
## Validation de la candidate
|
||||
|
||||
La candidate `0.2.2-pre.007-fix.002` a confirmé :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK, sans warning
|
||||
cargo test -p ksp-onchain-transport-lib OK
|
||||
cargo test --workspace OK
|
||||
```
|
||||
|
||||
Transport :
|
||||
|
||||
```text
|
||||
127 unit
|
||||
13 public API
|
||||
7 release completeness
|
||||
1 smoke live ignored par défaut
|
||||
```
|
||||
|
||||
Les deux smokes ont ensuite été exécutés explicitement :
|
||||
|
||||
```text
|
||||
Transport pur : settings programmatiques -> Accounts/Tokens/Cluster 1 passed
|
||||
Config -> Transport -> foundation 1 passed
|
||||
```
|
||||
|
||||
Le smoke Transport utilise `getAccountInfo`, `getTokenAccountsByOwner` sous sa forme Devnet documentée complète, `getEpochInfo` et `getVoteAccounts`. Il appartient à Transport parce qu'il construit ses settings programmatiquement. Le smoke Config -> Transport reste une exception transitoire cross-crates et doit migrer vers une future surface d'intégration/orchestration/demo lorsqu'elle existera.
|
||||
|
||||
Les graphes Cargo Transport/Config ont été inspectés pendant la candidate. Les fixes du smoke n'ont ajouté ni dépendance ni feature. La frontière reste :
|
||||
|
||||
```text
|
||||
ksp-config-lib -> ksp-onchain-transport-lib
|
||||
ksp-onchain-transport-lib -X-> ksp-config-lib
|
||||
Transport -X-> Store/Program/tracing direct
|
||||
```
|
||||
|
||||
Les doublons observés restent limités à `syn` 2.x/3.x dans les chaînes transitive/proc-macro ; aucune seconde stack HTTP/Tokio KSP n'est introduite.
|
||||
|
||||
## Documentation de clôture
|
||||
|
||||
Mises à jour :
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
crates/ksp-onchain-transport-lib/README.md
|
||||
crates/ksp-onchain-transport-lib/USAGE.md
|
||||
docs/000-README.md
|
||||
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||
docs/plans/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
docs/validation/000-README.md
|
||||
docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md
|
||||
```
|
||||
|
||||
Le prompt `prompts/008-V0_2_3_START_PROMPT.md` était déjà livré en `pre.007` et reste inchangé ; il attend comme base le tag stable `v0.2.2`.
|
||||
|
||||
## Commit et tag
|
||||
|
||||
Identifiant de commit attendu :
|
||||
|
||||
```text
|
||||
v0.2.2-rel.001
|
||||
```
|
||||
|
||||
Le tag stable ne doit être créé qu'après application et validation de ce delta :
|
||||
|
||||
```text
|
||||
v0.2.2
|
||||
```
|
||||
|
||||
## Validation finale après application
|
||||
|
||||
Comme `rel.001` ne modifie aucun code de production et ne change que le signal de version et la documentation, la gate finale reste :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Après succès : commit `v0.2.2-rel.001`, puis création du tag `v0.2.2`.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/000-README.md -->
|
||||
<!-- version: 24 -->
|
||||
<!-- version: 27 -->
|
||||
|
||||
# Documentation KSP
|
||||
|
||||
@@ -40,7 +40,8 @@ docs/
|
||||
│ ├── 005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
|
||||
│ ├── 006-V0_1_4_CONFIG_DESKTOP_PLAN.md
|
||||
│ ├── 007-V0_2_0_SERIES_PLANNING.md
|
||||
│ └── 008-V0_2_1_ONCHAIN_HTTP_PLAN.md
|
||||
│ ├── 008-V0_2_1_ONCHAIN_HTTP_PLAN.md
|
||||
│ └── 009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
├── validation/
|
||||
│ ├── 000-README.md
|
||||
│ ├── 001-V0_1_4_CONFIG_DESKTOP.md
|
||||
@@ -62,7 +63,7 @@ D'autres sous-répertoires seront ajoutés uniquement lorsque leur rôle aura é
|
||||
|
||||
## Documents de planification
|
||||
|
||||
Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.2` est conservé comme historique clôturé dans [`plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.3 — Configuration foundation` est conservé comme historique clôturé dans [`plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.4 — ksp-app-config-desk` est conservé comme historique clôturé dans [`plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md`](plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md), avec sa matrice finale [`validation/001-V0_1_4_CONFIG_DESKTOP.md`](validation/001-V0_1_4_CONFIG_DESKTOP.md). Son prompt d'ouverture historique reste [`../prompts/004-V0_1_4_START_PROMPT.md`](../prompts/004-V0_1_4_START_PROMPT.md). La release stable `0.2.0` clôt l'audit de bot3 et le découpage de la série. Son plan directeur est conservé comme historique clôturé dans [`plans/007-V0_2_0_SERIES_PLANNING.md`](plans/007-V0_2_0_SERIES_PLANNING.md), avec sa matrice finale [`validation/002-V0_2_0_SERIES_PLANNING.md`](validation/002-V0_2_0_SERIES_PLANNING.md). La release stable `0.2.1 — HTTP Solana foundation` a été ouverte par [`../prompts/006-V0_2_1_START_PROMPT.md`](../prompts/006-V0_2_1_START_PROMPT.md). Son gate de sizing et sa matrice exhaustive sont conservés dans [`plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md), avec la validation finale [`validation/003-V0_2_1_ONCHAIN_HTTP.md`](validation/003-V0_2_1_ONCHAIN_HTTP.md), README/USAGE Transport et le smoke Devnet opt-in de composition Config -> Transport. Le prompt [`../prompts/007-V0_2_2_START_PROMPT.md`](../prompts/007-V0_2_2_START_PROMPT.md) devient le point d'entrée de `0.2.2`.
|
||||
Le plan historique de la phase fondatrice clôturée est conservé dans [`plans/001-V0_0_3_PLAN.md`](plans/001-V0_0_3_PLAN.md). La séquence active des premières releases fonctionnelles est définie dans [`plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md`](plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md). Le plan détaillé de la release stable `0.1.1` est conservé comme historique clôturé dans [`plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md`](plans/003-V0_1_1_CORE_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.2` est conservé comme historique clôturé dans [`plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md`](plans/004-V0_1_2_LOGGING_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.3 — Configuration foundation` est conservé comme historique clôturé dans [`plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md). Le plan détaillé de la release stable `0.1.4 — ksp-app-config-desk` est conservé comme historique clôturé dans [`plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md`](plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md), avec sa matrice finale [`validation/001-V0_1_4_CONFIG_DESKTOP.md`](validation/001-V0_1_4_CONFIG_DESKTOP.md). Son prompt d'ouverture historique reste [`../prompts/004-V0_1_4_START_PROMPT.md`](../prompts/004-V0_1_4_START_PROMPT.md). La release stable `0.2.0` clôt l'audit de bot3 et le découpage de la série. Son plan directeur est conservé comme historique clôturé dans [`plans/007-V0_2_0_SERIES_PLANNING.md`](plans/007-V0_2_0_SERIES_PLANNING.md), avec sa matrice finale [`validation/002-V0_2_0_SERIES_PLANNING.md`](validation/002-V0_2_0_SERIES_PLANNING.md). La release stable `0.2.1 — HTTP Solana foundation` a été ouverte par [`../prompts/006-V0_2_1_START_PROMPT.md`](../prompts/006-V0_2_1_START_PROMPT.md). Son gate de sizing et sa matrice exhaustive sont conservés dans [`plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md), avec la validation finale [`validation/003-V0_2_1_ONCHAIN_HTTP.md`](validation/003-V0_2_1_ONCHAIN_HTTP.md), README/USAGE Transport et le smoke Devnet opt-in de composition Config -> Transport. Le prompt [`../prompts/007-V0_2_2_START_PROMPT.md`](../prompts/007-V0_2_2_START_PROMPT.md) a ouvert la release stable `0.2.2 — HTTP Accounts + Tokens + Cluster`. Son plan clôturé [`plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) conserve l'audit et l'implémentation des 22 wrappers typés, tandis que [`validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) enregistre les validations déterministes, les graphes Cargo et les deux smokes Devnet passés avant publication. Le prompt [`../prompts/008-V0_2_3_START_PROMPT.md`](../prompts/008-V0_2_3_START_PROMPT.md) ouvre la release suivante `0.2.3 — HTTP Transactions`.
|
||||
|
||||
`IDEAS.md` conserve les pistes et questions qui ne sont pas encore des engagements du roadmap ni des décisions architecturales.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/architecture/004-COMPONENT_INVENTORY.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# Inventaire initial des composants KSP
|
||||
|
||||
@@ -23,7 +23,7 @@ Ce document maintient l'inventaire synthétique des composants retenus ou presse
|
||||
| Logging | `ksp-logging-lib` | lib | Stable | `0.1.2` | façade unique tracing KSP |
|
||||
| Config | `ksp-config-lib` | lib | Stable | `0.1.3` | documents, profils, env et persistence Config |
|
||||
| Config Desk | `ksp-app-config-desk` | app | Stable | `0.1.4` | validation/management Config |
|
||||
| On-chain HTTP | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.1` | foundation HTTP, registry 52+14, pools/rôles, 4 canaris |
|
||||
| On-chain HTTP | `ksp-onchain-transport-lib` | lib | Stable | `0.2.1` | foundation HTTP + 26 wrappers typés stables après 0.2.2 |
|
||||
| Wallet | `ksp-wallet-lib` | lib | Retenu | `0.2.5` | `.kspwallet`, secrets, signature, import/export |
|
||||
| Wallet Desk | `ksp-app-wallet-desk` | app | Retenu | `0.2.6` | Wallet + Config composite + HTTP/balance |
|
||||
| Standard WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.7` | WebSocket Solana complet, sessions/subscriptions |
|
||||
@@ -78,7 +78,7 @@ ksp-data-api
|
||||
|
||||
## Transport
|
||||
|
||||
`ksp-onchain-transport-lib` doit couvrir l'intégralité des opérations documentées de la surface ciblée par chaque release. `0.2.1` stabilise la foundation HTTP et quatre wrappers typés canari ; la couverture typée des 48 autres méthodes courantes reste explicitement répartie sur `0.2.2`–`0.2.4`. Les statuts deprecated/obsolete encore fonctionnels et unstable/experimental restent exposés avec warning runtime KSP.
|
||||
`ksp-onchain-transport-lib` doit couvrir l'intégralité des opérations documentées de la surface ciblée par chaque release. `0.2.1` stabilise la foundation HTTP et quatre wrappers typés canari. `0.2.2` stabilise 22 wrappers Accounts/Tokens/Cluster supplémentaires, soit 26 wrappers typés au total ; les 26 méthodes Transactions/Blocks/Economics restantes restent affectées à `0.2.3`–`0.2.4`. Les statuts deprecated/obsolete encore fonctionnels et unstable/experimental restent exposés avec warning runtime KSP.
|
||||
|
||||
La Config standard Transport appartient à `ksp-config-lib`, qui adapte vers les settings publics du transport ; le transport ne dépend jamais de Config.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 31 -->
|
||||
<!-- version: 34 -->
|
||||
|
||||
# Plans KSP
|
||||
|
||||
@@ -17,6 +17,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
|
||||
- [`006-V0_1_4_CONFIG_DESKTOP_PLAN.md`](006-V0_1_4_CONFIG_DESKTOP_PLAN.md) — plan historique clôturé de la release stable `0.1.4 — ksp-app-config-desk`, établi par `0.1.4-pre.001` puis consolidé jusqu'à `0.1.4-rel.001`.
|
||||
- [`007-V0_2_0_SERIES_PLANNING.md`](007-V0_2_0_SERIES_PLANNING.md) — plan historique clôturé de la release stable `0.2.0`, ouvert par `pre.001`, consolidé par `pre.002`, audité par `pre.003` puis publié par `rel.001`; il fixe l'ordre `0.2.1+`, la stratégie RAW/CORE/DECODE/SPECIALIZED, les vertical slices Program et le prompt `0.2.1`.
|
||||
- [`008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](008-V0_2_1_ONCHAIN_HTTP_PLAN.md) — plan de `0.2.1`, établi par `0.2.1-pre.001`, recalibré par `pre.001-fix.001` et amené en clôture candidate par `pre.007`; il conserve l'inventaire 52 méthodes HTTP courantes + 14 Deprecated historiques, le design Transport/Config et le split de couverture typée sur `0.2.1`–`0.2.4`.
|
||||
- [`009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) — plan clôturé de la release stable `0.2.2`, établi par `pre.001`, corrigé après réaudit Agave v4.2.1 puis exécuté jusqu'à `pre.007-fix.002`; il couvre les 22 wrappers Accounts/Tokens/Cluster, le smoke Transport opt-in et la préparation de `0.2.3`.
|
||||
|
||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||
<!-- version: 33 -->
|
||||
<!-- version: 36 -->
|
||||
|
||||
# Séquence des releases fonctionnelles KSP
|
||||
|
||||
@@ -390,6 +390,8 @@ Ces trois releases constituent le découpage nominal, pas une obligation de troi
|
||||
|
||||
Chaque `pre.001` réaudite la documentation officielle actuelle. Les méthodes Deprecated réellement retirées restent tracées comme historiques/runtime removed au lieu d'être simulées.
|
||||
|
||||
`0.2.2-pre.001` a confirmé la partition de 22 méthodes et son fix documentaire a recoupé les formes wire avec Agave v4.2.1. Les tranches `pre.002`–`pre.006` ont livré les DTOs puis les 5 Accounts, 5 Tokens et 12 Cluster. `pre.007`, puis `pre.007-fix.001` et `pre.007-fix.002`, ont fermé les canaries exactes 22/22, le smoke Devnet Transport pur, README/USAGE, la matrice de validation `004` et le prompt `0.2.3`. `0.2.2-rel.001` publie cette surface stable après validation du workspace et des deux smokes Devnet. Le smoke cross-crates Config -> Transport de `0.2.1` reste séparé et transitoire.
|
||||
|
||||
## `0.2.5` — Wallet foundation
|
||||
|
||||
Mission : créer `ksp-wallet-lib` et le format `.kspwallet`.
|
||||
|
||||
576
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
Normal file
576
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
Normal file
@@ -0,0 +1,576 @@
|
||||
<!-- file: docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# Plan `0.2.2` — HTTP Accounts + Tokens + Cluster
|
||||
|
||||
## Statut
|
||||
|
||||
Ce plan, ouvert par `0.2.2-pre.001` sur la base stable `v0.2.1`, est clôturé par `0.2.2-rel.001` après validation de la candidate `0.2.2-pre.007-fix.002`.
|
||||
|
||||
`0.2.1` a déjà stabilisé la foundation HTTP commune : settings, endpoints/pool/rôles, admission et limites, retry, JSON-RPC 2.0,
|
||||
registry des méthodes, exécution HTTP générique, adapter Config -> Transport et quatre wrappers typés canari.
|
||||
|
||||
Cette release ne reconstruit aucun transport par famille. Elle complète uniquement la surface typée des 22 méthodes déjà attribuées à
|
||||
`HttpRpcCoverageRelease::V0_2_2`.
|
||||
|
||||
## Sources normatives réauditées les 2026-08-17 et 2026-08-18
|
||||
|
||||
Source documentaire principale :
|
||||
|
||||
```text
|
||||
https://solana.com/docs/rpc/http
|
||||
```
|
||||
|
||||
Références complémentaires officielles utilisées pour les formes wire communes :
|
||||
|
||||
```text
|
||||
https://solana.com/docs/rpc/json-structures
|
||||
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
|
||||
```
|
||||
|
||||
Les liens `Source` actuellement exposés par les pages RPC Solana consultées pointent encore vers Agave `v3.1.8`. Ce constat documentaire ne
|
||||
signifie pas que `v3.1.8` soit la source Agave la plus récente à auditer. Le 2026-08-18, KSP recoupe donc ces pages avec le tag Agave `v4.2.1`,
|
||||
dont le workspace déclare lui-même la version `4.2.1`, afin de détecter les évolutions wire plus récentes que les liens du site Solana.
|
||||
|
||||
KSP ne prend pas de dépendance sur ces crates : leurs sources servent seulement à lever les ambiguïtés de la documentation HTTP et à préserver
|
||||
les champs optionnels/versionnés réellement exposés par Agave `v4.2.1`, plus récent que le snapshot `v3.1.8` encore lié par le site Solana.
|
||||
|
||||
## Résultat de l'audit global
|
||||
|
||||
L'index HTTP officiel courant contient toujours **52 méthodes**. Pour les familles concernées par cette release :
|
||||
|
||||
```text
|
||||
Accounts : 6 current = 1 acquis en 0.2.1 + 5 ciblés en 0.2.2
|
||||
Tokens : 5 current = 5 ciblés en 0.2.2
|
||||
Cluster : 15 current = 3 acquis en 0.2.1 + 12 ciblés en 0.2.2
|
||||
```
|
||||
|
||||
La navigation officielle Deprecated contient toujours exactement les **14 méthodes historiques** déjà enregistrées dans KSP :
|
||||
|
||||
```text
|
||||
confirmTransaction
|
||||
getConfirmedBlock
|
||||
getConfirmedBlocks
|
||||
getConfirmedBlocksWithLimit
|
||||
getConfirmedSignaturesForAddress2
|
||||
getConfirmedTransaction
|
||||
getFeeCalculatorForBlockhash
|
||||
getFeeRateGovernor
|
||||
getFees
|
||||
getRecentBlockhash
|
||||
getSignatureConfirmation
|
||||
getSignatureStatus
|
||||
getSnapshotSlot
|
||||
getStakeActivation
|
||||
```
|
||||
|
||||
Aucune méthode ajoutée, supprimée ou déplacée n'a été détectée par rapport à l'audit `0.2.1` du 2026-08-17. Les 22 méthodes de cette release
|
||||
restent donc la partition exacte prévue. Les pages courantes ciblées ne portent pas de marqueur Deprecated ou Unstable ; les descriptors KSP
|
||||
restent `Stable / Supported / Stable`, `Read`, `RetrySafe`.
|
||||
|
||||
## Gate de sizing obligatoire
|
||||
|
||||
Question :
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
Le périmètre ne nécessite ni nouvelle foundation de transport, ni nouvelle crate, ni nouvelle dépendance externe identifiée à `pre.001`.
|
||||
Les difficultés sont concentrées dans quelques contrats wire, principalement les données de compte, `getProgramAccounts`, `getLeaderSchedule`
|
||||
et `getVoteAccounts`. Elles peuvent être isolées dans les prereleases prévues sans compresser les tests.
|
||||
|
||||
Le sizing reste conditionnel à la règle KSP habituelle : si une tranche dépasse réellement le budget de 15–20 minutes, une prerelease
|
||||
supplémentaire est ajoutée dans `0.2.2`; la release n'est pas artificiellement comprimée et aucune méthode n'est déplacée silencieusement.
|
||||
|
||||
## Matrice exacte — Accounts
|
||||
|
||||
| Méthode | Paramètres/config confirmés | Résultat typé à préserver | Limites / points significatifs |
|
||||
|-------------------------------------|-----------------------------------------------------------------|-------------------------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| `getAccountInfo` | `Pubkey`, config `commitment/encoding/dataSlice/minContextSlot` | `RpcResponse<Option<Account>>` | compte absent = `null`; erreur min-context distincte |
|
||||
| `getLargestAccounts` | config `commitment/filter/sortResults` | `RpcResponse<Vec<AccountBalance>>` | 20 résultats; filtre `circulating/nonCirculating`; cache provider possible |
|
||||
| `getMinimumBalanceForRentExemption` | longueur de données, `commitment?` | `u64` lamports | aucune valeur sentinelle inventée |
|
||||
| `getMultipleAccounts` | `Vec<Pubkey>`, config Account | `RpcResponse<Vec<Option<Account>>>` | maximum documenté 100; ordre des résultats = ordre demandé |
|
||||
| `getProgramAccounts` | `Pubkey`, config Account + `filters/withContext/sortResults` | résultat bare ou contextualisé | HTTP documente `dataSize`/`memcmp`; Agave accepte aussi `tokenAccountState`; 4 filtres max côté serveur |
|
||||
|
||||
### Wire Account commun
|
||||
|
||||
Le type `Account` KSP doit rester un DTO de transport, sans décodage Program. Les formes documentées imposent de conserver :
|
||||
|
||||
```text
|
||||
lamports u64
|
||||
owner Pubkey
|
||||
executable bool
|
||||
rentEpoch u64
|
||||
space Option<u64>
|
||||
data forme wire discriminée
|
||||
```
|
||||
|
||||
`data` n'est pas modélisable correctement par un simple `String`. Les formes à conserver sont :
|
||||
|
||||
- chaîne legacy `binary` lorsque le serveur l'émet ;
|
||||
- couple `[data, encoding]` pour `base58`, `base64` et `base64+zstd` ;
|
||||
- objet `jsonParsed` lorsque le parser RPC existe ;
|
||||
- fallback `[data, "base64"]` même lorsqu'un appel demande `jsonParsed` mais qu'aucun parser n'est disponible.
|
||||
|
||||
La valeur `jsonParsed.parsed` reste un `serde_json::Value` au niveau Transport. Aucun modèle SPL/Program n'est introduit ici.
|
||||
|
||||
`dataSlice` est un petit DTO `{ offset, length }`. Il n'impose aucun décodage local des données et ne justifie donc pas l'ajout de `base64`
|
||||
ou `bs58` dans cette release.
|
||||
|
||||
### `getProgramAccounts`
|
||||
|
||||
Le résultat dépend de `withContext` :
|
||||
|
||||
```text
|
||||
false/absent -> Vec<KeyedAccount>
|
||||
true -> RpcResponse<Vec<KeyedAccount>>
|
||||
```
|
||||
|
||||
Le contrat public doit refléter cette union au lieu de supprimer le contexte ou d'inventer un contexte lorsque le serveur n'en renvoie pas.
|
||||
|
||||
Les filtres KSP doivent distinguer les trois variantes acceptées par la source primaire Agave `v4.2.1` :
|
||||
|
||||
```text
|
||||
DataSize(u64)
|
||||
Memcmp { offset, bytes, encoding }
|
||||
TokenAccountState
|
||||
```
|
||||
|
||||
Pour `Memcmp`, `base58` est l'encodage implicite lorsque `encoding` est absent; `base58`, `base64` et les octets bruts sont acceptés par le wire
|
||||
courant, tandis que l'ancien libellé `binary` n'est plus accepté. La donnée comparée est limitée à 128 octets après décodage. Agave expose en
|
||||
outre `MAX_GET_PROGRAM_ACCOUNT_FILTERS = 4`, même si la page HTTP actuelle ne publie pas cette cardinalité. Le plan retient donc **4 filtres max
|
||||
comme limite runtime/source primaire à préserver**, en la distinguant explicitement d'une limite publiée sur la page HTTP.
|
||||
|
||||
Cette connaissance n'impose pas l'ajout immédiat de `base64` ou `bs58` à KSP : le DTO peut préserver les chaînes encodées sans les décoder.
|
||||
Une validation locale de la taille décodée ne sera ajoutée que si elle apporte une valeur claire et justifie la dépendance; le wrapper doit en
|
||||
revanche empêcher plus de quatre filtres, car cette requête serait refusée par le serveur actuel.
|
||||
|
||||
## Matrice exacte — Tokens
|
||||
|
||||
| Méthode | Paramètres/config confirmés | Résultat typé à préserver | Limites / points significatifs |
|
||||
|------------------------------|-----------------------------------------------------------------------|-----------------------------------------|-------------------------------------------------------------------|
|
||||
| `getTokenAccountBalance` | token account `Pubkey`, `commitment?` | `RpcResponse<TokenAmount>` | erreur RPC si le compte n'est pas exploitable comme token account |
|
||||
| `getTokenAccountsByDelegate` | delegate `Pubkey`, selector `{mint}` ou `{programId}`, config Account | `RpcResponse<Vec<KeyedAccount>>` | selector exclusif par construction |
|
||||
| `getTokenAccountsByOwner` | owner `Pubkey`, selector `{mint}` ou `{programId}`, config Account | `RpcResponse<Vec<KeyedAccount>>` | selector exclusif par construction |
|
||||
| `getTokenLargestAccounts` | mint `Pubkey`, `commitment?` | `RpcResponse<Vec<TokenAccountBalance>>` | 20 plus gros comptes du mint |
|
||||
| `getTokenSupply` | mint `Pubkey`, `commitment?` | `RpcResponse<TokenAmount>` | aucune conversion métier SPL |
|
||||
|
||||
Le selector commun doit rendre impossible la production accidentelle d'un objet contenant simultanément `mint` et `programId`, par exemple avec
|
||||
un enum KSP `Mint(Pubkey) | ProgramId(Pubkey)` sérialisé vers la forme RPC attendue.
|
||||
|
||||
Le `TokenAmount` wire commun conserve :
|
||||
|
||||
```text
|
||||
amount String
|
||||
decimals u8
|
||||
uiAmount Option<f64>
|
||||
uiAmountString String
|
||||
```
|
||||
|
||||
`uiAmount` est nullable dans les structures JSON officielles ; KSP ne doit pas le remplacer par `0.0`.
|
||||
|
||||
Les deux méthodes de liste réutilisent le DTO Account générique. `jsonParsed` peut contenir des structures SPL, mais Transport les conserve en
|
||||
JSON sans les convertir en modèle Token métier.
|
||||
|
||||
## Matrice exacte — Cluster
|
||||
|
||||
| Méthode | Paramètres/config confirmés | Résultat typé à préserver | Limites / points significatifs |
|
||||
|--------------------------|------------------------------------|---------------------------|---------------------------------------------------------------------------------------------|
|
||||
| `getClusterNodes` | aucun | `Vec<ClusterNode>` | nombreux endpoints/champs optionnels |
|
||||
| `getEpochInfo` | config `commitment/minContextSlot` | `EpochInfo` | `transactionCount` nullable |
|
||||
| `getEpochSchedule` | aucun | `EpochSchedule` | structure fixe d'epoch schedule |
|
||||
| `getHighestSnapshotSlot` | aucun | `SnapshotSlotInfo` | `incremental` nullable; absence de snapshot = erreur RPC |
|
||||
| `getIdentity` | aucun | identity `Pubkey` | objet `{identity}` sur le wire |
|
||||
| `getLeaderSchedule` | slot/config overload | `Option<LeaderSchedule>` | forme des paramètres particulière; résultat nullable |
|
||||
| `getMaxRetransmitSlot` | aucun | `u64` | lecture simple |
|
||||
| `getMaxShredInsertSlot` | aucun | `u64` | lecture simple |
|
||||
| `getSlot` | config `commitment/minContextSlot` | `u64` | erreur min-context possible |
|
||||
| `getSlotLeader` | config `commitment/minContextSlot` | leader `Pubkey` | erreur min-context possible |
|
||||
| `getSlotLeaders` | `startSlot`, `limit` | `Vec<Pubkey>` | limite documentée `1..=5000`; ordre par slot |
|
||||
| `getVoteAccounts` | config vote accounts | `VoteAccountStatus` | `current` + `delinquent`; Agave borne actuellement `epochCredits` à 5 entrées par validator |
|
||||
|
||||
### `ClusterNode`
|
||||
|
||||
La surface courante documente les champs suivants :
|
||||
|
||||
```text
|
||||
pubkey Pubkey
|
||||
featureSet Option<u32>
|
||||
gossip Option<String>
|
||||
pubsub Option<String>
|
||||
rpc Option<String>
|
||||
serveRepair Option<String>
|
||||
shredVersion Option<u16>
|
||||
tpu Option<String>
|
||||
tpuForwards Option<String>
|
||||
tpuForwardsQuic Option<String>
|
||||
tpuQuic Option<String>
|
||||
tpuVote Option<String>
|
||||
tvu Option<String>
|
||||
version Option<String>
|
||||
clientId Option<String>
|
||||
```
|
||||
|
||||
Même si Agave `v4.2.1` utilise `SocketAddr` en interne pour plusieurs endpoints, le DTO public Transport ne doit pas imposer davantage que
|
||||
le contrat JSON. Les adresses réseau restent donc des chaînes wire optionnelles à moins qu'un invariant officiel plus strict soit nécessaire.
|
||||
|
||||
La page Solana courante ne liste pas encore `clientId`, mais Agave `v4.2.1` expose `client_id: Option<String>` dans `RpcContactInfo`. KSP doit donc
|
||||
accepter et préserver ce champ optionnel sans le rendre obligatoire, afin de rester compatible avec les noeuds/providers qui l'émettent comme
|
||||
avec ceux qui suivent encore la forme documentée sans ce champ.
|
||||
|
||||
### `EpochInfo` et `EpochSchedule`
|
||||
|
||||
`EpochInfo` conserve :
|
||||
|
||||
```text
|
||||
absoluteSlot u64
|
||||
blockHeight u64
|
||||
epoch u64
|
||||
slotIndex u64
|
||||
slotsInEpoch u64
|
||||
transactionCount Option<u64>
|
||||
```
|
||||
|
||||
`EpochSchedule` conserve :
|
||||
|
||||
```text
|
||||
firstNormalEpoch u64
|
||||
firstNormalSlot u64
|
||||
leaderScheduleSlotOffset u64
|
||||
slotsPerEpoch u64
|
||||
warmup bool
|
||||
```
|
||||
|
||||
### `getHighestSnapshotSlot`
|
||||
|
||||
La page officielle possède les états « Snapshot / No Snapshot ». La source Agave reliée par cette page confirme qu'un noeud sans configuration
|
||||
snapshot ou sans full snapshot renvoie `RpcCustomError::NoSnapshot`. KSP conserve cette situation comme erreur JSON-RPC applicative ; il ne la
|
||||
convertit pas en `{ full: 0, incremental: null }`.
|
||||
|
||||
### `getLeaderSchedule`
|
||||
|
||||
La forme actuelle doit être respectée exactement :
|
||||
|
||||
```text
|
||||
paramètre 1 optionnel : slot u64 | config object | null
|
||||
paramètre 2 optionnel : config object lorsque le premier paramètre est slot/null
|
||||
config : commitment + identity
|
||||
résultat : map identity -> indices de slots relatifs au début de l'epoch, ou null
|
||||
```
|
||||
|
||||
Une API Rust typée doit empêcher les combinaisons incohérentes plutôt que demander à l'appelant de construire manuellement ce tableau JSON.
|
||||
|
||||
### `getVoteAccounts`
|
||||
|
||||
La config actuelle contient :
|
||||
|
||||
```text
|
||||
commitment
|
||||
votePubkey
|
||||
keepUnstakedDelinquents
|
||||
delinquentSlotDistance
|
||||
```
|
||||
|
||||
Chaque record conserve au minimum :
|
||||
|
||||
```text
|
||||
votePubkey Pubkey
|
||||
nodePubkey Pubkey
|
||||
activatedStake u64
|
||||
commission u8
|
||||
inflationRewardsCommissionBps Option<u16>
|
||||
epochVoteAccount bool
|
||||
epochCredits Vec<(epoch u64, credits u64, previousCredits u64)>
|
||||
lastVote u64
|
||||
rootSlot u64
|
||||
```
|
||||
|
||||
Un petit DTO nommé pour une entrée `epochCredits` est préférable à exposer un tuple public opaque, tout en conservant la forme tableau du wire
|
||||
au décodage. La source Agave `v4.2.1` limite la réponse à cinq entrées d'historique `epochCredits` par validator; KSP ne doit pas supposer que
|
||||
l'historique complet d'un vote account est exposé par cette méthode RPC.
|
||||
|
||||
Agave `v4.2.1` ajoute également `inflationRewardsCommissionBps`, sérialisé comme champ optionnel. La source précise que ce champ vaut `None` pour
|
||||
un noeud antérieur à son introduction. Le DTO KSP doit donc le conserver en `Option<u16>` et ne jamais le déduire artificiellement de
|
||||
`commission`.
|
||||
|
||||
## DTOs communs et organisation prévue
|
||||
|
||||
`pre.002` doit introduire des types partagés ciblés, pas un « mega DTO ». Le découpage prévu est conceptuellement :
|
||||
|
||||
```text
|
||||
RPC context / commitment
|
||||
account encoding + data slice + account wire
|
||||
keyed account
|
||||
program-account filters
|
||||
token selector + token amount
|
||||
cluster epoch/schedule/node/snapshot/leader/vote structures
|
||||
```
|
||||
|
||||
`SolanaCommitment` et `SolanaRpcContext`, actuellement nés avec les canaris `0.2.1`, doivent être mutualisés sans casser leur réexport public.
|
||||
Les wrappers continueront à utiliser des structs wire privés `serde` puis une conversion explicite vers les DTOs publics KSP, notamment pour
|
||||
valider et convertir les chaînes de pubkey vers `ksp_core_lib::Pubkey`.
|
||||
|
||||
Aucune activation anticipée d'une feature `serde` sur `solana-pubkey` n'est nécessaire : la foundation actuelle sait déjà sérialiser une Pubkey
|
||||
par sa représentation texte, et les réponses peuvent être décodées par wire structs privés avant conversion.
|
||||
|
||||
## Architecture d'exécution imposée
|
||||
|
||||
Tous les wrappers suivent :
|
||||
|
||||
```text
|
||||
wrapper typé
|
||||
-> descriptor central
|
||||
-> execute_standard_rpc
|
||||
-> pool/admission
|
||||
-> reqwest HTTP
|
||||
-> validation JSON-RPC
|
||||
-> decode typé
|
||||
```
|
||||
|
||||
Sont explicitement interdits :
|
||||
|
||||
- client HTTP parallèle par famille ;
|
||||
- appel `reqwest` direct dans un wrapper ;
|
||||
- bypass deadline/admission/retry ;
|
||||
- dépendance Transport -> Config/Store/Program ;
|
||||
- import direct de `tracing` ;
|
||||
- conversion des accounts en modèles Program/SPL métier.
|
||||
|
||||
## Erreurs, retry et sécurité
|
||||
|
||||
Les 22 descriptors sont réaudités comme lectures et restent `RetrySafe`. Aucun `WriteSubmission` n'entre dans `0.2.2`.
|
||||
|
||||
Les règles `0.2.1` sont inchangées :
|
||||
|
||||
- erreur JSON-RPC applicative distincte des erreurs transport ;
|
||||
- `reqwest::Error::without_url()` avant exposition comme source ;
|
||||
- aucune URL/provider credential/body complet dans diagnostics ou logs ordinaires ;
|
||||
- retry central uniquement selon descriptor/policy ;
|
||||
- unique hiérarchie d'erreur via `ksp_core_lib::Error`.
|
||||
|
||||
Les validations de paramètres KSP sont ajoutées seulement lorsqu'elles sont normatives et utiles avant I/O, notamment :
|
||||
|
||||
```text
|
||||
getMultipleAccounts : <= 100 pubkeys
|
||||
getProgramAccounts : <= 4 filtres (limite runtime Agave courante)
|
||||
getSlotLeaders : 1..=5000
|
||||
Token selector : exactement mint OU programId
|
||||
```
|
||||
|
||||
`minContextSlot` peut produire l'erreur RPC dédiée `MinContextSlotNotReached`; le wrapper ne doit pas la transformer en absence de données.
|
||||
|
||||
## Logging
|
||||
|
||||
Aucun événement par méthode n'est ajouté par défaut. Les événements génériques de Transport restent la source d'observabilité : méthode,
|
||||
rôle, endpoint logique, tentative et statut technique, avec metadata sûre.
|
||||
|
||||
Toute instrumentation temporaire `debug` ajoutée pendant les prereleases d'implémentation doit revenir à la baseline `info`/`warn` lors de la
|
||||
tranche finale.
|
||||
|
||||
## Config
|
||||
|
||||
L'audit de `std.transport.json`, de son schema et de `ksp-config-lib/src/transport.rs` ne révèle aucune capacité manquante nécessaire aux 22
|
||||
wrappers. Aucun changement Config n'est prévu dans `pre.001`.
|
||||
|
||||
La direction reste strictement :
|
||||
|
||||
```text
|
||||
ksp-config-lib -> ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Transport ne lit ni `.env`, ni `std::env`.
|
||||
|
||||
## Dépendances
|
||||
|
||||
Aucune nouvelle dépendance externe n'est requise par le design retenu à `pre.001`.
|
||||
|
||||
En particulier, `base64` et `bs58` ne sont pas nécessaires pour représenter les chaînes encodées du wire. Elles ne seront ajoutées que si une
|
||||
capacité publique réelle de décodage/validation binaire est décidée ultérieurement, ce qui n'est pas un objectif de `0.2.2`.
|
||||
|
||||
Aucune crate SPL, `solana-client`, SDK RPC haut niveau ou `ksp-interface-lib` n'est introduit pour cette surface.
|
||||
|
||||
## Stratégie de tests
|
||||
|
||||
Chaque wrapper doit disposer de fixtures déterministes et d'un serveur HTTP local couvrant au minimum :
|
||||
|
||||
- sérialisation exacte de la request ;
|
||||
- réponse success typée ;
|
||||
- `null`/option pertinent ;
|
||||
- config/overload pertinent ;
|
||||
- erreur JSON-RPC significative ;
|
||||
- cardinalité/selector lorsqu'un invariant est documenté.
|
||||
|
||||
Cas transversaux prioritaires :
|
||||
|
||||
- toutes les variantes Account data réellement supportées ;
|
||||
- fallback `jsonParsed -> base64` ;
|
||||
- `space: null` ;
|
||||
- `getAccountInfo` et `getMultipleAccounts` avec comptes absents ;
|
||||
- `getProgramAccounts` bare vs `withContext` ;
|
||||
- `TokenAmount.uiAmount: null` ;
|
||||
- fields optionnels de `getClusterNodes`, dont présence/absence de `clientId` ;
|
||||
- `EpochInfo.transactionCount: null` ;
|
||||
- `SnapshotSlotInfo.incremental: null` et erreur NoSnapshot ;
|
||||
- `getLeaderSchedule: null` + overloads ;
|
||||
- `getVoteAccounts` current/delinquent, epoch credits et présence/absence de `inflationRewardsCommissionBps` ;
|
||||
- limites 100, 4 filtres Program Accounts et 5000; variantes/encodages `memcmp`.
|
||||
|
||||
Les canaries de release doivent maintenir :
|
||||
|
||||
```text
|
||||
current methods == 52 exactes
|
||||
historical methods == 14 exactes
|
||||
coverage partition == 4 / 22 / 11 / 15
|
||||
0.2.1 typed canaries == getBalance/getGenesisHash/getHealth/getVersion, inchangés
|
||||
0.2.2 exact set == les 22 méthodes de ce plan
|
||||
0.2.3/0.2.4 != typed-complete prématurément
|
||||
```
|
||||
|
||||
Un smoke Devnet opt-in pourra couvrir un sous-ensemble représentatif, sans jamais remplacer les fixtures locales.
|
||||
|
||||
## Prévision souple des prereleases
|
||||
|
||||
| Tranche | Objectif |
|
||||
|-----------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `pre.001` | audit officiel actuel, matrice exacte, architecture DTO, dépendances et sizing |
|
||||
| `pre.002` | primitives/configs/results communs Account/Token/Cluster + fixtures de base; mutualiser Context/Commitment |
|
||||
| `pre.003` | 5 wrappers Accounts + tests déterministes |
|
||||
| `pre.004` | 5 wrappers Tokens + tests déterministes |
|
||||
| `pre.005` | Cluster simple : nodes, epoch info/schedule, snapshot, identity, max retransmit, max shred + tests |
|
||||
| `pre.006` | Cluster schedule/slot/vote : leader schedule, slot, slot leader(s), vote accounts + tests |
|
||||
| `pre.007` | canaries de complétude, smoke opt-in si utile, README/USAGE, validation finale, prompt `0.2.3`, préparation stable |
|
||||
|
||||
Le découpage est volontairement asymétrique : `pre.006` contient moins de méthodes mais les formes les plus complexes. Toute tranche qui dépasse
|
||||
le budget est scindée en une prerelease supplémentaire plutôt que compressée.
|
||||
|
||||
## État final publié
|
||||
|
||||
Les tranches fonctionnelles ont livré la surface prévue sans nouvelle dépendance externe :
|
||||
|
||||
```text
|
||||
pre.002 : primitives/configs/results communs
|
||||
pre.003 : 5 Accounts
|
||||
pre.004 : 5 Tokens
|
||||
pre.005 : 7 Cluster simples
|
||||
pre.006 : 5 Cluster restants
|
||||
pre.007 : clôture candidate
|
||||
```
|
||||
|
||||
La surface typed stable est donc :
|
||||
|
||||
```text
|
||||
0.2.1 : 4 wrappers foundation
|
||||
0.2.2 : 22 wrappers Accounts/Tokens/Cluster
|
||||
total : 26 wrappers typed
|
||||
future : 11 Transactions + 15 Blocks/Economics
|
||||
```
|
||||
|
||||
Le réaudit final du 2026-08-18 de `https://solana.com/docs/rpc/http` confirme toujours les 52 méthodes HTTP courantes et la même répartition des familles. La navigation Deprecated conserve les 14 méthodes historiques déjà enregistrées. Aucune méthode de `0.2.2` n'est déplacée ou reclassée.
|
||||
|
||||
Un smoke Devnet opt-in **propre à Transport** est ajouté sous `crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs`. Il construit ses settings programmatiquement et exerce `getAccountInfo`, `getTokenAccountsByOwner`, `getEpochInfo` et `getVoteAccounts`. La branche Token suit la forme Devnet documentée avec un owner Pubkey ordinaire, le programme SPL Token canonique comme selector `programId`, et une config explicite `finalized/jsonParsed`; elle n'exige aucun mint/token account Devnet persistant. Le smoke historique `Config -> Transport` reste distinct et transitoirement hébergé dans Config. Les fixtures HTTP locales restent les preuves reproductibles par défaut.
|
||||
|
||||
`pre.007-fix.001` corrige la première exécution opérateur de ce smoke : le mint repris de l'exemple officiel `getTokenSupply` a renvoyé RPC `-32602` sur Devnet, et deux `expect()` dans le helper de construction violaient le gate Clippy. Le nouveau chemin Token évite cette dépendance d'état externe et le helper retourne explicitement le `Result` de construction.
|
||||
|
||||
`pre.007-fix.002` corrige la deuxième exécution opérateur : le scénario `getTokenAccountsByOwner(System Program, { programId })` sans troisième paramètre a renvoyé RPC `-32600` sur le RPC public Devnet, alors que les validations déterministes et le smoke Config -> Transport restaient verts. Le smoke adopte donc la forme complète de l'exemple RPC public (`owner` ordinaire + `{ programId }` + config `finalized/jsonParsed`) sans modifier le contrat optionnel du wrapper.
|
||||
|
||||
La matrice durable de clôture est `docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md` et le prompt suivant est `prompts/008-V0_2_3_START_PROMPT.md`. `0.2.2-rel.001` publie la release stable et ajoute son entrée au `CHANGELOG.md`.
|
||||
|
||||
## Validation de clôture observée
|
||||
|
||||
La candidate finale `0.2.2-pre.007-fix.002` a été validée par l'opérateur le 2026-08-18 :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK, sans warning
|
||||
cargo test -p ksp-onchain-transport-lib OK
|
||||
cargo test --workspace OK
|
||||
smoke Transport pur Devnet 1 passed
|
||||
smoke Config -> Transport -> Devnet 1 passed
|
||||
```
|
||||
|
||||
La suite Transport confirme :
|
||||
|
||||
```text
|
||||
127 unit tests
|
||||
13 public API tests
|
||||
7 release completeness tests
|
||||
1 smoke Transport ignored par défaut
|
||||
```
|
||||
|
||||
Les graphes Cargo Transport et Config avaient été inspectés pendant la candidate avant les fixes du smoke. `pre.007-fix.001` et `pre.007-fix.002` ne modifient aucune dépendance ni feature ; ils changent uniquement le scénario live, sa documentation et le signal de version. La frontière reste donc :
|
||||
|
||||
```text
|
||||
ksp-config-lib -> ksp-onchain-transport-lib
|
||||
ksp-onchain-transport-lib -X-> ksp-config-lib
|
||||
Transport -X-> Store/Program/tracing direct
|
||||
```
|
||||
|
||||
Les doublons visibles sur le graphe inspecté restent limités aux générations `syn` 2.x/3.x de chaînes transitive/proc-macro ; aucune seconde stack HTTP/Tokio KSP n'est introduite.
|
||||
|
||||
## Hors périmètre
|
||||
|
||||
Sont exclus de `0.2.2` :
|
||||
|
||||
- les 11 méthodes Transactions de `0.2.3` ;
|
||||
- les 15 méthodes Blocks + Economics de `0.2.4` ;
|
||||
- WebSocket, Yellowstone, provider-specific streams ;
|
||||
- décodage SPL/Program ;
|
||||
- Store/materialization ;
|
||||
- `ksp-interface-lib` anticipé ;
|
||||
- modification de Config sans besoin concret ;
|
||||
- ajout de dépendances de décodage pour simple représentation wire.
|
||||
|
||||
## Validations de référence
|
||||
|
||||
Pendant le développement :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
À la clôture :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo test -p ksp-core-lib
|
||||
cargo test --workspace
|
||||
|
||||
cargo tree -p ksp-onchain-transport-lib
|
||||
cargo tree -p ksp-onchain-transport-lib -d
|
||||
cargo tree -p ksp-onchain-transport-lib -e features
|
||||
cargo tree -p ksp-onchain-transport-lib -e normal
|
||||
```
|
||||
|
||||
## Critères de clôture `0.2.2`
|
||||
|
||||
Les critères suivants sont satisfaits par `0.2.2-rel.001` :
|
||||
|
||||
- la matrice officielle est réauditée une dernière fois ;
|
||||
- les 22 méthodes de ce plan ont chacune un wrapper public typé et leurs tests ;
|
||||
- les quatre canaris `0.2.1` restent inchangés ;
|
||||
- la partition globale ne perd aucune méthode future ;
|
||||
- les DTOs conservent les `null`/options documentés ;
|
||||
- les frontières de dépendances, redaction, retry et logging restent conformes ;
|
||||
- README/USAGE et validation durable sont synchronisés ;
|
||||
- les commandes Cargo finales réellement exécutables passent ;
|
||||
- le prompt `0.2.3 — HTTP Transactions` est prêt ;
|
||||
- `rel.001` ne contient aucun développement fonctionnel nouveau.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/000-README.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# Validations KSP
|
||||
|
||||
@@ -12,3 +12,4 @@ Documents :
|
||||
- [`001-V0_1_4_CONFIG_DESKTOP.md`](001-V0_1_4_CONFIG_DESKTOP.md) — matrice finale de `0.1.4 — ksp-app-config-desk`.
|
||||
- [`002-V0_2_0_SERIES_PLANNING.md`](002-V0_2_0_SERIES_PLANNING.md) — matrice finale de la release stable `0.2.0`, avec audit de cohérence et preuves opérateur de `pre.003`.
|
||||
- [`003-V0_2_1_ONCHAIN_HTTP.md`](003-V0_2_1_ONCHAIN_HTTP.md) — matrice de clôture de `0.2.1 — HTTP Solana foundation`, registry 52+14, résilience, Config -> Transport, quatre canaris et smoke Devnet opt-in.
|
||||
- [`004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) — matrice finale validée de `0.2.2`, 22 wrappers Accounts/Tokens/Cluster, canaries 52+14, graphes Cargo, smoke Devnet Transport pur, smoke historique Config -> Transport et préparation de `0.2.3`.
|
||||
|
||||
263
docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md
Normal file
263
docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md
Normal file
@@ -0,0 +1,263 @@
|
||||
<!-- file: docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Validation finale `0.2.2` — HTTP Accounts + Tokens + Cluster
|
||||
|
||||
## Statut
|
||||
|
||||
Cette matrice clôt la release stable `0.2.2`. Les preuves opérateur finales ont été enregistrées sur la candidate corrigée `0.2.2-pre.007-fix.002` avant `0.2.2-rel.001`.
|
||||
|
||||
Base fonctionnelle validée avant `pre.007` :
|
||||
|
||||
```text
|
||||
v0.2.2-pre.006-fix.001
|
||||
```
|
||||
|
||||
L'opérateur a validé cette base le 2026-08-18 avec :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Résultats Transport communiqués :
|
||||
|
||||
```text
|
||||
127 unit tests passed
|
||||
13 public API tests passed
|
||||
6 release completeness passed
|
||||
0 warning clippy signalé
|
||||
```
|
||||
|
||||
## Réaudit officiel final
|
||||
|
||||
Le 2026-08-18, l'index officiel courant a été revérifié :
|
||||
|
||||
```text
|
||||
https://solana.com/docs/rpc/http
|
||||
https://solana.com/docs/rpc/deprecated/confirmtransaction
|
||||
```
|
||||
|
||||
La matrice reste :
|
||||
|
||||
```text
|
||||
52 méthodes HTTP courantes
|
||||
14 méthodes historiques Deprecated
|
||||
```
|
||||
|
||||
Répartition courante utile à cette release :
|
||||
|
||||
```text
|
||||
Accounts : 6 = getBalance acquis en 0.2.1 + 5 en 0.2.2
|
||||
Tokens : 5 = 5 en 0.2.2
|
||||
Cluster : 15 = getGenesisHash/getHealth/getVersion acquis en 0.2.1 + 12 en 0.2.2
|
||||
```
|
||||
|
||||
Aucune méthode `0.2.2` n'est reclassée Deprecated/Unstable. Les 22 descriptors restent `Stable / Supported / Read / RetrySafe`.
|
||||
|
||||
## Surface typée stable
|
||||
|
||||
`0.2.2` ajoute exactement :
|
||||
|
||||
```text
|
||||
Accounts : 5
|
||||
Tokens : 5
|
||||
Cluster : 12
|
||||
Total : 22
|
||||
```
|
||||
|
||||
Avec les quatre canaris de `0.2.1`, Transport expose donc 26 wrappers typed dans la release stable.
|
||||
|
||||
Les partitions futures restent intactes :
|
||||
|
||||
```text
|
||||
0.2.3 Transactions : 11
|
||||
0.2.4 Blocks + Economics : 15
|
||||
```
|
||||
|
||||
L'API raw `execute_standard_rpc()` reste disponible mais ne compte pas comme couverture typed d'une méthode future.
|
||||
|
||||
## Contrats wire à préserver
|
||||
|
||||
La release stable conserve notamment :
|
||||
|
||||
- `Account.data` legacy / encoded / `jsonParsed` sans décodage Program ;
|
||||
- `Account.space: Option<u64>` ;
|
||||
- comptes absents sous `null` ;
|
||||
- `getProgramAccounts` bare vs contextualisé ;
|
||||
- `TokenAmount.amount` et `uiAmountString` comme chaînes exactes ;
|
||||
- `TokenAmount.uiAmount: Option<f64>` ;
|
||||
- selector Token exclusif `Mint | ProgramId` ;
|
||||
- `ClusterNode.clientId: Option<String>` ;
|
||||
- `EpochInfo.transactionCount: Option<u64>` ;
|
||||
- `SnapshotSlotInfo.incremental: Option<u64>` ;
|
||||
- `getLeaderSchedule -> Option<schedule>` ;
|
||||
- `VoteAccountInfo.inflationRewardsCommissionBps: Option<u16>` ;
|
||||
- triplets `epochCredits` sans interprétation métier.
|
||||
|
||||
Limites locales figées :
|
||||
|
||||
```text
|
||||
getMultipleAccounts <= 100
|
||||
getProgramAccounts <= 4 filtres
|
||||
memcmp raw <= 128 octets
|
||||
getSlotLeaders 1..=5000
|
||||
```
|
||||
|
||||
## Canaries de complétude
|
||||
|
||||
`tests/release_completeness.rs` protège :
|
||||
|
||||
```text
|
||||
current == 52
|
||||
historical == 14
|
||||
partition == 4 / 22 / 11 / 15
|
||||
0.2.1 exact == 4 canaris foundation
|
||||
0.2.2 exact == 22 Accounts/Tokens/Cluster
|
||||
0.2.3 count == 11 sans avancement typed prématuré
|
||||
0.2.4 count == 15 sans avancement typed prématuré
|
||||
historical => Deprecated + Removed + Historical + NotApplicable
|
||||
```
|
||||
|
||||
La suite stable confirmée est :
|
||||
|
||||
```text
|
||||
127 unit tests
|
||||
13 public API tests
|
||||
7 release completeness tests
|
||||
1 smoke Transport live ignored par défaut
|
||||
```
|
||||
|
||||
## Smoke Devnet Transport pur
|
||||
|
||||
Nouveau test :
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
|
||||
```
|
||||
|
||||
Il construit `HttpTransportSettings` programmatiquement, sans Config ni environnement :
|
||||
|
||||
```text
|
||||
https://api.devnet.solana.com
|
||||
-> getAccountInfo(System Program)
|
||||
-> getTokenAccountsByOwner(owner ordinaire documenté, selector programId = SPL Token, finalized/jsonParsed)
|
||||
-> getEpochInfo
|
||||
-> getVoteAccounts
|
||||
```
|
||||
|
||||
Exécution explicite :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Le test est `ignored` par défaut. Sa branche Token ne dépend plus d'un mint d'exemple : elle utilise un owner Pubkey ordinaire de l'exemple RPC public, le selector `programId` canonique et une config explicite `finalized/jsonParsed`. Elle accepte naturellement une liste vide et vérifie seulement qu'un appel Token structurellement documenté atteint Devnet. Un incident réseau/rate-limit doit être analysé séparément ; le smoke ne remplace jamais les fixtures HTTP locales.
|
||||
|
||||
Correction `pre.007-fix.001` : lors de la première passe opérateur de `pre.007`, `getTokenSupply` sur le mint d'exemple de la documentation a échoué avec le code RPC `-32602`. Le smoke Config -> Transport, lui, a réussi. La branche Token est donc volontairement passée à `getTokenAccountsByOwner` avec selector `programId`, qui ne requiert aucun mint mutable pour prouver la traversée live du wrapper Token.
|
||||
|
||||
Correction `pre.007-fix.002` : lors de la passe suivante, `getTokenAccountsByOwner(System Program, { programId })` sans config explicite a échoué avec RPC `-32600`, tandis que `fmt`, `check`, `clippy`, les tests Transport, le workspace et le smoke Config -> Transport étaient verts. Le smoke Transport utilise désormais la forme complète documentée `owner ordinaire + { programId } + { commitment: finalized, encoding: jsonParsed }`. Ce changement reste limité au scénario live et ne redéfinit pas l'optionalité de la config dans le wrapper.
|
||||
|
||||
## Smoke de composition Config -> Transport
|
||||
|
||||
Le smoke historique `0.2.1` reste disponible :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Il valide `Config -> profil devnet_public -> Transport -> quatre canaris foundation`.
|
||||
|
||||
Ownership : ce smoke cross-crates est transitoire. `ksp-config-lib` ne doit pas devenir la destination générale des smokes `Config + autre crate`; il migrera vers une future surface d'intégration/orchestration/demo appropriée.
|
||||
|
||||
## Dépendances, sécurité et logging
|
||||
|
||||
La release stable n'introduit :
|
||||
|
||||
```text
|
||||
aucune dépendance externe nouvelle
|
||||
aucune dépendance Transport -> Config/Store/Program
|
||||
aucun tracing direct hors ksp-logging-lib
|
||||
aucun client HTTP parallèle
|
||||
```
|
||||
|
||||
Les invariants de sécurité `0.2.1` restent obligatoires : URL/provider credentials absents des diagnostics ordinaires, `reqwest::Error::without_url()` avant exposition comme source, aucune réponse/body massif journalisé par défaut.
|
||||
|
||||
La baseline Logging Transport reste `info` dans la publication stable.
|
||||
|
||||
## Documentation stable
|
||||
|
||||
Sont synchronisés :
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/README.md
|
||||
crates/ksp-onchain-transport-lib/USAGE.md
|
||||
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
ROADMAP.md
|
||||
prompts/008-V0_2_3_START_PROMPT.md
|
||||
```
|
||||
|
||||
`0.2.2-rel.001` ajoute l'entrée stable au `CHANGELOG.md` sans modifier la surface fonctionnelle.
|
||||
|
||||
## Validations finales opérateur observées
|
||||
|
||||
Sur `0.2.2-pre.007-fix.002`, l'opérateur a exécuté le 2026-08-18 :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test --workspace
|
||||
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Résultats :
|
||||
|
||||
```text
|
||||
fmt/check/clippy : succès, aucun warning Clippy
|
||||
Transport unit : 127 passed
|
||||
Transport public : 13 passed
|
||||
release canaries : 7 passed
|
||||
workspace : succès
|
||||
Transport smoke : 1 passed; 0 failed
|
||||
Config smoke : 1 passed; 0 failed
|
||||
```
|
||||
|
||||
Les suites normales conservent les smokes réseau sous `#[ignore]`; leur exécution explicite ci-dessus constitue la preuve live de clôture.
|
||||
|
||||
Les graphes suivants ont été inspectés pendant la candidate :
|
||||
|
||||
```bash
|
||||
cargo tree -p ksp-onchain-transport-lib
|
||||
cargo tree -p ksp-onchain-transport-lib -d
|
||||
cargo tree -p ksp-onchain-transport-lib -e features
|
||||
cargo tree -p ksp-onchain-transport-lib -e normal
|
||||
cargo tree -p ksp-config-lib
|
||||
cargo tree -p ksp-config-lib -d
|
||||
cargo tree -p ksp-config-lib -e features
|
||||
cargo tree -p ksp-config-lib -e normal
|
||||
```
|
||||
|
||||
Les fixes `pre.007-fix.001` et `pre.007-fix.002` n'ajoutent ni dépendance ni feature. Le graphe reste conforme au firewall : Config peut dépendre de Transport, Transport ne dépend pas de Config/Store/Program et n'importe pas `tracing` directement. Les doublons constatés sur les graphes inspectés restent ceux de `syn` 2.x/3.x dans les chaînes transitive/proc-macro ; aucune stack HTTP/Tokio KSP concurrente n'est introduite.
|
||||
|
||||
## Publication `rel.001`
|
||||
|
||||
`0.2.2-rel.001` reste strictement publicationnelle :
|
||||
|
||||
```text
|
||||
workspace.package.version -> 0.2.2
|
||||
ROADMAP : 0.2.2 -> [X]
|
||||
CHANGELOG : synthèse stable 0.2.2
|
||||
plan 009 / validation 004 : clôturés avec preuves opérateur
|
||||
deltas/0.2.2/rel.001.md
|
||||
commit v0.2.2-rel.001
|
||||
tag stable v0.2.2 après validation du commit
|
||||
```
|
||||
|
||||
Aucune nouvelle méthode HTTP, aucun nouveau DTO et aucun nouveau comportement runtime n'apparaissent dans `rel.001`.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: prompts/000-README.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# Prompts KSP
|
||||
|
||||
@@ -28,3 +28,4 @@ Le prompt générique `0.1.x` a été affiné pendant `0.0.3` puis remplacé par
|
||||
- [`005-V0_2_0_START_PROMPT.md`](005-V0_2_0_START_PROMPT.md) — prompt de reprise préparé à la clôture de `0.1.4`; il ouvre `0.2.0-pre.001`, release intermédiaire d'audit de `khadhroony-bot3`, de comparaison avec KSP et de planification/découpage du reste de `0.2.x` ;
|
||||
- [`006-V0_2_1_START_PROMPT.md`](006-V0_2_1_START_PROMPT.md) — prompt finalisé par `0.2.0-pre.003`, destiné à ouvrir `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation` après publication stable de `0.2.0`; il impose l'audit exhaustif des surfaces HTTP courantes et deprecated/unstable officiellement documentées, la séparation Config/Transport, les pools/rôles et le gate de sizing « une release = une session » ;
|
||||
- [`007-V0_2_2_START_PROMPT.md`](007-V0_2_2_START_PROMPT.md) — prompt préparé par la dernière prerelease de `0.2.1`, destiné à ouvrir `0.2.2 — HTTP Accounts + Tokens + Cluster` après publication stable de `0.2.1`; il cible les 22 wrappers typés restants de ces familles et impose un nouvel audit officiel/gate de sizing à `pre.001`.
|
||||
- [`008-V0_2_3_START_PROMPT.md`](008-V0_2_3_START_PROMPT.md) — prompt préparé par la dernière prerelease de `0.2.2`, destiné à ouvrir `0.2.3 — HTTP Transactions` après publication stable de `0.2.2`; il cible les 11 méthodes Transactions et impose un audit actuel ainsi que la politique no-resend des write submissions.
|
||||
|
||||
268
prompts/008-V0_2_3_START_PROMPT.md
Normal file
268
prompts/008-V0_2_3_START_PROMPT.md
Normal file
@@ -0,0 +1,268 @@
|
||||
<!-- file: prompts/008-V0_2_3_START_PROMPT.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Prompt de démarrage `0.2.3` — HTTP Transactions
|
||||
|
||||
## 1. Contexte de reprise
|
||||
|
||||
La base attendue est la release stable :
|
||||
|
||||
```text
|
||||
v0.2.2
|
||||
```
|
||||
|
||||
`0.2.1` a stabilisé la foundation HTTP Solana et quatre canaris typed. `0.2.2` complète Accounts/Tokens/Cluster avec 22 wrappers supplémentaires, un smoke Devnet Transport pur, des DTOs wire communs et les canaries de clôture.
|
||||
|
||||
La surface acquise au démarrage de `0.2.3` doit donc être :
|
||||
|
||||
```text
|
||||
52 méthodes HTTP courantes enregistrées
|
||||
14 méthodes historiques Deprecated / runtime Removed
|
||||
26 wrappers typed : 4 foundation + 22 Accounts/Tokens/Cluster
|
||||
partition restante : 11 Transactions + 15 Blocks/Economics
|
||||
Transport -X-> Config/Store/Program/tracing direct
|
||||
Config -> Transport autorisé
|
||||
```
|
||||
|
||||
La release à ouvrir est :
|
||||
|
||||
```text
|
||||
0.2.3 — HTTP Transactions
|
||||
```
|
||||
|
||||
La première tranche est `0.2.3-pre.001` et commence par **audit officiel actuel + brainstorming + sizing** avant toute implémentation lourde.
|
||||
|
||||
## 2. Mission
|
||||
|
||||
Compléter les 11 méthodes Transactions affectées à `HttpRpcCoverageRelease::V0_2_3` :
|
||||
|
||||
```text
|
||||
getFeeForMessage
|
||||
getLatestBlockhash
|
||||
getRecentPrioritizationFees
|
||||
getSignaturesForAddress
|
||||
getSignatureStatuses
|
||||
getTransaction
|
||||
getTransactionCount
|
||||
isBlockhashValid
|
||||
requestAirdrop
|
||||
sendTransaction
|
||||
simulateTransaction
|
||||
```
|
||||
|
||||
Cette liste est la partition KSP actuelle. `pre.001` doit la revérifier contre la documentation Solana du jour avant de confirmer le périmètre.
|
||||
|
||||
## 3. Classification de sécurité à préserver
|
||||
|
||||
La metadata KSP actuelle distingue :
|
||||
|
||||
```text
|
||||
8 Read / RetrySafe
|
||||
2 WriteSubmission / NeverAfterDispatch : requestAirdrop, sendTransaction
|
||||
1 Simulation / RetrySafe : simulateTransaction
|
||||
```
|
||||
|
||||
`pre.001` doit vérifier que cette classification reste correcte. La règle la plus importante est :
|
||||
|
||||
> aucune resoumission automatique d'une WriteSubmission après un dispatch ambigu.
|
||||
|
||||
Un timeout après dispatch, une rupture de connexion après envoi ou toute autre issue où KSP ne peut pas prouver l'absence de dispatch doit arrêter le retry automatique.
|
||||
|
||||
## 4. Sources internes obligatoires
|
||||
|
||||
Relire avant modification :
|
||||
|
||||
```text
|
||||
ROADMAP.md
|
||||
CHANGELOG.md
|
||||
RULES.md
|
||||
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
docs/validation/003-V0_2_1_ONCHAIN_HTTP.md
|
||||
docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md
|
||||
|
||||
docs/architecture/002-LAYERS_AND_DEPENDENCIES.md
|
||||
docs/architecture/003-COMPONENT_CONTRACTS.md
|
||||
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||
docs/architecture/005-DEPENDENCY_GRAPH.md
|
||||
|
||||
docs/rules/RULES_DEPENDENCIES.md
|
||||
docs/rules/RULES_RUST.md
|
||||
docs/rules/RULES_KSP.md
|
||||
docs/rules/FILE_CONTRACTS.md
|
||||
docs/rules/VERSION_WORKFLOW.md
|
||||
docs/rules/PROMPT_STRUCTURE.md
|
||||
|
||||
crates/ksp-onchain-transport-lib/README.md
|
||||
crates/ksp-onchain-transport-lib/USAGE.md
|
||||
crates/ksp-onchain-transport-lib/src/
|
||||
crates/ksp-onchain-transport-lib/unit_tests/
|
||||
crates/ksp-onchain-transport-lib/tests/
|
||||
```
|
||||
|
||||
Les anciens deltas sont historiques et ne doivent pas être réécrits.
|
||||
|
||||
## 5. Sources externes normatives
|
||||
|
||||
Au début de `pre.001`, réauditer :
|
||||
|
||||
```text
|
||||
https://solana.com/docs/rpc/http
|
||||
https://solana.com/docs/rpc/json-structures
|
||||
```
|
||||
|
||||
Et les sources primaires Agave actuelles lorsque la documentation HTTP laisse une ambiguïté sur les configs, limites ou champs wire.
|
||||
|
||||
Pour chaque méthode Transaction vérifier :
|
||||
|
||||
- paramètres et ordre exacts ;
|
||||
- commitments/minContextSlot ;
|
||||
- encodings actuels et formes legacy ;
|
||||
- cardinalités ;
|
||||
- null/optional ;
|
||||
- structures de signature/status/transaction/meta ;
|
||||
- `maxSupportedTransactionVersion` ;
|
||||
- erreurs significatives ;
|
||||
- statut stable/deprecated/unstable ;
|
||||
- limites provider/runtime utiles avant I/O.
|
||||
|
||||
`getTransaction` doit notamment conserver explicitement sa forme moderne et la compatibilité legacy encore auditée, sans présenter la forme legacy comme recommandée.
|
||||
|
||||
## 6. Gate de sizing obligatoire
|
||||
|
||||
Répondre explicitement :
|
||||
|
||||
```text
|
||||
Les 11 wrappers Transactions, leurs DTOs/wires, les règles write/simulation, les tests et la documentation peuvent-ils être clôturés proprement dans cette session ?
|
||||
```
|
||||
|
||||
Si NON, scinder `0.2.3` avant implémentation lourde. Une prerelease vise environ 15–20 minutes de travail effectif.
|
||||
|
||||
## 7. Architecture Transport
|
||||
|
||||
Conserver le flux unique :
|
||||
|
||||
```text
|
||||
wrapper typed
|
||||
-> descriptor central
|
||||
-> execute_standard_rpc
|
||||
-> pool/admission
|
||||
-> executor HTTP
|
||||
-> validation JSON-RPC
|
||||
-> decode typed
|
||||
```
|
||||
|
||||
Interdits : client HTTP parallèle, `reqwest` direct par wrapper, bypass retry/deadline/admission, Transport -> Config, modèle Store/Program, `tracing` direct.
|
||||
|
||||
Réutiliser les primitives `SolanaCommitment`, `SolanaContextConfig`, `SolanaRpcContext` et les erreurs KSP lorsque pertinentes.
|
||||
|
||||
## 8. Transaction wire et dépendances
|
||||
|
||||
Ne pas ajouter une crate Solana RPC/client haut niveau pour recopier les types du wire.
|
||||
|
||||
`pre.001` doit décider explicitement si la représentation/validation des messages et transactions sérialisés justifie enfin une dépendance `base64`, `bs58` ou autre. Ne pas ajouter une dépendance seulement parce que les exemples RPC l'utilisent.
|
||||
|
||||
Les apps/workers ne doivent pas recevoir de dépendance protocolaire Solana supplémentaire : la surface publique appartient à Transport/Core.
|
||||
|
||||
## 9. Read methods
|
||||
|
||||
Auditer particulièrement :
|
||||
|
||||
- fee/message et blockhash contextuels ;
|
||||
- recent prioritization fees ;
|
||||
- signatures pagination `before/until/limit` ;
|
||||
- signature statuses et recherche historique ;
|
||||
- transaction nullable, encoding et version ;
|
||||
- transaction count ;
|
||||
- validité d'un blockhash.
|
||||
|
||||
Conserver l'ordre des listes et les `null` documentés.
|
||||
|
||||
## 10. Write submissions
|
||||
|
||||
### `requestAirdrop`
|
||||
|
||||
Considérer l'appel comme une soumission : une signature retournée n'autorise pas un resend automatique aveugle après résultat ambigu.
|
||||
|
||||
### `sendTransaction`
|
||||
|
||||
La règle no-resend est critique. Le wrapper doit utiliser la policy centrale existante et ne jamais introduire sa propre boucle de retry.
|
||||
|
||||
Le contrat Transport ne devient pas un executor métier : il transporte une transaction déjà construite/signée et préserve les options RPC.
|
||||
|
||||
## 11. `simulateTransaction`
|
||||
|
||||
Simulation n'est pas une WriteSubmission et reste retry-safe selon la metadata actuelle, sous réserve du réaudit `pre.001`.
|
||||
|
||||
Préserver les options et résultats utiles sans introduire de décodage Program métier.
|
||||
|
||||
## 12. Tests
|
||||
|
||||
Fixtures HTTP locales obligatoires pour chaque méthode :
|
||||
|
||||
- request exacte ;
|
||||
- success typed ;
|
||||
- null/optional ;
|
||||
- config/legacy pertinent ;
|
||||
- erreurs RPC ;
|
||||
- cardinalités ;
|
||||
- write dispatch/no-resend ;
|
||||
- simulation.
|
||||
|
||||
Conserver les canaries :
|
||||
|
||||
```text
|
||||
current == 52 confirmées
|
||||
historical == 14 confirmées
|
||||
0.2.1 exact == 4
|
||||
0.2.2 exact == 22
|
||||
0.2.3 exact == 11
|
||||
0.2.4 reste 15 sans avancement prématuré
|
||||
```
|
||||
|
||||
Les smokes live restent opt-in et ne remplacent jamais les fixtures.
|
||||
|
||||
## 13. Documentation et fin de release
|
||||
|
||||
La dernière prerelease doit, comme `0.2.2-pre.007` :
|
||||
|
||||
- réauditer l'inventaire officiel ;
|
||||
- figer les canaries de complétude ;
|
||||
- décider/ajouter un smoke opt-in pertinent sans créer une mauvaise ownership boundary ;
|
||||
- synchroniser README/USAGE, plan et matrice de validation ;
|
||||
- préparer le prompt `0.2.4` ;
|
||||
- laisser `CHANGELOG.md` à `rel.001` ;
|
||||
- préparer une release stable strictement publicationnelle.
|
||||
|
||||
## 14. Validations
|
||||
|
||||
Pendant le développement :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
À la clôture :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo test -p ksp-core-lib
|
||||
cargo test -p ksp-app-config-desk
|
||||
cargo test --workspace
|
||||
|
||||
cargo tree -p ksp-onchain-transport-lib
|
||||
cargo tree -p ksp-onchain-transport-lib -d
|
||||
cargo tree -p ksp-onchain-transport-lib -e features
|
||||
cargo tree -p ksp-onchain-transport-lib -e normal
|
||||
```
|
||||
|
||||
Ne jamais déclarer une commande réussie sans preuve opérateur ou exécution réelle.
|
||||
Reference in New Issue
Block a user