Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b3967327a | |||
| 804b92b957 | |||
| e6772c109d | |||
| 7e84522787 | |||
| 37461ec248 | |||
| 4252a21140 | |||
| 7a889dd466 | |||
| d3cc7c93f0 | |||
| 561b6678ed | |||
| e500024c3a | |||
| 033800f897 | |||
| cf8d79567d | |||
| cb19742cf7 | |||
| c03a3eaf5d | |||
| f7acac3fc8 | |||
| 039b93cfb3 | |||
| c9a0f7fd37 | |||
| 704110d509 | |||
| fb32ed03f8 | |||
| 4439589f0d | |||
| 1e1bc0d421 | |||
| bd26d4f636 | |||
| 2a7f3e8f40 | |||
| 1d37f04d63 | |||
| 1ccf11bcc1 | |||
| d06284fca2 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,10 +1,18 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Changelog KSP
|
||||
|
||||
## 0.2.4 — HTTP Blocks + Economics + compliance HTTP finale — 2026-08-18
|
||||
|
||||
`0.2.4` achève la surface HTTP Solana standard de `ksp-onchain-transport-lib` : les 10 wrappers Blocks et 5 wrappers Economics portent la couverture typée à **52/52 méthodes HTTP courantes**, tandis que les **14/14 méthodes historiques Deprecated/runtime Removed** restent conservées pour compliance. La release stabilise notamment `getBlock` moderne avec sa forme bare encoding legacy dépréciée, les quatre variantes `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions` (SIMD-0118), `commissionBps` (SIMD-0291), les overloads de ranges/performance, `getBlockProduction`, les valeurs Economics fournies par le runtime et `getInflationReward` positionnel sans plafond d'adresses inventé. Le réaudit final confirme l'égalité exacte entre l'inventaire HTTP officiel courant et le registre KSP, applique `KSP-TRANSPORT-007` aux **52/52 wrappers**, et ne matérialise aucune extension spéculative issue de la watchlist SIMD. Les canaries de frontières, le workspace complet et les deux smokes Devnet — Transport pur couvrant Accounts/Tokens/Cluster/Transactions/Blocks/Economics et composition Config -> Transport transitoire — sont validés avant publication. Le prompt `prompts/010-V0_2_5_START_PROMPT.md`, enrichi par `pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` avec threat model, interopérabilité `.kspwallet` et capacités indépendantes VIEW/OWNER.
|
||||
|
||||
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
|
||||
|
||||
## 0.2.3 — HTTP Transactions — 2026-08-18
|
||||
|
||||
`0.2.3` complète la famille HTTP Transactions de `ksp-onchain-transport-lib` avec 11 wrappers typés, portant la surface HTTP typée courante à 37 méthodes : 4 foundation, 22 Accounts/Tokens/Cluster et 11 Transactions. La release stabilise la classification exacte `8 Read / RetrySafe`, `2 WriteSubmission / NeverAfterDispatch` (`requestAirdrop`, `sendTransaction`) et `1 Simulation / RetrySafe` (`simulateTransaction`), avec preuves end-to-end qu'une write submission n'est jamais resoumise après dispatch ambigu mais peut retenter lorsqu'un échec est prouvé `NotDispatched`. `getTransaction` couvre la config moderne complète et la forme bare encoding legacy dépréciée ; les variantes de transaction, metadata, version et `transactionIndex` restent préservées losslessly. `simulateTransaction` couvre sa config complète, ses invariants déterministes et son résultat riche sans introduire de client RPC Solana haut niveau ni de décodage transactionnel dans Transport. La release formalise `KSP-TRANSPORT-007` et réaudite rétroactivement les 37 wrappers courants sans remédiation fonctionnelle. Les canaries conservent 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et 15 Blocks/Economics réservées à `0.2.4`. Les graphes Cargo et les deux smokes Devnet — Transport pur read-only et composition Config -> Transport transitoire — ont été validés avant publication. Le prompt `prompts/009-V0_2_4_START_PROMPT.md` ouvre `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`.
|
||||
|
||||
## 0.2.2 — HTTP Accounts + Tokens + Cluster — 2026-08-18
|
||||
|
||||
`0.2.2` complète la surface HTTP typée de `ksp-onchain-transport-lib` avec 22 wrappers supplémentaires : 5 Accounts, 5 Tokens et 12 Cluster, portant la couverture typée totale à 26 méthodes avec les quatre canaris foundation de `0.2.1`. La release stabilise les DTOs/configs wire communs, préserve les formes Account encodées/`jsonParsed`, les `null` et champs optionnels, les selectors Token exclusifs, les contextes RPC, les structures Cluster/leader/vote et les limites locales auditées (`getMultipleAccounts <= 100`, `getProgramAccounts <= 4` filtres, `memcmp` raw <= 128 octets, `getSlotLeaders` entre 1 et 5000). Les 22 descriptors restent `Read / RetrySafe` et passent exclusivement par le flux central `descriptor -> execute_standard_rpc -> pool/admission -> executor HTTP`. La release n'ajoute aucune dépendance externe, ne crée aucun client HTTP parallèle et conserve le firewall Transport -> Config/Store/Program. Elle ajoute un smoke Devnet pur à Transport avec settings programmatiques couvrant Accounts/Tokens/Cluster, conserve séparément le smoke historique Config -> Transport comme exception transitoire, et valide les deux explicitement avant publication. Les canaries figent toujours 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et la partition typed `4 / 22 / 11 / 15`. Le prompt `prompts/008-V0_2_3_START_PROMPT.md` ouvre `0.2.3 — HTTP Transactions`.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 119
|
||||
# version: 142
|
||||
|
||||
[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.2"
|
||||
version = "0.2.4"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 37 -->
|
||||
<!-- version: 43 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -47,9 +47,9 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
||||
|
||||
- [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.
|
||||
- [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`.
|
||||
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
|
||||
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
|
||||
- [ ] `0.2.5` — Introduire `ksp-wallet-lib` et le format interopérable `.kspwallet` avec threat model offline, capacités indépendantes VIEW/OWNER, metadata protégées, signature, rotations de key slots et import/export extensible ; exclure `WalletPolicy`.
|
||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet.
|
||||
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
|
||||
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -69,16 +69,19 @@ Le registre porte notamment :
|
||||
- remplacement historique éventuel ;
|
||||
- release de couverture typée KSP.
|
||||
|
||||
La surface typée stable après `0.2.2` contient **26 méthodes** :
|
||||
La release stable `0.2.4` complète la surface typée des **52 méthodes courantes** :
|
||||
|
||||
```text
|
||||
0.2.1 foundation : 4
|
||||
0.2.2 Accounts : 5
|
||||
0.2.2 Tokens : 5
|
||||
0.2.2 Cluster : 12
|
||||
0.2.2 Accounts/Tokens/Cluster : 22
|
||||
0.2.3 Transactions : 11
|
||||
0.2.4 Blocks/Economics : 15
|
||||
total : 52
|
||||
```
|
||||
|
||||
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**.
|
||||
`0.2.4` ajoute les dix wrappers Blocks et les cinq wrappers Economics. Les points sensibles restent notamment `getBlock` moderne + bare encoding legacy deprecated, les quatre `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions`, `commissionBps`, les overloads de `getBlocks`, les ranges de production, les valeurs d'inflation/minimum de délégation fournies par le runtime et les `null` positionnels de `getInflationReward`.
|
||||
|
||||
`KSP-TRANSPORT-007` impose qu'un wrapper typé couvre toutes les possibilités RPC supportées retenues par l'audit : paramètres/options, overloads et formes legacy encore supportées, contraintes déterministes utiles et variantes de réponse pertinentes sans perte. Le réaudit final `0.2.4` agrège le réaudit 37/37 de `0.2.3` avec les 15 nouveaux wrappers et porte la preuve stable à **52/52**.
|
||||
|
||||
Les 14 méthodes historiques restent découvrables pour la compliance mais sont `Removed` et ne sont pas simulées comme appelables.
|
||||
|
||||
@@ -122,7 +125,10 @@ Deux smokes Devnet opt-in sont séparés par responsabilité :
|
||||
|
||||
```text
|
||||
Transport pur : settings programmatiques -> HttpTransportPool
|
||||
-> getAccountInfo/getTokenAccountsByOwner/getEpochInfo/getVoteAccounts
|
||||
-> Accounts/Tokens/Cluster représentatifs
|
||||
-> trois reads Transactions
|
||||
-> getBlockHeight
|
||||
-> getInflationRate/getStakeMinimumDelegation
|
||||
|
||||
Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool
|
||||
-> getHealth/getGenesisHash/getVersion/getBalance
|
||||
@@ -138,4 +144,8 @@ Les deux sont `ignored` par défaut. Le smoke Transport appartient durablement
|
||||
- [`../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md) — foundation HTTP stable ;
|
||||
- [`../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) — extension typed Accounts/Tokens/Cluster ;
|
||||
- [`../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) — matrice finale validée `0.2.2` ;
|
||||
- [`../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de `0.2.3` ;
|
||||
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée `0.2.3` ;
|
||||
- [`../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan Blocks/Economics et compliance HTTP finale ;
|
||||
- [`../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) — matrice finale validée `52/52 + 14/14` et audit `KSP-TRANSPORT-007` global ;
|
||||
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard HTTP.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Utilisation de `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -96,7 +96,34 @@ let epoch = pool.get_epoch_info(&role, None).await;
|
||||
let vote_accounts = pool.get_vote_accounts(&role, None).await;
|
||||
```
|
||||
|
||||
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.
|
||||
La release stable `0.2.4` contient les **52 wrappers typés courants** : 4 foundation + 22 Accounts/Tokens/Cluster + 11 Transactions + 10 Blocks + 5 Economics. Les DTOs Transport conservent les `null`, options, overloads et formes wire sans décodage Program/SPL métier.
|
||||
|
||||
Exemples Transaction représentatifs :
|
||||
|
||||
```rust
|
||||
let context = ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let latest = pool
|
||||
.get_latest_blockhash(&role, std::option::Option::Some(&context))
|
||||
.await;
|
||||
let transaction_count = pool
|
||||
.get_transaction_count(&role, std::option::Option::Some(&context))
|
||||
.await;
|
||||
```
|
||||
|
||||
`getTransaction` expose la config moderne complète et une forme bare-encoding legacy séparée et deprecated. `requestAirdrop` et `sendTransaction` sont des write submissions : elles utilisent la protection centrale `NeverAfterDispatch`. `simulateTransaction` reste une simulation retry-safe et conserve son résultat riche sans introduire de décodage Program.
|
||||
|
||||
Exemples Blocks/Economics représentatifs :
|
||||
|
||||
```rust
|
||||
let block_height = pool.get_block_height(&role, Some(&context)).await;
|
||||
let inflation_rate = pool.get_inflation_rate(&role).await;
|
||||
let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).await;
|
||||
```
|
||||
|
||||
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
|
||||
|
||||
## 4. Exécution JSON-RPC standard générique
|
||||
|
||||
@@ -108,7 +135,7 @@ if let Some(descriptor) = ksp_onchain_transport_lib::find_http_rpc_method("getSl
|
||||
}
|
||||
```
|
||||
|
||||
Cette API retourne un `serde_json::Value`. Elle est utile pour les consumers techniques et pour préparer les futures surfaces typées, mais elle ne remplace pas le wrapper typé d'une méthode dans la matrice de couverture KSP.
|
||||
Cette API retourne un `serde_json::Value`. Elle reste utile pour les extensions provider, les diagnostics et les méthodes hors registre standard, mais elle ne remplace jamais le wrapper typé d'une méthode HTTP standard désormais couverte par KSP.
|
||||
|
||||
Avant exécution, `ensure_runtime_supported()` est appliqué. Une méthode historique `Removed` retourne `ERROR_CODE_METHOD_REMOVED` au lieu d'émettre un appel réseau fictif.
|
||||
|
||||
@@ -155,13 +182,13 @@ La configuration standard route les événements `info` de Transport vers un fic
|
||||
|
||||
## 9. Smokes Devnet opt-in
|
||||
|
||||
Le smoke **Transport pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif de `0.2.2` :
|
||||
Le smoke **Transport pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics de la release stable `0.2.4` :
|
||||
|
||||
```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.
|
||||
Il appelle `getAccountInfo`, `getTokenAccountsByOwner`, `getEpochInfo`, `getVoteAccounts`, puis `getLatestBlockhash`, `isBlockhashValid`, `getTransactionCount`, `getBlockHeight`, `getInflationRate` et `getStakeMinimumDelegation`. La branche Token suit la forme Devnet documentée : owner Pubkey ordinaire de l'exemple officiel, selector `programId` avec l'ID canonique du programme SPL Token, puis config explicite `commitment: finalized` + `encoding: jsonParsed`. Une réponse vide reste acceptable. La branche Transaction reste read-only : elle ne déclenche ni airdrop ni soumission de transaction et ne remplace pas les fixtures déterministes couvrant les 11 wrappers.
|
||||
|
||||
Le smoke historique de **composition Config -> Transport** reste également disponible :
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{"commitment":[1,2,3,4],"totalStake":1000000000},
|
||||
{"commitment":null,"totalStake":1000000000}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"byIdentity": {
|
||||
"11111111111111111111111111111111": [8,7],
|
||||
"Vote111111111111111111111111111111111111111": [4,4]
|
||||
},
|
||||
"range": {"firstSlot":430000000,"lastSlot":430000099}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"previousBlockhash":"previous-a",
|
||||
"blockhash":"block-a",
|
||||
"parentSlot":1,
|
||||
"blockTime":null,
|
||||
"blockHeight":null
|
||||
},
|
||||
{
|
||||
"previousBlockhash":"previous-b",
|
||||
"blockhash":"block-b",
|
||||
"parentSlot":2,
|
||||
"transactions":null,
|
||||
"signatures":null,
|
||||
"rewards":null,
|
||||
"numRewardPartitions":null,
|
||||
"blockTime":null,
|
||||
"blockHeight":null
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"previousBlockhash":"previous-blockhash-fixture",
|
||||
"blockhash":"blockhash-fixture",
|
||||
"parentSlot":430000122,
|
||||
"transactions":[
|
||||
{
|
||||
"transaction":{"signatures":["signature-fixture"],"message":{"accountKeys":[],"recentBlockhash":"recent-fixture","instructions":[]}},
|
||||
"meta":{"err":null,"fee":5000},
|
||||
"version":1
|
||||
},
|
||||
{
|
||||
"transaction":["AQID","base64"],
|
||||
"meta":null
|
||||
}
|
||||
],
|
||||
"rewards":[
|
||||
{
|
||||
"pubkey":"11111111111111111111111111111111",
|
||||
"lamports":2500,
|
||||
"postBalance":1000002500,
|
||||
"rewardType":"Fee",
|
||||
"commission":null,
|
||||
"commissionBps":1234
|
||||
},
|
||||
{
|
||||
"pubkey":"Vote111111111111111111111111111111111111111",
|
||||
"lamports":1250,
|
||||
"postBalance":2000001250,
|
||||
"rewardType":"Voting",
|
||||
"commission":5
|
||||
}
|
||||
],
|
||||
"numRewardPartitions":4,
|
||||
"blockTime":1787072400,
|
||||
"blockHeight":410000000
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
[
|
||||
{
|
||||
"slot": 430000123,
|
||||
"blockTime": 1787000000,
|
||||
"transaction": ["AQIDBAUGBwg=", "base64"],
|
||||
"meta": {
|
||||
"err": null,
|
||||
"status": {"Ok": null},
|
||||
"fee": 5000,
|
||||
"preBalances": [10000],
|
||||
"postBalances": [5000]
|
||||
},
|
||||
"version": 0,
|
||||
"transactionIndex": 7
|
||||
},
|
||||
{
|
||||
"slot": 430000100,
|
||||
"blockTime": null,
|
||||
"transaction": "3Bxs4NN8M2Yn4TLb",
|
||||
"meta": null,
|
||||
"version": "legacy",
|
||||
"transactionIndex": null
|
||||
},
|
||||
{
|
||||
"slot": 430000050,
|
||||
"blockTime": null,
|
||||
"transaction": {
|
||||
"signatures": [],
|
||||
"message": {
|
||||
"accountKeys": [],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"instructions": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
"3Bxs4NN8M2Yn4TLb",
|
||||
["AQIDBAUGBwg=", "base64"],
|
||||
{
|
||||
"signatures": ["5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5"],
|
||||
"message": {
|
||||
"accountKeys": [],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"instructions": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-accounts"],"accountKeys":[{"pubkey":"11111111111111111111111111111111","writable":true,"signer":true,"source":"transaction"}]},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[10],"postBalances":[5]},"version":1}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["3BxsFixtureBase58","base58"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":"3BxsFixtureLegacyBinary","meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32004,"message":"Block not available for slot 430000123"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction version (1) is not supported by the requesting client. Please try the request again with maxSupportedTransactionVersion 1"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-v1"],"message":{"accountKeys":[],"recentBlockhash":"recent-v1","instructions":[],"transactionConfig":{"priorityFee":1,"computeUnitLimit":200000}}},"meta":{"err":null,"fee":5000,"status":{"Ok":null},"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":1,"postBalance":2,"rewardType":"Fee","commission":null,"commissionBps":1234}]},"version":1},{"transaction":["AQID","base64"],"meta":null}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-full-no-rewards"],"message":{"accountKeys":[],"recentBlockhash":"recent-full-no-rewards","instructions":[]}},"meta":{"err":null,"fee":5000,"rewards":null},"version":1}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-json"],"message":{"accountKeys":[],"recentBlockhash":"recent-json","instructions":[]}},"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-parsed"],"message":{"accountKeys":[],"recentBlockhash":"recent-parsed","instructions":[]}},"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":null,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-b","blockhash":"block-b","parentSlot":2,"transactions":null,"signatures":null,"rewards":null,"numRewardPartitions":null,"blockTime":null,"blockHeight":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-a","blockhash":"block-a","parentSlot":1,"blockTime":null,"blockHeight":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"signatures":["sig-a","sig-b"]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"commitment":[1,2,3,4],"totalStake":1000000000},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":410000000,"id":1}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"result":{
|
||||
"context":{"slot":430000123,"apiVersion":"4.2.1"},
|
||||
"value":{
|
||||
"byIdentity":{"not-a-pubkey":[8,7]},
|
||||
"range":{"firstSlot":430000000,"lastSlot":430000099}
|
||||
}
|
||||
},
|
||||
"id":1
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"result":{
|
||||
"context":{"slot":430000123,"apiVersion":"4.2.1"},
|
||||
"value":{
|
||||
"byIdentity":{"11111111111111111111111111111111":[8,7]},
|
||||
"range":{"firstSlot":430000000,"lastSlot":430000099}
|
||||
}
|
||||
},
|
||||
"id":1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":null,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":1787072400,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[430000100,430000103,430000109],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[430000200,430000201,430000205],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: unable to decode message"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000002},"value":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000001},"value":5000},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":250000,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"initial":0.07123456789,"terminal":0.0123456789,"taper":0.2718281828,"foundation":0.0042,"foundationTerm":6.25},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"total":0.0387654321,"validator":0.037654321,"foundation":0.0011111111,"epoch":913},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"epoch":912,"effectiveSlot":431500010,"amount":777,"postBalance":1000777,"commission":5,"commissionBps":500},null,{"epoch":912,"effectiveSlot":431500010,"amount":777,"postBalance":1000777,"commission":5,"commissionBps":500}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32017,"message":"Epoch rewards period still active at slot 431500000","data":{"slot":431500000,"currentBlockHeight":400000000,"rewardsCompleteBlockHeight":400000100}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32016,"message":"Minimum context slot has not been reached","data":{"contextSlot":431000000}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[null],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"epoch":912,"effectiveSlot":431500001,"amount":2500000,"postBalance":1002500000,"commission":7,"commissionBps":725},null,{"epoch":912,"effectiveSlot":431500003,"amount":1250000,"postBalance":501250000,"commission":null},{"epoch":912,"effectiveSlot":431500004,"amount":500000,"postBalance":200500000,"commission":3,"commissionBps":null}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000004},"value":{"blockhash":"","lastValidBlockHeight":431123456}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000003},"value":{"blockhash":"11111111111111111111111111111111","lastValidBlockHeight":431123456}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"slot":430000123,"numTransactions":250000,"numNonVoteTransactions":175000,"numSlots":120,"samplePeriodSecs":60},{"slot":430000000,"numTransactions":200000,"numSlots":110,"samplePeriodSecs":60}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"slot":431000030,"prioritizationFee":1200},{"slot":431000029,"prioritizationFee":0}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000052},"value":[null]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000051},"value":[]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000053},"value":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000050},"value":[null,{"slot":431000049,"confirmations":null,"status":{"Ok":null},"err":null,"confirmationStatus":"finalized"},{"slot":431000048,"confirmations":2,"status":{"Err":{"InstructionError":[0,"Custom"]}},"err":{"InstructionError":[0,"Custom"]},"confirmationStatus":"confirmed"}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction history is not available from this node"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"signature":"newest-signature","slot":431000040,"err":null,"memo":"memo-newest","blockTime":1787000100,"confirmationStatus":"finalized","transactionIndex":9},{"signature":"older-signature","slot":431000039,"err":{"InstructionError":[0,"Custom"]},"memo":null,"blockTime":null,"confirmationStatus":"confirmed"}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32016,"message":"Minimum context slot has not been reached","data":{"contextSlot":430999999}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"slot":431000123,"apiVersion":"4.2.1"},"value":42424242},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32012,"message":"scan failed"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"slot":431000125,"apiVersion":"4.2.1"},"value":{"total":610000000000000000,"circulating":510000000000000000,"nonCirculating":100000000000000000,"nonCirculatingAccounts":[]}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"slot":431000124,"apiVersion":"4.2.1"},"value":{"total":610000000000000000,"circulating":510000000000000000,"nonCirculating":100000000000000000,"nonCirculatingAccounts":["11111111111111111111111111111111","Vote111111111111111111111111111111111111111"]}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000061,"blockTime":1787000201,"transaction":["3Bxs4NN8M2Yn4TLb","base58"],"meta":null,"version":"legacy","transactionIndex":0},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000062,"blockTime":1787000202,"transaction":["AQIDBAUGBwg=","base64"],"meta":null,"version":0,"transactionIndex":1},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000060,"blockTime":null,"transaction":"3Bxs4NN8M2Yn4TLb","meta":null,"version":"legacy"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction version (1) is not supported by the requesting client. Please try the request again with the following configuration parameter: \"maxSupportedTransactionVersion\": 1"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000063,"blockTime":1787000203,"transaction":{"signatures":["fixture-signature"],"message":{"header":{"numRequiredSignatures":1,"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":1},"accountKeys":["11111111111111111111111111111111","SysvarRent111111111111111111111111111111111"],"recentBlockhash":"11111111111111111111111111111111","instructions":[{"programIdIndex":0,"accounts":[0,1],"data":"3Bxs4NN8","stackHeight":1}],"addressTableLookups":null}},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[100000,1],"postBalances":[95000,1],"innerInstructions":[],"logMessages":["Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"],"preTokenBalances":[],"postTokenBalances":[],"rewards":null,"loadedAddresses":{"writable":[],"readonly":[]},"returnData":{"programId":"11111111111111111111111111111111","data":["AQID","base64"]},"computeUnitsConsumed":150,"costUnits":190},"version":0,"transactionIndex":7},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"slot":431000064,"blockTime":1787000204,"transaction":{"signatures":["fixture-signature-parsed"],"message":{"accountKeys":[{"pubkey":"11111111111111111111111111111111","signer":true,"writable":true,"source":"transaction"},{"pubkey":"SysvarRent111111111111111111111111111111111","signer":false,"writable":false,"source":"transaction"}],"recentBlockhash":"11111111111111111111111111111111","instructions":[{"program":"system","programId":"11111111111111111111111111111111","stackHeight":1,"parsed":{"type":"transfer","info":{"lamports":1,"source":"11111111111111111111111111111111","destination":"SysvarRent111111111111111111111111111111111"}}}]}},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[100000,1],"postBalances":[95000,1],"innerInstructions":null,"logMessages":null,"preTokenBalances":[],"postTokenBalances":[],"rewards":null,"returnData":null,"computeUnitsConsumed":150,"costUnits":190},"version":"legacy","transactionIndex":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":null,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":987654321,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"initial":0.08,"terminal":0.015,"taper":0.15,"foundation":0.05,"foundationTerm":7.0}
|
||||
@@ -0,0 +1 @@
|
||||
{"total":0.042,"validator":0.041,"foundation":0.001,"epoch":812}
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"epoch":811,"effectiveSlot":429900000,"amount":2500000,"postBalance":1002500000,"commission":7,"commissionBps":725},
|
||||
null,
|
||||
{"epoch":811,"effectiveSlot":429900000,"amount":1250000,"postBalance":501250000,"commission":null}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000005},"value":true},"id":1}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"blockhash": "11111111111111111111111111111111",
|
||||
"lastValidBlockHeight": 430123456
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":123456,"id":1}
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{"slot":430000123,"numTransactions":250000,"numNonVoteTransactions":175000,"numSlots":120,"samplePeriodSecs":60},
|
||||
{"slot":430000003,"numTransactions":240000,"numSlots":118,"samplePeriodSecs":60}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{"slot": 430000123, "prioritizationFee": 1000},
|
||||
{"slot": 430000122, "prioritizationFee": 0}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: supplied blockhash is not valid"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":"airdrop-fixture-signature-111111111111111111111111111111111111111111111111","id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Error processing Instruction 0","data":{"err":{"InstructionError":[0,"Custom"]},"logs":["Program log: fixture preflight failure"],"unitsConsumed":123}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":"send-fixture-signature-22222222222222222222222222222222222222222222222222","id":1}
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"signature": "5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5",
|
||||
"slot": 430000123,
|
||||
"err": null,
|
||||
"memo": "fixture memo",
|
||||
"blockTime": 1787000000,
|
||||
"confirmationStatus": "finalized",
|
||||
"transactionIndex": 7
|
||||
},
|
||||
{
|
||||
"signature": "4Nd1mC5QfVhNq8xqP7z8d7K6a5m4n3b2v1c9x8z7y6w5u4t3s2r1q9p8o7n6m5L4K3J2H1G9F8E7D6C5B4A3",
|
||||
"slot": 430000100,
|
||||
"err": {"InstructionError": [0, "Custom"]},
|
||||
"memo": null,
|
||||
"blockTime": null,
|
||||
"confirmationStatus": "confirmed"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
null,
|
||||
{
|
||||
"slot": 430000123,
|
||||
"confirmations": null,
|
||||
"status": {"Ok": null},
|
||||
"err": null,
|
||||
"confirmationStatus": "finalized"
|
||||
},
|
||||
{
|
||||
"slot": 430000120,
|
||||
"confirmations": 3,
|
||||
"status": {"Err": {"InstructionError": [0, "Custom"]}},
|
||||
"err": {"InstructionError": [0, "Custom"]},
|
||||
"confirmationStatus": "confirmed"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "invalid transaction: failed to sanitize accounts offsets correctly"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"context": {
|
||||
"slot": 431000031,
|
||||
"apiVersion": "4.2.1"
|
||||
},
|
||||
"value": {
|
||||
"err": null,
|
||||
"logs": null,
|
||||
"accounts": null,
|
||||
"unitsConsumed": 99,
|
||||
"returnData": null,
|
||||
"replacementBlockhash": null
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"context": {
|
||||
"slot": 431000030,
|
||||
"apiVersion": "4.2.1"
|
||||
},
|
||||
"value": {
|
||||
"err": null,
|
||||
"logs": [
|
||||
"Program 11111111111111111111111111111111 invoke [1]",
|
||||
"Program 11111111111111111111111111111111 success"
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"lamports": 1000,
|
||||
"data": [
|
||||
"",
|
||||
"base64"
|
||||
],
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"executable": false,
|
||||
"rentEpoch": 0,
|
||||
"space": 0
|
||||
},
|
||||
null
|
||||
],
|
||||
"unitsConsumed": 150,
|
||||
"loadedAccountsDataSize": 64,
|
||||
"returnData": {
|
||||
"programId": "11111111111111111111111111111111",
|
||||
"data": [
|
||||
"AQI=",
|
||||
"base64"
|
||||
]
|
||||
},
|
||||
"innerInstructions": [],
|
||||
"replacementBlockhash": {
|
||||
"blockhash": "ComputeBudget111111111111111111111111111111",
|
||||
"lastValidBlockHeight": 430123999
|
||||
},
|
||||
"fee": 5000,
|
||||
"preBalances": [
|
||||
10000,
|
||||
0
|
||||
],
|
||||
"postBalances": [
|
||||
5000,
|
||||
5000
|
||||
],
|
||||
"preTokenBalances": [],
|
||||
"postTokenBalances": [],
|
||||
"loadedAddresses": {
|
||||
"writable": [],
|
||||
"readonly": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"err": null,
|
||||
"logs": ["Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"],
|
||||
"accounts": [
|
||||
{
|
||||
"lamports": 1000,
|
||||
"data": ["", "base64"],
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"executable": false,
|
||||
"rentEpoch": 0,
|
||||
"space": 0
|
||||
},
|
||||
null
|
||||
],
|
||||
"unitsConsumed": 150,
|
||||
"loadedAccountsDataSize": 64,
|
||||
"returnData": {
|
||||
"programId": "11111111111111111111111111111111",
|
||||
"data": ["AQI=", "base64"]
|
||||
},
|
||||
"innerInstructions": [],
|
||||
"replacementBlockhash": {
|
||||
"blockhash": "ComputeBudget111111111111111111111111111111",
|
||||
"lastValidBlockHeight": 430123999
|
||||
},
|
||||
"fee": 5000,
|
||||
"preBalances": [10000, 0],
|
||||
"postBalances": [5000, 5000],
|
||||
"preTokenBalances": [],
|
||||
"postTokenBalances": [],
|
||||
"loadedAddresses": {
|
||||
"writable": [],
|
||||
"readonly": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"total":600000000000000000,
|
||||
"circulating":500000000000000000,
|
||||
"nonCirculating":100000000000000000,
|
||||
"nonCirculatingAccounts":[
|
||||
"11111111111111111111111111111111",
|
||||
"Vote111111111111111111111111111111111111111"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 11
|
||||
// version: 19
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -10,7 +10,11 @@
|
||||
//! 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 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.
|
||||
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and all eleven Transaction wrappers through
|
||||
//! `pre.007`: eight reads, two write submissions with centralized no-resend protection, and retry-safe `simulateTransaction`, including complete
|
||||
//! modern/legacy `getTransaction` coverage. `0.2.4` completes the HTTP surface with all ten Blocks and five Economics wrappers, including complete
|
||||
//! modern/legacy `getBlock`, positional inflation rewards, runtime-provided economics values and the final `KSP-TRANSPORT-007` compliance target.
|
||||
//! The candidate surface therefore exposes typed wrappers for all 52 current audited Solana HTTP methods while retaining 14 removed historical descriptors.
|
||||
|
||||
mod client;
|
||||
mod constants;
|
||||
@@ -20,11 +24,14 @@ mod json_rpc;
|
||||
mod pool;
|
||||
mod resilience;
|
||||
mod rpc_accounts;
|
||||
mod rpc_blocks;
|
||||
mod rpc_canary;
|
||||
mod rpc_cluster;
|
||||
mod rpc_common;
|
||||
mod rpc_economics;
|
||||
mod rpc_method;
|
||||
mod rpc_tokens;
|
||||
mod rpc_transactions;
|
||||
mod settings;
|
||||
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
@@ -123,6 +130,28 @@ pub use self::rpc_accounts::SolanaProgramAccountFilter;
|
||||
pub use self::rpc_accounts::SolanaProgramAccountsConfig;
|
||||
/// Result union returned by `getProgramAccounts` with or without an RPC context.
|
||||
pub use self::rpc_accounts::SolanaProgramAccountsResult;
|
||||
/// Commitment distribution returned by `getBlockCommitment`.
|
||||
pub use self::rpc_blocks::SolanaBlockCommitment;
|
||||
/// Block-production counts returned by `getBlockProduction` before the shared RPC context wrapper is applied.
|
||||
pub use self::rpc_blocks::SolanaBlockProduction;
|
||||
/// Optional configuration accepted by `getBlockProduction`.
|
||||
pub use self::rpc_blocks::SolanaBlockProductionConfig;
|
||||
/// Slot range accepted inside `getBlockProduction` configuration.
|
||||
pub use self::rpc_blocks::SolanaBlockProductionRange;
|
||||
/// Effective range reported inside `getBlockProduction` results.
|
||||
pub use self::rpc_blocks::SolanaBlockProductionResultRange;
|
||||
/// One reward entry returned in a confirmed block.
|
||||
pub use self::rpc_blocks::SolanaBlockReward;
|
||||
/// One transaction entry returned inside a confirmed block.
|
||||
pub use self::rpc_blocks::SolanaBlockTransaction;
|
||||
/// Confirmed block wire result returned by `getBlock` when the RPC result is non-null.
|
||||
pub use self::rpc_blocks::SolanaConfirmedBlock;
|
||||
/// Modern configuration object accepted by `getBlock`.
|
||||
pub use self::rpc_blocks::SolanaGetBlockConfig;
|
||||
/// One entry returned by `getRecentPerformanceSamples`.
|
||||
pub use self::rpc_blocks::SolanaPerformanceSample;
|
||||
/// Transaction detail level accepted by modern `getBlock` requests.
|
||||
pub use self::rpc_blocks::SolanaTransactionDetails;
|
||||
/// Optional typed configuration for the `getBalance` canary.
|
||||
pub use self::rpc_canary::GetBalanceConfig;
|
||||
/// Typed lamport balance returned by the `getBalance` canary.
|
||||
@@ -169,6 +198,18 @@ pub use self::rpc_common::SolanaRpcResponse;
|
||||
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;
|
||||
/// Inflation-governor values returned by `getInflationGovernor`.
|
||||
pub use self::rpc_economics::SolanaInflationGovernor;
|
||||
/// Current inflation-rate values returned by `getInflationRate`.
|
||||
pub use self::rpc_economics::SolanaInflationRate;
|
||||
/// One non-null positional reward returned by `getInflationReward`.
|
||||
pub use self::rpc_economics::SolanaInflationReward;
|
||||
/// Optional epoch/context configuration accepted by `getInflationReward`.
|
||||
pub use self::rpc_economics::SolanaInflationRewardConfig;
|
||||
/// Supply totals returned inside the contextual `getSupply` response.
|
||||
pub use self::rpc_economics::SolanaSupply;
|
||||
/// Optional configuration accepted by `getSupply`.
|
||||
pub use self::rpc_economics::SolanaSupplyConfig;
|
||||
/// 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.
|
||||
@@ -197,6 +238,44 @@ pub use self::rpc_tokens::SolanaTokenAccountBalance;
|
||||
pub use self::rpc_tokens::SolanaTokenAccountSelector;
|
||||
/// Token amount returned by Solana HTTP token RPC methods.
|
||||
pub use self::rpc_tokens::SolanaTokenAmount;
|
||||
/// Confirmed transaction result returned by `getTransaction` when the RPC result is non-null.
|
||||
pub use self::rpc_transactions::SolanaConfirmedTransaction;
|
||||
/// Wire-preserving transaction payload returned by `getTransaction`.
|
||||
pub use self::rpc_transactions::SolanaEncodedTransaction;
|
||||
/// Modern configuration object accepted by `getTransaction`.
|
||||
pub use self::rpc_transactions::SolanaGetTransactionConfig;
|
||||
/// Blockhash information returned by `getLatestBlockhash` and optionally by simulation.
|
||||
pub use self::rpc_transactions::SolanaLatestBlockhash;
|
||||
/// One recent prioritization-fee sample returned by `getRecentPrioritizationFees`.
|
||||
pub use self::rpc_transactions::SolanaPrioritizationFee;
|
||||
/// Configuration accepted by `requestAirdrop`.
|
||||
pub use self::rpc_transactions::SolanaRequestAirdropConfig;
|
||||
/// Configuration accepted by `sendTransaction` without changing KSP transport retry semantics.
|
||||
pub use self::rpc_transactions::SolanaSendTransactionConfig;
|
||||
/// One ordered signature record returned by `getSignaturesForAddress`.
|
||||
pub use self::rpc_transactions::SolanaSignatureInfo;
|
||||
/// One non-null position returned by `getSignatureStatuses`.
|
||||
pub use self::rpc_transactions::SolanaSignatureStatus;
|
||||
/// Optional historical-search configuration accepted by `getSignatureStatuses`.
|
||||
pub use self::rpc_transactions::SolanaSignatureStatusesConfig;
|
||||
/// Pagination and context configuration accepted by `getSignaturesForAddress`.
|
||||
pub use self::rpc_transactions::SolanaSignaturesForAddressConfig;
|
||||
/// Configuration accepted by `simulateTransaction`.
|
||||
pub use self::rpc_transactions::SolanaSimulateTransactionConfig;
|
||||
/// Rich result payload returned inside the contextual `simulateTransaction` response.
|
||||
pub use self::rpc_transactions::SolanaSimulateTransactionResult;
|
||||
/// Account-return configuration nested under `simulateTransaction`.
|
||||
pub use self::rpc_transactions::SolanaSimulationAccountsConfig;
|
||||
/// Binary encoding accepted for serialized transaction input payloads.
|
||||
pub use self::rpc_transactions::SolanaTransactionBinaryEncoding;
|
||||
/// Confirmation state reported for a signature or transaction status.
|
||||
pub use self::rpc_transactions::SolanaTransactionConfirmationStatus;
|
||||
/// Encoding accepted by `getTransaction`, including its retained legacy `binary` alias.
|
||||
pub use self::rpc_transactions::SolanaTransactionEncoding;
|
||||
/// Transaction version reported by `getTransaction` when the version field is present.
|
||||
pub use self::rpc_transactions::SolanaTransactionVersion;
|
||||
/// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`.
|
||||
pub use self::rpc_transactions::SolanaWireField;
|
||||
/// Open cluster or network descriptor used by HTTP endpoint settings.
|
||||
pub use self::settings::HttpClusterName;
|
||||
/// Runtime settings for one role declared by an HTTP endpoint.
|
||||
|
||||
1063
crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
Normal file
1063
crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
Normal file
File diff suppressed because it is too large
Load Diff
591
crates/ksp-onchain-transport-lib/src/rpc_economics.rs
Normal file
591
crates/ksp-onchain-transport-lib/src/rpc_economics.rs
Normal file
@@ -0,0 +1,591 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_economics.rs
|
||||
// version: 5
|
||||
|
||||
/// Inflation-governor values returned by `getInflationGovernor`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SolanaInflationGovernor {
|
||||
initial: f64,
|
||||
terminal: f64,
|
||||
taper: f64,
|
||||
foundation: f64,
|
||||
foundation_term: f64,
|
||||
}
|
||||
|
||||
impl SolanaInflationGovernor {
|
||||
/// Returns the initial inflation rate.
|
||||
#[must_use]
|
||||
pub const fn initial(&self) -> f64 {
|
||||
return self.initial;
|
||||
}
|
||||
|
||||
/// Returns the terminal inflation rate.
|
||||
#[must_use]
|
||||
pub const fn terminal(&self) -> f64 {
|
||||
return self.terminal;
|
||||
}
|
||||
|
||||
/// Returns the taper/disinflation parameter reported by the runtime.
|
||||
#[must_use]
|
||||
pub const fn taper(&self) -> f64 {
|
||||
return self.taper;
|
||||
}
|
||||
|
||||
/// Returns the foundation allocation rate.
|
||||
#[must_use]
|
||||
pub const fn foundation(&self) -> f64 {
|
||||
return self.foundation;
|
||||
}
|
||||
|
||||
/// Returns the foundation allocation term.
|
||||
#[must_use]
|
||||
pub const fn foundation_term(&self) -> f64 {
|
||||
return self.foundation_term;
|
||||
}
|
||||
|
||||
/// Decodes an inflation-governor result from its 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::<WireInflationGovernor>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
|
||||
initial: wire.initial,
|
||||
terminal: wire.terminal,
|
||||
taper: wire.taper,
|
||||
foundation: wire.foundation,
|
||||
foundation_term: wire.foundation_term,
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Current inflation-rate values returned by `getInflationRate`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SolanaInflationRate {
|
||||
total: f64,
|
||||
validator: f64,
|
||||
foundation: f64,
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
impl SolanaInflationRate {
|
||||
/// Returns the total inflation rate.
|
||||
#[must_use]
|
||||
pub const fn total(&self) -> f64 {
|
||||
return self.total;
|
||||
}
|
||||
|
||||
/// Returns the validator inflation rate.
|
||||
#[must_use]
|
||||
pub const fn validator(&self) -> f64 {
|
||||
return self.validator;
|
||||
}
|
||||
|
||||
/// Returns the foundation inflation rate.
|
||||
#[must_use]
|
||||
pub const fn foundation(&self) -> f64 {
|
||||
return self.foundation;
|
||||
}
|
||||
|
||||
/// Returns the epoch associated with the rate.
|
||||
#[must_use]
|
||||
pub const fn epoch(&self) -> u64 {
|
||||
return self.epoch;
|
||||
}
|
||||
|
||||
/// Decodes an inflation-rate result from its 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::<WireInflationRate>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => {
|
||||
std::result::Result::Ok(Self { total: wire.total, validator: wire.validator, foundation: wire.foundation, epoch: wire.epoch })
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional epoch/context configuration accepted by `getInflationReward`.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaInflationRewardConfig {
|
||||
epoch: std::option::Option<u64>,
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaInflationRewardConfig {
|
||||
/// Creates an inflation-reward configuration.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
epoch: std::option::Option<u64>,
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
) -> Self {
|
||||
return Self { epoch, commitment, min_context_slot };
|
||||
}
|
||||
|
||||
/// Returns the optional target epoch.
|
||||
#[must_use]
|
||||
pub const fn epoch(&self) -> std::option::Option<u64> {
|
||||
return self.epoch;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// Returns whether this config would serialize to an empty object.
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return self.epoch.is_none() && self.commitment.is_none() && self.min_context_slot.is_none();
|
||||
}
|
||||
|
||||
/// Serializes this config to the exact Solana JSON-RPC 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(epoch) = self.epoch {
|
||||
object.insert("epoch".to_owned(), serde_json::Value::Number(epoch.into()));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// One non-null positional reward returned by `getInflationReward`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaInflationReward {
|
||||
epoch: u64,
|
||||
effective_slot: u64,
|
||||
amount: u64,
|
||||
post_balance: u64,
|
||||
commission: std::option::Option<u8>,
|
||||
commission_bps: crate::SolanaWireField<u16>,
|
||||
}
|
||||
|
||||
impl SolanaInflationReward {
|
||||
/// Returns the rewarded epoch.
|
||||
#[must_use]
|
||||
pub const fn epoch(&self) -> u64 {
|
||||
return self.epoch;
|
||||
}
|
||||
|
||||
/// Returns the slot at which the reward became effective.
|
||||
#[must_use]
|
||||
pub const fn effective_slot(&self) -> u64 {
|
||||
return self.effective_slot;
|
||||
}
|
||||
|
||||
/// Returns the reward amount in lamports.
|
||||
#[must_use]
|
||||
pub const fn amount(&self) -> u64 {
|
||||
return self.amount;
|
||||
}
|
||||
|
||||
/// Returns the post-reward account balance in lamports.
|
||||
#[must_use]
|
||||
pub const fn post_balance(&self) -> u64 {
|
||||
return self.post_balance;
|
||||
}
|
||||
|
||||
/// Returns the nullable legacy/effective commission percentage.
|
||||
#[must_use]
|
||||
pub const fn commission(&self) -> std::option::Option<u8> {
|
||||
return self.commission;
|
||||
}
|
||||
|
||||
/// Returns the basis-point commission while preserving omitted/null/present wire states.
|
||||
#[must_use]
|
||||
pub const fn commission_bps(&self) -> &crate::SolanaWireField<u16> {
|
||||
return &self.commission_bps;
|
||||
}
|
||||
|
||||
/// Decodes one non-null inflation reward from its 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::<WireInflationReward>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
|
||||
epoch: wire.epoch,
|
||||
effective_slot: wire.effective_slot,
|
||||
amount: wire.amount,
|
||||
post_balance: wire.post_balance,
|
||||
commission: wire.commission,
|
||||
commission_bps: wire.commission_bps,
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional configuration accepted by `getSupply`.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaSupplyConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
exclude_non_circulating_accounts_list: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
impl SolanaSupplyConfig {
|
||||
/// Creates a supply configuration.
|
||||
#[must_use]
|
||||
pub const fn new(commitment: std::option::Option<crate::SolanaCommitment>, exclude_non_circulating_accounts_list: std::option::Option<bool>) -> Self {
|
||||
return Self { commitment, exclude_non_circulating_accounts_list };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Returns whether the request explicitly controls omission of the non-circulating account list.
|
||||
#[must_use]
|
||||
pub const fn exclude_non_circulating_accounts_list(&self) -> std::option::Option<bool> {
|
||||
return self.exclude_non_circulating_accounts_list;
|
||||
}
|
||||
|
||||
/// Returns whether this config would serialize to an empty object.
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return self.commitment.is_none() && self.exclude_non_circulating_accounts_list.is_none();
|
||||
}
|
||||
|
||||
/// Serializes this config to the exact Solana JSON-RPC 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(exclude) = self.exclude_non_circulating_accounts_list {
|
||||
object.insert("excludeNonCirculatingAccountsList".to_owned(), serde_json::Value::Bool(exclude));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Supply totals returned inside the contextual `getSupply` response.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaSupply {
|
||||
total: u64,
|
||||
circulating: u64,
|
||||
non_circulating: u64,
|
||||
non_circulating_accounts: std::vec::Vec<ksp_core_lib::Pubkey>,
|
||||
}
|
||||
|
||||
impl SolanaSupply {
|
||||
/// Returns the total supply in lamports.
|
||||
#[must_use]
|
||||
pub const fn total(&self) -> u64 {
|
||||
return self.total;
|
||||
}
|
||||
|
||||
/// Returns the circulating supply in lamports.
|
||||
#[must_use]
|
||||
pub const fn circulating(&self) -> u64 {
|
||||
return self.circulating;
|
||||
}
|
||||
|
||||
/// Returns the non-circulating supply in lamports.
|
||||
#[must_use]
|
||||
pub const fn non_circulating(&self) -> u64 {
|
||||
return self.non_circulating;
|
||||
}
|
||||
|
||||
/// Returns the ordered non-circulating account list supplied by the runtime.
|
||||
#[must_use]
|
||||
pub fn non_circulating_accounts(&self) -> &[ksp_core_lib::Pubkey] {
|
||||
return self.non_circulating_accounts.as_slice();
|
||||
}
|
||||
|
||||
/// Decodes a supply result from its 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::<WireSupply>(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 accounts = std::vec::Vec::with_capacity(wire.non_circulating_accounts.len());
|
||||
for account in wire.non_circulating_accounts {
|
||||
let parsed = crate::parse_wire_pubkey(method, "nonCirculatingAccounts", account.as_str());
|
||||
match parsed {
|
||||
std::result::Result::Ok(pubkey) => accounts.push(pubkey),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(Self {
|
||||
total: wire.total,
|
||||
circulating: wire.circulating,
|
||||
non_circulating: wire.non_circulating,
|
||||
non_circulating_accounts: accounts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::HttpTransportPool {
|
||||
/// Executes typed `getInflationGovernor` and returns the runtime-provided schedule values unchanged.
|
||||
pub async fn get_inflation_governor(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaInflationGovernor> {
|
||||
let mut params = std::vec::Vec::new();
|
||||
push_economics_commitment_config(&mut params, config);
|
||||
let value = self.execute_economics_rpc("getInflationGovernor", role, params).await;
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => crate::SolanaInflationGovernor::decode_wire("getInflationGovernor", value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getInflationRate` without locally recalculating the inflation schedule.
|
||||
pub async fn get_inflation_rate(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<crate::SolanaInflationRate> {
|
||||
let value = self.execute_economics_rpc("getInflationRate", role, std::vec::Vec::new()).await;
|
||||
return match value {
|
||||
std::result::Result::Ok(value) => crate::SolanaInflationRate::decode_wire("getInflationRate", value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getInflationReward`, preserving input order, positional nulls and runtime commission extensions.
|
||||
pub async fn get_inflation_reward(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
addresses: &[ksp_core_lib::Pubkey],
|
||||
config: std::option::Option<&crate::SolanaInflationRewardConfig>,
|
||||
) -> ksp_core_lib::Result<std::vec::Vec<std::option::Option<crate::SolanaInflationReward>>> {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
|
||||
"getInflationReward commitment must be confirmed or finalized when explicitly provided",
|
||||
)
|
||||
.with_context("rpc_method", "getInflationReward")
|
||||
.with_context("commitment", "processed"),
|
||||
);
|
||||
}
|
||||
let address_values = addresses.iter().map(|address| return serde_json::Value::String(address.to_string())).collect::<std::vec::Vec<_>>();
|
||||
let mut params = std::vec![serde_json::Value::Array(address_values)];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
{
|
||||
params.push((*config).to_json_value());
|
||||
}
|
||||
let value = self.execute_economics_rpc("getInflationReward", 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_inflation_rewards("getInflationReward", value, addresses.len());
|
||||
}
|
||||
|
||||
/// Executes typed `getStakeMinimumDelegation` and preserves the contextual runtime value in lamports.
|
||||
pub async fn get_stake_minimum_delegation(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
config: std::option::Option<&crate::SolanaContextConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<u64>> {
|
||||
let mut params = std::vec::Vec::new();
|
||||
push_economics_context_config(&mut params, config);
|
||||
let value = self.execute_economics_rpc("getStakeMinimumDelegation", 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_economics_u64_response("getStakeMinimumDelegation", value);
|
||||
}
|
||||
|
||||
/// Executes typed `getSupply`, preserving the explicit account-list exclusion flag and contextual totals.
|
||||
pub async fn get_supply(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
config: std::option::Option<&crate::SolanaSupplyConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaSupply>> {
|
||||
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_economics_rpc("getSupply", 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_supply_response("getSupply", value);
|
||||
}
|
||||
|
||||
async fn execute_economics_rpc(
|
||||
&self,
|
||||
method_name: &'static str,
|
||||
role: &crate::HttpRoleName,
|
||||
params: std::vec::Vec<serde_json::Value>,
|
||||
) -> ksp_core_lib::Result<serde_json::Value> {
|
||||
let descriptor = economics_descriptor(method_name);
|
||||
let descriptor = match descriptor {
|
||||
std::result::Result::Ok(descriptor) => descriptor,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return self.execute_standard_rpc(role, descriptor, params).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn push_economics_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 push_economics_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_inflation_rewards(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
expected_count: usize,
|
||||
) -> ksp_core_lib::Result<std::vec::Vec<std::option::Option<crate::SolanaInflationReward>>> {
|
||||
let decoded = crate::decode_wire_json::<std::vec::Vec<std::option::Option<serde_json::Value>>>(method, value);
|
||||
let values = match decoded {
|
||||
std::result::Result::Ok(values) => values,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if values.len() != expected_count {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "getInflationReward result count does not match the requested address count")
|
||||
.with_context("rpc_method", method)
|
||||
.with_context("expected_count", expected_count.to_string())
|
||||
.with_context("actual_count", values.len().to_string()),
|
||||
);
|
||||
}
|
||||
let mut rewards = std::vec::Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
match value {
|
||||
std::option::Option::Some(value) => {
|
||||
let reward = crate::SolanaInflationReward::decode_wire(method, value);
|
||||
match reward {
|
||||
std::result::Result::Ok(reward) => rewards.push(std::option::Option::Some(reward)),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => rewards.push(std::option::Option::None),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(rewards);
|
||||
}
|
||||
|
||||
fn decode_economics_u64_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaRpcResponse<u64>> {
|
||||
let decoded = crate::decode_wire_json::<WireEconomicsRpcResponse<u64>>(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);
|
||||
return match context {
|
||||
std::result::Result::Ok(context) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, wire.value)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_supply_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaSupply>> {
|
||||
let decoded = crate::decode_wire_json::<WireEconomicsRpcResponse<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 supply = crate::SolanaSupply::decode_wire(method, wire.value);
|
||||
return match supply {
|
||||
std::result::Result::Ok(supply) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, supply)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
fn economics_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::Economics && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_4 =>
|
||||
{
|
||||
std::result::Result::Ok(descriptor)
|
||||
},
|
||||
_ => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Economics descriptor is missing from the audited 0.2.4 registry")
|
||||
.with_context("rpc_method", method),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireEconomicsRpcResponse<T> {
|
||||
context: serde_json::Value,
|
||||
value: T,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationGovernor {
|
||||
initial: f64,
|
||||
terminal: f64,
|
||||
taper: f64,
|
||||
foundation: f64,
|
||||
foundation_term: f64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationRate {
|
||||
total: f64,
|
||||
validator: f64,
|
||||
foundation: f64,
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationReward {
|
||||
epoch: u64,
|
||||
effective_slot: u64,
|
||||
amount: u64,
|
||||
post_balance: u64,
|
||||
#[serde(default)]
|
||||
commission: std::option::Option<u8>,
|
||||
#[serde(default)]
|
||||
commission_bps: crate::SolanaWireField<u16>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSupply {
|
||||
total: u64,
|
||||
circulating: u64,
|
||||
non_circulating: u64,
|
||||
non_circulating_accounts: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_economics.rs"]
|
||||
mod tests;
|
||||
1808
crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
Normal file
1808
crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 9
|
||||
// version: 23
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -243,3 +243,282 @@ fn public_pre_006_remaining_cluster_wrappers_are_available_from_crate_root() {
|
||||
let request = ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::default();
|
||||
assert!(matches!(request, ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::CurrentEpoch(std::option::Option::None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_002_shared_types_are_constructible_from_crate_root() {
|
||||
let get_transaction = ksp_onchain_transport_lib::SolanaGetTransactionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed),
|
||||
std::option::Option::Some(0),
|
||||
);
|
||||
assert_eq!(get_transaction.max_supported_transaction_version(), std::option::Option::Some(0));
|
||||
let send = ksp_onchain_transport_lib::SolanaSendTransactionConfig::new(
|
||||
std::option::Option::Some(false),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
|
||||
std::option::Option::Some(3),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(send.max_retries(), std::option::Option::Some(3));
|
||||
let pagination = ksp_onchain_transport_lib::SolanaSignaturesForAddressConfig::new(
|
||||
std::option::Option::Some("before".to_owned()),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(10),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(pagination.limit(), std::option::Option::Some(10));
|
||||
assert!(ksp_onchain_transport_lib::SolanaWireField::<u64>::Omitted.is_omitted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_003_read_wrappers_are_available_from_crate_root() {
|
||||
let _get_fee_for_message = ksp_onchain_transport_lib::HttpTransportPool::get_fee_for_message;
|
||||
let _get_latest_blockhash = ksp_onchain_transport_lib::HttpTransportPool::get_latest_blockhash;
|
||||
let _get_transaction_count = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_count;
|
||||
let _is_blockhash_valid = ksp_onchain_transport_lib::HttpTransportPool::is_blockhash_valid;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_004_additional_read_wrappers_are_available_from_crate_root() {
|
||||
let _get_recent_prioritization_fees = ksp_onchain_transport_lib::HttpTransportPool::get_recent_prioritization_fees;
|
||||
let _get_signatures_for_address = ksp_onchain_transport_lib::HttpTransportPool::get_signatures_for_address;
|
||||
let _get_signature_statuses = ksp_onchain_transport_lib::HttpTransportPool::get_signature_statuses;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn public_transaction_pre_005_get_transaction_complete_request_forms_are_available_from_crate_root() {
|
||||
let _get_transaction = ksp_onchain_transport_lib::HttpTransportPool::get_transaction;
|
||||
let _get_transaction_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_legacy;
|
||||
let config = ksp_onchain_transport_lib::SolanaGetTransactionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed),
|
||||
std::option::Option::Some(0),
|
||||
);
|
||||
assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed));
|
||||
assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_006_write_wrappers_and_complete_configs_are_available_from_crate_root() {
|
||||
let _request_airdrop = ksp_onchain_transport_lib::HttpTransportPool::request_airdrop;
|
||||
let _send_transaction = ksp_onchain_transport_lib::HttpTransportPool::send_transaction;
|
||||
let airdrop = ksp_onchain_transport_lib::SolanaRequestAirdropConfig::new(
|
||||
std::option::Option::Some("recent-blockhash".to_owned()),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
);
|
||||
assert_eq!(airdrop.recent_blockhash(), std::option::Option::Some("recent-blockhash"));
|
||||
let send = ksp_onchain_transport_lib::SolanaSendTransactionConfig::new(
|
||||
std::option::Option::Some(false),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
|
||||
std::option::Option::Some(5),
|
||||
std::option::Option::Some(431_000_000),
|
||||
);
|
||||
assert_eq!(send.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64));
|
||||
assert_eq!(send.max_retries(), std::option::Option::Some(5));
|
||||
assert_eq!(send.min_context_slot(), std::option::Option::Some(431_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_transaction_pre_007_simulation_wrapper_and_complete_config_are_available_from_crate_root() {
|
||||
let _simulate_transaction = ksp_onchain_transport_lib::HttpTransportPool::simulate_transaction;
|
||||
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let accounts = ksp_onchain_transport_lib::SolanaSimulationAccountsConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
|
||||
std::vec![address],
|
||||
);
|
||||
let config = ksp_onchain_transport_lib::SolanaSimulateTransactionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
|
||||
std::option::Option::Some(false),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(431_000_000),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(accounts),
|
||||
);
|
||||
assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed));
|
||||
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64));
|
||||
assert_eq!(config.sig_verify(), std::option::Option::Some(true));
|
||||
assert_eq!(
|
||||
config.accounts().and_then(ksp_onchain_transport_lib::SolanaSimulationAccountsConfig::encoding),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_4_pre_002_blocks_economics_types_are_available_from_crate_root() {
|
||||
let block = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Json),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(1),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
assert_eq!(block.max_supported_transaction_version(), std::option::Option::Some(1));
|
||||
let range = ksp_onchain_transport_lib::SolanaBlockProductionRange::new(100, std::option::Option::Some(200));
|
||||
assert_eq!(range.last_slot(), std::option::Option::Some(200));
|
||||
let production_config = ksp_onchain_transport_lib::SolanaBlockProductionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(range),
|
||||
);
|
||||
assert_eq!(production_config.range().map(ksp_onchain_transport_lib::SolanaBlockProductionRange::first_slot), std::option::Option::Some(100));
|
||||
let inflation = ksp_onchain_transport_lib::SolanaInflationRewardConfig::new(
|
||||
std::option::Option::Some(812),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(430_000_000),
|
||||
);
|
||||
assert_eq!(inflation.epoch(), std::option::Option::Some(812));
|
||||
let supply = ksp_onchain_transport_lib::SolanaSupplyConfig::new(std::option::Option::None, std::option::Option::Some(false));
|
||||
assert_eq!(supply.exclude_non_circulating_accounts_list(), std::option::Option::Some(false));
|
||||
let _block_commitment: std::option::Option<ksp_onchain_transport_lib::SolanaBlockCommitment> = std::option::Option::None;
|
||||
let _block_production: std::option::Option<ksp_onchain_transport_lib::SolanaBlockProduction> = std::option::Option::None;
|
||||
let _block_production_result_range: std::option::Option<ksp_onchain_transport_lib::SolanaBlockProductionResultRange> = std::option::Option::None;
|
||||
let _block_reward: std::option::Option<ksp_onchain_transport_lib::SolanaBlockReward> = std::option::Option::None;
|
||||
let _block_transaction: std::option::Option<ksp_onchain_transport_lib::SolanaBlockTransaction> = std::option::Option::None;
|
||||
let _confirmed_block: std::option::Option<ksp_onchain_transport_lib::SolanaConfirmedBlock> = std::option::Option::None;
|
||||
let _performance_sample: std::option::Option<ksp_onchain_transport_lib::SolanaPerformanceSample> = std::option::Option::None;
|
||||
let _governor: std::option::Option<ksp_onchain_transport_lib::SolanaInflationGovernor> = std::option::Option::None;
|
||||
let _rate: std::option::Option<ksp_onchain_transport_lib::SolanaInflationRate> = std::option::Option::None;
|
||||
let _reward: std::option::Option<ksp_onchain_transport_lib::SolanaInflationReward> = std::option::Option::None;
|
||||
let _supply: std::option::Option<ksp_onchain_transport_lib::SolanaSupply> = std::option::Option::None;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_4_pre_003_simple_block_wrappers_are_available_from_crate_root() {
|
||||
let _get_block_commitment = ksp_onchain_transport_lib::HttpTransportPool::get_block_commitment;
|
||||
let _get_block_height = ksp_onchain_transport_lib::HttpTransportPool::get_block_height;
|
||||
let _get_block_time = ksp_onchain_transport_lib::HttpTransportPool::get_block_time;
|
||||
let _get_first_available_block = ksp_onchain_transport_lib::HttpTransportPool::get_first_available_block;
|
||||
let _minimum_ledger_slot = ksp_onchain_transport_lib::HttpTransportPool::minimum_ledger_slot;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_4_pre_004_range_and_performance_wrappers_are_available_from_crate_root() {
|
||||
let _get_blocks = ksp_onchain_transport_lib::HttpTransportPool::get_blocks;
|
||||
let _get_blocks_with_limit = ksp_onchain_transport_lib::HttpTransportPool::get_blocks_with_limit;
|
||||
let _get_recent_performance_samples = ksp_onchain_transport_lib::HttpTransportPool::get_recent_performance_samples;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_4_pre_005_block_production_wrapper_is_available_from_crate_root() {
|
||||
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("public fixture identity must parse");
|
||||
let range = ksp_onchain_transport_lib::SolanaBlockProductionRange::new(10, std::option::Option::Some(20));
|
||||
let config = ksp_onchain_transport_lib::SolanaBlockProductionConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(identity),
|
||||
std::option::Option::Some(range),
|
||||
);
|
||||
assert_eq!(config.identity(), std::option::Option::Some(&identity));
|
||||
assert_eq!(config.range().expect("public range must exist").last_slot(), std::option::Option::Some(20));
|
||||
let _method = ksp_onchain_transport_lib::HttpTransportPool::get_block_production;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn public_v0_2_4_pre_006_get_block_complete_request_forms_are_available_from_crate_root() {
|
||||
let _get_block = ksp_onchain_transport_lib::HttpTransportPool::get_block;
|
||||
let _get_block_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_block_legacy;
|
||||
let config = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Accounts),
|
||||
std::option::Option::Some(1),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed));
|
||||
assert_eq!(config.transaction_details(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Accounts));
|
||||
assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(1));
|
||||
assert_eq!(config.rewards(), std::option::Option::Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_4_pre_007_simple_economics_wrappers_are_available_from_crate_root() {
|
||||
let _get_inflation_governor = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_governor;
|
||||
let _get_inflation_rate = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_rate;
|
||||
let _get_stake_minimum_delegation = ksp_onchain_transport_lib::HttpTransportPool::get_stake_minimum_delegation;
|
||||
let _get_supply = ksp_onchain_transport_lib::HttpTransportPool::get_supply;
|
||||
|
||||
let supply_config = ksp_onchain_transport_lib::SolanaSupplyConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(false),
|
||||
);
|
||||
assert_eq!(supply_config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed));
|
||||
assert_eq!(supply_config.exclude_non_circulating_accounts_list(), std::option::Option::Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_4_pre_008_inflation_reward_wrapper_is_available_from_crate_root() {
|
||||
let _get_inflation_reward = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_reward;
|
||||
let config = ksp_onchain_transport_lib::SolanaInflationRewardConfig::new(
|
||||
std::option::Option::Some(912),
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(431_000_000),
|
||||
);
|
||||
assert_eq!(config.epoch(), std::option::Option::Some(912));
|
||||
assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||
assert_eq!(config.min_context_slot(), std::option::Option::Some(431_000_000));
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn public_v0_2_4_pre_009_all_52_current_typed_wrappers_and_legacy_forms_are_available_from_crate_root() {
|
||||
let _get_account_info = ksp_onchain_transport_lib::HttpTransportPool::get_account_info;
|
||||
let _get_balance = ksp_onchain_transport_lib::HttpTransportPool::get_balance;
|
||||
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;
|
||||
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 _get_fee_for_message = ksp_onchain_transport_lib::HttpTransportPool::get_fee_for_message;
|
||||
let _get_latest_blockhash = ksp_onchain_transport_lib::HttpTransportPool::get_latest_blockhash;
|
||||
let _get_recent_prioritization_fees = ksp_onchain_transport_lib::HttpTransportPool::get_recent_prioritization_fees;
|
||||
let _get_signatures_for_address = ksp_onchain_transport_lib::HttpTransportPool::get_signatures_for_address;
|
||||
let _get_signature_statuses = ksp_onchain_transport_lib::HttpTransportPool::get_signature_statuses;
|
||||
let _get_transaction = ksp_onchain_transport_lib::HttpTransportPool::get_transaction;
|
||||
let _get_transaction_count = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_count;
|
||||
let _is_blockhash_valid = ksp_onchain_transport_lib::HttpTransportPool::is_blockhash_valid;
|
||||
let _request_airdrop = ksp_onchain_transport_lib::HttpTransportPool::request_airdrop;
|
||||
let _send_transaction = ksp_onchain_transport_lib::HttpTransportPool::send_transaction;
|
||||
let _simulate_transaction = ksp_onchain_transport_lib::HttpTransportPool::simulate_transaction;
|
||||
let _get_block = ksp_onchain_transport_lib::HttpTransportPool::get_block;
|
||||
let _get_block_commitment = ksp_onchain_transport_lib::HttpTransportPool::get_block_commitment;
|
||||
let _get_block_height = ksp_onchain_transport_lib::HttpTransportPool::get_block_height;
|
||||
let _get_block_production = ksp_onchain_transport_lib::HttpTransportPool::get_block_production;
|
||||
let _get_blocks = ksp_onchain_transport_lib::HttpTransportPool::get_blocks;
|
||||
let _get_blocks_with_limit = ksp_onchain_transport_lib::HttpTransportPool::get_blocks_with_limit;
|
||||
let _get_block_time = ksp_onchain_transport_lib::HttpTransportPool::get_block_time;
|
||||
let _get_first_available_block = ksp_onchain_transport_lib::HttpTransportPool::get_first_available_block;
|
||||
let _get_recent_performance_samples = ksp_onchain_transport_lib::HttpTransportPool::get_recent_performance_samples;
|
||||
let _minimum_ledger_slot = ksp_onchain_transport_lib::HttpTransportPool::minimum_ledger_slot;
|
||||
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_genesis_hash = ksp_onchain_transport_lib::HttpTransportPool::get_genesis_hash;
|
||||
let _get_health = ksp_onchain_transport_lib::HttpTransportPool::get_health;
|
||||
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_leader_schedule = ksp_onchain_transport_lib::HttpTransportPool::get_leader_schedule;
|
||||
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;
|
||||
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_version = ksp_onchain_transport_lib::HttpTransportPool::get_version;
|
||||
let _get_vote_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_vote_accounts;
|
||||
let _get_inflation_governor = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_governor;
|
||||
let _get_inflation_rate = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_rate;
|
||||
let _get_inflation_reward = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_reward;
|
||||
let _get_stake_minimum_delegation = ksp_onchain_transport_lib::HttpTransportPool::get_stake_minimum_delegation;
|
||||
let _get_supply = ksp_onchain_transport_lib::HttpTransportPool::get_supply;
|
||||
let _get_block_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_block_legacy;
|
||||
let _get_transaction_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_legacy;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 6
|
||||
// version: 21
|
||||
|
||||
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
||||
//! Release-level completeness canaries for the staged HTTP wrapper sequence.
|
||||
|
||||
#[test]
|
||||
fn release_registry_partition_matches_the_audited_http_plan() {
|
||||
@@ -194,3 +194,542 @@ fn release_pre_007_v0_2_2_exact_set_is_complete_without_advancing_future_release
|
||||
assert_eq!(future_transactions, 11);
|
||||
assert_eq!(future_blocks_economics, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_3_descriptor_set_and_safety_classes_are_exact() {
|
||||
let mut expected = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
"requestAirdrop",
|
||||
"sendTransaction",
|
||||
"simulateTransaction",
|
||||
];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
let mut reads = 0_usize;
|
||||
let mut writes = 0_usize;
|
||||
let mut simulations = 0_usize;
|
||||
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() != ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3 {
|
||||
continue;
|
||||
}
|
||||
actual.push(descriptor.method());
|
||||
match descriptor.operation_kind() {
|
||||
ksp_onchain_transport_lib::RpcOperationKind::Read => {
|
||||
reads += 1;
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
},
|
||||
ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission => {
|
||||
writes += 1;
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
},
|
||||
ksp_onchain_transport_lib::RpcOperationKind::Simulation => {
|
||||
simulations += 1;
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
},
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(reads, 8);
|
||||
assert_eq!(writes, 2);
|
||||
assert_eq!(simulations, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_003_transaction_read_subset_is_exact_and_retry_safe() {
|
||||
let mut expected = std::vec!["getFeeForMessage", "getLatestBlockhash", "getTransactionCount", "isBlockhashValid"];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for method_name in &expected {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.003 Transaction descriptor must exist");
|
||||
actual.push(descriptor.method());
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_004_transaction_read_subset_is_exact_and_retry_safe() {
|
||||
let mut expected = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
];
|
||||
let deferred = std::vec!["getTransaction", "requestAirdrop", "sendTransaction", "simulateTransaction"];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for method_name in &expected {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.004 Transaction descriptor must exist");
|
||||
actual.push(descriptor.method());
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
for method_name in &deferred {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("deferred Transaction descriptor must remain registered");
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 7);
|
||||
assert_eq!(deferred.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_005_transaction_read_subset_adds_complete_get_transaction_without_advancing_writes_or_simulation() {
|
||||
let mut expected = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
];
|
||||
let deferred = std::vec!["requestAirdrop", "sendTransaction", "simulateTransaction"];
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for method_name in &expected {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.005 Transaction descriptor must exist");
|
||||
actual.push(descriptor.method());
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
let get_transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
|
||||
assert!(get_transaction.request_form_status().has_deprecated_legacy());
|
||||
for method_name in &deferred {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("deferred Transaction descriptor must remain registered");
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 8);
|
||||
assert_eq!(deferred.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_006_transaction_subset_adds_both_write_submissions_without_advancing_simulation() {
|
||||
let reads = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
];
|
||||
let writes = std::vec!["requestAirdrop", "sendTransaction"];
|
||||
for method_name in &reads {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.006 read descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
for method_name in &writes {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.006 write descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
}
|
||||
let simulation = ksp_onchain_transport_lib::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must remain registered");
|
||||
assert_eq!(simulation.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Simulation);
|
||||
assert_eq!(reads.len(), 8);
|
||||
assert_eq!(writes.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_007_completes_all_v0_2_3_transaction_wrappers_with_exact_safety_classes() {
|
||||
let reads = std::vec![
|
||||
"getFeeForMessage",
|
||||
"getLatestBlockhash",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"isBlockhashValid",
|
||||
];
|
||||
let writes = std::vec!["requestAirdrop", "sendTransaction"];
|
||||
let simulations = std::vec!["simulateTransaction"];
|
||||
for method_name in &reads {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 read descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
for method_name in &writes {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 write descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
}
|
||||
for method_name in &simulations {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 simulation descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Simulation);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
assert_eq!(reads.len(), 8);
|
||||
assert_eq!(writes.len(), 2);
|
||||
assert_eq!(simulations.len(), 1);
|
||||
assert_eq!(reads.len() + writes.len() + simulations.len(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_008_ksp_transport_007_retro_audit_covers_all_typed_current_methods() {
|
||||
let mut audited = std::vec::Vec::<&str>::new();
|
||||
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||
if matches!(
|
||||
descriptor.coverage_release(),
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_1
|
||||
| ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2
|
||||
| ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3
|
||||
) {
|
||||
audited.push(descriptor.method());
|
||||
assert_eq!(descriptor.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Supported);
|
||||
match descriptor.operation_kind() {
|
||||
ksp_onchain_transport_lib::RpcOperationKind::Read | ksp_onchain_transport_lib::RpcOperationKind::Simulation => {
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
},
|
||||
ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission => {
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
audited.sort_unstable();
|
||||
assert_eq!(audited.len(), 37);
|
||||
assert_eq!(audited.first(), std::option::Option::Some(&"getAccountInfo"));
|
||||
assert_eq!(audited.last(), std::option::Option::Some(&"simulateTransaction"));
|
||||
let get_transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
|
||||
assert!(get_transaction.request_form_status().has_deprecated_legacy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_4_descriptor_set_is_exact_and_remains_read_retry_safe_through_wrapper_completion() {
|
||||
let mut expected_blocks = std::vec![
|
||||
"getBlock",
|
||||
"getBlockCommitment",
|
||||
"getBlockHeight",
|
||||
"getBlockProduction",
|
||||
"getBlocks",
|
||||
"getBlocksWithLimit",
|
||||
"getBlockTime",
|
||||
"getFirstAvailableBlock",
|
||||
"getRecentPerformanceSamples",
|
||||
"minimumLedgerSlot",
|
||||
];
|
||||
let mut expected_economics = std::vec!["getInflationGovernor", "getInflationRate", "getInflationReward", "getStakeMinimumDelegation", "getSupply",];
|
||||
let mut actual_blocks = std::vec::Vec::new();
|
||||
let mut actual_economics = 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_4 {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
match descriptor.category() {
|
||||
ksp_onchain_transport_lib::HttpRpcCategory::Blocks => actual_blocks.push(descriptor.method()),
|
||||
ksp_onchain_transport_lib::HttpRpcCategory::Economics => actual_economics.push(descriptor.method()),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
actual_blocks.sort_unstable();
|
||||
actual_economics.sort_unstable();
|
||||
expected_blocks.sort_unstable();
|
||||
expected_economics.sort_unstable();
|
||||
assert_eq!(actual_blocks, expected_blocks);
|
||||
assert_eq!(actual_economics, expected_economics);
|
||||
assert_eq!(actual_blocks.len(), 10);
|
||||
assert_eq!(actual_economics.len(), 5);
|
||||
let get_block = ksp_onchain_transport_lib::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
|
||||
assert!(get_block.request_form_status().has_deprecated_legacy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_4_pre_003_simple_blocks_subset_is_exact_and_retry_safe() {
|
||||
let mut expected = std::vec!["getBlockCommitment", "getBlockHeight", "getBlockTime", "getFirstAvailableBlock", "minimumLedgerSlot"];
|
||||
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_4
|
||||
|| descriptor.category() != ksp_onchain_transport_lib::HttpRpcCategory::Blocks
|
||||
{
|
||||
continue;
|
||||
}
|
||||
match descriptor.method() {
|
||||
"getBlockCommitment" | "getBlockHeight" | "getBlockTime" | "getFirstAvailableBlock" | "minimumLedgerSlot" => {
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
actual.push(descriptor.method());
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_4_pre_004_range_performance_subset_is_exact_and_retry_safe() {
|
||||
let mut expected = std::vec!["getBlocks", "getBlocksWithLimit", "getRecentPerformanceSamples"];
|
||||
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_4
|
||||
|| descriptor.category() != ksp_onchain_transport_lib::HttpRpcCategory::Blocks
|
||||
{
|
||||
continue;
|
||||
}
|
||||
match descriptor.method() {
|
||||
"getBlocks" | "getBlocksWithLimit" | "getRecentPerformanceSamples" => {
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
actual.push(descriptor.method());
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_4_pre_005_block_production_subset_is_exact_and_retry_safe() {
|
||||
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method("getBlockProduction").expect("getBlockProduction descriptor must exist");
|
||||
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Blocks);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4);
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_4_pre_006_get_block_completes_all_ten_block_wrappers_with_legacy_marker() {
|
||||
let expected = std::vec![
|
||||
"getBlock",
|
||||
"getBlockCommitment",
|
||||
"getBlockHeight",
|
||||
"getBlockProduction",
|
||||
"getBlocks",
|
||||
"getBlocksWithLimit",
|
||||
"getBlockTime",
|
||||
"getFirstAvailableBlock",
|
||||
"getRecentPerformanceSamples",
|
||||
"minimumLedgerSlot",
|
||||
];
|
||||
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_4
|
||||
&& descriptor.category() == ksp_onchain_transport_lib::HttpRpcCategory::Blocks
|
||||
{
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
actual.push(descriptor.method());
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
let mut expected = expected;
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 10);
|
||||
let get_block = ksp_onchain_transport_lib::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
|
||||
assert!(get_block.request_form_status().has_deprecated_legacy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_4_pre_007_simple_economics_subset_is_exact_and_retry_safe() {
|
||||
let mut expected = std::vec!["getInflationGovernor", "getInflationRate", "getStakeMinimumDelegation", "getSupply"];
|
||||
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_4
|
||||
|| descriptor.category() != ksp_onchain_transport_lib::HttpRpcCategory::Economics
|
||||
{
|
||||
continue;
|
||||
}
|
||||
assert!(matches!(
|
||||
descriptor.method(),
|
||||
"getInflationGovernor" | "getInflationRate" | "getInflationReward" | "getStakeMinimumDelegation" | "getSupply"
|
||||
));
|
||||
if descriptor.method() != "getInflationReward" {
|
||||
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
actual.push(descriptor.method());
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(actual.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_4_pre_008_completes_all_five_economics_wrappers_exactly_and_retry_safe() {
|
||||
let mut expected = std::vec!["getInflationGovernor", "getInflationRate", "getInflationReward", "getStakeMinimumDelegation", "getSupply"];
|
||||
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_4
|
||||
&& descriptor.category() == ksp_onchain_transport_lib::HttpRpcCategory::Economics
|
||||
{
|
||||
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(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_4_pre_009_final_http_inventory_and_coverage_partition_are_exact() {
|
||||
let expected_current = std::vec![
|
||||
"getAccountInfo",
|
||||
"getBalance",
|
||||
"getBlock",
|
||||
"getBlockCommitment",
|
||||
"getBlockHeight",
|
||||
"getBlockProduction",
|
||||
"getBlocks",
|
||||
"getBlocksWithLimit",
|
||||
"getBlockTime",
|
||||
"getClusterNodes",
|
||||
"getEpochInfo",
|
||||
"getEpochSchedule",
|
||||
"getFeeForMessage",
|
||||
"getFirstAvailableBlock",
|
||||
"getGenesisHash",
|
||||
"getHealth",
|
||||
"getHighestSnapshotSlot",
|
||||
"getIdentity",
|
||||
"getInflationGovernor",
|
||||
"getInflationRate",
|
||||
"getInflationReward",
|
||||
"getLargestAccounts",
|
||||
"getLatestBlockhash",
|
||||
"getLeaderSchedule",
|
||||
"getMaxRetransmitSlot",
|
||||
"getMaxShredInsertSlot",
|
||||
"getMinimumBalanceForRentExemption",
|
||||
"getMultipleAccounts",
|
||||
"getProgramAccounts",
|
||||
"getRecentPerformanceSamples",
|
||||
"getRecentPrioritizationFees",
|
||||
"getSignaturesForAddress",
|
||||
"getSignatureStatuses",
|
||||
"getSlot",
|
||||
"getSlotLeader",
|
||||
"getSlotLeaders",
|
||||
"getStakeMinimumDelegation",
|
||||
"getSupply",
|
||||
"getTokenAccountBalance",
|
||||
"getTokenAccountsByDelegate",
|
||||
"getTokenAccountsByOwner",
|
||||
"getTokenLargestAccounts",
|
||||
"getTokenSupply",
|
||||
"getTransaction",
|
||||
"getTransactionCount",
|
||||
"getVersion",
|
||||
"getVoteAccounts",
|
||||
"isBlockhashValid",
|
||||
"minimumLedgerSlot",
|
||||
"requestAirdrop",
|
||||
"sendTransaction",
|
||||
"simulateTransaction",
|
||||
];
|
||||
let expected_historical = std::vec![
|
||||
"confirmTransaction",
|
||||
"getConfirmedBlock",
|
||||
"getConfirmedBlocks",
|
||||
"getConfirmedBlocksWithLimit",
|
||||
"getConfirmedSignaturesForAddress2",
|
||||
"getConfirmedTransaction",
|
||||
"getFeeCalculatorForBlockhash",
|
||||
"getFeeRateGovernor",
|
||||
"getFees",
|
||||
"getRecentBlockhash",
|
||||
"getSignatureConfirmation",
|
||||
"getSignatureStatus",
|
||||
"getSnapshotSlot",
|
||||
"getStakeActivation",
|
||||
];
|
||||
|
||||
let current = ksp_onchain_transport_lib::current_http_rpc_methods();
|
||||
let historical = ksp_onchain_transport_lib::historical_http_rpc_methods();
|
||||
let mut actual_current = std::vec::Vec::with_capacity(current.len());
|
||||
let mut actual_historical = std::vec::Vec::with_capacity(historical.len());
|
||||
let mut v0_2_1 = 0_usize;
|
||||
let mut v0_2_2 = 0_usize;
|
||||
let mut v0_2_3 = 0_usize;
|
||||
let mut v0_2_4 = 0_usize;
|
||||
let mut historical_current = 0_usize;
|
||||
|
||||
for descriptor in current {
|
||||
actual_current.push(descriptor.method());
|
||||
assert_eq!(descriptor.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Supported);
|
||||
assert_ne!(descriptor.documentation_status(), ksp_onchain_transport_lib::RpcDocumentationStatus::Deprecated);
|
||||
match descriptor.coverage_release() {
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_1 => v0_2_1 += 1,
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2 => v0_2_2 += 1,
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3 => v0_2_3 += 1,
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4 => v0_2_4 += 1,
|
||||
ksp_onchain_transport_lib::HttpRpcCoverageRelease::Historical => historical_current += 1,
|
||||
}
|
||||
match descriptor.operation_kind() {
|
||||
ksp_onchain_transport_lib::RpcOperationKind::Read | ksp_onchain_transport_lib::RpcOperationKind::Simulation => {
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||
},
|
||||
ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission => {
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||
},
|
||||
}
|
||||
}
|
||||
for descriptor in historical {
|
||||
actual_historical.push(descriptor.method());
|
||||
assert_eq!(descriptor.documentation_status(), ksp_onchain_transport_lib::RpcDocumentationStatus::Deprecated);
|
||||
assert_eq!(descriptor.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Removed);
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::Historical);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NotApplicable);
|
||||
}
|
||||
|
||||
actual_current.sort_unstable();
|
||||
actual_historical.sort_unstable();
|
||||
let mut expected_current = expected_current;
|
||||
let mut expected_historical = expected_historical;
|
||||
expected_current.sort_unstable();
|
||||
expected_historical.sort_unstable();
|
||||
assert_eq!(actual_current, expected_current);
|
||||
assert_eq!(actual_historical, expected_historical);
|
||||
assert_eq!(actual_current.len(), 52);
|
||||
assert_eq!(actual_historical.len(), 14);
|
||||
assert_eq!((v0_2_1, v0_2_2, v0_2_3, v0_2_4), (4, 22, 11, 15));
|
||||
assert_eq!(historical_current, 0);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
|
||||
// version: 3
|
||||
// version: 5
|
||||
|
||||
//! Opt-in live Devnet smoke for the pure Transport Accounts/Tokens/Cluster path.
|
||||
//! Opt-in live Devnet smoke for representative pure Transport reads across the complete HTTP typed surface.
|
||||
|
||||
fn devnet_pool() -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTransportPool> {
|
||||
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
|
||||
@@ -42,7 +42,7 @@ fn devnet_pool() -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTranspor
|
||||
|
||||
#[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() {
|
||||
async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transactions_blocks_and_economics_reads() {
|
||||
let pool = devnet_pool().expect("programmatic Devnet Transport settings must construct a pool");
|
||||
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
|
||||
|
||||
@@ -94,4 +94,42 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_and_cluster_wrapp
|
||||
|
||||
let vote_accounts = pool.get_vote_accounts(&role, std::option::Option::None).await.expect("Devnet getVoteAccounts smoke must succeed");
|
||||
assert!(!vote_accounts.current().is_empty() || !vote_accounts.delinquent().is_empty());
|
||||
|
||||
let transaction_context = ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let latest_blockhash = pool
|
||||
.get_latest_blockhash(&role, std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet getLatestBlockhash smoke must succeed");
|
||||
assert!(!latest_blockhash.value().blockhash().is_empty());
|
||||
assert!(latest_blockhash.value().last_valid_block_height() > 0);
|
||||
|
||||
let blockhash_valid = pool
|
||||
.is_blockhash_valid(&role, latest_blockhash.value().blockhash(), std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet isBlockhashValid smoke must succeed");
|
||||
assert_eq!(blockhash_valid.value(), &true);
|
||||
|
||||
let transaction_count = pool
|
||||
.get_transaction_count(&role, std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet getTransactionCount smoke must succeed");
|
||||
assert!(transaction_count > 0);
|
||||
|
||||
let block_height = pool.get_block_height(&role, std::option::Option::Some(&transaction_context)).await.expect("Devnet getBlockHeight smoke must succeed");
|
||||
assert!(block_height > 0);
|
||||
|
||||
let inflation_rate = pool.get_inflation_rate(&role).await.expect("Devnet getInflationRate smoke must succeed");
|
||||
assert!(inflation_rate.total().is_finite());
|
||||
assert!(inflation_rate.validator().is_finite());
|
||||
assert!(inflation_rate.foundation().is_finite());
|
||||
|
||||
let stake_minimum_delegation = pool
|
||||
.get_stake_minimum_delegation(&role, std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet getStakeMinimumDelegation smoke must succeed");
|
||||
assert!(stake_minimum_delegation.context().slot() > 0);
|
||||
assert!(*stake_minimum_delegation.value() > 0);
|
||||
}
|
||||
|
||||
793
crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
Normal file
793
crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
Normal file
@@ -0,0 +1,793 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn transaction_details_and_get_block_config_preserve_all_modern_options() {
|
||||
assert_eq!(crate::SolanaTransactionDetails::Full.as_str(), "full");
|
||||
assert_eq!(crate::SolanaTransactionDetails::Signatures.as_str(), "signatures");
|
||||
assert_eq!(crate::SolanaTransactionDetails::None.as_str(), "none");
|
||||
assert_eq!(crate::SolanaTransactionDetails::Accounts.as_str(), "accounts");
|
||||
let config = crate::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed),
|
||||
std::option::Option::Some(crate::SolanaTransactionDetails::Accounts),
|
||||
std::option::Option::Some(1),
|
||||
std::option::Option::Some(false),
|
||||
);
|
||||
assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Confirmed));
|
||||
assert_eq!(config.encoding(), std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed));
|
||||
assert_eq!(config.transaction_details(), std::option::Option::Some(crate::SolanaTransactionDetails::Accounts));
|
||||
assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(1));
|
||||
assert_eq!(config.rewards(), std::option::Option::Some(false));
|
||||
assert_eq!(
|
||||
config.to_json_value(),
|
||||
serde_json::json!({
|
||||
"commitment":"confirmed",
|
||||
"encoding":"jsonParsed",
|
||||
"transactionDetails":"accounts",
|
||||
"maxSupportedTransactionVersion":1,
|
||||
"rewards":false
|
||||
})
|
||||
);
|
||||
assert!(crate::SolanaGetBlockConfig::default().is_empty());
|
||||
assert_eq!(crate::SolanaGetBlockConfig::default().to_json_value(), serde_json::json!({}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_production_config_preserves_identity_range_and_commitment() {
|
||||
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture identity must parse");
|
||||
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::Some(430_000_099));
|
||||
let config = crate::SolanaBlockProductionConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(identity),
|
||||
std::option::Option::Some(range),
|
||||
);
|
||||
assert_eq!(config.identity(), std::option::Option::Some(&identity));
|
||||
assert_eq!(config.range().expect("range must be present").first_slot(), 430_000_000);
|
||||
assert_eq!(config.range().expect("range must be present").last_slot(), std::option::Option::Some(430_000_099));
|
||||
assert_eq!(
|
||||
config.to_json_value(),
|
||||
serde_json::json!({
|
||||
"commitment":"finalized",
|
||||
"identity":"11111111111111111111111111111111",
|
||||
"range":{"firstSlot":430000000,"lastSlot":430000099}
|
||||
})
|
||||
);
|
||||
assert!(crate::SolanaBlockProductionConfig::default().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_commitment_fixture_preserves_nullable_distribution() {
|
||||
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/block_commitment.variants.json"))
|
||||
.expect("block commitment fixtures must decode");
|
||||
let present = crate::SolanaBlockCommitment::decode_wire("getBlockCommitment", values[0].clone()).expect("present commitment must decode");
|
||||
let absent = crate::SolanaBlockCommitment::decode_wire("getBlockCommitment", values[1].clone()).expect("null commitment must decode");
|
||||
assert_eq!(present.commitment(), std::option::Option::Some(&[1, 2, 3, 4][..]));
|
||||
assert_eq!(present.total_stake(), 1_000_000_000);
|
||||
assert_eq!(absent.commitment(), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_production_fixture_types_identity_keys_and_preserves_counts() {
|
||||
let value =
|
||||
serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/block_production.v4_2_1.json")).expect("block production fixture must decode");
|
||||
let result = crate::SolanaBlockProduction::decode_wire("getBlockProduction", value).expect("block production result must decode");
|
||||
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture identity must parse");
|
||||
assert_eq!(result.by_identity().get(&identity), std::option::Option::Some(&(8, 7)));
|
||||
assert_eq!(result.range().first_slot(), 430_000_000);
|
||||
assert_eq!(result.range().last_slot(), 430_000_099);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_block_fixture_preserves_rich_wire_and_simd_extensions() {
|
||||
let value =
|
||||
serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/confirmed_block.v4_2_1.json")).expect("confirmed block fixture must decode");
|
||||
let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value).expect("confirmed block must decode");
|
||||
assert_eq!(block.parent_slot(), 430_000_122);
|
||||
assert_eq!(block.block_time(), std::option::Option::Some(1_787_072_400));
|
||||
assert_eq!(block.block_height(), std::option::Option::Some(410_000_000));
|
||||
assert_eq!(block.num_reward_partitions().value(), std::option::Option::Some(&4));
|
||||
let transactions = block.transactions().value().expect("transactions must be present");
|
||||
assert_eq!(transactions.len(), 2);
|
||||
assert!(matches!(transactions[0].version(), crate::SolanaWireField::Value(crate::SolanaTransactionVersion::Number(1))));
|
||||
assert!(transactions[1].version().is_omitted());
|
||||
assert!(transactions[1].meta().is_null());
|
||||
let rewards = block.rewards().value().expect("rewards must be present");
|
||||
assert_eq!(rewards.len(), 2);
|
||||
assert_eq!(rewards[0].reward_type(), std::option::Option::Some("Fee"));
|
||||
assert_eq!(rewards[0].commission(), std::option::Option::None);
|
||||
assert_eq!(rewards[0].commission_bps().value(), std::option::Option::Some(&1_234));
|
||||
assert_eq!(rewards[1].commission(), std::option::Option::Some(5));
|
||||
assert!(rewards[1].commission_bps().is_omitted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_block_fixture_distinguishes_omitted_and_explicit_null_fields() {
|
||||
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/confirmed_block.omissions.json"))
|
||||
.expect("confirmed block omission fixtures must decode");
|
||||
let omitted = crate::SolanaConfirmedBlock::decode_wire("getBlock", values[0].clone()).expect("omitted-fields block must decode");
|
||||
let nulls = crate::SolanaConfirmedBlock::decode_wire("getBlock", values[1].clone()).expect("null-fields block must decode");
|
||||
assert!(omitted.transactions().is_omitted());
|
||||
assert!(omitted.signatures().is_omitted());
|
||||
assert!(omitted.rewards().is_omitted());
|
||||
assert!(omitted.num_reward_partitions().is_omitted());
|
||||
assert!(nulls.transactions().is_null());
|
||||
assert!(nulls.signatures().is_null());
|
||||
assert!(nulls.rewards().is_null());
|
||||
assert!(nulls.num_reward_partitions().is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn performance_sample_fixture_accepts_current_and_older_shapes() {
|
||||
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/performance_sample.variants.json"))
|
||||
.expect("performance sample fixtures must decode");
|
||||
let current = crate::SolanaPerformanceSample::decode_wire("getRecentPerformanceSamples", values[0].clone()).expect("current sample must decode");
|
||||
let older = crate::SolanaPerformanceSample::decode_wire("getRecentPerformanceSamples", values[1].clone()).expect("older sample must decode");
|
||||
assert_eq!(current.slot(), 430_000_123);
|
||||
assert_eq!(current.num_transactions(), 250_000);
|
||||
assert_eq!(current.num_non_vote_transactions(), std::option::Option::Some(175_000));
|
||||
assert_eq!(current.num_slots(), 120);
|
||||
assert_eq!(current.sample_period_secs(), 60);
|
||||
assert_eq!(older.num_non_vote_transactions(), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_reward_rejects_invalid_pubkey_without_echoing_value() {
|
||||
let result = crate::SolanaConfirmedBlock::decode_wire(
|
||||
"getBlock",
|
||||
serde_json::json!({
|
||||
"previousBlockhash":"previous",
|
||||
"blockhash":"block",
|
||||
"parentSlot":1,
|
||||
"rewards":[{"pubkey":"not-a-pubkey","lamports":1,"postBalance":2,"rewardType":"Fee","commission":null}],
|
||||
"blockTime":null,
|
||||
"blockHeight":null
|
||||
}),
|
||||
);
|
||||
let error = result.expect_err("invalid reward pubkey must fail closed");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert!(!error.to_string().contains("not-a-pubkey"));
|
||||
}
|
||||
|
||||
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_block_commitment_serializes_slot_and_preserves_distribution() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_commitment.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_block_commitment(&crate::HttpRoleName::new("default"), 430_000_123).await.expect("block commitment fixture must succeed");
|
||||
assert_eq!(result.commitment(), std::option::Option::Some(&[1, 2, 3, 4][..]));
|
||||
assert_eq!(result.total_stake(), 1_000_000_000);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getBlockCommitment"));
|
||||
assert_eq!(body["params"], serde_json::json!([430000123]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_height_serializes_context_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_height.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 height = pool
|
||||
.get_block_height(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("block height fixture must succeed");
|
||||
assert_eq!(height, 410_000_000);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"finalized","minContextSlot":429000000}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_height_omits_explicitly_empty_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_height.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaContextConfig::default();
|
||||
let height = pool
|
||||
.get_block_height(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("block height fixture must succeed");
|
||||
assert_eq!(height, 410_000_000);
|
||||
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_block_time_preserves_timestamp_and_null() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_time.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let timestamp = pool.get_block_time(&crate::HttpRoleName::new("default"), 430_000_123).await.expect("block time fixture must succeed");
|
||||
assert_eq!(timestamp, std::option::Option::Some(1_787_072_400));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000123]));
|
||||
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_time.null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let timestamp = pool.get_block_time(&crate::HttpRoleName::new("default"), 430_000_124).await.expect("null block time fixture must succeed");
|
||||
assert_eq!(timestamp, std::option::Option::None);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_first_available_block_has_no_params() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_first_available_block.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let slot = pool.get_first_available_block(&crate::HttpRoleName::new("default")).await.expect("first available block fixture must succeed");
|
||||
assert_eq!(slot, 250_000);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getFirstAvailableBlock"));
|
||||
assert_eq!(body["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_minimum_ledger_slot_has_no_params() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/minimum_ledger_slot.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let slot = pool.minimum_ledger_slot(&crate::HttpRoleName::new("default")).await.expect("minimum ledger slot fixture must succeed");
|
||||
assert_eq!(slot, 123_456);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("minimumLedgerSlot"));
|
||||
assert_eq!(body["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_blocks_covers_all_four_overloads_and_preserves_server_order() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(429_999_999));
|
||||
let empty_config = crate::SolanaContextConfig::default();
|
||||
let cases = [
|
||||
(std::option::Option::None, std::option::Option::None, serde_json::json!([430000100])),
|
||||
(std::option::Option::Some(430_000_109), std::option::Option::None, serde_json::json!([430000100, 430000109])),
|
||||
(
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(&config),
|
||||
serde_json::json!([430000100,{"commitment":"confirmed","minContextSlot":429999999}]),
|
||||
),
|
||||
(
|
||||
std::option::Option::Some(430_000_109),
|
||||
std::option::Option::Some(&config),
|
||||
serde_json::json!([430000100,430000109,{"commitment":"confirmed","minContextSlot":429999999}]),
|
||||
),
|
||||
(std::option::Option::None, std::option::Option::Some(&empty_config), serde_json::json!([430000100, {}])),
|
||||
];
|
||||
for (end_slot, config, expected_params) in cases {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let blocks = pool.get_blocks(&role, 430_000_100, end_slot, config).await.expect("getBlocks overload fixture must succeed");
|
||||
assert_eq!(blocks, std::vec![430_000_100, 430_000_103, 430_000_109]);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getBlocks"));
|
||||
assert_eq!(body["params"], expected_params);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_blocks_allows_reversed_and_boundary_ranges_but_rejects_oversized_before_io() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.empty.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let blocks = pool
|
||||
.get_blocks(&role, 430_000_100, std::option::Option::Some(430_000_099), std::option::Option::None)
|
||||
.await
|
||||
.expect("reversed getBlocks range must remain a valid RPC request");
|
||||
assert!(blocks.is_empty());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000100, 430000099]));
|
||||
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let blocks = pool
|
||||
.get_blocks(&role, 10, std::option::Option::Some(500_010), std::option::Option::None)
|
||||
.await
|
||||
.expect("500000-slot getBlocks range must remain valid");
|
||||
assert_eq!(blocks, std::vec![430_000_100, 430_000_103, 430_000_109]);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([10, 500010]));
|
||||
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let result = pool.get_blocks(&role, 10, std::option::Option::Some(500_011), std::option::Option::None).await;
|
||||
let error = result.expect_err("range above 500000 must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "start_slot");
|
||||
assert_eq!(error.context()[1].value(), "10");
|
||||
assert_eq!(error.context()[2].key(), "end_slot");
|
||||
assert_eq!(error.context()[2].value(), "500011");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_block_range_wrappers_reject_processed_commitment_before_io() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Processed), std::option::Option::None);
|
||||
let get_blocks = pool.get_blocks(&role, 1, std::option::Option::None, std::option::Option::Some(&config)).await;
|
||||
let error = get_blocks.expect_err("getBlocks processed commitment must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "commitment");
|
||||
assert_eq!(error.context()[1].value(), "processed");
|
||||
|
||||
let get_blocks_with_limit = pool.get_blocks_with_limit(&role, 1, 1, std::option::Option::Some(&config)).await;
|
||||
let error = get_blocks_with_limit.expect_err("getBlocksWithLimit processed commitment must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "commitment");
|
||||
assert_eq!(error.context()[1].value(), "processed");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_blocks_with_limit_accepts_zero_and_maximum_and_rejects_above_maximum() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks_with_limit.empty.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let blocks = pool.get_blocks_with_limit(&role, 430_000_200, 0, std::option::Option::None).await.expect("zero getBlocksWithLimit limit must remain valid");
|
||||
assert!(blocks.is_empty());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200, 0]));
|
||||
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(430_000_000));
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks_with_limit.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let blocks = pool
|
||||
.get_blocks_with_limit(&role, 430_000_200, 500_000, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("maximum getBlocksWithLimit limit must remain valid");
|
||||
assert_eq!(blocks, std::vec![430_000_200, 430_000_201, 430_000_205]);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200,500000,{"commitment":"finalized","minContextSlot":430000000}]));
|
||||
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let result = pool.get_blocks_with_limit(&role, 1, 500_001, std::option::Option::None).await;
|
||||
let error = result.expect_err("getBlocksWithLimit above 500000 must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "limit");
|
||||
assert_eq!(error.context()[1].value(), "500001");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_recent_performance_samples_preserves_default_explicit_limit_order_and_older_shape() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
for (limit, expected_params) in [(std::option::Option::None, serde_json::json!([])), (std::option::Option::Some(720), serde_json::json!([720]))] {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_recent_performance_samples.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let samples = pool.get_recent_performance_samples(&role, limit).await.expect("performance sample fixture must succeed");
|
||||
assert_eq!(samples.len(), 2);
|
||||
assert_eq!(samples[0].slot(), 430_000_123);
|
||||
assert_eq!(samples[0].num_transactions(), 250_000);
|
||||
assert_eq!(samples[0].num_non_vote_transactions(), std::option::Option::Some(175_000));
|
||||
assert_eq!(samples[1].slot(), 430_000_000);
|
||||
assert_eq!(samples[1].num_non_vote_transactions(), 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!("getRecentPerformanceSamples"));
|
||||
assert_eq!(body["params"], expected_params);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_recent_performance_samples_rejects_above_720_before_io() {
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let result = pool.get_recent_performance_samples(&crate::HttpRoleName::new("default"), std::option::Option::Some(721)).await;
|
||||
let error = result.expect_err("performance sample limit above 720 must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "limit");
|
||||
assert_eq!(error.context()[1].value(), "721");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_production_serializes_full_config_and_decodes_contextual_result() {
|
||||
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture identity must parse");
|
||||
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::Some(430_000_099));
|
||||
let config = crate::SolanaBlockProductionConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(identity),
|
||||
std::option::Option::Some(range),
|
||||
);
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let response = pool
|
||||
.get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("block production fixture must succeed");
|
||||
assert_eq!(response.context().slot(), 430_000_123);
|
||||
assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1"));
|
||||
assert_eq!(response.value().by_identity().get(&identity), std::option::Option::Some(&(8, 7)));
|
||||
assert_eq!(response.value().range().first_slot(), 430_000_000);
|
||||
assert_eq!(response.value().range().last_slot(), 430_000_099);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getBlockProduction"));
|
||||
assert_eq!(
|
||||
body["params"],
|
||||
serde_json::json!([{
|
||||
"commitment":"finalized",
|
||||
"identity":"11111111111111111111111111111111",
|
||||
"range":{"firstSlot":430000000,"lastSlot":430000099}
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_production_omits_empty_config_and_supports_open_range_with_processed_commitment() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let empty = crate::SolanaBlockProductionConfig::default();
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let response = pool.get_block_production(&role, std::option::Option::Some(&empty)).await.expect("empty block production config must succeed");
|
||||
assert_eq!(response.value().range().last_slot(), 430_000_099);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||
|
||||
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::None);
|
||||
let config = crate::SolanaBlockProductionConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Processed),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(range),
|
||||
);
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
pool.get_block_production(&role, std::option::Option::Some(&config)).await.expect("open block production range must succeed");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"processed","range":{"firstSlot":430000000}}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_production_rejects_reversed_range_before_io() {
|
||||
let range = crate::SolanaBlockProductionRange::new(430_000_100, std::option::Option::Some(430_000_099));
|
||||
let config = crate::SolanaBlockProductionConfig::new(std::option::Option::None, std::option::Option::None, std::option::Option::Some(range));
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let result = pool.get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)).await;
|
||||
let error = result.expect_err("reversed block production range must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "first_slot");
|
||||
assert_eq!(error.context()[1].value(), "430000100");
|
||||
assert_eq!(error.context()[2].key(), "last_slot");
|
||||
assert_eq!(error.context()[2].value(), "430000099");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_production_rejects_invalid_wire_identity_without_echoing_value() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_production.invalid_identity.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_block_production(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid block production identity must fail closed");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert!(!error.to_string().contains("not-a-pubkey"));
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_modern_full_config_preserves_rich_wire_and_simd_fields() {
|
||||
let config = crate::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed),
|
||||
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(1),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.full.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let block = pool
|
||||
.get_block(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("modern getBlock fixture must succeed")
|
||||
.expect("modern getBlock fixture must contain a block");
|
||||
assert_eq!(block.parent_slot(), 430_000_122);
|
||||
assert_eq!(block.num_reward_partitions().value(), std::option::Option::Some(&4));
|
||||
let transactions = block.transactions().value().expect("full block transactions must be present");
|
||||
assert_eq!(transactions.len(), 2);
|
||||
assert!(matches!(transactions[0].version(), crate::SolanaWireField::Value(crate::SolanaTransactionVersion::Number(1))));
|
||||
let first_meta = transactions[0].meta().value().expect("full transaction metadata must be present");
|
||||
assert_eq!(first_meta["rewards"][0]["commissionBps"], serde_json::json!(1234));
|
||||
let rewards = block.rewards().value().expect("block rewards must be present");
|
||||
assert_eq!(rewards[0].commission(), std::option::Option::None);
|
||||
assert_eq!(rewards[0].commission_bps().value(), std::option::Option::Some(&1_234));
|
||||
assert!(rewards[1].commission_bps().is_omitted());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getBlock"));
|
||||
assert_eq!(
|
||||
body["params"],
|
||||
serde_json::json!([430000123,{"commitment":"finalized","encoding":"jsonParsed","transactionDetails":"full","maxSupportedTransactionVersion":1,"rewards":true}])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_preserves_absent_and_explicit_empty_modern_config() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
for (config, expected_params) in [
|
||||
(std::option::Option::None, serde_json::json!([430000123])),
|
||||
(std::option::Option::Some(crate::SolanaGetBlockConfig::default()), serde_json::json!([430000123, {}])),
|
||||
] {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let block = pool.get_block(&role, 430_000_123, config.as_ref()).await.expect("nullable getBlock request must succeed");
|
||||
assert!(block.is_none());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], expected_params);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_legacy_covers_all_bare_encoding_labels_and_response_shapes() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let cases = [
|
||||
(crate::SolanaTransactionEncoding::Binary, "binary", include_str!("../fixtures/http/get_block.binary.success.json")),
|
||||
(crate::SolanaTransactionEncoding::Base58, "base58", include_str!("../fixtures/http/get_block.base58.success.json")),
|
||||
(crate::SolanaTransactionEncoding::Base64, "base64", include_str!("../fixtures/http/get_block.base64.success.json")),
|
||||
(crate::SolanaTransactionEncoding::Json, "json", include_str!("../fixtures/http/get_block.json.success.json")),
|
||||
(crate::SolanaTransactionEncoding::JsonParsed, "jsonParsed", include_str!("../fixtures/http/get_block.json_parsed.success.json")),
|
||||
];
|
||||
for (encoding, wire_label, response) in cases {
|
||||
let (url, handle) = serve_once(response);
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let block = pool
|
||||
.get_block_legacy(&role, 430_000_123, encoding)
|
||||
.await
|
||||
.expect("legacy getBlock encoding must succeed")
|
||||
.expect("legacy getBlock encoding must return a block");
|
||||
let transactions = block.transactions().value().expect("legacy full response must contain transactions");
|
||||
assert_eq!(transactions.len(), 1);
|
||||
match encoding {
|
||||
crate::SolanaTransactionEncoding::Binary => assert!(matches!(transactions[0].transaction(), crate::SolanaEncodedTransaction::LegacyBinary(_))),
|
||||
crate::SolanaTransactionEncoding::Base58 => assert!(matches!(
|
||||
transactions[0].transaction(),
|
||||
crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base58, .. }
|
||||
)),
|
||||
crate::SolanaTransactionEncoding::Base64 => assert!(matches!(
|
||||
transactions[0].transaction(),
|
||||
crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. }
|
||||
)),
|
||||
crate::SolanaTransactionEncoding::Json | crate::SolanaTransactionEncoding::JsonParsed => {
|
||||
assert!(matches!(transactions[0].transaction(), crate::SolanaEncodedTransaction::Json(_)));
|
||||
},
|
||||
}
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000123, wire_label]));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_modern_covers_all_agave_v4_2_1_encoding_labels() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let cases = [
|
||||
(crate::SolanaTransactionEncoding::Binary, "binary", include_str!("../fixtures/http/get_block.binary.success.json")),
|
||||
(crate::SolanaTransactionEncoding::Base58, "base58", include_str!("../fixtures/http/get_block.base58.success.json")),
|
||||
(crate::SolanaTransactionEncoding::Base64, "base64", include_str!("../fixtures/http/get_block.base64.success.json")),
|
||||
(crate::SolanaTransactionEncoding::Json, "json", include_str!("../fixtures/http/get_block.json.success.json")),
|
||||
(crate::SolanaTransactionEncoding::JsonParsed, "jsonParsed", include_str!("../fixtures/http/get_block.json_parsed.success.json")),
|
||||
];
|
||||
for (encoding, wire_label, response) in cases {
|
||||
let config = crate::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(encoding),
|
||||
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(1),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
let (url, handle) = serve_once(response);
|
||||
let pool = pool_for_url(url.as_str());
|
||||
pool.get_block(&role, 430_000_123, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("modern getBlock encoding must succeed")
|
||||
.expect("modern getBlock encoding must return a block");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(
|
||||
request_body(request.as_str())["params"],
|
||||
serde_json::json!([430000123,{"commitment":"confirmed","encoding":wire_label,"transactionDetails":"full","maxSupportedTransactionVersion":1,"rewards":true}])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_covers_full_signatures_none_and_accounts_transaction_details() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let cases = [
|
||||
(crate::SolanaTransactionDetails::Full, "full", include_str!("../fixtures/http/get_block.full_no_rewards.success.json")),
|
||||
(crate::SolanaTransactionDetails::Signatures, "signatures", include_str!("../fixtures/http/get_block.signatures.success.json")),
|
||||
(crate::SolanaTransactionDetails::None, "none", include_str!("../fixtures/http/get_block.none.success.json")),
|
||||
(crate::SolanaTransactionDetails::Accounts, "accounts", include_str!("../fixtures/http/get_block.accounts.success.json")),
|
||||
];
|
||||
for (details, wire_label, response) in cases {
|
||||
let config = crate::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed),
|
||||
std::option::Option::Some(details),
|
||||
std::option::Option::Some(1),
|
||||
std::option::Option::Some(false),
|
||||
);
|
||||
let (url, handle) = serve_once(response);
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let block = pool
|
||||
.get_block(&role, 430_000_123, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("transactionDetails getBlock fixture must succeed")
|
||||
.expect("transactionDetails getBlock fixture must return a block");
|
||||
match details {
|
||||
crate::SolanaTransactionDetails::Full => {
|
||||
let transactions = block.transactions().value().expect("full transaction list must be present");
|
||||
assert_eq!(transactions.len(), 1);
|
||||
assert!(block.signatures().is_omitted());
|
||||
assert_eq!(transactions[0].meta().value().expect("full transaction metadata must be present")["rewards"], serde_json::Value::Null);
|
||||
},
|
||||
crate::SolanaTransactionDetails::Signatures => {
|
||||
assert!(block.transactions().is_omitted());
|
||||
assert_eq!(block.signatures().value().expect("signatures must be present"), &std::vec!["sig-a".to_owned(), "sig-b".to_owned()]);
|
||||
},
|
||||
crate::SolanaTransactionDetails::None => {
|
||||
assert!(block.transactions().is_omitted());
|
||||
assert!(block.signatures().is_omitted());
|
||||
},
|
||||
crate::SolanaTransactionDetails::Accounts => {
|
||||
let transactions = block.transactions().value().expect("accounts transaction list must be present");
|
||||
assert_eq!(transactions.len(), 1);
|
||||
assert!(matches!(transactions[0].transaction(), crate::SolanaEncodedTransaction::Json(_)));
|
||||
if let crate::SolanaEncodedTransaction::Json(transaction) = transactions[0].transaction() {
|
||||
assert_eq!(transaction["accountKeys"][0]["pubkey"], serde_json::json!("11111111111111111111111111111111"));
|
||||
}
|
||||
assert!(matches!(transactions[0].version(), crate::SolanaWireField::Value(crate::SolanaTransactionVersion::Number(1))));
|
||||
},
|
||||
}
|
||||
assert!(block.rewards().is_omitted());
|
||||
assert_eq!(block.num_reward_partitions().value(), std::option::Option::Some(&4));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(
|
||||
request_body(request.as_str())["params"],
|
||||
serde_json::json!([430000123,{"commitment":"confirmed","encoding":"jsonParsed","transactionDetails":wire_label,"maxSupportedTransactionVersion":1,"rewards":false}])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_preserves_null_and_omitted_top_level_wire_states() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
for (response, expect_null) in [
|
||||
(include_str!("../fixtures/http/get_block.omitted_fields.success.json"), false),
|
||||
(include_str!("../fixtures/http/get_block.null_fields.success.json"), true),
|
||||
] {
|
||||
let (url, handle) = serve_once(response);
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let block = pool
|
||||
.get_block(&role, 430_000_123, std::option::Option::None)
|
||||
.await
|
||||
.expect("wire-state getBlock fixture must succeed")
|
||||
.expect("wire-state getBlock fixture must return a block");
|
||||
if expect_null {
|
||||
assert!(block.transactions().is_null());
|
||||
assert!(block.signatures().is_null());
|
||||
assert!(block.rewards().is_null());
|
||||
assert!(block.num_reward_partitions().is_null());
|
||||
} else {
|
||||
assert!(block.transactions().is_omitted());
|
||||
assert!(block.signatures().is_omitted());
|
||||
assert!(block.rewards().is_omitted());
|
||||
assert!(block.num_reward_partitions().is_omitted());
|
||||
}
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_block_preserves_unsupported_version_rpc_error() {
|
||||
let config = crate::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||
std::option::Option::Some(0),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.error_unsupported_version.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_block(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config)).await;
|
||||
let error = result.expect_err("unsupported transaction version 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_block_preserves_block_not_available_rpc_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.error_block_not_available.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_block(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::None).await;
|
||||
let error = result.expect_err("unavailable block 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_block_rejects_processed_commitment_before_io() {
|
||||
let config = crate::SolanaGetBlockConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(crate::SolanaTransactionEncoding::Json),
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let result = pool.get_block(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config)).await;
|
||||
let error = result.expect_err("processed getBlock commitment must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "commitment");
|
||||
assert_eq!(error.context()[1].value(), "processed");
|
||||
}
|
||||
492
crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs
Normal file
492
crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs
Normal file
@@ -0,0 +1,492 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn inflation_reward_config_preserves_epoch_commitment_and_min_context_slot() {
|
||||
let config = crate::SolanaInflationRewardConfig::new(
|
||||
std::option::Option::Some(811),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(429_000_000),
|
||||
);
|
||||
assert_eq!(config.epoch(), std::option::Option::Some(811));
|
||||
assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||
assert_eq!(config.min_context_slot(), std::option::Option::Some(429_000_000));
|
||||
assert_eq!(config.to_json_value(), serde_json::json!({"epoch":811,"commitment":"finalized","minContextSlot":429000000}));
|
||||
assert!(crate::SolanaInflationRewardConfig::default().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supply_config_distinguishes_omitted_from_explicit_false() {
|
||||
let omitted = crate::SolanaSupplyConfig::default();
|
||||
let explicit_false = crate::SolanaSupplyConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(false));
|
||||
assert!(omitted.is_empty());
|
||||
assert_eq!(omitted.to_json_value(), serde_json::json!({}));
|
||||
assert_eq!(explicit_false.exclude_non_circulating_accounts_list(), std::option::Option::Some(false));
|
||||
assert_eq!(explicit_false.to_json_value(), serde_json::json!({"commitment":"confirmed","excludeNonCirculatingAccountsList":false}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inflation_governor_and_rate_fixtures_preserve_runtime_values() {
|
||||
let governor_value = serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/inflation_governor.v4_2_1.json"))
|
||||
.expect("inflation governor fixture must decode");
|
||||
let rate_value =
|
||||
serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/inflation_rate.v4_2_1.json")).expect("inflation rate fixture must decode");
|
||||
let governor = crate::SolanaInflationGovernor::decode_wire("getInflationGovernor", governor_value).expect("inflation governor must decode");
|
||||
let rate = crate::SolanaInflationRate::decode_wire("getInflationRate", rate_value).expect("inflation rate must decode");
|
||||
assert_eq!(governor.initial(), 0.08);
|
||||
assert_eq!(governor.terminal(), 0.015);
|
||||
assert_eq!(governor.taper(), 0.15);
|
||||
assert_eq!(governor.foundation(), 0.05);
|
||||
assert_eq!(governor.foundation_term(), 7.0);
|
||||
assert_eq!(rate.total(), 0.042);
|
||||
assert_eq!(rate.validator(), 0.041);
|
||||
assert_eq!(rate.foundation(), 0.001);
|
||||
assert_eq!(rate.epoch(), 812);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inflation_reward_fixture_preserves_position_null_and_commission_bps_states() {
|
||||
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/inflation_reward.variants.json"))
|
||||
.expect("inflation reward fixtures must decode");
|
||||
let first = crate::SolanaInflationReward::decode_wire("getInflationReward", values[0].clone()).expect("first reward must decode");
|
||||
let third = crate::SolanaInflationReward::decode_wire("getInflationReward", values[2].clone()).expect("third reward must decode");
|
||||
assert_eq!(values.len(), 3);
|
||||
assert!(values[1].is_null());
|
||||
assert_eq!(first.epoch(), 811);
|
||||
assert_eq!(first.effective_slot(), 429_900_000);
|
||||
assert_eq!(first.amount(), 2_500_000);
|
||||
assert_eq!(first.post_balance(), 1_002_500_000);
|
||||
assert_eq!(first.commission(), std::option::Option::Some(7));
|
||||
assert_eq!(first.commission_bps().value(), std::option::Option::Some(&725));
|
||||
assert_eq!(third.commission(), std::option::Option::None);
|
||||
assert!(third.commission_bps().is_omitted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supply_fixture_types_non_circulating_accounts_and_preserves_order() {
|
||||
let value = serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/supply.v4_2_1.json")).expect("supply fixture must decode");
|
||||
let supply = crate::SolanaSupply::decode_wire("getSupply", value).expect("supply must decode");
|
||||
assert_eq!(supply.total(), 600_000_000_000_000_000);
|
||||
assert_eq!(supply.circulating(), 500_000_000_000_000_000);
|
||||
assert_eq!(supply.non_circulating(), 100_000_000_000_000_000);
|
||||
assert_eq!(supply.non_circulating_accounts().len(), 2);
|
||||
assert_eq!(supply.non_circulating_accounts()[0].to_string(), "11111111111111111111111111111111");
|
||||
assert_eq!(supply.non_circulating_accounts()[1].to_string(), "Vote111111111111111111111111111111111111111");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supply_rejects_invalid_non_circulating_pubkey_without_echoing_value() {
|
||||
let result = crate::SolanaSupply::decode_wire(
|
||||
"getSupply",
|
||||
serde_json::json!({"total":3,"circulating":2,"nonCirculating":1,"nonCirculatingAccounts":["invalid-pubkey"]}),
|
||||
);
|
||||
let error = result.expect_err("invalid supply pubkey must fail closed");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert!(!error.to_string().contains("invalid-pubkey"));
|
||||
}
|
||||
|
||||
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_inflation_governor_serializes_processed_commitment_and_preserves_runtime_values() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_governor.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Processed));
|
||||
let governor = pool
|
||||
.get_inflation_governor(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("inflation governor fixture must succeed");
|
||||
assert_eq!(governor.initial(), 0.07123456789);
|
||||
assert_eq!(governor.terminal(), 0.0123456789);
|
||||
assert_eq!(governor.taper(), 0.2718281828);
|
||||
assert_eq!(governor.foundation(), 0.0042);
|
||||
assert_eq!(governor.foundation_term(), 6.25);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getInflationGovernor"));
|
||||
assert_eq!(body["params"], serde_json::json!([{"commitment":"processed"}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_inflation_governor_omits_absent_and_empty_config() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let empty = crate::SolanaCommitmentConfig::default();
|
||||
for config in [std::option::Option::None, std::option::Option::Some(&empty)] {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_governor.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
pool.get_inflation_governor(&role, config).await.expect("inflation governor fixture must succeed");
|
||||
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_inflation_rate_has_no_params_and_preserves_runtime_values() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_rate.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let rate = pool.get_inflation_rate(&crate::HttpRoleName::new("default")).await.expect("inflation rate fixture must succeed");
|
||||
assert_eq!(rate.total(), 0.0387654321);
|
||||
assert_eq!(rate.validator(), 0.037654321);
|
||||
assert_eq!(rate.foundation(), 0.0011111111);
|
||||
assert_eq!(rate.epoch(), 913);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getInflationRate"));
|
||||
assert_eq!(body["params"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_stake_minimum_delegation_preserves_context_and_runtime_value_without_local_minimum() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_stake_minimum_delegation.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Processed), std::option::Option::Some(431_000_000));
|
||||
let result = pool
|
||||
.get_stake_minimum_delegation(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("stake minimum delegation fixture must succeed");
|
||||
assert_eq!(*result.value(), 42_424_242);
|
||||
assert_eq!(result.context().slot(), 431_000_123);
|
||||
assert_eq!(result.context().api_version(), std::option::Option::Some("4.2.1"));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"processed","minContextSlot":431000000}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_stake_minimum_delegation_omits_absent_and_empty_config() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let empty = crate::SolanaContextConfig::default();
|
||||
for config in [std::option::Option::None, std::option::Option::Some(&empty)] {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_stake_minimum_delegation.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
pool.get_stake_minimum_delegation(&role, config).await.expect("stake minimum delegation fixture must succeed");
|
||||
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_supply_preserves_explicit_false_full_account_list_and_context() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_supply.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaSupplyConfig::new(std::option::Option::Some(crate::SolanaCommitment::Processed), std::option::Option::Some(false));
|
||||
let result = pool.get_supply(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)).await.expect("supply fixture must succeed");
|
||||
assert_eq!(result.context().slot(), 431_000_124);
|
||||
assert_eq!(result.value().total(), 610_000_000_000_000_000);
|
||||
assert_eq!(result.value().circulating(), 510_000_000_000_000_000);
|
||||
assert_eq!(result.value().non_circulating(), 100_000_000_000_000_000);
|
||||
assert_eq!(result.value().non_circulating_accounts().len(), 2);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"processed","excludeNonCirculatingAccountsList":false}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_supply_preserves_explicit_true_as_empty_runtime_account_list() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_supply.excluded.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaSupplyConfig::new(std::option::Option::None, std::option::Option::Some(true));
|
||||
let result = pool
|
||||
.get_supply(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("excluded supply fixture must succeed");
|
||||
assert_eq!(result.value().non_circulating_accounts(), &[]);
|
||||
assert_eq!(result.value().non_circulating(), 100_000_000_000_000_000);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"excludeNonCirculatingAccountsList":true}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_supply_omits_absent_and_empty_config() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let empty = crate::SolanaSupplyConfig::default();
|
||||
for config in [std::option::Option::None, std::option::Option::Some(&empty)] {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_supply.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
pool.get_supply(&role, config).await.expect("supply fixture must succeed");
|
||||
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_stake_minimum_delegation_preserves_min_context_slot_rpc_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_stake_minimum_delegation.error_min_context.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::None, std::option::Option::Some(431_000_000));
|
||||
let result = pool.get_stake_minimum_delegation(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)).await;
|
||||
let error = result.expect_err("min-context-slot RPC error must propagate");
|
||||
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_supply_preserves_scan_rpc_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_supply.error_scan.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_supply(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
|
||||
let error = result.expect_err("supply scan RPC error must propagate");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
fn inflation_reward_fixture_pubkey(value: &str) -> ksp_core_lib::Pubkey {
|
||||
return value.parse::<ksp_core_lib::Pubkey>().expect("fixture inflation-reward pubkey must parse");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_inflation_reward_serializes_full_config_and_preserves_position_and_commission_states() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_reward.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let addresses = std::vec![
|
||||
inflation_reward_fixture_pubkey("11111111111111111111111111111111"),
|
||||
inflation_reward_fixture_pubkey("Vote111111111111111111111111111111111111111"),
|
||||
inflation_reward_fixture_pubkey("SysvarRent111111111111111111111111111111111"),
|
||||
inflation_reward_fixture_pubkey("Stake11111111111111111111111111111111111111"),
|
||||
];
|
||||
let config = crate::SolanaInflationRewardConfig::new(
|
||||
std::option::Option::Some(912),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(431_000_000),
|
||||
);
|
||||
let rewards = pool
|
||||
.get_inflation_reward(&crate::HttpRoleName::new("default"), addresses.as_slice(), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("inflation reward fixture must succeed");
|
||||
assert_eq!(rewards.len(), addresses.len());
|
||||
let first = rewards[0].as_ref().expect("first reward must be present");
|
||||
assert_eq!(first.epoch(), 912);
|
||||
assert_eq!(first.effective_slot(), 431_500_001);
|
||||
assert_eq!(first.amount(), 2_500_000);
|
||||
assert_eq!(first.post_balance(), 1_002_500_000);
|
||||
assert_eq!(first.commission(), std::option::Option::Some(7));
|
||||
assert_eq!(first.commission_bps().value(), std::option::Option::Some(&725));
|
||||
assert!(rewards[1].is_none());
|
||||
let third = rewards[2].as_ref().expect("third reward must be present");
|
||||
assert_eq!(third.commission(), std::option::Option::None);
|
||||
assert!(third.commission_bps().is_omitted());
|
||||
let fourth = rewards[3].as_ref().expect("fourth reward must be present");
|
||||
assert_eq!(fourth.commission(), std::option::Option::Some(3));
|
||||
assert!(fourth.commission_bps().is_null());
|
||||
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getInflationReward"));
|
||||
assert_eq!(
|
||||
body["params"],
|
||||
serde_json::json!([
|
||||
[
|
||||
"11111111111111111111111111111111",
|
||||
"Vote111111111111111111111111111111111111111",
|
||||
"SysvarRent111111111111111111111111111111111",
|
||||
"Stake11111111111111111111111111111111111111"
|
||||
],
|
||||
{"epoch":912,"commitment":"finalized","minContextSlot":431000000}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_inflation_reward_preserves_duplicate_input_order_and_positional_nulls() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_reward.duplicates.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let first = inflation_reward_fixture_pubkey("11111111111111111111111111111111");
|
||||
let second = inflation_reward_fixture_pubkey("Vote111111111111111111111111111111111111111");
|
||||
let addresses = std::vec![first, second, first];
|
||||
let rewards = pool
|
||||
.get_inflation_reward(&crate::HttpRoleName::new("default"), addresses.as_slice(), std::option::Option::None)
|
||||
.await
|
||||
.expect("duplicate inflation reward fixture must succeed");
|
||||
assert_eq!(rewards.len(), 3);
|
||||
assert_eq!(rewards[0], rewards[2]);
|
||||
assert!(rewards[1].is_none());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(
|
||||
request_body(request.as_str())["params"],
|
||||
serde_json::json!([["11111111111111111111111111111111", "Vote111111111111111111111111111111111111111", "11111111111111111111111111111111"]])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_inflation_reward_omits_absent_and_empty_config_but_keeps_address_parameter() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let address = inflation_reward_fixture_pubkey("11111111111111111111111111111111");
|
||||
let addresses = std::vec![address];
|
||||
let empty = crate::SolanaInflationRewardConfig::default();
|
||||
for config in [std::option::Option::None, std::option::Option::Some(&empty)] {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_reward.single_null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let rewards = pool.get_inflation_reward(&role, addresses.as_slice(), config).await.expect("single null inflation reward fixture must succeed");
|
||||
assert_eq!(rewards, std::vec![std::option::Option::None]);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([["11111111111111111111111111111111"]]));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_inflation_reward_accepts_empty_address_list_without_inventing_a_minimum() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_reward.empty.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let rewards = pool
|
||||
.get_inflation_reward(&crate::HttpRoleName::new("default"), &[], std::option::Option::None)
|
||||
.await
|
||||
.expect("empty inflation reward fixture must succeed");
|
||||
assert!(rewards.is_empty());
|
||||
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_inflation_reward_does_not_invent_a_256_address_limit() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_reward.many_nulls.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let address = inflation_reward_fixture_pubkey("11111111111111111111111111111111");
|
||||
let addresses = std::vec![address; 300];
|
||||
let rewards = pool
|
||||
.get_inflation_reward(&crate::HttpRoleName::new("default"), addresses.as_slice(), std::option::Option::None)
|
||||
.await
|
||||
.expect("300-address inflation reward fixture must succeed without local cardinality cap");
|
||||
assert_eq!(rewards.len(), 300);
|
||||
assert!(rewards.iter().all(std::option::Option::is_none));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["params"][0].as_array().expect("address parameter must be an array").len(), 300);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_inflation_reward_rejects_processed_commitment_before_io() {
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let pool = pool_for_url("http://127.0.0.1:1");
|
||||
let address = inflation_reward_fixture_pubkey("11111111111111111111111111111111");
|
||||
let config = crate::SolanaInflationRewardConfig::new(
|
||||
std::option::Option::Some(912),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Processed),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = pool.get_inflation_reward(&role, &[address], std::option::Option::Some(&config)).await;
|
||||
let error = result.expect_err("processed inflation-reward commitment must fail before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "commitment");
|
||||
assert_eq!(error.context()[1].value(), "processed");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_inflation_reward_rejects_response_cardinality_mismatch() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_reward.single_null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let first = inflation_reward_fixture_pubkey("11111111111111111111111111111111");
|
||||
let second = inflation_reward_fixture_pubkey("Vote111111111111111111111111111111111111111");
|
||||
let result = pool.get_inflation_reward(&crate::HttpRoleName::new("default"), &[first, second], std::option::Option::None).await;
|
||||
let error = result.expect_err("inflation reward 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(), "2");
|
||||
assert_eq!(error.context()[2].key(), "actual_count");
|
||||
assert_eq!(error.context()[2].value(), "1");
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_inflation_reward_preserves_min_context_slot_rpc_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_reward.error_min_context.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let address = inflation_reward_fixture_pubkey("11111111111111111111111111111111");
|
||||
let config = crate::SolanaInflationRewardConfig::new(
|
||||
std::option::Option::Some(912),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(431_500_000),
|
||||
);
|
||||
let result = pool.get_inflation_reward(&crate::HttpRoleName::new("default"), &[address], std::option::Option::Some(&config)).await;
|
||||
let error = result.expect_err("inflation reward min-context RPC error must propagate");
|
||||
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_inflation_reward_preserves_epoch_rewards_period_active_rpc_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_inflation_reward.error_epoch_rewards_active.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let address = inflation_reward_fixture_pubkey("11111111111111111111111111111111");
|
||||
let result = pool.get_inflation_reward(&crate::HttpRoleName::new("default"), &[address], std::option::Option::None).await;
|
||||
let error = result.expect_err("active epoch-rewards RPC error must propagate");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user