Skip to content

Commit 21c85f4

Browse files
committed
Release v1.1.0: Complete the WebSocket subscription surface with SubscribeSlotsUpdatesAsync (slot lifecycle with per-stage transaction stats) and SubscribeVotesAsync (gossip votes; requires --rpc-pubsub-enable-vote-subscription on the node), both unstable per Solana, with notification models, tests, and documentation across USAGE, README, changelog, and package metadata.
1 parent 73a6375 commit 21c85f4

11 files changed

Lines changed: 285 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to SolSharp are documented here. The format is loosely based
55
[semantic versioning](https://semver.org) — from 1.0.0 breaking changes only come with a major
66
version (on the earlier 0.x releases, minor versions could carry them).
77

8+
## [1.1.0]
9+
10+
### Added
11+
12+
- Two new WebSocket streams completing the subscription surface: `SubscribeSlotsUpdatesAsync`
13+
(`slotsUpdatesSubscribe` — every stage of the slot lifecycle: shreds received, bank created, frozen
14+
with per-slot transaction stats, optimistic confirmation, root, dead) and `SubscribeVotesAsync`
15+
(`voteSubscribe` — gossip votes before they land in a block; requires a node started with
16+
`--rpc-pubsub-enable-vote-subscription`). Both are parameterless `IAsyncEnumerable` streams like
17+
`SubscribeSlotsAsync`, and both are marked unstable by Solana — the wire shape can change between
18+
node versions.
19+
- New notification models: `SlotsUpdate` (+ `SlotsUpdateStats`) and `VoteNotification`.
20+
821
## [1.0.1]
922

1023
Documentation-only release; no code changes.

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A lean, modern .NET SDK for Solana: RPC + WebSocket streaming, wire-level transa
44
signing/building. Optimised for low latency and a small dependency footprint — it is a
55
deliberate, focused alternative to the heavier general-purpose SDKs, not a clone of them.
66

7-
Status: 1.0.1, first stable release line (semver compatibility promise now applies to the public API). All four projects are in place: Core primitives (incl. a Borsh reader/writer), the Rpc client (reads + typed account state via `Mint`/`TokenAccount`/`NonceAccount` + Token-2022 extension decoding (`TokenExtensionSet`), `jsonParsed` transaction/block/account reads, the full current JSON-RPC HTTP read surface (deprecated getStakeActivation excluded), send/simulate, JSON-RPC batching via `RpcBatch`, typed transaction errors, multiplexed WebSocket streaming with auto-reconnect, DI + resilience), the Wallet (Ed25519 keys, signing, verification, key parsing, BIP-39/SLIP-0010 mnemonic derivation), and Programs (System/Token/ATA/Compute Budget/Memo + the Address Lookup Table program, PDA/ATA, legacy + v0 transaction building/signing/parsing, durable-nonce builder support, and instruction decompilation). As of 0.7.0 all JSON is source-generated (no reflection) and every assembly is Native AOT compatible (`IsAotCompatible`), with an AOT smoke sample published and run in CI. A separate live integration suite exercises the read and streaming paths against a real cluster.
7+
Status: 1.1.0, stable release line (semver compatibility promise now applies to the public API). All four projects are in place: Core primitives (incl. a Borsh reader/writer), the Rpc client (reads + typed account state via `Mint`/`TokenAccount`/`NonceAccount` + Token-2022 extension decoding (`TokenExtensionSet`), `jsonParsed` transaction/block/account reads, the full current JSON-RPC HTTP read surface (deprecated getStakeActivation excluded), send/simulate, JSON-RPC batching via `RpcBatch`, typed transaction errors, full WebSocket subscription surface (incl. unstable voteSubscribe/slotsUpdatesSubscribe) with auto-reconnect, DI + resilience), the Wallet (Ed25519 keys, signing, verification, key parsing, BIP-39/SLIP-0010 mnemonic derivation), and Programs (System/Token/ATA/Compute Budget/Memo + the Address Lookup Table program, PDA/ATA, legacy + v0 transaction building/signing/parsing, durable-nonce builder support, and instruction decompilation). As of 0.7.0 all JSON is source-generated (no reflection) and every assembly is Native AOT compatible (`IsAotCompatible`), with an AOT smoke sample published and run in CI. A separate live integration suite exercises the read and streaming paths against a real cluster.
88

99
## Commands
1010

‎Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</PropertyGroup>
1919

2020
<PropertyGroup Label="Package metadata">
21-
<Version>1.0.1</Version>
21+
<Version>1.1.0</Version>
2222
<Authors>Yevhen Koval</Authors>
2323
<Product>SolSharp</Product>
2424
<Title>SolSharp — Solana SDK for .NET</Title>

‎README.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,11 @@ bool ok = PublicKey.TryParse(input, out var key);
145145
- `GetParsedTransactionAsync` / `GetParsedBlockAsync` / `GetParsedAccountInfoAsync` return the node's
146146
`jsonParsed` decoding — typed instructions, token balances, account state, and logs without local Borsh
147147
work — each instruction keeping both its parsed form and its raw program id / accounts / data.
148-
- WebSocket streaming multiplexed over one connection: `SubscribeSlotsAsync` and `SubscribeRootsAsync`
149-
(`IAsyncEnumerable`), `SubscribeLogsAsync`, `SubscribeAccountAsync`, `SubscribeParsedAccountAsync`, `SubscribeProgramAsync`,
150-
`SubscribeSignatureAsync`, `SubscribeBlocksAsync`, and `SubscribeParsedBlocksAsync` (`ChannelReader`), with
151-
automatic reconnect and resubscribe across dropped connections.
148+
- WebSocket streaming multiplexed over one connection: `SubscribeSlotsAsync`, `SubscribeRootsAsync`,
149+
`SubscribeSlotsUpdatesAsync` (slot lifecycle with per-stage stats), and `SubscribeVotesAsync` (gossip
150+
votes) as `IAsyncEnumerable`; `SubscribeLogsAsync`, `SubscribeAccountAsync`, `SubscribeParsedAccountAsync`,
151+
`SubscribeProgramAsync`, `SubscribeSignatureAsync`, `SubscribeBlocksAsync`, and `SubscribeParsedBlocksAsync`
152+
(`ChannelReader`) — with automatic reconnect and resubscribe across dropped connections.
152153
- DI registration with a built-in resilience pipeline (retry on transient errors and HTTP 429), plus
153154
`AddSolanaWs` for a container-managed streaming client.
154155
- JSON-RPC batching — `CreateBatch()` queues reads (and sends) and submits them in one HTTP round-trip.

‎docs/USAGE.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,21 @@ Also available: `SubscribeRootsAsync` (rooted slots, like `SubscribeSlotsAsync`)
697697
streams `SubscribeParsedBlocksAsync` / `SubscribeParsedAccountAsync`. Cancel any channel subscription by
698698
cancelling the `CancellationToken` you pass in.
699699

700+
Two more streams cover the slot lifecycle in depth — both are marked *unstable* by Solana, so their
701+
wire shape can change between node versions:
702+
703+
```csharp
704+
// Every stage a slot moves through: firstShredReceived, createdBank, frozen (with
705+
// transaction stats), optimisticConfirmation, root, dead. Richer than SubscribeSlotsAsync:
706+
await foreach (var update in ws.SubscribeSlotsUpdatesAsync())
707+
Console.WriteLine($"{update.Slot} {update.Type} fails={update.Stats?.NumFailedTransactions}");
708+
709+
// Votes as they arrive in gossip, before they land in a block. The node must run with
710+
// --rpc-pubsub-enable-vote-subscription, otherwise the subscribe is rejected:
711+
await foreach (var vote in ws.SubscribeVotesAsync())
712+
Console.WriteLine($"{vote.VotePubkey} voted on {vote.Slots[^1]}");
713+
```
714+
700715
The reconnect policy is tunable through `SolanaWsClientOptions`: `AutoReconnect` (on by default), the
701716
`ReconnectInitialDelay` → `ReconnectMaxDelay` exponential backoff, and `MaxReconnectAttempts` (`0` retries
702717
forever). When the attempts are exhausted — or auto-reconnect is off — every subscription completes with the

‎src/SolSharp.Rpc/Protocol/SolanaJsonContext.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ namespace SolSharp.Rpc.Protocol;
9090
[JsonSerializable(typeof(RpcResponse<PublicKey>))]
9191
// WebSocket notification payloads (SubscriptionSink roots).
9292
[JsonSerializable(typeof(SlotInfo))]
93+
[JsonSerializable(typeof(VoteNotification))]
94+
[JsonSerializable(typeof(SlotsUpdate))]
9395
[JsonSerializable(typeof(RpcContextValue<LogInfo>))]
9496
[JsonSerializable(typeof(RpcContextValue<AccountInfo>))]
9597
[JsonSerializable(typeof(RpcContextValue<ParsedAccountInfo>))]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace SolSharp.Rpc.Streaming;
4+
5+
/// <summary>
6+
/// A slot-lifecycle notification payload from <c>slotsUpdatesSubscribe</c> - one update per stage a
7+
/// slot moves through (see <see cref="Type"/>).
8+
/// </summary>
9+
/// <seealso href="https://solana.com/docs/rpc/websocket/slotsupdatessubscribe">slotsUpdatesSubscribe</seealso>
10+
public sealed record SlotsUpdate
11+
{
12+
/// <summary>The slot the update is about.</summary>
13+
[JsonPropertyName("slot")]
14+
public ulong Slot { get; init; }
15+
16+
/// <summary>
17+
/// The update type: <c>firstShredReceived</c>, <c>completed</c>, <c>createdBank</c>, <c>frozen</c>,
18+
/// <c>dead</c>, <c>optimisticConfirmation</c>, or <c>root</c>. Kept as a string so new node-side
19+
/// stages do not break deserialization.
20+
/// </summary>
21+
[JsonPropertyName("type")]
22+
public string Type { get; init; } = string.Empty;
23+
24+
/// <summary>The update's Unix timestamp in milliseconds.</summary>
25+
[JsonPropertyName("timestamp")]
26+
public long Timestamp { get; init; }
27+
28+
/// <summary>The parent slot; only present on <c>createdBank</c> updates.</summary>
29+
[JsonPropertyName("parent")]
30+
public ulong? Parent { get; init; }
31+
32+
/// <summary>Why the slot died; only present on <c>dead</c> updates.</summary>
33+
[JsonPropertyName("err")]
34+
public string? Error { get; init; }
35+
36+
/// <summary>Transaction counts for the slot; only present on <c>frozen</c> updates.</summary>
37+
[JsonPropertyName("stats")]
38+
public SlotsUpdateStats? Stats { get; init; }
39+
}
40+
41+
/// <summary>The per-slot transaction counts attached to a <c>frozen</c> <see cref="SlotsUpdate"/>.</summary>
42+
public sealed record SlotsUpdateStats
43+
{
44+
/// <summary>The number of transaction entries in the slot.</summary>
45+
[JsonPropertyName("numTransactionEntries")]
46+
public ulong NumTransactionEntries { get; init; }
47+
48+
/// <summary>The number of successful transactions in the slot.</summary>
49+
[JsonPropertyName("numSuccessfulTransactions")]
50+
public ulong NumSuccessfulTransactions { get; init; }
51+
52+
/// <summary>The number of failed transactions in the slot.</summary>
53+
[JsonPropertyName("numFailedTransactions")]
54+
public ulong NumFailedTransactions { get; init; }
55+
56+
/// <summary>The largest number of transactions in a single entry.</summary>
57+
[JsonPropertyName("maxTransactionsPerEntry")]
58+
public ulong MaxTransactionsPerEntry { get; init; }
59+
}

‎src/SolSharp.Rpc/Streaming/SolanaWsClient.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,32 @@ public IAsyncEnumerable<SlotInfo> SubscribeSlotsAsync(CancellationToken cancella
112112
public IAsyncEnumerable<ulong> SubscribeRootsAsync(CancellationToken cancellationToken = default)
113113
=> SubscribeAsync<ulong>("rootSubscribe", [], "rootUnsubscribe", cancellationToken);
114114

115+
/// <summary>
116+
/// Subscribes to new votes observed in gossip, before they land in a block. Ending the enumeration
117+
/// sends the matching unsubscribe. This subscription is marked unstable by Solana and is only
118+
/// available on nodes started with <c>--rpc-pubsub-enable-vote-subscription</c>; on other nodes the
119+
/// subscribe call is rejected.
120+
/// See <see href="https://solana.com/docs/rpc/websocket/votesubscribe">voteSubscribe</see>.
121+
/// </summary>
122+
/// <param name="cancellationToken">Stops the subscription when cancelled.</param>
123+
/// <returns>An async stream of vote notifications.</returns>
124+
/// <exception cref="InvalidOperationException">Surfaced during enumeration if the connection closes or the node rejects the subscription (for example, when vote subscriptions are not enabled).</exception>
125+
public IAsyncEnumerable<VoteNotification> SubscribeVotesAsync(CancellationToken cancellationToken = default)
126+
=> SubscribeAsync<VoteNotification>("voteSubscribe", [], "voteUnsubscribe", cancellationToken);
127+
128+
/// <summary>
129+
/// Subscribes to slot-lifecycle updates - one notification per stage a slot moves through
130+
/// (shreds received, bank created, frozen, optimistically confirmed, rooted, or dead), richer and
131+
/// more frequent than <see cref="SubscribeSlotsAsync"/>. Ending the enumeration sends the matching
132+
/// unsubscribe. This subscription is marked unstable by Solana.
133+
/// See <see href="https://solana.com/docs/rpc/websocket/slotsupdatessubscribe">slotsUpdatesSubscribe</see>.
134+
/// </summary>
135+
/// <param name="cancellationToken">Stops the subscription when cancelled.</param>
136+
/// <returns>An async stream of slot-lifecycle updates.</returns>
137+
/// <exception cref="InvalidOperationException">Surfaced during enumeration if the connection closes or the node rejects the subscription.</exception>
138+
public IAsyncEnumerable<SlotsUpdate> SubscribeSlotsUpdatesAsync(CancellationToken cancellationToken = default)
139+
=> SubscribeAsync<SlotsUpdate>("slotsUpdatesSubscribe", [], "slotsUpdatesUnsubscribe", cancellationToken);
140+
115141
/// <summary>
116142
/// Subscribes to transaction logs mentioning <paramref name="program"/>, delivered through a channel.
117143
/// Cancelling <paramref name="cancellationToken"/> unsubscribes and completes the channel.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Text.Json.Serialization;
2+
using SolSharp.Core.Primitives;
3+
4+
namespace SolSharp.Rpc.Streaming;
5+
6+
/// <summary>
7+
/// A new-vote notification payload from <c>voteSubscribe</c> - a vote observed in gossip before it
8+
/// lands in a block.
9+
/// </summary>
10+
/// <seealso href="https://solana.com/docs/rpc/websocket/votesubscribe">voteSubscribe</seealso>
11+
public sealed record VoteNotification
12+
{
13+
/// <summary>The identity of the voting validator.</summary>
14+
[JsonPropertyName("votePubkey")]
15+
public PublicKey VotePubkey { get; init; }
16+
17+
/// <summary>The slots the vote covers.</summary>
18+
[JsonPropertyName("slots")]
19+
public IReadOnlyList<ulong> Slots { get; init; } = [];
20+
21+
/// <summary>The hash the vote is for (base58).</summary>
22+
[JsonPropertyName("hash")]
23+
public string Hash { get; init; } = string.Empty;
24+
25+
/// <summary>The vote's Unix timestamp in seconds, when the validator attached one.</summary>
26+
[JsonPropertyName("timestamp")]
27+
public long? Timestamp { get; init; }
28+
29+
/// <summary>The signature of the transaction carrying the vote (base58).</summary>
30+
[JsonPropertyName("signature")]
31+
public string Signature { get; init; } = string.Empty;
32+
}

‎src/SolSharp/SolSharp.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<PackageId>SolSharp</PackageId>
66
<IsPackable>true</IsPackable>
77
<Description>A lean, Native AOT-ready .NET 8 SDK for Solana — source-generated JSON, no reflection, trimmable, compiles to a native binary. The full JSON-RPC HTTP read surface, send/simulate, and multiplexed WebSocket streaming; Ed25519 keys and signing; SPL Token, PDA, and ATA helpers; and spec-accurate legacy and v0 (versioned) transaction building, signing, and decoding — every wire format checked byte-for-byte against the Rust solana-sdk. Ships as one package bundling the Core, Wallet, Rpc, and Programs assemblies.</Description>
8-
<PackageReleaseNotes>1.0.1 — documentation release, no code changes. Fixes the package README rendering on nuget.org (the page showed raw HTML from the GitHub README; the package now ships a dedicated markdown-only README) and expands the usage guide with a Native AOT publishing section plus examples for signature-history paging, block walking, data-driven priority fees, and the node/cluster basics. 1.0.0 recap — the first stable release: the full current Solana JSON-RPC HTTP read surface (19 methods added), all JSON source-generated (no reflection), every assembly Native AOT compatible, and the public API under the semver compatibility promise. Changelog: https://github.com/jecacs/SolSharp/blob/main/CHANGELOG.md</PackageReleaseNotes>
8+
<PackageReleaseNotes>1.1.0 — completes the WebSocket subscription surface with two new IAsyncEnumerable streams: SubscribeSlotsUpdatesAsync (slotsUpdatesSubscribe — the full slot lifecycle: shreds received, bank created, frozen with per-slot transaction stats, optimistic confirmation, root, dead) and SubscribeVotesAsync (voteSubscribe — gossip votes before they land in a block; the node must run with --rpc-pubsub-enable-vote-subscription). Both are marked unstable by Solana. New models: SlotsUpdate, SlotsUpdateStats, VoteNotification. No breaking changes. Changelog: https://github.com/jecacs/SolSharp/blob/main/CHANGELOG.md</PackageReleaseNotes>
99
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);BundleProjectReferences</TargetsForTfmSpecificBuildOutput>
1010
</PropertyGroup>
1111

0 commit comments

Comments
 (0)