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
87 changes: 10 additions & 77 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,102 +77,35 @@ jobs:
- name: Verify formatting and analyzer style
run: dotnet format --no-restore --verify-no-changes --severity info

- name: Require private integration endpoints
shell: bash
env:
SOLSHARP_RPC_URL: ${{ secrets.SOLSHARP_RPC_URL }}
SOLSHARP_WS_URL: ${{ secrets.SOLSHARP_WS_URL }}
SOLSHARP_DEVNET_RPC_URL: ${{ secrets.SOLSHARP_DEVNET_RPC_URL }}
run: |
for variable in SOLSHARP_RPC_URL SOLSHARP_WS_URL SOLSHARP_DEVNET_RPC_URL; do
value="${!variable:-}"
if [[ -z "${value}" ]]; then
echo "Required release integration secret ${variable} is not configured."
exit 1
fi

parsed_endpoint="$(python3 -c '
import sys
from urllib.parse import urlsplit

try:
endpoint = urlsplit(sys.argv[1])
_ = endpoint.port
except ValueError as error:
raise SystemExit(f"Invalid endpoint URI: {error}") from error

if not endpoint.scheme or not endpoint.hostname:
raise SystemExit("Endpoint URI must include a scheme and hostname.")

print(endpoint.scheme.lower(), endpoint.hostname.rstrip(".").lower())
' "${value}")" || exit 1
read -r endpoint_scheme endpoint_host <<< "${parsed_endpoint}"
case "${variable}:${endpoint_scheme}" in
SOLSHARP_RPC_URL:http|SOLSHARP_RPC_URL:https|SOLSHARP_DEVNET_RPC_URL:http|SOLSHARP_DEVNET_RPC_URL:https|SOLSHARP_WS_URL:ws|SOLSHARP_WS_URL:wss)
;;
*)
echo "Release integration secret ${variable} has an unsupported URI scheme."
exit 1
;;
esac
case "${endpoint_host}" in
api.mainnet-beta.solana.com|api.devnet.solana.com|api.testnet.solana.com)
echo "Release integration secret ${variable} must use a private endpoint, not a public fallback."
exit 1
;;
esac
done

# Unit tests plus live read/streaming integration form the blocking release gate. Unlike ordinary local
# runs, this pass is strict: transport failures and every inconclusive/skipped result fail the job.
- name: Test (strict suite and live reads)
# Release validation must not depend on external RPC providers or devnet faucets.
- name: Test (offline)
run: >-
dotnet test
--no-build
--no-restore
--configuration Release
--filter "TestCategory!=DevnetWrite"
--filter "TestCategory!=Integration"
--logger "console;verbosity=normal"
--logger trx
--results-directory "${{ runner.temp }}/solsharp-strict-test-results"
env:
SOLSHARP_RPC_URL: ${{ secrets.SOLSHARP_RPC_URL }}
SOLSHARP_WS_URL: ${{ secrets.SOLSHARP_WS_URL }}
SOLSHARP_INTEGRATION_STRICT: 'true'
--results-directory "${{ runner.temp }}/solsharp-offline-test-results"

- name: Reject inconclusive or skipped strict tests
- name: Reject inconclusive or skipped offline tests
shell: bash
env:
STRICT_RESULTS_DIR: ${{ runner.temp }}/solsharp-strict-test-results
OFFLINE_RESULTS_DIR: ${{ runner.temp }}/solsharp-offline-test-results
run: |
mapfile -t result_files < <(find "${STRICT_RESULTS_DIR}" -type f -name '*.trx')
mapfile -t result_files < <(find "${OFFLINE_RESULTS_DIR}" -type f -name '*.trx')
if (( ${#result_files[@]} == 0 )); then
echo "No strict-suite TRX test results were produced."
echo "No offline-suite TRX test results were produced."
exit 1
fi

if grep --with-filename --extended-regexp \
'outcome="(NotExecuted|Skipped|Inconclusive|NotRunnable|Warning)"' "${result_files[@]}"; then
echo "Strict release tests contained an inconclusive or skipped result."
echo "Offline release tests contained an inconclusive or skipped result."
exit 1
fi

# requestAirdrop is deliberately not retried because it is not idempotent, and the shared devnet faucet
# can return 429/-32603 even through a healthy private RPC provider. Attempt the real write paths on every
# release, but let only classified faucet/transport failures become inconclusive; deterministic client,
# wire, assertion, or cluster-safety failures still fail this step.
- name: Probe devnet writes (faucet-limited)
run: >-
dotnet test tests/SolSharp.IntegrationTests/SolSharp.IntegrationTests.csproj
--no-build
--configuration Release
--filter "TestCategory=DevnetWrite"
--logger "console;verbosity=normal"
--logger trx
--results-directory "${{ runner.temp }}/solsharp-devnet-write-results"
env:
SOLSHARP_DEVNET_RPC_URL: ${{ secrets.SOLSHARP_DEVNET_RPC_URL }}
SOLSHARP_INTEGRATION_STRICT: 'false'

# No --no-build: packing rebuilds so the facade's bundling target (which folds the four
# assemblies into the single SolSharp package) runs against fully resolved references.
- name: Pack
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ against immutable Anza Solana SDK, Agave, and SPL source revisions; exact pins,
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
assemblies are Native AOT compatible; the package also contains a minimal facade. The live integration suite exercises read, streaming,
and devnet write paths against real nodes.
and devnet write paths against real nodes when run manually; CI and release test gates run offline only.

## Commands

Run from the repo root (where `SolSharp.sln` lives):

- `dotnet build` — Roslyn and StyleCop code style is enforced on build (`EnforceCodeStyleInBuild`), so actionable style violations surface as warnings. Repository-specific StyleCop severities live in `.editorconfig`; the member-order precedence is explicit in `stylecop.json`.
- `dotnet test` — NUnit suite. Add `--filter "TestCategory!=Integration"` for a fast offline run.
- `dotnet test --filter "TestCategory!=Integration"` — offline NUnit suite used by CI and release. An unfiltered `dotnet test` also runs the optional live integration tests.
- `dotnet format` — applies supported style fixes. It cannot reorder members (SA1201/SA1202/SA1203/SA1204/SA1214) or fix naming (IDE1006); move or rename those by hand.

**Before pushing, run the core developer gates with the same flags CI uses.** `.githooks/pre-push` runs
Expand Down Expand Up @@ -136,7 +136,7 @@ SolSharp/
- `IDE1006` is disabled for `tests/**` so `Method_Scenario_Expectation` names are allowed.
- For constructor-throws-only tests use an explicit discard: `Action act = () => _ = new T(...);`.
- **Arrange / Act / Assert comments.** Mark the three phases with `// Arrange`, `// Act`, `// Assert`. When the call under test and its check are a single fluent statement (exception delegates, `(await …).Should()…`), use one `// Act & Assert`. Skip the labels on expression-bodied or single-statement `[TestCase]` tests where there is nothing to separate — never restructure a test body just to fit them.
- **Integration tests** live in `SolSharp.IntegrationTests`, hit a real cluster, and run as part of `dotnet test`. They are tagged `[Category("Integration")]`; read/streaming tests default to public mainnet (`SOLSHARP_RPC_URL` / `SOLSHARP_WS_URL` override), and the write suite (airdrop, transfer, durable nonce) always targets devnet (`SOLSHARP_DEVNET_RPC_URL` override) — never mainnet. HTTP read and write harnesses use shared two-request-per-second token buckets; WebSocket probes are serialized and their starts are paced at 500 ms. Write fixtures additionally carry `[Category("DevnetWrite")]` and are non-parallel. No key is ever committed. Ordinary runs report transient endpoint/faucet failures as inconclusive. The release gate is strict for unit/read/streaming tests, while it attempts the faucet-dependent write probe separately so a shared-faucet 429 cannot block publication; deterministic write-path failures still fail. Skip all live tests for a fast offline run with `dotnet test --filter "TestCategory!=Integration"`.
- **Integration tests** live in `SolSharp.IntegrationTests` and remain available for explicit manual runs. CI and release exclude all `[Category("Integration")]` tests with `TestCategory!=Integration`; deterministic fixtures in the same project still run, and the release gate rejects skipped or inconclusive offline results. An unfiltered `dotnet test` also includes live tests. Read/streaming tests default to public mainnet (`SOLSHARP_RPC_URL` / `SOLSHARP_WS_URL` override), and the write suite (airdrop, transfer, durable nonce) always targets devnet (`SOLSHARP_DEVNET_RPC_URL` override) — never mainnet. HTTP read and write harnesses use shared two-request-per-second token buckets; WebSocket probes are serialized and their starts are paced at 500 ms. Write fixtures additionally carry `[Category("DevnetWrite")]` and are non-parallel. No key is ever committed. Ordinary manual runs report transient endpoint/faucet failures as inconclusive; `SOLSHARP_INTEGRATION_STRICT=1` makes those failures and missing V1 discovery data fail a manual run. Deterministic client failures always fail. Release validation does not require private endpoint secrets or run a devnet write probe.

## Security (money-critical)

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ version (on the earlier 0.x releases, minor versions could carry them).

## [Unreleased]

### Changed

- Release validation now runs offline tests only, matching CI, and no longer requires private RPC/WebSocket
endpoint secrets or runs a devnet write probe. Live HTTP, WebSocket, and devnet tests remain available for
explicit manual runs, including optional strict mode. Offline test validation, dependency/security checks,
package/API validation, packed Native AOT smoke checks, and release provenance remain in place.

## [4.0.0] - 2026-09-20

### Breaking changes
Expand Down
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ dotnet test --no-build --configuration Release --filter "TestCategory!=Integrati
dotnet format --no-restore --verify-no-changes --severity info
```

The offline test command excludes live mainnet/devnet probes. See [README.md](README.md#build--test) for
the integration-test endpoints and filters. The benchmark project is intentionally outside the solution;
changes to performance-sensitive code should also build and format it explicitly.
CI and release use the offline test selection above, including deterministic fixtures in the integration
test project. Release validation rejects skipped or inconclusive offline results. Live HTTP, WebSocket,
and devnet write tests remain available for explicit manual runs; they are excluded from both pipelines,
and release validation does not require private endpoint secrets. See [README.md](README.md#build--test)
for manual endpoints, filters, and optional `SOLSHARP_INTEGRATION_STRICT=1` mode. The benchmark project is
intentionally outside the solution; changes to performance-sensitive code should also build and format it
explicitly.

## Change requirements

Expand Down
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,36 +354,48 @@ Contributions are welcome; read the [contribution guide](CONTRIBUTING.md) before

```bash
dotnet build
dotnet test
dotnet test --filter "TestCategory!=Integration"
dotnet format # apply the enforced code style
```

CI and release run offline tests only, including the deterministic fixtures in `SolSharp.IntegrationTests`.
Release validation rejects skipped or inconclusive offline results. Live HTTP, WebSocket, and devnet
checks remain available for explicit manual runs; release validation does not require private endpoint secrets.
An unfiltered `dotnet test` still includes the live tests.

The suite includes a `SolSharp.IntegrationTests` project that exercises the read and streaming paths against a
live cluster, plus a write suite (airdrop, transfer, durable nonce) that always targets **devnet**. Reads
default to the public mainnet endpoint (`SOLSHARP_RPC_URL` / `SOLSHARP_WS_URL` override); the write suite uses
the public devnet endpoint (`SOLSHARP_DEVNET_RPC_URL` override); no credentials are committed. These tests hit
the network, so they tolerate rate limits by reporting inconclusive rather than failing, and are tagged
the network, so ordinary manual runs report transient endpoint/faucet failures as inconclusive, and are tagged
`Integration`. Live HTTP reads and devnet writes use two-request-per-second test-only limiters; WebSocket
probes run serially with starts spaced by 500 ms. The shared faucet can still reject `requestAirdrop`
independently of RPC traffic. For a fast, offline-only run, exclude them:

```bash
dotnet test --filter "TestCategory!=Integration"
```
independently of RPC traffic. Deterministic client failures always fail the test.

Micro-benchmarks live in a standalone BenchmarkDotNet harness:

```bash
dotnet run -c Release --project benchmarks/SolSharp.Benchmarks
```

To point the integration tests at your own node, set the endpoints (the key stays in your shell, never the repo):
To run the live read and streaming tests manually against your own node, set the endpoints (the key stays
in your shell, never the repo):

```bash
SOLSHARP_RPC_URL=https://your-node SOLSHARP_WS_URL=wss://your-node \
dotnet test --filter "TestCategory=Integration"
dotnet test --filter "TestCategory=Integration&TestCategory!=DevnetWrite"
```

Run the devnet write tests separately when needed:

```bash
SOLSHARP_DEVNET_RPC_URL=https://your-devnet-node \
dotnet test --filter "TestCategory=DevnetWrite"
```

Set `SOLSHARP_INTEGRATION_STRICT=1` for a manual run that fails on transient endpoint/faucet errors and
missing V1 discovery data instead of reporting those conditions as inconclusive.

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.
Expand Down
Loading