Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ body:
id: version
attributes:
label: SolSharp version
placeholder: 3.2.0
placeholder: 4.0.0
validations:
required: true
- type: dropdown
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A modern, contract-driven .NET SDK for Solana: keys and signing, program instruc
transaction wire formats, RPC, and WebSocket streaming. It is optimized for low latency,
bounded hostile-input handling, focused dependencies with a dependency-light Core, and Native AOT.

Status: 3.2.0. SolSharp is independently implemented
Status: 4.0.0. SolSharp is independently implemented
against immutable Anza Solana SDK, Agave, and SPL source revisions; exact pins, client-side
coverage, verification criteria, and deliberate node/runtime exclusions live in
`docs/RUST_PARITY.md`. All JSON used by the library is source-generated and all four functional
Expand Down Expand Up @@ -70,7 +70,7 @@ Three traps worth knowing:
**`global.json` pins one exact SDK with `rollForward: disable`; every CI/release job that restores or
builds the checkout installs it via `global-json-file: global.json`.** The pin is not stylistic: `IsAotCompatible` injects
`Microsoft.NET.ILLink.Tasks` and `PublishAot` injects `Microsoft.DotNet.ILCompiler`, both versioned by
the SDK's *runtime* (10.0.303 carries runtime 10.0.11), and both are recorded in every
the SDK's *runtime* (10.0.401 carries runtime 10.0.12), and both are recorded in every
`packages.lock.json` as direct references. A floating SDK therefore breaks
`dotnet restore --locked-mode` with NU1004 the moment .NET ships a patch, with no source change at all.
Every checkout job that restores or builds asserts `dotnet --version` equals the pinned value; do not
Expand Down Expand Up @@ -158,6 +158,7 @@ SolSharp/
Bouncy Castle-based Ed25519 signer/verification path. The pathological-encoding verifier divergence is
recorded in `docs/RUST_PARITY.md`. They are not transactions and examples must not imply on-chain authority.
- Transactions support **legacy**, **v0**, and feature-gated **SIMD-0385 V1** messages behind `ITransactionMessage`. Account ordering matches the pinned Solana SDK (fee payer first, then public-key byte order within writable-signer / readonly-signer / writable / readonly classes). v0 drains eligible accounts into lookup tables and prefixes `0x80`; V1 prefixes `0x81`, keeps all addresses inline, carries an inline execution config, and writes the message before its fixed number of signatures. V1's omitted compute/data limits mean zero and cluster activation is external, so examples must set deliberate limits and never imply universal availability. All formats use exact pinned Rust vectors.
- **Transaction/block reads and block subscriptions accept V1 by default.** Preserve explicit numeric version overrides and explicit `null` in full options. The latter omits the field; it does not mean unlimited version support. Signatures-only block responses are not version-filtered by Agave.
- `PublicKey.IsOnCurve` is field arithmetic over BouncyCastle's `X25519Field`, **not** BouncyCastle's public-key validation: BC's validation rejects non-canonical encodings (y ≥ p) that Solana's `curve25519-dalek` accepts after reducing mod p, so the check is written directly as `SqrtRatioVar((y²−1)/(dy²+1))` with the top bit masked and no canonicality test. Solders-derived edge KATs and a deterministic corpus checked against an independent BigInteger/Legendre-symbol oracle pin the network semantics. Using `X25519Field` rather than `BigInteger.ModPow` is what makes it ~39× faster; keep the reduction semantics if that code is ever touched.
- **SPL Token account state uses the fixed-size `Pack` layout, not Borsh.** `Mint` (82 bytes) and `TokenAccount` (165 bytes) read a `COption` as a 4-byte little-endian tag followed by an *always-present* value (the slot is reserved even when `None`) — unlike Borsh's 1-byte tag with the value present only when `Some`. So `BorshReader` / `BorshWriter` are for Anchor/Borsh data; the SPL decoders are hand-written against the Pack layout and KAT'd against `solders.token.state`. (The Token *instruction* data is different again: a minimal `COption` of a 1-byte tag plus the value only when `Some`.)
- Money-critical encodings (message/transaction serialization, instruction data, PDA/ATA, on-curve) are checked byte-for-byte against `solana-sdk` (solders) and `solana-py`, not just round-trips.
Expand Down
42 changes: 41 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,45 @@ version (on the earlier 0.x releases, minor versions could carry them).

## [Unreleased]

## [4.0.0] - 2026-09-20

### Breaking changes

- Default HTTP transaction/block reads and WebSocket block subscriptions now advertise support through
transaction version 1. Existing callers can receive V1 transactions; use the explicit
`*WithMaxVersionAsync(..., 0)` methods to retain a legacy/v0 ceiling.
- Full transaction/block configuration objects now default `MaxSupportedTransactionVersion` to `1`
instead of `null`. Explicit `null` still omits the field. Base58/binary users must explicitly select
`0` or `null`; accepting V1 requires base64, JSON, or jsonParsed. Parsed V1 resource limits and total
priority fees remain available in `transactionConfig`.

See [Upgrading from 3.x](docs/USAGE.md#upgrading-from-3x) for migration examples and signatures-only behavior.

### Added

- Added `GetTransactionOptions.MinContextSlot` and `GetSignatureStatusesWithOptionsAsync` with
`GetSignatureStatusesOptions` for history lookup, commitment, and minimum-context-slot checks.

### Fixed

- Updated SourceLink and its `Microsoft.Build.Tasks.Git` dependency to 10.0.401 to address
GHSA-23fw-v26w-5fgq, which caused the scheduled security dependency audit to fail. Solution,
benchmark, and packed Native AOT dependency locks are synchronized with the updated references.

### Changed

- Updated Microsoft.Extensions runtime dependencies, test tooling, BenchmarkDotNet, and banned-API
analyzers to their latest stable releases. FluentAssertions remains on the latest 7.x release (7.2.2)
with its existing license, and the existing StyleCop prerelease remains unchanged.
- Pinned repository builds to .NET SDK 10.0.401 and updated the Native AOT toolchain packages to 10.0.12.

### Tests

- Added default V1 HTTP/WebSocket regressions, explicit v0 and omitted-version checks, transaction
freshness request/error coverage, and packed Native AOT checks for the new request configuration.
- Added a bounded, read-only live V1 test that compares raw and parsed responses and verifies the
transaction's wire bytes, signatures, and inline configuration.

## [3.2.0] - 2026-08-29

### Fixed
Expand Down Expand Up @@ -665,7 +704,8 @@ bundles four layered assemblies.
transaction building, signing and serialization, `Transaction.Deserialize`, and instruction
decompilation — every wire format validated byte-for-byte against the Rust `solana-sdk`.

[Unreleased]: https://github.com/jecacs/SolSharp/compare/v3.2.0...HEAD
[Unreleased]: https://github.com/jecacs/SolSharp/compare/v4.0.0...HEAD
[4.0.0]: https://github.com/jecacs/SolSharp/compare/v3.2.0...v4.0.0
[3.2.0]: https://github.com/jecacs/SolSharp/compare/v3.1.0...v3.2.0
[3.1.0]: https://github.com/jecacs/SolSharp/compare/v3.0.0...v3.1.0
[3.0.0]: https://github.com/jecacs/SolSharp/compare/v2.0.0...v3.0.0
Expand Down
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</PropertyGroup>

<PropertyGroup Label="Package metadata">
<Version>3.2.0</Version>
<Version>4.0.0</Version>
<Authors>Yevhen Koval</Authors>
<Product>SolSharp</Product>
<Title>SolSharp — Solana SDK for .NET</Title>
Expand Down Expand Up @@ -51,7 +51,7 @@
</ItemGroup>

<ItemGroup Condition="'$(IsTestProject)' != 'true'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.301" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.401" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
Expand All @@ -66,7 +66,7 @@
review. Analyzer-only, so it never becomes a dependency of the shipped package. -->
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
50 changes: 33 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ bounded codecs, and typed network responses. If you are
writing wallets, bots, indexers, or backend services that talk to Solana from .NET and care
about correctness, speed, and control, this is aimed at you.

> **Status: 3.2.0.** SolSharp ships as a single NuGet package — `SolSharp` —
> **Status: 4.0.0.** SolSharp ships as a single NuGet package — `SolSharp` —
> bundling the Core (primitives + encodings), Wallet (Ed25519 and BLS12-381 keys, signing, verification,
> key import/export, BIP-39/SLIP-0010 derivation, and signed off-chain messages), Rpc (the full applicable
> non-admin JSON-RPC HTTP surface + WebSocket streaming + DI), and
Expand All @@ -38,6 +38,12 @@ about correctness, speed, and control, this is aimed at you.
SPL token state, building/signing/sending transactions, v0 + address lookup tables, SIMD-0385 V1, decoding transactions,
WebSocket subscriptions, confirmation, Native AOT publishing, and more.

**Upgrading from 3.x:** transaction reads and block subscriptions now advertise V1 support by default,
and full options default `MaxSupportedTransactionVersion` to `1`. Keep a legacy/v0 ceiling with an
explicit `0`; existing base58/binary configurations need an explicit `0` or `null`, or a V1-compatible
encoding. See the [4.0 migration guide](docs/USAGE.md#upgrading-from-3x) for exact examples and the
difference between a version ceiling and filtering transactions.

## Motivation

When this was started, the .NET options for Solana were either unmaintained and stale or
Expand Down Expand Up @@ -79,17 +85,17 @@ valuable ecosystem-oriented program surface. SolSharp is independently written w
application-side parity with immutable official Rust contracts, plus a verifiable .NET deployment story.
The official Rust column below is the reference contract rather than another client implementation.

Comparison basis: SolSharp is release `3.2.0`; Solnet means its
Comparison basis: SolSharp is release `4.0.0`; Solnet means its
[published `8.7.0` release](https://github.com/bmresearch/Solnet/commit/e8df87bdb2006376ba3eea9e1d3b857c84fc5685)
(2025-11-26), with unreleased-head differences called out explicitly; the Rust reference is the
[pinned Anza SDK/Agave/SPL matrix](docs/RUST_PARITY.md).

| Dimension | Official Rust SDK / Agave reference | SolSharp 3.2.0 | Solnet official packages/source |
| Dimension | Official Rust SDK / Agave reference | SolSharp 4.0.0 | Solnet official packages/source |
| --- | --- | --- | --- |
| **Transaction formats** | Legacy, V0, and feature-gated [SIMD-0385 V1](https://github.com/anza-xyz/solana-sdk/blob/ec7a0467e268774b724d55120ad952b518f27d64/message/src/versions/v1/message.rs), including inline V1 configuration and a message-first signature envelope | Legacy/V0/V1 build, sanitize, parse, sign, serialize, and decompile; exact V1 config/framing and envelope vectors | Published 8.7: Legacy/V0 and [rejects versions above 0](https://github.com/bmresearch/Solnet/blob/e8df87bdb2006376ba3eea9e1d3b857c84fc5685/src/Solnet.Rpc/Models/Message.cs#L275-L286). Unreleased head names V1, but its current body/envelope is not the pinned SIMD-0385 layout (details below) |
| **Native and SPL clients** | Canonical native-program and SPL interface crates, split by contract | System, Stake, Vote, legacy/upgradeable/V4 loaders, Compute Budget, ALT, Memo, three signature precompiles; Token, Token-2022 extensions/interfaces, ATA, metadata/group/transfer-hook, and ElGamal proof/registry client contracts with typed decoders | Broader ecosystem-oriented set including Governance, Stake Pool, Token Swap, Account Compression, Name Service, and Shared Memory; repository head adds an initial Token-2022 surface |
| **HTTP RPC** | [53 applicable non-admin, non-obsolete request variants](https://github.com/anza-xyz/agave/blob/ab6553293094e59dee7d3e7c928c7fa1023d0684/rpc-client-types/src/request.rs#L12-L75) in the pinned Agave client enum | 53/53 typed async methods, including current context-slot, filter, slice, encoding/detail/reward, raw/parsed V1, and context-wrapped response variants; batching, bounded responses, typed errors, and send/simulate/confirm | 50/53 pinned methods through sync/async `RequestResult<T>` APIs; no `getAgGenesisCert`, `getRecentPrioritizationFees`, or `getStakeMinimumDelegation` at the examined head |
| **PubSub** | Nine families: account, program, logs, signature, slot, slots-updates, block, vote, and root | 9/9, including exact logs/block filter unions, parsed account/program forms, early signature-receipt events, bounded channels, cancellation isolation, reconnect/replay, and V1 block opt-ins | [Six families](https://github.com/bmresearch/Solnet/blob/ebec9e1a3b708dbe86d103dd8fcf869d0cd923b6/src/Solnet.Rpc/IStreamingRpcClient.cs): account, program, logs, signature, slot, and root |
| **PubSub** | Nine families: account, program, logs, signature, slot, slots-updates, block, vote, and root | 9/9, including exact logs/block filter unions, parsed account/program forms, early signature-receipt events, bounded channels, cancellation isolation, reconnect/replay, and V1 block support by default | [Six families](https://github.com/bmresearch/Solnet/blob/ebec9e1a3b708dbe86d103dd8fcf869d0cd923b6/src/Solnet.Rpc/IStreamingRpcClient.cs): account, program, logs, signature, slot, and root |
| **Offline / multisig signing** | Signer, presigner/null-signer, partial signing, fixed signature slots, and per-slot verification primitives | Exact message-byte export/hash, typed fixed slots, partial/all signing, verified external insertion, `Presigner`, `NullSigner`, and SPL multisig builders | Partial signing, externally supplied signatures, and program multisig builders/examples |
| **AOT / trimming** | Native Rust output; not a .NET compatibility contract | Every assembly declares `IsAotCompatible`; generated JSON metadata, trim/AOT analyzers, and CI that publishes and runs a native package consumer | Targets .NET 8, but the examined projects publish no solution-wide AOT/trimming declaration or native-publish CI contract; reflection paths remain |
| **Packaging** | Modular Cargo crates | One NuGet package containing four compiler-layered functional assemblies plus a minimal packaging facade | Five installable packages: `Solana.Rpc`, `Solana.Wallet`, `Solana.Programs`, `Solana.Extensions`, and `Solana.KeyStore` |
Expand Down Expand Up @@ -119,7 +125,7 @@ dotnet add package SolSharp
```

```xml
<PackageReference Include="SolSharp" Version="3.2.0" />
<PackageReference Include="SolSharp" Version="4.0.0" />
```

| Assembly | Purpose |
Expand All @@ -140,10 +146,10 @@ After downloading all three assets, verify them before using the package outside
flow (replace the version in the filenames):

```bash
sha256sum --check SolSharp.3.2.0.nupkg.sha256
gh attestation verify SolSharp.3.2.0.nupkg \
sha256sum --check SolSharp.4.0.0.nupkg.sha256
gh attestation verify SolSharp.4.0.0.nupkg \
--repo jecacs/SolSharp \
--bundle SolSharp.3.2.0.nupkg.sigstore.json \
--bundle SolSharp.4.0.0.nupkg.sigstore.json \
--signer-workflow jecacs/SolSharp/.github/workflows/release.yml \
--deny-self-hosted-runners
```
Expand Down Expand Up @@ -205,15 +211,16 @@ bool ok = PublicKey.TryParse(input, out var key);
metadata — transaction version/index, pre/post SOL and token balances, inner (CPI) instructions, loaded
lookup-table addresses, logs, compute/cost units, program return data, and rewards. Failures decode to a
typed `TransactionError` (including parameterized runtime errors and the program's `Custom` code) on
`TransactionMeta`, `SignatureStatus`, and `SimulateTransactionResult`. The compatibility-preserving default
read advertises legacy/v0; `GetTransactionWithMaxVersionAsync(..., 1)` opts into V1 bytes, which
`Transaction.Deserialize` understands locally.
`TransactionMeta`, `SignatureStatus`, and `SimulateTransactionResult`. Default transaction and block reads
advertise support through V1; `Transaction.Deserialize` understands legacy/v0/V1 locally. Explicit
`*WithMaxVersionAsync(..., 0)` calls retain a v0 ceiling. `GetTransactionWithOptionsAsync` exposes
`MinContextSlot`, and `GetSignatureStatusesWithOptionsAsync` adds commitment and minimum-context-slot
checks when supported by the RPC node.
- `GetParsedTransactionAsync` / `GetParsedBlockAsync` / `GetParsedAccountInfoAsync` return the node's
`jsonParsed` decoding — typed instructions, token balances, account state, and logs without local Borsh
work. Recognized instructions carry the node's parsed action; unrecognized instructions retain their raw
program id, account list, and base58 data, matching the upstream tagged response union. Explicit
`*WithMaxVersionAsync` variants opt parsed transaction/block reads into V1 and preserve its inline
`transactionConfig`.
program id, account list, and base58 data, matching the upstream tagged response union. Parsed
transaction/block reads support V1 by default and preserve its inline `transactionConfig`.
- WebSocket streaming multiplexed over one connection: `SubscribeSlotsAsync`, `SubscribeRootsAsync`,
`SubscribeSlotsUpdatesAsync` (slot lifecycle with per-stage stats), and `SubscribeVotesAsync` (gossip
votes) as `IAsyncEnumerable`; `SubscribeLogsAsync`, `SubscribeAccountAsync`, `SubscribeParsedAccountAsync`,
Expand All @@ -222,8 +229,8 @@ bool ok = PublicKey.TryParse(input, out var key);
transport (message-size cap, per-subscription buffers, opt-in receive timeout). The source-safe
`SubscribeAccountWithOptionsAsync` and `SubscribeProgramWithOptionsAsync` paths expose the effective
encoding/commitment fields (plus program filters) and return the same exact `RpcAccountData` union as HTTP.
Agave-accepted subscription fields that its encoder ignores are deliberately not advertised. Block
subscriptions also provide explicit `*WithMaxVersionAsync` V1 opt-ins; full methods cover logs/block filter
Fields ignored by the pinned base Agave subscription encoder are deliberately not advertised. Block
subscriptions support V1 by default and retain explicit `*WithMaxVersionAsync` overrides; full methods cover logs/block filter
unions, parsed program streams, and the optional early `receivedSignature` event before final processing.
- DI registration with a built-in resilience pipeline (retry on transient errors and HTTP 429), plus
`AddSolanaWs` for a container-managed streaming client.
Expand Down Expand Up @@ -335,7 +342,7 @@ var signature = await rpc.SendTransactionAsync(tx.Serialize());

## Requirements

- .NET 10 SDK for consumers. Repository builds use the exact SDK 10.0.303 pinned by `global.json` with
- .NET 10 SDK for consumers. Repository builds use the exact SDK 10.0.401 pinned by `global.json` with
`rollForward: disable`; CI jobs that restore or build the checkout assert that exact selection.
- Calling the BLS12-381 API requires one of the native RIDs shipped by `Nethermind.Crypto.Bls` 1.1.0:
`linux-x64`, `linux-arm64`, `osx-x64`, `osx-arm64`, or `win-x64`. All non-BLS SolSharp APIs remain
Expand Down Expand Up @@ -377,6 +384,15 @@ SOLSHARP_RPC_URL=https://your-node SOLSHARP_WS_URL=wss://your-node \
dotnet test --filter "TestCategory=Integration"
```

The read-only V1 regression test finds a V1 transaction in at most three recent finalized blocks, then
checks default raw/parsed reads, wire round-tripping, signatures, and execution configuration. Set
`SOLSHARP_V1_TRANSACTION_SIGNATURE` to use a known V1 transaction on the configured cluster instead.
Missing V1 data is inconclusive normally and fails with `SOLSHARP_INTEGRATION_STRICT=1`:

```bash
dotnet test --filter "FullyQualifiedName~DefaultReads_DecodeAndVerifyALiveV1Transaction"
```

## Layout

```
Expand Down
Loading
Loading